目录
一、AOP的概念
AOP,面向切面编程,是对OOP的补充。从网上看到的一句话:这种在运行时,动态的将代码切入到类的指定方法或者指定位置上的编程思想,就是面向切面的编程。这是其中的一种方式,在运行时动态添加。还有另外一种是在编译代码的时候,将代码切入到指定的方法或者位置上去,这是静态添加的方式。
二、发现代码中冗余
如果我们写了一段需要添加事务的代码,例如账户转账,那么此时这个Service层代码的方法都需要添加上事务来保证一致性,因此会导致一个问题,大量的重复代码加在方法中(添加事务控制、事务提交、出错返回的回滚等),而调用的业务代码实际上只有一行。
同时我们也可以想一下,这样写代码会使得事务的控制和业务层代码紧密结合,耦合程度太大,如果此时事务控制中一个方法需要更改名字,此时需要在所有的方法中进行修改。
- 总结问题:
- 业务层重复代码多
- 业务层代码和事务层控制耦合严重
- 解决方案:
我们可以采用装饰模式和动态代理来解决代码的冗余和耦合。
三、代理模式
3.1 代理模式
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
简单来说就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式角色分为 3 种:
- Subject(抽象主题角色): 委托对象和代理对象都共同实现的接口。
- RealSubject(真实主题角色,也叫原对象,为委托对象): 真正实现业务逻辑的类;
- Proxy(代理主题角色,也叫代理对象): 用来代理和封装真实主题;
简单来说就是三类:功能接口、功能提供者、功能代理者。
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层
3.2 代理模式的分类
根据字节码的创建时机来分类,可以分为静态代理和动态代理:
- 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
而Spring中则使用的是动态代理技术。
3.3 动态代理
动态代理的核心思想是通过 Java Proxy 类,为传入进来的任意对象动态生成一个代理对象,这个代理对象默认实现了委托对象的所有接口。
Java实现动态代理的大致步骤如下:
- 定义一个委托类和公共接口。
- 自己定义一个类(调用处理器类,即实现 InvocationHandler 接口),这个类的目的是指定运行时将生成的代理类需要完成的具体任务(包括Preprocess和Postprocess),即代理类调用任何方法都会经过这个调用处理器类(在本文最后一节对此进行解释)。
- 生成代理对象(当然也会生成代理类),需要为他指定
(1)委托对象
(2)实现的一系列接口
(3)调用处理器类的实例。
因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。
代理类和委托类 互相透明独立,逻辑没有任何耦合,在运行时才绑定在一起。这也就是静态代理与动态代理最大的不同,带来的好处就是:无论委托类有多少个,代理类不受到任何影响,而且在编译时无需知道具体委托类。
其实动态代理并不复杂,通过一个 Proxy 工具,为委托类的接口自动生成一个代理对象,后续的函数调用都通过这个代理对象进行发起,最终会执行到 InvocationHandler#invoke 方法,在这个方法里除了调用真实委托类对应的方法,还可以做一些其他自定义的逻辑。
四、使用SpringAOP解决代码冗余
AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。
两种方法同时存在,各有优劣。jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。
常用的动态代理分为两种
- 基于接口的动态代理,使用JDK 官方的 Proxy 类,要求被代理者至少实现一个接口
- 基于子类的动态代理,使用第三方的 CGLib库,要求被代理类不能是final类.
4.1 相关术语
-
Joinpoint(连接点): 被拦截到的方法.
-
Pointcut(切入点): 我们对其进行增强的方法.
-
Advice(通知/增强): 对切入点进行的增强操作
包括前置通知,后置通知,异常通知,最终通知,环绕通知
-
Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。
-
Aspect(切面): 是切入点和通知的结合
4.2 使用xml配置AOP
在bean.xml中配置AOP要经过以下几步:
- 在bean.xml中引入约束并将通知类注入Spring容器中
<?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="logger" class="cn.maoritian.utils.Logger"></bean>
</beans>
- 使用aop:config标签声明AOP配置,所有关于AOP配置的代码都写在aop:config标签内
<aop:config>
<!-- AOP配置的代码都写在此处 -->
</aop:config>
-
使用aop:aspect标签配置切面,其属性如下
- id: 指定切面的id
- ref: 引用通知类的id
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型要写在此处-->
</aop:aspect>
</aop:config>
- 使用aop:pointcut标签配置切入点表达式,指定对哪些方法进行增强,其属性如下
- id: 指定切入点表达式的id
- expression: 指定切入点表达式
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
</aop:aspect>
</aop:config>
-
使用aop:xxx标签配置对应类型的通知方法
其属性如下:- method: 指定通知类中的增强方法名.
- ponitcut-ref: 指定切入点的表达式的id
- poinitcut: 指定切入点表达式
其中pointcut-ref和pointref属性只能有其中一个
具体的通知类型:
- aop:before: 配置前置通知,指定的增强方法在切入点方法之前执行.
- aop:after-returning: 配置后置通知,指定的增强方法在切入点方法正常执行之后执行.
- aop:after-throwing: 配置异常通知,指定的增强方法在切入点方法产生异常后执行.
- aop:after: 配置最终通知,无论切入点方法执行时是否发生异常,指定的增强方法都会最后执行.
- aop:around: 配置环绕通知,可以在代码中手动控制增强代码的执行时机.
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!--指定切入点表达式-->
<aop:pointcut expression="execution(* cn,maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
<!--配置各种类型的通知-->
<aop:before method="printLogBefore" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="printLogAfterReturning" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="printLogAfterThrowing" pointcut-ref="pt1"></aop:after-throwing>
<aop:after method="printLogAfter" pointcut-ref="pt1"></aop:after>
<!--环绕通知一般单独使用-->
<!-- <aop:around method="printLogAround" pointcut-ref="pt1"></aop:around> -->
</aop:aspect>
</aop:config>
4.3 切入点表达式
切入点表达式的写法: execution([修饰符] 返回值类型 包路径.类名.方法名(参数))
切入点表达式的省略写法:
-
全匹配方式:
<aop:pointcut expression="execution(public void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
其中访问修饰符可以省略:
<aop:pointcut expression="execution(void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
返回值可使用*,表示任意返回值:
<aop:pointcut expression="execution(* cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
包路径可以使用*,表示任意包. 但是*.的个数要和包的层级数相匹配
<aop:pointcut expression="execution(* *.*.*.*.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
包路径可以使用*…,表示当前包,及其子包(因为本例子中将bean.xml放在根路径下,因此…可以匹配项目内所有包路径)
<aop:pointcut expression="execution(* *..AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
类名可以使用*,表示任意类
<aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
-
方法名可以使用*,表示任意方法
<aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
-
参数列表可以使用*,表示参数可以是任意数据类型,但是必须存在参数
<aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut>
-
参数列表可以使用…表示有无参数均可,有参数可以是任意类型
<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
-
全通配方式,可以匹配匹配任意方法
<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
切入点表达式的一般写法
一般我们都是对业务层所有实现类的所有方法进行增强,因此切入点表达式写法通常为
<aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
4.4 环绕通知
一、 前置通知,后置通知,异常通知,最终通知的执行顺序
Spring是基于动态代理对方法进行增强的,前置通知,后置通知,异常通知,最终通知在增强方法中的执行时机如下:
// 增强方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
Object rtValue = null;
try {
// 执行前置通知
// 执行原方法
rtValue = method.invoke(accountService, args);
// 执行后置通知
return rtValue;
} catch (Exception e) {
// 执行异常通知
} finally {
// 执行最终通知
}
}
二、环绕通知允许我们更自由地控制增强代码执行的时机
Spring框架为我们提供一个接口ProceedingJoinPoint,它的实例对象可以作为环绕通知方法的参数,通过参数控制被增强方法的执行时机.
- ProceedingJoinPoint对象的getArgs()方法返回被拦截的参数
- ProceedingJoinPoint对象的proceed()方法执行被拦截的方法
// 环绕通知方法,返回Object类型
public Object printLogAround(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs();
printLogBefore(); // 执行前置通知
rtValue = pjp.proceed(args);// 执行被拦截方法
printLogAfterReturn(); // 执行后置通知
}catch(Throwable e) {
printLogAfterThrowing(); // 执行异常通知
}finally {
printLogAfter(); // 执行最终通知
}
return rtValue;
}
4.4 使用注解配置AOP
4.4.1 半注解配置AOP
Spring注解配置AOP的步骤
半注解配置AOP,需要在bean,xml中加入下面语句开启对注解AOP的支持
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
Spring用于AOP的注解
- 用于声明切面的注解
- @Aspect: 声明当前类为通知类,该类定义了一个切面.相当于xml配置中的aop:aspect标签
@Component("logger")
@Aspect
public class Logger {
// ...
}
- 用于声明通知的注解
- @Before: 声明该方法为前置通知.相当于xml配置中的aop:before标签
- @AfterReturning: 声明该方法为后置通知.相当于xml配置中的aop:after-returning标签
- @AfterThrowing: 声明该方法为异常通知.相当于xml配置中的aop:after-throwing标签
- @After: 声明该方法为最终通知.相当于xml配置中的aop:after标签
- @Around: 声明该方法为环绕通知.相当于xml配置中的aop:around标签
属性:
value: 用于指定切入点表达式或切入点表达式的引用
@Component("logger")
@Aspect //表示当前类是一个通知类
public class Logger {
// 配置前置通知
@Before("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogBefore(){
System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。");
}
// 配置后置通知
@AfterReturning("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfterReturning(){
System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。");
}
// 配置异常通知
@AfterThrowing("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfterThrowing(){
System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。");
}
// 配置最终通知
@After("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfter(){
System.out.println("最终通知Logger类中的printLogAfter方法开始记录日志了。。。");
}
// 配置环绕通知
@Around("execution(* cn.maoritian.service.impl.*.*(..))")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();
printLogBefore(); // 执行前置通知
rtValue = pjp.proceed(args); // 执行切入点方法
printLogAfterReturning(); // 执行后置通知
return rtValue;
}catch (Throwable t){
printLogAfterThrowing(); // 执行异常通知
throw new RuntimeException(t);
}finally {
printLogAfter(); // 执行最终通知
}
}
}
- 用于指定切入点表达式的注解
- @Pointcut: 指定切入点表达式,其属性如下:
value: 指定表达式的内容 - @Pointcut注解没有id属性,通过调用被注解的方法获取切入点表达式.
- @Pointcut: 指定切入点表达式,其属性如下:
@Component("logger")
@Aspect //表示当前类是一个通知类
public class Logger {
// 配置切入点表达式
@Pointcut("execution(* cn.maoritian.service.impl.*.*(..))")
private void pt1(){}
// 通过调用被注解的方法获取切入点表达式
@Before("pt1()")
public void printLogBefore(){
System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。");
}
// 通过调用被注解的方法获取切入点表达式
@AfterReturning("pt1()")
public void printLogAfterReturning(){
System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。");
}
// 通过调用被注解的方法获取切入点表达式
@AfterThrowing("pt1()")
public void printLogAfterThrowing(){
System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。");
}
}
4.4.2 纯注解配置AOP
在Spring配置类前添加@EnableAspectJAutoProxy注解,可以使用纯注解方式配置AOP
@Configuration
@ComponentScan(basePackages="cn.maoritian")
@EnableAspectJAutoProxy // 允许AOP
public class SpringConfiguration {
// 具体配置
//...
}
使用注解配置AOP的bug
在使用注解配置AOP时,会出现一个bug. 四个通知的调用顺序依次是:前置通知,最终通知,后置通知. 这会导致一些资源在执行最终通知时提前被释放掉了,而执行后置通知时就会出错
参考资料:
http://wingjay.com/2018/02/11/java-dynamic-proxy/ Java 技术之动态代理机制
https://juejin.im/post/5a3284a75188252970793195 深入理解Spring AOP的动态代理