1. 程式人生 > >手寫Spring之IOC基於註解動態建立物件

手寫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框架的理解,並非要寫一個可用的框架。

因此在程式碼中省略了大量的對於引數的判斷和異常處理,免去程式碼的冗餘,方便看官理解。