【Spring】重构--手写Spring核心逻辑(二)实现IOC/DI(beans包)

系列文章:

上篇我们介绍了手写框架的项目结构,这篇我们就从 IOC/DI 开始,先完成 beans 包的内容。

在这里插入图片描述
beans 包中一般放的是配置、规范、标准等,关于 IOC 容器的具体逻辑实现是在 context 包中。

1.MYBeanFactory

BeanFactory是顶层设计,相当于规范了IOC容器的功能。

public interface MYBeanFactory {
	// 通过beanName获取bean
    Object getBean(String beanName) throws Exception;
	// 通过Class获取bean
    Object getBean(Class<?> beanClass) throws Exception;
}

注意,IOC 容器默认是单例模式。

2.MYFactoryBean

FactoryBean 工厂Bean,在我们的手写框架中没用到,暂时先不写。

public interface MYFactoryBean {
}

关于 BeanFactory 和 FactoryBean 的区别可以看这篇 【Spring】IOC&DI:FactoryBean和BeanFactory分析对比

3.MYBeanDefinition

用来保存Bean的信息,包括实际类信息和配置信息。在我们平时使用Spring时,xml中有许多能配置的选项,但是为了简单我们这里只包括了 beanName,class,isLazyInit,isSigleton 这四项。

在通过getBean获取Bean实例时,首先要拿到这里实例的类信息(BeanDefinition)。

