IoC 反轉控制原則也被叫做依賴注入 DI, 容器按照設定注入範例化的物件.
假設 A
的相互依賴關係如下圖, 如何將 A
物件範例化並注入屬性.
本文將實現一個輕量化的 IoC 容器, 完成物件的範例化和注入, 基於註解不依賴於任何庫. (註解參考 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);
A a = (A) ioc.get("a");
System.out.println(a.getC().getA().getB().getName()); // >hello world!
}
}
全部程式碼可以參考 xnuc - insni 喜歡可以幫忙點個 ⭐Star.