極客時間——設計模式之美 工廠模式(下):如何設計實現一個Dependency Injection框架?

小豬快跑22發表於2020-12-02

當建立物件是一個“大工程”的時候,我們一般會選擇使用工廠模式,來封裝物件複雜的建立過程,將物件的建立和使用分離,讓程式碼更加清晰。那何為“大工程”呢?上一節課中我們講了兩種情況,一種是建立過程涉及複雜的 if-else 分支判斷,另一種是物件建立需要組裝多個其他類物件或者需要複雜的初始化過程。

再來講一個建立物件的“大工程”,依賴注入框架,或者叫依賴注入容器(Dependency Injection Container),簡稱 DI 容器。在今天的講解中,我會帶你一塊搞清楚這樣幾個問題:DI 容器跟我們講的工廠模式又有何區別和聯絡?DI 容器的核心功能有哪些,以及如何實現一個簡單的 DI 容器?

工廠模式和 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 容器的具體實現的時候,我們會講“反射”這種機制,它能在程式執行的過程中,動態地載入類、建立物件,不需要事先在程式碼中寫死要建立哪些物件。所以,不管是建立一個物件還是十個物件,BeansFactory 工廠類程式碼都是一樣的。

最後,我們來看物件的生命週期管理。

簡單工廠模式有兩種實現方式,一種是每次都返回新建立的物件,另一種是每次都返回同一個事先建立好的物件,也就是所謂的單例物件。在 Spring 框架中,我們可以通過配置 scope 屬性,來區分這兩種不同型別的物件。scope=prototype 表示返回新建立的物件,scope=singleton 表示返回單例物件。

除此之外,我們還可以配置物件是否支援懶載入。如果 lazy-init=true,物件在真正被使用到的時候(比如:BeansFactory.getBean(“userService”))才被被建立;如果 lazy-init=false,物件在應用啟動的時候就事先建立好。

不僅如此,我們還可以配置物件的 init-method 和 destroy-method 方法,比如 init-method=loadProperties(),destroy-method=updateConfigFile()。DI 容器在建立好物件之後,會主動呼叫 init-method 屬性指定的方法來初始化物件。在物件被最終銷燬之前,DI 容器會主動呼叫 destroy-method 屬性指定的方法來做一些清理工作,比如釋放資料庫連線池、關閉檔案。

如何實現一個簡單的 DI 容器?

實際上,用 Java 語言來實現一個簡單的 DI 容器,核心邏輯只需要包括這樣兩個部分:配置檔案解析、根據配置檔案通過“反射”語法來建立物件。

1. 最小原型設計

因為我們主要是講解設計模式,所以,在今天的講解中,我們只實現一個 DI 容器的最小原型。像 Spring 框架這樣的 DI 容器,它支援的配置格式非常靈活和複雜。為了簡化程式碼實現,重點講解原理,在最小原型中,我們只支援下面配置檔案中涉及的配置語法。


配置檔案beans.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();
    //...
  }
}
2. 提供執行入口

前面我們講到,物件導向設計的最後一步是:組裝類並提供執行入口。在這裡,執行入口就是一組暴露給外部使用的介面和類。

通過剛剛的最小原型使用示例程式碼,我們可以看出,執行入口主要包含兩部分:ApplicationContext 和 ClassPathXmlApplicationContext。其中,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);
  }
}

從上面的程式碼中,我們可以看出,ClassPathXmlApplicationContext 負責組裝 BeansFactory 和 BeanConfigParser 兩個類,串聯執行流程:從 classpath 中載入 XML 格式的配置檔案,通過 BeanConfigParser 解析為統一的 BeanDefinition 格式,然後,BeansFactory 根據 BeanDefinition 來建立物件。

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 來建立物件。

如果物件的 scope 屬性是 singleton,那物件建立之後會快取在 singletonObjects 這樣一個 map 中,下次再請求此物件的時候,直接從 map 中取出返回,不需要重新建立。如果物件的 scope 屬性是 prototype,那每次請求物件,BeansFactory 都會建立一個新的物件返回。

實際上,BeansFactory 建立物件用到的主要技術點就是 Java 中的反射語法:一種動態載入類和建立物件的機制。我們知道,JVM 在啟動的時候會根據程式碼自動地載入類、建立物件。至於都要載入哪些類、建立哪些物件,這些都是在程式碼中寫死的,或者說提前寫好的。但是,如果某個物件的建立並不是寫死在程式碼中,而是放到配置檔案中,我們需要在程式執行期間,動態地根據配置檔案來載入類、建立物件,那這部分工作就沒法讓 JVM 幫我們自動完成了,我們需要利用 Java 提供的反射語法自己去編寫程式碼。

搞清楚了反射的原理,BeansFactory 的程式碼就不難看懂了。具體程式碼實現如下所示:


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

相關文章