注解是 Spring Boot 与开发者之间的接口语言:声明组件、绑定请求、注入配置、划定事务边界,全部通过注解表达。本文按功能域整理实际开发中的高频注解,每个注解只保留最简示例与选型决策。启动与自动装配机制见 Spring Boot Startup,Bean 定义与注入方式对比见 Spring Boot Bean 使用规范。

Bean 定义与注入

组件注解均派生自 @Component,差异只在语义分层:

注解语义场景
@Component通用组件不属于下列分层的组件
@Service业务逻辑Service 层
@Repository数据访问持久化层,附加异常转换
@Controller / @RestControllerWeb 入口视图渲染 / REST API
@Configuration配置声明内含 @Bean 方法

第三方类无法添加注解,用 @Bean 声明:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class HttpClientConfig {

@Bean
public RestClient restClient(
RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.build();
}
}

注入注解的选择:

注解匹配方式说明
@Autowired先类型后名称单构造器可省略
@Qualifier显式限定多实现时指定 Bean 名称
@Primary标记首选多实现中的默认候选
@Resource先名称后类型JSR-250 标准

决策:依赖注入一律构造器注入加 final 字段;多实现用 @Qualifier 显式指定,不依赖字段名巧合匹配;@Autowired 与 @Resource 不混用,团队统一一种。三种注入方式的完整对比见 Bean 使用规范一文。

生命周期回调用 @PostConstruct / @PreDestroy,比实现 InitializingBean / DisposableBean 接口侵入性更低:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class CacheWarmer {

@PostConstruct
public void warmup() {
// Bean 初始化完成后执行
}

@PreDestroy
public void cleanup() {
// 容器关闭前释放资源
}
}

Web 层注解

@RestController

@Controller + @ResponseBody 的组合:方法返回值直接序列化为响应体,不走视图解析。REST API 一律用它;需要渲染模板(Thymeleaf 等)才用 @Controller。

请求映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/api/users")
public class UserController {

@GetMapping // GET /api/users
public List<UserDTO> list() { ... }

@GetMapping("/{id}") // GET /api/users/42
public UserDTO get(@PathVariable Long id) { ... }

@PostMapping // POST /api/users
public UserDTO create(...) { ... }

@PutMapping("/{id}") // PUT /api/users/42
public UserDTO update(...) { ... }

@DeleteMapping("/{id}") // DELETE /api/users/42
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) { ... }
}

路径设计约束:资源用名词复数,动作用 HTTP 方法表达;创建返回 201,删除返回 204。@RequestMapping 的 consumers / produces 限定媒体类型;跨域单点控制用 @CrossOrigin,全局则走配置。

参数绑定

注解数据来源典型场景
@PathVariableURL 路径模板变量GET /users/{id}
@RequestParam查询参数 / 表单字段?page=0&size=20
@RequestHeader请求头Authorization
@CookieValueCookie会话标识
@RequestBody请求体JSON 反序列化到 record
@RequestPartmultipart 部分文件上传
@ModelAttribute查询参数绑定到对象列表筛选条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/{id}/orders")
public List<OrderDTO> userOrders(
@PathVariable Long id,
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) String keyword,
@RequestHeader("Authorization") String token) {
...
}

@PostMapping(value = "/{id}/avatar",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void uploadAvatar(
@PathVariable Long id,
@RequestPart("file") MultipartFile file) {
...
}

@RequestBody 每个方法最多一个(请求体流只读一次);参数名与变量名不一致时用 name 属性显式指定。

参数校验

入口校验用 @Valid 触发 Bean Validation,DTO 用 record 承载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public record UserCreateRequest(
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20)
String username,

@NotBlank
@Email(message = "邮箱格式不正确")
String email,

@NotNull
@Min(1)
@Max(150)
Integer age
) {}
1
2
3
4
5
6
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDTO create(
@Valid @RequestBody UserCreateRequest request) {
return userService.create(request);
}

校验失败抛出 MethodArgumentNotValidException,由全局处理器统一转 400,Controller 不做 try-catch。

全局异常处理

@RestControllerAdvice + @ExceptionHandler 把领域异常映射为 HTTP 响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(
ResourceNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}

@ExceptionHandler(
MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalid(
MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(a, b) -> a));
return new ValidationErrorResponse(errors);
}
}

