Spring5(五)——AOP

Craftsman-L 2021-09-15 09:57:23
java spring 博客园 AOP spring5


一、AOP

1、介绍

AOP(Aspect Oriented Programming),面向切面编程。它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
AOP是对所有对象或者是一类对象编程,核心是在不增加代码的基础上,还增加新功能。实际上在开发框架本身用的多,在实际项目中,用的不多。

切面(aspect):要实现的交叉功能,是系统模块化的一个切面或领域。如日志记录。
连接点:应用程序执行过程中插入切面的地点,可以是方法调用,异常抛出,或者要修改的字段。
通知(增强):切面的实际实现,他通知系统新的行为。如在日志通知包含了实现日志功能的代码,如向日志文件写日志。通知在连接点插入到应用系统中。
切入点:定义了通知应该应用在哪些连接点,通知可以应用到AOP框架支持的任何连接点。连接点(静态)-->切入点(动态)。
引入:为类添加新方法和属性。
目标对象:被通知的对象(被代理的对象)。既可以是你编写的类也可以是第三方类。
代理对象:将通知应用到目标对象后创建的对象,应用系统的其他部分不用为了支持代理对象而改变。
织入:将切面应用到目标对象从而创建一个新代理对象的过程。织入发生在目标对象生命周期的多个点上:
①编译期:切面在目标对象编译时织入,这需要一个特殊的编译器。
②类装载期:切面在目标对象被载入JVM时织入,这需要一个特殊的类载入器。
③运行期:切面在应用系统运行时织入。

原理:AOP的核心思想为设计模式的动态代理模式。

2、案例

注意:接下来介绍的案例,更像是AOP的静态代理实现,个人理解。
定义目标对象(被代理的对象)

 1 // 定义一个接口
 2 public interface ITeacher {
 3 void teach();
 4 int add(int i, int j);
 5 }
 6
 7 // 定义目标对象
 8 public class Teacher implements ITeacher {
 9  @Override
10 public void teach() {
11 System.out.println("老师正在上课");
12  }
13
14  @Override
15 public int add(int i, int j) {
16 int add = i + j;
17 System.out.println("执行目标方法:老师正在做加法,结果为:" + add);
18 // int throwable = 10 / 0; 测试异常通知
19 return add;
20  }
21
22 // 目标对象自己的方法,此方法不是接口所以无法代理
23 public void sayHello() {
24 System.out.println("老师会说hello");
25  }
26
27 }

编写一个通知(前置通知)

 1 // 前置通知
 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
 3
 4 // method:被 代理对象 调用的方法(目标方法)
 5 // objects:被 代理对象 调用的方法入参(参数)
 6 // target:目标对象
 7  @Override
 8 public void before(Method method, Object[] objects, Object target) throws Throwable {
 9 System.out.println("前置通知=====1======函数名:" + method.getName());
10 System.out.println("前置通知=====2======参数值:" + JSON.toJSONString(objects));
11 System.out.println("前置通知=====3======对象值:" + JSON.toJSONString(target));
12  }
13
14 }

配置 application.xml

 1 <!-- 配置目标对象 -->
 2 <bean id="teacher" class="com.lx.spring.day1.Teacher"/>
 3
 4 <!-- 配置前置通知 -->
 5 <bean id="myMethodBeforeAdvice" class="com.lx.spring.day1.MyMethodBeforeAdvice"/>
 6
 7 <!-- 配置代理对象 -->
 8 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
 9 <!-- 1.指定要代理的目标对象 -->
10 <property name="target" ref="teacher"/>
11
12 <!-- 2.指定要代理的接口集 -->
13 <property name="proxyInterfaces">
14 <list>
15 <value>com.lx.spring.day1.ITeacher</value>
16 </list>
17 </property>
18
19 <!-- 3.指定要织入的通知 -->
20 <property name="interceptorNames">
21 <list>
22 <value>myMethodBeforeAdvice</value>
23 </list>
24 </property>
25 </bean>
 1 // 测试类
 2 public class Main {
 3 public static void main(String[] args) {
 4 ApplicationContext app = new ClassPathXmlApplicationContext("app1.xml");
 5 // 获取代理对象
 6 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean");
 7 // 通过代理对象执行目标对象的方法
 8 int add = iTeacher.add(1, 2);
 9  }
10 }
11
12 // 前置通知=====1======函数名:add
13 // 前置通知=====2======参数值:[1,2]
14 // 前置通知=====3======对象值:{}
15 // 执行目标方法:老师正在做加法,结果为:3

