目前的手写源码是通过xml的方法,扫描、注册;
本章目标是 包的扫描注册、注解配置的使用、占位符属性的填充等,更加自动化
本章要实现的效果
通过xml文件里,添加context:component-scan这个标签,来指定扫描包路径
<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 属性的机制。
整体设计结构如下图:
结合bean的生命周期,包扫描只不过是扫描特定注解的类,提取类的相关信息组装成BeanDefinition注册到容器中。
1、在XmlBeanDefinitionReader中解析
自动扫描注册主要是扫描添加了自定义注解的类,在xml加载过程中提取类的信息,组装 BeanDefinition 注册到 Spring 容器中。
所以我们会用到
2、因为我们需要完成对占位符配置信息的加载,所以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,修改 BeanDefinition 的属性信息
1、整个类的关系结构来看,其实涉及的内容并不多,主要包括的就是 xml 解析类 XmlBeanDefinitionReader 对 ClassPathBeanDefinitionScanner#doScan 的使用。
2、在 doScan 方法中处理所有指定路径下添加了注解的类,拆解出类的信息:名称、作用范围等,进行创建 BeanDefinition 好用于 Bean 对象的注册操作。
3、PropertyPlaceholderConfigurer 目前看上去像一块单独的内容,后续会把这块的内容与自动加载 Bean 对象进行整合,也就是可以在注解上使用占位符配置一些在配置文件里的属性信息
/**
* @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()));
/**
* @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 即可。
/**
* @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 的作用域和类名,如果不配置类名基本都是把首字母缩写。
/**
* 解析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 的方式进行解析处理。
@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。这是为了分别测试包扫描和占位符属性。
token.properties
token=RejDlI78hu223Opo983Ds
这里配置一个 token 的属性信息,用于通过占位符的方式进行获取
spring-property.xml
<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
<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
@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 的属性值了。
@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