万字教学 | 基于注解实现的Spring IoC容器/框架简单仿写

简述

本文将对Spring IoC容器的仿写进行剖析,梳理仿写过程中需要解决的问题,并附全部仿写代码进行说明教学,本文的仿写中不引用任何外部类,纯手动实现简要IoC控制反转执行流程。

对IoC的作用不熟悉或不了解什么是控制反转的读者,可以先参考笔者先前的文章或查阅其他资料:IoC控制反转的原理思想

仿写Spring IoC需要解决哪些问题

想要顺利并且理解的仿写一个IoC框架,在动手前应该先理清要解决的问题,或者说IoC框架的执行流程。

笔者将IoC执行流程大致归纳为了以下几个步骤:

  1. 对指定包路径进行扫描,找出所有添加了IoC注解的目标类。
  2. 获取目标类的信息,Class对象以及类名beanName,并将其封装以便后续使用。
  3. 根据封装好的信息类动态创建bean对象。
  4. 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成功完成了对象的管理、注入等工作,除展示的测试部分除外,剩余各部分代码笔者均已测试无误。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7rulyL1ar

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值