乐趣区

关于spring:Spring-AOP使用篇熟悉使用前置通知后置通知返回通知异常通知并了解其相关特性

前言

  • 本次将会总结 5 篇对于 spring aop 的知识点,次要围绕:AOP 应用篇、AOP 原理篇、事务应用篇、事务原理篇、事务同步器应用篇 五个主题进行论述。
  • AOP 原理篇分为两个主题:

    1、源码中是如何将咱们定义的各种告诉与指标办法绑定起来的
    2、咱们的 aop 代理对象的执行程序是怎么的
  • 事务原理篇次要是以一个事务流传机制的案例来解说 spring 事务在底层的执行过程
  • 而本次总结的外围为:如何应用 Spring AOP,以及理解相干 告诉 的个性(相干的概念在这就不再累述了,大家能够参考其余文章),废话不多说,咱们间接开始吧!

一、测试案例

1.1 预览测试案例我的项目构造

  • 我的项目启动入口类 Entry.java:

    public class Entry {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectService objectService = context.getBean(ObjectService.class);
            objectService.list("hi");
        }
    
        // 配置扫描类,启动了 aop 性能
        @Configuration
        @ComponentScan("com.eugene.sumarry.aop.csdn")
        @EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)
        public static class AppConfig { }
    
        // 被这个注解标识的办法会被加强
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        public @interface AspectAnnotation {}}
  • 接口类 ObjectService.java

    public interface ObjectService {String[] list(String str);
    
        String findOne();}
  • 接口实现类 ObjectServiceImpl.java

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {return new String[] {str, "avenger", "eug"};
        }
    
        @Override
        public String findOne() {return "avengerEug";}
    }
  • 定义切面 AspectDefinition.java

    @Aspect
    @Component
    public class AspectDefinition {
    
        /**
         * 指定蕴含 @AspectAnnotation 注解的办法才会被加强
         */
        @Pointcut("@annotation(com.eugene.sumarry.aop.csdn.Entry.AspectAnnotation)")
        public void pointcutAnnotation() {}
    
        /**
         * 前置告诉:执行指标办法前 触发
         */
        @Before(value = "pointcutAnnotation()")
        public void methodBefore(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();
            System.out.println("前置告诉:办法名:" + methodName + ",参数" + Arrays.asList(joinPoint.getArgs()));
        }
    
        /**
         * 后置告诉:执行指标办法后触发
         */
        @After(value = "pointcutAnnotation()")
        public void methodAfter(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();
            System.out.println("后置告诉:办法名:" + methodName + ",参数" + Arrays.asList(joinPoint.getArgs()));
        }
    
        /**
         * 返回告诉:指标办法执行完并返回参数后触发。*/
        @AfterReturning(value = "pointcutAnnotation()", returning = "result")
        public void methodAfterReturning(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();
            System.out.println("返回告诉:办法名:" + methodName + "," +
                    "参数" + Arrays.asList(joinPoint.getArgs()) + "," +
                    "返回后果:");
            if (result instanceof String[]) {Arrays.stream((String[]) result).forEach(System.out::println);
            } else {System.out.println(result);
            }
        }
    
        /**
         * 异样告诉:指标办法抛出异样后触发
         */
        @AfterThrowing(value = "pointcutAnnotation()", throwing="ex")
        public void methodExceptionOccurred(JoinPoint joinPoint, Exception ex) {String methodName = joinPoint.getSignature().getName();
            System.out.println("异样告诉:办法名:" + methodName + ",参数" + Arrays.asList(joinPoint.getArgs()) + ",异样信息:" + ex.getMessage());
        }
    
    }