原理剖析:案例中,重点在于理解ProxyFactoryBean这个Bean到底做了什么?有一个前置通知,在目标方法前调用(从打印结果也能看出)。
那么,用静态代理模式不难理解,简单理解上面的实现如下,详细的请查看源码。

 1 // 代理对象,静态代理
 2 public class ProxyFactoryBean implements ITeacher { // 2.指定要代理的接口集,即要实现哪些接口
 3 // 目标对象,通过接口来聚合
 4 private Teacher target;
 5 // 前置通知
 6 private MethodBeforeAdvice methodBeforeAdvice;
 7
 8 // 3.指定要织入的通知,即在执行目标对象方法要执行什么代码
 9 public void setMethodBeforeAdvice(MethodBeforeAdvice methodBeforeAdvice) {
10 this.methodBeforeAdvice = methodBeforeAdvice;
11  }
12
13 public ProxyFactoryBean(Teacher teacher) { // 1.指定要代理的目标对象
14 this.target = teacher;
15  }
16
17  @Override
18 public void teach() {
19
20  }
21
22 // 代理方法的编写
23  @Override
24 public int add(int i, int j) {
25 try {
26 // 1.如果设置了前置通知,执行前置通知
27 if (methodBeforeAdvice != null) {
28 Method method = ITeacher.class.getMethod("add", int.class, int.class);
29 List<Object> objectList = new ArrayList<>();
30  objectList.add(i);
31  objectList.add(j);
32 Object[] objects = objectList.toArray();
33
34  methodBeforeAdvice.before(method, objects, target);
35  }
36
37 // 2.执行目标方法
38 return target.add(i, j);
39
40 // 3.如果设置了后置通知,执行后置通知
41 } catch (Throwable e) {
42  e.printStackTrace();
43  }
44 return 0;
45  }
46 }
 1 // 测试类
 2 public class Main {
 3 public static void main(String[] args) {
 4 // 创建代理对象,同时将创建的目标对象作为参数传递,即为谁代理.
 5 ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean(new Teacher());
 6 // 设置一个前置通知
 7 proxyFactoryBean.setMethodBeforeAdvice(new MyMethodBeforeAdvice());
 8 // 通过代理对象执行目标对象的方法.
 9 proxyFactoryBean.add(1, 4);
10  }
11 }
12
13 // 前置通知=====1======函数名:add
14 // 前置通知=====2======参数值:[1,3]
15 // 前置通知=====3======对象值:{}
16 // 执行目标方法:老师正在做加法,结果为:4

3、通知类别

除了案例中使用的前置通知,Spring中还提供了如下几种通知:

通知类型
接口
描述
前置通知
org.springframework.aop.MethodBeforeAdvice
在目标方法前调用
后置通知
org.springframework.aop.AfterReturningAdvice
在目标方法后调用
环绕通知
org.aopalliance.intercept.MethodInterceptor
拦截对目标方法调用
异常通知
org.springframework.aop.ThrowsAdvice
目标方法抛出异常时调用
引入通知
org.springframework.aop.support.NameMatchMethodPointcutAdvisor
可以指定切入点

前置通知:接口提供了获得目标方法,参数和目标对象的机会。该接口唯一能阻止目标方法被调用的途径是抛出异常或(System.exit())。
后置通知:同前置通知类似。
环绕通知:该通知能够控制目标方法是否真的被调用。通过invocation.proceed()方法来调用。该通知可以控制返回的对象。可以返回一个与proceed()方法返回对象完全不同的对象。但要谨慎使用。特别注意:配置通知时,指定要织入的通知,环绕顺序不同,会影响执行顺序。
异常通知:该接口为标识性(tag)接口,没有任何方法,但实现该接口的类必须要有如下形式的方法:

