zoukankan      html  css  js  c++  java
  • Spring AOP解析(1)--AOP的简介及使用

    前言

    软件开发的目的是为了解决各种需求,而需求又分成业务需求和系统需求,比如有一个登录功能,那么用户输入密码之后登录就是业务需求,而在用户登录前后分别打印一行日志,这个就是系统需求;又或者用户访问系统的网页获取数据这个是业务需求,而用户每一次访问的时候,都需要进行一次用户权限校验,这个就是系统需求。可以看出业务需求是用户感知的,而系统需求是用户无感知的。业务需求和实现代码的对应关系往往是一一对应的关系,比如登录需求,就需要开发一个登录的接口;注册需求就需要开发一个注册接口。而系统需求往往是一对多的关系,比如打印用户操作日志功能,用户注册时需要打印日志,用户登录时还是需要打印日志。而如果在实现业务代码的时候,将系统需求的代码手动写入进去,那么就会导致系统需求的代码需要在每个业务代码中都需要加入,很显然就会导致很多的问题,比如维护比较困难,一旦需要改系统需求的代码,就需要将所有业务代码中的系统需求代码全部改一遍。如果我们将系统需求的实现代码抽离出来,由系统自动将系统需求的代码插入到业务代码中,很显然就解决了这个问题。而Spring的AOP思想就是这样的设计思想

    一、AOP简介

    AOP,全称是Aspect Oriented Programming,也叫做面向方面编程,或者叫面向切面编程。比如日志打印、权限校验等系统需求就像一把刀一样,横切在各个业务功能模块之上,在AOP中这把刀就叫做切面。

    1.1、AOP基本概念

    joinPoint(连接点、切入点):表示可以将横切的逻辑织入的地方,比如方法调用、方法执行、属性设置等

    pointCut(切点):通过表达式定义的一组joinPoint的集合,比如定义一个pointCut为"com.lucky.test包下的所有Service中的add方法",这样就可以定义哪些具体的joinPoint需要织入横切逻辑

    Advice(增强):横切的具体逻辑,比如日志打印,权限校验等这些系统需求就需要在业务代码上增强功能,这些具体的横切逻辑就叫做Advice

    aspect(切面):切点和增强组合一起就叫做切面,一个切面就定义了在哪些连接点需要织入什么横切逻辑

    target(目标):需要织入切面的具体目标对象,比如在UserService类的addUser方法前面织入打印日志逻辑,那么UserService这个类就是目标对象

    weaving(织入):将横切逻辑添加到目标对象的过程叫做织入

    用这些概念造句总结就是:在target的joinPoint处weaving一个或多个以Advice和pointCut组成的Aspect

    1.2、增强的类型

    增强根据执行时机和完成功能的不同分成以下几种类型

    1.2.1、前置增强(Before Advice),在joinPoint代码执行之前执行,比如可以在前置增强中进行权限校验或者参数校验等,不合法的请求就可以直接在前置增强中处理掉了。

    1.2.2、后置增强(After Advice),后置增强根据时机又分成三种类型

    1.2.2.1、返回增强(After Returning Advice)当joinPoint方法正常执行并返回结果时,返回增强才会执行

    1.2.2.2、异常增强(After throwing Advice)当joinPoint方法抛异常之后,异常增强才会执行

    1.2.2.3、最终增强(After Advice)无论joinPoint如何执行,都会执行最终增强,相当于是在finally中执行的逻辑一样

    1.2.3、环绕增强(Around Advice)在joinPoint方法执行之前和之后都会执行增强逻辑,环绕增强相当于同时实现了前置增强和后置增强的功能

    1.2.4、附加增强(Introuction)在不改变目标类的定义的情况下,为目标类附加了新的属性和行为,这就好比开发人员本来只需要干开发的工作,但是如果测试资源不足需要开发也参与测试工作,那么就需要在保持开发人员定义不变的情况下,附加测试人员的角色给开发人员身上,但是并没有改变此人是一个开发人员的本质。

    1.3、织入的分类

    根据织入的时机可以分成三种:

    编译期织入:在编译时期就将Advice织入到目标对象中

    类加载期织入:在目标对象所在类加载的时候织入Advice

    运行时动态织入:在目标对象的方法运行时,动态织入Advice

    二、AOP的使用

    2.1、AOP的使用案例

    Spring2.0开始支持@Aspect注解形式的AOP实现,使用案例如下:

    首先需要定义一个切面,通过在类上添加@Aspect可以表示当前类是一个切面,代码如下:

     1 /**
     2  * @Aspect 表示当前类是一个切面
     3  * @Component 表示当前类会在Spring容器中初始化bean
     4  * */
     5 @Aspect
     6 @Component
     7 public class TimeAspect {
     8 
     9     private final static ThreadLocal<Long> localTime = new ThreadLocal<>();
    10 
    11     /**
    12      * 前置增强:方法执行之前执行
    13      * */
    14     @Before("execution(* com.lucky.test.spring.aop..*.add*(..))")
    15     public void beforeMethod(JoinPoint joinPoint){
    16         localTime.set(System.currentTimeMillis());
    17         String methodName = joinPoint.getSignature().getName();
    18         System.out.println("前置通知:" + methodName);
    19     }
    20 
    21     /** 后置增强:方法执行之后执行*/
    22     @After("execution(* com.lucky.test.spring.aop..*.add*(..))")
    23     public void afterMethod(JoinPoint joinPoint){
    24         String methodName = joinPoint.getSignature().getName();
    25         System.out.println("后置通过:"+methodName+",耗时:" + (System.currentTimeMillis() - localTime.get()));
    26     }
    27 
    28     /**
    29      * 环绕增强:方法执行之前和之后均执行,分别在前置和后置增强的前面执行
    30      * */
    31     @Around("execution(* com.lucky.test.spring.aop..*.*(..))")
    32     public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    33         Long startTime = System.currentTimeMillis();
    34         String methodName = joinPoint.getSignature().getName();
    35         System.out.println("环绕通知开始:" + methodName);
    36         Object result = joinPoint.proceed();//执行业务具体业务逻辑
    37         System.out.println("环绕通知结束:" + methodName + ",耗时:" + (System.currentTimeMillis() - startTime));
    38         return result;
    39     }
    40 
    41     /**
    42      * 异常增强:方法抛出异常之后会执行,如果方法没有抛异常则不执行
    43      * */
    44     @AfterThrowing(value = "execution(* com.lucky.test.spring.aop..*.*(..))", throwing = "e")
    45     public void afterException(JoinPoint point, Exception e){
    46         String method = point.getSignature().getName();
    47         System.out.println("异常增强:" + method + "抛异常:" + e.getMessage());
    48     }
    49 
    50     /** 返回增强:方法返回之后会执行,如果方法没有返回值则不执行*/
    51     @AfterReturning(value = "execution(!void com.lucky.test.spring.aop..*.*())", returning = "res")
    52     public String afterReturn(JoinPoint point, Object res){
    53         String method = point.getSignature().getName();
    54         System.out.println("返回增强:" + method + "返回结果:" + res);
    55         return res.toString();
    56     }
    57 }

    本案例中在TimeAspect上添加注解@Aspect表示当前类是一个切面,添加@Component注解表示当前类需要初始化一个bean到Spring容器中

    @Before注解修饰方法表示当前方法会在连接点方法之前执行

    @After注解修饰方法表示当前方法会在连接点之后执行

    @Arount注解修饰方法表示当前方法会在连接点之前和之后均会执行

    @AfterThrowing注解修饰方法表示当前方法会在连接点抛异常之后执行,如果不抛异常则不会执行

    @AfterReturning注解修饰方法表示当前方法会在连接点返回结果时执行,如果连接点方法是void,则该注解不会生效

    通过以上注解可以相当于定义了Advice,然后还需要定义切点,通过execution表达式来定义,如案例中的注解后面的execution表达式

    * com.lucky.test.spring.aop..*.add*(..):表示拦截com.lucky.test.spring.aop包下的所有类中的以add为前缀的方法,方法参数无限制,方法返回类型无限制
    * com.lucky.test.spring.aop..*.*(..):表示拦截com.lucky.test.spring.aop包下的所有类中的所有方法,方法参数无限制,方法返回类型无限制
    !void com.lucky.test.spring.aop..*.*():表示拦截com.lucky.test.spring.aop包下的所有类中的所有方法,方法参数无限制,方法返回类型不可以是void修饰,也就是方法需要return结果

    所以TimeAspect定义的切面效果为:

    1、com.lucky.test.spring.aop包下的所有类中的add前缀的方法添加了前置增强和后置增强

    2、com.lucky.test.spring.aop包下的所有类中的所有方法添加了环绕增强和异常增强

    3、com.lucky.test.spring.aop包下的所有类中的所有有return结果的方法添加了返回增强

    测试案例,假设有两个测试接口定义和实现分别如下:

    public interface GoodsService {
    
        public void addGoods();
    
        public String getGoods();
    }
    1 public interface OrderService {
    2 
    3     public void addOrder();
    4 
    5     public void getOrder();
    6 }
     1 public class GoodsServiceImpl implements GoodsService {
     2 
     3     @Override
     4     public void addGoods() {
     5         System.out.println("处理addGoods方法业务逻辑");
     6     }
     7 
     8     @Override
     9     public String getGoods() {
    10         System.out.println("处理getGoods方法业务逻辑");
    11         return "myGoods";
    12     }
    13 }
     1 public class OrderServiceImpl implements OrderService {
     2 
     3     @Override
     4     public void addOrder() {
     5         System.out.println("处理addOrder方法");
     6         throw new RuntimeException("订单号不存在");
     7     }
     8 
     9     @Override
    10     public void getOrder() {
    11         System.out.println("处理getOrder方法");
    12     }
    13 }

    测试方法如下:

     1 public static void main(String[] args) throws Exception {
     2 
     3         ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
     4         GoodsService goodsService = context.getBean(GoodsService.class);
     5         OrderService orderService = context.getBean(OrderService.class);
     6 
     7         goodsService.addGoods();
     8         System.out.println("******************");
     9         goodsService.getGoods();
    10         System.out.println("******************");
    11         orderService.addOrder();
    12 
    13     }

    调用goodsService的addGoods方法,该方法会执行前置增强、后置增强、环绕增强

    调用goodsService的getGoods方法,该方法会执行环绕增强、返回增强

    调用orderService的addOrder方法,由于addOrder方法抛了异常,或者该方法会执行前置增强、后置增强、环绕增强(前置处理)和异常增强,而不会执行环绕增强的后置处理,因为一旦抛了异常,后面的逻辑就不会再执行了,包括环绕增强的后置处理

    执行结果如下:

     1 环绕通知开始:addGoods
     2 前置通知:addGoods
     3 处理addGoods方法业务逻辑
     4 环绕通知结束:addGoods,耗时:2
     5 后置通过:addGoods,耗时:0
     6 ******************
     7 环绕通知开始:getGoods
     8 处理getGoods方法业务逻辑
     9 环绕通知结束:getGoods,耗时:0
    10 返回增强:getGoods返回结果:myGoods
    11 ******************
    12 环绕通知开始:addOrder
    13 前置通知:addOrder
    14 处理addOrder方法
    15 后置通过:addOrder,耗时:0
    16 异常增强:addOrder抛异常:订单号不存在
    17 Exception in thread "main" java.lang.RuntimeException: 订单号不存在
    18     at com.lucky.test.spring.aop.impl.OrderServiceImpl.addOrder(OrderServiceImpl.java:15)
    19     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    20     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    21     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    22     at java.lang.reflect.Method.invoke(Method.java:498)
    23     at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    24     at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
    25     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    26     at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
    27     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    28     at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
    29     at com.lucky.test.spring.aop.TimeAspect.aroundMethod(TimeAspect.java:49)
    30     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    31     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    32     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    33     at java.lang.reflect.Method.invoke(Method.java:498)
    34     at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
    35     at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
    36     at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
    37     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    38     at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
    39     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    40     at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
    41     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    42     at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
    43     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    44     at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    45     at com.sun.proxy.$Proxy19.addOrder(Unknown Source)
    46     at com.lucky.test.spring.MainTest.main(MainTest.java:25)

    从结果可以得出以下结论:

    1、环绕增强分别在前置增强和后置增强的前面执行

    2、执行了异常执行,就不会再执行环绕增强的后置处理

    3、返回增强是在后置增强的后面执行

    4、如果同时存在环绕增强和返回增强,那么环绕增强必须返回数据,否则返回增强就获取不到结果(如案例中的环绕增强,如果ProceedingJoinPoint执行了proceed方法没有返回结果,那么返回增强获取的值就是null)

    5、各种类型增强执行顺序为:环绕增强(前置) -> 前置增强 -> 环绕增强(后置) -> 后置增强 ->返回增强,如果存在异常增强的话,那么环绕增强(后置)和返回增强都不会执行,而后置增强相当于finally,肯定会执行,且在异常增强之前执行

    2.2、AOP公开当前代理对象

    由2.1可知定义了切面之后,被拦截的方法会执行额外的增强逻辑,但是如下我们在一个连接点的方法中直接调用另外一个连接点的方法时,这两个方法会都增强吗?答案是否定的。测试案例如下:

    修改上面案例中的GoodsServiceImpl类型,代码如下:

     1 public class GoodsServiceImpl implements GoodsService {
     2 
     3     @Override
     4     public void addGoods() {
     5         System.out.println("处理addGoods方法业务逻辑");
     6         /** 内部调用getGoods方法*/
     7         getGoods();
     8     }
     9 
    10     @Override
    11     public String getGoods() {
    12         System.out.println("处理getGoods方法业务逻辑");
    13         return "test_goods";
    14     }
    15 }

    直接获取GoodsService类的bean,执行addGoods方法,而addGoods方法内部执行了getGoods方法,执行结果如下:

    1 环绕通知开始:addGoods
    2 前置通知:addGoods
    3 处理addGoods方法业务逻辑
    4 处理getGoods方法业务逻辑
    5 环绕通知结束:addGoods,耗时:2
    6 后置通过:addGoods,耗时:0

    从结果可以看出,环绕通知、前置通知后置通知都只在addGoods方法上增强了,但是没有增强getGoods方法

    原因是Spring AOP采用的是代理模式实现的,也就是说具体的横切逻辑是织入的bean的代理对象上的,当调用GoodsService的bean的addGoods方法时,实际上并不是直接调用目标对象上的addGoods方法,而是调用的代理对象的addGoods方法,只有代理对象上的连接点方法才会织入横切逻辑, 原目标对象上的方法没有做任何增强。所以通过内部调用addGoods时实际内部调用的还是目标对象的getGoods方法,而不是代理对象的getGoods方法,所以是不会执行增强逻辑的。如下图示:

    如上图示:通过AOP之后,调用目标对象的方法实际是调用的代理对象的方法,会在调用目标对象方法的前后执行增强逻辑,而如果是在目标方法内部直接调用内部方法,此时也就没有走代理对象的执行逻辑,所以不会执行横切逻辑。

    那么问题来了,如果想要在内部调用方法时也需要执行横切逻辑,有什么办法可以实现吗?很显然Spring既然留下了问题,肯定会给用户一个解决方案的。

     当目标对象方法依赖其他方法时,如果其他方法是其他目标对象的方法,那么此时我们可以注入其他目标对象的代理对象,调用其他目标对象的代理对象的方法,这也是我们经常用到的,比如GoodsServiceImpl的addGoods方法内部调用了OrderService的getOrder()方法,那么就可以注入一个OrderService对象,此时注入的是一个代理对象,所以调用代理对象的getOrder方法是可以被拦截并且织入横切逻辑的。但是如果目标对象调用的是本身的方法,就无法通过注入代理对象的方式来实现的。

    其实实际上能不能执行横切逻辑,关键就看调用的是不是代理对象的方法,如果是就可以执行横切逻辑;如果不是就不可以,那么很显然如果想要在目标对象方法中调用本身的方法也需要执行横切逻辑,那么就需要通过代理对象的方式来实现。而Spring AOP中提供了AopContext工具类,AopContext工具类可以返回当前目标对象的代理对象。使用案例如下:

    修改GoodsServiceImpl代码:

     1 public class GoodsServiceImpl implements GoodsService {
     2 
     3     @Override
     4     public void addGoods() {
     5         System.out.println("处理addGoods方法业务逻辑");
     6         /** 通过AopContext获取当前对象的代理对象*/
     7         GoodsService service = (GoodsService) AopContext.currentProxy();
     8         service.getGoods();
     9     }
    10 
    11     @Override
    12     public String getGoods() {
    13         System.out.println("处理getGoods方法业务逻辑");
    14         return "test_goods";
    15     }
    16 }

    调用AopContext.currentProxy()方法可以获取当前对象的代理对象,然后调用代理对象的getGoods(),此时执行结果如下:

     1 环绕通知开始:addGoods
     2 前置通知:addGoods
     3 处理addGoods方法业务逻辑
     4 环绕通知开始:getGoods
     5 前置通知:getGoods
     6 处理getGoods方法业务逻辑
     7 环绕通知结束:getGoods,耗时:0
     8 后置通过:getGoods,耗时:0
     9 环绕通知结束:addGoods,耗时:44700
    10 后置通过:addGoods,耗时:0

    可以看出通过代理对象访问getGoods方法之后,横切逻辑也都执行的。

    当然还有其他的方式,比如在GoodsServiceImpl中直接注入一个GoodsService,通过Spring容器注入一个当前对象的代理对象,此时调用内部方法同样也是会走代理逻辑的,如下:

     1 public class GoodsServiceImpl implements GoodsService {
     2 
     3     @Autowired
     4     private GoodsService goodsService;
     5 
     6     @Override
     7     public void addGoods() {
     8         System.out.println("处理addGoods方法业务逻辑");
     9         /** 通过AopContext获取当前对象的代理对象*/
    10         goodsService.getGoods();
    11     }
    12 
    13     @Override
    14     public String getGoods() {
    15         System.out.println("处理getGoods方法业务逻辑");
    16         return "test_goods";
    17     }
    18 }

    Tips:

    1、Spring AOP模式是不暴露出来的,也就是默认AopContext.currentProxy()方法默认返回的都是null,需要配置Aop代理对象暴露,需要在<aop:aspectj-autoproxy />标签中设置属性 expose-proxy = true,如下示:

    1 <aop:aspectj-autoproxy expose-proxy="true" />

    当expose-proxy为false时,表示AOP不会暴露代理对象,所以用户无法访问代理对象;当expose-proxy为true时,AOP才会暴露代理对象

    2、<aop:aspectj-autoproxy />标签的作用是声明自动为Spring容器中那些配置@Aspectj注解的切面的bean创建代理,织入切面。

    <aop:aspectj-autoProxy />有两个属性分别如下:

    proxy-target-class="true",表示是否使用CGLib动态代理技术织入增强;默认为false表示默认使用JDK动态代理织入增强;设置为true表示使用CGLib动态代理

    expose-proxy="true":表示是否暴露代理对象,默认为false表示不暴露,那么调用AopContext.currentProxy()方法会返回null;设置为true表示暴露代理,可以通过AopContext.currentProxy()方法获取当前对象的代理对象

    3、由于Spring AOP是动态织入的,如果是像AspectJ直接在编译期间就将横切逻辑织入到目标对象,那么无论如何调用目标对象的方法,均可以执行横切逻辑了

    4、直接通过依赖注入当前bean自己,那么Spring容器注入进来的也是代理对象,此时在通过代理对象直接调用内部方法也可以走横切逻辑。

    下一篇:Spring AOP(2)--AOP实现原理及源码解析

  • 相关阅读:
    如何找出数组中重复次数最多的数
    如何计算两个有序整型数组的交集
    如何分别使用递归与非递归实现二分查找算法
    如何用递归算法判断一个数组是否是递增
    如何用一个for循环打印出一个二维数组
    如何用递归实现数组求和
    ElasticSearch安装和head插件安装
    SpringBoot全局异常处理方式
    Redis高级命令操作大全--推荐
    Mysql中FIND_IN_SET和REPLACE函数简介
  • 原文地址:https://www.cnblogs.com/jackion5/p/13358657.html
Copyright © 2011-2022 走看看