← 返回场景决策

🍃 Spring Boot 深度场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 8 大核心场景全解析

AOP 事件机制 Profile 条件装配 自定义Starter 拦截器vs过滤器 异常体系 配置管理

1. AOP — 切面编程决策

AOP = 交叉路口装摄像头 不用每辆车里装,路口一个摄像头全拍到
不用AOP = 每辆车里装记录仪 每个方法里重复写日志/权限/事务代码
什么时候用 AOP
  • 日志记录:接口调用日志、操作审计 — 不侵入业务代码
  • 权限校验:@RequiresPermission — 统一权限拦截
  • 性能监控:方法耗时统计 — @Timed 注解
  • 事务管理:@Transactional 底层就是 AOP — Spring 的事务就是AOP实现
  • 缓存抽象:@Cacheable — AOP 拦截方法调用,有缓存直接返回
什么时候不用 AOP
  • 核心业务逻辑:订单创建、支付流程 — AOP 让代码流向不清晰
  • 高频方法:每秒调用10万次 — AOP 代理有性能开销
  • 内部调用:同一个 Bean 内方法互调 — AOP 代理不生效(经典坑)
  • 简单逻辑:就一个 if-else — 不需要切面

📊 AOP 决策表

场景用AOP?原因
日志/审计横切关注点,与业务无关
事务管理Spring 已经帮你做了
权限校验统一拦截,避免遗漏
缓存注解声明式缓存,简洁优雅
核心业务流程代码不透明,调试困难
同类内部调用代理不生效,白写

💻 AOP 日志切面实现