void afterThrowing(Throwable throwable);
void afterThrowing(Method m,Object[] os,Object target,Exception e);
第一个方法只接受一个参数:需要抛出的异常。
第二个方法接受异常、被调用的方法、参数以及目标对象

引入通知:以前定义的通知类型是在目标对象的方法被调用的周围织入。引入通知给目标对象添加新的方法和属性。

几种通知:

 1 // 前置通知(上面已介绍过)
 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
 3 // 目标方法、参数、目标对象
 4  @Override
 5 public void before(Method method, Object[] objects, Object target) throws Throwable {
 6 System.out.println("前置通知=====1======函数名:" + method.getName());
 7 System.out.println("前置通知=====2======参数值:" + JSON.toJSONString(objects));
 8 System.out.println("前置通知=====3======对象值:" + JSON.toJSONString(target));
 9  }
10 }
11
12 // 后置通知
13 public class MyAfterReturningAdvice implements AfterReturningAdvice {
14 // object:方法的返回值
15 // method:目标方法
16 // objects:参数
17 // target:目标对象.注:该对象由前置通知传入的 target
18  @Override
19 public void afterReturning(Object object, Method method, Object[] objects, Object target) throws Throwable {
20 System.out.println("后置通知=====0======返回值:" + object);
21 System.out.println("后置通知=====1======函数名:" + method.getName());
22 System.out.println("后置通知=====2======参数值:" + JSON.toJSONString(objects));
23 System.out.println("后置通知=====3======对象值:" + JSON.toJSONString(target));
24  }
25 }
26
27 // 环绕通知
28 public class MyMethodInterceptor implements MethodInterceptor {
29 // object:目标方法的返回值
30  @Override
31 public Object invoke(MethodInvocation invocation) throws Throwable {
32 System.out.println("============环绕前==============");
33 Object object = invocation.proceed(); // 这里会切入目标方法
34 System.out.println("============环绕后==============");
35 return object;
36  }
37 }
38
39 // 异常通知
40 public class MyThrowsAdvice implements ThrowsAdvice {
41
42 public void afterThrowing(Method method, Object[] objects, Object target, Exception e) {
43 System.out.println("异常通知=====1======函数名:" + method.getName());
44 System.out.println("异常通知=====2======参数值:" + JSON.toJSONString(objects));
45 System.out.println("异常通知=====3======对象值:" + JSON.toJSONString(target));
46 System.out.println("异常通知=====4======异常值:" + JSON.toJSONString(e.getMessage()));
47  }
48 }

经过上面案例及四种通知的说明,可以看到,ProxyFactoryBean是一个在BeanFactory中显式创建代理对象的中心类,可以给它一个要代理的目标对象、一个要代理的接口集、一个要织入的通知,他将创建一个崭新的代理对象。

引入通知(切点):前四种通知已经指明了在目标方法前,还是后,还是环绕调用。如果不能表达在什么地方应用通知的话,通知将毫无用处,这就是切入点的用处。
切入点决定了一个特定的类的特定方法是否满足一定的规则。若符合,通知就应用到该方法上。引入通知可以指定切入点(即指定在哪些方法上织入通知)。
注:引入通知并不像前4个介绍的那样是一个通知。而重点思想在于:①在哪里(切点,或者说方法)引入?②引入一个什么样的通知?
规则如下:

符号
描述
示例
匹配
不匹配
.
匹配任何单个字符
setFoo.
setFooB
setFooBar setFooB
+
匹配前一个字符一次或多次
setFoo.+
setFooBar setFooB
setFoo
*
匹配前一个字符0次或多次
setFoo.*
setFoosetFooB, setFooBar
 
\
匹配任何正则表达式符号
\.setFoo.
bar.setFoo
setFoo

下面给出一份完整的配置文件 application.xml

 1 <!-- 配置目标对象 -->
 2 <bean id="teacher" class="com.lx.test.Teacher"/>
 3
 4 <!-- 配置前置通知 -->
 5 <bean id="myMethodBeforeAdvice" class="com.lx.advice.MyMethodBeforeAdvice"/>
 6 <!-- 配置后置通知 -->
 7 <bean id="myAfterReturningAdvice" class="com.lx.advice.MyAfterReturningAdvice"/>
 8 <!-- 配置环绕通知 -->
 9 <bean id="myMethodInterceptor" class="com.lx.advice.MyMethodInterceptor"/>