1.2 测试案例我的项目构造剖析

  • 整个我的项目构造采纳 java config 的办法来启动 spring,在 AppConfig 类中,定义了 spring 我的项目的外围性能:

    1、@Configuration 注解标识 AppConfig 是一个配置类

    2、@ComponentScan 指定了 spring 扫描的包门路为:com.eugene.sumarry.aop.csdn,这将阐明在此包下的所有 spring 辨认的注解都会被解析

    3、@EnableAspectJAutoProxy 注解示意导入了 spring aop 相干的组件,代表着 aop 性能启动了。其中,proxyTargetClass 属性指定了 aop 应用哪种代理形式来加强指标类,其规定如下所示:

    proxyTargetClass 含意
    默认为 false 应用的是 jdk 动静代理,最终代理对象和指标对象会实现同一个接口
    设置为 true 看指标类的构造,如果指标类实现了接口,这里采纳的还是 jdk 动静代理。如果指标类没有实现接口,将应用 cglib 代理。后续将在 AOP 原理篇 中证实

    将 exposeProxy 设置为 true,则示意 spring 会将代理对象裸露到线程变量 ThreadLocal 中,具体有哪些场景,能够接着往下看。

    而在切面定义类 AspectDefinition 中,咱们定义了一个切面,而切的点就是:如果 spring 中的 bean 中有办法被 @AspectAnnotation 注解标识了,那么这个 bean 的对应办法将会被加强,并且加强的逻辑蕴含:前置告诉、后置告诉、返回告诉、异样告诉 这四个局部,这里应用告诉来形容可能不太形象,大家能够把它了解成 4 个钩子函数,其对应钩子函数的触发机会及特色如下表所示(也能够参考 AspectDefinition 类中的定义):

    告诉类型(钩子函数) 触发机会 特色
    前置告诉 调用指标办法前触发 只有程序没有重大问题并且定义了前置告诉则必定会触发
    后置告诉 指标办法逻辑执行结束后触发,或指标办法外部抛异样 只有程序没有重大问题并且定义了后置告诉则必定会触发
    返回告诉 当指标办法的返回值返回后触发 异样告诉 互斥,一山不容二虎
    异样告诉 当指标办法执行过程中产生异样时触发 返回告诉 互斥,一山不容二虎

    根据上述的介绍,咱们很容易可能发现,针对于 ObjectServiceImpl 类而言,list 办法被加强了,findOne 办法没有被加强。这代表着,当执行 list 办法时,会有两种状况产生。状况一 :触发 前置告诉、后置告诉、返回告诉 状况二 前置告诉、后置告诉、异样告诉 。对于异样告诉和返回告诉互斥的起因,会在后续的AOP 原理篇 中证实。

1.3 测试 list 办法的三种调用状况

1.3.1 指标对象的 list 办法外部无异样抛出

  • 间接运行 Entry.java 类的 main 办法,失去如下后果

  • 指标办法 list 无异样往外抛时,只触发 前置告诉、后置告诉、返回告诉 ,合乎上述剖析的 第一种状况

1.3.2 指标对象的 list 办法外部抛出异样

  • 将 list 办法改成一下,改成如下模样:

    @Entry.AspectAnnotation
    @Override
    public String[] list(String str) {
        int x = 1 / 0;
        return new String[] {str, "avenger", "eug"};
    }
  • 间接运行 Entry.java 类的 main 办法,失去如下后果

  • 指标办法 list 有异样往外抛时,只触发 前置告诉、后置告诉、异样告诉 ,合乎上述剖析的 第二种状况 。应用程序也证实了 返回告诉和异样告诉 是互斥的,不能同时存在。(后续会在 AOP 原理篇 中证实

1.3.3 在指标对象的 list 办法中调用另一个被加强的办法

  • 革新下下面的 ObjectServiceImpl.java 类,将 findOne 办法也增加上 @Entry.AspectAnnotation 注解,革新后的构造如下所示:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {
            // 要在此处调用具备加强性能的 findOne 办法,如何做?return new String[] {str, "avenger", "eug"};
        }
    
        // 此处增加 @Entry.AspectAnnotation 注解
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }
  • 如上述的正文:要在 list 办法中调用具备加强性能的 findOne 办法,如何做?间接应用 this.findOne 可行吗?这里能够明确的告知:应用 this.findOne 的形式必定无奈调用到具备加强性能的 findOne 办法。这波及到 aop 生效的一些场景,要想理解这些场景,得理解 aop 的实现原理。这里后续再独自写篇 AOP 生效场景 文章进行阐明。那么有哪些形式来实现这个需要呢?
