前言:为何引入注解 + 扫描机制
在传统的 Spring 应用中,开发者需要在 XML 中显式配置每个 Bean,这种方式一方面繁琐、难以维护;另一方面如果 Bean 数量多、依赖复杂,还容易出错。现代应用更倾向于“约定优于配置”,实现自动化注册:
- 开发者只需在类上增加注解;
- 框架通过扫描指定包路径,自动识别并注册所有相关组件;
- 配置化属性(如
${...})在 BeanDefinition 加载完成后注入,支持属性占位符灵活管理。
本章手撸 Spring,将采用自定义注解与包扫描方式替代 XML 的显式注册,并结合 BeanFactoryPostProcessor 实现占位符配置注入。
一、功能目标
- 注解标记 Bean:通过自定义注解(如
@Component、@Scope)标识候选 Bean; - 包路径扫描:解析 XML 中
<context:component-scan>指定扫描路径; - 类加载与注解识别:扫描类文件,识别注解,读取 bean 名称和作用域;
- BeanDefinition 注册:根据扫描结果组装
BeanDefinition并注册到容器; - 属性占位符注入:实现
${…}占位符解析,通过读取配置文件填充属性。
通过上述功能,将自动完成 Bean 的定义、注册与属性注入,减少配置冗余,提升开发效率和系统灵活性。
二、设计方案概览
核心组件如下:
- 工程结构:增加
context.annotation包用于注解与扫描机制; - 注解定义:
@Component标识 Bean,@Scope定义作用域; - 扫描工具:
ClassPathScanningCandidateComponentProvider扫描包中的注解类;ClassPathBeanDefinitionScanner解析注解、作用域和 Bean 名称;
- XML 解析增强:在
XmlBeanDefinitionReader#doLoadBeanDefinitions中加入扫描解析<context:component-scan>; - 占位符注入:通过实现
BeanFactoryPostProcessor,在 BeanDefinition 加载后、实例化前处理占位符。
整体流程如下:
- 读取 XML 配置;
- 在XmlBeanDefinitionReader中解析,遇到
<context:component-scan>,触发扫描; - 扫描注解类并生成 BeanDefinition;
- 将 BeanDefinition 注入 BeanFactory;
- 应用
PropertyPlaceholderConfigurer解析${…}并替换为配置值; - 按常规流程实例化 Bean。

三、架构:
1. 工程结构
small-spring-step-13
└── src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── aop
│ │ ├── aspectj
│ │ │ └── AspectJExpressionPointcut.java
│ │ │ └── AspectJExpressionPointcutAdvisor.java
│ │ ├── framework
│ │ │ ├── adapter
│ │ │ │ └── MethodBeforeAdviceInterceptor.java
│ │ │ ├── autoproxy
│ │ │ │ └── MethodBeforeAdviceInterceptor.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
│ │ │ │ ├── AbstractBeanDefinitionReader.java
│ │ │ │ ├── AbstractBeanFactory.java
│ │ │ │ ├── BeanDefinitionReader.java
│ │ │ │ ├── BeanDefinitionRegistry.java
│ │ │ │ ├── CglibSubclassingInstantiationStrategy.java
│ │ │ │ ├── DefaultListableBeanFactory.java
│ │ │ │ ├── DefaultSingletonBeanRegistry.java
│ │ │ │ ├── DisposableBeanAdapter.java
│ │ │ │ ├── FactoryBeanRegistrySupport.java
│ │ │ │ ├── InstantiationStrategy.java
│ │ │ │ └── SimpleInstantiationStrategy.java
│ │ │ ├── support
│ │ │ │ └── XmlBeanDefinitionReader.java
│ │ │ ├── Aware.java
│ │ │ ├── BeanClassLoaderAware.java
│ │ │ ├── BeanFactory.java
│ │ │ ├── BeanFactoryAware.java
│ │ │ ├── BeanNameAware.java
│ │ │ ├── ConfigurableListableBeanFactory.java
│ │ │ ├── DisposableBean.java
│ │ │ ├── FactoryBean.java
│ │ │ ├── HierarchicalBeanFactory.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
│ └── utils
│ └── ClassUtils.java
└── test
└── java
└── cn.bugstack.springframework.test
├── bean
│ ├── IUserService.java
│ └── UserService.java
└── ApiTest.java
2.在Bean的生命周期中自动加载包扫描注册Bean对象和设置占位符属性的类关系

