Spring IOC好神奇?我教你实现一个

目录

前言

在阅读本文之前,你必须:

  • 掌握Java语法
  • 掌握Java反射的用法
  • 掌握IOC的概念,并用过SpringIOC功能

预期达到的效果:

  • 能正确加载配置文件来确定扫描包的范围
  • 能正确识别我们自定义的注解,初始化自定义的IOC容器
  • 能正确装配Bean

全部代码皆可戳本github仓库链接找到。

一、定义注解

我们首先定义4个注解,分别是@Repository、@Service、@Autowired和@Qualifier,熟悉Spring的同学肯定也对这些注解很熟悉。

  • @Repository和@Service都是用来将类标识为Bean,功能上区别不大,只是方便更好区分不同的类各自的功能,分别对应存储层Bean和业务层Bean
  • @Autowired根据对象的类名来查找IOC容器中的Bean,并装配
  • @Qualifier根据指定的BeanName来查找IOC容器中的Bean,并装配
@Target(value = ElementType.TYPE) // 该注解只能用在类上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Repository {
    String name() default ""; // 配置BeanName
}

@Target(value = ElementType.TYPE) // 该注解只能用在类上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Service {
    String name() default ""; // 配置BeanName
}

@Target(value = ElementType.FIELD) // 该注解只能用在成员变量上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Autowired {

}

@Target(value = ElementType.FIELD) // 该注解只能用在成员变量上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Qualifier {
    String value() default ""; // 配置BeanName
}

二、实现IOC功能

写具体代码之前,首先分析一下实现一个IOC功能,需要划分为几个步骤:

  1. 加载配置文件
  2. 扫描指定包下的.class文件
  3. 初始化IOC容器
  4. 装配Bean

将步骤划分清楚后,我们可以先把对应的函数创建出来,并在构造函数里调用:

public class AnnotationBeanFactory{
  
    private Properties properties; // 配置信息
    private Map<String, Object> ioc = new HashMap<String, Object>(); // IOC容器
    private List<String> classNames = new ArrayList<String>(); // 扫描出的.class文件
    private String basePackage; // 指定扫描的包
  
    public AnnotationBeanFactory(String configPath) {
    	loadConfig(configPath);
        scanner(basePackage);
        initIoC();
        inject();
    }

    private void inject(){
  	  // 装配Bean
    }

    private void initIoC(){
  	  // 初始化IOC容器
    }

    private void scanner(String basePackage){
	  // 扫描指定包下的.class文件
    }

    private void loadConfig(String configPath){
     // 加载配置文件
    }
}

1.加载配置文件