📁 shop-service
📂 src/main/java
📂 com.shop.common.aspect
📄 ApiLogAspect.java
@Aspect @Component @Slf4j public class ApiLogAspect { // 定义切点:所有 Controller 方法 @Pointcut("execution(* com.example.controller..*.*(..))") public void controllerPointcut() {} @Around("controllerPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); String method = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); log.info("→ {} args={}", method, Arrays.toString(args)); try { Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; log.info("← {} cost={}ms result={}", method, cost, result); return result; } catch (Throwable e) { log.error("✕ {} error={}", method, e.getMessage()); throw e; } } }
面试金句 @Around 环绕通知是 AOP 最强大的通知类型,通过 ProceedingJoinPoint.proceed() 控制目标方法执行时机,实现 before+after+exception 三合一。execution(* com.example.controller..*.*(..)) 切点表达式匹配 controller 包下所有类的所有方法——⚠️ Spring AOP 基于 JDK 动态代理/CGLIB,只对 Spring 管理的 Bean 的外部调用生效,同类内部调用(this.methodB())绕过代理,@Transactional 等注解失效。
⚠️ AOP 内部调用不生效的坑:Spring AOP 基于代理,同一个 Bean 中 methodA() 调用 methodB(),methodB 上的 @Transactional/@Cacheable 等 AOP 注解不生效。解决:注入自身 (self-injection) 或抽取到另一个 Bean。

2. 事件机制 — 解耦决策

事件 = 寄快递 你只管寄,快递公司怎么送、谁签收你不管
直接调用 = 亲自送 每个收件人你都得跑一趟,累且耦合
什么时候用事件
  • 注册后发短信/邮件:注册成功是主流程,发短信是旁路 — 不用等短信发完才返回
  • 下单后扣库存/发通知:创建订单后触发库存扣减、积分发放 — 解耦多步操作
  • 数据变更通知:配置修改后通知缓存刷新 — 发布/订阅模式
  • 审计日志:关键操作后记录审计 — 业务代码不关心日志细节
什么时候不用事件
  • 强一致性要求:扣款必须成功才能下单 — 事件是最终一致性
  • 需要返回结果:需要知道下游处理成功/失败 — 事件是 fire-and-forget
  • 简单两步操作:就调一个方法 — 过度设计
  • 必须顺序执行:步骤之间有强依赖 — 事件执行顺序不可控

💻 Spring 事件完整示例

📁 order-service
📂 src/main/java
📂 com.shop.order.event
📄 OrderCreatedEvent.java
📂 com.shop.order.service
📄 OrderService.java
📂 com.shop.order.listener
📄 OrderEventListener.java
// 1. 定义事件 public class OrderCreatedEvent extends ApplicationEvent { private final OrderDTO order; public OrderCreatedEvent(Object source, OrderDTO order) { super(source); this.order = order; } public OrderDTO getOrder() { return order; } } // 2. 发布事件 @Service @RequiredArgsConstructor public class OrderService { private final ApplicationEventPublisher publisher; @Transactional public OrderDTO createOrder(CreateOrderReq req) { OrderDTO order = // ... 创建订单 // 发布事件,其他模块监听处理 publisher.publishEvent(new OrderCreatedEvent(this, order)); return order; } } // 3. 监听事件 @Component @Slf4j public class OrderEventListener { @EventListener public void onOrderCreated(OrderCreatedEvent event) { log.info("订单创建: {}", event.getOrder().getId()); // 发短信、扣库存、加积分... } // 异步处理 — 不阻塞主流程 @Async @EventListener public void sendSms(OrderCreatedEvent event) { smsService.send(...); } }
面试金句 ApplicationEventPublisher 是 Spring 内建的进程内发布-订阅机制,事件默认同步发布(publishEvent 阻塞到所有监听器执行完毕)。加 @Async 后切换到异步线程池执行,不阻塞主流程。⚠️ @TransactionalEventListener(phase=AFTER_COMMIT) 可以确保事务提交后才触发事件——避免事务回滚后已发通知的不一致问题。

3. Profile — 环境隔离决策

Profile = 换衣服 上班穿正装,周末穿休闲,同一人不同场合不同打扮
Profile 典型用法
  • 数据源切换:dev 用 H2,prod 用 MySQL — @Profile("dev")/@Profile("prod")
  • 配置隔离:application-dev.yml / application-prod.yml — 各环境独立配置
  • Bean 条件注入:Swagger 只在 dev 环境开启 — @Profile("!prod")
  • 第三方服务:dev 用测试账号,prod 用生产账号

💻 Profile 配置实战

📁 shop-service
📂 src/main/resources
📄 application.yml
📄 application-dev.yml
📄 application-prod.yml
📂 src/main/java
📄 SwaggerConfig.java
# application.yml — 公共配置 spring: profiles: active: @profile.active@ // Maven 动态替换 # application-dev.yml spring: datasource: url: jdbc:h2:mem:testdb redis: host: localhost # application-prod.yml spring: datasource: url: jdbc:mysql://prod-db:3306/app redis: host: redis-cluster // Swagger 只在非生产环境开启 @Configuration @Profile("!prod") public class SwaggerConfig { @Bean public OpenAPI openAPI() { ... } } // 激活方式 // 1. 启动参数: --spring.profiles.active=prod // 2. 环境变量: SPRING_PROFILES_ACTIVE=prod // 3. JVM参数: -Dspring.profiles.active=prod
面试金句 Profile 实现了 Spring 的环境隔离机制:spring.profiles.active 决定加载哪个 application-{profile}.yld,@Profile 注解控制 Bean 的条件注册。优先级:命令行参数 > 环境变量 > application-{profile}.yml > application.yml。@Profile("!prod") 取反表达式让 Swagger 仅在非生产环境注册,防止 API 文档泄露到公网。

4. 条件装配 — Bean 动态注册决策

📊 条件注解决策表

注解条件典型场景
@ConditionalOnProperty配置项值功能开关:feature.xxx.enabled=true
@ConditionalOnClass类路径存在有 Redis 依赖才配置 RedisTemplate
@ConditionalOnMissingBean容器中无该 Bean自动配置默认值,用户自定义覆盖
@ConditionalOnBean容器中有该 Bean有 DataSource 才配置 JdbcTemplate
@ConditionalOnExpressionSpEL 表达式复杂条件组合

💻 条件装配实战

📁 shop-service
📂 src/main/java
📂 com.shop.config
📄 SmsConfig.java
📂 com.shop.service
📄 AliyunSmsService.java
📄 NoopSmsService.java
// 功能开关:短信服务可选 @Configuration public class SmsConfig { @Bean @ConditionalOnProperty( prefix = "app.sms", name = "enabled", havingValue = "true", matchIfMissing = false // 默认不开 ) public SmsService smsService() { return new AliyunSmsService(); } @Bean @ConditionalOnMissingBean(SmsService.class) public SmsService noopSmsService() { return new NoopSmsService(); // 不发短信的空实现 } } // application.yml // app: // sms: // enabled: true # 开发环境开启,测试环境关闭
面试金句 @ConditionalOnMissingBean 是 Spring Boot 自动配置的基石:它实现约定优于配置——如果没有用户自定义的 SmsService Bean,Spring Boot 就用 NoopSmsService 作为默认值;如果用户自定义了一个 SmsService,默认值自动让位。这就是为什么覆盖 Spring Boot 默认组件只需注册一个同类型 Bean。

5. 自定义 Starter — 通用模块封装决策

Starter = 即插即用的电器 插上插座就能用,不用关心内部电路
什么时候做自定义 Starter
  • 多项目共用:公司内3+项目都用同一套认证/日志/SMS — 抽成 Starter
  • 通用基础设施:统一异常处理、统一序列化、统一监控 — 每个项目都该有
  • 中间件封装:内部 Redis/Kafka/ES 的标准化用法 — 一行依赖搞定
什么时候不做
  • 单项目用:只有一个项目用 — 放项目里就行
  • 业务耦合:包含业务逻辑 — Starter 应该是纯技术组件
  • 过度抽象:什么都要可配置 — 配置项比代码还多

💻 自定义 Starter 结构

📁 my-sms-spring-boot-starter
📂 src/main/java
📂 com.example.sms
📄 SmsAutoConfiguration.java
📄 SmsProperties.java
📄 SmsService.java
📂 src/main/resources/META-INF
📄 spring.factories
// 项目结构 // my-sms-spring-boot-starter/ // ├── pom.xml // └── src/main/ // ├── java/com/example/sms/ // │ ├── SmsAutoConfiguration.java ← 自动配置类 // │ ├── SmsProperties.java ← 配置属性 // │ └── SmsService.java ← 核心服务 // └── resources/META-INF/ // └── spring.factories ← 自动配置注册 // SmsProperties — 配置映射 @ConfigurationProperties(prefix = "my.sms") @Data public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; private boolean enabled = true; } // SmsAutoConfiguration @AutoConfiguration @EnableConfigurationProperties(SmsProperties.class) @ConditionalOnProperty(prefix = "my.sms", name = "enabled", havingValue = "true") public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean public SmsService smsService(SmsProperties props) { return new AliyunSmsService( props.getAccessKeyId(), props.getAccessKeySecret(), props.getSignName() ); } } // spring.factories (Spring Boot 2.x) // org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ // com.example.sms.SmsAutoConfiguration // Spring Boot 3.x 用新文件: // META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports // com.example.sms.SmsAutoConfiguration
面试金句 自定义 Starter 的核心是 Auto-Configuration SPI 机制:Spring Boot 启动时扫描 META-INF/spring.factories(2.x)或 AutoConfiguration.imports(3.x),加载所有 @AutoConfiguration 类。每个配置类通过 @Conditional* 注解实现按需装配——只有满足条件(类存在、配置项匹配、Bean 缺失)才创建 Bean,这就是 Spring Boot "开箱即用"的底层原理。

6. 拦截器 vs 过滤器 — 请求处理决策

Filter = 大门保安 Servlet 级别,所有请求都过,不认识 Spring
Interceptor = 楼层前台 Spring MVC 级别,能拿到 Controller 信息

📊 拦截器 vs 过滤器决策表

维度Filter(过滤器)Interceptor(拦截器)
规范Servlet 规范Spring MVC 规范
作用范围所有请求(含静态资源)只拦截 Controller 请求
获取Bean麻烦直接注入
获取Handler信息✅ HandlerMethod
执行时机DispatcherServlet 之前Controller 之前/之后
典型场景编码、CORS、压缩认证、权限、日志

💻 认证拦截器实现

📁 shop-service
📂 src/main/java
📂 com.shop.common.interceptor
📄 AuthInterceptor.java
📂 com.shop.config
📄 WebMvcConfig.java
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 只拦截 Controller 方法 if (!(handler instanceof HandlerMethod)) { return true; } // 检查是否跳过认证 HandlerMethod hm = (HandlerMethod) handler; if (hm.hasMethodAnnotation(SkipAuth.class)) { return true; } // Token 验证 String token = request.getHeader("Authorization"); if (token == null || !tokenService.validate(token)) { response.setStatus(401); return false; } // 存入用户信息,Controller 可以直接用 request.setAttribute("userId", tokenService.getUserId(token)); return true; } } // 注册拦截器 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); } }
面试金句 HandlerInterceptor 运行在 DispatcherServlet 内部,能拿到 HandlerMethod(目标 Controller 方法的元信息),因此可以做基于注解的细粒度权限控制(如 @SkipAuth 跳过、@RequiresPermission 校验)。与 Filter 相比,Interceptor 可以直接注入 Spring Bean,且只拦截进入 Controller 的请求(静态资源等不经过)。preHandle 返回 false 则中断后续流程,afterCompletion 仍然执行。

7. 异常体系 — 全局异常处理决策

统一异常处理架构
  • 业务异常:BizException(code, message) — 可预期的业务错误
  • 参数校验异常:MethodArgumentNotValidException — @Valid 校验失败
  • 权限异常:AccessDeniedException — 无权限
  • 系统异常:NullPointerException、SQLException — 不可预期的BUG

💻 全局异常处理器

📁 shop-service
📂 src/main/java
📂 com.shop.common.exception
📄 GlobalExceptionHandler.java
📄 BizException.java
📂 com.shop.order.service
📄 OrderService.java
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 1. 业务异常 — 返回业务错误码 @ExceptionHandler(BizException.class) public Result<?> handleBizException(BizException e) { log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage()); return Result.fail(e.getCode(), e.getMessage()); } // 2. 参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<?> handleValidException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors().stream() .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) .collect(Collectors.joining("; ")); return Result.fail(400, msg); } // 3. 兜底 — 未知异常 @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("系统异常", e); return Result.fail(500, "系统繁忙,请稍后重试"); // 不暴露内部信息 } } // 业务异常类 @Getter public class BizException extends RuntimeException { private final int code; public BizException(int code, String message) { super(message); this.code = code; } } // 使用 public OrderDTO createOrder(CreateOrderReq req) { Product product = productMapper.selectById(req.getProductId()); if (product == null) { throw new BizException(404, "商品不存在"); } if (product.getStock() < req.getQuantity()) { throw new BizException(4001, "库存不足"); } // ... }
面试金句 @RestControllerAdvice = @ControllerAdvice + @ResponseBody,本质是一个全局的 HandlerExceptionResolver。Spring MVC 在 DispatcherServlet.processDispatchResult 中捕获 Controller 抛出的异常,按 @ExceptionHandler 的异常类型精确匹配(子类优先于父类)路由到对应处理方法。兜底的 Exception.class 处理器必须隐藏堆栈信息(返回通用错误消息),防止 SQL/类名等敏感信息泄露。

