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

上一篇博客介紹瞭如何基於xml配置文件在運行時建立實例對象,這篇博客將介紹基於註解方式怎樣實現對象的建立。java

廢話很少說,直接上代碼。spring

首先仍是建立項目,因爲此次不須要使用第三方的API,建立一個簡單的Java項目便可,依然仍是JDK7的環境下。app

clipboard.png

第二步是建立屬於本身的註解。框架

clipboard.png

MyComponent註解內容以下:ide

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

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

第三步是建立entity對象類型,用於在運行時建立其實例對象。url

方便測試,該User類型分別建立兩個單例singleton和多例prototype的User類型。spa

clipboard.png

SingletonUser類的內容以下:.net

package entity;

import annotation.MyComponent;
import annotation.MyValue;

@MyComponent(scope="singleton")
public class SingletonUser {
    @MyValue("1")
    private Integer id;
    @MyValue("zhangsan")
    private String name;
    @MyValue("zhangsan")
    private String password;
    
    public SingletonUser() {
        System.out.println("無參構造方法執行");
    }
    //setters和getters...
}

PrototypeUser類的內容以下:prototype

package entity;

import annotation.MyComponent;
import annotation.MyValue;

@MyComponent(scope="prototype")
public class PrototypeUser {
    @MyValue("2")
    private Integer id;
    @MyValue("lisi")
    private String name;
    @MyValue("lisi")
    private String password;
    public PrototypeUser() {
        System.out.println("無參構造方法執行");
    }
    //setters和getters...
}

主角登場,建立AnnotationConfigApplicationContext工廠類

clipboard.png

該類的內容以下:

首先定義兩個Map容器用於存儲對象。

package applicationContext;  
  
import java.io.File;  
import java.io.FileFilter;  
import java.net.URL;  
import java.util.Map;  
import java.util.concurrent.ConcurrentHashMap;  
  
import annotation.MyComponent;  
  
public class AnnotationConfigApplicationContext {  
    /**此Map容器用於存儲類定義對象*/  
    private Map<String, Class<?>> beanDefinationFacotry=new ConcurrentHashMap<>();  
    /**此Map容器用於存儲單例對象*/  
    private Map<String,Object> singletonbeanFactory=new ConcurrentHashMap<>();  
}

定義有參構造方法:

/**有參構造方法,參數類型爲指定要掃描加載的包名*/  
public AnnotationConfigApplicationContext(String packageName) {  
    /**掃描指定的包路徑*/  
    scanPkg(packageName);  
}

在建立對象初始化時,會調用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 key=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(key, c);  
            }  
        }catch(Exception e){  
            throw new RuntimeException(e);   
        }  
    }  
}

以上是初始化方法,在建立AnnotationConfigApplicationContext對象時即會掃描指定的包路徑,並加載類定義對象到容器中。

接下來定義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)){
            //判斷容器中是否已有該對象的實例,若是沒有,建立一個實例對象放到容器中
            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);
}

在獲取對象時須要爲其成員屬性賦值,調用了setFieldValue方法,所以定義該方法:

/**
 * 此方法用於爲對象的屬性賦值
 * 內部是經過獲取成員屬性上註解的值,在轉換爲類型以後,經過反射爲對象賦值
 * @param cls 類定義對象
 * @param obj 要爲其賦值的實例對象
 */
public 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

測試類的內容以下:

package springTest;  
  
import applicationContext.AnnotationConfigApplicationContext;  
import entity.PrototypeUser;  
import entity.SingletonUser;  
  
public class springIocTest {  
    public static void main(String[] args) {  
        //建立AnnotationConfigApplicationContext對象  
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("entity");  
        //僅使用key做爲參數獲取對象,須要強轉  
        SingletonUser singletonUser1=(SingletonUser) ctx.getBean("singletonUser");  
        System.out.println("單例User對象:"+singletonUser1);  
        //使用key和類對象做爲參數獲取對象,無需強轉  
        SingletonUser singletonUser2=ctx.getBean("singletonUser",SingletonUser.class);  
        System.out.println("單例User對象:"+singletonUser2);  
        //僅使用key做爲參數獲取對象,須要強轉  
        PrototypeUser prototypeUser1=(PrototypeUser) ctx.getBean("prototypeUser");  
        System.out.println("多例User對象:"+prototypeUser1);  
        //使用key和類對象做爲參數獲取對象,無需強轉  
        PrototypeUser prototypeUser2=ctx.getBean("prototypeUser",PrototypeUser.class);  
        System.out.println("多例User對象:"+prototypeUser2);  
        //銷燬資源  
        ctx.close();  
    }  
}

運行此測試類,控制檯輸出結果以下:

clipboard.png

能夠看到,在建立單例對象時,無參的構造方法只調用了一次,而且兩次調用getBean方法獲取的對象是一致的。而在建立多例對象時,無參的構造方法被調用了兩次,兩次調用getBean所獲取的對象是不一樣的。

若是須要看對象的屬性是否注入成功,能夠在兩個User類中增長toSrting方法

@Override
public String toString() {
    return "SingletonUser [id=" + id + ", name=" + name + ", password=" + password + "]";
}

再次執行程序,結果以下:

clipboard.png

從控制檯的輸處結果中能夠看到,所獲取的每一個對象都已經有值。注意:這種爲對象注入屬性值的方式耦合度較高,可根據狀況使用。

寫在最後:因爲手寫SpringIOC只是出於對Spring框架的理解,並不是要寫一個可用的框架。所以在代碼中省略了大量的對於參數的判斷和異常處理,免去代碼的冗餘,方便看官理解。高手請勿吐槽啊,如有問題或建議,歡迎留言指正。

相關文章
相關標籤/搜索