Spring5框架之AOP-ProxyFactory底层实现(五)

1. 前言

Spring aop 是Spring核心组件之一,通过aop可以简化编程。本文接下来将开始介绍spring aop入门学习

2. aop重要概念

  • (Aspect) 切面

切面是封装在类中通知与切入点的集合,在Spring中可以由 @Aspect 注解或者xml配置定义一个切面类。

  • (joinPoint) 连接点

应用程序中定义的一个点,对于Spring而言每一个执行的方法就是一个连接点。

  • (advice) 通知

在连接点可以执行特定的代码逻辑,spring定义了before、after、around通知。

  • (pointcut) 切点

切入点可以理解为一种表达式去匹配在特定的位置上执行运行特定的代码。

  • (weaing) 织入

就是在适当的位置上将切面插入到应用程序代码的过程,然后织入的时候可以完成通知的执行。spring提供的通知由如下几种:

  1. before:在某个连接点之前执行程序逻辑。
  2. after returning:连接点正常后执行的程序逻辑,需要注意的是如果程序抛出异常该通知并不会执行。
  3. after throwing :当程序出现异常时候执行的程序逻辑。
  4. after:当连接点结束执行的程序逻辑(无论是否出现异常都会执行)
  5. around:spring中最强大的通知功能,它可以完成并实现上面4种功能的实现。

3.Spring AOP 架构

Spring Aop核心架构是基于代理模式,spring中提供了两种代理模式的创建。一是使用ProxyFactory纯程序方法创建AOP代理另一种就是通过使用借助于@Aspect注解或者xml进行声明式创建代理,Spring底层可以使用两种代理方法即JDK动态代理、CGLIB动态代理。默认情况下当被通知的目标实现了接口时,Spring将会采用JDK动态代理,若目标对象没有实现任何接口将会采用CGLIB动态代理。这是因为JDK仅提供了基于接口的代理方法实现。

3.1 切面

Spring的切面由实现了Advisor类的实例表示,这个接口由两个实现PointcutAdvisor、PointcutAdvisor 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lkSebXlv-1590596132183)(/Users/codegeekgao/Library/Application Support/typora-user-images/image-20200523235247693.png)]

3.2 ProxyFactory

Spring中实现织入、代理创建过程。创建代理之前需指定被通知对象,在底层ProxyFactory将代理过程委托给DefaultAopProxyFactory,然后该类又将代理委托给 CglibAopProxy 或 JdkDynamicAopProxy 实现。

3.3 Spring通知

Spring AOP中关于通知相关通知接口的继承树如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XG2b4HwQ-1590596132185)(/Users/codegeekgao/Library/Application Support/typora-user-images/image-20200524002344814.png)]

3.3.1 创建前置通知

通知名称 接口实现 功能描述
前置通知 org.springframework.aop.MethodBeforeAdvice 可以理解为连接点(具体的执行方法)执行之前的预处理方法。前置通知可以拿到方法执行目标以及传递给目标的方法参数,但是无法控制目标方法的执行。若前置方法抛出异常目标方法执行将会被终止。

前置通知是Spring中比较有用的通知,它在目标方法执行执行执行。可以获得方法的参数并可以对方法参数进行修改,当前置通知抛出异常时可以阻止目标方法的执行。下面简单的使用案例演示前置通知的使用。

新增一个员工接口方法:

public interface EmployeeService {

    String getEmployeeName(int type);
}

@Service
public class EmployeeServiceImpl implements EmployeeService {
    @Override
    public String getEmployeeName(int type) {
      	 System.out.println("开始执行getEmployeeName方法.......");
        if(type==1) return "经理";
        if(type==0) return "普通员工";
        return null;
    }
}

xml中配置可以扫描到此接口:

   <context:component-scan base-package="com.codegeek.aop.day2"/>

新增MethodBeforeAdvice 接口实现:

public class SimpleBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("\n" + "before......advice");
        System.out.println("执行的方法是:" + method.getName());
        System.out.println("执行的参数是:" + Arrays.asList(args));
        System.out.println("执行的对象是:" + target);
    }
}

