今天在学习通过注解来实现AOP功能发现了一个bug,就是后置通知和最终通知的顺序与xml配置下实现AOP功能的顺序有些出入
先记录下这几个通知类型并解释其意义
1、前置通知
- 在目标方法前执行
- 无论目标方法是否抛出异常,都执行,因为在执行前置通知的时候,目标方法还没有执行,还没有遇到异常
2、后置通知
- 在目标方法执行之后执行
- 当目标方法遇到异常,后置通知将不再执行
- 后置通知可以接受目标方法的返回值,但是必须注意:aop:after-returning标签,后置通知的参数的名称和配合文件中returning="var"的值是一致的
3、最终通知
- 在目标方法执行之后执行
- 无论目标方法是否抛出异常,都执行,因为相当于finally
4、异常通知
- 接受目标方法抛出的异常信息
5、环绕通知
- 如果不在环绕通知中调用ProceedingJoinPoint的proceed,目标方法不会执行
- 环绕通知可以控制目标方法的执行
言归正传,说起BUG
学习过程中,写了一个小例子:计算器实现加减乘除,我想在这些功能的前后做些事情,于是就会用到spring的aop功能。
简单看下实现的例子目录:
切面类:
package com.zcl.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Author:markusZhang
* degree of proficiency:
* Date:Create in 2020/2/25 17:29
*/
@Component
@Aspect
@Order(1)
public class LogUtils {
/**
* try{
* @Before
* method.invoke()
* @AfterReturning
* }catch(){
* @AfterThrowing
* }finally{
* @After
* }
* 5个通知注解
* @Before: 在方法执行之前运行 前置通知
* @After:在方法执行之后运行 最终通知
* @AfterReturning:在目标方法正常返回之后运行 后置通知
* @AfterThrowing:在目标方法抛出异常之后运行 异常通知
* @Around: 环绕通知
*
* 抽取可重用切入点表达式:
* 1 随便声明一个没有实现的返回void的空方法
* 2 给方法标注上@PointCut注解
*
*/
@Pointcut("execution(public int com.zcl.impl.MyMathCalculator.*(int,int))")
public void hhMyPoint(){}
//想在执行目标方法之前运行,写切入点表达式
//execution(访问权限符 返回值类型 方法签名)
@Before("hhMyPoint()")
public void logStart(JoinPoint joinPoint){
//获取到目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
System.out.println("【LogUtils】【前置通知:"+signature.getName()+"】将被调用,它的参数是【"+ Arrays.asList(args)+"】");
}
/**
* 切入点表达式的写法:
* 固定格式:execution(访问权限符 返回值类型 方法全类名(参数表))
*
* 通配符:
* *:
* 1) 匹配一个或者多个字符:
* 2) 匹配任意一个参数:
* 3) 只能匹配一层路径
* 4) 权限位置*不能表示 权限位置不写也行
* ..:
* 1) 匹配任意多个参数,任意类型参数
* 2) 匹配任意多层路径
*
* 记住两种:
* 最精确的:execution(public int com.zcl.impl.MyMathCalculator.add(int,int))
* 最模糊的:execution(* *.*(..)) 千万别写;
*
* &&:同时满足
* ||:满足任意一个表达式即可
* !:只要不是这个表达式即可
*/
@AfterReturning(value = "hhMyPoint()",returning = "result")
public void logReturn(JoinPoint joinPoint,Object result){
System.out.println("【LogUtils】【后置通知:"+joinPoint.getSignature().getName()+"】调用完成,他的结果是:"+result);
}
/**
* 细节四:我们可以在通知方法运行的时候,拿到目标方法的详细信息
* 1)只需要为通知方法的参数列表上写一个参数:
* JoinPoint joinPoint:封装了当前目标方法的详细信息
* 2)告诉spring哪个参数是用来接受异常
* throwing = "exception"
* 3)Exception exception:指定通知方法可以接收哪些异常
*
*
* ajax接受服务器数据
*
*/
@After("hhMyPoint()")
public void logEnd(JoinPoint joinPoint){
System.out.println("【Log】【最终通知:"+joinPoint.getSignature().getName()+"】最终结束");
}
/**
*Spring对通知方法的要求不严格;
* 唯一的要求就是方法的参数列表不能乱写
* 通知方法是Spring利用反射调用,每次方法调用得确定这个方法的参数表的值;
* 参数表上的每一个参数,spring都得知道是什么?
* JoinPoint:认识
* 不知道的参数一定告诉Spring这是什么
*/
@AfterThrowing(value = "hhMyPoint()",throwing = "exception")
public void logException(JoinPoint joinPoint,Exception exception) {
System.out.println("【Log】【异常通知:"+joinPoint.getSignature().getName()+"】方法计算出现异常,异常是:"+exception);
}
/**
* @Around :环绕通知:是spring中最强大的通知;
* @Around :环绕通知:动态代理
*
* try{
* @Before
* method.invoke()
* @AfterReturning
* }catch(){
* @AfterThrowing
* }finally{
* @After
* }
*
* 四合一通知就是环绕通知:
* 环绕通知中有一个参数 ProceedingJoinPoint pjp
*/
@Around("hhMyPoint()")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object proceed=null;
String methodName = pjp.getSignature().getName();
try{
//直接处理的意思 就是利用反射调用目标方法即可 就是method.invoke(args);
System.out.println("【前置通知:】【"+methodName+"方法】即将计算,它的参数是:【"+Arrays.asList(args)+"】");
proceed = pjp.proceed(args);
System.out.println("【后置通知:】【"+methodName+"方法】正常计算返回!它的结果是:"+proceed);
}catch (Exception exception){
System.out.println("【异常通知:】【"+methodName+"方法】出现异常,它的异常是:"+exception);
//为了让外界能知道这个异常,这个异常一定抛出去
throw new RuntimeException(exception);
}finally{
System.out.println("【最终通知:】【"+methodName+"方法】结束计算");
}
//反射调用后的返回值也一定返回出去
return proceed;
}
}
功能类:
package com.zcl.impl;
import org.springframework.stereotype.Service;
/**
* Author:markusZhang
* degree of proficiency:
* Date:Create in 2020/2/25 17:16
*/
@Service
public class MyMathCalculator /*implements Calculator*/ {
public int add(int i, int j) {
System.out.println("方法内部执行...");
int result = i+j;
return result;
}
public int sub(int i, int j) {
System.out.println("方法内部执行...");
int result = i-j;
return result;
}
public int mul(int i, int j) {
System.out.println("方法内部执行...");
int result = i*j;
return result;
}
public int div(int i, int j) {
System.out.println("方法内部执行...");
int result = i / j;
return result;
}
}
测试类:
package com.zcl.test;
import com.zcl.impl.MyMathCalculator;
import com.zcl.inter.Calculator;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Author:markusZhang
* degree of proficiency:
* Date:Create in 2020/2/25 17:24
*/
public class AOPTest {
/**
* 有了动态代理,日志记录可以做的非常强大,而且与业务逻辑解耦
*
* jdk默认的动态代理,如果目标对象没有实现任何接口,是无法为它创建代理对象
*/
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
/**
* 通知方法的执行顺序:
*
* try{
* @Before
* method.invoke()
* @AfterReturning
* }catch(){
* @AfterThrowing
* }finally{
* @After
* }
* 正常执行: @Before(前置通知)==>@AfterReturn(后置通知)==>@After(最终通知)
* 异常执行: @Before(前置通知)==>@AfterThrowing(异常通知)==>@After(最终通知)
*
*
* 环绕通知:是优先于普通通知执行,执行顺序:
*
* [前置通知]
* {
* try{
* 环绕前置
* 环绕执行:目标方法执行
* 环绕返回
* }catch(e){
* 环绕出现异常
* }finally{
* 环绕后置
* }
* }
* [后置/异常通知]
* [最终通知]
*
*
*/
@Test
public void test01(){
MyMathCalculator bean = ioc.getBean(MyMathCalculator.class);
bean.add(2,3);
}
}
结果:
按道理来说:
正常返回的执行顺序应该是:
环绕前置—>普通前置—>目标方法执行—>环绕后置—>环绕最终—>普通后置—>普通最终
但实际情况是这样的:
环绕通知的顺序是正常的,而普通的后置通知和最终通知顺序不对。
总结
我觉得,如果在省时省力且对顺序无要求的情况下使用注解,在要求顺序的情况下那就得使用xml配置了,这是最正确的。
目前我用的是spring4.0版本的,如果spring高级版本修复了这个bug,欢迎指正