一、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技术。

Spring5(五)——AOP的更多相关文章

  1. Spring(五)AOP简述

    一.AOP简述 AOP全称是:aspect-oriented programming,它是面向切面编号的思想核心, AOP和OOP既面向对象的编程语言,不相冲突,它们是两个相辅相成的设计模式型 AOP ...

  2. Spring 学习十五 AOP

    http://www.hongyanliren.com/2014m12/22797.html 1: 通知(advice): 就是你想要的功能,也就是安全.事物.日子等.先定义好,在想用的地方用一下.包 ...

  3. Spring学习之旅(五)--AOP

    什么是 AOP AOP(Aspect-OrientedProgramming,面向方面编程),可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善. OO ...

  4. Java开发学习(十五)----AOP入门案例及其工作流程解析

    一.AOP简介 1.1 什么是AOP AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构. OOP(Object Oriented ...

  5. 惊人!Spring5 AOP 默认使用Cglib ?从现象到源码深度分析

    Spring5 AOP 默认使用 Cglib 了?我第一次听到这个说法是在一个微信群里: 真的假的?查阅文档 刚看到这个说法的时候,我是保持怀疑态度的. 大家都知道 Spring5 之前的版本 AOP ...

  6. Spring AOP With AspectJ

    一.AOP和拦截器 某些情况下,AOP和拦截器包括Filter能够实现同样的功能,一般都是请求即controller层的操作,这三个执行顺序为Filter>Interceptor>AOP, ...

  7. Spring Aop的执行顺序

    Spring Aop的执行顺序 首先回忆一下 AOP 的常用注解 @Before:前置通知:目标方法之前执行 @After:后置通知:目标方法之后执行 @AfterReturning:返回后通知:执行 ...

  8. 20181123_控制反转(IOC)和依赖注入(DI)

    一.   控制反转和依赖注入: 控制反转的前提, 是依赖倒置原则, 系统架构时,高层模块不应该依赖于低层模块,二者通过抽象来依赖 (依赖抽象,而不是细节) 如果要想做到控制反转(IOC), 就必须要使 ...

  9. Spring之旅第五篇-AOP详解

    一.什么是AOP? Aspect oritention programming(面向切面编程),AOP是一种思想,高度概括的话是“横向重复,纵向抽取”,如何理解呢?举个例子:访问页面时需要权限认证,如 ...

随机推荐

  1. Send Excerpts from Jenkins Console Output as Email Contents

    Sometimes we need to send some excerpts from Jenkins console output (job logs) as email, such as tes ...

  2. 附件携马之CS免杀shellcode过国内主流杀软

    0x01 写在前面 其实去年已经写过类似的文章,但是久没用了,难免有些生疏.所谓温故而知新,因此再详细的记录一下,一方面可以给各位看官做个分享,另一方面等到用时也不至于出现临阵磨枪的尴尬场面. 0x0 ...

  3. 使用Netcat实现通信和反弹Shell

    一.概述 nc全称为netcat,所做的就是在两台电脑之间建立链接,并返回两个数据流 可运行在TCP或者UDP模式,添加参数 -u 则调整为udP,默认为tcp -v 参数,详细输出 -n参数,net ...

  4. C51—模拟IIC总线实现EEPROM存取数据

    a - 什么是IIC总线 -什么是EEPROM -IIC总线的通信格式 模块化设计注解 整体代码 - 什么是IIC总线 IIC总线是同步通信的一种特殊形式,具有接线口少.控制简单.器件封装形式小.通信 ...

  5. CentOS8 安装MySQL5.7

    CentOS_8 安装MySQL5.7 1.在安装之前,如果你的系统曾经安装过Mariadb,请先卸载:yum remove mariadb*2.安装依赖 yum install -y epel-re ...

  6. 公司新来了一个质量工程师,说团队要保证 0 error,0 warning

    摘要:静态代码检查又称为静态程序分析,是指在不运行计算机程序的条件下,进行程序分析的方法. 本文分享自华为云社区<公司新来了一个质量工程师,说团队要保证 0 error,0 warning> ...

  7. SpringCloud升级之路2020.0.x版-22.Spring Cloud LoadBalancer核心源码

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 经过上一节的详细分 ...

  8. Mybatis框架及原理实例分析

    摘要 本篇文章只是个人阅读mybatis源码总结的经验或者个人理解mybatis的基本轮廓,作为抛砖引玉的功能,希望对你有帮助,如果需要深入了解细节还需亲自去阅读源码. mybatis基本架构 myb ...

  9. FastReport.net 绿色破解版winform中使用

    FastReport 是非常有名的报表库,曾经在delphi中经常看到 现在FastReport.net 是.net平台下的实现.它的价格对于个人开发者来说确实非常非常贵 出于学习的目的(0<& ...

  10. ASP.Net Core Web Api 使用 IdentityServer4 最新版 踩坑记录

    辅助工具 日志追踪包 : Serilog.AspNetCore 源码查看工具 : ILSpy 项目环境 ###: ASP.NetCore 3.1 IdentityServer4 4.0.0+ 主题内容 ...