• 解密Spring AOP 之AspectJ与动态代理基础知识


    Spring AOP是Spring为AOP(面向切面编程)提供支持的一种框架,也是Spring核心框架之一,那么Spring是如何实现对AOP的支持的,其功能又是如何实现的,本文将尝试从Spring AOP开始往后倒推,解密Spring AOP背后的秘密

    1. 从一个简单的Spring AOP Demo开始

    public interface Fruit {
        void eat();
    }
    
    @Component
    public class Apple implements Fruit {
        
        @Override
        public void eat() {
            System.out.println("eat apple");
        }
    }
    
    @Component
    @Aspect
    public class FruitAnnotationHandler {
    
        @Pointcut("execution( * com.cy.aop.*.*(..))")
        public void eatFruit() {
    
        }
    
        @Before("eatFruit()")
        public void startEatFruit() {
            System.out.println("开始执行方法,记一个日志");
        }
    
        @After("eatFruit()")
        public void endEatFruit() {
            System.out.println("方法执行完毕,记一个日志");
        }
    }
    
    @RestController
    public class AopController {
    
        @Autowired
        Apple apple;
        
        @GetMapping("aop")
        public String testAop() {
            apple.eat();
            return "OK";
        }
    }
    
    

    执行结果

    开始执行方法,记一个日志
    eat apple
    方法执行完毕,记一个日志
    

    通过一个简单的demo我们快速的体验了一下Spring AOP的功能,即在我们执行apple.eat()方法时动态的执行了记录日志的方法。显然,如果我们不使用Spring AOP,要想实现在apple.eat()方法执行前后记录日志,那就需要我们手动的调用相关的log方法记录日志。如果在实际需求中有大量像日志记录这种与原有业务逻辑无关,但是又需要横切到原有的业务逻辑之间方法,采用直接写到业务逻辑之间方式显然并不是很合理,而Spring采用的AOP面向切面编程的方法则有利于实现业务方法与附加功能方法之间的解耦。
    那么Spring AOP又是如何实现Demo中的功能的呢?为了找到这个答案不妨让我从Spring AOP回溯到最开始的AspectJ,看看AspectJ是如何实现的。

    2. 早期的AspectJ(静态AOP)

    首先下载、安装 AspectJ的jar包(http://www.eclipse.org/aspectj/downloads.php),具体的安装,配置方式,可以自行百度,接着我们尝试用AspectJ的原生方法实现一个简单的AOP功能。

    一个简单的Apple类

    public class Apple {
    
    
        public void eat() {
            System.out.println("eat apple");
        }
    	
    	public static void main(String[] args) {
    		
    		Apple apple = new Apple();
    		apple.eat();
    		
    	}
    	
    }
    

    定义模拟记录日志的切面aspect类

    public aspect TxAspect {
    
        void around() : call(void Apple.eat()) {
            System.out.println("开始执行方法,记一个日志");
    		proceed();
            System.out.println("方法执行完毕,记一个日志");
            }
    }
    

    可以发现这个类使用的aspect并不是常见的Java的class,interface,enum等关键字,而是AspectJ独有关键字,这也说明这个TxAspect类并不是一个Java类,用Java自动的编译器是无法对其进行编译的,为此需要用AspectJ提供的编译器进行编译,接下来我们编译一下看看结果

    使用ajc -d Apple.java TxAspect.java命令进行编译,几结果会出现两个class文件,Apple.class和Txpect.class。然后使用jd-gui工具对class文件进行反编译,查看反编译的结果

    public class Apple {
      public void eat() {
        System.out.println("eat apple");
      }
      
      public static void main(String[] args) {
        Apple apple = new Apple();
        Apple apple1 = apple;
        eat_aroundBody1$advice(apple1, TxAspect.aspectOf(), null);
      }
    }
    

    可以发现反编译的结果已经与我们原本定义的Apple类不一致了,在TxAspect类中的定义的方法在编译的时候织入到了Apple类之中,直接改变了Apple类的class字节码文件,从而实现了功能的增强。由此可以看出早期的静态AOP是通过在编译期将需要插入的方法编译入目标类,改变目标类的class文件,从而在执行目标类的class文件时能够同时执行插入的方法。但是通过这种方法也就意味着每增加一次功能就要新写一个Aspect文件,并进行重新编译,这在实际使用的时候显然是不合适的,那么有没有一种不是在编译期而是在运行时动态实现AOP的方法呢。

    显然Spring AOP就是一种在运行时实现动态AOP的框架,它并不是将方法预编译到目标类中,而是在系统运行过程后在进行AOP的织入。为了实现这种功,Spring还利用些其他的技术,比如说动态代理,那么接下来我们不妨从动态代理入手继续探索。

    3. 动态AOP的实现机制

    3.1 JDK动态代理

    如果我们想实现一个动态的AOP,使其能在不修改目标类的情况下,同时能为目标类插入切面方法,看起来非常困难,但是细想之下,好像有一种设计模式能够帮助我们做到这一点,而这就是代理模式。通过代理模式,我们可以通过增强代理类来实现AOP。
    首先我们看看使用JDK的动态代理如何实现这个Demo

    
    public interface Fruit {
        void eat();
    }
    
    @Component
    public class Apple implements Fruit {
    
        @Override
        public void eat() {
            System.out.println("eat apple");
        }
    }
    
    public class FruitHandler implements InvocationHandler {
    
        private Object target;
    
        public Object bind(Object target) {
            this.target = target;
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object result = null;
            System.out.println("开始执行一个方法,记一个日志");
            result = method.invoke(target, args);
            System.out.println("方法执行完毕,记录一个日志");
            return result;
        }
    }
    
    @RestController
    public class AopController {
    
        @Autowired
        Apple apple;
    
        @GetMapping("aop")
        public String testAop() {
            FruitHandler handler = new FruitHandler();
            Fruit proxy = (Fruit)handler.bind(apple);
            proxy.eat();
            return "OK";
        }
    }
    
    

    执行结果

    开始执行一个方法,记一个日志
    eat apple
    方法执行完毕,记录一个日志
    

    通过以上的代码可以发现,在没有使用Spring AOP框架的情况下,我们通过使用JDK的动态代理机制,成功实现在执行eat()方法前后执行了插入了的记录日志的方法,虽然这种方式还是将切面方法直接写到了代理类的业务方法前后,但是至少实现了在不改变原有目标类的情况下动态插入切面方法的功能。而这也为如何实现像Spring AOP这样的动态AOP框架提供了一种解决思路,那就是通过动态生成代理类来实现动态AOP。
    Spring AOP在实现动态AOP时便是利用了JDK的动态代理机制,那么Spring AOP是如何使用动态代理来实现动态AOP的呢?这个我们之后再详细的分析,在此之前不妨再看一看另一种实现方式,即cglib。

    3.2 cglib动态字节码生成

    我们知道Java虚拟机的class文件都是符合一定的规范的,所以只要交给虚拟机的class文件符合相应的规范,那么虚拟机就运行提交的class文件。如果我们能够在程序运行期间,能够动态的将插入切面方法的类生成相应的class文件再交由虚拟机执行,那么就能实现动态AOP,而cglib(Code Generation Library)就是这样的一种code生成类库。

    使用cglib来实现这个Demo

    @Component
    public class Orange {
        
        public void eat() {
            System.out.println("eat orange");
        }
    }
    
    public class OrangeInterceptor implements MethodInterceptor {
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("方法执行前,记一个日志");
            Object obj = methodProxy.invokeSuper(o, objects);
            System.out.println("方法执行后,记录一个日志");
            return obj;
        }
    }
    
        @GetMapping("aop")
        public String testAop() {
            Enhancer en = new Enhancer();
            en.setSuperclass(Orange.class);
            en.setCallback(new OrangeInterceptor());
            Orange o = (Orange) en.create();
            o.eat();
            return "OK";
        }
        
    

    执行结果

    方法执行前,记一个日志
    eat orange
    方法执行后,记一个日志
    

    不妨修改系统变量来查看一下cglib生成的代理类

    // Application类添加配置
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\Learning\java_mistake\target\classes\com\cy\aop");
    

    可以发现cglib生成了代理类Orange$$EnhancerByCGLIB$$ada62b3b.class,我们继续反编译class文件

    public class Orange$$EnhancerByCGLIB$$ada62b3b extends Orange implements Factory {
      private boolean CGLIB$BOUND;
      
      public static Object CGLIB$FACTORY_DATA;
      
      private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
      
      private static final Callback[] CGLIB$STATIC_CALLBACKS;
      
      ...
      
      final void CGLIB$eat$0() {
        super.eat();
      }
      
      public final void eat() {
        if (this.CGLIB$CALLBACK_0 == null)
          CGLIB$BIND_CALLBACKS(this); 
        if (this.CGLIB$CALLBACK_0 != null)
          return; 
        super.eat();
      }
    

    可以发现生成cglib生成了一个代理类,这个类继承了原有的目标类并修改了原有的eat()方法,从而实现对目标类的加强。

    虽然说我们可以利用JDK动态代理和cglib动态代理来实现Spring的动态AOP,但是目前我们的Demo目前还是以一种侵入式的方式将模拟记录日志的方法插入到代理类的业务逻辑之中,这离我们想要实现的业务方法与切面方法解耦的AOP还相距甚远。在下一篇博文中,我打算在这些基础知识之上,往上探索,看看Spring AOP究竟是如何利用动态代理的机制来实现AOP的功能的。

  • 相关阅读:
    openjudge 2750
    hexo部署云服务器
    freemaker传输数据问题
    FormData在axios中的骚操作
    Docker安装与初次使用
    docker-compose实现前后端分离的自动化部署
    centos7下设置静态ip
    centos7 安装mariadb 并配置主从复制
    centos7安装solr服务
    centos7安装redis
  • 原文地址:https://www.cnblogs.com/cy1995/p/13346466.html
Copyright © 2020-2023  润新知