书写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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值