文章目录

  • 目标
  • 包扫描
  • 注解配置的使用
  • 占位符属性的填充
  • 设计
  • 类结构
  • 一、实现
  • 1、处理占位符配置——PropertyPlaceholderConfigurer
  • 2、定义@Scope、@Component拦截注解
  • 3、处理对象扫描装配——ClassPathBeanDefinitionScanner
  • 4、解析xml中调用扫描
  • 二、测试
  • 1、准备
  • 2、属性配置文件
  • 3、pring.xml 配置对象
  • 4、单元测试(占位符)
  • 5、单元测试(包扫描)
  • 总结



目标

目前的手写源码是通过xml的方法,扫描、注册;

本章目标是 包的扫描注册、注解配置的使用、占位符属性的填充等,更加自动化

本章要实现的效果

包扫描

通过xml文件里,添加context:component-scan这个标签,来指定扫描包路径

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <context:component-scan base-package="springframework.test.bean" />
</beans>

注解配置的使用

添加@Component注解,如果在指定包下,则会被扫描到并且注册到BeanDefinition里,最终getBean

@Component("userService")
public class UserService implements IUserService {
	private String token;
}

占位符属性的填充

${token}解析配置文件的数据,并且进行属性填充

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

设计

首先我们要考虑,为了可以简化 Bean 对象的配置,让整个 Bean 对象的注册都是自动扫描的,那么需要的元素包括:扫描路径入口、XML解析扫描信息、给需要扫描的Bean对象做注解标记、扫描Class对象摘取Bean注册的基本信息,组装注册信息、注册成Bean对象
那么在这些条件元素的支撑下,就可以实现出通过自定义注解和配置扫描路径的情况下,完成 Bean 对象的注册。

解决一个配置中占位符属性的知识点,比如可以通过 ${token} 给 Bean 对象注入进去属性信息,那么这个操作需要用到 BeanFactoryPostProcessor,因为它可以处理 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,提供修改 BeanDefinition 属性的机制。

整体设计结构如下图:

Spring项目中那些xml文件被扫面 spring扫描xml_java


结合bean的生命周期,包扫描只不过是扫描特定注解的类,提取类的相关信息组装成BeanDefinition注册到容器中

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

自动扫描注册主要是扫描添加了自定义注解的类,在xml加载过程中提取类的信息,组装 BeanDefinition 注册到 Spring 容器中。
所以我们会用到 <context:component-scan /> 配置包路径并在 XmlBeanDefinitionReader 解析并做相应的处理。这里的处理会包括对类的扫描、获取注解信息等

2、因为我们需要完成对占位符配置信息的加载,所以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,修改 BeanDefinition 的属性信息


类结构

Spring项目中那些xml文件被扫面 spring扫描xml_xml_02


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

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

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


一、实现

1、处理占位符配置——PropertyPlaceholderConfigurer

/**
 * @desc 处理占位符配置
 */
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {

    /**
     * Default placeholder prefix: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

    /**
     * Default placeholder suffix: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";


    // 资源文件位置
    private String location;


    /**
     * @desc: 占位符属性配置解析,通过BeanFactoryPostProcessor修改beanDefintion属性
     **/
    @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;
    }
}

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

2、通过截取${}里面的内容,然后通过properties.getProperty,获取到配置文件里面的值

3、这样就可以把提取到的配置信息放置到属性配置中了,

buffer.replace(startIdx,stopIdx+1,propVal);
propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(),buffer.toString()));

2、定义@Scope、@Component拦截注解

/**
 * @desc: 作用域注解
 **/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

    String value() default "singleton";

}

用于配置作用域的自定义注解,方便通过配置Bean对象注解的时候,拿到Bean对象的作用域。不过一般都使用默认的 singleton

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {

    String value() default "";

}

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


3、处理对象扫描装配——ClassPathBeanDefinitionScanner

/**
 * @desc 处理对象扫描装配
 */
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.ljc.springframework.test.bean,解析出 classes 信息的工具方法 findCandidateComponents,通过这个方法就可以扫描到所有 @Component 注解的 Bean 对象了。

