9. Spring AOP
9. Spring AOP
1. 基本概念
AOP (Aspect-Oriented Programming),即 面向切面编程, 它与 OOP (Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.
在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
AOP 是 Spring 是最难理解的概念之一,同时也是非常重要的知识点,因为它真的很常用。
2. 面向切面编程
在面向切面编程的思想里面,把功能分为两种
- 核心业务:登陆、注册、增、删、改、查、都叫核心业务
- 周边功能:日志、事务管理这些次要的为周边业务
在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的;
然后把周边功能和核心业务功能 "编织" 在一起,这就叫 AOP
3. AOP 的目的
AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
4. 术语
- 连接点(join point):对应的是具体被拦截的对象,因为 Spring 只能支持方法,所以被拦截的对象往往就是指特定的方法。具体是指一个方法 
- 切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。具体是指具体共同特征的多个方法。 
- 通知(advice):它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件,有这几种: - 前置通知(before advice)
- 环绕通知(around advice)
- 后置通知(after advice)
- 异常通知(afterThrowing advice)
- 事后返回通知(afterReturning advice)
 
- **目标对象(target):**即被代理的对象,通俗理解各个切点的所在的类就是目标对象。 
- 引入(introduction):是指引入新的类和其方法,增强现有 Bean 的功能。 
- 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。 
- 切面(aspect):定义切点、各类通知和引入的内容,AOP 将通过它的信息来增强 Bean 的功能或将对应的方法织入流程。 
5. 流程
图片的流程顺序基于 Spring 5

1. 五大通知执行顺序
不同版本的 Spring 是有一定差异的,使用时候要注意
- Spring 4 - 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing
 
- Spring 5.28 - 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终
 
2. 图例
举一个实际中的例子来说明一下方便理解:
 房东的核心诉求其实就是签合同,收钱,浅绿部分都是次要的,交给中介就好。
 房东的核心诉求其实就是签合同,收钱,浅绿部分都是次要的,交给中介就好。
不过有的人可能就有疑问了,让房东带着不是更好吗,租客沟通起来不是更轻松吗?为啥非要分成两部分呢?
那么请看下面这种情况:
 当我们有很多个房东的时候,中介的优势就体现出来了。代入到我们实际的业务中,AOP 能够极大的减轻我们的开发工作,**让关注点代码与业务代码分离!**实现解藕!
 当我们有很多个房东的时候,中介的优势就体现出来了。代入到我们实际的业务中,AOP 能够极大的减轻我们的开发工作,**让关注点代码与业务代码分离!**实现解藕!
6. AOP 实现方式一(SpringAPI 接口)
命名空间:
xmlns:aop="http://www.springframework.org/schema/aop"http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd
两个接口:
interface MethodBeforeAdvice 方法执行前
interface AfterReturningAdvice 方法执行后
- 周边功能 - @Component public class BeforeLog implements MethodBeforeAdvice { @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("before"); } } @Component public class AfterLog implements AfterReturningAdvice { @Override public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable { System.out.println("after"); } }
- 核心业务 - @Component public class ServiceImpl { public void service() { System.out.println("service"); } }
- applicationContext.xml - <?xml version="1.0" encoding="UTF8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--开启组件扫描--> <context:component-scan base-package="demo"/> <aop:config> <!--pointcut:切入点;expression:表达式(要执行的位置)--> <!--中括号表示可选部分--> <!--expression:([访问权限类型] 返回值类型 [包名.类名.]方法名(参数类型和参数个数) [异常类型])--> <!-- * 0至多个任意字符 .. 用在方法参数中表示任意多个参数 用在包名后, 表示当前包及其子包路径 + 用在类名后, 表示当前类及其子类 用在接口后, 表示当前接口及其实现类 --> <aop:pointcut id="pointcut" expression="execution(* demo.ServiceImpl.*(..))"/> <!--执行环绕增加--> <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/> <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/> </aop:config> </beans>
- 测试 - public void AOPTest() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = context.getBean("userServiceImpl", UserService.class); userService.service(); }
- 结果  
7. AOP 实现方式二(配置文件)
- 周边功能 - public class Broker { public void before() { System.out.println("带租客看房"); System.out.println("谈钱"); } public void after() { System.out.println("给钥匙"); } }
- 核心业务 - public class Landlord { public void service() { System.out.println("签合同"); System.out.println("收钱"); } }
- applicationContext.xml - <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="broker" class="demo.Broker"/> <bean id="landlord" class="demo.Landlord"/> <aop:config> <aop:pointcut id="pointcut" expression="execution(* demo.Landlord.service(..))"/> <aop:aspect id="broker" ref="broker"> <aop:after method="after" pointcut-ref="pointcut"/> <aop:before method="before" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> </beans>
- 测试 - public void AOPTest2() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Landlord landlord = context.getBean("landlord", Landlord.class); landlord.service(); }
- 结果  
8. AOP 实现方式三(注解)
- 开启注解支持(自动代理生成器) - <aop:aspectj-autoproxy proxy-target-class="false"/>
- false:(默认)jdk 方式实现 - proxy-target-class="false"
 
