导航
📖 本讲目标
- 理解组件扫描的原理
- 实现 @Component 注解
- 实现类路径扫描功能
- 自动注册 BeanDefinition
- 理解包扫描的优化策略
🎯 组件扫描概述
什么是组件扫描?
组件扫描(Component Scanning) 是 Spring 框架自动发现和注册 Bean 的机制。通过扫描指定包路径下的所有类,自动识别带 @Component 注解的类,并将其注册为 Bean。
传统方式 vs 组件扫描
传统方式(手动注册):
ApplicationContext context = new ApplicationContext();
BeanDefinition bd = new BeanDefinition(UserService.class);
context.registerBeanDefinition("userService", bd);
组件扫描方式(自动注册):
@Component
public class UserService {
// 自动被扫描并注册
}
ApplicationContext context = new ApplicationContext("com.example.demo");
// 自动扫描并注册所有 @Component 类
🏷️ @Component 注解实现
注解定义
创建 src/main/java/com/minispring/stereotype/Component.java:
package com.minispring.stereotype;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Component 注解用于标记一个类为 Spring 管理的组件
* 容器在启动时会扫描所有带此注解的类,并将其注册为 Bean
*/
@Target(ElementType.TYPE) // 只能用于类
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射获取
public @interface Component {
/**
* Bean 的名称,如果不指定则使用类名首字母小写作为默认名称
* 例如:UserService -> userService
*/
String value() default "";
}
使用示例
@Component
public class UserService {
// Bean 名称默认为 "userService"
}
@Component("myUserService")
public class UserService {
// Bean 名称为 "myUserService"
}
🔍 类路径扫描实现
实现思路
类路径扫描的核心流程:
- 包路径转换:将包名转换为文件路径(
com.example→com/example) - 获取资源:通过 ClassLoader 获取包对应的 URL 资源
- 递归扫描:递归扫描目录下的所有
.class文件 - 加载类:使用
Class.forName()加载类 - 处理类:检查是否有 @Component 注解,如果有则注册
实现代码
创建 src/main/java/com/minispring/context/ClassPathBeanScanner.java:
package com.minispring.context;
import com.minispring.beans.factory.config.BeanDefinition;
import com.minispring.context.annotation.Lazy;
import com.minispring.context.annotation.Scope;
import com.minispring.stereotype.Component;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* 类路径扫描器,负责扫描指定包下的所有类
* 识别带有 @Component 注解的类,并将其注册为 BeanDefinition
*/
public class ClassPathBeanScanner {
private final ApplicationContext context;
public ClassPathBeanScanner(ApplicationContext context) {
this.context = context;
}
/**
* 扫描指定包路径下的所有类
* @param basePackage 基础包路径,如 "com.example.demo"
*/
public void scan(String basePackage) {
// 1. 将包路径转换为文件路径
String path = basePackage.replace('.', '/');
// 2. 获取类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
// 3. 获取包对应的 URL 资源
URL resource = classLoader.getResource(path);
if (resource == null) {
throw new RuntimeException("找不到包路径: " + basePackage);
}
// 4. 将 URL 转换为 File 对象
File directory = new File(resource.getFile());
// 5. 递归扫描目录,获取所有 .class 文件
List<Class<?>> classes = findClasses(directory, basePackage);
// 6. 处理每个类,注册为 BeanDefinition
for (Class<?> clazz : classes) {
processClass(clazz);
}
} catch (Exception e) {
throw new RuntimeException("扫描包路径失败: " + basePackage, e);
}
}
/**
* 递归查找目录下的所有类
* @param directory 目录
* @param packageName 包名
* @return 类列表
*/
private List<Class<?>> findClasses(File directory, String packageName) {
List<Class<?>> classes = new ArrayList<>();
if (!directory.exists()) {
return classes;
}
// 获取目录下的所有文件和子目录
File[] files = directory.listFiles();
if (files == null) {
return classes;
}
for (File file : files) {
if (file.isDirectory()) {
// 递归扫描子目录
String subPackage = packageName + "." + file.getName();
classes.addAll(findClasses(file, subPackage));
} else if (file.getName().endsWith(".class")) {
// 加载 .class 文件
String className = packageName + "." +
file.getName().substring(0, file.getName().length() - 6);
try {
Class<?> clazz = Class.forName(className);
classes.add(clazz);
} catch (ClassNotFoundException e) {
throw new RuntimeException("加载类失败: " + className, e);
}
}
}
return classes;
}
/**
* 处理单个类,如果带有 @Component 注解,则注册为 BeanDefinition
* @param clazz 要处理的类
*/
private void processClass(Class<?> clazz) {
// 1. 检查是否有 @Component 注解
Component component = clazz.getAnnotation(Component.class);
if (component == null) {
return; // 不是组件,跳过
}
// 2. 创建 BeanDefinition 对象
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setBeanClass(clazz);
// 3. 确定 Bean 名称
String beanName = component.value();
if (beanName.isEmpty()) {
// 如果没有指定名称,使用类名首字母小写作为默认名称
beanName = toLowerCaseFirstLetter(clazz.getSimpleName());
}
beanDefinition.setBeanName(beanName);
// 4. 处理 @Scope 注解
Scope scope = clazz.getAnnotation(Scope.class);
if (scope != null) {
beanDefinition.setScope(scope.value());
} else {
beanDefinition.setScope("singleton"); // 默认单例
}
// 5. 处理 @Lazy 注解
Lazy lazy = clazz.getAnnotation(Lazy.class);
if (lazy != null) {
beanDefinition.setLazy(lazy.value());
} else {
beanDefinition.setLazy(false); // 默认非懒加载
}
// 6. 注册 BeanDefinition 到容器
context.registerBeanDefinition(beanName, beanDefinition);
System.out.println("注册 Bean: " + beanName +
" [scope=" + beanDefinition.getScope() +
", lazy=" + beanDefinition.isLazy() + "]");
}
/**
* 将字符串首字母转换为小写
* @param str 原字符串
* @return 首字母小写的字符串
*/
private String toLowerCaseFirstLetter(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return Character.toLowerCase(str.charAt(0)) + str.substring(1);
}
}
🔗 集成到 ApplicationContext
更新 ApplicationContext
在 ApplicationContext 中添加扫描功能:
/**
* 扫描指定包路径并初始化容器
* @param basePackage 基础包路径(如 "com.example.demo")
*/
public ApplicationContext(String basePackage) {
// 1. 扫描并注册 BeanDefinition
scan(basePackage);
// 2. 实例化所有非懒加载的单例 Bean
preInstantiateSingletons();
}
/**
* 扫描指定包路径,注册所有带 @Component 注解的类
* @param basePackage 基础包路径
*/
public void scan(String basePackage) {
ClassPathBeanScanner scanner = new ClassPathBeanScanner(this);
scanner.scan(basePackage);
System.out.println("完成包扫描: " + basePackage);
}
🎯 扫描优化策略
1. 过滤策略
可以添加过滤条件,只扫描需要的类:
// 跳过接口和抽象类
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
return;
}
// 跳过内部类
if (clazz.isMemberClass()) {
return;
}
2. 性能优化
- 缓存扫描结果:避免重复扫描
- 并行扫描:使用多线程加速扫描
- 索引文件:使用索引文件快速定位类
3. 路径处理
处理不同环境下的路径问题:
// 处理 JAR 包中的资源
if (resource.getProtocol().equals("jar")) {
// JAR 包处理逻辑
} else {
// 文件系统处理逻辑
}
🧪 测试示例
测试类
package com.minispring.demo;
import com.minispring.context.annotation.Scope;
import com.minispring.stereotype.Component;
import com.minispring.context.ApplicationContext;
@Component
class UserRepository {
public String getName() {
return "UserRepository";
}
}
@Component("myUserService")
@Scope("singleton")
class UserService {
@Autowired
private UserRepository userRepository;
public UserRepository getUserRepository() {
return userRepository;
}
}
public class ComponentScanTest {
public static void main(String[] args) throws Exception {
// 自动扫描并注册
ApplicationContext context = new ApplicationContext("com.minispring.demo");
// 获取 Bean
UserService service = context.getBean("myUserService", UserService.class);
System.out.println("获取到 Bean: " + service);
System.out.println("依赖注入成功: " + service.getUserRepository().getName());
}
}
📝 本讲总结
本讲我们完成了:
- ✅ 理解了组件扫描的原理
- ✅ 实现了 @Component 注解
- ✅ 实现了类路径扫描功能
- ✅ 实现了自动注册 BeanDefinition
- ✅ 集成了扫描功能到 ApplicationContext
- ✅ 理解了扫描优化策略
🎯 关键点回顾
- 包路径转换:
com.example→com/example - 递归扫描:遍历目录树查找所有
.class文件 - 类加载:使用
Class.forName()加载类 - 注解检查:通过反射检查 @Component 注解
- 自动注册:自动创建并注册 BeanDefinition
⚠️ 注意事项
- 路径问题:JAR 包和文件系统的路径处理不同
- 类加载:需要处理类加载异常
- 性能考虑:大量类时扫描可能较慢
- 过滤策略:可以添加过滤条件提高效率
🚀 下一讲预告
在下一讲中,我们将学习 AOP 基础概念,包括:
- 什么是 AOP
- 切面、切点、通知
- JDK 动态代理原理
- AOP 应用场景
准备好了吗?让我们继续 第 09 讲:AOP 基础概念!
思考题:
- 组件扫描的性能如何优化?
- 如何处理 JAR 包中的类扫描?
- 如何实现更灵活的过滤策略?
