書寫Spring -從零開始製作一個IoC (1) 容器前奏-組件的定義

概述

說起來,爲什麼事情會變成這樣呢?其實我最開始只是對springBoot的包掃描比較感興趣而已,不過最終卻是寫出了一個IOC容器,就在這裏特別的記錄一下吧,雖然容器現在看起來很菜,而且不支持AOP,可能還有我沒想到的Bug。

先說明什麼是IoC,IoC是控制反轉的一個縮寫,控制反轉的意思是,一些java的對象我們不再自己去控制他的創建和初始化,而是交給另一個東西來控制,這個東西會負責處理對象的初始化和創建並且可以管理這些對象之間的關係,我們只需要向這個東西索要我們想使用的java對象即可,由於這個東西維護着衆多java對象,因此就稱它爲容器,用來進行控制反轉的容器,就是IoC容器了。

那麼控制反轉是怎麼做到的呢?有一種比較常見的手法,就是首先初始化各實體放入容器中,然後在根據某些配置,搭建實體之間的關係,比如說,向一個對象內部添加另一個它需要的對象,這就是依賴注入(DI)了。

簡單來說,就是容器通過依賴注入做到了IoC的效果。

首先呢,容器什麼的,裏面得有Class才行,準確的來說,應該是描述class的東西,根據這些東西我就可以創建一個Class的對象,並且向它注入需要的其他對象,所以要做一個IOC,這個描述Class的實體是必不可少的,我把它稱作Definition

組件定義Definition

這樣一個定義,需要包含哪些東西呢?首先是這個類本身,然後是這個類的構造方法的描述,類需要注入的字段的描述生命週期的描述,以及是否爲單例,是否爲需要工廠,需要靜態工廠還是動態工廠等一系列的東西。

但是這裏有一個問題,構造方法和字段還需要單獨的一些數據結構,因此我定義了ExecutableParamDefiniton用於記載方法的參數以及參數的類型和默認值,FieldAwaredDefinition用來記錄字段的類型,注入方式以及默認值。

完成之後,這些definition就像下面這樣:

public class Definition {
	/**
	 * 此組件定義對應的class
	 */
	private Class<?> clazz;
	
	/**
	 * 組件名
	 */
	private String name;
	
	/**
	 * 初始化方法名稱
	 */
	private List<ExecutableParamDefinition> initMethod;
	
	/**
	 * 銷燬方法名稱
	 */
	private List<ExecutableParamDefinition> destoryMethod;
	
	/**
	 * 創建類型
	 */
	private Scope scope;
	
	/**
	 * 依賴的其他組件
	 */
	private List<Class<?>> dependsOn;
	
	/**
	 * 是否爲通過工廠創建的組件
	 */
	private boolean isFactoryInjectDefinition;
	
	/**
	 * 創建組件的工廠是否爲靜態工廠
	 */
	private boolean isStaticFactory;
	
	/**
	 * 工廠組件的類
	 */
	private Class<?> factoryComponentClass;
	
	/**
	 * 工廠方法名
	 */
	private String factoryMethodName;
	
	/**
	 * 構造函數的參數名和參數類型map
	 */
	private List<ExecutableParamDefinition> constructorArgsList;
	
	/**
	 * 字段名稱和依賴類型map
	 */
	private Map<String, FieldAwareDefinition> propClassesMap;
	
	public Definition() {
		dependsOn = new LinkedList<>();
		constructorArgsList = new LinkedList<>();
		propClassesMap = new LinkedHashMap<>();
		destoryMethod = new LinkedList<>();
		initMethod = new LinkedList<>();
	}
	// 省略get/set
}

描述方法的Definition:

public class ExecutableParamDefinition {
	
	/**
	 * 方法的參數個數
	 */
	private int paramCount;
	
	/**
	 * 方法名
	 */
	private String name;
	
	/**
	 * 方法的參數名稱 - 類型map
	 */
	private Map<String, Class<?>> paramNameTypeMap;
	
	/**
	 * 方法的參數名稱 - 值map(value註解提供默認值)
	 */
	private Map<String, Object> paramNameValueMap;
	
	/**
	* 方法的注入類型(name/type)
	*/
	private AwareType awareType;

	public ExecutableParamDefinition() {
		this.paramNameTypeMap = new HashMap<>();
		this.paramNameValueMap = new HashMap<>();
	}
	// 省略get/set
}

字段注入的描述:

public class FieldAwareDefinition {

	private String name;
	private AwareType type;
	private Class<?> clazz;
	private Object value;
	// 省略get/set
}