10 <!-- 配置异常通知 -->
11 <bean id="myThrowsAdvice" class="com.lx.advice.MyThrowsAdvice"/>
12
13 <!-- 配置引入通知 -->
14 <bean id="pointcutAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
15 <property name="advice" ref="myMethodBeforeAdvice"/>
16 <property name="mappedNames">
17 <list>
18 <value>add*</value> <!-- 对以add开头的函数织入前置通知 -->
19 <value>del*</value>
20 <value>teach*</value>
21 </list>
22 </property>
23 </bean>
24
25 <!-- 配置代理对象 -->
26 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
27 <!-- 指定要代理的目标对象 -->
28 <property name="target" ref="teacher"/>
29
30 <!-- 指定要代理的接口集 -->
31 <property name="proxyInterfaces">
32 <list>
33 <value>com.lx.test.ITeacher</value>
34 </list>
35 </property>
36
37 <!-- 指定要织入的通知 -->
38 <property name="interceptorNames">
39 <list>
40 <value>myMethodBeforeAdvice</value>
41 <value>myAfterReturningAdvice</value>
42 <value>myMethodInterceptor</value>
43 <value>myThrowsAdvice</value>
44
45 <value>pointcutAdvisor</value>
46 </list>
47 </property>
48 </bean>
 1 // 测试类
 2 public class Main {
 3 public static void main(String[] args) {
 4 ApplicationContext app = new ClassPathXmlApplicationContext("beans.xml");
 5 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean");
 6 int add = iTeacher.add(11, 22);
 7  }
 8 }
 9
10 // 前置通知=====1======函数名:add
11 // 前置通知=====2======参数值:[11,22]
12 // 前置通知=====3======对象值:{}
13 // ============环绕前==============
14 // 前置通知=====1======函数名:add
15 // 前置通知=====2======参数值:[11,22]
16 // 前置通知=====3======对象值:{}
17 // 执行目标方法:老师正在做加法,结果为:33
18 // ============环绕后==============
19 // 后置通知=====0======返回值:33
20 // 后置通知=====1======函数名:add
21 // 后置通知=====2======参数值:[11,22]
22 // 后置通知=====3======对象值:{}
23
24 // row14、15、16是由于配置了引入通知又执行了一次前置通知

4、小结

Spring在运行时通知对象,在运行期创建代理,不需要特殊的编译器。Spring有两种代理方式:
若目标对象实现了若干接口:Spring使用JDK的java.lang.reflect.Proxy类代理。该类让Spring动态产生一个新类,它实现了所需的接口,织入了通知和代理对目标对象的所有请求。
若目标对象没有实现任何接口:Spring使用CGLIB库生成目标对象的子类。使用该方式时需要注意:
①对接口创建代理优于对类创建代理,因为会产生更加松耦合的系统。对类代理是让遗留系统或无法实现接口的第三方类库同样可以得到通知,这种方式应该是备用方案。
②标记为final的方法不能够被通知。Spring是为目标类产生子类,任何需要被通知的方法都需要被复写,将通知织入。final方法是不允许重写的。
Spring实现了aop联盟接口。Spring只支持方法连接点,不提供属性接入点。Spring的观点是属性拦截破坏了封装。面向对象的概念是对象自己处理工作,其他对象只能通过方法调用的得到的结果。
问:说Spring的aop中,当你通过代理对象去实现aop的时候,获取的ProxyFactoryBean是什么类型?
答:返回的是一个代理对象,如果目标对象实现了接口,则Spring使用jdk动态代理技术;如果目标对象没有实现接口,则Spring使用CGLIB技术。

 

