简述
本文将对Spring IoC容器的仿写进行剖析,梳理仿写过程中需要解决的问题,并附全部仿写代码进行说明教学,本文的仿写中不引用任何外部类,纯手动实现简要IoC控制反转执行流程。
对IoC的作用不熟悉或不了解什么是控制反转的读者,可以先参考笔者先前的文章或查阅其他资料:IoC控制反转的原理思想
仿写Spring IoC需要解决哪些问题
想要顺利并且理解的仿写一个IoC框架,在动手前应该先理清要解决的问题,或者说IoC框架的执行流程。
笔者将IoC执行流程大致归纳为了以下几个步骤:
- 对指定包路径进行扫描,找出所有添加了IoC注解的目标类。
- 获取目标类的信息,Class对象以及类名beanName,并将其封装以便后续使用。
- 根据封装好的信息类动态创建bean对象。
- bean的自动装载。
详细流程图如下:
以上四步是IoC容器仿写的大概流程,也基本是Spring IoC运作的大致流程,下文将对具体的工作进行介绍和分析。
仿写
本部分代码解释说明性质的文字都包含在注释中,重点关注代码注释部分。
前期准备
准备测试类
首先准备测试类,非必须,本部分可以跳过,读者仿写时可自行设计,笔者仿写完整测试类代码如下(为便于理解,给出的是未添加任何注解版本):
User类:
public class User{
public User() {
}
private Long id;
private String username;
private String password;
private Relationship relationship;
private Role role;
//getter和setter方法省略...
}
Role类:
public class Role extends Relationship{
private Long id;
private String roleName;
public Role(Long id, String roleName) {
this.id = id;
this.roleName = roleName;
}
public Role() {
}
//getter和setter方法省略...
}
Relationship类:
public class Relationship {
private Long userId;
private Long RoleId;
public Relationship(Long userId, Long roleId) {
this.userId = userId;
RoleId = roleId;
}
public Relationship() {
}
//getter和setter方法省略...
}
准备IoC容器入口类
准备一个AnnotationApplicationContext,在本类中完成上文中阐述的四个步骤,实际分步骤测试时,直接准备一个主类在其main方法下创建AnnotationApplicationContext对象测试即可。
AnnotationApplicationContext类结构如下(省略了方法内的内容,下文中会给出):
public class AnnotationApplicationContext {
// 用于存储bean对象
public static final Map<String, Object> beans = new HashMap<>();
public AnnotationApplicationContext(String packageName) {
//获取BeanDefinition集合
Set<BeanDefinition> beanDefinitions = getBeanDefinitions(packageName);
//通过BeanDefinition逐个创建对象
createObject(beanDefinitions);
//自动装载
autowireObject(beanDefinitions);
}
public void autowireObject(Set<BeanDefinition> beanDefinitions){
// 篇幅受限省略 下文给出...
}
public Object getBean(String beanName){
return beans.get(beanName);
}
public void createObject(Set<BeanDefinition> beanDefinitions){
// ...
}
public Set<BeanDefinition> getBeanDefinitions(String packageName){
// ...
}
}
扫描目标包下目标类
首先要进行扫描包,此步骤只负责将目标路径下的所有类,即所有.class文件保存,以便后续使用,判断类是否有IoC注解的步骤整合到下一步。
笔者仿写时单独准备了一个工具类专门用于包扫描,工具类PackageScanner代码如下:
public class PackageScanner {
//首先准备一个Set集合 用于存放被扫描的Class对象 并且避免重复扫描
public final static Set<Class<?>> clazz = new LinkedHashSet();
/**
* @MethodName: getClasses
* @Description: 通过传入包名 获取URL并判断资源是否为本地文件类型
* @Param: [packageName]
* @Return: java.util.Set<java.lang.Class<?>>
**/
public static Set<Class<?>> getClasses(String packageName){
// 类名转换包名
String packagePath = packageName.replace('.','/');
// 获取URL
URL resource = Thread.currentThread().getContextClassLoader().getResource(packagePath);
// 此处判断URL是否为为本地文件 同时该包扫描类只简单实现扫描本地文件
if("file".equals(resource.getProtocol())){
findLocalClasses(packageName);
return clazz;
}
return null;
}
/**
* @MethodName: findLocalClasses
* @Description: 扫描路径下所有类 并加入clazz容器内
* @Param: [packageName]
* @Return: void
**/
private static void findLocalClasses(String packageName){
// 获取类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URI uri = null;
try {
// 类名转换包名
uri = classLoader.getResource(packageName.replace('.', '/')).toURI();
} catch (URISyntaxException e) {
throw new RuntimeException();
}
File file = new File(uri);
//File部分说明见下文
file.listFiles(new FileFilter() {
@Override
public boolean accept(File chiFile) {
// 如果当前是文件夹 则将包名后追加当前文件夹名作为参数 递归调用findLocalClasses
if(chiFile.isDirectory()){
findLocalClasses(packageName + "." + chiFile.getName());
}
// 读取.class文件
if(chiFile.getName().endsWith(".class")){
Class<?> c = null;
try {
// 通过类加载器获取Class对象
c = classLoader.loadClass(packageName + "." + chiFile.getName().replace(".class", ""));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 仅将类加入到容器中
if(!c.isInterface()){
clazz.add(c);
}
return true;
}
return false;
}
});
}
}
扫包工具类总结
在工具类PackageScanner中
- getClasses(String)方法起预处理作用。负责将类名转换为包名,并在判断传入包名是本地文件后调用findLocalClasses(String)。
- findLocalClasses(String)方法实际扫描包并保存.class文件。本方法递归调用,遇到文件夹则继续调用查找,将所有.class文件保存到一个Set集合中。
关于File部分的说明
非核心部分,只是为了更好理解代码,可忽略。
文中使用了File类中包含的方法listFiles,该方法需要传入一个文件拦截器FileFilter,并且返回一个File[]数组,本文中没有接受该数组,因为在判断过程中就已经可以将.class文件加入Set集合了。
FileFilter是一个接口,接口中只有一个accept方法需要实现,该方法返回值类型为boolean,重写accept方法时需要写出判断逻辑,因为File.listFiles方法就是依靠accept方法的返回值来判断文件是否要添加到File[]数组中。本文直接在accept中判断文件类型,如果为文件夹则递归调用findLocalClasses(String)方法向下查找,如果是.class文件则添加到Set集合。
File中的listFiles方法源码如下:
public File[] listFiles(FileFilter filter) {
String ss[] = list();
if (ss == null) return null;
ArrayList<File> files = new ArrayList<>();
for (String s : ss) {
File f = new File(s, this);
if ((filter == null) || filter.accept(f))
files.add(f);
}
return files.toArray(new File[files.size()]);
}
目标类信息封装
本部分操作均在AnnotationApplicationContext的getBeanDefinitions(String)方法下完成。
@Component注解
此处模仿Spring IoC,在目标路径下,打上了@Component注解注解的类表示该类交由IoC容器管理,在SpringBoot中经常使用的@Service、@Repository等注解与本文实现的原理大致相同,只是针对不同功能的类有不同的处理,本文只实现@Component。
笔者仿写@Component注解中value值设置了默认值,为的就是在使用@Component注解时,可以使用@Component(“xxxx”)指定beanName,也可以直接使用@Component使用默认值,直接使用时会将类名首字母小写作为默认beanName,处理详见下文。
@Component注解代码如下:
// 标注某个类要交给ioc容器处理
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
// 默认设"" 允许不设值 当@Component注解内未传值 在AnnotationApplicationContext内经过转换 将类的首字母小写后作为beanName值
String value() default "";
}
使用示例如下:
//此时默认beanName为user
@Component
public class User{
//...
}
//此时beanName为指定字符串USER
@Component("USER")
public class User{
//...
}
BeanDefinition类
BeanDefinition类用于保存每个受管对象的Class对象和类名beanName。
代码如下:
public class BeanDefinition {
private String beanName;
private Class beanClass;
public BeanDefinition(String beanName, Class beanClass) {
this.beanName = beanName;
this.beanClass = beanClass;
}
public BeanDefinition(){}
// getter和setter方法省略...
}
将目标类封装为BeanDefinition
getBeanDefinitions(String)方法中会先调用扫包工具类获取目标路径下的所有.class文件的Class对象,并使用反射技术获取每个Class类对象的注解,如果发现包含@Component注解,说明该对象应交给IoC容器管理,将Class和beanName封装成BeanDefinition添加到Set集合,最终方法返回Set集合。
getBeanDefinitions(String)方法代码如下:
/**
* @MethodName: getBeanDefinitions
* @Description: 先通过包扫描获取全部的类Class对象 随后判断类是否具有@Component注解
* 如果有 则将其封装为一个BeanDefination对象 并加入到Set容器中
* @Param: [packageName]
* @Return: java.util.Set<Spring.SpringIoc.beans.factory.config.BeanDefinition>
**/
public Set<BeanDefinition> getBeanDefinitions(String packageName){
// 获取指定包下所有类的集合
Set<Class<?>> clazz = getClasses(packageName);
Set<BeanDefinition> beanDefinitions = new HashSet<>();
for(Class c : clazz){
// 通过反射获取当前Class对象的@Component注解
Component component = (Component) c.getAnnotation(Component.class);
//component不为null说明类被打上了IoC注解
if(component != null){
String beanName = component.value();
// 如果@Component注解内未赋值value
if(beanName.equals("")){
String className = c.getSimpleName();
// 将类名首字母小写后的字符串作为beanName
beanName = className.substring(0,1).toLowerCase() + className.substring(1,className.length());
}
beanDefinitions.add(new BeanDefinition(beanName,c));
}
}
return beanDefinitions;
}
动态创建bean对象
我们现在获取了目标路径下的所有受管对象的必要信息,现在可以开始根据这些信息创建bean对象了,本部分操作全部在AnnotationApplicationContext的createObject(Set<BeanDefinition>)方法下完成。
@Value注解
在创建bean对象时,开发人员期待可以对对象进行初始化操作,仍然通过注解+反射实现这种操作, 详细见下文。
@Value注解代码如下:
//创建对象过程中 为ioc容器中的对象标注了@Value注解的字段赋值
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value {
String value();
}
@Value注解代码中可以窥见一个问题,就是我们的value值是被写死为String的,因此处理时需要判断目标字段的类型并进行正确的类型转换。
使用示例如下:
@Value("1")
private Long id;
@Value("admin")
private String username;
创建bean对象
实现过程中,由于只探讨/仿写IoC核心部分,笔者对两个细枝末节的地方进行了简化。
- 默认bean对象均使用单例模式。
- 对于@Value注解,只处理了Long的类型转换,并且没有添加任何关于传入值的判断,期望使用人员每次传入的值都是合理正确的。Long类型除外的类型转换与本文的Long类型处理完全一致。
createObject(Set<BeanDefinition>)方法代码如下:
/**
* @MethodName: createObject
* @Description: 根据BeanDefinition 利用反射创建对象 检查@Value注解赋值 并加入到Map集合中
* @Param: [beanDefinitions]
* @Return: void
**/
public void createObject(Set<BeanDefinition> beanDefinitions){
for(BeanDefinition b : beanDefinitions){
Class c = b.getBeanClass();
//确保bean唯一 本实现默认bean单例
if(!beans.containsKey(b.getBeanName())) {
try {
// 通过反射获取Class对象的构造函数并创建实例
Object obj = c.getConstructor().newInstance();
//接下来检查字段是否有@Value注解 如果有 则执行赋值操作
Field[] declaredFields = c.getDeclaredFields();
for (Field f: declaredFields) {
Value value = f.getAnnotation(Value.class);
if(value != null){
String fieldName = f.getName();
// 反射获取对应字段setter方法
Method method = c.getMethod("set" + fieldName.substring(0,1).toUpperCase()
+ fieldName.substring(1,fieldName.length())
,f.getType());
// 由于@Value的value是固定String类型 针对不同字段类型需要分别处理
if(f.getType().equals(Long.class)){
method.invoke(obj,Long.parseLong(value.value()));
}else if(f.getType().equals(String.class)){
method.invoke(obj,value.value());
}//其他类型判断写法相同
}
}
//将bean加入AnnotationApplicationContext类下的Map集合
beans.put(b.getBeanName(), obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
bean的自动装载
到了这一步,我们成功创建了目标类的对象,并且为部分字段做了初始化处理,bean对象已经接近可以使用了,此部分需要解决最后一个问题,即将bean注入到其他bean中,bean的自动装载。翻译成人话就是,我们的bean对象中可能会引用其他的bean对象,我们需要对这部分对象进行初始化处理(注入)。
Spring中提供了两种方法进行bean注入
- byName:顾名思义就是直接通过指定beanName注入。
- byType:顾名思义就是查找匹配的类型进行bean注入。
本部分操作全部在AnnotationApplicationContext的autowireObject(Set<BeanDefinition>)方法下完成。
@Qualifier注解
先从较为简单的byName入手,下文方法代码内读者也可以先挑读Qualifier部分。
@Qualifier注解代码如下:
//byName通过beanName查找bean注入
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifier {
String value();
}
使用示例如下:
@Component
public class User{
// ...
@Qualifier("relationship")
private Relationship relationship;
}
@Autowired注解
@Autowired注解模仿Spring IoC添加一个判断是否必要注入的字段required,默认false不需要。
@Autowired注解代码如下:
//byType通过类型查找bean注入
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
//该值表示当前要进行的bean注入是否必要
boolean required() default false;
}
使用示例如下:
public class User{
// ...
@Autowired(required = true)
private Role role;
}
自动装载
autowireObject(Set<BeanDefinition>)方法代码如下:
/**
* @MethodName: autowireObject
* @Description: 自动装配被@Autowired或@Qualifier修饰的字段
* @Param: [beanDefinitions]
* @Return: void
**/
public void autowireObject(Set<BeanDefinition> beanDefinitions){
for(BeanDefinition b : beanDefinitions){
Class c = b.getBeanClass();
//通过反射获取目标类字段
Field[] declaredFields = c.getDeclaredFields();
for(Field f : declaredFields){
Autowired autowired = f.getAnnotation(Autowired.class);
Qualifier qualifier = f.getAnnotation(Qualifier.class);
//注入目标对象
Object targetObj = beans.get(b.getBeanName());
//要被注入的bean对象
Object obj = null;
try {
String fieldName = f.getName();
Method method = c.getMethod("set" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1, fieldName.length())
, f.getType());
if (qualifier != null) {
//byName
obj = beans.get(qualifier.value());
if (obj == null) {
//未找到指定类直接返回 可以添加一些信息提示和错误处理
return;
}
method.invoke(targetObj,obj);
} else if (autowired != null) {
//byType
String fieldType = f.getType().getTypeName();
//获取beans内所有bean对象 并遍历判断匹配项
Collection<Object> values = beans.values();
// 判断f.getType() 是否是 values内某对象的子类
//1.无匹配 检查required并进行后续处理
//2.最终单个匹配 直接bean注入
//3.多项匹配 则根据属性名查找
//3-1可以通过属性名查找到 直接bean注入
//3-2否则检查required并进行后续处理
for(Object o : values){
//isAssignableFrom方法判断 f.getType()是否为o.getClass()的子类
if(o.getClass().isAssignableFrom(f.getType())){
if(obj == null){
obj = o;
}else{
//多个匹配项
String fieldTypeName= f.getType().getSimpleName();
String beanName = fieldTypeName.substring(0,1).toLowerCase()
+ fieldTypeName.substring(1,fieldTypeName.length());
if(beans.containsKey(beanName)){
obj = beans.get(beanName);
}else{
//多项匹配且无法通过属性名匹配具体项
obj = null;
}
break;
}
}
}
if(obj == null){
//如果autowired的required被设为必须 则抛出异常
if(autowired.required()){
//抛出注入失败异常 此处不演示
}
}else{
//找到了匹配项
method.invoke(targetObj,obj);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
/**
* @MethodName: getBean
* @Description: 通过beanName获取实例
* @Param: [beanName]
* @Return: java.lang.Object
**/
public Object getBean(String beanName){
return beans.get(beanName);
}
至此我们的IoC框架的简单仿写已经彻底完成,从零手动实现了控制反转,下面演示代码的测试。
测试
代码结构
目录结构如下:
测试类
最总测试时entity类下三个用于测试的模拟类代码如下:
User类:
@Component
public class User{
public User() {
}
@Value("1")
private Long id;
@Value("admin")
private String username;
private String password;
@Qualifier("relationship")
private Relationship relationship;
@Autowired(required = true)
private Role role;
//getter和setter方法省略...
}
Role类:
@Component("role")
public class Role extends Relationship{
//与文首给出内容无差异 省略...
}
Relationship类:
@Component
public class Relationship {
//与文首给出内容无差异 省略...
}
测试结果
Main类代码准备如下:
public class Main {
public static void main(String[] args) {
AnnotationApplicationContext annotationApplicationContext = new AnnotationApplicationContext("Spring.SpringIoc.entity");
//注入测试
User user = (User) annotationApplicationContext.getBean("user");
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getRole());
System.out.println(user.getRelationship());
}
}
打印结果如下:
1
admin
null
Spring.SpringIoc.entity.Role@4b1210ee
Spring.SpringIoc.entity.Relationship@4d7e1886
由打印结果可见我们从零仿写的IoC成功完成了对象的管理、注入等工作,除展示的测试部分除外,剩余各部分代码笔者均已测试无误。