IoC容器通過註解進行注入的簡單實現
控制反轉和和依賴注入是Spring中的重要概念。控制反轉是將當前bean所依賴的其他bean元件的例項化過程交由IoC容器來實現並由IoC容器注入到當前bean當中。之前對其實現有一個簡單的認識,即根據類名通過反射的方式載入所需要的bean元件。現在對其做一個通過註解方式進行注入簡單的實現。 首先定義Container介面,容器的主要功能即實現對bean的儲存,即實現bean的增刪查。所以定義其獲取、註冊、刪除、初始化功能。
/** * IoC容器介面 * @author OrangeXXL * */ public interface Container { //根據class獲取Bean public <T> T getBean(Class<T> clazz); //使用name獲取Bean public <T> T getBeanByName(String name); //將Bean註冊到容器 public Object registerBean(Object bean); //將class註冊到容器 public Object registerBean(Class<?> clazz); //註冊帶名稱的bean到容器 public Object registerBean(String name, Object bean); //刪除一個Bean public void remove(Class<?> clazz); //根據名稱刪除Bean public void removeByName(String name); //返回所有bean物件名稱 public Set<String> getBeanNames(); //初始化裝配 public void initWired(); }
接下來是對容器的具體實現。容器實現的是在載入bean的過程中,對其進行掃描,找到有註解的欄位後對其所依賴的bean元件進行注入。首先我們需要儲存已載入的bean。所以通過map以<class name,bean object>的形式儲存bean。同時使用者會對bean進行命名,因此再建立一個map以<custom name, class name>的形式儲存bean名稱和類名稱的對應關係。再初始化當中載入當前bean。因此容器的實現如下。
public class SampleContainer implements Container { /** * 建立map用來儲存所有beans * key是className value為Bean物件 */ private Map<String, Object> beans; /** * 儲存Bean和className的關係 * 其中key是customName value是className */ private Map<String, String> beanKeys; public SampleContainer() { this.beans = new ConcurrentHashMap<String, Object>(); this.beanKeys = new ConcurrentHashMap<String, String>(); } @Override //根據class獲取Bean public <T> T getBean(Class<T> clazz) { String name = clazz.getName(); Object object = beans.get(name); if(null != object){ return (T) object; } return null; } @Override //根據name獲取bean public <T> T getBeanByName(String customName) { String className = beanKeys.get(customName); Object object = beans.get(className); if(null != object){ return (T) object; } return null; } @Override //將Bean註冊到容器 public Object registerBean(Object bean) { String customName = bean.getClass().getName(); beanKeys.put(customName, customName); beans.put(customName, bean); return bean; } @Override //將class註冊到容器 public Object registerBean(Class<?> clazz) { String className = clazz.getName(); beanKeys.put(className, className); Object bean = ReflectUtil.newInstance(clazz); beans.put(className, bean); return bean; } @Override //註冊帶名稱的bean到容器 public Object registerBean(String customName, Object bean) { String className = bean.getClass().getName(); beanKeys.put(customName, className); beans.put(className, bean); return bean; } @Override //刪除一個Bean public void remove(Class<?> clazz) { String className = clazz.getName(); if(null != className && !className.equals("")){ beanKeys.remove(className); beans.remove(className); } } @Override //根據名稱刪除Bean public void removeByName(String customName) { String className = beanKeys.get(customName); if(null != className && !className.equals("")){ beanKeys.remove(customName); beans.remove(className); } } @Override //返回所有bean物件名稱 public Set<String> getBeanNames() { return beanKeys.keySet(); } @Override //初始化裝配 public void initWired() { Iterator<Entry<String, Object>> it = beans.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Object> entry = (Map.Entry<String, Object>) it.next(); System.out.println("[DEBUG] className=>"+entry.getKey()+"\n[DEBUG] beanName=>"+entry.getValue().getClass().getName()); Object object = entry.getValue(); injection(object); } } /** * 注入物件 * @param obj * @return */ public void injection(Object obj){ try{ Field[] fields = obj.getClass().getDeclaredFields(); for(Field field : fields){ /** * 獲取Autowired註解標註的欄位 * 即獲取需要注入的欄位 */ /** * getAnnotation方法是AccessibleObject類的方法。 * 如果存在註解,則返回該元素的指定型別的註解,否則返回null */ Autowired autowired = field.getAnnotation(Autowired.class); System.out.println("[DEBUG] print autowired field name"+autowired+"end"); if(null != autowired){ //進入判斷條件的即為存在Autowired型別註解的欄位 Object autowiredField = null; String name = autowired.name(); System.out.println("[DEBUG] autowired field name"+"["+name+"]"); if(!name.equals("")){ //name不為空,則需要注入的就是bean,所以首先通過beanName拿到className String className = beanKeys.get(name); if(null != className && className.equals("")){ //className存在說明曾經注入過。則直接取出相應的例項即可 autowiredField = beans.get(className); } if(null == autowiredField){ throw new RuntimeException("Unable to load " + name); } }else{ //name為空則需要注入的是class, System.out.println("[DEBUG] name == null in this"); if(autowired.value() == Class.class){ autowiredField = recursiveAssembly(field.getType()); }else{ autowiredField = this.getBean(autowired.value()); if(autowiredField == null){ autowiredField = recursiveAssembly(autowired.value()); } } } if (null == autowiredField) { throw new RuntimeException("Unable to load " + field.getType().getCanonicalName()); } //將獲取到的例項注入到相應欄位(autowiredField)中 boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(obj, autowiredField); field.setAccessible(accessible); } } }catch(SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); }catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 裝配class * @param clazz * @return */ private Object recursiveAssembly(Class<?> clazz){ if(null != clazz){ return this.registerBean(clazz); } return null; } }
這裡對initWired函式做一個說明。當我們需要對某個bean進行載入時,首先我們會先將該類註冊到容器當中。之後初始化時,我們迭代容器中所有的bean對其進行檢檢視是否存在需要注入的欄位。並進行注入操作。這裡迭代容器中的bean有一個很有意思的地方。舉個例子,我們需要載入的bean中有若干欄位需要被注入。第一次,我們將該bean註冊時候去檢查其欄位並載入它的依賴。當載入完成後,初始化操作還會繼續迭代(此時map中有了新的元素)。此時倘若被依賴的bean中同樣存在對別的bean的依賴則同樣會被載入進來。 但這一處我也有個疑問,在載入當前bean的時候map通過迭代器讀取當前Entry,之後我們對當前Value中儲存的bean進行注入。而注入時又會向map中新增元素。雖然concurrentHashMap支援迭代器的邊訪問,便修改的操作。concurrentHashMap通過segements細化粒度來提高其併發度,但是也存在機率新新增的元素和正在訪問的元素在一個segement當中。我認為同一個segement中是會阻塞的吧。但是看到有文章說支援邊訪問邊修改。我目前也不太清楚怎麼證實。如果有了解的老哥可以評論一下。 另外容器中註冊bean中有一個ReflectUtil其中主要有一個newInstance的過載方法。可以根據類名(全限定類名)和類物件建立bean。
/**
* 根據類名建立物件
* @param className
* @return
*/
public static Object newInstance(String className){
Object obj = null;
try{
Class<?> clazz = Class.forName(className);
obj = clazz.newInstance();
}catch(Exception e){
e.printStackTrace();
}
return obj;
}
/**
* 建立類的例項
* @param clazz
* @return
*/
public static Object newInstance(Class<?> clazz){
try{
return clazz.newInstance();
}catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
以上就是容器的基本實現。另外我們需要通過註解對依賴的bean進行標註。因此需要實現一個註解。註解有兩個預設引數,一個是類物件,另一個是類名。程式碼如下。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
/**
* @return 要注入的類型別
*/
Class<?> value() default Class.class;
/**
* @return bean的名稱
*/
String name() default "";
}
最後我們建立兩個類Stu和OutputStu來進行驗證。
- Stu是一個被依賴的bean
public class Stu {
private String name;
private int age;
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
public void show(){
System.out.println(name+"的年齡為:"+age);
}
}
OutputStu是我們希望載入的bean
public class OutputStu {
@Autowired
private Stu s;
public void setValue(String name, int age){
s.setName(name);
s.setAge(age);
}
public void printAns(){
s.show();
}
}
他們之間的關係可以將OutoutStu對應為開發中的Controller將Stu對應為Service 以下是測試類:
public class IoCTest {
public static Container container = new SampleContainer();
public static void testAutowired(){
container.registerBean(OutputStu.class);
container.initWired();
OutputStu op = container.getBeanByName("com.wen.ioc.test.OutputStu");
op.setValue("hezhiheng", 23);
op.printAns();
}
public static void main(String[] args){
testAutowired();
}
}
首先我們載入OutputStu類,之後通過初始化裝配,載入依賴beanStu。之後我們獲取OutputStu物件進行相應操作。 至此一個通過註解進行注入的簡單IoC容器就實現了。 參考來自biezhi的github。如有紕漏歡迎指正交流。