Spring AoP总结
Spring 复习
2.Spring AoP
本文启发自关于 Spring AOP (AspectJ) 你该知晓的一切_zejian的博客-CSDN博客_springaop
2.1 基本概念
2.1.1 存在问题
如果使用传统OOP的方式来对大量重复的代码进行管理,通常采用的是继承或抽取静态工厂方法的方式,然而这两种方式仍然会使项目中出现过多重复的代码。为了能够对业务代码更好的解耦,让每个业务模块更加独立,需要存在一种方式能够解决这个问题,AoP应运而生
2.1.2 AoP(Aspect Oriented Programming)
字面意思为面向切面编程,其逻辑分为如下几步
-
抽取重复逻辑
通过将不同模块会使用到的重复的逻辑(如日志、监控等)进行抽取,将其定义在专门的模块中,并将功能划分为多个方法,如方法a,b,c;
-
分析切入点
对需要用到前面抽取逻辑的模块(如查询订单),首先需要分析哪些方法上及这些方法的哪些位置上应用前面抽取出的功能
-
织入功能
在分析好切入点后,只需要将需要的模块插入至各个对应的切入点即可
2.1.3 AspectJ
1)基本概念
AspectJ是一种AoP的解决方案,其采用编译器或链接期织入的方式,对原有类实现增强,逻辑如下

