Spring 组件:一场关于“对象如何被组织起来”
Spring 组件:一场关于“对象如何被组织起来”
节目类型:技术播客逐字稿
主题:Spring Framework 与 Spring Boot 中的核心组件
建议时长:15~20 分钟
适合听众:Java 初学者、正在使用 Spring Boot 的开发者、想弄清@Component背后机制的工程师
节目简介
很多人第一次接触 Spring,记住的是一串注解:@Component、@Service、@Repository、@Autowired、@Bean。注解记了不少,可一到实际项目里,还是会问:这个对象到底是谁创建的?为什么能自动注入?@Service 和 @Component 究竟差在哪儿?
这期节目不把 Spring 讲成一张注解背诵表,而是从一个核心问题出发:Spring 是如何把一堆普通 Java 对象,组织成一个可配置、可扩展、可维护的应用系统的?
片头:Spring 到底解决了什么问题?
主播:
欢迎收听本期节目。今天我们聊一个看起来老生常谈、实际上经常被用错的话题:Spring 组件。
先想象一个没有 Spring 的业务系统。控制器需要服务,服务需要仓储,仓储需要数据库客户端。于是代码里到处都是这样的写法:
public class OrderController {
private final OrderService orderService;
public OrderController() {
this.orderService = new OrderService(
new OrderRepository(new DatabaseClient())
);
}
}
这段代码并不是不能运行,问题在于 OrderController 不仅负责处理请求,还负责决定依赖怎么创建、怎么组合、什么时候释放。业务代码和对象组装逻辑搅在一起,项目一大,维护就开始变得像拆一团耳机线。
Spring 的核心思路是:把对象的创建、组装、生命周期和部分横切能力交给容器管理,业务类本身尽量保持普通 Java 对象的形态。
所以,Spring 组件不是某种神秘的“特殊对象”,它本质上仍然是一个 Java 对象,只是它被 Spring 容器发现、注册、创建并管理了。
第一部分:什么叫 Spring 组件?
1. 组件的本质是 Bean
在 Spring 语境里,最重要的词不是注解,而是 Bean。
Bean 可以理解为:交给 Spring ApplicationContext 管理的对象。容器会保存它的定义,并根据配置完成实例化、依赖注入、初始化和销毁。
一个最简单的组件是这样:
@Component
public class PriceCalculator {
public BigDecimal calculate(BigDecimal price) {
return price.multiply(new BigDecimal("0.90"));
}
}
当 Spring 扫描到这个类时,会把它注册为 Bean。之后其他组件可以直接依赖它:
@Service
public class OrderService {
private final PriceCalculator priceCalculator;
public OrderService(PriceCalculator priceCalculator) {
this.priceCalculator = priceCalculator;
}
}
这里没有手动调用 new PriceCalculator(),也没有在 OrderService 里编写查找逻辑。对象之间的关系由容器完成,这就是控制反转,也就是 IoC,Inversion of Control。
2. @Component 和几个常见派生注解
Spring 提供了一组语义化注解,它们大多都可以被组件扫描识别:
| 注解 | 典型用途 | 关键含义 |
|---|---|---|
@Component |
通用组件 | 这是一个由 Spring 管理的组件 |
@Service |
业务服务层 | 表达业务逻辑和用例编排 |
@Repository |
数据访问层 | 表达持久化边界,并支持异常转换 |
@Controller |
MVC 控制器 | 处理 Web 请求并返回视图 |
@RestController |
REST 接口 | 控制器方法默认返回响应体 |
@Configuration |
配置类 | 声明 Bean 和应用配置 |
从继承关系上看,@Service、@Repository 和 @Controller 都是 @Component 的特殊形式。它们并没有把 Java 类变成不同的语言结构,主要价值是表达架构意图。
不过 @Repository 有一点额外价值:Spring 可以把底层持久化异常转换成统一的数据访问异常体系。注解不是装饰品,语义和框架行为要一起看。
第二部分:@Component 和 @Bean,到底怎么选?
这是 Spring 初学者最容易混淆的一组概念。
@Component:标记自己的业务类
当一个类由我们自己编写,并且适合通过组件扫描自动发现,可以使用 @Component 或它的语义化派生注解:
@Service
public class UserService {
// 业务逻辑
}
@Bean:在配置中显式声明对象
当对象来自第三方库,不能直接修改源码,或者创建过程需要明确配置时,使用 @Bean:
@Configuration
public class ClientConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.findAndRegisterModules();
return mapper;
}
}
简单来说:能给自己写的类加组件注解,就让它被扫描;需要精确控制构造过程,就用 @Bean。
还有一个经常被忽略的事实:@Configuration 类默认会被 Spring 增强,确保同一个配置方法在容器语境下返回的是同一个 Bean,而不是每调用一次就重新创建一个对象。这也是为什么配置类不能只当成普通工具类来理解。
第三部分:依赖注入,Spring 是怎么把对象接上的?
依赖注入就是 DI,Dependency Injection。它的含义很直白:对象需要什么依赖,不自己创建,由外部注入进来。
推荐构造器注入
@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
private final PaymentRepository paymentRepository;
public PaymentService(
PaymentGateway paymentGateway,
PaymentRepository paymentRepository) {
this.paymentGateway = paymentGateway;
this.paymentRepository = paymentRepository;
}
}
构造器注入有三个优点:
- 依赖是显式的,类需要什么一眼就能看出来。
final字段保证对象创建完成后依赖不会被替换。- 单元测试时可以直接传入 fake 或 mock,不需要启动 Spring 容器。
如果类只有一个构造器,现代 Spring 通常不要求再额外写 @Autowired。这让代码更接近普通 Java,也减少了注解噪音。
出现多个候选 Bean 怎么办?
比如系统里有两个支付网关:
@Component("wechatGateway")
public class WechatPaymentGateway implements PaymentGateway {
}
@Component("alipayGateway")
public class AlipayPaymentGateway implements PaymentGateway {
}
此时直接注入 PaymentGateway 会出现歧义。可以用 @Qualifier 指定名称:
public PaymentService(
@Qualifier("wechatGateway") PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
也可以用 @Primary 指定默认实现。工程里更推荐使用有意义的接口和限定名称,别把 Bean 名称写成一串看不出业务含义的字母。
第四部分:组件生命周期与作用域
Spring 不只负责创建 Bean,还会管理它的生命周期。
默认情况下,Bean 是 singleton 作用域,也就是在一个 Spring 容器中只创建一个实例:
@Component
public class TokenCache {
}
常见作用域包括:
singleton:容器内单例,默认值。prototype:每次获取时创建新实例。request:每个 HTTP 请求一个实例,适用于 Web 场景。session:每个 HTTP Session 一个实例。
这里有个非常实用的工程判断:无状态服务适合使用默认 singleton;有可变共享状态的组件必须认真考虑线程安全。
单例不等于“所有线程都安全”。如果一个 singleton 里放了普通可变字段,它可能被多个请求线程同时访问,问题就不是 Spring 注解能替你解决的了。
生命周期回调也可以通过注解表达:
@Component
public class SearchIndex {
@PostConstruct
public void start() {
// 加载索引或建立连接
}
@PreDestroy
public void stop() {
// 释放资源
}
}
资源型组件要特别注意关闭逻辑。连接池、线程池、文件句柄,打开之后不关,最后都得由线上告警来提醒你,代价通常比较贵。
第五部分:Spring Boot 把这些组件串了起来
如果说 Spring Framework 提供了容器和组件模型,那么 Spring Boot 做的事情,是把大量常见配置自动化,让应用可以更快启动。
1. 自动配置
@SpringBootApplication 通常组合了三个能力:
@SpringBootConfiguration:表明这是应用配置入口。@EnableAutoConfiguration:根据 classpath 和配置自动装配框架能力。@ComponentScan:扫描并注册应用组件。
也就是说,我们写下一个启动类之后,Spring Boot 会根据依赖、配置和条件判断,自动准备 Web、JSON、数据库等基础设施。
2. Starter
Starter 是一组经过整理的依赖集合。例如 Web 应用常见的 spring-boot-starter-web,会带上 Spring MVC、内嵌服务器和 JSON 相关能力。
它解决的不是“有没有依赖”的问题,而是“哪些依赖应该一起使用、版本怎么保持兼容”的问题。
3. 自动配置不是魔法
自动配置通常基于条件注解,例如:
- classpath 中存在某个类;
- 容器中还没有用户自定义的某个 Bean;
- 配置文件中打开了某个开关;
- 当前应用处于特定环境。
当自动配置和预期不一致时,正确的排查路径是查看条件评估报告、启动日志和最终的 Bean 定义,而不是盲目地重复添加注解。注解叠得越多,问题通常越像一锅被搅乱的粥。
第六部分:AOP 和代理,为什么 @Transactional 有时“不生效”?
Spring 组件体系里还有一类非常关键的能力:AOP,面向切面编程。
事务、缓存、权限校验、日志、重试,这些能力都属于横切关注点。Spring 常通过代理对象,在目标方法前后织入额外行为。
例如:
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 写入订单
// 扣减库存
}
}
调用方拿到的往往不是原始 OrderService,而是 Spring 创建的代理对象。代理先开启事务,再调用目标方法,最后根据执行结果提交或回滚。
这也解释了一个经典问题:同一个类内部通过 this.createOrder() 调用方法时,可能绕过代理,导致事务、缓存或切面不生效。
排查这类问题时,要问的不是“注解写没写”,而是:
- 这个 Bean 是否真的被 Spring 管理?
- 调用方拿到的是不是代理对象?
- 调用是否经过了代理边界?
- 事务管理器和数据库配置是否匹配?
这比盯着注解颜色看半天靠谱得多。
第七部分:一套小型组件架构示例
把前面的内容串起来,可以得到这样的分层结构:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PricingService pricingService;
public OrderService(
OrderRepository orderRepository,
PricingService pricingService) {
this.orderRepository = orderRepository;
this.pricingService = pricingService;
}
}
@Repository
public class OrderRepository {
private final JdbcTemplate jdbcTemplate;
public OrderRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
在这套结构中:
Controller负责协议适配,不承载核心业务判断;Service负责编排业务用例和事务边界;Repository负责数据访问;JdbcTemplate由 Spring Boot 自动配置或显式配置提供;- 构造器注入把组件关系表达得清清楚楚。
这就是 Spring 组件的价值:它不是让代码少写几个 new,而是让系统的依赖关系变得显式、可替换、可测试。
常见误区速记
误区一:加了 @Component 就一定能注入
不一定。类必须位于组件扫描范围内,而且不能存在 Bean 定义冲突。包结构、扫描配置、条件注解都可能影响最终结果。
误区二:@Autowired 越多越专业
不是。字段注入隐藏依赖,也让测试和对象不变性变差。优先使用构造器注入。
误区三:Spring Bean 都是全局单例
默认是 singleton,但作用域可以改变,Web 场景也有 request 和 session 作用域。更重要的是,单例对象里的共享状态可能带来并发问题。
误区四:@Service 会自动让代码变成事务服务
不会。@Service 主要表达层次语义;事务需要 @Transactional、事务管理器、代理机制和正确的数据源配置共同参与。
误区五:启动成功就说明组件配置正确
启动成功只代表容器完成了初始化。真正的组件问题可能出现在运行时:事务未生效、代理丢失、循环依赖、配置环境错误,以及某个 singleton 的共享字段发生竞态。
结尾:别背注解,先看对象关系
主播:
今天我们从 Bean、组件扫描、依赖注入,一路聊到生命周期、Spring Boot 自动配置和 AOP 代理。
如果只记住一句话,那就是:Spring 组件的核心不是注解,而是对象管理和依赖关系。
@Component 解决的是“这个类可以被容器发现”;@Bean 解决的是“这个对象应该如何被显式创建”;构造器注入解决的是“组件需要哪些依赖”;作用域和生命周期解决的是“对象应该活多久”;AOP 代理解决的是“事务、缓存和日志如何在不侵入业务代码的情况下织入”。
当你理解了这些关系,Spring 就不再是一堆凭记忆试出来的注解,而是一套有清晰边界的对象组织系统。
感谢收听本期节目。下一次再看到 @Service、@Repository 或 @Transactional,别只问“这个注解怎么用”,顺手再问一句:这个对象是谁创建的?调用有没有经过代理?依赖关系是否清楚?
这几个问题问明白,Spring 的大半功夫也就被你啃干净了——咳,技术播客差点串台。我们下期见。
节目配套:一句话总结
Spring 组件 = 普通 Java 对象 + 容器管理 + 依赖注入 + 生命周期控制 + 可选的代理增强。
- 点赞
- 收藏
- 关注作者
评论(0)