依赖注入(Dependency Injection)框架是如何实现的?

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,实用技术文章及时了解依赖注入(Dependency Injection)框架是如何实现的?

来源:blog.csdn.net/fuzhongmin05/

article/details/109572151

当创建对象是一件庞大又复杂的大工程的时候,我们一般会选择使用工厂模式,来封装对象复杂的创建过程,将对象的创建和使用分离,让代码更加清晰。那什么样的对象创建可以算得上是庞大又复杂呢?一共有两种情况,一种是创建过程涉及复杂的if-else分支判断,另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。

典型的能够将创建对象这样的大过程封装好的就是依赖注入框架,或者叫依赖注入容器(Dependency Injection Container),简称DI容器,Spring本身就是一个DI容器。

依赖注入和控制反转含义相同,它们是从两个角度描述的同一个概念。当某个Java实例需要另一个Java实例时,传统的方法是由调用者创建被调用者的实例(例如,使用new关键字获得被调用者实例),而使用Spring框架后,被调用者的实例不再由调用者创建,而是由Spring容器创建,这称为控制反转。Spring容器在创建被调用者的实例时,会自动将调用者需要的对象实例注入给调用者,这样,调用者通过Spring容器获得被调用者实例,这称为依赖注入。

工厂模式和DI容器的区别

实际上,DI容器底层最基本的设计思路就是基于工厂模式的。DI容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。

DI容器相对于工厂模式的来说,它处理的是更大的对象创建工程。工厂模式中一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而DI容器负责的是整个应用中所有类对象的创建。

除此之外,DI容器负责的事情要比单纯的工厂模式要多。比如,它还包括配置的解析、对象生命周期的管理。接下来,我们详细剖析一个简单的DI 容器应该包含哪些核心功能。

DI容器的核心功能

总结一下,一个简单的DI容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。

首先,我们来看配置解析。

在工厂模式中,工厂类要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,DI容器事先并不知道应用会创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。所以,我们需要通过某种形式,让应用层开发者告知DI容器要创建哪些对象。这种形式就是配置。

我们将需要由DI容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等)放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。

下面是一个典型的Spring容器的配置文件。Spring容器读取这个配置文件,解析出要创建的两个对象:rateLimiter和redisCounter,并且得到两者的依赖关系:rateLimiter依赖redisCounter。

public class RateLimiter {

  private RedisCounter redisCounter;
  
  public RateLimiter(RedisCounter redisCounter) {
    this.redisCounter = redisCounter;
  }
  public void test() {
    System.out.println("Hello World!");
  }
  //...
}

public class RedisCounter {
  private String ipAddress;
  private int port;
  public RedisCounter(String ipAddress, int port) {
    this.ipAddress = ipAddress;
    this.port = port;
  }
  //...
}

配置文件beans.xml:

<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</beans>

其次,我们再来看对象创建。

在DI容器中,如果我们给每个类都对应创建一个工厂类,那项目中类的个数会成倍增加,这会增加代码的维护成本。要解决这个问题并不难。我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如BeansFactory。

你可能会说,如果要创建的类对象非常多,BeansFactory中的代码会不会线性膨胀(代码量跟创建对象的个数成正比)呢?实际上并不会。DI容器使用“反射”机制在程序运行的过程中动态地加载类并创建对象,不需要感知应用层要创建哪些对象,自然也就不需要事先在代码中写死要创建哪些对象了。所以,不管是创建1个对象还是10个对象,BeansFactory工厂类代码都是一样的。

最后,我们来看对象的生命周期管理。

简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是所谓的单例对象。在 Spring框架中,我们可以通过配置scope属性,来区分这两种不同类型的对象。scope=prototype表示返回新创建的对象,scope=singleton表示返回单例对象。

除此之外,我们还可以配置对象是否支持懒加载。如果lazy-init=true,对象只有在真正被使用到的时候才被被创建;如果lazy-init=false,对象在应用启动的时候就事先创建好。不仅如此,我们还可以配置对象的init-methoddestroy-method方法,DI容器在创建好对象之后,会主动调用 init-method属性指定的方法来初始化对象。在对象被最终销毁之前,DI容器会主动调用destroy-method属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。

