文章目录
- 一、目标:自动扫描Bean对象注册
- 二、设计:自动扫描Bean对象注册
- 三、实现:自动扫描Bean对象注册
- 3.0 引入依赖
- 3.1 工程结构
- 3.2 Bean生命周期中自动加载包扫描注册Bean对象和设置占位符属性类图
- 3.3 主力占位符配置
- 3.4 定义拦截注解
- 3.4.1 定义拦截注解
- 3.4.2 定义注册注解
- 3.5 处理对象扫描装配
- 3.5.1 类路径扫描装配提供者类
- 3.5.2 类路径扫描装配实现类
- 3.6 解析xml中调用扫描
- 四、测试:自动扫描Bean对象注册
- 4.1 添加测试配置
- 4.1.1 用户服务层实现类
- 4.1.2 属性配置文件
- 4.1.3 Spring属性配置文件
- 4.1.4 Spring扫描配置文件
- 4.2 单元测试
- 4.2.1 占位符测试
- 4.2.2 包扫描测试
- 五、总结:自动扫描Bean对象注册
一、目标:自动扫描Bean对象注册
💡 怎么完成自动化扫描Bean对象,自动注册填充?
- 早期的 Spring 版本,需要一个一个在 spring.xml 中进行配置。
- 现在的 Spring 版本,在核心功能逻辑上建设的更少的配置下,做到更简化的使用。包括:包的扫描注册、注解配置的使用、占位符属性的填充等。
- 目标:在目前的核心逻辑上填充一些自动化的功能。
二、设计:自动扫描Bean对象注册
💡 怎么简化 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
的属性信息。
三、实现:自动扫描Bean对象注册
3.0 引入依赖
pom.xml
<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
3.1 工程结构
spring-step-12
|-src
|-main
| |-java
| |-com.lino.springframework
| |-aop
| | |-aspectj
| | | |-AspectJExpressionPointcut.java
| | | |-AspectJExpressionPointcutAdvisor.java
| | |-framework
| | | |-adapter
| | | | |-MethodBeforeAdviceInterceptor.java
| | | |-autoproxy
| | | | |-DefaultAdvisorAutoProxyCreator.java
| | | |-AopProxy.java
| | | |-Cglib2AopProxy.java
| | | |-JdkDynamicAopProxy.java
| | | |-ProxyFactory.java
| | | |-ReflectiveMethodInvocation.java
| | |-AdvisedSupport.java
| | |-Advisor.java
| | |-BeforeAdvice.java
| | |-ClassFilter.java
| | |-MethodBeforeAdvice.java
| | |-MethodMatcher.java
| | |-Pointcut.java
| | |-PointcutAdvisor.java
| | |-TargetSource.java
| |-beans
| | |-factory
| | | |-config
| | | | |-AutowireCapableBeanFactory.java
| | | | |-BeanDefinition.java
| | | | |-BeanFactoryPostProcessor.java
| | | | |-BeanPostProcessor.java
| | | | |-BeanReference.java
| | | | |-ConfigurableBeanFactory.java
| | | | |-InstantiationAwareBeanPostProcessor.java
| | | | |-SingletonBeanRegistry.java
| | | |-support
| | | | |-AbstractAutowireCapableBeanFactory.java
| | | | |-AbstractBeabDefinitionReader.java
| | | | |-AbstractBeabFactory.java
| | | | |-BeabDefinitionReader.java
| | | | |-BeanDefinitionRegistry.java
| | | | |-CglibSubclassingInstantiationStrategy.java
| | | | |-DefaultListableBeanFactory.java
| | | | |-DefaultSingletonBeanRegistry.java
| | | | |-DisposableBeanAdapter.java
| | | | |-FactoryBeanRegistrySupport.java
| | | | |-InstantiationStrategy.java
| | | | |-SimpleInstantiationStrategy.java
| | | |-xml
| | | | |-XMLBeanDefinitionReader.java
| | | |-Aware.java
| | | |-BeanClassLoaderAware.java
| | | |-BeanFactory.java
| | | |-BeanFactoryAware.java
| | | |-BeanNameAware.java
| | | |-ConfigurableListableBeanFactory.java
| | | |-DisposableBean.java
| | | |-FactoryBean.java
| | | |-HierarcgicalBeanFactory.java
| | | |-InitializingBean.java
| | | |-ListableBeanFactory.java
| | | |-PropertyPlaceholderConfigurer.java
| | |-BeansException.java
| | |-PropertyValue.java
| | |-PropertyValues.java
| |-context
| | |-annotation
| | | |-ClassPathBeanDefinitionScanner.java
| | | |-ClassPathScanningCandidateComponentProvider.java
| | | |-Scope.java
| | |-event
| | | |-AbstractApplicationEventMulticaster.java
| | | |-ApplicationContextEvent.java
| | | |-ApplicationEventMulticaster.java
| | | |-ContextclosedEvent.java
| | | |-ContextRefreshedEvent.java
| | | |-SimpleApplicationEventMulticaster.java
| | |-support
| | | |-AbstractApplicationContext.java
| | | |-AbstractRefreshableApplicationContext.java
| | | |-AbstractXmlApplicationContext.java
| | | |-ApplicationContextAwareProcessor.java
| | | |-ClassPathXmlApplicationContext.java
| | |-ApplicationContext.java
| | |-ApplicationContextAware.java
| | |-ApplicationEvent.java
| | |-ApplicationEventPublisher.java
| | |-ApplicationListener.java
| | |-ConfigurableApplicationContext.java
| |-core.io
| | |-ClassPathResource.java
| | |-DefaultResourceLoader.java
| | |-FileSystemResource.java
| | |-Resource.java
| | |-ResourceLoader.java
| | |-UrlResource.java
| |-stereotype
| | |-Component.java
| |-util
| | |-ClassUtils.java
|-test
|-java
|-com.lino.springframework.test
|-bean
| |-IUserService.java
| |-UserService.java
|-ApiTest.java
|-resources
|-spring-property.xml
|-spring-scan.xml
|-token.properties
3.2 Bean生命周期中自动加载包扫描注册Bean对象和设置占位符属性类图
- 整个类图看,主要包括的就是 xml 解析类
XmlBeanDefinitionReader
对ClassPathBeanDefinitionScanner#doScan
的使用。 - 在
doScan
方法中处理所有指定路径下添加了注解的类,拆解出类的信息:名称、作用范围等,进行创建BeanDefinition
好用于 Bean 对象的注册操作。 PropertyPlaceholderConfigurer
后续会与自动加载 Bean 对象进行整合,也就是可以在注解上使用占位符配置一些在配置文件里的属性信息。
3.3 主力占位符配置
PropertyPlaceholderConfigurer.java
package com.lino.springframework.beans.factory;
import com.lino.springframework.beans.BeansException;
import com.lino.springframework.beans.PropertyValue;
import com.lino.springframework.beans.PropertyValues;
import com.lino.springframework.beans.factory.config.BeanDefinition;
import com.lino.springframework.beans.factory.config.BeanFactoryPostProcessor;
import com.lino.springframework.core.io.DefaultResourceLoader;
import com.lino.springframework.core.io.Resource;
import java.io.IOException;
import java.util.Properties;
/**
* @description: 处理占位符配置类
*/
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
3.4 定义拦截注解
3.4.1 定义拦截注解
Scope.java
package com.lino.springframework.context.annotation;
import java.lang.annotation.*;
/**
* @description: 拦截注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
String value() default "singleton";
}
- 用于配置作用域的自定义注解,方便通过配置 Bean 对象注解的时候,拿到 Bean 对象的作用域。
3.4.2 定义注册注解
Component.java
package com.lino.springframework.stereotype;
import java.lang.annotation.*;
/**
* @description: 注册注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String value() default "";
}
- Component 自定义注解,用于配置到 Class 类上。
3.5 处理对象扫描装配
3.5.1 类路径扫描装配提供者类
ClassPathScanningCandidateComponentProvider.java
package com.lino.springframework.context.annotation;
import cn.hutool.core.util.ClassUtil;
import com.lino.springframework.beans.factory.config.BeanDefinition;
import com.lino.springframework.stereotype.Component;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @description: 类路径扫描装配提供者
*/
public class ClassPathScanningCandidateComponentProvider {
public Set<BeanDefinition> findCandidateComponents(String basePackages) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackages, Component.class);
for (Class<?> clazz : classes) {
candidates.add(new BeanDefinition(clazz));
}
return candidates;
}
}
- 这里先要提供一个可以通过配置路径
beanPackage=com.lino.springframework.test.bean
,解析出classes
信息的工具方法findCandidateComponents
,通过这个方法就可以扫描到所有@Component
注解的 Bean 对象。
3.5.2 类路径扫描装配实现类
ClassPathBeanDefinitionScanner.java
package com.lino.springframework.context.annotation;
import cn.hutool.core.util.StrUtil;
import com.lino.springframework.beans.factory.config.BeanDefinition;
import com.lino.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.lino.springframework.stereotype.Component;
import java.util.Set;
/**
* @description: 类路径扫描装配实现类
*/
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);
}
}
}
private String resolveBeanScope(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Scope scope = beanClass.getAnnotation(Scope.class);
if (null != scope) {
return scope.value();
}
return StrUtil.EMPTY;
}
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 的作用域和类名,如果不配置类名基本都是把首字母缩写。
3.6 解析xml中调用扫描
XmlBeanDefinitionReader.java
package com.lino.springframework.beans.factory.xml;
import cn.hutool.core.util.StrUtil;
import com.lino.springframework.beans.BeansException;
import com.lino.springframework.beans.PropertyValue;
import com.lino.springframework.beans.factory.config.BeanDefinition;
import com.lino.springframework.beans.factory.config.BeanReference;
import com.lino.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import com.lino.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.lino.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import com.lino.springframework.core.io.Resource;
import com.lino.springframework.core.io.ResourceLoader;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @description: XML处理Bean注册
*/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
...
@Override
public void loadBeanDefinitions(Resource resource) throws BeansException {
try {
try (InputStream inputStream = resource.getInputStream()) {
doLoadBeanDefinitions(inputStream);
}
} catch (IOException | ClassNotFoundException | DocumentException e) {
throw new BeansException("IOException parsing XML document from " + resource, e);
}
}
...
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);
}
List<Element> beanList = root.elements("bean");
for (Element bean : beanList) {
String id = bean.attributeValue("id");
String name = bean.attributeValue("name");
String className = bean.attributeValue("class");
String initMethod = bean.attributeValue("init-method");
String destroyMethodName = bean.attributeValue("destroy-method");
String beanScope = bean.attributeValue("scope");
// 获取 Class, 方便获取类中的名称
Class<?> clazz = Class.forName(className);
// 优先级 id > name
String beanName = StrUtil.isNotEmpty(id) ? id : name;
if (StrUtil.isEmpty(beanName)) {
beanName = StrUtil.lowerFirst(clazz.getSimpleName());
}
// 定义bean
BeanDefinition beanDefinition = new BeanDefinition(clazz);
beanDefinition.setInitMethodName(initMethod);
beanDefinition.setDestroyMethodName(destroyMethodName);
if (StrUtil.isNotEmpty(beanScope)) {
beanDefinition.setScope(beanScope);
}
List<Element> propertyList = bean.elements("property");
// 读取属性并填充
for (Element property : propertyList) {
// 解析标签:property
String attrName = property.attributeValue("name");
String attrValue = property.attributeValue("value");
String attrRef = property.attributeValue("ref");
// 获取属性值:引入对象、值对象
Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
// 创建属性信息
PropertyValue propertyValue = new PropertyValue(attrName, value);
beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
}
if (getRegistry().containsBeanDefinition(beanName)) {
throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
}
// 注册 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
进行解析处理。
四、测试:自动扫描Bean对象注册
4.1 添加测试配置
4.1.1 用户服务层实现类
UserService.java
package com.lino.springframework.test.bean;
import com.lino.springframework.stereotype.Component;
import java.util.Random;
/**
* @description: 用户接口实现类
*/
@Component("userService")
public class UserService implements IUserService {
private String token;
@Override
public String queryUserInfo() {
try {
Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "张三,100001,杭州";
}
@Override
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
。
4.1.2 属性配置文件
token.properties
token=RejDlI78hu223Opo983Ds
- 这里配置一个
token
属性信息,通过占位符的方式进行获取。
4.1.3 Spring属性配置文件
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.lino.springframework.beans.factory.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:token.properties"/>
</bean>
<bean id="userService" class="com.lino.springframework.test.bean.UserService">
<property name="token" value="${token}"/>
</bean>
</beans>
- 加载
classpath:token.properties
设置占位符属性值${token}
4.1.4 Spring扫描配置文件
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="com.lino.springframework.test.bean"/>
</beans>
- 添加
component-scan
属性,设置包扫描根路径。
4.2 单元测试
4.2.1 占位符测试
ApiTest.java
@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
的属性值了。
4.2.2 包扫描测试
ApiTest.java
@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());
}
测试结果
测试结果:张三,100001,杭州
- 测试结果看,现在使用注解的方式就可以让 Class 注册完成 Bean 对象了。
五、总结:自动扫描Bean对象注册
- 通过整篇的内容实现来看,目前的功能添加其实不是很复杂,都是在
IOC
、AOP
核心的基础上来补全功能。这些补全的功能也是在完善 Bean 的生命周期,让整个功能使用越来越容易。