天天看點

如何設計實作一個DI架構

設計模式之美 - 45

簡單的 DI 容器的實作原理,其核心邏輯主要包括:

  • 配置檔案解析
  • 以及根據配置檔案通過“反射”文法來建立對象

其中,建立對象的過程就應用到了我們在學的工廠模式。對象建立、組裝、管理完全有 DI 容器來負責,跟具體業務代碼解耦,讓程式員聚焦在業務代碼的開發上。

目錄

一、工廠模式和 DI 容器有何差別?

二、DI 容器的核心功能有哪些?

1、配置解析

2、對象建立

3、對象生命周期管理

三、如何實作一個簡單的DI容器

1、最小原型設計

2、提供執行入口

3、配置檔案解析

4、核心工廠類設計

一、工廠模式和 DI 容器有何差別?

DI 容器底層最基本的設計思路就是基于工廠模式的。

DI 容器相當于一個大的工廠類,負責在程式啟動的時候,根據配置(要建立哪些類對象,每個類對象的建立需要依賴哪些其他類對象)事先建立好對象。當應用程式需要使用某個類對象的時候,直接從容器中擷取即可。正是因為它持有一堆對象,是以這個架構才被稱為“容器”。

二、DI 容器的核心功能有哪些?

簡單的DI容器的核心功能一般有三個:配置解析、對象建立 和 對象生命周期管理

1、配置解析

架構代碼跟應用代碼應該是高度解耦的,DI 容器事先并不知道應用會建立哪些對象,我們需要通過一種形式,讓應用告知 DI 容器要建立哪些對象。這種形式就是配置。

我們将需要由 DI 容器來建立的類對象和建立類對象的必要資訊(使用哪個構造函數以及對應的構造函數參數都是什麼等等),放到配置檔案中。容器讀取配置檔案,根據配置檔案提供的資訊來建立對象。

典型的Spring容器的配置檔案:

Spring 容器讀取這個配置檔案,解析出要建立的兩個對象:rateLimiter 和 redisCounter,并且得到兩者的依賴關系:rateLimiter 依賴 redisCounter。

<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>
           

2、對象建立

在 DI 容器中,如果我們給每個類都對應建立一個工廠類,那項目中類的個數會成倍增加,這會增加代碼的維護成本。要解決這個問題并不難。我們隻需要将所有類對象的建立都放到一個工廠類中完成就可以了,比如 BeansFactory。

問題:如果要建立的類對象非常多,BeansFactory 中的代碼會不會線性膨脹(代碼量跟建立對象的個數成正比)呢?

實際上并不會。“反射”這種機制,它能在程式運作的過程中,動态地加載類、建立對象,不需要事先在代碼中寫死要建立哪些對象。是以,不管是建立一個對象還是十個對象,BeansFactory 工廠類代碼都是一樣的。

3、對象生命周期管理

對象建立方式:通過配置scope屬性

  • scope=prototype 表示傳回新建立的對象
  • scope=singleton 表示傳回單例對象。

對象建立時間:

  • lazy-init=true,對象在真正被使用到的時候(比如:BeansFactory.getBean(“userService”))才被被建立;
  • lazy-init=false,對象在應用啟動的時候就事先建立好。

對象的初始化和銷毀:

  • init-method=loadProperties(),DI 容器在建立好對象之後,會主動調用 init-method 屬性指定的方法來初始化對象。
  • destroy-method=updateConfigFile(),在對象被最終銷毀之前,DI 容器會主動調用 destroy-method 屬性指定的方法來做一些清理工作,比如釋放資料庫連接配接池、關閉檔案。

三、如何實作一個簡單的DI容器

用 Java 語言來實作一個簡單的 DI 容器,核心邏輯隻需要包括這樣兩個部分:

  1. 配置檔案解析
  2. 根據配置檔案通過“反射”文法來建立對象。

1、最小原型設計

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
           
public class Demo {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
            "beans.xml");
    RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
    rateLimiter.test();
    //...
  }
}
           

2、提供執行入口

main方法中的執行入口主要包含兩部分:ApplicationContext 和 ClassPathXmlApplicationContext。

public interface ApplicationContext {
  Object getBean(String beanId);
}

public class ClassPathXmlApplicationContext implements ApplicationContext {
  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);
      }
      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);
  }
}
           

3、配置檔案解析

配置檔案解析主要包含 BeanConfigParser 接口和 XmlBeanConfigParser 實作類,負責将配置檔案解析為 BeanDefinition 結構,以便 BeansFactory 根據這個結構來建立對象。

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

public class XmlBeanConfigParser implements BeanConfigParser {

  @Override
  public List<BeanDefinition> parse(InputStream inputStream) {
    String content = null;
    // TODO:...
    return parse(content);
  }

  @Override
  public List<BeanDefinition> parse(String configContent) {
    List<BeanDefinition> beanDefinitions = new ArrayList<>();
    // TODO:...
    return beanDefinitions;
  }

}

public class BeanDefinition {
  private String id;
  private String className;
  private List<ConstructorArg> constructorArgs = new ArrayList<>();
  private Scope scope = Scope.SINGLETON;
  private boolean lazyInit = false;
  // 省略必要的getter/setter/constructors
 
  public boolean isSingleton() {
    return scope.equals(Scope.SINGLETON);
  }


  public static enum Scope {
    SINGLETON,
    PROTOTYPE
  }
  
  public static class ConstructorArg {
    private boolean isRef;
    private Class type;
    private Object arg;
    // 省略必要的getter/setter/constructors
  }
}
           

4、核心工廠類設計

BeansFactory 是如何設計和實作的。這也是我們這個 DI 容器最核心的一個類了。它負責根據從配置檔案解析得到的 BeanDefinition 來建立對象。

BeansFactory 建立對象用到的主要技術點就是 Java 中的反射文法:一種動态加載類和建立對象的機制。

JVM 在啟動的時候會根據代碼自動地加載類、建立對象。至于都要加載哪些類、建立哪些對象,這些都是在代碼中寫死的,或者說提前寫好的。但是,如果某個對象的建立并不是寫死在代碼中,而是放到配置檔案中,我們需要在程式運作期間,動态地根據配置檔案來加載類、建立對象,那這部分工作就沒法讓 JVM 幫我們自動完成了,我們需要利用 Java 提供的反射文法自己去編寫代碼。

public class BeansFactory {
  private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
  private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();

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

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

  public Object getBean(String beanId) {
    BeanDefinition beanDefinition = beanDefinitions.get(beanId);
    if (beanDefinition == null) {
      throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
    }
    return createBean(beanDefinition);
  }

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

    Object bean = null;
    try {
      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.getIsRef()) {
            argClasses[i] = arg.getType();
            argObjects[i] = arg.getArg();
          } else {
            BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
            if (refBeanDefinition == null) {
              throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
            }
            argClasses[i] = Class.forName(refBeanDefinition.getClassName());
            argObjects[i] = createBean(refBeanDefinition);
          }
        }
        bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
      }
    } catch (ClassNotFoundException | IllegalAccessException
            | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
      throw new BeanCreationFailureException("", e);
    }

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

繼續閱讀