按照Spring的模式,我们会将配置信息写在XML里,但是这里我们一切从简,将配置信息存放位置设置在properties文件中,且只读取ScannerPackage这个熟属性。读取配置文件信息的方法如下:

    private void loadConfig(String configPath) {
        properties = new Properties();
        InputStream is = getClass().getClassLoader().getResourceAsStream(configPath); // 根据路径寻找properties文件
        try {
            properties.load(is); // 加载properties
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close(); // 关闭输入流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        basePackage = properties.getProperty("ScanPackage"); // 读取配置文件中指定扫描的package
    }

扫描包下的.Class文件

接下来就要根据之前读取的properties文件中的ScanPackage属性,定位到需要扫描的目录位置。主要是通过File对象,使用递归来遍历整个目录

 private void scanner(String basePackage) throws FileNotFoundException {
        String path = "/" + basePackage.replaceAll("\\.", "/");
        URL url = getClass().getResource(path);
		
		// 用户配置的需要扫描的包有误,抛出异常
        if (url == null) throw new FileNotFoundException("package " + path + " not exists");

        File dir = new File(url.getFile());

        for (File file : dir.listFiles()) {

            if (file.isDirectory()) { // 如果当前File是文件夹,则递归遍历
                scanner(basePackage + "." + file.getName());
            }

            if (!file.getName().endsWith(".class")) { // 当前File不是.class文件,则跳过不管
                continue;
            }
            
            // 将读取到的.class文件的全限定名放入List中
            classNames.add(basePackage + "." + file.getName().replace(".class", "")); 
        }

    }

初始化IOC容器

IOC容器其实没有那么神秘,本质上是一个HashMap,以BeanName为键,以Object为值。
首先遍历上一步扫描到的.class文件,通过反射检查当前遍历到的class是否有@Repository或者@Service注解,没有的就跳过不管,有就进入下一步操作。

接下来要确定BeanName值,如果用户指定了具体的BeanName,则以用户为准,否则使用类的名称作为BeanName,首字母小写,并将BeanName作为,通过反射创建的实例作为putIOC容器。

另外,如果是@service注解,且用户没有特别指定BeanName的值,那么我们就用反射寻找这个类实现的接口,并遍历,把所有接口的名称作为BeanName

具体实现代码如下:

private void initIoC() throws Exception, ClassNotFoundException, IllegalAccessException, InstantiationException {

        for (String className : classNames) { // 遍历扫描到的.class文件的全限定名
            Class clazz = Class.forName(className); // 通过反射加载class

            if (clazz.isAnnotationPresent(Repository.class)) { // 如果class被@Repository标记

                Repository repository = (Repository) clazz.getAnnotation(Repository.class);// 加载注解的内容
                String beanName = repository.name();
                if ("".equals(beanName)) { // 没有特别指定BeanName
                    beanName = lowerFirst(clazz.getSimpleName().replace(".class", ""));
                }
                if (ioc.containsKey(beanName)) { // 已有重复的BeanName,抛出异常
                        throw new Exception("bean " + beanName + " already exists");
                    }
                ioc.put(beanName, clazz.newInstance()); // 放进IOC容器

            } else if (clazz.isAnnotationPresent(Service.class)) { // 如果class被@Service标记

                Service service = (Service) clazz.getAnnotation(Service.class);
                Class<?>[] interfaces = service.getClass().getInterfaces();// 获得该class实现的所有接口
                for (Class<?> i : interfaces) { 
                    String beanName = service.name();
                    if ("".equals(beanName)) { // 没有特别指定BeanName
                        beanName = lowerFirst(i.getSimpleName().replace(".class", ""));
                    }
                    if (ioc.containsKey(beanName)) { // 已有重复的BeanName,抛出异常
                        throw new Exception("bean " + beanName + " already exists");
                    }
                    ioc.put(beanName, clazz.newInstance()); // 放进IOC容器
                }

            }
        }

    }

   /**
     * 将类名首字符变为小写
     */
   private String lowerFirst(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return new String(chars);
    }

装配bean

容器初始化完毕后,则需要将bean装配到用户用@Autowired或者@Qualifier标记的成员变量上。

我们通过遍历IOC容器中的所有实例,获得实例中的每一个成员变量,如果某变量被@Autowired或者@Qualifier标记,那么我们就通过BeanName作为键值,去IOC容器get相应的实例,并通过反射注入给对应成员变量

private void inject() throws IllegalAccessException, Exception {

        for (Map.Entry<String, Object> entry : ioc.entrySet()) { // 遍历IOC容器

            Class clazz = entry.getValue().getClass(); // 反射获得class
            if (!clazz.isAnnotationPresent(Repository.class) && !clazz.isAnnotationPresent(Service.class)) {
                continue; // 如果没有被`@Autowired`或者`@Qualifier`标记,跳过不管
            }

            Field[] fields = clazz.getDeclaredFields();// 通过反射获得所有成员变量

            for (Field field : fields) { // 遍历成员变量

                field.setAccessible(true); // 重要!如果没有这一段代码,则不能向private变量注入

                if (field.isAnnotationPresent(Autowired.class)) { // 如果是`@Autowired`,则通过类名注入
                    String beanName = lowerFirst(field.getType().getSimpleName().replace(".class", ""));

                    if (!containsBean(beanName)) { // IOC容器中找不到相应的Bean,抛出异常
                        throw new Exception("Bean " + beanName + " not exist");
                    }

                    field.set(entry.getValue(), ioc.get(beanName)); // 注入实例
                } else if (field.isAnnotationPresent(Qualifier.class)) {
                    Qualifier qualifier = field.getAnnotation(Qualifier.class);
                    String beanName = qualifier.value();
                    if ("".equals(beanName)) { // 用户没有特别指定BeanName,则通过类名注入
                        beanName = lowerFirst(field.getType().getSimpleName().replace(".class", ""));
                    }

                    if (!containsBean(beanName)) { // IOC容器中找不到相应的Bean,抛出异常
                        throw new Exception("Bean " + beanName + " not exist");
                    }

                    field.set(entry.getValue(), ioc.get(beanName));// 注入实例
                }
            }
        }
    }
    // 判断IOC容器中是否有相应的Bean
    public boolean containsBean(String beanName) {
        return ioc.containsKey(beanName);
    }

三、测试功能

将代码通过IDE打包成jar之后,新建一个工程,引入该jar,编写如下测试代码:

配置文件:

ScanPackage=org.dylan.application

测试类:

package org.dylan.application;


import org.dylan.application.serviceI.ServiceI;
import org.dylan.springframework.factory.AnnotationBeanFactory;
import org.dylan.springframework.factory.BeanFactory;

public class Main {


    public static void main(String[] args) {
        BeanFactory factory = new AnnotationBeanFactory("properties.properties");
        ServiceI serviceI = (ServiceI) factory.getBean("service");
        System.out.println(serviceI.query());
    }

}

serviceI接口:

package org.dylan.application.serviceI;
public interface ServiceI {
    String query();
}

service实现类:

package org.dylan.application.service;


import org.dylan.application.dao.Dao;
import org.dylan.application.serviceI.ServiceI;
import org.dylan.springframework.annotation.Autowired;

@org.dylan.springframework.annotation.Service
public class Service implements ServiceI {
   
    @Autowired
    private Dao dao; // 自动装配

    public String query() {
        return dao.query();
    }
}

DAO类:

package org.dylan.application.dao;


import org.dylan.springframework.annotation.Repository;

@Repository
public class Dao {
    public String query() {
        return "注入成功";
    }
}

运行结果:

注入成功
Process finished with exit code 0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黄嘉成

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

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

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

打赏作者

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

抵扣说明:

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

余额充值