/**
 * @desc bean定义扫描器
 */
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {


    // 注册Bean定义
    private BeanDefinitionRegistry registry;

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

    /**
     * @desc: 扫描包
     **/
    public void doScan(String... basePackages) {
        for (String basePackage : basePackages) {
        	// 获取所有的@component注解的bean定义
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition beanDefinition : candidates) {
                // 解析Bean的作用域
                String beanScope = resolveBeanScope(beanDefinition);
                if (StrUtil.isNotEmpty(beanScope)) {
                    beanDefinition.setScope(beanScope);
                }
                // 注册bean定义
                registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
            }
        }

    }


    /**
     * @desc: 获取bean的作用域
     **/
    private String resolveBeanScope(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Scope scope = beanClass.getAnnotation(Scope.class);
        if (scope != null) {
            return scope.value();
        }
        return StrUtil.EMPTY;
    }


    /**
     * @desc: 确定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 的作用域和类名,如果不配置类名基本都是把首字母缩写。


4、解析xml中调用扫描

/**
 * 解析XML处理Bean注册
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        super(registry, resourceLoader);
    }


    @Override
    public void loadBeanDefinitions(Resource resource) throws BeansException {
        try {
            InputStream inputStream = resource.getInputStream();
            doLoadBeanDefinitions(inputStream);
        } catch (Exception e) {
            throw new BeansException("IOException parsing XML document from " + resource, e);
        }
    }

    @Override
    public void loadBeanDefinitions(Resource... resources) throws BeansException {
        for (Resource resource : resources) {
            loadBeanDefinitions(resource);
        }
    }

    @Override
    public void loadBeanDefinitions(String location) throws BeansException {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(location);
        loadBeanDefinitions(resource);
    }

    @Override
    public void loadBeanDefinitions(String... locations) throws BeansException {
        for (String location : locations) {
            loadBeanDefinitions(location);
        }
    }


    protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {

        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();

        // 解析xml文件中的context:component-scan 标签,扫描包中的类并提取相关信息,用于组装 BeanDefinition
        Element componentScan = root.element("component-scan");
        if (componentScan != null) {
        	// 获取指定的包路径
            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");
            }
            // 扫描包,把component注册
            scanPackage(scanPath);
        }

		// ....解析xml每个标签的数据,并填充到beanDefintion(之前的代码这里就不显示了,这里只展示本章新增内容)
		
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }



    /**
     * @desc: 扫描包
     **/
    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 的方式进行解析处理。


二、测试

1、准备

@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 "ljc,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;
    }
}

给 UserService 类添加一个自定义注解 @Component(“userService”)一个属性信息 String token。这是为了分别测试包扫描和占位符属性。


2、属性配置文件

token.properties

token=RejDlI78hu223Opo983Ds

这里配置一个 token 的属性信息,用于通过占位符的方式进行获取


3、pring.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"
       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">

    <bean class="cn.ljc.springframework.beans.factory.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:token.properties"/>
    </bean>

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


</beans>

加载 classpath:token.properties 设置占位符属性值 ${token}

spring-scan.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="springframework.test.bean" />

</beans>

添加 component-scan 属性,设置包扫描根路径,用于获取指定包路径下的@component注解的bean


4、单元测试(占位符)

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

测试结果

测试结果:UserService#token = { RejDlI78hu223Opo983Ds }

通过测试结果可以看到 UserService 中的 token 属性,已经通过占位符的方式,设置进去配置文件里的 token.properties 的属性值了。


5、单元测试(包扫描)

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

测试结果

测试结果:ljc,100001,上海

通过这个测试结果可以看出来,现在使用注解的方式就可以让 Class 注册完成 Bean 对象了。


总结

占位符的处理,通过上面代码可以知道,BeanFactoryPostProcessor的扩展,spring内部就是通过这个BeanFactoryPostProcessor来实现的。

包扫描则是则是获取到指定的xml标签,获取包路径,然后通过doScan扫描到包下指定的**@component@scope**,然后注册到BeanDefintion