测试类测试代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:/aop/day2/*.xml"})
public class AdviceTest {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 前置通知前需要给proxyFactory设置通知以及代理对象
     */
    @Test
    public void testBefore() {
        ProxyFactory proxyFactory = new ProxyFactory();
        SimpleBeforeAdvice simpleBeforeAdvice = new SimpleBeforeAdvice();
        proxyFactory.addAdvice(simpleBeforeAdvice);
        proxyFactory.setTarget(applicationContext.getBean(EmployeeService.class));
        // 获取代理对象
        EmployeeService proxy = (EmployeeService) proxyFactory.getProxy();
        System.out.println(proxy.getEmployeeName(1));
    }
}

执行结果如下:

before......advice
执行的方法是:getEmployeeName
执行的参数是:[1]
执行的对象是:com.codegeek.aop.day2.methodbefore.EmployeeServiceImpl@31e5415e
开始执行getEmployeeName方法.......
经理

我们让前置通知抛出异常如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0DB3REtg-1590596132188)(spring-aop.assets/image-20200526164617539.png)]
在看一下测试程序执行结果我们看到目标方法并没有执行到getEmployeeName 如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vMsMWnrm-1590596132190)(spring-aop.assets/image-20200526164731520.png)]
前置通知使用场景常常用于目标方法执行之前处理一些检查、查询等预处理,若不满足指定条件则返回异常以终止目标执行,例如:当用户开始调用目标方法时,我们可以检查当前执行的用户是否有权限,若不满足操作权限则抛出无权限的异常以阻止当前用户执行目标方法。

3.3.2 创建后置返回通知

通知名称 接口实现 功能描述
后置返回通知 org.springframework.aop.AfterReturningAdvice 在连接点方法(目标方法)执行返回结果后执行,后置返回通知可以访问目标对象、以及目标方法参数、返回值。如果目标方法抛出异常则该通知将不会执行。

后置返回通知方法执行发生在目标方法执行返回结果之后,所以其不能更改目标方法的执行参数。我们复用EmployeeService以及其实现下面简单演示其使用如下:

新增后置返回通知如下所示:

public class SimpleAfterReturningAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("\n"+"当前执行对象:"+target);
        System.out.println("执行方法名称:"+method.getName());
        System.out.println("执行方法参数:"+ Arrays.asList(args));
        System.out.println("执行方法的返回值:"+returnValue);
    }
}

测试类代码:

    @Test
    public void testAfterReturning() {
        ProxyFactory proxyFactory = new ProxyFactory();
        SimpleAfterReturningAdvice simpleAfterReturningAdvice = new SimpleAfterReturningAdvice();
        proxyFactory.addAdvice(simpleAfterReturningAdvice);
        proxyFactory.setTarget(applicationContext.getBean(EmployeeService.class));
        EmployeeService proxy = (EmployeeService) proxyFactory.getProxy();
        System.out.println(proxy.getEmployeeName(0));
    }

测试程序执行结果:

开始执行getEmployeeName方法.......
当前执行对象:com.codegeek.aop.day2.methodbefore.EmployeeServiceImpl@31e5415e
执行方法名称:getEmployeeName
执行方法参数:[0]
执行方法的返回值:普通员工
普通员工

后置返回通知使用场景:对于目标方法的返回值可以做后置预处理。比如目标返回值符合预期目标,我们可以在后置返回通知进行特殊处理,例如:电商下单场景中若用户进行了下单处理并且支付成功,我们可以在后置返回通知进行添加会员积分、对接第三方物流公司发货处理等等,如果客户下单失败或者下单接口不可用后置通知中业务逻辑也不会进行执行。

3.3.3 创建环绕通知

接口名称 接口实现 功能描述
环绕通知 org.aopalliance.intercept.MethodInterceptor 环绕通知是一个强大的通知,使用其可以完成前置通知、后置通知、后置返回通知、异常通知。如有必要完全可以使用其改变方法的逻辑。

