手寫Spring之IOC基於註解動態建立物件
上一篇部落格介紹瞭如何基於xml配置檔案在執行時建立例項物件,這篇部落格將介紹基於註解方式怎樣實現物件的建立。
廢話不多說,直接上程式碼。
首先還是建立專案,由於這次不需要使用第三方的API,建立一個簡單的Java專案即可,依然還是JDK 7的環境下。
第二步是建立屬於自己的註解。
註解內容如下:
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(); }
第三步是建立entity物件型別,用於在執行時建立其例項物件。
方便測試,該User型別分別建立兩個單例singleton和多例prototype的User型別。
User類的內容如下:
注:PrototypeUser和SingletonUser的內容基本相同,僅類名和註解的scope屬性不同。
PrototypeUser的註解為:@MyComponent(scope="prototype")
SingletonUser的註解為:@MyComponent(scope="singleton")
package entity; import annotation.MyComponent; @MyComponent(scope="prototype") public class PrototypeUser { private Integer id; private String name; private String password; public PrototypeUser() { System.out.println("無參構造方法執行"); } public PrototypeUser(int id, String name, String password) { System.out.println("有參構造方法執行"); this.id = id; this.name = name; this.password = password; } //setters和getters... }
主角登場,建立AnnotationConfigApplicationContext工廠類。
該類的內容如下:
首先定義兩個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();
singletonbeanFactory.put(beanId,instance);
}
//根據beanId獲取物件並返回
return singletonbeanFactory.get(beanId);
}
//如果scope等於prototype,則建立並返回多例物件
if("prototype".equals(scope)){
return cls.newInstance();
}
} 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);
}
定義close銷燬方法:/**
* 銷燬方法,用於釋放資源
*/
public void close(){
beanDefinationFacotry.clear();
beanDefinationFacotry=null;
singletonbeanFactory.clear();
singletonbeanFactory=null;
}
至此,該工廠類的內容全部完成。接下來寫測試類:
該測試類的內容如下:
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();
}
}
執行此測試類,控制檯輸出結果如下:
可以看到,在建立單例物件時,無參的構造方法只調用了一次,並且兩次呼叫getBean方法獲取的物件是一致的。
而在建立多例物件時,無參的構造方法被呼叫了兩次,兩次呼叫getBean所獲取的物件是不同的。
注:由於手寫SpringIOC只是出於對Spring框架的理解,並非要寫一個可用的框架。
因此在程式碼中省略了大量的對於引數的判斷和異常處理,免去程式碼的冗餘,方便看官理解。