Spring:基于注解完成aop和相关的细节

1.Aop的底层原理之——动态代理详解

2.基于注解配置各个切面

3.切面执行顺序详解

4.多切面环境下切面之间的优先级关系

那么下面就进入正题吧!

1.java的动态代理
动态代理能够在不该便一个类方法源码的情况下,对这个方法进行增强
要对一个类的方法进行增强,那么这个类一定要实现一个接口!!!
至于原因,我么可以在下面的具体实现代码中体会。

声明一个计算器的接口,里面定义了加和减两个方法的规范。`

public interface OldCalcutor {

    public int add(int i,int j);

    public int sub(int i,int j);
}

然后我们需要定义一个类实现该接口。

public class Calculator implements OldCalcutor {

    public int add(int i,int j){
        System.out.println("方法内部执行了");

        return i+j;
    }

    public int sub(int i,int j){
        System.out.println("方法内部执行了");
        return i-j;
    }
}

准备工作就算完成了,我们可以进行动态代理代码的编写了。
首先打开JDK API对动态代理的介绍。

在这里插入图片描述
代码如下:

public class MyProxy {

    public static OldCalcutor getProxyCalculator(final OldCalcutor calculator){


        ClassLoader classLoader = calculator.getClass().getClassLoader();
        Class<?>[] interfaces = calculator.getClass().getInterfaces();

        InvocationHandler invocationHandler=new InvocationHandler() {
            @Override
            /**
             *  1.proxy:代理对象,任何时候都不轻易去动这个对象,这是专门给JDK使用的
             *  2.method:需要扩展的方法
             *  3.args:调用方法所需要的参数
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = method.invoke(calculator, args);
                return result;

            }
        };


        System.out.println(calculator.getClass());
        Object o = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        return (OldCalcutor) o;
    }
}
  • 我们需要调用方法先取得目标类的类加载器和实现的接口信息(从这里我们就可以看出,动态代理需要接口的信息,如果我们没有实现接口,那么肯定会有异常!!)
  • 核心是InvocationHandler这个接口,里面定义了一个invoke方法,方法有三个参数。
    参数1(proxy):这个参数就是我们目标类的实例对象,任何时候我们都不要轻易去改变这个对象的属性。
    参数2(method):这个对象代表的就是增强的方法对象。
    参数3(args):这是一个可变长参数,在这里面保存的是方法调用时传递的参数。

我们只需要实现这个接口的方法,在里面自定义需要完成的事务代码,就可以实现增强了。

注意事项:

上述方法的返回值必须写成OldCalcutor,而不能直接写类类型。
我们打印出通过动态代理得到的代理对象的信息:如图:

 @Test
    public void test1(){

       OldCalcutor calcutor=new Calculator();

        OldCalcutor proxyCalculator = MyProxy.getProxyCalculator(calcutor);

        System.out.println(proxyCalculator.getClass());


    }


可以看到这是一个代理对象。它与Calculator类之间的唯一关系是它们实现了一个共同的接口–OldCalcutor。因此返回值类型写成目标类类型是会发生类型的转化错误。

2.基于注解配置各个切面
在此之前,先生成一个切面类。

@Aspect
@Controller
public class MyAop {
	@Before(value = "execution(public int com.psf.bean.Calculator.*(int,int))")
    public void logStart(){
        System.out.println("【MyAop前置】");
    }

	 @AfterReturning(value = "execution(public int com.psf.bean.Calculator.*(int,int))")
        public void logReturn(){
        Object[] args = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        System.out.println("【MyAop返回】");
    }

   @AfterThrowing(value = "execution(public int com.psf.bean.Calculator.*(int,int))")
    public void logException(){
        System.out.println("【MyAop异常】");
    }
     @After("execution(public int com.psf.bean.Calculator.*(int,int))")
    public void logEnd(){
        System.out.println("【MyAop后置】");
    }
  1. @Before注解表示这个通知将在切点执行
  2. @After注解表示这个通知将在切点之后执行
  3. @AfterReturning表示这个方法将在切点正常执行后执行
  4. @AfterThrowing表示这个通知方法将在出现异常时执行

值得说明的一点是:这里我们不需要再实现一个接口,导入了CGLIbjar包之后,CGlib可以帮助我们完成工作。
开启测试,运行顺序。

 public void test5(){
        String config="applicationContext.xml";

        ApplicationContext ac=new ClassPathXmlApplicationContext(config);

        Calculator calculator = ac.getBean("calculator", Calculator.class);

        calculator.add(1, 2);

    }

在这里插入图片描述
可以看到没有发生异常时,运行顺序是:前置通知->方法执行->后置通知->返回通知。
接下来我们故意在方法内部抛出异常,查看运行的顺序。
在这里插入图片描述
可以看到运行顺序是:前置通知->方法执行->后置通知->异常通知。
因为这里出现了异常,方法没有正常返回,所以不执行返回通知。

上述的四个通知都是用的同一个切入点表达式,那么我们当然就可以把它提取出来复用。

 @Pointcut("execution(public int com.psf.bean.Calculator.*(int,int))")
    public void myExecution(){

    }

只要在方法上面加上@PointCut注解,本身方法不需要添加任何的实现。然后直接调用该方法即可。

@Before("myExecution()")
    public void logStart(){
        System.out.println("【MyAop前置】");
    }

这里实现的方法与上面直接进行动态代理不同,我们不能够拿到运行时方法的信息(方法名、返回值、参数等等)。

我们只需要在通知方法里面里加上一个JoinPoint类的对象就可以获取切点方法的签名,参数等等。在通知方法任意添加一个Object类型的参数,再在注解里指定returning属性的值为参数名,那么这个参数就可以获取到方法的返回值。

注解的类似属性还有:
Exception:指定抛出异常时的异常对象名称。
argsNames:指定参数的名称。

3.切面执行顺序详解
还有一个很重要的切面配置没有提到,那就是@Around,顾名思义,这个切面可以把目标切点的方法环绕,是Spring aop最强大的一个配置。接下来就细聊它的用法。先上代码。

@Around("myExecution()")
    public Object myAround(ProceedingJoinPoint pdj){
        //ProceedingJoinPoint接口实现了JoinPoint接口 是它的子接口
        //通过此接口我们可以得到手动调用代理方法
        Object proceed=null;

        try {
            System.out.println("前置通知");
            proceed = pdj.proceed(pdj.getArgs()); //调用未经加强的方法得到返回值
            System.out.println("后置通知");
        } catch (Throwable throwable) {
            System.out.println("异常通知");
          throw new RuntimeException(throwable);
        } finally {
            System.out.println("最终通知");
        }
        return proceed;
    }
}
  1. ProceedingJoinPoint是JoinPoint的子接口,它定义了更多的接口方法,其中就包括对原生方法的调用。
  2. @Around注解实际上就是我们上述提到的动态代理,在手动调用原生方法前后插入自己想要增强的代码。

那么加入Around注解之后切面方法的执行顺序是什么样的呢?
测试便知。

没有异常时:
在这里插入图片描述
顺序是:环绕前置通知->前置通知->调用原生方法->环绕后置通知->环绕返回通知->后置通知->返回通知

为什么会出现这样的顺序呢?其实我们之前提到过的@Around注解实际上就是一个动态代理。普通通知的后置通知,返回通知,异常通知等等,都需要等到原生方法调用之后才能发挥作用,而我们用@Around注解对原生的方法进行了加强,相当于把环绕通知嵌入进了原生方法中。所以才会出现这种执行顺序。而环绕前置通知和普通前置通知之间的先后顺序不一定,我们通过注解方式配置时一定是环绕前置先执行,但是我们利用xml配置来实现时,执行顺序是它们的配置顺序决定的。

出现异常时的执行顺序:

在这里插入图片描述
只是把返回通知换成了异常通知。

4.当我们配置了多个切面类,对同一个切点进行增强时,多个切面方法之间的运行顺序是怎样的呢?

这里我们用一个模型就可以说明其中的道理:

在这里插入图片描述
试想这样一个场景:
当我们从这个图形外引一条直线穿过这个图形,最先接触到的是切面一的前一部分,然后经过切面二整个部分,然后才走完切面一的后面部分。

所以切面的顺序就是:
1.内外部都没有环绕通知时:

  • 外部前置通知->内部前置通知->方法执行->内部后置通知->内部返回通知->外部后置通知->外部返回通知
  • =========================
  • 2.外部添加了环绕通知时:
  • 外部环绕前置通知->外部前置通知->内部前置通知->方法执行->内部后置通知->内部返回通知->外部环绕后置通知->外部环绕返回通知
  • ->外部后置通知->外部返回通知
  • ============================
  • 3.内部添加了环绕通知时:
  • 外部前置通知->内部环绕前置通知->内部前置通知->方法执行->内部环绕后置通知->内部环绕返回通知->内部后置通知->内部返回通知
  • ->外部后置通知->外部返回通知。

那么如何确定切面之间的优先级呢?其实没有我们想象中那么复杂
当我们没有配置时,按照切面类的名称字典序排序,字典序最小的排在外围。我们也可以使用@Order注解来配置优先级,数字越小优先级越高

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值