环绕通知是一个强大的通知使用它可以完成前置通知、后置返回通知、后置通知、异常通常等功能。通过环绕通知可以更改更改目标方法执行逻辑(可以更改目标方法入参、以及目标方法执行返回值)。通过环绕通知可以将其作为方法的拦截器,Spring中有很多都是基于此接口((MethodInterceptor))创建如:远程RMI方法调用、事务管理等功能如下所示:在这里插入图片描述
下面以一个简单的例子演示其使用,首先创建环绕实现如下所示:

public class SimpleAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      // StopWatch 是Spring提供计时的类
        StopWatch stopWatch = new StopWatch();
        System.out.println("\n" + "切点表达式:" + invocation.getStaticPart());
        System.out.println("开始计时类:" + invocation.getThis() + "的" + invocation.getMethod().getName() + "方法");
        stopWatch.start(invocation.getMethod().getName());
        // 执行方法
        Object proceed = invocation.proceed();
        System.out.println("\n" + "共打印了" + Arrays.stream(invocation.getArguments()).findFirst().get() + "次");
        stopWatch.stop();
        System.out.println("执行任务共耗时:" + stopWatch.getTotalTimeSeconds());
        return proceed;
    }
}

MethodInterceptor这个接口只有一个invoke方法,而这个方法入参数MethodInvocation对象,通过这个对象我们可以拿到执行的方法对象、执行方法的执行参数、以及方法的返回结果,除此之外我们甚至可以改变执行方法逻辑根据不同的情况返回不同的值。在这里插入图片描述
目标类方法代码如下:

public class MessagePrinter {

    public void print(int times) {
        for (int i = 0; i < times; i++) {
            System.out.print("*");
        }
    }
}

测试类代码如下所示:

    @Test
    public void testAround() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleAroundAdvice());
        proxyFactory.setTarget(new MessagePrinter());
        MessagePrinter proxy = (MessagePrinter) proxyFactory.getProxy();
        proxy.print(1000000);
    }
}

运行结果如下:

切点表达式:public void com.codegeek.aop.day2.around.MessagePrinter.print(int)
开始计时类:com.codegeek.aop.day2.around.MessagePrinter@5b7a7f33的print方法
**************************
共打印了1000000次
执行任务共耗时:1.258593654

3.3.4 创建异常通知

接口名称 接口实现 功能描述
异常通知 org.springframework.aop.ThrowsAdvice 该方法在目标方法执行抛出异常之后运行,与后置返回通知相反,只有目标方法执行抛异常才能运行此通知。

新增计算接口及其实现:

public interface CalculateService {

    int divide(int a,int b);
}

@Service
public class CalculateImpl implements CalculateService {
    @Override
    public int divide(int a, int b) {
        return a / b;
    }
}

创建异常通知如下:

public class SimpleThrowing implements ThrowsAdvice {

    public void afterThrowing(Exception e) {
        System.out.println("\n"+"抛出的异常是:" + e.getClass().getName());
        System.out.println("错误消息:" + e.getMessage());
        System.out.println("导致的原因是:" + e.getCause());
    }
		// 需要注意此方法的参数顺序必须是如下顺序,否则会报java.lang.IllegalArgumentException: argument type mismatch
    public void afterThrowing(Method method, Object[] args,Object target,  Exception e) {
        System.out.println("\n" + "执行的方法为:" + method.getName());
        System.out.println("抛出的异常是:" + e.getClass().getName());
        System.out.println("错误消息:" + e.getMessage());
        System.out.println("导致的原因是:" + e.getCause());
    }
}

测试类代码:

    @Test
    public void testThrowing() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleThrowing());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        proxy.divide(5,0);
    }

运行结果如下:

执行的方法为:divide
抛出的异常是:java.lang.ArithmeticException
错误消息:/ by zero
导致的原因是:null
java.lang.ArithmeticException: / by zero

异常通知使用场景:可以监控特定的类如果目标方法执行出现异常可以进行特殊处理的逻辑,例如:电商下单调用第三方支付接口出现失败,可以先生成订单然后在紧急排查原因然后在对客户账户上的金额进行扣款。