定義的數據結構是有了,可是具體的定義數據從哪裏來呢?其實我感覺Spring的xml寫起來還是比較麻煩,所以我決定還是仿照springBoot的掃描方式獲取定義。

實現一個包掃描

所以呢,其實掃描的時候環境分爲兩種,一個是在jar包裏面,一個是在文件系統。

什麼意思呢?我在開發的時候,所有的class都是直接在文件夾裏面的,當然也有一些jar包,不過如果發佈的話所有的class都會進入jar包中,因此class文件可以呆在兩個地方,要麼直接的文件夾,要麼就是在jar包裏面。

可是別管在哪,掃描的目的都是類似的,而且由於環境的不同,需要一個以上的掃描方式,因此應該使用接口來規範掃描器的行爲。

public interface IPackageScanner {

	/**
	 * 掃描指定目標爲基礎,所有的class
	 * @return
	 */
	List<Class<?>> scanPackage();

	/**
	 * 掃描指定目標爲基礎,指定的class的子類或實現
	 * @param parentClazz
	 * @return
	 */
	List<Class<?>> scanSubClazz(Class<?> parentClazz);
	
	/**
	 * 掃描含有某註解的類
	 * @param annotationClazz
	 * @return
	 */
	List<Class<?>> scanAnnotation(Class<?> annotationClazz);

}

那麼,掃描的目的嘛,就是爲了可以得到含有註解的類,或者某個類的子類,或者乾脆就是所有的類,簡單明瞭。

首先掃描文件夾的class吧,感覺會比較好處理。

大概的思路是先找到存放class的文件夾,然後從文件夾開始遍歷,遞歸子文件夾尋找class文件,然後在文件的地址中去掉文件夾的地址和class後綴,最後斜槓替換爲點,進行Class.forName加載class。

其實如果我這裏有classLoader的話,現在就可以直接使用classLoader加載它,不過現在沒有寫,因此直接forName也不是不可以的,但是如果想實現springboot那樣的重啓效果,是必須要有一個classLoader的。

public class FileSystemScanner implements IPackageScanner {

	private File baseDir;
	private String base;
	private List<Class<?>> result;
	
	public FileSystemScanner(Class<?> baseClass) {
		String packagePath = baseClass.getPackageName().replace('.', File.separatorChar);
		baseDir = new File(baseClass.getResource("").getFile());
		base = baseDir.getAbsolutePath().replace(packagePath, ""); 
	}
	
	public FileSystemScanner(String path) {
		baseDir = new File(path);
		base = path;
	}
}

這就是一個基礎的掃描器了,它保留了一個用來替換地址的baseDir,這個baseDir是用文件的絕對路徑刪除class文件的包路徑得到的,掃描到的class文件的絕對路徑,去掉這個base就是含有class後綴的包路徑。

例如:C:\project\bin\com\test\Hello.class

以這個爲基礎掃描,可以得到C:\project\bin\com\test\Hello.class是絕對路徑,com\test是包路徑,如果發現子文件夾裏面有這樣的class:C:\project\bin\com\test\World.class,只需要去掉C:\project\bin就可以得到com\test\World.class,去掉class,就可以得到類的全限定名,用來加載類,這個C:\project\bin就是base路徑。

然後通過遞歸查找class。

private void scanClasses(WhenClassFound founded,String base,File file,List<Class<?>> container, Class<?> reference) throws ClassNotFoundException {
		if (file.isDirectory()) {
			List<File> files = Arrays.asList(file.listFiles());
			for (File elem : files) {
				scanClasses(founded, base, elem,container,reference);
			}
		} else {
			String className = file.getAbsolutePath().replace(base, "");
			if (!className.toLowerCase().endsWith("class") || className.contains("module-info")) {
				return;
			}
			className = className.replace(".class", "");
			if (className.startsWith(File.separator)) {
				className = className.substring(1);
			}
			className = className.replace(File.separatorChar, '.');
			try {
				Class<?> clazz = Class.forName(className);
				founded.accept(clazz, container, reference);
			} catch (Throwable e) {
			}
		}
	}
@FunctionalInterface
public interface WhenClassFound {
	
	void accept(Class<?> clazz, List<Class<?>> container, Class<?> reference);
	
}

查找class就算了,這個接口是什麼鬼呢 ?其實是這樣,我發現如果要實現這三個掃描方法,他們的遞歸搜索的步驟其實差不多,只有得到class之後進行的操作有所區別,這個區別處於被包裹在遞歸和循環的內部,不太好處理,畢竟一個差不多的代碼複製三份是有點過分了。