实现一个简单的DI容器

实际上,用Java来实现一个简单的DI容器,核心逻辑只需要包括这样两个部分:配置文件解析、根据配置文件解析的结果通过“反射”语法来创建对象。

最小原型设计

我们只实现一个DI容器的最小原型。像Spring框架这样的DI容器,它支持的配置格式非常灵活和复杂。为了简化代码实现,重点理解原理,在最小原型中,我们只支持下面使用xml配置文件的配置语法。

<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter" scope="singleton" lazy-init="true">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</bean>

最小原型的使用方式跟Spring框架非常类似,示例代码如下所示:

public class Demo {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
    RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
    rateLimiter.test();
    //...
  }
}

提供执行入口

面向对象设计的最后一步是:组装类并提供执行入口。在这里,执行入口就是一组暴露给外部使用的接口和类。通过刚刚的最小原型使用示例代码,我们可以看出,执行入口主要包含两部分:ApplicationContextClassPathXmlApplicationContext。其中,ApplicationContext是接口,ClassPathXmlApplicationContext是接口的实现类。两个类具体实现如下所示:

public interface ApplicationContext {
  //根据id获取bean实例
  Object getBean(String beanId);
}

public class ClassPathXmlApplicationContext implements ApplicationContext {
 
  //bean工厂,负责创建bean
  private BeansFactory beansFactory;
  //配置文件解析器,负载解析配置文件
  private BeanConfigParser beanConfigParser;

  public ClassPathXmlApplicationContext(String configLocation) {
    this.beansFactory = new BeansFactory();
    this.beanConfigParser = new XmlBeanConfigParser();
    loadBeanDefinitions(configLocation);
  }

  private void loadBeanDefinitions(String configLocation) {
    InputStream in = null;
    try {
      in = this.getClass().getResourceAsStream("/" + configLocation);
      if (in == null) {
        throw new RuntimeException("Can not find config file: " + configLocation);
      }
      //将配置解析成多个BeanDefinition对象
      List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
      beansFactory.addBeanDefinitions(beanDefinitions);
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
          // TODO: log error
        }
      }
    }
  }

  @Override
  public Object getBean(String beanId) {
    return beansFactory.getBean(beanId);
  }
}

从上面的代码中,我们可以看出,ClassPathXmlApplicationContext负责组装BeansFactory和BeanConfigParser两个类,串联执行流程:从classpath中加载XML格式的配置文件,通过BeanConfigParser解析为统一的BeanDefinition格式,然后,BeansFactory根据BeanDefinition来创建对象。

配置文件解析

配置文件解析主要包含BeanConfigParser接口和XmlBeanConfigParser实现类,接口定义行为,实现类负责将XML配置文件解析为BeanDefinition结构,以便BeansFactory根据这个结构来创建对象。如果有其他格式的配置文件,例如JSON,还可以再写一个JsonBeanConfigParser实现类。

配置文件的解析比较繁琐,不是我们研究的重点,这里我只给出两个类的大致设计思路,具体的代码框架如下所示:

public interface BeanConfigParser {
  List<BeanDefinition> parse(InputStream inputStream);
}

public class XmlBeanConfigParser implements BeanConfigParser{

    @Override
    public List<BeanDefinition> parse(InputStream inputStream) {
        List beanDefinitions = new ArrayList<>();

        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document doc = documentBuilder.parse(inputStream);

            //optional, but recommended
            //read this - http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
            doc.getDocumentElement().normalize();

            NodeList beanList = doc.getElementsByTagName("bean");
            //下面开始进行标签解析,将scopesingletonlazy-init等标签解析成beanDefinition的属性
            for (int i = 0; i < beanList.getLength(); i++) {
                Node node = beanList.item(i);
                if (node.getNodeType() != Node.ELEMENT_NODE) continue;

                Element element = (Element) node;
                BeanDefinition beanDefinition = new BeanDefinition(
                        element.getAttribute("id"),
                        element.getAttribute("class")
                );
                if (element.getAttribute("scope").equals("singleton")) {
                    beanDefinition.setScope(BeanDefinition.Scope.SINGLETON);
                }
                if (element.getAttribute("lazy-init").equals("true")) {
                    beanDefinition.setLazyInit(true);
                }
                loadConstructorArgs(
                        element.getElementsByTagName("constructor-arg"),
                        beanDefinition
                );

                beanDefinitions.add(beanDefinition);
             }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return beanDefinitions;
    }

