← 返回场景决策
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 日志切面实现
@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 事件完整示例
// 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 配置实战
# 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 |
| @ConditionalOnExpression | SpEL 表达式 | 复杂条件组合 |
💻 条件装配实战
// 功能开关:短信服务可选
@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/
// ├── 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、压缩 | 认证、权限、日志 |
💻 认证拦截器实现
@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
💻 全局异常处理器
@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
💻 配置管理实战
// 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。