• Spring IoC和AOP的实现原理解析(整理版)


    1.概述

      Spring核心概念为IoC和AOP。

    2.Spring IoC底层原理

      要了解控制反转,需要先了解软件设计的一个重要思想:依赖倒置原则。

      什么事依赖倒置原则?假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。但这种设计维护性很低。

       换一种思路:我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。

      

         这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘、车身、汽车的设计了。

      这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。

      控制反转就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入。这几种概念的关系大概如下:

      

       为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码,我们先定义四个Class,车、车身、底盘、轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:

      

       这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要像上面这样改。

      由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:

      

       由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

      所以我们需要进行控制反转(IoC),即上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:

      

       这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。

       这里我们采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递,核心思路都是一样的,都是为了实现控制反转。

       那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。

      

       因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml,也可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。

      IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:

      

       这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new注入。

      而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new。

      实际项目中,有Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写一个API需要实例化这个Service,我们总不可能回去搞清楚这几百个类的构造函数吧。IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。

    3.Spring AOP底层原理

       Spring AOP是运行时织入的,那么运行时织入到底是怎么实现的呢?答案就是代理对象。代理对象又可以分为静态代理和动态代理。

    • 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
    • 动态代理:在程序运行时,运用反射机制动态创建而成。

      静态代理的每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。

      动态代理与静态代理对照的是动态代理类,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。java.lang.reflect包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。

      Spring AOP使用动态代理技术在运行期间织入增强的代码,主要有两种代理机制:基于JDK的动态代理;基于cglib的动态代理。JDK本身只提供接口的代理,而不支持类的代理。

    3.1 代理模式

      首先我们来了解代理模式的基本定义。

      代理模式的英文叫做Proxy,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。  

       

    //Subject接口
    public interface Subject {
        void request();
    }
    //目标对象RealSubject
    public class RealSubject implements Subject{
        @Override
        public void request() {
            System.out.println("real subject execute request");
        }
    }
    //Proxy代理类
    public class Proxy implements Subject{
    
        private RealSubject realSubject;
    
        public Proxy(RealSubject realSubject) {
            this.realSubject = realSubject;
        }
    
        @Override
        public void request() {
            System.out.println("before");
            try{
                realSubject.request();
            }catch (Exception e){
                System.out.println("ex:"+e.getMessage());
                throw e;
            }finally {
                System.out.println("after");
            }
        }
    }
    //Client客户端
    public class Client {
        public static void main(String[] args){
            Subject subject = new Proxy(new RealSubject());
            subject.request();
        }
    }

      这样就会输出:

    before
    real subject execute request
    after

      是不是达到和AOP一样的效果了呢?AOP将真正要执行的代码交给RealSubject来执行,然后通过proxy来对额外的代码进行织入。

    3.2 动态代理

      上面的案例就是一个典型的静态代理的案例,这样有什么缺点呢?

      如果当你要代理的方法越多时,你需要重复的逻辑就越多,假设你的目标类有100个方法,那么你的代理类就需要对这100个方法进行委托,但是又可能他们前后需要执行的逻辑时一样的,这样就会产生很多冗余。

      这样,就有个更好的动态代理的方法出现了。

    • 动态代理也分为两类:基于接口的代理和基于继承的代理
    • 两类实现的代表是:JDK代理 CGlib代理

    3.2.1 JDK代理模式

      JDK动态代理主要涉及java.lang.reflect包下的两个类:Proxy类和InvocationHandler接口。

      JDK代理实现的三个要点:

      ① 通过Java.lang.reflect.Proxy类来动态生成代理类;

      ② 代理类要实现InvocationHandler接口;

      ③ JDK代理只能基于接口进行动态代理的;

    //客户端Client
    public class Client {
        /*
        *newProxyInstance方法参数解析
        *ClassLoader loader:类加载器 
        *Class<?>[] interfaces:得到全部的接口 
        *InvocationHandler h:得到InvocationHandler接口的子类实例 
        */
        public static void main(String[] args){
            Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(),
                          new Class[]{Subject.class},new JdkProxySubject(new RealSubject()));
            subject.hello();
            subject.request();
        }
    }
    //代理类JdkProxySubject
    public class JdkProxySubject implements InvocationHandler{
    
        private RealSubject realSubject;
    
        public JdkProxySubject(RealSubject realSubject) {
            this.realSubject = realSubject;
        }
    
        /*
        *invoke方法方法参数解析
        *Object proxy:指被代理的对象。 
        *Method method:要调用的方法 
        *Object[] args:方法调用时所需要的参数 
        *InvocationHandler接口的子类可以看成代理的最终操作类。
        */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before");
            Object result = null;
            try{
                //利用反射动态的来反射方法,这就是动态代理和静态代理的区别
                result = method.invoke(realSubject,args);
            }catch (Exception e){
                System.out.println("ex:"+e.getMessage());
                throw e;
            }finally {
                System.out.println("after");
            }
            return result;
        }
    }
    //目标对象RealSubject
    public class RealSubject implements Subject{
        @Override
        public void request() {
            System.out.println("real subject execute request");
        }
    
        @Override
        public void hello() {
            System.out.println("hello");
        }
    }
    //subject接口,这个是jdk动态代理必须的前提。
    public interface Subject {
        void request();
        void hello();
    }

      输出:

    before
    hello
    after
    before
    real subject execute request
    after

      我们在subject接口中新增加了一个hello()方法,然后在RealSubject中对hello()方法进行实现,但是在代理类中,我们不需要再去为hello方法再去写一个代理方法,而是通过反射调用目标对象的方法,来动态的生成代理类。

      总结:

    • 因为利用JdkProxySubject生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有;
    • 生成的代理类的所有方法都拦截了目标类的所有的方法。而拦截器中invoke方法的内容正好就是代理类的各个方法的组成体;
    • 利用JDK代理方式必须有接口的存在。  

    3.2.2 CGLib代理模式

      CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法去技术拦截所有的父类方法的调用,并顺势织入横切逻辑。

      CGLib和JDK的原理类似,也是通过方法去反射调用目标对象的方法。

    //客户端
    public class Client {
        public static void main(String[] args){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(RealSubject.class);
            enhancer.setCallback(new DemoMethodInterceptor());
            // 此刻,realSubject不是单纯的目标类,而是增强过的目标类  
            RealSubject realSubject = (RealSubject) enhancer.create();
            realSubject.hello();
            realSubject.request()
        }
    }
    //代理对象
    public class DemoMethodInterceptor implements MethodInterceptor{
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("before in cglib");
            Object result = null;
            try{
                result = proxy.invokeSuper(obj, args);
            }catch (Exception e){
                System.out.println("get ex:"+e.getMessage());
                throw e;
            }finally {
                System.out.println("after in cglib");
            }
            return result;
        }
    }
    //目标对象RealSubject,cglib不需要定义目标类的统一接口
    public class RealSubject {
    
        public void request() {
            System.out.println("real subject execute request");
        }
    
        public void hello() {
            System.out.println("hello");
        }
    }

      输出:

    before in cglib
    hello
    after in cglib
    before in cglib
    real subject execute request
    after in cglib

    3.3 Spring两种代理方式

      若目标对象实现了接口,spring默认使用JDK的动态代理。

      优点:因为有接口,所以使系统更加松耦合;

      缺点:为每一个目标类创建接口;

      

      若目标对象没有实现任何接口,spring使用CGLib进行动态代理。

      优点:因为代理类与目标类是继承关系,所以不需要有接口的存在。

      缺点:因为没有使用接口,所以系统的耦合性没有使用JDK的动态代理好。

      若目标对象实现了接口,但是强制cglib代理,则使用cglib代理

    @SpringBootApplication
    //强制使用cglib代理
    @EnableAspectJAutoProxy(proxyTargetClass = tree)
    public class AopDemoApplication{
        public static void main(String[] args){
            SpringApplication.run(AopDemoApplication.class,args);
        }
    }

    总结

    【参考资料】

    https://blog.csdn.net/DoUUnderstand/article/details/78865385 

     https://blog.csdn.net/xqnode/article/details/102959399

  • 相关阅读:
    core 3.7.1 报错 SDK.InvalidRegionId : Can not find endpoint to access.
    定时30分钟清除缓存,重置
    文件的分割与合并
    mybatis <collection property="GoodsList" column="orderId" javaType="java.util.List" ofType="ui.model.vo.GoodsList" select="selectOrderDetail" fetchType="eager"/>
    hashMap 源码注释分析(二)
    hashMap 源码注释分析(一)
    ElasticSearch 入门
    Java 三高 ,高并发 ,高可用 。高性能
    使用MyBatis返回map对象,字段值为null时不返回或返回null,目标返回自定义的默认值...
    idea html 中文乱码,控制台中文乱码,工程文件中文乱码
  • 原文地址:https://www.cnblogs.com/swordfall/p/12880809.html
Copyright © 2020-2023  润新知