天天看點

手寫spring13(xml自動掃描bean注冊)目标設計類結構一、實作二、測試總結

文章目錄

  • 目标
    • 包掃描
    • 注解配置的使用
    • 占位符屬性的填充
  • 設計
  • 類結構
  • 一、實作
    • 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 屬性的機制。

整體設計結構如下圖:

手寫spring13(xml自動掃描bean注冊)目标設計類結構一、實作二、測試總結

結合bean的生命周期,包掃描隻不過是掃描特定注解的類,提取類的相關資訊組裝成BeanDefinition注冊到容器中。

1、在XmlBeanDefinitionReader中解析<context:component-scan />标簽,掃描類組裝BeanDefinition然後注冊到容器中的操作在ClassPathBeanDefinitionScanner#doScan中實作。

自動掃描注冊主要是掃描添加了自定義注解的類,在xml加載過程中提取類的資訊,組裝 BeanDefinition 注冊到 Spring 容器中。

是以我們會用到 <context:component-scan /> 配置包路徑并在 XmlBeanDefinitionReader 解析并做相應的處理。這裡的處理會包括對類的掃描、擷取注解資訊等

2、因為我們需要完成對占位符配置資訊的加載,是以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加載完成後,執行個體化 Bean 對象之前,修改 BeanDefinition 的屬性資訊

類結構

手寫spring13(xml自動掃描bean注冊)目标設計類結構一、實作二、測試總結

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 的屬性資訊,用于通過占位符的方式進行擷取

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 屬性,已經通過占位符的方式,設定進去配置檔案裡的 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