四、实现详解:
1.注解定义
前置知识:JAVA-注解 Annotation – CMD137’s Blog
/**
* 用于配置作用域的自定义注解,方便通过配置Bean对象注解的时候,拿到Bean对象的作用域。不过一般都使用默认的 singleton
*/
@Target({ElementType.TYPE, ElementType.METHOD}) // 可作用于类和方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取
@Documented // 生成 Javadoc 时包含该注解信息
public @interface Scope {
/**
* 作用域值,默认为 singleton。
* @return 作用域名称
*/
String value() default "singleton";
}
/**
* 组件注解,用于标识一个类为Spring容器管理的Bean
* 被此注解标记的类会被自动扫描并注册到Spring容器中
*/
@Target(ElementType.TYPE) // 仅可作用于类、接口(包括注解类型)、枚举
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取
@Documented // 生成 Javadoc 时包含该注解信息
public @interface Component {
/**
* Bean 名称,可选。
* @return Bean 名称
*/
String value() default "";
}
2.处理对象扫描装配:
该类的核心作用是在指定的包路径下扫描带有特定注解的类,并将这些类转换为组件定义(BeanDefinition),为后续的对象创建和管理提供元数据。
/**
* 在基于注解的 Bean 注册机制中,需要有一个步骤能从指定包路径中发现符合条件的类,
* 并将它们作为候选组件(Candidate Component)交给 BeanDefinition 注册流程处理。
* 该类就是完成“扫描 + 转换”这一功能的核心工具类。
*/
public class ClassPathScanningCandidateComponentProvider {
/**
* 扫描指定基础包路径,查找被 {@link Component} 注解标记的类,
* 并将其封装为 {@link BeanDefinition} 对象集合返回。
*
* @param basePackage 基础包路径(如 "com.example.service")
* @return 候选 BeanDefinition 集合
*/
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
// 用 LinkedHashSet 保证扫描结果有序且不重复
Set<BeanDefinition> candidates = new LinkedHashSet<>();
// 调用工具方法扫描出被 @Component 标记的类
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);
// 将扫描到的类转换为 BeanDefinition 并加入集合
for (Class<?> clazz : classes) {
candidates.add(new BeanDefinition(clazz));
}
return candidates;
}
}
ClassPathBeanDefinitionScanner 是继承自 ClassPathScanningCandidateComponentProvider 的具体扫描包处理的类,在 doScan 中除了获取到扫描的类信息以后,还需要获取 Bean 的作用域和类名,
/**
* 该类的核心作用是在指定包路径下扫描符合条件的组件(继承父类的扫描能力),
* 并将这些组件的定义(BeanDefinition)注册到 Bean 定义注册表(BeanDefinitionRegistry)中,
* 是连接 “组件扫描” 与 “容器注册” 的关键桥梁。
*/
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
// BeanDefinition 注册器,用于将扫描到的 BeanDefinition 注册到容器
private BeanDefinitionRegistry registry;
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
this.registry = registry;
}
/**
* 扫描指定的基础包路径,并注册符合条件的 BeanDefinition
*/
public void doScan(String... basePackages) {
for (String basePackage : basePackages) {
// 扫描该包下所有被 @Component 标注的类,转换成 BeanDefinition 集合
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition beanDefinition : candidates) {
// 解析 Bean 的作用域(singleton、prototype 等)
String beanScope = resolveBeanScope(beanDefinition);
if (StrUtil.isNotEmpty(beanScope)) {
beanDefinition.setScope(beanScope);
}
// 注册 BeanDefinition 到容器
// determineBeanName() 用于解析 Bean 名称(@Component value 或类名首字母小写)
registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
}
}
}
/**
* 根据 @Scope 注解解析 Bean 的作用域
*/
private String resolveBeanScope(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Scope scope = beanClass.getAnnotation(Scope.class);
if (null != scope) return scope.value(); // 如果有 @Scope 注解,返回其值
return StrUtil.EMPTY; // 默认返回空字符串,表示使用默认作用域(singleton)
}
/**
* 根据 @Component 注解解析 Bean 的名称
* 如果 @Component value 为空,则使用类名首字母小写
*/
private String determineBeanName(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Component component = beanClass.getAnnotation(Component.class);
String value = component.value();
if (StrUtil.isEmpty(value)) {
// 例如 UserService -> userService
value = StrUtil.lowerFirst(beanClass.getSimpleName());
}
return value;
}
}
此处简单提一嘴同名Bean的问题:
真实 Spring 的处理逻辑:
在 Spring 框架中,DefaultListableBeanFactory.registerBeanDefinition 会先检查容器中是否已存在同名 BeanDefinition。如果存在,默认允许覆盖,并会在日志中输出覆盖信息,提示开发者哪个 BeanDefinition 被替换。如果开发者将 allowBeanDefinitionOverriding 设置为 false,则遇到同名 Bean 会抛出 BeanDefinitionOverrideException,从而禁止重复注册。这种机制既保证了启动灵活性,又提供了安全检查与调试提示。
mini-spring 的处理逻辑:
在 mini-spring 中,ClassPathBeanDefinitionScanner 扫描到的同名 Bean 在注册时直接调用 BeanDefinitionRegistry.registerBeanDefinition 存入 Map。由于默认没有做重复检测,后注册的 BeanDefinition 会直接覆盖之前注册的同名 Bean,容器中只保留最后一个注册的 Bean。
3.处理占位符配置 PropertyPlaceholderConfigurer:
功能
- 用于解析 Bean 定义中属性的占位符(placeholder),例如
${property.name}。 - 将占位符替换为真实的值,通常来自外部配置文件(
.properties)或者环境变量。
作用
避免硬编码:例如数据库 URL、用户名、密码、API Key 等可以统一在配置文件管理。
实现属性外部化:配置与代码分离,提高灵活性。
支持统一管理:多个 Bean 可以共享同一个配置属性。
PropertyPlaceholderConfigurer 本质上是一个 BeanFactoryPostProcessor,它在 Bean 实例化之前工作。流程如下:
- 加载配置文件
- 通过
location属性读取.properties文件,加载到Properties对象中。
- 通过
- 遍历 BeanDefinition
- 遍历容器中所有已注册的 BeanDefinition(此时 Bean 还未实例化)。
- 查找属性值中是否包含占位符
${...}。
- 占位符替换
- 根据 key 从 Properties 中获取对应的值。
- 将 BeanDefinition 的属性值替换成真实值。
- Bean 实例化时使用
- 当容器真正实例化 Bean 时,属性已经是最终替换后的值,不需要再次解析。
/**
* 该类实现了Spring的BeanFactoryPostProcessor接口,
* 用于处理Bean定义中的属性占位符,将${...}形式的占位符替换为属性文件中的实际值
*/
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {
/**
* 默认的占位符前缀: {@value}
*/
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
/**
* 默认的占位符后缀: {@value}
*/
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
// 属性文件的位置
private String location;
/**
* 实现BeanFactoryPostProcessor接口的方法,在BeanFactory加载完所有Bean定义后执行
* 用于替换Bean定义中的属性占位符
* @param beanFactory 可配置的Bean工厂,用于获取和修改Bean定义
* @throws BeansException 如果处理过程中发生错误
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 加载属性文件并替换占位符
try {
// 创建默认的资源加载器,用于加载属性文件
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
// 根据配置的位置加载资源
Resource resource = resourceLoader.getResource(location);
// 创建Properties对象并加载资源中的属性
Properties properties = new Properties();
properties.load(resource.getInputStream());
// 获取所有Bean定义的名称
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
// 遍历每个Bean定义
for (String beanName : beanDefinitionNames) {
// 获取当前Bean的定义信息
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
// 获取Bean定义中的所有属性值
PropertyValues propertyValues = beanDefinition.getPropertyValues();
// 遍历每个属性值
for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
// 获取属性值对象
Object value = propertyValue.getValue();
// 如果属性值不是字符串类型,则跳过
if (!(value instanceof String)) continue;
String strVal = (String) value;
// 查找占位符前缀的位置
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);
// 替换占位符为实际属性值
String newValue = strVal.replace(DEFAULT_PLACEHOLDER_PREFIX + propKey + DEFAULT_PLACEHOLDER_SUFFIX, propVal);
// 更新Bean定义中的属性值
propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(), newValue));
}
}
}
} catch (IOException e) {
// 如果加载属性文件失败,抛出异常
throw new BeansException("Could not load properties", e);
}
}
/**
* 设置属性文件的位置
* @param location 属性文件的路径
*/
public void setLocation(String location) {
this.location = location;
}
}
4.解析xml(XmlBeanDefinitionReader)中调用扫描:
此处主要修改了doLoadBeanDefinitions,换了另一个包(dom4j)来解析xml。
protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {
// 1. 创建 SAXReader 对象,使用 dom4j 解析 XML
SAXReader reader = new SAXReader();
// 2. 读取输入流,生成 Document 对象(DOM 树)
Document document = reader.read(inputStream);
// 3. 获取根元素 <beans>
Element root = document.getRootElement();
// ---------------------------
// 4. 解析 <context:component-scan> 标签
// 目的是扫描指定包下的类并生成 BeanDefinition
// ---------------------------
Element componentScan = root.element("component-scan");
if (componentScan != null) {
// 5. 获取 base-package 属性值,支持逗号分隔多个包
String scanPath = componentScan.attributeValue("base-package");
// 6. 如果 base-package 为空,抛出异常
if (StrUtil.isEmpty(scanPath)) {
throw new BeansException("The value of base-package attribute can not be empty or null");
}
// 7. 调用 scanPackage 方法扫描包
scanPackage(scanPath);
}
// ---------------------------
// 8. 解析 <bean> 标签
// ---------------------------
List<Element> beanList = root.elements("bean");
for (Element bean : beanList) {
// 9. 获取 bean 标签的基本属性:id、name、class、init-method、destroy-method、scope
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");
// 10. 根据 class 属性获取 Class 对象
Class<?> clazz = Class.forName(className);
// 11. 确定 Bean 名称,优先使用 id,其次 name,最后类名首字母小写
String beanName = StrUtil.isNotEmpty(id) ? id : name;
if (StrUtil.isEmpty(beanName)) {
beanName = StrUtil.lowerFirst(clazz.getSimpleName());
}
// 12. 创建 BeanDefinition 对象,用于封装 bean 信息
BeanDefinition beanDefinition = new BeanDefinition(clazz);
beanDefinition.setInitMethodName(initMethod);
beanDefinition.setDestroyMethodName(destroyMethodName);
// 13. 如果 scope 不为空,设置到 BeanDefinition
if (StrUtil.isNotEmpty(beanScope)) {
beanDefinition.setScope(beanScope);
}
// ---------------------------
// 14. 解析 <property> 子标签
// 用于读取 Bean 属性并封装成 PropertyValue 对象
// ---------------------------
List<Element> propertyList = bean.elements("property");
for (Element property : propertyList) {
// 15. 获取属性名、属性值、引用
String attrName = property.attributeValue("name");
String attrValue = property.attributeValue("value");
String attrRef = property.attributeValue("ref");
// 16. 判断是引用其他 Bean 还是普通值
Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
// 17. 封装属性名和值到 PropertyValue
PropertyValue propertyValue = new PropertyValue(attrName, value);
// 18. 添加到 BeanDefinition 的属性集合中
beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
}
// 19. 检查容器中是否已有同名 Bean,防止重复注册
if (getRegistry().containsBeanDefinition(beanName)) {
throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
}
// 20. 注册 BeanDefinition 到注册表中
getRegistry().registerBeanDefinition(beanName, beanDefinition);
}
}
/**
* 扫描指定的包路径,生成 BeanDefinition 并注册到容器
* @param scanPath 逗号分隔的基础包路径
*/
private void scanPackage(String scanPath) {
// 1. 分割多个包名
String[] basePackages = StrUtil.splitToArray(scanPath, ',');
// 2. 创建扫描器,传入 BeanDefinition 注册器
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());
// 3. 执行扫描并注册 BeanDefinition
scanner.doScan(basePackages);
}
测试:
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 "CMD137,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;
}
}
2.属性配置文件 classpath:token.properties:
token=123_TEST_TOKEN_321
3.xml 配置文件:
3.1 spring-property.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean class="com.miniSpring.beans.factory.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:token.properties"/>
</bean>
<bean id="userService" class="com.miniSpring.test.bean.UserService">
<property name="token" value="${token}"/>
</bean>
</beans>
3.2 spring-scan.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<component-scan base-package="com.miniSpring.test.bean"/>
</beans>
4.单元测试:
4.1 测试包扫描:
@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());
}
测试结果:CMD137,100001,深圳
Process finished with exit code 0
4.2 测试占位符配置:
@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 = { 123_TEST_TOKEN_321 }
Process finished with exit code 0
