视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在前两篇中,我们系统学习了Spring Boot 的基础注解和进阶注解。但很多小伙伴反馈:“我知道每个注解怎么用,但一到真实项目就手忙脚乱,不知道怎么组合、怎么优化。”
本文将带你走进真实业务场景,通过一个完整的“用户积分系统”案例,展示如何灵活组合多个注解,并附上性能调优技巧、常见陷阱和最佳实践,让你从“会用”进阶到“用好”!
一、需求背景:用户积分系统
🎯 核心功能
- 用户每日签到可获得积分(需防重复签到)
- 积分变动记录需持久化,并支持查询
- 高并发下保证数据一致性(不能多发/少发积分)
- 签到成功后异步发送通知(短信 + 消息中心)
- 支持开发、测试、生产多环境配置
- 接口需校验参数合法性,失败时友好提示
这是一个典型的高并发 + 数据一致性 + 异步解耦 + 多环境适配场景,非常适合展示注解的综合运用。
二、核心代码实现(注解组合拳)
1. 实体类 + 参数校验
public class SignInRequest { @NotNull(message = "用户ID不能为空") private Long userId; @NotBlank(message = "设备ID不能为空") @Pattern(regexp = "[a-zA-Z0-9]{10,32}", message = "设备ID格式不合法") private String deviceId; // getter/setter }✅ 使用
@NotNull+@Pattern确保输入安全,避免脏数据入库。
2. Controller 层:接收请求 + 校验
@RestController @RequestMapping("/api/v1/sign-in") @Validated // 启用方法级别校验(配合 @Valid) public class SignInController { private final SignInService signInService; public SignInController(SignInService signInService) { this.signInService = signInService; } @PostMapping public Result<String> signIn(@Valid @RequestBody SignInRequest request) { signInService.processSignIn(request); return Result.success("签到成功!"); } }⚠️ 注意:
@Validated加在类上,才能支持方法参数校验(如@Min、
3. Service 层:核心逻辑 + 事务 + 缓存 + 异步
@Service public class SignInService { @Autowired private PointRepository pointRepo; @Autowired private NotificationService notificationService; // 防重缓存:key = "sign_in:{userId}:{date}" @Cacheable( value = "dailySignIn", key = "'sign_in:' + #request.userId + ':' + T(java.time.LocalDate).now()", unless = "#result != null" // 只有未签到才继续 ) public Boolean isAlreadySignedIn(SignInRequest request) { return pointRepo.existsByUserIdAndDate(request.getUserId(), LocalDate.now()); } @Transactional(rollbackFor = Exception.class) public void processSignIn(SignInRequest request) { // 1. 检查是否已签到(利用缓存) if (Boolean.TRUE.equals(isAlreadySignedIn(request))) { throw new BusinessException("今日已签到"); } // 2. 增加积分(模拟) pointRepo.addPoints(request.getUserId(), 10); // 3. 记录日志 pointRepo.logSignIn(request.getUserId(), request.getDeviceId()); // 4. 异步发通知(不阻塞主流程) notificationService.sendSignInNotificationAsync(request.getUserId()); } }🔥 关键点:
@Cacheable防止重复签到(缓存穿透问题已在上一篇解决)@Transactional保证“加积分 + 记日志”原子性- 调用异步方法
sendSignInNotificationAsync解耦
4. 异步通知服务
@Service @Slf4j public class NotificationService { @Async("taskExecutor") // 指定线程池 public void sendSignInNotificationAsync(Long userId) { try { // 模拟发短信 smsClient.send(userId, "签到成功,获得10积分!"); // 模拟写消息中心 messageCenter.push(userId, "积分+10"); } catch (Exception e) { log.error("通知发送失败, userId={}", userId, e); // 可加入重试机制或死信队列 } } }✅ 自定义线程池避免阻塞主线程,异常捕获防止任务中断影响其他流程。
5. 多环境配置:按需启用组件
// 仅在 prod 环境启用短信服务 @Component @Profile("prod") public class SmsClient { public void send(Long userId, String msg) { // 调用真实短信网关 } } // dev/test 环境使用 Mock @Component @Profile({"dev", "test"}) public class MockSmsClient extends SmsClient { @Override public void send(Long userId, String msg) { System.out.println("[MOCK] 发送短信: " + msg); } }✅ 利用
@Profile实现环境隔离,避免测试时误发真实短信!
6. 全局异常处理 + 统一返回
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<?> handleBusiness(BusinessException e) { return Result.error(e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result<?> handleValidation(MethodArgumentNotValidException e) { String msg = e.getBindingResult() .getFieldError() .getDefaultMessage(); return Result.error("参数错误: " + msg); } @ExceptionHandler(Exception.class) public Result<?> handleSystem(Exception e) { log.error("系统异常", e); return Result.error("系统繁忙,请稍后再试"); } }✅ 所有异常统一拦截,前端无需处理各种 HTTP 状态码。
三、性能与安全优化技巧
✅ 1. 缓存 + 本地锁 防并发重复签到
虽然@Cacheable能减少 DB 查询,但在高并发下仍可能缓存击穿(多个请求同时穿透缓存)。
解决方案:加分布式锁(Redis)或本地锁(Guava)
private final LoadingCache<String, Boolean> localLock = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .build(); public void processSignIn(SignInRequest request) { String lockKey = "lock:sign_in:" + request.getUserId(); Boolean locked = localLock.get(lockKey, k -> true); if (locked == null) { throw new BusinessException("操作太频繁"); } try { // 执行签到逻辑... } finally { localLock.invalidate(lockKey); // 释放锁 } }💡 小流量可用本地锁,大流量建议 Redis 分布式锁(
Redisson)。
✅ 2. 异步任务监控
异步任务失败容易被忽略!建议:
- 记录任务执行日志
- 关键任务加入重试机制(
@Retryable) - 使用
CompletableFuture获取结果
@Async public CompletableFuture<Boolean> sendNotificationAsync(Long userId) { try { // ... return CompletableFuture.completedFuture(true); } catch (Exception e) { return CompletableFuture.failedFuture(e); } }✅ 3. 条件装配避免启动失败
如果某个 Bean 依赖外部服务(如 Redis),但测试环境没装,会导致启动失败。
正确做法:
@Bean @ConditionalOnBean(RedisTemplate.class) public RedisLock redisLock(RedisTemplate template) { return new RedisLock(template); }或提供 fallback:
@Bean @ConditionalOnMissingBean(RedisLock.class) public RedisLock dummyLock() { return new NoOpLock(); }四、反例 vs 正例对比表
| 场景 | 反例 | 正例 |
|---|---|---|
| 事务管理 | 在 private 方法加@Transactional | 用 public 方法 + AOP 代理 |
| 异步调用 | 直接this.asyncMethod() | 注入自身或拆 Service |
| 缓存空值 | 不缓存 null → 缓存穿透 | 缓存特殊空对象或占位符 |
| 参数校验 | 手动 if 判断 | @Valid+ 全局异常处理 |
| 多环境配置 | 写死开关 | @Profile+ 配置文件 |
五、结语
Spring Boot 注解的强大之处,在于组合使用。
- 用
@Cacheable提升性能 - 用
@Transactional保证数据一致 - 用
@Async解耦非核心流程 - 用
@Valid守住入口安全 - 用
@Profile适配多环境
真正的高手,不是记住所有注解,而是知道在什么场景用什么注解,以及它们如何协同工作。
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!