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 能够极大的减轻我们的开发工作,**让关注点代码与业务代码分离!**实现解藕!
6. AOP 实现方式一(SpringAPI 接口)
命名空间:
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://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 为分界线。
- 环绕通知很灵活、强大,但是也就意味着很难控制,如非必要,优先使用其他通知来完成。
- 多切面作用同一个切点时候注意切片顺序。