1. 项目概述:@Component 不是“加个注解就完事”的魔法标签
你刚在 Spring Boot 项目里写完一个工具类,顺手在类名上方敲下
@Component
,刷新浏览器,功能跑通了——于是你心里一松:“哦,Spring 的自动装配就是这么简单。”但三个月后,当这个类在测试环境突然报
NoSuchBeanDefinitionException
,而开发环境一切正常;或者当你把同一个类挪到另一个模块,发现它被扫描了两次、导致事务失效、线程池冲突;又或者你在面试时被问到“
@Component
和
@Service
有什么本质区别”,你脱口而出“只是语义不同”,结果面试官微微一笑,打开源码问你:“那
@Service
的
@Documented
和
@Target({ElementType.TYPE})
是怎么影响 Spring 容器行为的?”——那一刻,你才意识到:
@Component
这个看似最基础的注解,其实是 Spring 容器启动时第一道闸门,是整个 IoC 体系的地基,更是无数线上故障的隐性源头。
@Component
是 Spring 框架中
最原始、最通用的组件声明注解
,它告诉 Spring:“这个类,我要交给你来管理生命周期、注入依赖、参与 AOP 织入。”它不是语法糖,而是 Spring 容器启动流程中
类路径扫描(ClassPathBeanDefinitionScanner)
的触发开关。所有
@Service
、
@Repository
、
@Controller
都是它的派生注解(
@Service
是
@Component
的
元注解
),它们共享同一套底层机制,但各自承载着不同的工程语义与容器行为约束。比如
@Repository
会自动将数据访问异常转换为 Spring 的统一
DataAccessException
层次结构,而
@Component
不会;
@Controller
会被
RequestMappingHandlerMapping
自动注册为 Web 请求处理器,而
@Component
不会——这些差异,全靠 Spring 在扫描到
@Component
及其派生注解后,根据注解元信息动态加载不同的
BeanPostProcessor
和
BeanFactoryPostProcessor
来实现。
我带过三届校招生,在他们第一次独立重构微服务模块时,80% 的 Bean 冲突、循环依赖、扫描遗漏问题,根源都出在对
@Component
的“想当然”使用上。有人把
@Component
加在工具类的静态内部类上,结果 Spring 扫描失败(静态内部类默认不被
@ComponentScan
处理);有人在
@Configuration
类里用
@Bean
方法定义了一个对象,又同时给该类加了
@Component
,导致同一个实例被注册两次;还有人把
@Component
加在接口实现类上,却忘了在
@ComponentScan
中配置正确的 basePackages,结果生产环境因包路径大小写敏感(Linux)而彻底找不到 Bean。这些都不是“高级技巧缺失”,而是对
@Component
背后那套
扫描-注册-实例化-依赖注入-初始化
的完整链条缺乏具象认知。所以这篇内容,不讲“怎么用”,只讲“为什么必须这样用”——从字节码层面看它如何被
ASM
解析,从容器启动日志里追踪它如何被
AnnotatedBeanDefinitionReader
注册,从 JVM 线程堆栈中观察它如何参与
AbstractAutowireCapableBeanFactory
的 createBean 流程。它适合两类人:一是刚写完第一个
@RestController
的新手,需要建立对 Spring 容器的敬畏心;二是已能手写
BeanFactoryPostProcessor
的老手,需要补全对基础注解的底层掌控力。这不是入门教程,而是一份你调试线上
BeanCreationException
时真正会翻出来的现场手册。
2. 核心设计思路拆解:为什么 Spring 选择
@Component
作为组件声明的基石?
2.1 从 XML 到注解:Spring 容器演进的必然选择
在 Spring 2.0 时代,我们靠
<bean id="userService" class="com.example.UserService"/>
声明 Bean。这种方式清晰、可控,但致命缺陷是
配置与代码严重割裂
。一个 UserService 类修改了构造函数参数,XML 就得同步改
<constructor-arg>
;团队协作时,新人要理解一个模块,得同时打开 Java 文件和十几个 XML 配置文件。更麻烦的是,当项目模块数超过 50,XML 文件体积膨胀到 MB 级,IDE 加载卡顿,Git 合并冲突频发。Spring 团队在 2007 年推出
@Component
,表面是语法糖,实则是
架构范式的迁移
:把组件声明权从中心化配置文件,下放到每个类自身。这背后有三层深意:
第一层是
关注点分离的再定义
。XML 配置承担了“容器管理职责”(生命周期、作用域、依赖关系),而
@Component
让每个类自己声明“我是否愿意被容器管理”。这种契约式声明,比 XML 的强制注入更符合面向对象的封装原则。就像你不会在 XML 里写“这个类必须被单例”,而是让类通过
@Scope("singleton")
主动申明自己的作用域策略。
第二层是
可发现性(Discoverability)的工程实践
。Spring Boot 的
@SpringBootApplication
默认启用
@ComponentScan
,它会递归扫描指定包路径下的所有类,只要类上有
@Component
或其派生注解,就立即纳入候选 Bean。这种“主动上报”机制,让新模块接入系统变得极简:你只需把新模块的包路径加到
@ComponentScan(basePackages = {"com.example.newmodule"})
,无需修改任何中心配置。我曾参与一个金融核心系统升级,从 Spring 3.x 迁移到 Spring Boot 2.x,200+ 个子模块的 Bean 注册工作,仅靠调整
@ComponentScan
的包路径就完成了 90%,剩余 10% 是历史遗留的 XML 配置,这才是
@Component
设计的真正价值——它让大型分布式系统的模块治理成本呈指数级下降。
第三层是
元编程(Metaprogramming)能力的释放
。
@Component
本身是一个
@Retention(RetentionPolicy.RUNTIME)
的注解,这意味着它在运行时可通过反射读取。Spring 容器在启动时,会用
ClassPathScanningCandidateComponentProvider
扫描 classpath,调用
MetadataReader.getAnnotationMetadata().getAnnotationTypes()
获取所有注解类型,再通过
AnnotationTypeFilter
判断是否匹配
@Component
。这个过程完全不依赖类的字节码执行,只读取
.class
文件的常量池和注解表。正因如此,
@Component
才能支撑起 Spring Cloud 的
@EnableDiscoveryClient
、Spring Security 的
@EnableWebSecurity
等一系列基于注解的自动装配扩展——它们本质上都是在
@Component
的扫描结果上,叠加了额外的
BeanDefinitionRegistryPostProcessor
来动态注册新 Bean。没有
@Component
这个统一入口,整个 Spring 生态的自动装配大厦就会坍塌。
2.2
@Component
与派生注解的本质关系:语义分层而非功能替代
很多人误以为
@Service
是
@Component
的“增强版”,能做更多事。这是典型误解。我们来看
@Service
的源码定义:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 注意这一行!
public @interface Service {
String value() default "";
}
关键点在于
@Component
这行注解——它表明
@Service
是
@Component
的
元注解(Meta-annotation)
。Spring 的
ClassPathBeanDefinitionScanner
在扫描时,会递归解析类上的所有注解,如果发现某个注解自身标注了
@Component
,就认为该类是候选组件。也就是说,
@Service
类能被扫描到,
完全是因为它继承了
@Component
的元注解属性,而不是 Spring 容器对
@Service
做了特殊处理
。
那
@Service
的价值在哪?在
语义分层与生态协同
。Spring 团队刻意设计了四层语义注解:
-
@Component:通用组件,无业务语义,适用于工具类、配置类等; -
@Service:业务服务层,暗示该类包含核心业务逻辑,会被TransactionManagementConfigurer自动识别为事务切面的候选目标; -
@Repository:数据访问层,其@ExceptionHandler会自动捕获SQLException并转换为 Spring 的DataAccessException; -
@Controller:Web 层,被RequestMappingHandlerMapping识别为请求处理器,并支持@ResponseBody等 Web 特性。
这种分层不是为了增加功能,而是为了
降低框架的耦合度
。假设你用
@Component
替代
@Repository
,Spring 依然能创建 Bean,但
PersistenceExceptionTranslationPostProcessor
就不会对该 Bean 生效,数据库异常无法被统一转换。同理,用
@Component
替代
@Controller
,
@RequestMapping
注解会失效,因为
RequestMappingHandlerMapping
的
isHandler()
方法只检查
@Controller
和
@RequestMapping
的组合。我在线上排查过一个经典案例:某支付服务将
PaymentService
标记为
@Component
,结果事务注解
@Transactional
在某些分支路径下失效。根因是 Spring 的
TransactionInterceptor
在判断代理目标时,会优先检查类是否被
@Service
或
@Repository
标记(通过
AnnotationUtils.findAnnotation()
),若未找到,会退回到更宽泛的
@Component
检查,但此时可能因
@Transactional
的传播行为配置不当,导致事务边界错乱。这说明:语义注解是 Spring 生态各模块间
约定的通信协议
,跳过协议直接用底层
@Component
,等于在高速公路上开拖拉机——能跑,但随时可能被其他车辆的规则制裁。
2.3 扫描机制的底层逻辑:
@ComponentScan
如何精准定位你的类?
@Component
本身不触发扫描,它只是“身份证”;真正干活的是
@ComponentScan
。这个注解通常出现在
@SpringBootApplication
中(
@SpringBootApplication
是
@ComponentScan
、
@EnableAutoConfiguration
、
@Configuration
的组合注解)。
@ComponentScan
的核心参数
basePackages
决定了扫描范围,其原理远比“遍历文件夹”复杂:
-
资源定位(Resource Location) :
ClassPathScanningCandidateComponentProvider首先调用ResourcePatternResolver.getResources("classpath*:**/com/example/**/**/*.class"),生成所有匹配的Resource对象。注意classpath*:表示扫描所有 classpath(包括 jar 包),而**是 Ant 风格通配符,对应任意深度的子目录。 -
元数据读取(Metadata Reading) :对每个
Resource,Spring 使用SimpleMetadataReader(基于 ASM 字节码库)读取.class文件的AnnotationMetadata。ASM 的优势在于 无需加载类到 JVM ,直接解析字节码的常量池,速度比反射快 10 倍以上。它只关心RuntimeVisibleAnnotations属性,提取所有@Component相关注解。 -
类型过滤(Type Filtering) :
AnnotationTypeFilter会检查AnnotationMetadata.getAnnotationTypes()返回的注解集合,判断是否包含@Component或其派生注解。这里有个关键细节:@ComponentScan支持includeFilters和excludeFilters,例如:@ComponentScan( basePackages = "com.example", includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Service.class}), excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*") )这种过滤是在元数据层面完成的, 不触发类加载 ,因此即使
excludeFilters匹配到一个有复杂静态块的类,也不会执行其初始化代码。 -
BeanDefinition 构建(Definition Building) :对通过过滤的类,
ScannedGenericBeanDefinition会构建BeanDefinition对象,设置beanClassName、scope(默认 singleton)、lazyInit(默认 false)等属性。此时@Component的value()属性(即 Bean 名称)会被读取,若为空,则默认使用类名首字母小写(UserService→userService)。
我曾在一个高并发订单系统中,因
@ComponentScan(basePackages = "com")
扫描了整个
com
包,导致容器启动时间从 3 秒飙升到 47 秒。通过
debug
模式跟踪
ClassPathScanningCandidateComponentProvider.findCandidateComponents()
,发现它扫描了 12000+ 个类(包括
com.google
、
com.fasterxml
等第三方包)。解决方案不是减少
@Component
数量,而是
精确控制
basePackages
:
@ComponentScan(basePackages = "com.example.order, com.example.payment")
,将扫描范围缩小到 3 个业务包,启动时间回归 3.2 秒。这印证了一个铁律:
@Component
的威力,永远取决于
@ComponentScan
的精度。
3. 核心细节解析与实操要点:那些官方文档绝不会写的“坑”
3.1
@Component
的
value()
参数:Bean 名称的生成规则与陷阱
@Component
的
value()
参数用于显式指定 Bean 的名称,例如
@Component("userCacheManager")
。但多数人忽略了一个事实:
当
value()
为空字符串时,Spring 的默认命名策略并非简单的“类名小写”,而是遵循 JavaBeans 规范的
Introspector.decapitalize()
方法
。我们来对比几个真实案例:
| 类名 |
@Component
未指定
value()
| 实际 Bean 名称 | 原因分析 |
|---|---|---|---|
UserService
|
userService
| ✅ 正确 | 符合标准驼峰转小写 |
XMLParser
|
xMLParser
|
❌ 错误!应为
xmlParser
|
Introspector.decapitalize()
对连续大写字母处理异常,将
XML
视为缩写,只小写首字母
X
|
URLValidator
|
uRLValidator
|
❌ 错误!应为
urlValidator
|
同上,
URL
被识别为缩写
|
iOSAppService
|
iOSAppService
|
❌ 错误!应为
iosAppService
|
iOS
中的
O
和
S
被视为独立大写字母
|
这个问题在 Spring 5.2.0 之前是普遍存在的 Bug,直到 Spring 5.2.2 才修复。但很多老项目仍运行在 Spring 4.x,这就成了隐形雷。我在线上遇到过一次事故:一个
@Component
类叫
IOSPushService
,在开发环境 Bean 名称是
iOSPushService
,而测试环境因 JDK 版本不同(JDK 8u202 vs 8u292),
Introspector.decapitalize()
行为不一致,导致
@Autowired private IOSPushService iosPushService;
在测试环境注入失败,报
No qualifying bean of type 'IOSPushService' available
。
解决方案有三 :
-
显式指定
value():@Component("iosPushService"),一劳永逸; -
重命名类
:改为
IosPushService,符合 JavaBeans 规范; -
自定义 BeanNameGenerator
:在
@ComponentScan中指定:
其中@ComponentScan( basePackages = "com.example", nameGenerator = CustomBeanNameGenerator.class ) public class AppConfig {}CustomBeanNameGenerator继承AnnotationBeanNameGenerator,重写buildDefaultBeanName()方法,用正则替换([A-Z]+)([A-Z][a-z])为$1_$2再转小写,彻底解决缩写问题。
提示:在 Spring Boot 2.2+ 中,可通过
spring.main.allow-bean-definition-overriding=true允许 Bean 名称覆盖,但这只是掩盖问题,而非解决。真正的工程规范是: 所有@Component必须显式指定value(),且值必须是合法的 Java 标识符(不能含-、.等符号) 。
3.2
@Component
与
@Configuration
的共存陷阱:双重注册的静默灾难
这是最隐蔽、最致命的坑。当你在一个类上同时标注
@Configuration
和
@Component
,会发生什么?答案是:
该类会被注册两次 Bean
。一次是
@Configuration
作为配置类,由
ConfigurationClassPostProcessor
处理;另一次是
@Component
作为普通组件,由
ClassPathBeanDefinitionScanner
处理。由于
@Configuration
类默认是
@Component
的派生注解(
@Configuration
本身标注了
@Component
),所以双重注册几乎不可避免。
我们用代码验证:
@Configuration
@Component // 危险!
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(); // 创建一个 DataSource Bean
}
}
启动日志会显示:
Creating shared instance of singleton bean 'databaseConfig'
Creating shared instance of singleton bean 'databaseConfig' // 第二次!
更可怕的是,
dataSource()
方法会被调用两次,创建两个独立的
HikariDataSource
实例,而你的
@Transactional
可能只作用于其中一个实例,导致事务失效。
为什么 Spring 不阻止这种写法?
因为
@Configuration
和
@Component
在 Spring 的设计哲学中属于不同维度:
@Configuration
是“配置来源”,
@Component
是“组件声明”。Spring 认为开发者应该清楚自己在做什么。但现实是,很多开发者复制粘贴代码时,看到别人在配置类上加了
@Component
,就跟着加,殊不知这是反模式。
避坑指南 :
-
@Configuration类 绝对不要 加@Component; -
如果你需要将配置类作为 Bean 注入(例如在
@Test中@Autowired配置类),请用@Import(DatabaseConfig.class)方式,而非@Component; -
在
@Configuration类中定义的@Bean方法,其返回对象的生命周期由@Configuration类管理,与@Component无关; -
使用 IDE 插件(如 IntelliJ 的 Spring Assistant)开启
@Configuration类的@Component检测警告,提前拦截。
3.3
@Component
在多模块项目中的扫描边界:Maven 依赖与包路径的博弈
在典型的 Maven 多模块项目中,常见结构如下:
parent/
├── common/ # 工具类、实体类
├── user-service/ # 用户服务模块
├── order-service/ # 订单服务模块
└── api-gateway/ # 网关模块
问题来了:
user-service
模块中的
@Component
类,能否被
order-service
扫描到?答案是:
不能,除非你显式配置
@ComponentScan
。
原因在于
@ComponentScan
的
basePackages
默认只扫描
当前模块的 classpath
。
user-service
编译后的
target/classes
目录不在
order-service
的 classpath 中(Maven 依赖传递的是 jar 包,不是编译输出目录)。即使你在
order-service
的
pom.xml
中添加了
<dependency><groupId>com.example</groupId><artifactId>user-service</artifactId></dependency>
,Spring 也只会扫描
user-service-1.0.0.jar!/com/example/user/**
下的类,前提是
user-service
的
@Component
类在
com.example.user
包下,且
order-service
的
@ComponentScan
包含该路径。
我曾在一个电商项目中踩过这个坑:
common
模块定义了一个
@Component
的
IdGenerator
,
user-service
和
order-service
都依赖
common
。但
order-service
的
@ComponentScan(basePackages = "com.example.order")
没有包含
com.example.common
,导致
IdGenerator
无法注入,
order-service
启动时报
UnsatisfiedDependencyException
。
正确做法有二 :
-
统一扫描根包
:在
api-gateway(启动模块)的主类上,@ComponentScan(basePackages = "com.example"),覆盖所有子模块; -
按需导入
:在
order-service的配置类中,用@Import(IdGenerator.class)显式导入,而非依赖扫描。
注意:
@Import导入的类,即使没有@Component注解,也会被注册为 Bean。这是 Spring 提供的“手动注册”机制,比扫描更精准、更可控。
3.4
@Component
与
@Lazy
的组合:延迟加载的真相与适用场景
@Lazy
常被误解为“懒加载 Bean”,实则它是
控制 Bean 创建时机的开关
。
@Component
类默认是
EAGER
(急切)加载:容器启动时就调用
createBean()
创建实例。而
@Lazy
会让 Spring 生成一个代理对象(CGLIB 或 JDK Proxy),只有第一次调用其方法时,才真正创建目标 Bean。
但有一个关键限制:
@Lazy
只对单例(Singleton)Bean 有效
。对于
@Scope("prototype")
的
@Component
,每次
getBean()
都会新建实例,
@Lazy
无意义。
更隐蔽的坑是:
@Lazy
与
@PostConstruct
的冲突。看这个例子:
@Component
@Lazy
public class CacheLoader {
@PostConstruct
public void init() {
System.out.println("Cache loaded!"); // 这行永远不会执行!
}
}
因为
@PostConstruct
是在 Bean 初始化阶段(
initializeBean()
)调用的,而
@Lazy
的代理对象在初始化时并不会触发目标 Bean 的创建,所以
init()
方法根本不会被执行。
@PostConstruct
的执行前提是 Bean 已被创建,而
@Lazy
延迟了创建时机。
何时该用
@Lazy
?
-
解决循环依赖
:A 依赖 B,B 依赖 A,且无法用
@Setter或@Lookup解决时,@Lazy是最后手段; -
避免启动耗时
:如
@Component类的构造函数中连接外部 Redis,而该服务在启动时并不需要,可用@Lazy延迟连接; -
条件化加载
:结合
@ConditionalOnProperty,实现配置驱动的组件加载。
何时不该用?
-
@PostConstruct必须执行的初始化逻辑; -
@PreDestroy清理逻辑(@LazyBean 的销毁时机不可控); -
作为
@EventListener监听容器事件的类(事件发布时 Bean 可能尚未创建)。
4. 实操过程与核心环节实现:从零开始构建一个可调试的
@Component
系统
4.1 构建最小可运行环境:剥离 Spring Boot 的“黑盒”干扰
要真正理解
@Component
,必须绕过 Spring Boot 的自动配置,用最原始的 Spring Framework 构建容器。这样你能看到每一行日志背后的逻辑。以下是纯 Spring Framework 的启动代码:
// 1. 创建 AnnotationConfigApplicationContext(非 Spring Boot)
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext();
// 2. 注册配置类(相当于 @SpringBootApplication)
context.register(AppConfig.class);
// 3. 手动触发扫描(模拟 @ComponentScan)
ClassPathBeanDefinitionScanner scanner =
new ClassPathBeanDefinitionScanner(context);
scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
scanner.scan("com.example"); // 指定扫描包
// 4. 刷新容器(触发 Bean 创建)
context.refresh();
// 5. 获取 Bean 验证
UserService userService = context.getBean(UserService.class);
System.out.println("Bean created: " + userService);
对应的
AppConfig
:
@Configuration
public class AppConfig {
// 手动注册一个 Bean,用于对比
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
}
UserService
:
@Component // 这是我们要研究的核心
public class UserService {
private final DataSource dataSource;
// 构造函数注入,验证依赖注入是否生效
public UserService(DataSource dataSource) {
this.dataSource = dataSource;
System.out.println("UserService created with dataSource: " + dataSource);
}
}
关键调试点 :
-
在
ClassPathBeanDefinitionScanner.scan()处打断点,观察findCandidateComponents()返回的Resource列表; -
在
ScannedGenericBeanDefinition构造函数中,查看beanClassName和scope属性; -
在
AbstractAutowireCapableBeanFactory.createBean()中,观察doCreateBean()的执行栈,确认@ComponentBean 的创建流程与@Bean方法的区别。
提示:在
logback-spring.xml中添加以下配置,开启 Spring 扫描日志:<logger name="org.springframework.context.annotation.ClassPathBeanDefinitionScanner" level="DEBUG"/> <logger name="org.springframework.beans.factory.support.DefaultListableBeanFactory" level="DEBUG"/>启动时你会看到类似
Scanning for beans matching filter: annotation [org.springframework.stereotype.Component]的日志,这就是@Component被识别的瞬间。
4.2
@Component
的生命周期钩子:从
@PostConstruct
到
DisposableBean
@Component
类的生命周期,由 Spring 容器严格管控。理解其钩子执行顺序,是调试 Bean 初始化问题的关键。完整生命周期如下(以单例 Bean 为例):
- 实例化(Instantiation) :调用构造函数创建对象;
-
属性填充(Populate Properties)
:注入
@Autowired、@Value等依赖; -
Aware 接口回调
:依次调用
BeanNameAware.setBeanName()、BeanFactoryAware.setBeanFactory()、ApplicationContextAware.setApplicationContext(); -
@PostConstruct方法 :执行@PostConstruct标注的方法(在CommonAnnotationBeanPostProcessor中处理); -
InitializingBean.afterPropertiesSet():执行该接口方法; -
自定义
init-method:执行@Bean(initMethod = "init")或 XML 中的init-method; - Bean 就绪 :可被其他 Bean 依赖注入;
-
@PreDestroy方法 :容器关闭时,执行@PreDestroy标注的方法; -
DisposableBean.destroy():执行该接口方法; -
自定义
destroy-method:执行@Bean(destroyMethod = "close")。
我们用代码验证执行顺序:
@Component
public class LifecycleBean implements InitializingBean, DisposableBean {
public LifecycleBean() {
System.out.println("1. Constructor");
}
@Autowired
public void setDataSource(DataSource dataSource) {
System.out.println("2. Property injection");
}
@Override
public void setBeanName(String name) {
System.out.println("3. BeanNameAware: " + name);
}
@PostConstruct
public void postConstruct() {
System.out.println("4. @PostConstruct");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5. InitializingBean.afterPropertiesSet");
}
public void customInit() {
System.out.println("6. custom init-method");
}
@PreDestroy
public void preDestroy() {
System.out.println("8. @PreDestroy");
}
@Override
public void destroy() throws Exception {
System.out.println("9. DisposableBean.destroy");
}
public void customDestroy() {
System.out.println("10. custom destroy-method");
}
}
在
@Configuration
中注册:
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public LifecycleBean lifecycleBean() {
return new LifecycleBean();
}
启动日志输出:
1. Constructor
2. Property injection
3. BeanNameAware: lifecycleBean
4. @PostConstruct
5. InitializingBean.afterPropertiesSet
6. custom init-method
...
8. @PreDestroy
9. DisposableBean.destroy
10. custom destroy-method
关键结论 :
-
@PostConstruct是 最常用、最安全的初始化入口 ,因为它在所有依赖注入完成后执行,且不依赖特定接口; -
@PreDestroy是 唯一可靠的销毁入口 ,DisposableBean.destroy()和destroy-method在某些容器关闭场景下可能不被调用; -
@Component类若实现了InitializingBean或DisposableBean,必须确保@PostConstruct和@PreDestroy的逻辑与接口方法不冲突(例如不要在@PostConstruct中重复初始化已在afterPropertiesSet()中做的工作)。
4.3
@Component
的作用域(Scope)实战:
@Scope("prototype")
的线程安全陷阱
@Component
默认是
singleton
(单例),但你可以用
@Scope("prototype")
声明原型 Bean。这看似简单,却暗藏线程安全危机。
看这个例子:
@Component
@Scope("prototype")
public class RequestContext {
private String userId;
public void setUserId(String userId) {
this.userId = userId; // 危险!非线程安全
}
public String getUserId() {
return userId;
}
}
在 Web 环境中,
RequestContext
通常被注入到
@Service
类中:
@Service
public class OrderService {
@Autowired
private RequestContext requestContext; // 每次 getBean() 都是新实例
public void createOrder() {
String userId = requestContext.getUserId(); // 可能为 null!
// ... 业务逻辑
}
}
问题在于:
@Scope("prototype")
的 Bean,
每次
getBean()
都返回新实例,但 Spring 不会自动帮你管理其状态
。
setUserId()
方法在多线程环境下,如果多个请求并发调用
OrderService.createOrder()
,
requestContext
实例是隔离的,但
userId
字段的赋值操作本身是线程安全的(因为每个实例独享字段)。真正的问题是:
你如何确保
userId
在每次请求开始时被正确设置?
解决方案是
@Scope("request")
(Web 环境专属):
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
// ...
}
proxyMode = ScopedProxyMode.TARGET_CLASS
会为
RequestContext
生成 CGLIB 代理,当
OrderService
注入时,实际注入的是代理对象。每次调用
getUserId()
,代理会从当前
HttpServletRequest
的
ThreadLocal
中获取真实的
RequestContext
实例,确保线程隔离。
@Scope
取值对照表
:
| Scope 值 | 适用场景 | 生命周期 | 是否线程安全 | 代理模式要求 |
|---|---|---|---|---|
singleton
| 默认,全局唯一 | 容器启动到关闭 | 是(单例无状态) | 否 |
prototype
|
每次
getBean()
新建
| 每次调用 | 是(实例隔离) | 否 |
request
| Web 请求级别 | 请求开始到结束 |
是(
ThreadLocal
隔离)
|
是(
TARGET_CLASS
)
|
session
| HTTP Session 级别 | Session 创建到销毁 | 是(Session 隔离) |
是(
TARGET_CLASS
)
|
application
| ServletContext 级别 | 应用启动到关闭 | 否(需自行同步) | 否 |
注意:
@Scope("request")在非 Web 环境(如单元测试)中会报IllegalStateException: No Scope registered for scope name 'request'。解决方案是在测试类中添加@WebAppConfiguration,或使用MockServletContext。
4.4
@Component
的条件化注册:
@Conditional
与
@Profile
的深度应用
@Component
不是“一刀切”的开关,而是可以按条件激活的组件。
@Conditional
是 Spring 4.0 引入的条件化注解,它接受一个
Condition
实现类,该类的
matches()
方法返回
true
时,
@Component
才会被注册。
我们来实现一个“按数据库类型加载不同 DAO”的场景:
@Component
@Conditional(MySQLCondition.class)
public class MySQLUserDao implements UserDao {
// MySQL 特有实现
}
@Component
@Conditional(PostgreSQLCondition.class)
public class PostgreSQLUserDao implements UserDao {
// PostgreSQL 特有实现
}
MySQLCondition
:
public class MySQLCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 从 Environment 中读取 spring.datasource.url
String url = context.getEnvironment()
.getProperty("spring.datasource.url", "");
return url.contains("mysql");
}
}
PostgreSQLCondition
同理,检查
postgresql
。
@Profile
是
@Conditional
的语法糖
:
@Component
@Profile("mysql")
public class MySQLUserDao implements UserDao { }
@Component
@Profile("postgresql")
public class PostgreSQLUserDao implements UserDao { }
@Profile("mysql")
等价于
@Conditional(ProfileCondition.class)
,而
ProfileCondition.matches()
会检查
Environment.getActiveProfiles()
是否包含
"mysql"
。
高级技巧:组合条件
Spring 5.0+ 支持
@ConditionalOn...
系列注解,它们是
@Conditional
的预设实现:
-
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true"):检查配置项; -
@ConditionalOnClass(DataSource.class):
348

被折叠的 条评论
为什么被折叠?



