手写简易版Spring框架(十二):通过注解配置和包自动扫描的方式完成Bean对象的注册

目标

其实到本章节我们已经把关于 IOC 和 AOP 全部核心内容都已经实现完成了,只不过在使用上还有点像早期的 Spring 版本,需要一个一个在 spring.xml 中进行配置。这与实际的目前使用的 Spring 框架还是有蛮大的差别,而这种差别其实都是在核心功能逻辑之上建设的在更少的配置下,做到更简化的使用。

这其中就包括:包的扫描注册、注解配置的使用、占位符属性的填充等等,而我们的目标就是在目前的核心逻辑上填充一些自动化的功能,让大家可以学习到这部分的设计和实现,从中体会到一些关于代码逻辑的实现过程,总结一些编码经验。

设计

首先我们要考虑🤔,为了可以简化 Bean 对象的配置,让整个 Bean 对象的注册都是自动扫描的,那么基本需要的元素包括:扫描路径入口、XML解析扫描信息、给需要扫描的Bean对象做注解标记、扫描Class对象摘取Bean注册的基本信息,组装注册信息、注册成Bean对象。那么在这些条件元素的支撑下,就可以实现出通过自定义注解和配置扫描路径的情况下,完成 Bean 对象的注册。除此之外再顺带解决一个配置中占位符属性的知识点,比如可以通过 ${token} 给 Bean 对象注入进去属性信息,那么这个操作需要用到 BeanFactoryPostProcessor,因为它可以处理 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,提供修改 BeanDefinition 属性的机制 而实现这部分内容是为了后续把此类内容结合到自动化配置处理中。

类图:
在这里插入图片描述
结合bean的生命周期,包扫描只不过是扫描特定注解的类,提取类的相关信息组装成BeanDefinition注册到容器中。

在XmlBeanDefinitionReader中解析<context:component-scan />标签,扫描类组装BeanDefinition然后注册到容器中的操作在ClassPathBeanDefinitionScanner#doScan中实现。

自动扫描注册主要是扫描添加了自定义注解的类,在xml加载过程中提取类的信息,组装 BeanDefinition 注册到 Spring 容器中。所以我们会用到 <context:component-scan /> 配置包路径并在 XmlBeanDefinitionReader 解析并做相应的处理。这里的处理会包括对类的扫描、获取注解信息等。最后还包括了一部分关于 BeanFactoryPostProcessor 的使用,因为我们需要完成对占位符配置信息的加载,所以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,修改 BeanDefinition 的属性信息。

整个类的关系结构来看,其实涉及的内容并不多,主要包括的就是 xml 解析类 XmlBeanDefinitionReader 对 ClassPathBeanDefinitionScanner#doScan 的使用。

在 doScan 方法中处理所有指定路径下添加了注解的类,拆解出类的信息:名称、作用范围等,进行创建 BeanDefinition 好用于 Bean 对象的注册操作。

PropertyPlaceholderConfigurer 目前看上去像一块单独的内容,后续会把这块的内容与自动加载 Bean 对象进行整合,也就是可以在注解上使用占位符配置一些在配置文件里的属性信息。

实现

处理占位符配置

package com.qingyun.springframework.beans.factory;

import com.qingyun.springframework.beans.BeansException;
import com.qingyun.springframework.beans.factory.config.BeanDefinition;
import com.qingyun.springframework.beans.factory.config.BeanFactoryPostProcessor;
import com.qingyun.springframework.core.io.DefaultResourceLoader;
import com.qingyun.springframework.core.io.Resource;

import java.io.IOException;
import java.util.Properties;

/**
 * @description: 处理Bean定义信息中的占位符信息
 * @author: 張青云
 * @create: 2021-08-24 18:02
 **/
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {

    //  默认占位符的前缀
    public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

    //  默认占位符的后缀
    public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";

    private String location;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 加载属性文件
        try {
            DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = resourceLoader.getResource(location);
            Properties properties = new Properties();
            properties.load(resource.getInputStream());

            String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
            for (String beanName : beanDefinitionNames) {
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

                PropertyValues propertyValues = beanDefinition.getPropertyValues();
                for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
                    Object value = propertyValue.getValue();
                    if (!(value instanceof String)) continue;
                    String strVal = (String) value;
                    StringBuilder buffer = new StringBuilder(strVal);
                    int startIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
                    int stopIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
                    if (startIdx != -1 && stopIdx != -1 && startIdx < stopIdx) {
                        String propKey = strVal.substring(startIdx + 2, stopIdx);
                        String propVal = properties.getProperty(propKey);
                        buffer.replace(startIdx, stopIdx + 1, propVal);
                        //  这里并没有删除原属性,但是在进行属性填充的时候会使用修改后的属性值覆盖原来的属性值
                        propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(), buffer.toString()));
                    }
                }
            }
        } catch (IOException e) {
            throw new BeansException("Could not load properties", e);
        }
    }

    public void setLocation(String location) {
        this.location = location;
    }

}

依赖于 BeanFactoryPostProcessor 在 Bean 生命周期的属性,可以在 Bean 对象实例化之前,改变属性信息。所以这里通过实现 BeanFactoryPostProcessor 接口,完成对配置文件的加载以及摘取占位符中的在属性文件里的配置。

这样就可以把提取到的配置信息放置到属性配置中了,buffer.replace(startIdx, stopIdx + 1, propVal); propertyValues.addPropertyValue

定义要扫描的注解

package com.qingyun.springframework.beans.stereotype;

import java.lang.annotation.*;

/**
 * @description: 标注该注解的类会被当作Bean来加载
 * @author: 張青云
 * @create: 2021-08-24 18:13
 **/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {

    String value() default "";

}