版权声明
本文为[Craftsman-L]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/originator/p/15270938.html

  1. L'arrivée de marchandises sèches, l'entretien d'emploi Java 12 grandes usines ont réussi à changer d'emploi,
  2. Multiple postures for handling container time in k8s environment
  3. Echarts remove left Gap, Blank
  4. Hotspot Weekly | zoom $100 million, docker fees, $38 billion Data bricks
  5. JsonMappingException: No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory...
  6. Java. Security. Securerandom source code analysis Java. Security. EGD = file: / dev /. / urandom
  7. When using IntelliJ idea, jump directly and quickly from the mapper interface to mapper.xml
  8. When idea writes SQL in mybatis XML, the solution to the problems of table name, field and red reporting
  9. Spring cloud integrates Nacos
  10. 应届毕业生Java笔试题目,2021大厂Java社招最全面试题,
  11. Liver explosion! Take you to understand Hadoop serialization
  12. linux系列之:告诉他,他根本不懂kill
  13. java版gRPC实战之三:服务端流
  14. RabbitMQ核心知识总结!
  15. linux系列之:告诉他,他根本不懂kill
  16. java版gRPC实战之三:服务端流
  17. RabbitMQ核心知识总结!
  18. 10天拿到字节跳动Java岗位offer,学习Java开发的步骤
  19. 10天拿到字节跳动Java岗位offer,Java知识点思维导图
  20. Résumé des connaissances de base de rabbitmq!
  21. 10天拿到字節跳動Java崗比特offer,Java知識點思維導圖
  22. 10 jours pour obtenir un Byte Jump Java post offer, Java Knowledge point Mind Map
  23. 10 jours pour obtenir l'octet Jump Java post offer, apprendre les étapes du développement Java
  24. Java version of gppc Reality Three: server side stream
  25. Linux Series: Dites - lui qu'il ne connaît pas kill du tout
  26. "Data structure and algorithm" of front end -- binary search
  27. 2020-2021京东Java面试真题解析,如何才能通过一线互联网公司面试
  28. 13 SpringBoot整合RocketMQ实现过滤消息-根据SQL表达式过滤消息
  29. 12 SpringBoot整合RocketMQ实现过滤消息-根据TAG方式过滤消息
  30. 11 SpringBoot整合RocketMQ实现事务消息
  31. 11 springboot Consolidated rocketmq Implementation transaction message
  32. 12 springboot Consolidated rocketmq Implements Filtering messages - Filtering messages according to tag method
  33. 13 springboot Consolidated rocketmq Implementation Filtering messages - Filtering messages according to SQL expressions
  34. linux系列之:告诉他,他根本不懂kill
  35. (1)java Spring Cloud+Spring boot企业快速开发架构之微服务是什么?它的优缺点有哪些?
  36. Oracle 检查 DATE 列 RANGE 分区表已有分区的最大日期时间
  37. ConcurrentHashMap--原理
  38. 2020 - 2021 JD Java interview Real question Analysis, How can interview through First - Line Internet Company
  39. Concurrenthashmap - - Principes
  40. Oracle vérifie l'heure de date maximale d'une partition existante dans la colonne date
  41. Docker Compose 实践及梳理
  42. Qu'est - ce qu'un microservice pour Java Spring Cloud + Spring Boot Enterprise Quick Development architecture?Quels sont ses avantages et ses inconvénients?
  43. Plus sign interview knowledge points in Java
  44. Pratique et organisation de la composition des dockers
  45. Linux Series: Dites - lui qu'il ne connaît pas kill du tout
  46. Convenient CSS and jQuery drop-down menu solution
  47. Linux analog packet loss rate
  48. Redis:我是如何与客户端进行通信的
  49. 15 useful cron work examples commonly used by Senior Linux system administrators
  50. 24个 JavaScript 循环遍历方法,你都知道吗?
  51. Reading notes of JavaScript advanced programming (3rd Edition) 4
  52. 30分钟学会Docker里面开启k8s(Kubernetes)登录仪表盘(图文讲解)
  53. 24 méthodes de traversée de boucle Javascript, vous savez?
  54. 30 minutes pour apprendre à ouvrir le tableau de bord k8s (kubernets) dans le docker (explication graphique)
  55. Redis: comment je communique avec les clients
  56. Wsl2: Windows native Linux subsystem
  57. 30分钟学会Docker里面开启k8s(Kubernetes)登录仪表盘(图文讲解)
  58. Python高级用法总结(8)-函数式编程
  59. 261页前端面试题宝典,JavaScript变量声明提升
  60. The performance of JVM and Java applications of the same version differs by 30% on X86 and aarch64 platforms. Why?