那怎麼辦呢,我想起來有一個類似的東西,Java8有一個StreamAPI,裏面有一個方法叫做filter,也是遍歷集合,也是在內部有所區別,那麼filter是怎麼做到的呢?他通過一個函數式接口,讓用戶把過濾條件以lambda表達式的形式體現在參數裏面,只需要在filter的時候回調這個lambda,就可以做到按照用戶的條件進行過濾。

仿照這個思路,我也編寫了一個這樣的接口,在找到class後調用,傳入存放class的List和參考class,以及剛剛發現的class。

然後只需要給出這樣的lambda,就可以在一個遞歸或循環內按照不同的方式查找class。

此時我突然意識到,如果待會去掃描jar包裏面的class,它掃描到class之後進行的動作和現在掃描到class的動作應該沒有太大的區別,因此這裏可以更進一步化簡。

Java8的特性,可以在接口使用default,直接爲接口添加實現好的方法。
Java8的特性,可以使用雙冒號進行方法引用,如果方法的參數和lambda接口參數一致的話,那麼方法就可以直接充當這個lambda接口的實現方法。

因此,掃描所有class的判斷方法,掃描含有指定註解的class的判斷方法以及掃描指定class子類的判斷方法都可以以default方法的形式放入PackageScanner接口,而PackageScanner的實現類將會直接通過方法引用來複用他們,然後將執行掃描的方法寫入接口作爲實現規範。

增加到PackageScanner接口的四個方法。

/**
	 * 提供lambda調用,發現一個Class,那麼直接加入容器
	 * @param clazz 發現的class
	 * @param container 存放結果的容器
	 * @param reference 參照類
	 */
	default void justAdded(Class<?> clazz, List<Class<?>> container, Class<?> reference) {
		if (isValidClass(clazz)) {
			container.add(clazz);
		}
	}
	
	/**
	 * 提供lambda調用,發現一個class,如果是參照類的子類或實現,就加入容器
	 * @param clazz 發現的class
	 * @param container 存放結果的容器
	 * @param reference 參照類
	 */
	default void assignableAdded(Class<?> clazz, List<Class<?>> container, Class<?> reference) {
		if (isValidClass(clazz) && reference.isAssignableFrom(clazz) ) {
			container.add(clazz);
		}
	}
	
	/**
	 * 提供lambda調用,發現一個class,如果含有參照類的註解,就加入容器
	 * @param clazz 發現的class
	 * @param container 存放結果的容器
	 * @param reference 參照類
	 */
	default void annotationAdded(Class<?> clazz, List<Class<?>> container, Class<?> reference) {
		if (isValidClass(clazz) && AnnotationUtil.getAnnotation(reference, clazz) != null) {
			container.add(clazz);
		}
	}
	
	/**
	 * 執行掃描的方法
	 * @param found 發現類後的動作
	 * @param container 存放結果的容器
	 * @param reference 參照類(如果需要)
	 */
	void scanClasses(WhenClassFound found, List<Class<?>> container, Class<?> reference);

然後就是三個掃描方法的真正實現:

@Override
	public List<Class<?>> scanPackage() {
		if (baseDir == null || !baseDir.exists() || baseDir.isFile()) {
			throw new RuntimeException("文件不存在。");
		}
		LinkedList<Class<?>> container = new LinkedList<>();
		this.scanClasses(this::justAdded , container, null);
		result = container;
		return new LinkedList<>(container);
	}
	
	@Override
	public List<Class<?>> scanSubClazz(Class<?> parentClazz) {
		if (this.result != null) {
			return this.result.stream()
					.filter(clazz -> parentClazz.isAssignableFrom(clazz))
					.collect(Collectors.toList());
		}
		if (baseDir == null || !baseDir.exists() || baseDir.isFile()) {
			throw new RuntimeException("文件不存在。");
		}
		LinkedList<Class<?>> container = new LinkedList<>();
		this.scanClasses(this::assignableAdded ,container, parentClazz);
		return new LinkedList<>(container);
	}
	
	@Override
	public List<Class<?>> scanAnnotation(Class<?> annotationClazz) {
		if (this.result != null) {
			return  this.result.stream()
					.filter(clazz -> AnnotationUtil.getAnnotation(annotationClazz, clazz) != null)
					.collect(Collectors.toList());
		}
		if (baseDir == null || !baseDir.exists() || baseDir.isFile()) {
			throw new RuntimeException("文件不存在。");
		}
		try {
			LinkedList<Class<?>> container = new LinkedList<>();
			this.scanClasses(this::annotationAdded ,base, baseDir, container, annotationClazz);
			return new LinkedList<>(container);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

有時間繼續寫。(to be continue)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章