8. 配置管理 — 敏感信息与动态配置

配置管理最佳实践
  • @ConfigurationProperties:类型安全的配置映射 — 优于 @Value
  • 敏感信息:数据库密码、API Key — 不放代码仓库,用环境变量或配置中心
  • Nacos/Apollo:配置中心动态刷新 — 不用重启就能改配置
  • 配置优先级:命令行 > 环境变量 > application-prod.yml > application.yml

💻 配置管理实战

📁 order-service
📂 src/main/java
📂 com.shop.order.config
📄 OrderProperties.java
📂 com.shop.order.controller
📄 FeatureController.java
📂 src/main/resources
📄 application.yml
// 1. @ConfigurationProperties(推荐) @ConfigurationProperties(prefix = "app.order") @Data public class OrderProperties { private int maxItems = 100; // 默认值 private Duration paymentTimeout = Duration.ofMinutes(30); private boolean autoCancel = true; } // 2. 敏感信息用环境变量 // application.yml // spring: // datasource: // password: ${DB_PASSWORD:default_dev_pass} // # 生产环境通过环境变量注入,不用写在代码里 // 3. Nacos 动态配置 @RefreshScope // 配置变更时自动刷新 Bean @RestController public class FeatureController { @Value("${feature.new-ui:false}") private boolean newUiEnabled; @GetMapping("/feature/switch") public Result switchFeature() { return Result.ok(Map.of("newUi", newUiEnabled)); } // Nacos 改配置 → 不用重启 → 立即生效 }
面试金句 @ConfigurationProperties 优于 @Value 的三大优势:(1)类型安全——自动映射到 POJO,启动时校验类型;(2)支持 Duration/Map/List 等复杂类型;(3)支持 @RefreshScope 热更新——@Value 注入的值在 @RefreshScope 的 Bean 中也能刷新。${DB_PASSWORD:default} 语法提供默认值,冒号后为 fallback。

🍃 Spring Boot 深度场景决策 · 全栈资料库

← 返回场景决策