3.3.5 创建后置通知

通知名称 接口实现 功能描述
后置通知 org.springframework.aop.AfterAdvice 无论目标方法执行成功或失败,该通知都会进行执行

新增后置通知实现:

public class SimpleAfterService implements AfterAdvice {

    public void after(Method method,Object [] args,Object target) {
        System.out.println("执行的方法名:"+method.getName());
        System.out.println("执行参数:"+ Arrays.asList(args));
        System.out.println("执行的目标类:"+target.getClass().getName());
    }
}

测试方法如下:

    @Test
    public void testAfter() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleAfterService());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        proxy.divide(5, 6);
    }

当我们运行时候该测试方法抛出异常信息如下:在这里插入图片描述
我们debug上面报错的第一行代码处:
在这里插入图片描述
上面判断通知是否属于环绕通知,因为我们创建的是后置通知故程序不会进入if中,我们继续debug如下:
在这里插入图片描述
但是这个this.adapters竟没有后置通知适配器,所以下面for循环后interceptors的size依然为0,然后程序判断interceptors的size为0后就抛出异常。在这里插入图片描述
为什么这里Spring AOP没有提供对后置通知的支持呢?很奇怪待有时间再来研究Spring AOP后置通知处理这块相关知识。

4. AOP 的综合案例

日常开发中经常需要对重要功能方法进行日志输出,可以使用代码在每个方法体里输出方法参数、返回值等信息,但这无疑与代码进行了耦合。此时可以借助于Spring aop完成日志输入与输出并且可以做到日志功能与核心功能分离实现零浸入。

  1. 定义业务模型计算器接口以及其实现以演示aop功能的使用
public interface Calculator {

    int add(int a, int b);

    int sub(int a, int b);

    double divide(int a, int b);
}

@Service
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public double divide(int a, int b) {
        return a / b;
    }
}

我们在测试将之前的前置通知、后置返回通知、环绕通知、异常通知一起综合使用如下所示:

    @Test
    public void testAll() {
        ProxyFactory proxyFactory = new ProxyFactory();
        // 先添加环绕通知
        proxyFactory.addAdvice(new SimpleAroundAdvice());
        // 添加后置返回通知
        proxyFactory.addAdvice(new SimpleAfterReturningAdvice());
        // 添加前置通知
        proxyFactory.addAdvice(new SimpleBeforeAdvice());
        // 添加异常通知
        proxyFactory.addAdvice(new SimpleThrowing());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        int add = proxy.add(1, 5);
        System.out.println("计算的值:---------" + add);
    }
}

测试类输出结果如下所示:

around...before...advice...start
切点表达式:public int com.codegeek.aop.day2.throwexception.CalculateImpl.add(int,int)
开始计时类:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3的add方法

before......advice....start
执行的方法是:add
执行的参数是:[1, 5]
执行的对象是:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3
before......advice...end

afterReturning......advice...start
当前执行对象:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3
执行方法名称:add
执行方法参数:[1, 5]
执行方法的返回值:6
afterReturning......advice...end
共打印了1次
执行任务共耗时:0.011571445
around...before...advice...end
计算的值:---------6

我们可以发现执行顺序是按照如下步骤:在这里插入图片描述
我们将调整一下添加前置通知与环绕通知的顺序如下所示:

输出结果如下所示:

before......advice....start
执行的方法是:add
执行的参数是:[1, 5]
执行的对象是:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99
before......advice...end

around...before...advice...start
切点表达式:public int com.codegeek.aop.day2.throwexception.CalculateImpl.add(int,int)
开始计时类:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99的add方法

afterReturning......advice...start
当前执行对象:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99
执行方法名称:add
执行方法参数:[1, 5]
执行方法的返回值:6
afterReturning......advice...end
共打印了1次
执行任务共耗时:0.016663616
around...before...advice...end
计算的值:---------6

我们发现通知执行流程如下所示;
在这里插入图片描述

源码

以上代码均可在 codegeekgao.git 下载查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章