有手就行系列——Spring自动注入的简单实现(新手向)(1)

前言

  最近学习了Spring的自动注入机制,纸上谈来终觉浅,打算自己来实现一个简单的自动注入机制,加深理解。这篇博客一方面用于反思所学的知识,另一方面希望能给各位朋友带来一些启发。如有错误/问题,欢迎评论区指出。

  每一章都会在前一版本的基础上进行功能的完善与改进,如果有想法的话,也可以在评论区提出来。

一、本文所要实现的效果

  在使用lombok、假设所注入目标全为多例(prototype )的情况下,采用实现自动注入的简单情况。包含以下功能:

注解作用
@Autowird在类中自动注入
@Component把bean进行注册
@ComponentScan确认扫描bean的路径

  实现完成后,做到可以使得如下程序正常运行。
Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        //todo 获取注册完成后,储存的bean的信息
        ApplicationContext applicationContext=
                new ApplicationContext(AppConfig.class);
        //todo 获取我们的注册文件
        Test test=(Test) applicationContext.getBean("test");
        //测试
        test.output();
        test.setUserService(test.getUserService().setUsername("Han Meme"));
        test.output();
    }
}

Test.java

@Component("test")
public class Test {

    @Autowired
    private UserService userService;

    public Test() {
        userService = null;
    }

    public void output(){
        assert userService != null;
        if (userService.getUsername()==null) {
            userService.setUsername("Li Ming");
        }
        System.out.println(userService);
    }
	//此处省略Setter、Getter、toString方法
}

UserService.java

@Component("userService")
public class UserService {
    private String username=null;

    public UserService() {
    }

    @Override
    public String toString() {
        return "UserService{" +
                "username='" + username + '\'' +
                '}';
    }
    //此处省略Setter、Getter、toString方法
}

二、本自动注入机制的结构

│  Main.java
│
├─Config
│      AppConfig.java
│
├─Bean
│  │  Test.java
│  │  UserService.java
│  
└─Ioc
   │  ApplicationContext.java
   │  Autowired.java
   │  BeanDefinition.java
   │  Component.java
   │  ComponentScan.java
文件名作用
ApplicationContext.java包含对所有Bean的扫描、注册、注入、反射等功能
AppConfig.java对自动注入机制的相关配置
BeanDefinition.java储存Bean的单元

三、思路+实现

3.1 对配置类的实现

  这里采用注解的方式对配置类(AppConfig.java)进行配置。在本文中,我们所需配置的内容仅为Bean的扫描路径。

AppConfig.java

@ComponentScan("com/IocV1/Ioc")
public class AppConfig {
	
}

ComponentScan.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ComponentScan {
    String value() default "";
}

BeanDefinition.java

public class BeanDefinition {
    private String scope;
    private Class<?> beanClass;
    //此处省略Setter、Getter、toString方法
}

3.2 实现自动装配机制

3.2.1获取扫描路径

获取的流程为:

  1. 向装配类传入配置类
  2. 获取配置类上对应的注解所标注的路径
  3. 将获得的路径转换为File文件的格式

具体到代码上即为:

private File position(Class<?> Config) {
    //判断有没有对应的注解
    if (Config.isAnnotationPresent(ComponentScan.class)) {
        //获取扫描路径
        ComponentScan componentScan = Config.getAnnotation(ComponentScan.class);
        String scanPath = componentScan.value();
        //转化为标准路径格式
        scanPath = scanPath.replace('.', '/');
        //获取File
        ClassLoader classLoader = ApplicationContext.class.getClassLoader();
        URL resource = classLoader.getResource(scanPath);
        assert resource != null;
        return new File(resource.getFile());
    }else {
        return null;
    }
}

3.2.2实现对Bean的扫描机制

扫描的流程为:

  1. 对目录进行遍历,获取所有java文件
  2. 检查该java文件是否含有@Component注解
  3. 将含有@Component注解的java文件对应的Class存入数组
  4. 返回储存Class的数组

具体到代码上即为:

private List<Class<?>> scan(File file) {
	//建立储存的数组
    List<Class<?>> classList = new ArrayList<>();
    ClassLoader classLoader = ApplicationContext.class.getClassLoader();
    //获取该目录下所有文件
    File[] files = file.listFiles();
    assert files != null;
    for (File f : files) {
        if(f.isDirectory()){//如果是目录,就进行递归
            classList.addAll(scan(f));
        }else {
        	//如果不是目录,先判断这是不是class文件,若不是则跳过
            if(!f.getName().endsWith("class"))
                continue;
            //获取该文件绝对路径
            String absolutePath = f.getAbsolutePath();
            absolutePath = absolutePath
                    .substring(
                            absolutePath.indexOf("com"),
                            absolutePath.indexOf(".class")
                    ).replace("\\", ".");
            try {
            	//根据路径获取该文件的Class并储存
                Class<?> clazz = classLoader.loadClass(absolutePath);
                if (clazz.isAnnotationPresent(Component.class)) {
                    classList.add(clazz);
                }
            } catch (Exception ignored) {}
        }
    }
    return classList;
}