一个应用一个 @RestControllerAdvice;兜底的 Exception 处理器不回传堆栈与内部细节,只记日志。

一个请求经过的注解

一次 REST 请求中,路由、参数绑定、校验、异常映射、事务与 JSON 响应各由哪些 Spring 注解触发;图中标明请求体只能读取一次、校验失败的 400 分支与事务应落在 Service 的边界。

配置注入注解

@Value

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class MailService {

private final String host;
private final int port;

public MailService(
@Value("${mail.host:smtp.example.com}") String host,
@Value("${mail.port:587}") int port) {
this.host = host;
this.port = port;
}
}

支持默认值(: 后缀)与 SpEL(#{} 表达式)。只适合零散标量。

@ConfigurationProperties

相关配置项成组绑定为不可变对象,record 的构造器绑定天然契合:

1
2
3
4
5
6
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
@NotBlank String host,
@Min(1) int port,
Duration connectTimeout
) {}
1
2
3
4
5
app:
mail:
host: smtp.example.com
port: 587
connect-timeout: 5s

注册方式二选一:配置类上 @EnableConfigurationProperties(MailProperties.class),或主类标注 @ConfigurationPropertiesScan。引入 spring-boot-configuration-processor 后 IDE 可补全并校验属性名。

维度@Value@ConfigurationProperties
粒度单个属性一组属性
松散绑定不支持kebab-case 自动映射驼峰
校验不支持类上 @Validated 启用
IDE 元数据无配置处理器生成

决策:一个前缀下超过两个属性就改用 @ConfigurationProperties。

配置从 application.yml 到 Java 对象的两条路径:@Value 直接解析单个表达式,@ConfigurationProperties 则按前缀收集、松散绑定、类型转换和校验;图中给出两者的适用边界与注册方式。

@Profile 与 @PropertySource

@Profile 按 spring.profiles.active 激活对应组件:

1
2
3
@Configuration
@Profile("dev")
public class DevConfig { ... }

@PropertySource 引入额外 properties 文件;实践中分环境更常用 application-{profile}.yml,前者多用于装载第三方配置文件。

数据访问注解

实体映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {

@Id
@GeneratedValue(strategy =
GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String username;

@CreatedDate
private Instant createdAt;

@OneToMany(mappedBy = "user",
fetch = FetchType.LAZY,
orphanRemoval = true)
private List<Order> orders =
new ArrayList<>();
}

三条硬规则:

  • 关联一律 LAZY,EAGER 会在列表查询中放大成 N+1
  • @CreatedDate / @LastModifiedDate 必须配 @EntityListeners 与 @EnableJpaAuditing,缺失时字段静默为 null
  • Entity 用普通类而非 record,JPA 需要无参构造与可变状态

@Repository

标记持久化层,并把底层异常翻译为 DataAccessException 层次。接口继承 JpaRepository 即获得 CRUD 与分页:

1
2
3
4
5
6
7
public interface UserRepository
extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);

boolean existsByEmail(String email);
}

@Transactional

事务边界放在 Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class OrderService {

private final OrderRepository orders;

public OrderService(OrderRepository orders) {
this.orders = orders;
}

@Transactional
public Order create(Order order) {
return orders.save(order);
}

@Transactional(readOnly = true)
public List<Order> findByUser(Long userId) {
return orders.findByUserId(userId);
}
}

readOnly = true 让 Hibernate 跳过脏检查,列表查询明显受益。

四个常见失效或错用场景:

  • 自调用:同类方法内部调用不经过代理,注解失效
  • 非 public 方法上的注解不生效(代理机制限制)
  • 异常被方法内 try-catch 吞掉,事务照常提交
  • 默认只回滚 RuntimeException 与 Error,受检异常需 rollbackFor = Exception.class 显式声明

@Transactional 的代理调用链:从 Controller 经 Spring AOP 代理进入 public Service 方法后才开启事务;对比同类自调用绕过代理、异常被吞掉仍提交,以及 RuntimeException 与受检异常的默认回滚差异。

@EntityGraph 与 @Modifying

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface OrderRepository
extends JpaRepository<Order, Long> {

@EntityGraph(attributePaths = {"user", "items"})
List<Order> findByStatus(OrderStatus status);

@Modifying
@Query("UPDATE Order o SET o.status = :status "
+ "WHERE o.id IN :ids")
int bulkUpdateStatus(
@Param("status") OrderStatus status,
@Param("ids") List<Long> ids);
}