    public void loadConstructorArgs(NodeList nodes, BeanDefinition beanDefinition) {
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) continue;
            Element element = (Element) node;

            BeanDefinition.ConstructorArg constructorArg = null;
            if (!element.getAttribute("type").isEmpty()) {
                constructorArg = new BeanDefinition.ConstructorArg.Builder()
                        .setArg(element.getAttribute("value"))
                        .setType(String.class)
                        .build()
;
            }

            if (!element.getAttribute("ref").isEmpty()) {
                constructorArg = new BeanDefinition.ConstructorArg.Builder()
                        .setRef(true)
                        .setArg(element.getAttribute("ref"))
                        .build();
            }

            beanDefinition.addConstructorArg(constructorArg);
        }
    }
}

想要通过配置文件来定义清楚一个bean,就需要约定一个语法,用语法中的属性去描述它,将bean的定义同样抽象成BeanDefinition类,一个BeanDefinition实例能够代表一个唯一的bean类型。为了描述一个类,我们将BeanDefinition类定义如下。

public class BeanDefinition {
    private String id;
    private String className;
    private List<ConstructorArg> constructorArgs = new ArrayList();
    private Scope scope = Scope.PROTOTYPE;
    private boolean lazyInit = false;

    public BeanDefinition(String id, String className) {
        this.id = id;
        this.className = className;
    }

    // 省略必要的getter && setter
    public Boolean isSingleton() {
        return scope.equals(Scope.SINGLETON);
    }
    public void addConstructorArg(ConstructorArg constructorArg) {
        this.constructorArgs.add(constructorArg);
    }

    // Static Below bean的生命周期杖举类
    public static enum Scope {
        SINGLETON,
        PROTOTYPE
    }

    public static class ConstructorArg {
        private boolean isRef;
        private Class type;
        private Object arg;

        private ConstructorArg(Builder builder) {
            this.isRef = builder.getIsRef();
            this.type = builder.getType();
            this.arg = builder.getArg();
        }

        public static class Builder {
            private boolean isRef = false;
            private Class type;
            private Object arg;

            //Getter
            public boolean getIsRef() {
                return isRef;
            }

            // 省略必要的getter && setter
            public ConstructorArg build() {
                if (this.isRef) {
                    if (this.type != null) {
                        throw new IllegalArgumentException("当参数为引用类型时,无需设置 type 参数");
                    }
                } else {
                    if (this.type == null || this.arg == null) {
                        throw new IllegalArgumentException("当参数为非引用类型时,type 和 arg 参数必填");
                    }
                }
                return new ConstructorArg(this);
            }  
        }
    }
}

值得注意的是,在Spring的源码中,有一个BeanDefinition接口,它是配置文件<bean> 标签在 Spring 容器中的内部表现形式,<bean>标签拥有的属性也会对应于BeanDefinition中的属性,它们是一一对应的,即一个<bean>标签对应于一个BeanDefinition实例。

BeanDefinition接口在Spring中共有3个实现类,配置文件中的bean可以有父bean和子bean,父bean用RootBeanDefinition来表示,子bean用 ChildBeanDefinition来表示,而GenericBeanDefinition是一个通用的BeanDefinition。

另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!

核心工厂类的设计

最后,我们来看,BeansFactory是如何设计和实现的。这也是我们这个DI 容器最核心的一个类了。它负责根据从配置文件解析得到的BeanDefinition来创建对象。

如果对象的scope属性是singleton,那对象创建之后会缓存在singletonObjects这样一个map中,下次再请求此对象的时候,直接从map中取出返回,不需要重新创建。如果对象的scope属性是prototype,那每次请求对象,BeansFactory都会创建一个新的对象返回。

