一、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. 惊人!Spring5 AOP 默认使用Cglib ?从现象到源码深度分析

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

  5. Spring AOP With AspectJ

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

  6. Spring Aop的执行顺序

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

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

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

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

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

  9. Spring 使用介绍(五)—— AOP(一)

    一.简单使用:Hello World实例 1.定义目标类 public interface Hello { void sayHello(); } public class HelloImpl impl ...

  10. spring aop的五种通知类型

    昨天在腾讯课堂看springboot的视频,老师随口提问,尼玛竟然回答错了.特此记录! 问题: Spring web项目如果程序启动时出现异常,调用的是aop中哪类通知? 正确答案是: 异常返回通知. ...

随机推荐

  1. 传入url地址请求服务器api,浏览器显示图片

    @RequestMapping("/proxyImage") public void proxyImage(HttpServletRequest request, HttpServ ...

  2. AChartEngine 安卓折线图 柱形图等利器

    http://www.eoeandroid.com/thread-548233-1-6.html 最近公司项目中要用到折线图,状态类型的图标要用到折线图,柱形图等,并且能够动态显示,在网上找了许多de ...

  3. HTTP基础08--追加协议

    消除 HTTP 瓶颈的 SPDY HTTP 的瓶颈 Web 网站为了保存这些新增内容,在很短的时间内就会发生大量的内容更新;为了尽可能实时地显示这些更新的内容,服务器上一有内容更新,就需要直接把那些内 ...

  4. 元数据集 DatabaseMetaData ResultSetMetaData

  5. Android ViewPager使用详解

    这是谷歌官方给我们提供的一个兼容低版本安卓设备的软件包,里面包囊了只有在安卓3.0以上可以使用的api.而viewpager就是其中之一利用它,我们可以做很多事情,从最简单的导航,到页面菜单等等.那如 ...

  6. 2、Android应用程序基本特性

    1. apk是android应用程序安装格式,.dex是Android二进制执行文件格式. 2.Android操作系统是基于Linux的多用户操作系统,每一个应用程序都是使用的不同的用户. 3. 默认 ...

  7. HDU 1983 BFS&amp;amp;&amp;amp;DFS

    大多数刚需封锁4区域可以,DFS地区封锁.BFS无论是通过 #include "stdio.h" #include "string.h" #include &q ...

  8. 关于Linux虚拟化技术KVM的科普 科普二(KVM虚拟机代码揭秘)

    代码分析文章<KVM虚拟机代码揭秘--QEMU代码结构分析>.<KVM虚拟机代码揭秘--中断虚拟化>.<KVM虚拟机代码揭秘--设备IO虚拟化>.<KVM虚拟 ...

  9. Java 学习笔记 泛型

    泛型 上界匹配 ? extends Number 下界匹配 ? super Number getSimpleName 不包括包名 getName 会包括包名 常和反射联合使用,做框架 Type是一个标 ...

  10. Sublime Text3 安装 markdownediting插件 报错 Error loading syntax file &quot;Packages/Markdown/Markdown.tmLanguage&quot;:

    问题: Error loading syntax file "Packages/Markdown/Markdown.sublime-syntax": 解决方法: ./Data/Lo ...