@EntityGraph 一次 JOIN 预取关联,消除 N+1(列表场景优先考虑 DTO 投影);@Modifying 标记 UPDATE / DELETE 的 JPQL,必须在事务内调用,返回受影响行数。

条件装配注解

自动配置的基石,日常开发多用于 starter 与内部组件库:

注解生效条件
@ConditionalOnClass类路径存在指定类
@ConditionalOnMissingClass类路径不存在指定类
@ConditionalOnBean容器中存在指定 Bean
@ConditionalOnMissingBean容器中不存在指定 Bean
@ConditionalOnProperty属性匹配期望值
1
2
3
4
@Configuration
@ConditionalOnProperty(name = "app.cache.enabled",
havingValue = "true")
public class CacheConfig { ... }

条件评估的时机与 @Conditional 元注解机制见 Startup 一文的自动配置章节。用户定义的 Bean 永远优先于自动配置(@ConditionalOnMissingBean),这也是覆盖框架默认组件的标准姿势。

任务执行注解

@Scheduled

1
2
3
4
5
6
7
8
9
10
@Component
public class MaintenanceJob {

@Scheduled(cron = "0 0 2 * * ?") // 每天 02:00
public void nightlyCleanup() { ... }

@Scheduled(fixedDelay = 60,
timeUnit = TimeUnit.SECONDS)
public void healthCheck() { ... }
}

启用需 @EnableScheduling。注意三点:

  • 默认调度线程池只有 1 个线程,一个任务阻塞会顺延全部任务,多任务场景配置 spring.task.scheduling.pool.size
  • cron 表达式 6 位(秒 分 时 日 月 周),与 Linux crontab 的 5 位不同
  • fixedDelay 是上次结束到下次开始,fixedRate 是固定频率,语义不同

@Scheduled 的时间轴对比:fixedDelay 从上一次执行结束开始计时,fixedRate 按计划时刻触发;同时说明默认单线程调度器在任务耗时过长时会造成后续任务顺延。

@Async

1
2
3
4
5
6
7
8
9
10
@Service
public class NotificationService {

@Async
public CompletableFuture<Void> notify(
User user, String event) {
// 发送邮件 / 短信
return CompletableFuture.completedFuture(null);
}
}

启用需 @EnableAsync。两个前提:不能自调用,方法需 public。void 方法的异常进入 AsyncUncaughtExceptionHandler,调用方不可见–需要感知结果或失败时,返回 CompletableFuture。

校验注解速查

Bean Validation(Jakarta Validation)常用约束:

注解语义适用类型
@NotNull不为 null任意
@NotBlank去除空白后非空String
@NotEmpty非 null 且非空String / 集合
@Size(min, max)长度或元素数String / 集合
@Min / @Max数值下限 / 上限数字
@Email邮箱格式String
@Pattern(regexp)正则匹配String
@Past / @Future必须在过去 / 未来时间类型

@Valid 与 @Validated 的分工:@Valid 加在参数上触发对象校验与嵌套级联;@Validated 加在类上启用方法级参数校验,覆盖 @RequestParam、@PathVariable 这类标量参数。

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@Validated
public class SearchController {

@GetMapping("/api/search")
public List<ItemDTO> search(
@RequestParam
@Size(min = 2, message = "关键词过短")
String keyword) {
...
}
}

方法级校验失败抛出 ConstraintViolationException,同样交给全局处理器转 400。

速查总表

分类注解
启动@SpringBootApplication
Bean 定义@Component / @Service / @Repository / @Configuration / @Bean
依赖注入@Autowired / @Qualifier / @Primary / @Resource
Web@RestController / @RequestMapping 族 / @PathVariable / @RequestParam / @RequestBody
校验@Valid / @Validated / Bean Validation 约束
配置@Value / @ConfigurationProperties / @Profile
数据访问@Entity / @Repository / @Transactional / @EntityGraph
条件装配@ConditionalOnClass / @ConditionalOnProperty / @ConditionalOnMissingBean
任务@Scheduled / @Async

注解背后是同一条主线:声明元数据交给框架,框架在运行期通过代理、后置处理器、条件评估补全装配逻辑。理解每个注解触发的机制,比记住语法更能避免"注解不生效"这类问题。