实际上,BeansFactory创建对象用到的主要技术点就是Java中的反射语法:一种动态加载类和创建对象的机制。我们知道,JVM在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让JVM帮我们自动完成了,我们需要利用Java提供的反射语法自己去编写代码。

搞清楚了反射的原理,BeansFactory的代码就不难看懂了。具体代码实现如下所示:

public class BeansFactory {
    //缓存单例bean,key是beanId,value是对象
    private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
    //缓存beanDefinition,key是beanDefinitionId,value是BeanDefinition对象
    private ConcurrentHashMap<String, BeanDefinition> beanDefinations = new ConcurrentHashMap<>();

    public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
        for (BeanDefinition beanDefinition: beanDefinitionList) {
            this.beanDefinations.putIfAbsent(beanDefinition.getId(), beanDefinition);
        }

        for (BeanDefinition beanDefinition : beanDefinitionList) {
            if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {
                createBean(beanDefinition);
            }
        }
    }

    public Object getBean(String beanId) {
        BeanDefinition beanDefinition = beanDefinations.get(beanId);
        //找不到beanDefinition对象,直接抛异常
        if (beanDefinition == null) {
            throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
        }
        //根据beanDefinition来创建bean
        return createBean(beanDefinition);
    }

    @VisibleForTesting
    protected Object createBean(BeanDefinition beanDefinition) {
        if (beanDefinition.isSingleton() && singletonObjects.containsKey(beanDefinition.getId())) {
            return singletonObjects.get(beanDefinition.getId());
        }

        Object bean = null;
        try {
            // TODO: 此处Class的路径是如何计算的
            Class beanClass = Class.forName(beanDefinition.getClassName());
            List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs();
            if (args.isEmpty()) {
                //这里反射调用,创建实例
                bean = beanClass.newInstance();
            } else {
                Class[] argClasses = new Class[args.size()];
                Object[] argObjects = new Object[args.size()];
                for (int i = 0; i < args.size(); i++) {
                    BeanDefinition.ConstructorArg arg = args.get(i);
                    if (!arg.isRef()) {
                        argClasses[i] = arg.getType();
                        argObjects[i] = arg.getArg();
                    } else {
                     //拿到当前bean依赖的bean的BeanDefinition
                        BeanDefinition refBeanDefinition = beanDefinations.get(arg.getArg());
                        if (refBeanDefinition == null) {
                            throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
                        }
                        //递归调用,创建依赖bean的实例
                        argObjects[i] = createBean(refBeanDefinition);
                        argClasses[i] = argObjects[i].getClass();
                    }
                }

                bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }

        if (bean != null && beanDefinition.isSingleton()) {
            singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
            return singletonObjects.get(beanDefinition.getId());
        }

        return bean;
    }
}

总结

DI容器在一些软件开发中已经成为了标配,比如Spring IOC、Google Guice。但是,大部分人可能只是把它当作一个黑盒子来使用,并未真正去了解它的底层是如何实现的。

当然,如果只是做一些简单的小项目,简单会用就足够了,但是,如果我们面对的是非常复杂的系统,当系统出现问题的时候,对底层原理的掌握程度,决定了我们排查问题的能力,直接影响到我们排查问题的效率。一个简单的DI容器的实现原理,其核心逻辑主要包括:配置文件解析,以及根据配置文件通过“反射”语法来创建对象。

其中,创建对象的过程就应用到了工厂模式。DI容器相当于一个大的工厂类,负责在程序启动时根据配置参数将所需要的对象都创建好,当程序需要时,直接从容器中获取某类对象;工厂类只负责创建某一个或某一组类对象,而DI容器是创建整个应用所有需要的类对象。对象创建、组装、管理完全有DI容器来负责,跟具体业务代码解耦,让程序员聚焦在业务代码的开发上。

依赖注入(Dependency Injection)框架是如何实现的?
【练手项目】基于SpringBoot的ERP系统,自带进销存+财务+生产功能
分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!

依赖注入(Dependency Injection)框架是如何实现的?
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

原文始发于微信公众号(Java知音):依赖注入(Dependency Injection)框架是如何实现的?