zoukankan      html  css  js  c++  java
  • Spring AOP-用代理代替繁琐逻辑

    Spring AOP

    基础概念

    AOP 是一种面向切面的编程思想,通俗来讲,这里假如我们有多个方法。

    @Component
    public class Demo {
        public void say1() {
            System.out.println("say1~~~~~~~");
        }
        
        public void say2() {
            System.out.println("say2~~~~~~~");
        }
        
        public void say3() {
            System.out.println("say3~~~~~~~");
        }
    }
    


    此时,如果我们要在每个方法执行完毕后,再输出一句话,则需要在每个方法里面都再加一个方法。

        public void say1() {
            System.out.println("say1~~~~~~~");
            System.out.println("XX say good!!!");
        }
    
        public void say2() {
            System.out.println("say2~~~~~~~");
            System.out.println("XX say good!!!");
        }
    
        public void say3() {
            System.out.println("say3~~~~~~~");
            System.out.println("XX say good!!!");
        }
    


    这种方式,就会显得代码十分的冗余且不够优雅。


    我们想一下,该实现的逻辑是在我们要在每个方法后面(切点)实现一个差不多的逻辑(切面实现),通过类似于下图所示的方式,将和主要业务无关的代码抽离出来,实现代码的解耦。


    类似于下图所示的方式:
    image.png

    Spring 实现

    首先,我们在一个 Spring Web 程序中,引入 spring-aop 的相关 jar 包。

         <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
       	</dependency>
    


    然后,我们构建一个切面类,在该类里面,我们来定义要切入的点,以及切入后该做什么。

    @Aspect
    @Component
    public class LogAspect {
        @After("execution(public * say*(..))")
        public void saveLog() {
            System.out.println("XX say good!!!");
        }
    }
    


    在这里,首先我们用 @Aspect 来声明这是一个切面,然后用 @Component 来让 Spring 容器可以扫描到该类。


    紧接着,我们定义一个方法 saveLog() ,该方法的目的是在执行完 say1() 后,可以输出一条日志,所通过的方式便是注解: @After("execution(public * say*(..))")

    有关于 aop 可以使用的注解,已经注解里配置的切点表达式,在后续再进行展开。


    最后,我们在启动类上加上 @EnableAspectJAutoProxy 即可。


    最后的实现效果,如下所示:
    image.png

    概念详解

    切面

    Aspect,要抽象出来的横跨多个地方的功能。

    连接点

    Joinpoint,定义在应用程序流程的何处插入切面进行执行。

    切入点

    Pointcut,一组连接点的集合。


    其实在 AOP 中,这些概念点并不重要,重要是理解,以及如何在实战中进行演练。

    可用切面

    • before:先执行拦截代码,如果拦截代码错误,目标代码不执行;
    • after:先执行目标代码,无论目标代码执行正确与否,都会执行拦截代码;
    • afterReturning:和after不同的是,只有目标代码正确返回,才会执行拦截代码;
    • afterThrowing:和after不同的是,只有目标代码抛出异常,才会执行拦截代码;
    • around:能完全控制代码执行,并可以在目标代码前后,任意执行拦截代码。

    切点表达式

    execution

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)  
    
    • motifiers-pattern?:修饰符,public、protect、private、*(所有类型);
    • ret-type-pattern:返回值;
    • declaring-type-pattern?:类路径匹配;
    • name-pattern:方法名,支持*,_占位符;
    • param-pattern:参数匹配,..代表所有参数类型;
    • throws-pattern?异常类型匹配


    其中?代表该项是可选项。


    另外切点表达式是可以组合的,用 || 或 && 可以进行逻辑组合。(不止是 execution,也可以跟其他的切点表达式进行组合)

    // 匹配所有方法,无法使用
    execution(* *(..))
    // 匹配所有 com.demo 包下的公有的,返回值为void的,方法名是say为前缀的,参数随意的方法
    execution(public void com.demo say*(..))
    

    @annotation

    当执行的方法上有指定的注解,则算是匹配成功。


    我认为该方式会更加的灵活些,在下面的实战演练中,我用的就是该方式,其拦截规则可以充分自定义,且可以在注解中,定义一些自己需要的值,然后在切面中进行使用。

    args

    用来匹配方法参数的。

    • args():匹配不带参数的方法;
    • args(type(String)):匹配一个参数,且类型为String的方法;
    • args(..):匹配任意参数方法;
    • args(String,..):匹配任意参数方法,但第一个参数类型是String的方法;
    • args(..,String):匹配任意参数方法,但最后一个参数类型是String的方法;

    该方法其实就是 execution 的变种形式,了解即可。

    @args

    也是用来匹配方法参数的,但是其匹配的逻辑是方法参数带有执行注解的方法。


    其他方法,如 within、this、target、@target、@within、bean 不多做介绍了,平常用的也不多,以后有兴趣,或者在实际使用中有所涉及,再进行补充。

    实战演练

    在实战中,我们通过注解的方式来进行切入,

    定义注解

    /**
     * 操作行为注解,通过该注解获取数据详情
     *
     * @author iceWang
     * @date 2020/9/10
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface OperatorAnnotation {
        String bodyType();
    
        String operatorType();
    }
    

    在这里,我们定义一个注解,后续在要拦截的方法上,加上该注解即可。


    其中 bodyType 代表我们要操作的实体类型,OperatorType 代表我们要操作的行为类型。

    业务逻辑

     @OperatorAnnotation(bodyType = LogAspect.BODY_TYPE_COMPANY, operatorType = LogAspect.OPERATOR_TYPE_DELETE)
        public String deleteCompany(String companyUniqueId) {
            Optional.of(companyMapper.deleteCompany(companyUniqueId))
                    .filter(result -> result > 0)
                    .orElseThrow(() -> new IllegalArgumentException("无法删除,请稍后再试!"));
            return companyUniqueId;
        }
    

    因为个人原因,这里我们只展示一部分代码——根据 id 删除公司,定义实体类型为 company,操作类型为删除,为后续插入日志做数据铺垫。


    切面定义

    @Aspect
    @Component
    public class LogAspect { 
        public static final String BODY_TYPE_COMPANY = "company";
        public static final String OPERATOR_TYPE_DELETE = "delete";
        
        @AfterReturning(value = "@annotation(OperatorAnnotation)", returning = "result")
        public void saveOperatorLog(JoinPoint joinPoint, Object result) {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            OperatorAnnotation operatorAnnotation = methodSignature.getMethod().getAnnotation(OperatorAnnotation.class);
            String bodyType = operatorAnnotation.bodyType();
            String operatorType = operatorAnnotation.operatorType();
    
            if (bodyType.contains(BODY_TYPE_COMPANY) && operatorType.contains(OPERATOR_TYPE_DELETE)) {
                saveOperatorLog(bodyType, operatorType, result);
                return;
            }
        }
        
            /**
         * 返回日志操作实体类
         * @param bodyType
         * @param operatorType
         * @return
         */
        private Operator getOperator(String bodyType, String operatorType) {
            return Operator.builder()
                    .bodyType(bodyType)
                    .operatorType(operatorType)
                    .createTime(LocalDateTime.now())
                    .build();
        }
    
        /**
         * 保存日志操作实体类
         * @param bodyType
         * @param operatorType
         * @param result
         */
        private void saveOperatorLog(String bodyType, String operatorType, Object result) {
            Operator operator = getOperator(bodyType, operatorType);
            operator.setOperatorUser(mdUserInfo.getPhone());
            operator.setBody(result.toString());
            operatorMapper.insert(operator);
        }
    }
    

    在切面中,首先,我们用反射的方式来获取方法上的注解,通过注解获取实际的操作实体类型和操作类型,然后根据不同的实体类型和操作类型,执行不同的方法,将日志插入数据库中。

    iceWang公众号

    文章在公众号「iceWang」第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

  • 相关阅读:
    Route the message within a requestresponse receive port
    SQL Server 表连接删除
    RoR(Ruby on Rails)学习笔记(1)
    算法导论学习笔记(1)——快排中hoarePartition的实现(问题已解决)
    RoR(Ruby on Rails)学习笔记(2)
    网络七层协议的形象说明
    Missing requirement: JDT Core patch for GroovyEclipse plugin 2.5.1.xx201106271300e36 (org.codehaus.groovy.jdt.patch.
    小技巧: 实用的一行 Linux 命令
    IE: 如何使用 F12 开发人员工具调试网页
    Grep 用法
  • 原文地址:https://www.cnblogs.com/JRookie/p/13667421.html
Copyright © 2011-2022 走看看