2)术语
-
JointPoint
只要增强的类中哪些方法可以被当作Pointcut点
-
Pointcut
指定了真正要被增强的方法
-
Advice
即对Pointcut指定的方法要做的增强
-
Aspect
Advice+Pointcut构成一个切面
-
Weaving
将Advice织入到Pointcut的过程
3)注解使用
其提供了如下注解
-
Advice相关
-
@Before
在目标方法(pointcut)之前执行
-
@AfterRuturning
在目标方法返回后执行,其中可以设置
returning
属性用于接收方法返回值,并将创建属性值同名的方法参数 -
@AfterThrowing
在目标方法抛出异常后执行,可以设置
throwing
属性用于接收方法抛出的异常,并将创建属性值同名的方法参数 -
@After
在方法结束后return前执行
-
@Around
类似于定义InvocationHandler中的invoke方法,是前面几种的组合
try{ //前置通知 Before }catch(Exception e){ //异常通知 AfterThrowing }finally{ //最终通知 After }
-
-
Pointcut相关
-
@Pointcut
用于定义切点,Advice注解可以直接使用
-
-
Aspect相关
-
@Aspect
用于修饰切面类
-
2.2 Spring AoP
2.2.1 原理
Spring中没有采用类似于AspectJ的方法在编译器生成代理类对象,而是采用动态代理的方式,在运行期生成代理类对象,这样可以配合IoC来使用,更适合Spring框架。运行期生成代理类对象有两种方式,分别为使用JDK 提供的动态代理工具类Proxy,和CGLIB的方式
1)JDK 动态代理(接口
)
(1)原理
这种方式衍生于静态代理,将代理类的生成放在运行期通过反射来创建。但是,都需要被代理类将需要增强的方法放入接口并实现,这样代理类就可以通过反射方式调用被代理类的实现的接口中的方法,完成动态代理
(2)使用
-
创建代理类对象
使用
Proxy.newProxyInstance
来创建,其中需要传入如下三个参数-
classLoader
将被代理类的类加载器传入即可
-
class<?>[] interfaces
传入被代理类实现的所有接口
-
InvocationHandler
代理类的增强功能
上面最关键的是创建
InvocationHandler
的实现类,并重写其中的invoke方法,一个常见的实现模板如下public class Handler implements InvocationHandler{ //被代理对象 private Object obj; public Handler() { } //将被代理对象传入 public WorkHandler(Object obj) { this.obj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try{ //前置增强部分 //传入被代理对象,反射调用原始方法 Object invoke = method.invoke(obj, args); //后置增强部分 return invoke; }catch(Exception e){ //异常增强部分 }finally{ //最终增强部分 } } }
-
-
使用代理类对象
使用工厂方法newProxyInstance返回的是Object类型,需要向下转型至想要的接口类型并调用接口方法
2)CGLIB(子类
)
(1)原理
通过创建被代理类的子类,采用继承的方式对被代理类中的非final方法进行增强,这种方式适用于没有实现接口的情况,但是要求被代理类及其想要被增强的方法必须不是final的
(2)使用
下面例子摘自关于 Spring AOP (AspectJ) 你该知晓的一切_zejian的博客-CSDN博客_springaop
//被代理的类即目标对象
public class A {
public void execute(){
System.out.println("执行A的execute方法...");
}
}
//代理类
public class CGLibProxy implements MethodInterceptor {
/**
* 被代理的目标类
*/
private A target;
public CGLibProxy(A target) {
super();
this.target = target;
}
/**
* 创建代理对象
* @return
*/
public A createProxy(){
// 使用CGLIB生成代理:
// 1.声明增强类实例,用于生产代理类
Enhancer enhancer = new Enhancer();
// 2.设置被代理类字节码,CGLIB根据字节码生成被代理类的子类
enhancer.setSuperclass(target.getClass());
// 3.//设置回调函数,即一个方法拦截
enhancer.setCallback(this);
// 4.创建代理:
return (A) enhancer.create();
}
/**
* 回调函数
* @param proxy 代理对象
* @param method 委托类方法
* @param args 方法参数
* @param methodProxy 每个被代理的方法都对应一个MethodProxy对象,
* methodProxy.invokeSuper方法最终调用委托类(目标类)的原始方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//过滤不需要该业务的方法
if("execute".equals(method.getName())) {
//调用前验证权限(动态添加其他要执行业务)
AuthCheck.authCheck();
//调用目标对象的方法(执行A对象即被代理对象的execute方法)
Object result = methodProxy.invokeSuper(proxy, args);
//记录日志数据(动态添加其他要执行业务)
Report.recordLog();
return result;
}else if("delete".equals(method.getName())){
//.....
return methodProxy.invokeSuper(proxy, args);
}
//如果不需要增强直接执行原方法
return methodProxy.invokeSuper(proxy, args);
}
}
可见,和JDK动态代理相比,有如下类似点
- MethodInterceptor接口功能类似于InvocationHandler
- 创建Enhancer实例(代理(增强)对象)类比于调用Proxy.newProxyInstance
methodProxy.invokeSuper(proxy, args);
类比于method.invoke(obj, args);
,注意其中传入的对象不同
2.2.2 使用
1)开启AspectJ注解
为了使用AspecJ提供的注解的规范,需要开启对AspecJ注解的支持,有如下两种方式
-
@EnableAspectJAutoProxy
在配置类上修饰
-
在xml中添加
2)定义切入细则
这里包括定义切入点、通知、切面等,可以采用基于注解或基于xml的方式
(1)注解
@Component("logger")
@Aspect
public class Logger {
@Pointcut("execution(* aoptest1.impl.AccountService.*(..))")
private void pt1() {
}
//采用注解的方式配置就需要避免这四种通知和环绕通知同时出现,否则会出现冗余
// @Before("pt1()")
// public void beforePrintLog(){
// System.out.println("前置:打印日志");
// }
/*
后置通知在return之后执行
*/
// @AfterReturning(value = "pt1()", returning = "returnVal")
// public void afterReturningPrintLog(Object returnVal){
// System.out.println("后置:打印日志" + returnVal);
// }
// @AfterThrowing(value = "pt1()", throwing = "e")
// public void afterThrowingPrintLog(Throwable e){
// System.out.println("异常:打印日志" + e.getMessage);
// }
// @After("pt1()")
// public void afterPrintLog(){
// System.out.println("最终:打印日志");
// }
/**
* 环绕通知的方式类似于Proxy创建动态代理对象时新建InvocationHandler对象时重写invoke方法的过程
*
* @param pjp 它可以作为环绕通知的方法参数。在环绕通知执行时,spring框架会为我们提供该接口的实现类对象,直接使用即可。
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();//获取方法执行参数
try {
System.out.println("前置:打印日志");
Object result = pjp.proceed(args);//执行被拦截的方法
//int i = 1/0;
return result;
} catch (Throwable e) {
System.out.println("异常:打印日志");
throw new RuntimeException(e);
} finally {
System.out.println("最终:打印日志");
}
}
}
不要忘记
@Component
注解
(2)xml
<?xml version="1.0" encoding="UTF-8"?>
# 附 - Spring中的事务
由于Spring中的事务依赖于AoP,因而在此记录,以下内容整理自JavaGuide (gitee.io)及关于PROPAGATION_NESTED的理解_yanxin1213的博客-CSDN博客
#1. 基本概念
1)事务实现的方式
- 编程式事务,在代码中硬编码。(不推荐使用)
- 声明式事务,在配置文件中配置(推荐使用)
声明式事务又分为两种:
- 基于XML的声明式事务
- 基于注解的声明式事务
2)Spring 事务中的隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
- TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
3) Spring 事务传播行为
支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
-
TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
举例如下,定义serviceA.methodA()以PROPAGATION_REQUIRED修饰; 定义serviceB.methodB()以表格中三种方式修饰; methodA中调用methodB
异常状态 PROPAGATION_REQUIRES_NEW (两个独立事务) PROPAGATION_NESTED (B的事务嵌套在A的事务中) PROPAGATION_REQUIRED (同一个事务) methodA抛异常 methodB正常 A回滚,B正常提交 A与B一起回滚 A与B一起回滚 methodA正常 methodB抛异常 1.如果A中捕获B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交; 2.如果A未捕获B的异常,默认则会将B的异常向上抛,则B先回滚,A再回滚 B先回滚,A再正常提交 A与B一起回滚 methodA抛异常 methodB抛异常 B先回滚,A再回滚 A与B一起回滚 A与B一起回滚 methodA正常 methodB正常 B先提交,A再提交 A与B一起提交 A与B一起提交
#2. 使用
1)开启事务支持
-
@EnableTransactionManagement
修饰于配置类上
-
添加在xml中
2)创建数据源
可以使用Druid作为数据源
3)创建事务管理器
一般为创建DataSourceTransactionManager
对象,传入数据源Bean,并返回为Bean
4)修饰需要事务的类/方法
将@Transactional
注解修饰于类(对public方法有效)或者方法上即可,此注解有如下关键属性
-
transactionManager
指定事务管理器
-
propagation
指定事务传播规则
-
isolation
指定事务隔离级别
-
readOnly
如果是只读操作,可以设置为true,这样会在运行期被优化执行
-
rollbackFor
如果不配置,则只在抛出
RuntimeException
时回滚,如果指定为Exception.class,则受查异常也会回滚