Spring 常用注解:日常使用与底层机制
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 |
|
一个容易忽略的点:@Configuration 类本身也是一个 Bean,Spring 会为它创建 CGLIB 代理,确保 @Bean 方法不论被调用多少次,返回的是同一个单例对象。如果你在里面写 new 去调用另一个 @Bean 方法,代理会拦截并返回容器中的已有实例,而不是真的 new 一次。
@ComponentScan
指定组件扫描的包路径。SpringBoot 的 @SpringBootApplication 已经包含了此注解,默认扫描启动类所在包及其子包。只有在你的 Mapper 或其他组件放在启动类包路径之外时,才需要手动加。
二、依赖注入:让对象之间的关系自动装配
@Autowired
按类型注入。这是日常开发中使用频率最高的注入方式。Spring 在容器启动时,通过反射找到字段的类型,去容器里匹配对应的 Bean,然后赋值。
遇到多个同类型 Bean 时,@Autowired 会按字段名再做一次匹配。如果还是找不到唯一的,启动时直接报错。
@Qualifier
配合 @Autowired 使用,按 Bean 名称精确指定注入哪一个。当一个接口有多个实现类时——比如 PaymentService 有 AlipayServiceImpl 和 WechatPayServiceImpl——只用 @Autowired 会报错,加上 @Qualifier("alipayServiceImpl") 就明确指定了用哪个。
1 |
|
@Resource
这是 JDK 自带的注解(JSR-250),不是 Spring 的。默认按名称匹配,找不到名称再按类型。和 @Autowired 的主要区别是匹配策略的优先级不同,日常使用选一个就行,不用太纠结。
我在项目里通常只用 @Autowired,保持一致性。如果你的团队习惯用 @Resource,那就统一用它——技术上没有明显的优劣之分。
@Value
从配置文件注入单个属性值。支持默认值语法:
1 |
|
一个注意事项:@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-size、maxSize、MAX_SIZE 都能匹配)。
1 |
|
注解的底层:两块核心机制
抛开所有细节,Spring 注解体系的底层只有两件事:
反射:启动时扫描 class,读到注解信息,依此创建对象、注入字段、执行方法。@Autowired 注入字段靠的是反射直接给 private 字段赋值(Field.setAccessible(true)),不是通过 setter。
动态代理:在 Bean 初始化时包裹一层代理。所有”在执行前后插入逻辑”的特性——事务、切面、异步——都是 JDK 动态代理或 CGLIB 在起作用。理解代理的存在是理解事务失效、切面不触发等问题的基础。
这两条线索串起来,整个 Spring 的行为就清晰了。
