本文轉載自開源中國的黃勇老師(跨平臺這個好傷啊555,求大神教訣竅),文章地址:https://my.oschina.net/huangyong/blog/158992
IOC 也就是“控制反轉”了,不過更流行的叫法是“依賴注入”(DI - Dependency Injection)。聽起來挺高深,其實實現起來並不複雜。下面就看看如何來實現這個輕量級 IOC 框架。
從實例出發,先看看以下 Action 代碼。
@Bean
public class ProductAction extends BaseAction {
@Inject
private ProductService productService;
@Request("GET:/product/{id}")
public Result getProductById(long productId) {
if (productId == 0) {
return new Result(ERROR_PARAM);
}
Product product = productService.getProduct(productId);
if (product != null) {
return new Result(OK, product);
} else {
return new Result(ERROR_DATA);
}
}
}
以上使用了兩個自定義註解:@Bean 與 @Inject。
在 ProductAction 類上標註了 @Bean 註解,表示該類會交給“容器”處理,以便加入依賴注入框架。
在 produceService 字段上標註了 @Inject 註解,表示該字段將會被注入進來,而無需 new ProductServiceImpl(),實際上 new 這件事情不是我們做的,而是框架做的,也就是說控制權正好反過來了,所以“依賴注入(DI)”也稱作“控制反轉(IoC)”。
那麼,應該如何實現依賴注入框架呢?首先還是看看下面的 BeanHelper 類吧。
public class BeanHelper {
private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
static {
try {
// 獲取並遍歷所有的 Bean(帶有 @Bean 註解的類)
List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean.class);
for (Class<?> beanClass : beanClassList) {
// 創建 Bean 實例
Object beanInstance = beanClass.newInstance();
// 將 Bean 實例放入 Bean Map 中(鍵爲 Bean 類,值爲 Bean 實例)
beanMap.put(beanClass, beanInstance);
}
// 遍歷 Bean Map
for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
// 獲取 Bean 類與 Bean 實例
Class<?> beanClass = beanEntry.getKey();
Object beanInstance = beanEntry.getValue();
// 獲取 Bean 類中所有的字段(不包括父類中的方法)
Field[] beanFields = beanClass.getDeclaredFields();
if (ArrayUtil.isNotEmpty(beanFields)) {
// 遍歷所有的 Bean 字段
for (Field beanField : beanFields) {
// 判斷當前 Bean 字段是否帶有 @Inject 註解
if (beanField.isAnnotationPresent(Inject.class)) {
// 獲取 Bean 字段對應的接口
Class<?> interfaceClass = beanField.getType();
// 獲取該接口所有的實現類
List<Class<?>> implementClassList = ClassHelper.getClassListByInterface(interfaceClass);
if (CollectionUtil.isNotEmpty(implementClassList)) {
// 獲取第一個實現類
Class<?> implementClass = implementClassList.get(0);
// 從 Bean Map 中獲取該實現類對應的實現類實例
Object implementInstance = beanMap.get(implementClass);
// 設置該 Bean 字段的值
beanField.setAccessible(true); // 必須使該字段可訪問
beanField.set(beanInstance, implementInstance);
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Map<Class<?>, Object> getBeanMap() {
return beanMap;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> cls) {
return (T) beanMap.get(cls);
}
}
其實很簡單,依賴注入其實分爲兩個步驟:1. 通過反射創建實例;2. 獲取需要注入的接口實現類並將其賦值給該接口。以上代碼中的兩個 for 循環就是幹這兩件事情的。
依賴注入框架實現完畢!
請大家給出評價,謝謝!
有些網友對如何尋找接口的實現類的算法有些疑問,如果一個接口存在兩個實現類,應該獲取哪一個實現類呢?我之前的做法是,只獲取第一個實現類。而 Spring 的做法是,直接報錯,應用都起不來。經過反覆思考,我做了一個慎重的決定,就是在接口上使用 @Impl 註解來強制指定哪個實現類。而 BeanHelper 在做依賴注入的時候,會首先判斷接口上是否有 @Impl 註解,如果有就獲取這個強制指定的實現類實例,否則就獲取所有實現類中的第一個實現類,仍然不會像 Spring 那樣讓應用報錯。下面是對 BeanHelper 類中部分代碼的修改:
...
// 判斷當前 Bean 字段是否帶有 @Inject 註解
if (beanField.isAnnotationPresent(Inject.class)) {
// 獲取 Bean 字段對應的接口
Class<?> interfaceClass = beanField.getType();
// 判斷接口上是否標註了 @Impl 註解
Class<?> implementClass = null;
if (interfaceClass.isAnnotationPresent(Impl.class)) {
// 獲取強制指定的實現類
implementClass = interfaceClass.getAnnotation(Impl.class).value();
} else {
// 獲取該接口所有的實現類
List<Class<?>> implementClassList = ClassHelper.getClassListByInterface(interfaceClass);
if (CollectionUtil.isNotEmpty(implementClassList)) {
// 獲取第一個實現類
implementClass = implementClassList.get(0);
}
}
// 若存在實現類,則執行以下代碼
if (implementClass != null) {
// 從 Bean Map 中獲取該實現類對應的實現類實例
Object implementInstance = beanMap.get(implementClass);
// 設置該 Bean 字段的值
beanField.setAccessible(true); // 必須使該字段可訪問
beanField.set(beanInstance, implementInstance);
}
}
...
在接口中是這樣使用的:
@Impl(ProductServiceImpl2.class)
public interface ProductService {
Product getProduct(long productId);
}
假設這個接口的實現類是 ProductServiceImpl2。
這個解決方案相信大家還是滿意的吧?
大家上面看到的 BeanHelper 類,其實兼任了兩種職責:1.初始化所有的 Bean 類;2.實現依賴注入。
這違法了設計模式中的“單一責任原則”,所有有必要將其重構一下,現在的 BeanHelper 類更加苗條了,只是負責初始化 Bean 類而已。代碼如下:
public class BeanHelper {
// Bean 類 => Bean 實例
private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
static {
try {
// 獲取並遍歷所有的 Bean(帶有 @Bean 註解的類)
List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean.class);
for (Class<?> beanClass : beanClassList) {
// 創建 Bean 實例
Object beanInstance = beanClass.newInstance();
// 將 Bean 實例放入 Bean Map 中(鍵爲 Bean 類,值爲 Bean 實例)
beanMap.put(beanClass, beanInstance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Map<Class<?>, Object> getBeanMap() {
return beanMap;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> cls) {
return (T) beanMap.get(cls);
}
}
那麼,依賴注入功能放哪裏呢?我搞了一個 IOCHelper,用這個類來實現 IOC 功能。代碼如下:
public class IOCHelper {
static {
try {
// 獲取並遍歷所有的 Bean 類
Map<Class<?>, Object> beanMap = BeanHelper.getBeanMap();
for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
// 獲取 Bean 類與 Bean 實例
Class<?> beanClass = beanEntry.getKey();
Object beanInstance = beanEntry.getValue();
// 獲取 Bean 類中所有的字段(不包括父類中的方法)
Field[] beanFields = beanClass.getDeclaredFields();
if (ArrayUtil.isNotEmpty(beanFields)) {
// 遍歷所有的 Bean 字段
for (Field beanField : beanFields) {
// 判斷當前 Bean 字段是否帶有 @Inject 註解
if (beanField.isAnnotationPresent(Inject.class)) {
// 獲取 Bean 字段對應的接口
Class<?> interfaceClass = beanField.getType();
// 判斷接口上是否標註了 @Impl 註解
Class<?> implementClass = null;
if (interfaceClass.isAnnotationPresent(Impl.class)) {
// 獲取強制指定的實現類
implementClass = interfaceClass.getAnnotation(Impl.class).value();
} else {
// 獲取該接口所有的實現類
List<Class<?>> implementClassList = ClassHelper.getClassListByInterface(interfaceClass);
if (CollectionUtil.isNotEmpty(implementClassList)) {
// 獲取第一個實現類
implementClass = implementClassList.get(0);
}
}
// 若存在實現類,則執行以下代碼
if (implementClass != null) {
// 從 Bean Map 中獲取該實現類對應的實現類實例
Object implementInstance = beanMap.get(implementClass);
// 設置該 Bean 字段的值
if (implementInstance != null) {
beanField.setAccessible(true); // 取消類型安全檢測(可提高反射性能)
beanField.set(beanInstance, implementInstance); // beanInstance 是普通實例,或 CGLib 動態代理實例(不能使 JDK 動態代理實例)
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
可見,IOCHelper 是依賴於 BeanHelper 的。這樣分離,還有一個好處,就是方便實現 ServiceHelper 與 AOPHelper。也就是說,首先通過 BeanHelper 初始化所有的 Bean 類,然後依次初始化 ServiceHelper、IOCHelper、AOPHelper,這個順序不能搞錯。因爲在 ServcieHelper 中,對 Servcie 實現類進行了動態代理,所有保證了 IOC 注入進來的是代理類,而並非目標類。