Component 自定义注解大家都非常熟悉了,用于配置到 Class 类上的。除此之外还有 Service、Controller,不过所有的处理方式基本一致,这里就只展示一个 Component 即可。

package com.qingyun.springframework.beans.annotation;

import java.lang.annotation.*;

/**
 * @description: Bean的类型:单例还是原型(多例)
 * @author: 張青云
 * @create: 2021-08-24 18:04
 **/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

    String value() default "singleton";

}

用于配置作用域的自定义注解,方便通过配置Bean对象注解的时候,拿到Bean对象的作用域。

处理对象扫描装配

public class ClassPathScanningCandidateComponentProvider {

    public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);
        for (Class<?> clazz : classes) {
            candidates.add(new BeanDefinition(clazz));
        }
        return candidates;
    }

}

这里先要提供一个可以通过配置路径 basePackage=cn.bugstack.springframework.test.bean,解析出 classes 信息的工具方法 findCandidateComponents,通过这个方法就可以扫描到所有 @Component 注解的 Bean 对象了。

package com.qingyun.springframework.beans.annotation;

import cn.hutool.core.util.StrUtil;
import com.qingyun.springframework.beans.factory.config.BeanDefinition;
import com.qingyun.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.qingyun.springframework.beans.stereotype.Component;

import java.util.Set;

/**
 * @description:
 * @author: 張青云
 * @create: 2021-08-24 18:16
 **/
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

    private BeanDefinitionRegistry registry;

    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    public void doScan(String... basePackages) {
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition beanDefinition : candidates) {
                // 解析 Bean 的作用域 singleton、prototype
                String beanScope = resolveBeanScope(beanDefinition);
                if (StrUtil.isNotEmpty(beanScope)) {
                    beanDefinition.setScope(beanScope);
                }
                registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
            }
        }
    }

    /**
     * 解析Bean的作用域singleton、prototype
     */
    private String resolveBeanScope(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Scope scope = beanClass.getAnnotation(Scope.class);
        if (null != scope) {
            return scope.value();
        }
        return StrUtil.EMPTY;
    }

    /**
     * 获取Bean注册到容器中的名字
     */
    private String determineBeanName(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Component component = beanClass.getAnnotation(Component.class);
        String value = component.value();
        if (StrUtil.isEmpty(value)) {
            value = StrUtil.lowerFirst(beanClass.getSimpleName());
        }
        return value;
    }

}

ClassPathBeanDefinitionScanner 是继承自 ClassPathScanningCandidateComponentProvider 的具体扫描包处理的类,在 doScan 中除了获取到扫描的类信息以后,还需要获取 Bean 的作用域和类名,如果不配置类名基本都是把首字母缩写。

解析xml时调用扫描

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();

        // 解析 context:component-scan 标签,扫描包中的类并提取相关信息,用于组装 BeanDefinition
        Element componentScan = root.element("component-scan");
        if (null != componentScan) {
            String scanPath = componentScan.attributeValue("base-package");
            if (StrUtil.isEmpty(scanPath)) {
                throw new BeansException("The value of base-package attribute can not be empty or null");
            }
            scanPackage(scanPath);
        }
       
        // ... 省略其他
            
        // 注册 BeanDefinition
        getRegistry().registerBeanDefinition(beanName, beanDefinition);
    }

    private void scanPackage(String scanPath) {
        String[] basePackages = StrUtil.splitToArray(scanPath, ',');
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());
        scanner.doScan(basePackages);
    }

}

关于 XmlBeanDefinitionReader 中主要是在加载配置文件后,处理新增的自定义配置属性 component-scan,解析后调用 scanPackage 方法,其实也就是我们在 ClassPathBeanDefinitionScanner#doScan 功能。

另外这里需要注意,为了可以方便的加载和解析xml,XmlBeanDefinitionReader 已经全部替换为 dom4j 的方式进行解析处理。

测试

准备

package com.qingyun.springframework.context.test.annotationTest;

import com.qingyun.springframework.beans.stereotype.Component;

import java.util.Random;

/**
 * @description:
 * @author: 張青云
 * @create: 2021-08-24 18:27
 **/
@Component("userService")
public class UserService implements IUserService {

    private String token;

    public String queryUserInfo() {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "青云先生,100001,深圳";
    }

    public String register(String userName) {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "注册用户:" + userName + " success!";
    }

    @Override
    public String toString() {
        return "UserService#token = { " + token + " }";
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

属性值配置文件spring.properties

token=zqy

XML配置文件

spring-property.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	         http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--替换占位符-->
    <bean class="com.qingyun.springframework.beans.factory.PropertyPlaceholderConfigurer">
        <!--配置文件的路径-->
        <property name="location" value="classpath:spring.properties"/>
    </bean>

    <bean id="userService" class="com.qingyun.springframework.context.test.annotationTest.UserService">
        <property name="token" value="${token}"/>
    </bean>

</beans>

ScanTest.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context">

    <context:component-scan base-package="com.qingyun.springframework.context.test.annotationTest"/>

</beans>

占位符单元测试

    @Test
    public void test_property() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-property.xml");
        IUserService userService = applicationContext.getBean("userService", IUserService.class);
        System.out.println("测试结果:" + userService);
    }

结果:
在这里插入图片描述
可以发现占位符被正确替换了!

包扫描单元测试

    @Test
    public void test_scan() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:ScanTest.xml");
        IUserService userService = applicationContext.getBean("userService", IUserService.class);
        System.out.println("测试结果:" + userService.queryUserInfo());
    }

结果:
在这里插入图片描述
可以发现通过注解和包扫描的方式,我们也完成了Bean对象的创建。

项目代码Github地址:https://github.com/Zhang-Qing-Yun/mini-spring,本节代码对应的commit标识为25efe02

欢迎标星

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值