3.2.3 对ApplicationContext进行初始化,完成扫描与注册

初始化过程为:

  1. 获取扫描路径
  2. 获取扫描结果
  3. 对扫描结果根据每个class中注解规定的名称进行注册(也就是byName)
private ConcurrentHashMap<String, BeanDefinition> beanDefinitionHashMap =
        new ConcurrentHashMap<>();
        
//ApplicationContext构造器
public ApplicationContext(Class<?> Config){
	//获取路径
    File BeanPath=position(Config);

	//获取扫描结果
    assert BeanPath != null;
    List<Class<?>> classList=scan(BeanPath);

	//进行注册
    for(Class<?> clazz:classList){
        BeanDefinition beanDefinition=new BeanDefinition();
        //确认是否为多例
        if (clazz.isAnnotationPresent(Scope.class)) {
            beanDefinition.setScope(clazz.getAnnotation(Scope.class).value());
        }else {
            beanDefinition.setScope("prototype");
        }
        beanDefinition.setBeanClass(clazz);
        beanDefinitionHashMap.put(
                clazz.getAnnotation(Component.class).value(),beanDefinition);
    }
}

3.2.4 编写getBean(String beanName),即通过名称获取Bean

获取流程:

  1. 确认是否存在该Bean,获得该名称对应的beanDefinition
  2. 获取该beanDefinition中的Class
  3. 对该Class通过无参构造器进行反射获取object
  4. 通过反射,获取该object的所有变量,检查是否有变量存在Autowired注解
  5. 若存在,则先设定该属性可外部访问,然后通过getBean进行该Bean的注入
  6. 注入完成后,返回该object
public Object getBean(String beanName) throws Exception {
	//获得beanName对应的beanDefinition
    BeanDefinition beanDefinition=beanDefinitionHashMap.get(beanName);

    Object object=null;
	//获取该beanDefinition中的Class
    if ("prototype".equals(beanDefinition.getScope())) {
        try {
            object=beanDefinition.getBeanClass().getConstructor().newInstance();
        }catch (Exception ignored){}
    }

	//对该示例的内部进行自动注入
    assert object != null;
    attriAssign(object);

    return object;
}

private void attriAssign(Object object) throws Exception {
	//获取所有的内部变量
    Class<?> classInfo = object.getClass();
    Field[] declaredFields = classInfo.getDeclaredFields();

	//对所有变量进行获取
    for (Field field : declaredFields) {
        Autowired extResource = field.getAnnotation(Autowired.class);
        //如果这个变量需要进行自动注入
        if (extResource != null) {
        	
            String beanId = field.getName();
            Object bean = getBean(beanId);
            if (bean != null) {
				//设定该属性可外部访问
                field.setAccessible(true);
				//注入
                field.set(object, bean);
            }
        }
    }
}

3.3 在使用时的案例

  我在这里可以写两个名称为test和userService的bean

@Component("test")
public class Test {

    @Autowired
    private UserService userService;
	//其余内部结构见开头
}
@Component("userService")
public class UserService {
	//内部结构见开头
}

  然后在测试时,可以先对自动注入进行初始化,然后获取对应的bean,在获取的同时对该bean内部完成自动注入。获取成功后,可以对该bean进行各种正常操作。

public class Main {
    public static void main(String[] args) throws Exception {
        //获取我们的注册文件
        ApplicationContext applicationContext=
                new ApplicationContext(AppConfig.class);
        //获取注册完成后,储存的bean的信息
        Test test=(Test) applicationContext.getBean("test");
        //测试
        test.output();
    }
}

  ok,到这里,你已经从无到有的实现了一个自动注入机制,虽然它还有很多明显的缺陷,但还是个不错的开头(好歹能用)

四、总结

  到这里,我们已经自己完成了一个自动注入机制的简单实现,他还有很多问题,比如:

  • 如何解决循环Autowired的问题
  • 如何扫描多文件夹下的bean
  • 对于程序内抛出的各种异常的处理
  • 如何使用xml对该注入机制进行配置
  • 如何对单例实现注册与注入

  我会在该系列下一节中,继续完善这个机制。

(各位朋友如果感觉有收获的话,点个赞+关注一下呗)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值