- true:cglib 方式实现 - proxy-target-class="true"
 
 
- 当业务方法实现了接口时, 默认走 jdk 动态代理, 当业务方法没有接口时, 会自动走 cglib 
- jdk 动态代理 - 使用 jdk 反射包中的类实现创建代理对象的功能
- 要求:目标类必须实现接口
 
- cglib 动态代理 - 使用第三方的工具库, 实现代理对象的创建
- 要求: 目标类必须能够继承, 不是 final
- 原理: 就是继承, 子类就是代理
 
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="demo"/>
<aop:aspectj-autoproxy proxy-target-class="false"/>
</beans>
- @Aspect: 是 aspectj 框架中的注解。- 作用:表示当前类是切面类。
- 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
- 位置:在类定义的上面
 
- 通知方法,方法是实现切面功能的。 - 方法的定义要求: - 公共方法 public
- 方法没有返回值
- 方法名称自定义
- 方法可以有参数,也可以没有参数。
 
- 如果有参数,参数不是自定义的,有几个参数类型可以使用。 - 指定通知方法中的参数 : JoinPoint 
- JoinPoint:业务方法,要加入切面功能的业务方法 - 作用是:可以在通知方法中获取业务方法执行时的信息, 例如方法名称,方法的实参
- 如果你的切面功能中需要用到业务方法的信息,就加入 JoinPoint
- 这个 JoinPoint 参数的值是由框架赋予, 必须是第一个位置的参数
 
 
 
1. 前置通知和后置通知
- @Before: 前置通知注解- 属性:value ,是切入点表达式,表示切面的功能执行的位置。 
- 位置:在通知方法的上面 
- 特点: - 在业务方法之前先执行的
- 不会改变业务方法的执行结果
- 不会影响业务方法的执行。
 
 
- @AfterReturning:后置通知注解- 属性: - value ,是切入点表达式,表示切面的功能执行的位置。
- returning 自定义的变量,表示业务方法的返回值。
- 自定义变量名必须和通知方法的形参名一样。
 
- 位置:在通知方法的上面 
- 特点: - 在业务方法之后先执行的 
- 能够获取到业务方法的返回值,可以根据这个返回值做不同的处理功能 
- 可以修改这个返回值 - 如果返回的是普通类型, 那么修改后的返回值, 不会影响到业务方法原本的返回值
- 如果返回的是对象类型, 那么可以修改该对象的属性值, 业务方法原本的对象属性也会跟着改变, 但是业务方法返回的对象依旧是同一个
 
 
 
- 周边功能 - @Component @Aspect public class Broker { /*** 前置通知*/ @Before(value = "execution(* demo.Landlord.service(..))") public void before() { System.out.println("带租客看房"); System.out.println("谈钱"); } /** 后置通知*/ @AfterReturning(value = "execution(* demo.Landlord.service(..))") public void after() { System.out.println("给钥匙"); } } @Component @Aspect public class Broker { /** 有参通知方法 */ @Before(value = "execution(* demo.Landlord.service(..))") public void before(JoinPoint jp) { // 获取方法的完整定义 System.out.println("方法的签名(定义)=" + jp.getSignature()); System.out.println("方法的名称=" + jp.getSignature().getName()); // 获取方法的实参 Object args[] = jp.getArgs(); for (Object arg : args) { System.out.println("参数=" + arg); } System.out.println("带租客看房"); System.out.println("谈钱"); } /** 后置通知带returning属性(普通类型) */ @AfterReturning(value = "execution(* demo.Landlord.service(..))", returning = "res") public void after(Object res) { System.out.println("给钥匙"); System.out.println("后置通知接收的返回值:" + res); // 修改res的值 res = "我被修改了"; System.out.println("后置通知中修改的返回值:" + res); } }
- 核心业务 - @Component public class Landlord { /**无返回值业务方法*/ public void service() { System.out.println("签合同"); System.out.println("收钱"); } } @Component public class Landlord { /**有返回值业务方法(普通类型)*/ public String service() { System.out.println("签合同"); System.out.println("收钱"); return "Landlord 的 返回值"; } }
- 测试 - /**无返回值*/ public void AOPTest2() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Landlord landlord = context.getBean("landlord", Landlord.class); landlord.service(); } /**有返回值*/ public void test() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Landlord bean = context.getBean("landlord", Landlord.class); String service = bean.service(); System.out.println(service); }
- 无参通知方法  
- 有参通知方法  
2. 环绕通知(Around)
- @Around: 环绕通知- 属性:value 切入点表达式
- 位置:在通知方法的定义上面
 