这里明确下三组对应关系:

  • 一个Class对应多个BeanDefinition,比如Abean,Bbean属于同一Class的实例,
    • beanName不同:Abean 的 beanName 是类名小写,Bbean 的 beanName 是其它的某个接口首字母小写
    • isLazyInit不同:Abean 不是懒加载,Bbean是懒加载
    • isSigleton 不同:Abean 是单例模式,Bbean 是多例模式
  • 一个 BeanDefinition 对应一个 Bean
  • 一个 Bean 有两种获取方式(从 BeanFactory#getBean() 可以看出),所以一个 BeanDefinition 也有两种获取方式
    • ByName:Name 来源有两个
      • 默认:所属类名(首字母小写)
      • 自定义:用户在将 bean 交给 IOC 容器时,注解中自己定义的 beanName
    • ByType:当一个 type 下有多个 bean(BeanDefinition)时,只会返回最后一个(覆盖)

注意,对于有接口的类,还需要构建其父接口的 BeanDefinition

  • beanName:接口名(首字母小写)
  • beanClass:实现类
public class MYBeanDefinition {

	// factoryBeanName:即每个类的对象应该用什么具体工厂bean创建 --> 一个Bean对应一个工厂
    // 注意:但是在这里,factoryBeanName 用来保存beanName的,同时作为bean的唯一标识!!!
    private String factoryBeanName;

    // 全类名
    // 为了后面通过反射拿到 Class对象,然后创建实例和注解判断
    private String beanClassName;

    // 懒加载,默认false(后面构建BeanDefinition时就可以不用管了)
    private boolean isLazyInit = false;
    // 单例模式,默认true
    private boolean isSigleton = true;
	
	// getter、setter...
    public String getBeanClassName() {
        return beanClassName;
    }

    public void setBeanClassName(String beanClassName) {
        this.beanClassName = beanClassName;
    }

    public String getFactoryBeanName() {
        return factoryBeanName;
    }

    public void setFactoryBeanName(String factoryBeanName) {
        this.factoryBeanName = factoryBeanName;
    }

    public boolean isLazyInit() {
        return isLazyInit;
    }

    public void setLazyInit(boolean lazyInit) {
        isLazyInit = lazyInit;
    }

    public boolean isSigleton() {
        return isSigleton;
    }

    public void setSigleton(boolean sigleton) {
        isSigleton = sigleton;
    }
}

4.MYBeanDefinitionReader

两个作用:

  1. 加载配置文件,在Spring中本来是由Resource相关类进行解析,而这里为了简便就都放在这一个类中了
  2. 将配置文件组装成Beandefinition

为了扩展,这里本来还应有一个统一接口,然后应用策略模式,但为了方便这里只加载properties文件

public class MYBeanDefinitionReader {
	
	// 保存所有Bean的class信息(全类名)
    private List<String> registerBeanClasses = new ArrayList<String>();

    private Properties config = new Properties();

    // 定义Properties文件中要扫描包的 key,相当于一种规范
    private final String SCAN_PACKAGE = "scanPackage";
	
	// 构造函数,传入要加载配置文件的路径
    public MYBeanDefinitionReader(String... locations) {
        // 根据url读取配置文件,加载成io流
        // 注:这里因为知道要加载properties文件,所以取[0]
        //     删掉classpath是因为,在传入配置文件位置时,常见写法是 classpath:application.Properties
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(locations[0].replace("classpath:", ""));
        // 通过Properties类,将IO流解析成properties,然后关闭IO流
        try {
            config.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // 对指定包进行扫描
        doScanner(config.getProperty(SCAN_PACKAGE));
    }
}

doScanner()

扫描指定包,得到指定包下所有类的全类名,本质是文件操作

private void doScanner(String scanPackage) {
    // 1.获取需要扫描包的File对象
    // 这里不能直接new File,因为new File需要的是相对路径或绝路径,而上一步得到的包名什么都不是,所以我们的策略是先获取URL对象(里面封装了File对象)。
    // 注:getResource方法返回的是URL对象,用来获取指定类或包的绝对路径
    //    getResource获取绝对路径,首先要将包名转化成项目相对路径。而最前面 / 表示从根路径中寻找(显然要找到这个包不能从当前目录下寻找)
    URL url = this.getClass().getClassLoader().
            getResource("/" + scanPackage.replaceAll("\\.", "/"));
	// 通过getFile获取到要扫描包的File对象(文件夹)
    File classpath = new File(url.getFile());

    // 2.遍历文件夹,寻找class文件
    for (File file : classpath.listFiles()) {
        if (file.isDirectory()) {
            // 这里是通过递归遍历文件夹,还是包就再执行上述步骤(解析路径->创建目录->遍历)
            doScanner(scanPackage + "." + file.getName());
        } else {
            // 不是class文件的不管
            if (!file.getName().endsWith("class")) {continue;}
            // 这里要保存全类名(包.类名),因为后面要通过反射Class.forName获取Class对象
            String className = (scanPackage + "." + file.getName()).replace(".class", "");
            registerBeanClasses.add(className);
        }
    }
}

loadBeanDefinitions()

将Scanner方法读取出来的类(名)转换成 BeanDefinition

public List<MYBeanDefinition> loadBeanDefinitions() {
	// 保存构建的所有 BeanDefinition 
    List<MYBeanDefinition> result = new ArrayList<MYBeanDefinition>();
    try {
    	// 扫描所有加载进来的类
        for (String className : registerBeanClasses) {
            Class<?> clazz = Class.forName(className);
            // 接口不能实例化(相当于不能有factoryBean),不处理
            if (clazz.isInterface()) continue;

            // 一个Class对应多个BeanDefinition,其中还有一个原因就是 beanName 这里可以分为三类
            // 1.beanName是对应类的类名(默认)
            result.add(doCreateBeanDefinition(toLowerFirstCase(clazz.getSimpleName()), clazz.getName()));
            // 2.beanName是接口名(当前类存在接口时)
            Class<?>[] interfaces = clazz.getInterfaces();
            for (Class<?> i : interfaces) {
                // 注:这里若一个接口有多个实现类,那么后扫描会覆盖先扫描的(相当于只能保存一个实现类的)
                //    这种情况下,可以通过注入Bean时指定name解决
                result.add(doCreateBeanDefinition(i.getName(), clazz.getName()));
            }
            // TODO 
            // 3.beanName是用户自定义的(一般配合@Resource注解)
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}
// 再封装了构建 BeanDefinition 的方法
private MYBeanDefinition doCreateBeanDefinition(String factoryBeanName, String beanClassName) {
    MYBeanDefinition myBeanDefinition = new MYBeanDefinition();
    myBeanDefinition.setBeanClassName(beanClassName);
    myBeanDefinition.setFactoryBeanName(factoryBeanName);
    return myBeanDefinition;
}
// 将首字母转为小写
private String toLowerFirstCase(String simpleName) {
    char [] chars = simpleName.toCharArray();
    chars[0] += 32;
    return String.valueOf(chars);
}

getConfig()

返回保存了配置文件内容的Properties对象,拿到该对象相当于拿到了配置文件的内容

public Properties getConfig() {
      return this.config;
}

后面 aop 的切面等信息和 mvc 的页面模板信息都在配置文件中

5.MYBeanWrapper

在IOC容器中,Spring 并不会把最原始的对象进去,而是会用一个BeanWrapper来进行一次包装

public class MYBeanWrapper {
	
	// 实例 Bean
    private Object wrappedInstance;
    // 还要保存 Class,目的为了多例模式服务
    private Class<?> wrappedClass;

    public MYBeanWrapper(Object wrappedInstance) {
        this.wrappedInstance = wrappedInstance;
    }

    public Object getWrappedInstance() {
        return this.wrappedInstance;
    }

    // 这里没有通过构造函数或者set方法进行注入,而是直接通过instance获取到Class
    public Class<?> getWrappedClass() {
        return this.wrappedInstance.getClass();
    }
}

6.MYBeanPostProcessor

事件处理器,它的作用主要是如果我们需要在Spring 容器完成 Bean 的实例化、配置和其他的初始化前后添加一些自己的逻辑处理。

注意,在Spring中BeanPostProcessor是一个接口,我们可以定义一个或者多个 BeanPostProcessor 接口的实现,然后注册到容器中。但这里我们只是为了模仿,并且出于在 ApplicationContext#getBean 中能够进行实例化的目的,此处就直接写成了 class。

public class MYBeanPostProcessor {
	// 为在Bean的初始化前提供回调入口
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return null;
    }
	// 为在Bean的初始化之后提供回调入口
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return  null;
    }
}

还少了一个 MYDefaultListableBeanFactory ,我们放在下篇 【Spring】重构–仿写Spring核心逻辑(三)实现IOC/DI(context包) 去说。

完整代码我放到 GitHub 上了,可以点击这里跳转…

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

A minor

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

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

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

打赏作者

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

抵扣说明:

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

余额充值