Spring 常用注解:你真正需要理解的那些

Spring 的注解体系很庞大,但日常开发中高频使用的其实就二十来个。这篇文章从实际项目出发,按使用频率和重要性排列,聊清楚每个注解它实际干了什么、背后的机制是什么、以及哪些地方容易踩坑。

一、Bean 管理:让 Spring 替你创建对象

这组注解是 Spring 容器的基础设施。没有它们,依赖注入和 AOP 都无从谈起。

@Component / @Repository / @Service / @Controller / @RestController

这五个注解在 Spring 容器层面的行为是一样的——标记一个类,让 Spring 通过组件扫描发现它,创建单例实例,放进容器管理。

它们的区别纯粹是语义分层,外加 @Repository 有一个额外功能:自动将数据库异常翻译为 Spring 的 DataAccessException

@RestController@Controller + @ResponseBody 的组合。在前后端分离的项目里,你几乎只会用 @RestController,因为所有接口都返回 JSON,不需要 @Controller 的视图跳转。

@Configuration + @Bean

@Configuration 标记配置类,@Bean 作用于方法,将方法返回值注册为 Bean。这个组合的使用场景很明确:需要放进容器但你又不能直接加 @Component 的类。

最典型的例子是 DataSource——它是第三方库的类,源码不在你项目里,你没法进去加注解。所以你在配置类里用 @Bean 方法把它创建出来交给 Spring:

1
2
3
4
5
6
7
8
9
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/test");
return ds;
}
}

一个容易忽略的点:@Configuration 类本身也是一个 Bean,Spring 会为它创建 CGLIB 代理,确保 @Bean 方法不论被调用多少次,返回的是同一个单例对象。如果你在里面写 new 去调用另一个 @Bean 方法,代理会拦截并返回容器中的已有实例,而不是真的 new 一次。

@ComponentScan

指定组件扫描的包路径。SpringBoot 的 @SpringBootApplication 已经包含了此注解,默认扫描启动类所在包及其子包。只有在你的 Mapper 或其他组件放在启动类包路径之外时,才需要手动加。

二、依赖注入:让对象之间的关系自动装配

@Autowired

按类型注入。这是日常开发中使用频率最高的注入方式。Spring 在容器启动时,通过反射找到字段的类型,去容器里匹配对应的 Bean,然后赋值。

遇到多个同类型 Bean 时,@Autowired 会按字段名再做一次匹配。如果还是找不到唯一的,启动时直接报错。

@Qualifier

配合 @Autowired 使用,按 Bean 名称精确指定注入哪一个。当一个接口有多个实现类时——比如 PaymentServiceAlipayServiceImplWechatPayServiceImpl——只用 @Autowired 会报错,加上 @Qualifier("alipayServiceImpl") 就明确指定了用哪个。

1
2
3
@Autowired
@Qualifier("alipayServiceImpl")
private PaymentService paymentService;

@Resource

这是 JDK 自带的注解(JSR-250),不是 Spring 的。默认按名称匹配,找不到名称再按类型。和 @Autowired 的主要区别是匹配策略的优先级不同,日常使用选一个就行,不用太纠结。

我在项目里通常只用 @Autowired,保持一致性。如果你的团队习惯用 @Resource,那就统一用它——技术上没有明显的优劣之分。

@Value

从配置文件注入单个属性值。支持默认值语法:

1
2
3
4
5
@Value("${server.port}")
private Integer port;

@Value("${app.name:默认名称}") // 读不到配置时用默认值
private String appName;

一个注意事项:@Value 注入的时机在 Bean 构造之后,所以构造函数里是拿不到 @Value 注入的值的。如果需要在初始化阶段使用配置值,用 @ConfigurationProperties 更合适(配合 setter 或构造器绑定)。

三、Web 请求处理:把 HTTP 请求映射到 Java 方法

@RequestMapping 及其派生注解

@GetMapping@PostMapping@PutMapping@DeleteMapping@RequestMapping 的快捷写法,分别限制了 HTTP 方法。这个没什么好说的,用就是了。

实际开发中的两个习惯:

  • 类上加 @RequestMapping("/api/xxx") 统一前缀,方法上只写具体路径
  • RESTful 接口用 @GetMapping("/{id}") 而不是 @GetMapping("?id=xxx"),前者路径更干净

@PathVariable vs @RequestParam vs @RequestBody

三者的本质区别不在于语法,而在于数据从哪里来:

  • @PathVariable:从 URL 路径中取值,适合资源标识(哪个用户、哪个订单)
  • @RequestParam:从查询字符串中取值,适合筛选条件和分页参数
  • @RequestBody:从请求体中取值,把 JSON 自动反序列化为 Java 对象。适合复杂参数

@RequestBody 的底层依赖 HttpMessageConverter,默认用 Jackson 做 JSON 解析。如果你自定义了消息转换器(比如改了日期格式),这里的行为会跟着变。

四、事务管理:AOP 的典型应用

@Transactional

@Transactional 的实现完全基于 AOP 代理。Spring 为加了此注解的 Bean 创建代理对象,在方法执行前开启事务,方法正常返回后提交,抛出未捕获异常时回滚。

几个实际中容易出问题的点:

  • 同类方法调用不走代理。在同一个类里,方法 A 直接调用方法 B(B 有 @Transactional),事务不会生效。因为调用走的是 this.xxx() 而不是代理对象。需要事务的方法应该放在单独的类里,或者注入自己(但通常暗示设计有问题)。
  • 默认只回滚 RuntimeException。checked exception 不会触发回滚,除非你显式指定 @Transactional(rollbackFor = Exception.class)。这是一个经典的坑。
  • 事务的传播行为。默认 REQUIRED——当前有事务就加入,没有就新建。大多数场景的默认行为够用,除非你在做批量处理、异步消息等特殊场景,否则不需要改。

五、Spring Boot 特有注解

@SpringBootApplication

这是 Spring Boot 项目的入口注解,等价于同时标注了三个注解:

  • @Configuration:声明为配置类
  • @EnableAutoConfiguration:启用自动配置(这是 Spring Boot 最核心的机制)
  • @ComponentScan:启用组件扫描

自动配置的原理是:Spring Boot 根据 classpath 里的 jar 包和你的配置,通过 spring.factories 文件加载对应的自动配置类。比如 classpath 里有 MySQL 驱动但没有配置 DataSource Bean,Spring Boot 会自动给你配一个。这个机制的价值在于——省掉了大量模板配置,但代价是,当你需要自定义行为时,要先搞清楚”自动配置给你做了什么”,否则容易被默认行为绊倒。

@ConfigurationProperties

批量注入配置文件中某个前缀下的所有属性。和 @Value 的区别一个是批量 vs 单个,另一个是它支持宽松绑定(max-sizemaxSizeMAX_SIZE 都能匹配)。

1
2
3
4
5
6
7
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
private String endpoint;
private String accessKey;
// getter/setter
}

注解的底层:两块核心机制

抛开所有细节,Spring 注解体系的底层只有两件事:

反射:启动时扫描 class,读到注解信息,依此创建对象、注入字段、执行方法。@Autowired 注入字段靠的是反射直接给 private 字段赋值(Field.setAccessible(true)),不是通过 setter。

动态代理:在 Bean 初始化时包裹一层代理。所有”在执行前后插入逻辑”的特性——事务、切面、异步——都是 JDK 动态代理或 CGLIB 在起作用。理解代理的存在是理解事务失效、切面不触发等问题的基础。

这两条线索串起来,整个 Spring 的行为就清晰了。