- 特点: - 它是功能最强的通知
- 在业务方法的前和后都能增强功能。
- 控制业务方法是否被调用执行
- 修改原来的业务方法的执行结果。 影响最后的调用结果
- 环绕通知,等同于 jdk 动态代理的 InvocationHandler 接口
 
- 参数:ProceedingJoinPoint - 就等同于 Method
- 作用:执行业务方法
 
- 返回值:就是业务方法的执行结果,可以被修改。 
- 环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务 
/**
   * 环绕增强
   * 可以在该方法中传递一个参数, 获取切入点
   *
   * @param point ProceedingJoinPoint
   */
@Around(value = "execution(* demo.Landlord.service(..))")
public void around(ProceedingJoinPoint point) throws Throwable {
    System.out.println("环绕前");
    System.out.println(point.getSignature());
    System.out.println("环绕后");
    // 执行切入的通知方法
    point.proceed();
}
3. 异常通知 AfterThrowing
- @AfterThrowing:异常通知
- 属性: - value 切入点表达式
- throwinng 自定义的变量,表示业务方法抛出的异常对象。
- 变量名必须和通知方法的参数名一样
 
- 特点: - 在业务方法抛出异常时执行的
- 可以做异常的监控程序, 监控业务方法执行时是不是有异常。
- 如果有异常,可以发送邮件,短信进行通知
 
- 异常通知方法的定义格式 - public
- 没有返回值
- 方法名称自定义
- 方法有一个 Exception 参数, 如果还有参数那就是 JoinPoint
 - @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex") public void myAfterThrowing(Exception ex) { System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage()); //发送邮件,短信,通知开发人员 }
- 执行 - try{ SomeServiceImpl.doSecond(..) } catch (Exception e) { myAfterThrowing(e); }
4. 最终通知 After
- @After:最终通知
- 属性:value 切入点表达式 
- 位置:在通知方法的上面 
- 特点: - 总是会执行
- 在业务方法之后执行的
 
- 最终通知方法的定义格式 - public
- 没有返回值
- 方法名称自定义
- 方法没有参数,如果还有是 JoinPoint
 - @After(value = "execution(* *..SomeServiceImpl.doThird(..))") public void myAfter(){ System.out.println("执行最终通知,总是会被执行的代码"); //一般做资源清除工作的。 }
- 执行 - try{ SomeServiceImpl.doThird(..) } catch (Exception e) { ... } finally { myAfter() {
5. 定义和管理切入点(Pointcut)
@Pointcut
- 如果你的项目中有多个切入点表达式是重复的,可以复用的, 可以使用@Pointcut
- 属性:value 切入点表达式 
- 位置:在自定义的方法上面 
- 特点: - 当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
- 其它的通知中,value 属性就可以使用这个方法名称,代替切入点表达式了
 
- 当使用
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))" )
private void mypt(){
    //无需代码,
}
@After(value = "mypt()")
public  void  myAfter(){
    System.out.println("执行最终通知,总是会被执行的代码");
    //一般做资源清除工作的。
}
@Before(value = "mypt()")
public  void  myBefore(){
    System.out.println("前置通知,在目标方法之前先执行的");
}
6. 总结
- AOP 的出现是为了对程序解耦,减少系统的重复代码,提高可拓展性和可维护性。
- 常见的应用场景有权限管理、缓存、记录跟踪、优化、校准、日志、事务等等等等……总之 AOP 的使用是非常常见的。
- 需要注意不同 Spring 版本之间的AOP 通知顺序是有差别的。补充:Spring5.28 为分界线。
- 环绕通知很灵活、强大,但是也就意味着很难控制,如非必要,优先使用其他通知来完成。
- 多切面作用同一个切点时候注意切片顺序。