IoC 反轉控制原則也被叫做依賴註入 DI, 容器按照配置註入實例化的對象. 本文將實現一個輕量化的 IoC 容器, 完成對象的實例化和註入, 基於註解不依賴於任何庫. (註解參考 JSR-330) ...
IoC 反轉控制原則也被叫做依賴註入 DI, 容器按照配置註入實例化的對象.
假設 A
的相互依賴關係如下圖, 如何將 A
對象實例化並註入屬性.
本文將實現一個輕量化的 IoC 容器, 完成對象的實例化和註入, 基於註解不依賴於任何庫. (註解參考 JSR-330)
前提 JSR-330
註解 | 說明 |
---|---|
@Inject | 標識可註入的欄位 |
@Named | 基於字元串的限定符, 表示需要 IoC 接管的類 |
JSR-330 遠比前提中提到的更多, 可以看下官方的解釋說明, 這裡只截取了本文目的需要開發的部分.
類定義
按照背景中的依賴關係圖, 先定義出來對象.
@Named("a")
public class A {
@Inject
public B b;
@Inject
public C c;
// getter and setter
// constructor
}
@Named("b")
public class B {
@Value("hello world!")
public String name;
// getter and setter
// constructor
}
@Named("c")
public class C {
@Inject
public A a;
// getter and setter
// constructor
}
為了清晰, 這裡省略了構造器和 setter 函數, 這些對於實現是必要的, 如果需要完整代碼可以參照項目 xnuc-insni.
先考慮簡單情況, A
與 B
的相互依賴如何實現.
註解定義
註解定義參照 inject, 這裡只截取了需要的部分.
@Target(FIELD)
@Retention(RUNTIME)
public @interface Inject {}
@Target(TYPE)
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
對於簡單類型, 可以提供一個設定的數值, 使用 Val 註解完成.
@Target(FIELD)
@Retention(RUNTIME)
public @interface Value {
String value() default "";
}
容器定義
容器定義很簡單, 有一個實例的表和類定義的表.
public class Context {
public HashMap<String, Object> instances; // 實例
public HashMap<String, Class<?>> defineds; // 類定義
}
獲取類定義
獲取類定義用到反射和註解, 不瞭解相關知識的同學可以先補一下這部分. 如果要獲取類定義, 最簡單的方法就是找到全部類進行類載入. 首先獲取主類載入器, 找到全部 .class
路徑.
Enumeration<URL> resources = Main.class.getClassLoader().getResources(pkg.replace(".", "/"));
File file = new File(resources.nextElement().getFile());
獲取全部包下的全部類, 存在子包的情況, 可以用遞歸或者隊列, 最開始用的隊列, 但是發現隊列對於子包處理時非常複雜的, 需要根據隊列信息維護當前包名. 遞歸的系統棧會幫我們記錄下來自然就不需要我們自己維護了, 選擇遞歸的方式處理子包.
private void subdir(String pkg, File file, List<Class<?>> clzes) throws Exception {
for (File f : file.listFiles()) {
if (f.isFile()) { // 退出條件
String clsName = String.format("%s.%s", pkg, f.getName().substring(0, f.getName().lastIndexOf(".")));
clzes.add(Class.forName(clsName));
}
if (f.isDirectory())
subdir(String.format("%s.%s", pkg, f.getName()), f, clzes);
}
}
這裡選擇了參數傳返回值, 更好的方式還是直接內部將 list
構造出來, 返回出去.
拿到全部類後, 將有存在註解的類篩選出來. 放入 Context
的 defineds
.
for (Class<?> c : clzList) {
if (c.getAnnotation(Named.class) != null) {
defineds.put(c.getAnnotation(Named.class).value(), c);
}
}
此時初始化完畢, 類定義獲取到. 另外, 其實這裡已經可以開始註入了, 但是真實情況下, 如果類定義比較多, 那麼初始化將非常耗時, 如果選擇用到再說的原則, 初始化就會快很.
用到再說
Context#get
用來獲取容器對象, 如果對象還沒有實例化, 就實例化. 實例化 instance 實現比較簡單, 找到定義的 filed 進行 set. 這也解釋了, 為什麼需要無參構造器和 setter. 對於基礎值也可以通過 @Value
主動賦予自定義的數值. 對於 @Inject
直接去 Context#get
即可.
private Object instance(Object rto, Class<?> clz) throws Exception {
for (Field field : clz.getFields()) {
if (field.getAnnotation(Value.class) != null) {
PropertyDescriptor descriptor = new PropertyDescriptor(field.getName(), clz);
descriptor.getWriteMethod().invoke(rto, field.getAnnotation(Value.class).value());
}
if (field.getAnnotation(Inject.class) != null) {
PropertyDescriptor descriptor = new PropertyDescriptor(field.getName(), clz);
descriptor.getWriteMethod().invoke(rto, get(field.getName()));
}
}
return rto;
}
寫完 instance 的邏輯, get 的邏輯就比較簡單了. 有返回, 沒有實例化.
public Object get(String objName) throws Exception {
if (instances.get(objName) != null)
return instances.get(objName);
if (defineds.get(objName) == null)
throw new Exception(String.format("objName %s undefined", objName));
Class<?> clz = defineds.get(objName);
instances.put(objName, instance(unreadyInstances.get(objName), clz));
return instances.get(objName);
}
這樣只能解決 A
和 B
的問題, 對於 A
和 C
的問題這樣就會導致註入 A
時發現需要註入 C
, 而註入 C
時又要去註入 A
, 最終導致迴圈.
依賴迴圈
迴圈依賴解決方法很簡單, 只需要一個表記錄下我現在正在註入 A
, 所以 C
註入 A
的時候直接把正在註入的 A
丟給 C
即可.
所以新增 Context
成員 public HashMap<String, Object> unreadyInstances
public class Context {
public HashMap<String, Object> instances;
++ public HashMap<String, Object> unreadyInstances;
public HashMap<String, Class<?>> defineds;
}
註入前先把這個對象扔進去, 註入時如果其他對象有迴圈依賴, Context#get
可以直接返回這個對象.
public Object get(String objName) throws Exception {
if (instances.get(objName) != null)
return instances.get(objName);
++ if (unreadyInstances.get(objName) != null)
++ return unreadyInstances.get(objName);
if (defineds.get(objName) == null)
throw new Exception(String.format("objName %s undefined", objName));
Class<?> clz = defineds.get(objName);
++ unreadyInstances.put(objName, clz.getDeclaredConstructor().newInstance());
instances.put(objName, instance(unreadyInstances.get(objName), clz));
++ unreadyInstances.remove(objName);
return instances.get(objName);
}
最終的代碼就是這樣了, 寫個 Main 類, 運行下.
public class Main {
public static void main(String[] args) throws Exception {
Context ioc = Context.run(Main.class);
C c = (C) ioc.get("c");
System.out.println(c.getA().getB().getName()); // >hello world!
}
}
全部代碼可以參考 xnuc - insni 喜歡可以幫忙點個 ⭐Star.