手寫Spring之DI依賴注入

前兩篇文章介紹了關於手寫Spring IOC控制反轉,由Spring工廠在運行過程當中動態地建立對象的兩種方式。如感興趣,可移步:java

手寫Spring之IOC基於xml動態建立對象spring

手寫Spring之IOC基於註解動態建立對象segmentfault

今天將詳細介紹如何手寫Spring DI依賴注入,在運行過程當中如何動態地爲對象的屬性賦值。服務器

首先仍是建立項目,用於本次測試須要使用到junit,所以建立的是Maven項目,方便添加依賴jar包,JDK環境仍是1.7:ide

clipboard.png

接下來在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

clipboard.png

進行DI注入前須要建立工廠,在運行時從工廠中取出對象爲屬性賦值。所以先作一些準備工做,建立幾個要用到的註解:spa

clipboard.png

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();
}

接下來建立實體類:

clipboard.png

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:

clipboard.png

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();
    }
}

建立註解工廠類:

clipboard.png

工廠類的內容以下:

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;
}

工廠類建立完畢後,開始寫測試類進行測試:

clipboard.png

測試類內容以下:

@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();
    }
}

以上是全部的代碼,寫完以後就能夠運行程序進行測試了。運行結果以下:

clipboard.png

從控制檯打印輸出的結果能夠看出,UserService類中的兩個User屬性都已經成功注入,並調用了模擬用戶登陸的login方法,輸出的結果正是爲User對象所設置的值。因爲User類是單例的,所以UserService中的兩個User屬性所注入的值都是同一個對象(根據對象所映射的地址hashcode值相同能夠證實這一點),並且無參的構造方法也只執行了一次。

那麼如何爲多例模式的對象進行注入呢?咱們在User類的註解中加上scope屬性,指定爲prototype:

@MyComponent(scope="prototype")
public class User {
    ... ...
}

而後再次運行程序進行測試,結果以下:

clipboard.png

如今能夠看到,爲兩個User屬性所賦的值已是不一樣的對象了,無參構造方法執行了兩次。

相關文章
相關標籤/搜索