前兩篇文章介紹了關於手寫Spring IOC控制反轉,由Spring工廠在運行過程當中動態地建立對象的兩種方式。如感興趣,可移步:java
手寫Spring之IOC基於xml動態建立對象spring
手寫Spring之IOC基於註解動態建立對象segmentfault
今天將詳細介紹如何手寫Spring DI依賴注入,在運行過程當中如何動態地爲對象的屬性賦值。服務器
首先仍是建立項目,用於本次測試須要使用到junit,所以建立的是Maven項目,方便添加依賴jar包,JDK環境仍是1.7:ide
接下來在pom.xml文件中添加junit的依賴座標:測試
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency>
第一次添加時,若本地倉庫中沒有此版本的jar包,Maven會根據配置的鏡像聯網下載,默認是去中心倉庫下載,中心倉庫的服務器在國外,下載速度較慢,建議修改配置文件鏈接阿里雲的Maven鏡像倉庫下載,速度較快,如何配置在此很少贅述。你也能夠根據本身本地倉庫已有的junit版本 對依賴座標的版本進行修改,這樣就能夠直接使用本地倉庫的jar包,不用耗時連外網去下載了。阿里雲
完成後在Maven Dependencies中會有相關的jar包出現:url
進行DI注入前須要建立工廠,在運行時從工廠中取出對象爲屬性賦值。所以先作一些準備工做,建立幾個要用到的註解:spa
MyComponent註解內容以下:prototype
package annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /**@Target 屬性用於註明此註解用在什麼位置, * ElementType.TYPE表示可用在類、接口、枚舉上等*/ @Target(ElementType.TYPE) /**@Retention 屬性表示所定義的註解什麼時候有效, * RetentionPolicy.RUNTIME表示在運行時有效*/ @Retention(RetentionPolicy.RUNTIME) /**@interface 表示註解類型*/ public @interface MyComponent { /**爲此註解定義scope屬性*/ public String scope() default ""; }
MyAutowired註解內容以下:
package annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAutowired { }
MyValue註解內容以下:
package annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyValue { /**定義value屬性*/ public String value(); }
接下來建立實體類:
User實體類內容以下,實體類中的屬性值暫用註解方式寫死做爲測試(實際中並不會這麼用),此實體類暫時爲單例類(不註明scope屬性默認爲單例模式):
@MyComponent public class User { @MyValue("1") private Integer id; @MyValue("zhangsan") private String name; @MyValue("zhangsan") private String password; public User() { System.out.println("無參構造方法執行"); } public void login(){ System.out.println("用戶登陸:id=" + id + ", name=" + name + ", password=" + password); } //setters和getters... }
而後建立UserService類,在Service類中使用依賴注入User:
UserService內容以下:
package service; import annotation.MyAutowired; import annotation.MyComponent; import entity.User; @MyComponent public class UserService { @MyAutowired User user1; @MyAutowired User user2; public void userLogin(){ System.out.println("用戶1:"+user1); user1.login(); System.out.println("用戶2:"+user2); user2.login(); } }
建立註解工廠類:
工廠類的內容以下:
public class AnnotationConfigApplicationContext { /**此Map容器用於存儲類定義對象*/ private Map<String, Class<?>> beanDefinationFacotry=new ConcurrentHashMap<>(); /**此Map容器用於存儲單例對象*/ private Map<String,Object> singletonbeanFactory=new ConcurrentHashMap<>(); /**有參構造方法,參數類型爲指定要掃描加載的包名,此工廠可接收多個包路徑*/ public AnnotationConfigApplicationContext(String... packageNames) { //遍歷掃描指定的全部包路徑 for (String packageName : packageNames) { System.out.println("開始掃描包:"+packageName); /**掃描指定的包路徑*/ scanPkg(packageName); } /**進行DI依賴注入*/ dependencyInjection(); } }
在工廠類的構造方法中,能夠接收多個包路徑,而且遍歷循環掃描每個包路徑,掃描包的scanPkg方法以下:
/** * 掃描指定包,找到包中的類文件。 * 對於標準(類上有定義註解的)類文件反射加載建立類定義對象並放入容器中 */ private void scanPkg(final String pkg){ //替換包名中的".",將包結構轉換爲目錄結構 String pkgDir=pkg.replaceAll("\\.", "/"); //獲取目錄結構在類路徑中的位置(其中url中封裝了具體資源的路徑) URL url=getClass().getClassLoader().getResource(pkgDir); //基於這個路徑資源(url),構建一個文件對象 File file=new File(url.getFile()); //獲取此目錄中指定標準(以".class"結尾)的文件 File[] fs=file.listFiles(new FileFilter() { @Override public boolean accept(File file) { //獲取文件名 String fName=file.getName(); //判斷該文件是否爲目錄,如爲目錄,遞歸進一步掃描其內部全部文件 if(file.isDirectory()){ scanPkg(pkg+"."+fName); }else{ //斷定文件的後綴是否爲.class if(fName.endsWith(".class")){ return true; } } return false; } }); //遍歷全部符合標準的File文件 for(File f:fs){ //獲取文件名 String fName=f.getName(); //獲取去除.class以後的文件名 fName=fName.substring(0,fName.lastIndexOf(".")); //將名字(類名,一般爲大寫開頭)的第一個字母轉換小寫(用它做爲key存儲工廠中) String beanId=String.valueOf(fName.charAt(0)).toLowerCase()+fName.substring(1); //構建一個類全名(包名.類名) String pkgCls=pkg+"."+fName; try{ //經過反射構建類對象 Class<?> c=Class.forName(pkgCls); //斷定這個類上是否有MyComponent註解 if(c.isAnnotationPresent(MyComponent.class)){ //將類對象存儲到map容器中 beanDefinationFacotry.put(beanId, c); } }catch(Exception e){ throw new RuntimeException(e); } } }
掃描全部的包完成以後,對須要的屬性進行注入,dependencyInjection方法以下:
/** * 此方法用於對屬性進行依賴注入。 * 從工廠中獲取全部的類對象,若是類中的屬性上有MyAutowired註解, * 那麼首先從根據屬性名從工廠中獲取對象,或者根據對象類型獲取對象。 * 最後用該對象對屬性進行注入。 */ private void dependencyInjection(){ //獲取容器中全部的類定義對象 Collection<Class<?>> classes = beanDefinationFacotry.values(); //遍歷每個類對象 for (Class<?> cls : classes) { //獲取類對象的名字全稱(包名+類名) String clsName = cls.getName(); //獲取類名 clsName = clsName.substring(clsName.lastIndexOf(".")+1); //將類名(一般爲大寫開頭)的第一個字母轉換小寫 String beanId=String.valueOf(clsName.charAt(0)).toLowerCase()+clsName.substring(1); //獲取類中全部的屬性 Field[] fields = cls.getDeclaredFields(); //遍歷每個屬性 for (Field field : fields) { //若是這個屬性上有MyAutowired註解,進行注入操做 if(field.isAnnotationPresent(MyAutowired.class)){ try { //獲取屬性名 String fieldName = field.getName(); System.out.println("屬性名:"+fieldName); //定義爲屬性注入的bean對象(此對象從容器中獲取) Object fieldBean = null; //首先根據屬性名從容器中取出對象,若是不爲null,則賦值給fieldBean對象 if(beanDefinationFacotry.get(fieldName) != null){ fieldBean = getBean(fieldName,field.getType()); }else{ //不然按照屬性的類型從容器中取出對象進行注入 //獲取屬性的類型(包名+類名) String type = field.getType().getName(); //截取最後的類名 type = type.substring(type.lastIndexOf(".")+1); //將類名(一般爲大寫開頭)的第一個字母轉換小寫 String fieldBeanId=String.valueOf(type.charAt(0)).toLowerCase()+type.substring(1); System.out.println("屬性類型ID:"+fieldBeanId); //根據轉換後的類型beanId,從容器中獲取對象並賦值給fieldBean對象 fieldBean = getBean(fieldBeanId,field.getType()); } System.out.println("要爲屬性注入的值:"+fieldBean); //若是fieldBean對象不爲空,則爲該屬性進行注入 if(fieldBean != null){ //獲取此類定義的對象的實例對象 Object clsBean = getBean(beanId, cls); //設置此屬性可訪問 field.setAccessible(true); //爲該屬性注入值 field.set(clsBean, fieldBean); System.out.println("注入成功!"); }else{ System.out.println("注入失敗!"); } } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } } } } }
在dependencyInjection方法中調用了getBean方法,內容以下:
/** * 根據傳入的bean的id值獲取容器中的對象,類型爲Object */ public Object getBean(String beanId){ //根據傳入beanId獲取類對象 Class<?> cls = beanDefinationFacotry.get(beanId); //根據類對象獲取其定義的註解 MyComponent annotation = cls.getAnnotation(MyComponent.class); //獲取註解的scope屬性值 String scope = annotation.scope(); try { //若是scope等於singleton,建立單例對象 if("singleton".equals(scope) || "".equals(scope)){ //判斷容器中是否已有該對象的實例,若是沒有,建立一個實例對象放到容器中 if(singletonbeanFactory.get(beanId)==null){ Object instance = cls.newInstance(); setFieldValues(cls,instance); singletonbeanFactory.put(beanId,instance); } //根據beanId獲取對象並返回 return singletonbeanFactory.get(beanId); } //若是scope等於prototype,則建立並返回多例對象 if("prototype".equals(scope)){ Object instance = cls.newInstance(); setFieldValues(cls,instance); return instance; } //目前僅支持單例和多例兩種建立對象的方式 } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } //若是遭遇異常,返回null return null; } /** * 此爲重載方法,根據傳入的class對象在內部進行強轉, * 返回傳入的class對象的類型 */ public <T>T getBean(String beanId, Class<T> c){ return (T)getBean(beanId); }
在getBean方法中從工廠容器中獲取對象,而且須要調用setFieldValues方法爲對象的屬性賦值,該方法內容以下:
/** * 此方法用於爲對象的屬性賦值 * 內部是經過獲取成員屬性上註解的值,在轉換爲類型以後,經過反射爲對象賦值 * @param cls 類定義對象 * @param obj 要爲其賦值的實例對象 */ private void setFieldValues(Class<?> cls,Object obj){ //獲取類中全部的成員屬性 Field[] fields = cls.getDeclaredFields(); //遍歷全部屬性 for (Field field : fields) { //若是此屬性有MyValue註解修飾,對其進行操做 if(field.isAnnotationPresent(MyValue.class)){ //獲取屬性名 String fieldName = field.getName(); //獲取註解中的值 String value = field.getAnnotation(MyValue.class).value(); //獲取屬性所定義的類型 String type = field.getType().getName(); //將屬性名改成以大寫字母開頭,如:id改成ID,name改成Name fieldName = String.valueOf(fieldName.charAt(0)).toUpperCase()+fieldName.substring(1); //set方法名稱,如:setId,setName... String setterName = "set" + fieldName; try { //根據方法名稱和參數類型獲取對應的set方法對象 Method method = cls.getDeclaredMethod(setterName, field.getType()); //判斷屬性類型,如類型不一致,則轉換類型後調用set方法爲屬性賦值 if("java.lang.Integer".equals(type) || "int".equals(type)){ int intValue = Integer.valueOf(value); method.invoke(obj, intValue); } else if("java.lang.String".equals(type)){ method.invoke(obj, value); } //做爲測試,僅判斷Integer和String類型,其它類型同理 } catch (NoSuchMethodException | SecurityException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } }
最後是釋放工廠資源的close方法,內容以下:
/** * 銷燬方法,用於釋放資源 */ public void close(){ beanDefinationFacotry.clear(); beanDefinationFacotry=null; singletonbeanFactory.clear(); singletonbeanFactory=null; }
工廠類建立完畢後,開始寫測試類進行測試:
測試類內容以下:
@MyComponent public class TestSpringDi { /**建立AnnotationConfigApplicationContext對象*/ AnnotationConfigApplicationContext ctx; /**建立UserService對象*/ UserService userService; /** * 初始化方法 */ @Before public void init(){ //實例化工廠類,傳入entity/service/springTest三個包路徑進行掃描 ctx = new AnnotationConfigApplicationContext("entity","service","springTest"); //調用工廠的getBean方法動態獲取對象 userService = ctx.getBean("userService",UserService.class); } /** * 測試方法 */ @Test public void testSpringDi(){ userService.userLogin(); } /** * 銷燬方法 */ @After public void close(){ ctx.close(); } }
以上是全部的代碼,寫完以後就能夠運行程序進行測試了。運行結果以下:
從控制檯打印輸出的結果能夠看出,UserService類中的兩個User屬性都已經成功注入,並調用了模擬用戶登陸的login方法,輸出的結果正是爲User對象所設置的值。因爲User類是單例的,所以UserService中的兩個User屬性所注入的值都是同一個對象(根據對象所映射的地址hashcode值相同能夠證實這一點),並且無參的構造方法也只執行了一次。
那麼如何爲多例模式的對象進行注入呢?咱們在User類的註解中加上scope屬性,指定爲prototype:
@MyComponent(scope="prototype") public class User { ... ... }
而後再次運行程序進行測試,結果以下:
如今能夠看到,爲兩個User屬性所賦的值已是不一樣的對象了,無參構造方法執行了兩次。