视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在上一篇《Spring Boot 注解大合集:从入门到精通》中,我们已经掌握了@SpringBootApplication、@Service、@RestController等核心注解。但实际开发中,你还会遇到缓存、异步、参数校验、条件装配等复杂场景。
本文继续深入,带你掌握Spring Boot 高频进阶注解,结合真实业务案例 + 反例 + 注意事项,让你写出更健壮、更高效的代码!
一、为什么需要进阶注解?
🌰 需求场景
你正在开发一个电商系统:
- 商品详情页访问量极大 → 需要缓存减少数据库压力
- 用户下单后要发短信、发邮件 → 这些操作不能阻塞主流程,需异步执行
- 提交订单时要校验手机号、地址格式 → 需要参数校验
- 测试环境不想连 Redis → 希望按环境决定是否启用某个 Bean
这些需求,光靠基础注解无法优雅解决。这时候,就需要我们的“进阶注解天团”登场了!
二、高频进阶注解详解(附完整代码)
1.@Cacheable/@CacheEvict—— 缓存神器
✅ 场景:缓存商品信息,避免重复查库
@Service public class ProductService { @Cacheable(value = "product", key = "#id") public Product getProductById(Long id) { System.out.println("查询数据库..."); // 仅首次打印 return productDao.findById(id); } @CacheEvict(value = "product", key = "#id") public void updateProduct(Long id, Product product) { productDao.update(id, product); // 自动清除缓存,下次查询会重新加载 } }🔧 启用缓存(启动类或配置类):
@SpringBootApplication @EnableCaching // 必须加!否则缓存无效 public class ECommerceApplication { public static void main(String[] args) { SpringApplication.run(ECommerceApplication.class, args); } }❌ 反例(缓存穿透):
@Cacheable("product") public Product getProductById(Long id) { Product p = dao.findById(id); if (p == null) { return null; // 空值不缓存 → 每次都查库! } return p; }✅ 正确做法(缓存空值防穿透):
@Cacheable(value = "product", key = "#id", unless = "#result == null") public Product getProductById(Long id) { Product p = dao.findById(id); return p != null ? p : new NullProduct(); // 自定义空对象 }⚠️ 注意事项:
- 默认使用
SimpleCacheManager(内存缓存),生产环境应集成Redis(引入spring-boot-starter-data-redis) unless表示“除非满足条件才缓存”,常用于过滤 null- 缓存 key 支持 SpEL 表达式,如
#user.id
2.@Async—— 异步任务轻松实现
✅ 场景:用户注册后异步发送欢迎邮件
@Service public class UserService { @Autowired private EmailService emailService; public void register(User user) { userDao.save(user); sendWelcomeEmailAsync(user); // 不阻塞主线程 } @Async // 标记为异步方法 public void sendWelcomeEmailAsync(User user) { emailService.send(user.getEmail(), "欢迎注册!"); } }🔧 启用异步(必须!):
@SpringBootApplication @EnableAsync // 开启异步支持 public class ECommerceApplication { ... }❌ 反例(同类调用失效):
@Service public class OrderService { public void placeOrder(Order order) { saveOrder(order); this.sendNotification(order); // ❌ 通过 this 调用,@Async 无效! } @Async public void sendNotification(Order order) { ... } }✅ 正确做法(注入自身或使用 AOP 代理):
@Service public class OrderService { @Autowired private OrderService self; // 自注入(不推荐但可行) public void placeOrder(Order order) { saveOrder(order); self.sendNotification(order); // ✅ 通过代理调用 } @Async public void sendNotification(Order order) { ... } }更优雅的方式:拆分为独立的
NotificationService
⚠️ 注意事项:
- 异步方法返回值只能是 void 或 Future/CompletableFuture
- 默认使用
SimpleAsyncTaskExecutor(每次新建线程),建议自定义线程池:
@Configuration @EnableAsync public class AsyncConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } }3.@Valid/@Validated+ JSR-303 注解 —— 参数校验三板斧
✅ 场景:校验用户注册信息
public class UserRegisterDTO { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度3-20") private String username; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") private String phone; // getter/setter }@RestController public class UserController { @PostMapping("/register") public Result register(@Valid @RequestBody UserRegisterDTO dto) { // 如果校验失败,会抛出 MethodArgumentNotValidException userService.register(dto); return Result.success(); } }🔧 全局异常处理(推荐):
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidation(MethodArgumentNotValidException ex) { String msg = ex.getBindingResult() .getFieldError() .getDefaultMessage(); return Result.error(msg); } }⚠️ 注意事项:
@Valid用于普通校验,@Validated支持分组校验(如注册 vs 修改)- 必须配合
@RequestBody使用(JSON 请求体) - 基本类型(如
Long id)不能直接加@NotNull,需用包装类或@RequestParam+required = false
4.@ConditionalOn...系列 —— 条件化装配 Bean
✅ 场景:只有当配置了 Redis 地址时,才初始化 Redis 工具类
@Component @ConditionalOnProperty(name = "redis.enabled", havingValue = "true") public class RedisUtil { // ... }# application.yml redis: enabled: true host: localhost常用条件注解:
| 注解 | 作用 |
|---|---|
@ConditionalOnClass | 类路径存在某类时生效(如RedisTemplate) |
@ConditionalOnMissingBean | 容器中没有该 Bean 时才创建(避免覆盖) |
@ConditionalOnProperty | 配置文件中某属性满足条件 |
@ConditionalOnExpression | 支持 SpEL 表达式,如"${env} == 'prod'" |
❌ 反例(误用导致 Bean 未加载):
// 如果 redis.enabled=false,RedisUtil 不会被创建 // 但其他地方@Autowired RedisUtil → 启动报错!✅ 正确做法(提供 fallback):
@Bean @ConditionalOnMissingBean(RedisUtil.class) public RedisUtil defaultRedisUtil() { return new NoOpRedisUtil(); // 空实现 }5.@Scheduled—— 定时任务
✅ 场景:每天凌晨清理过期订单
@Component public class OrderCleanupTask { @Scheduled(cron = "0 0 2 * * ?") // 每天2点执行 public void cleanExpiredOrders() { orderService.deleteExpired(); } }🔧 启用定时任务:
@SpringBootApplication @EnableScheduling public class ECommerceApplication { ... }⚠️ 注意事项:
- 默认单线程执行,多个任务会排队 → 建议配置线程池
- 时间表达式用6位 cron(秒 分 时 日 月 周),注意和 Linux 的5位区别
- 任务方法不能有参数,返回值必须为 void
三、终极避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
@Cacheable不生效 | 忘记加@EnableCaching | 在启动类加注解 |
@Async不异步 | 同类方法调用 or 未加@EnableAsync | 自注入 or 拆 Service |
| 参数校验不触发 | 忘记加@Valid或没用@RequestBody | 检查注解位置 |
| 条件 Bean 未加载 | 条件不满足 or 依赖类缺失 | 打日志确认条件 |
| 定时任务阻塞 | 多个任务共用单线程 | 自定义TaskScheduler线程池 |
四、总结
Spring Boot 的注解体系就像一套“乐高积木”:
- 基础注解(
@Service,@RestController)搭骨架 - 进阶注解(
@Cacheable,@Async,@Valid)添功能 - 条件注解(
@ConditionalOn...)做适配
记住:注解不是越多越好,而是“恰到好处”。理解每个注解背后的原理(AOP、代理、反射),才能真正驾驭 Spring Boot!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!