Spring Aop 基于注解的实现
1、用到的技术
- 动态代理(InvocationHandler):JDK 原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口。
- cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
- AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver 就是织入器。Spring 只是借用了 AspectJ 中的注解。
2、AOP 术语
① 横切关注点
从每个方法中抽取出来的同一类非核心业务。
在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
② 通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知(@Before):在被代理的目标方法前执行
- 返回通知(@AfterReturning):在被代理的目标方法成功结束后执行
- 异常通知(@AfterThrowing):在被代理的目标方法异常结束后执行
- 后置通知(@After):在被代理的目标方法最终结束后执行
- 环绕通知(@Around):使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
③ 切面
封装通知方法的类。
④ 目标
被代理的目标对象。
⑤ 代理
向目标对象应用通知之后创建的代理对象
⑥ 连接点
把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点。
⑦ 切入点
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP 可以通过切入点定位到特定的连接点。
切入点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3、基于注解的 Spring AOP
① 导入 jar 包
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.3.jar
hamcrest-core-1.3.jar
junit-4.12.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
② 创建被代理类
由于我们加入了 cglib 的支持,所以允许被代理类不实现任何接口。但是这个类必须加入 IOC 容器,否则 Spring 没法控制和操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component public class CalculatorImpl { public int add(int a, int b) { System.out.println("(a+b) = " + (a + b)); return a + b; }
public int sub(int a, int b) { System.out.println("(a-b) = " + (a - b)); return a - b; }
public int mul(int a, int b) { System.out.println("(a×b) = " + (a * b)); return a * b; }
public int div(int a, int b) { System.out.println("(a÷b) = " + (a / b)); return a / b; } }
|
③ 创建切面类
要想把一个类变成切面类,只需 3 步:
[1] 在类上使用 @Aspect 注解使之成为切面类
[2] 切面类需要交由 Spring 容器管理,所以类上还需要有 @Service、@Repository、@Controller、@Component 等注解
[3] 在切面类中自定义方法接收通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
@Aspect @Component public class LogAspect {
@Before(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))") public void doBeforeLog() { System.out.println("[aop log]method begin"); }
@AfterReturning(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))") public void doSuccessLog() { System.out.println("[aop log]method successfully end"); }
@AfterThrowing(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))") public void doExceptionLog() { System.out.println("[aop log]method ended with exception"); }
@After(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))") public void doAfterLog() { System.out.println("[aop log]method finally end"); } }
|
④ 配置 Spring 配置文件(applicationContext.xml)
1 2 3 4 5
| <context:component-scan base-package="com.atguigu.aop"/>
<aop:aspectj-autoproxy/>
|
⑤junit 测试
1 2 3 4 5 6 7 8 9 10 11 12 13
|
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
@Test public void testAOP() { CalculatorImpl bean = iocContainer.getBean(CalculatorImpl.class); bean.div(5, 1);
System.out.println("bean.getClass().getName() = " + bean.getClass().getName()); }
|
4、通知执行的顺序
- Spring 版本 5.3.x 以前:
- Spring 版本 5.3.x 以后:
5、各个通知获取细节信息
① JoinPoint 接口
org.aspectj.lang.JoinPoint
JoinPoint 对象封装了 Spring Aop 中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的 JoinPoint 对象.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Before(value = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))") public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
List<Object> argList = Arrays.asList(args);
System.out.println("[前置]"+ methodName + " 方法开始执行,参数列表是" + argList); }
|
需要获取方法签名、传入的实参等信息时,可以在通知方法声明 JoinPoint 类型的形参。
② 方法返回值
在返回通知中,通过@AfterReturning 注解的 returning 属性获取目标方法的返回值
1 2 3 4 5 6 7 8 9 10 11
| @AfterReturning( pointcut = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))",
// 使用returning属性指定一个名称,Spring会自动将目标方法的返回值传入到同名的参数位置 // 注意:目标方法必须确定有返回值,如果目标方法返回值是void,那么Spring给参数位置传入null returning = "targetMethodReturnValue" ) public void printLogAfterCoreOperationSuccess(Object targetMethodReturnValue){ System.out.println("[返回]method success,return value=" + targetMethodReturnValue); }
|
③ 目标方法抛出的异常
在异常通知中,通过@AfterThrowing 注解的 throwing 属性获取目标方法抛出的异常对象
1 2 3 4 5 6 7 8 9 10
| @AfterThrowing( value = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))",
// 使用throwing属性指定一个形参名称,Spring调用当前方法时,会把目标方法抛出的异常对象从这里传入 throwing = "ep" ) public void printLogAfterCoreOperationFailed(Throwable ep) { System.out.println("[异常]method failed,exception name=" + ep.getClass().getName()); }
|
6、切入点表达式语法
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,使用***..**表示包名任意、包的层次深度任意
- 在类名的部分,使用*****号表示类名任意
- 在类名的部分,可以使用*****号代替类名的一部分
上面例子表示匹配所有类名、接口名以 Service 结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
上面例子表示匹配所有方法名以 Operation 结尾的方法
- 在方法参数列表部分,使用(..)表示参数列表任意
- 在方法参数列表部分,使用(int,..)表示参数列表以一个 int 类型的参数开头
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
1
| execution(public int *..*Service.*(.., int))
|
上面例子是对的,下面例子是错的:
1
| execution(* int *..*Service.*(.., int))
|
- 对于 execution()表达式整体可以使用三个逻辑运算符号
- execution() || execution()表示满足两个 execution()中的任何一个即可
- execution() && execution()表示两个 execution()表达式必须都满足
- !execution()表示不满足表达式的其他方法
7、环绕通知
环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Aspect @Component public class MessageAspect {
@Around(value = "com.atguigu.aop.component.LogAspect.declarePointCut()") public Object roundAdvice(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object targetMethodReturnValue = null;
Object[] args = joinPoint.getArgs();
try {
System.out.println("[环绕] message before target method " + methodName);
targetMethodReturnValue = joinPoint.proceed(args);
System.out.println("[环绕]message after target method " + methodName + " success,return value="+targetMethodReturnValue);
} catch (Throwable throwable) { throwable.printStackTrace();
System.out.println("[环绕]message after target method " + methodName + " failed " + throwable.getClass().getName());
} finally { System.out.println("[环绕]message after target method " + methodName + " finally end"); }
return targetMethodReturnValue; } }
|
8、重用切入点表达式
在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。
声明方式如下:
1 2 3
| @Pointcut("execution(* *..*.div(..))") public void declarPointCut() {}
|
同一个类内部引用时:
1 2
| @Before(value = "declarPointCut()") public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
|
在不同类中引用:
1 2
| @Around(value = "com.atguigu.aop.component.LogAspect.declarPointCut()") public Object roundAdvice(ProceedingJoinPoint joinPoint) {
|
举个例子,上面 5.2 章节的代码可以修改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @AfterReturning( value = "declarePointCut()",
// 使用returning指定一个形参名,Spring会在调用当前方法时,把目标方法的返回值从这个位置传入 returning = "returnValue" )
public void printLogAfterCoreOperationReturn(JoinPoint joinPoint, Object returnValue) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[返回通知]" + methodName + "方法成功结束,返回值是:" + returnValue);
}
|
通过将 value 的值改为”declarePointCut()”,我们实现了代码的重用,易于维护,一处修改,处处生效。
9、切面的优先级
① 概念
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
使用@Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
② 实际意义
实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。