形式 1:本人依赖注入本人
  • 如下代码所示:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        // 本人依赖注入本人,此时注入的是一个代理对象
        @Autowired
        ObjectService objectService;
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {objectService.findOne();
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }
形式 2:应用 spring 上下文从新获取 bean
  • 如下代码所示:

    @Service
    public class ObjectServiceImpl implements ObjectService, ApplicationContextAware {
    
        // 应用 spring 上下文。或者网上有许多的 SpringContextUtils 工具类,能够应用它们,但原理都一样,都实现了 ApplicationContextAware 接口
        ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {applicationContext.getBean(ObjectService.class).findOne();
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }
形式 3:利用 @EnableAspectJAutoProxy 注解的 exposeProxy 属性
  • 下面有介绍到 exposeProxy 属性,将 exposeProxy 设置为 true 的话,spring 会将代理对象裸露到线程变量中。因而,在 exposeProxy 设置为 true 的状况下,持续批改 ObjectServiceImpl 类来实现加强办法 list 外部调用加强办法 findOne 的需要
  • 其代码批改如下所示:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {((ObjectService) AopContext.currentProxy()).findOne();
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }

应用AopContext.currentProxy() api 来获取 spring 裸露在线程变量中的代理对象。因为代理对象是存在于线程变量中的,即 ThreadLocal 中,因而,咱们须要保障整个调用链不会切换线程,否则将无奈获取到 spring 裸露进去的代理对象。

不论是下面的哪种形式,最终的执行后果都是这样的:

在触发 list 办法的前置告诉后,就执行到了 findOne 办法,这也间接表明了,前置告诉是执行指标办法之前触发的,因为 findOne 是在指标办法 list 外部执行的,因而指标办法 list 的逻辑并还没有执行结束,要等到 findOne 办法的逻辑执行结束后才会触发 list 办法的后置告诉、返回告诉或异样告诉。其调用流程如下图所示:

1.4 基于 1.3.3 的形式 3,测试 list 办法抛异样的几种状况

1.4.1 在调用 findOne 办法之前抛异样

  • 革新 ObjectServiceImpl.java 类:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {
            // 此处抛异样,还没调用到 findOne 办法
            int x = 1 / 0;
            ((ObjectService) AopContext.currentProxy()).findOne();
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }
  • 其运行后果为:

    很显著:由上论断能够得悉,在调用 findOne 办法之前抛异样,仅仅是影响到了 list 办法而已。这也比拟好了解,因为我还没有调用到 findOne 办法,当然不会影响到 findOne 办法。

1.4.2 在调用 findOne 办法之后抛出异样

  • 革新 ObjectServiceImpl.java 类:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {((ObjectService) AopContext.currentProxy()).findOne();
            // 调用 findOne 办法之后抛异样
            int x = 1 / 0;
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {return "avengerEug";}
    }
  • 其执行后果如下所示:

    很显著,咱们在 findOne 办法执行之后抛异样也没有影响到 findOne 办法的执行,拿咱们之前画的流程图来阐明:

如图所示,在继续执行指标办法 list 残余的逻辑 节点处抛了异样,也就是上述的 int x = 1 / 0; 这段代码,它仅仅影响到的只是 list 办法的逻辑,不会再影响到 findOne 办法,因为 findOne 办法的加强逻辑都曾经执行结束了。

  • 说句额定话:如果有对 Spring 事务的 REQUIRES_NEW 流传机制比拟理解的话,应该也会呈现这种状况。假如咱们的 list 是默认流传机制 REQUIRED,而 findOne 是REQUIRES_NEW 流传机制。那么在这种状况下,findOne 的事务会被提交,而 list 的事务会被回滚。其原理和上述的剖析统一,就是因为 findOne 是 REQUIRES_NEW 流传机制,它会额定创立一个新的事务,在 findOne 事务提交后继续执行 list 残余逻辑时抛异样,这时,findOne 的事务曾经提交了,当然不会影响到 findOne,所以此时仅仅影响到的是 list 办法。

1.4.3 在调用 findOne 办法时,在 findOne 办法外部抛出异样

  • 革新 ObjectServiceImpl.java 类:

    @Service
    public class ObjectServiceImpl implements ObjectService {
    
        @Entry.AspectAnnotation
        @Override
        public String[] list(String str) {((ObjectService) AopContext.currentProxy()).findOne();
            return new String[] {str, "avenger", "eug"};
        }
    
        @Entry.AspectAnnotation
        @Override
        public String findOne() {
            // 此处抛异样
            int x = 1 / 0;
            return "avengerEug";
        }
    }
  • 其运行后果如下所示:

    依据后果可知:它影响到了 list 和 findOne 办法,也就是说在 findOne 办法外部抛出的异样,会影响整个调用链上的办法,进而都触发了 list 和 findOne 的异样告诉。不晓得看到这里的小伙伴们心里有没有一点感觉:这十分像责任链的执行过程,如果咱们没有对每一个链上的节点做额定的异样捕获的话,只有有一个链抛出了异样,就会把异样一层一层的向上抛。 十分侥幸的是,在 Spring 的 AOP 的调用过程应用的就是责任链的设计模式。咱们将在下一篇 AOP 原理篇 来进行阐明。

  • 说句额定话:如果有对 Spring 事务的 REQUIRED 流传机制比拟理解的话,应该也会呈现外部办法抛异样最终会将整个调用链给回滚的状况。这种状况和咱们当初测试的这种用例的原理是统一的,因为两个办法应用的是同一个事务,最终异样会向上一层一层的抛,当事务管理器感知到由异样产生时,事务管理器就会做回滚操作。

二、总结

  • 通过本次的学习,咱们理解了 前置告诉、后置告诉、返回告诉、异样告诉 实际上就是一个钩子函数的回调,而针对于 返回告诉和异样告诉互斥 以及 Spring 如何确定应用哪种代理形式来生成代理对象 的疑难,我将在后续的 AOP 原理篇 证实。
  • 在 1.3 和 1.4 章节中,针对 aop 的各种调用状况、抛异常情况都做了测试,还额定提了一嘴对于事务相干的知识点,如果这块不太明确的话,后续会在 事务原理篇 具体解释。
  • 源码链接:点击查看
  • 如果你感觉我的文章有用的话,欢送点赞、珍藏和关注。
  • I’m a slow walker, but I never walk backwards
退出移动版