
很多 Spring Boot 面试题看起来是在问注解,实际上考察的是三件事:
- 你是否理解一次请求如何在系统中流转;
- 你是否理解对象由谁创建、依赖如何注入;
- 你是否知道框架能力的边界,以及生产环境中的常见问题。
本文以 Spring Boot 3.x、Java 17 为背景,串联 Controller、Service、DAO、Filter、Interceptor、AOP、IoC、异常体系、配置文件和 Redis。
一、Controller、Service、DAO 为什么要分层?
一个典型请求的调用链如下:
text
客户端
↓ HTTP
Controller
↓
Service
↓
DAO / Repository
↓
数据库
分层的目的并不是"多写几个类",而是让不同职责发生变化时互不干扰。
1. Controller:协议适配层
Controller 负责接收路径、查询参数和请求体,完成参数校验,调用 Service,并将结果转换为 HTTP 响应。
java
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
Controller 不适合承担复杂业务逻辑,也不应该直接操作数据库。
面试中可以这样回答:
Controller 是 Web 层,负责协议转换和参数校验;业务规则放在 Service;数据库访问交给 DAO。这样既符合单一职责,也便于测试、复用和事务管理。
2. Service:业务逻辑与事务边界
Service 负责业务流程编排、业务规则、协调多个 DAO 或外部服务,以及定义事务边界。
java
@Service
public class UserService {
private final UserRepository userRepository;
private final CouponRepository couponRepository;
public UserService(UserRepository userRepository,
CouponRepository couponRepository) {
this.userRepository = userRepository;
this.couponRepository = couponRepository;
}
@Transactional
public UserResponse register(RegisterCommand command) {
if (userRepository.existsByPhone(command.phone())) {
throw new BusinessException("手机号已经注册");
}
User user = userRepository.save(User.create(command));
couponRepository.issueNewUserCoupon(user.getId());
return UserResponse.from(user);
}
}
事务通常放在 Service 层,因为一个完整业务操作可能包含多个数据库操作。DAO 的一次调用往往只代表一项数据操作,而事务应该覆盖完整的业务用例。
3. DAO / Repository:数据访问层
DAO 负责屏蔽底层存储细节。实际项目可能使用 MyBatis Mapper、Spring Data JPA Repository、JdbcTemplate 或自定义数据访问组件。
java
@Repository
public class UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"select id, name from user where id = ?",
userRowMapper,
id
);
}
}
@Repository 不仅表示这是数据访问组件,还可以参与 Spring 的数据访问异常转换。
4. DTO 和 Entity 要不要分开?
建议分开:
text
HTTP 请求 DTO → Service 命令对象 → Domain/Entity → 响应 DTO
数据库 Entity 如果直接暴露给前端,容易导致数据库字段变化影响 API、敏感字段泄露、非法字段覆盖,以及 JPA 延迟加载和序列化问题。
二、Filter、Interceptor、AOP 有什么区别?
先看执行位置:
text
请求
↓
Filter
↓
DispatcherServlet
↓
Interceptor.preHandle
↓
Controller
↓
Service 的 AOP 代理
↓
Interceptor.postHandle
↓
Interceptor.afterCompletion
↓
Filter
↓
响应
1. Filter:Servlet 容器层
Filter 属于 Servlet 规范,工作在 Spring MVC 的 DispatcherServlet 外围,适合处理编码、跨域、链路 ID、请求包装、基础鉴权和访问日志。
Spring Boot 3 使用 jakarta.servlet 包:
java
@Component
public class TraceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
try {
response.setHeader("X-Trace-Id", traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
2. Interceptor:Spring MVC 层
Interceptor 属于 Spring MVC,围绕 Controller 的 Handler 执行。
java
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (token == null) {
throw new UnauthorizedException("请先登录");
}
return true;
}
}
三个主要回调:
preHandle:Controller 执行前,可中断调用链;postHandle:Controller 执行后、视图渲染前;afterCompletion:请求完成后,用于清理资源。
对于 @ResponseBody 或 ResponseEntity,响应体可能在 postHandle 前就已写出。需要统一修改响应体时,可以使用 ResponseBodyAdvice。安全控制则更适合交给 Spring Security 或 Servlet Filter 链。
3. AOP:Spring Bean 方法层
AOP 面向方法调用,不局限于 HTTP 请求,适合事务、耗时统计、操作审计、权限、重试、缓存和幂等控制。
java
@Aspect
@Component
public class TimeCostAspect {
@Around("@within(org.springframework.stereotype.Service)")
public Object recordTime(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
try {
return point.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println(point.getSignature() + " cost=" + cost + "ms");
}
}
}
Spring AOP 的本质通常是代理:
text
调用方 → 代理对象 → 增强逻辑 → 目标对象
因此存在一个经典问题:同类内部调用可能导致 AOP 失效。
java
@Service
public class OrderService {
public void createOrder() {
this.saveOrder(); // 没有经过代理
}
@Transactional
public void saveOrder() {
// ...
}
}
this.saveOrder() 是目标对象内部调用,没有再次经过 Spring 代理,所以 @Transactional 可能不会生效。这也是 @Async、@Cacheable 等注解失效的常见原因。
4. 面试对比总结
| 对比项 | Filter | Interceptor | AOP |
|---|---|---|---|
| 所属体系 | Servlet | Spring MVC | Spring IoC/AOP |
| 主要拦截对象 | HTTP 请求与响应 | Controller Handler | Spring Bean 方法 |
| 能否脱离 Spring MVC | 可以 | 不可以 | 可以 |
| 常见用途 | 编码、CORS、链路、请求包装 | 登录校验、接口上下文 | 事务、审计、缓存、耗时 |
| 核心机制 | FilterChain | HandlerExecutionChain | 动态代理 |
一句话回答:
Filter 拦截 Servlet 请求,Interceptor 拦截 Spring MVC Handler,AOP 拦截 Spring Bean 的方法调用。
三、@Configuration 和 @Bean
1. 什么是 Bean?
Bean 就是由 Spring IoC 容器创建、管理和装配的对象。对象的创建、依赖注入、初始化、代理和销毁都可以由 Spring 管理。
2. @Configuration 和 @Bean
@Configuration 表示这个类主要用于提供 Bean 定义;@Bean 告诉 Spring,把方法返回的对象注册到 IoC 容器。
java
@Configuration
public class ClientConfig {
@Bean
public HttpClient httpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
}
}
默认情况下,Bean 名称是方法名,作用域是 singleton,返回值类型参与依赖匹配。
3. @Component 和 @Bean 的区别
@Component 通常标在自己编写的类上,由组件扫描发现。@Bean 更适合注册第三方类、定制对象创建过程,或者根据配置构造不同实现。
java
@Bean
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.findAndAddModules()
.build();
}
4. proxyBeanMethods 有什么作用?
默认完整配置模式下,Spring 会增强 @Configuration 类,使 @Bean 方法之间的调用能够从容器获取已有的 singleton Bean。
如果使用:
java
@Configuration(proxyBeanMethods = false)
那么 @Bean 方法之间的直接调用就是普通 Java 方法调用。推荐通过方法参数表达依赖:
java
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public Client client() {
return new Client();
}
@Bean
public OrderService orderService(Client client) {
return new OrderService(client);
}
}
四、@Autowired 的工作原理
@Autowired 表示让 Spring 从 IoC 容器中寻找合适的 Bean 并注入,底层主要由 AutowiredAnnotationBeanPostProcessor 等容器扩展完成。
1. 推荐使用构造器注入
java
@Service
public class PaymentService {
private final PaymentClient paymentClient;
public PaymentService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
如果类只有一个构造器,通常可以省略 @Autowired。构造器注入让依赖更明确,字段可以声明为 final,也更便于测试和发现循环依赖。
2. 多个 Bean 如何选择?
Spring 首先按类型查找。如果有多个相同类型的候选,可以使用 @Primary 指定默认实现,或者使用 @Qualifier 显式选择。
java
public PaymentService(
@Qualifier("wechatPayClient") PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
3. 循环依赖
text
A 依赖 B
B 又依赖 A
构造器注入会直接暴露循环依赖并导致启动失败。即使部分字段或 Setter 循环依赖能够通过提前引用处理,也通常意味着职责设计有问题。应优先重新划分职责、抽取第三个服务或使用事件解耦。
五、统一异常处理
如果每个 Controller 都写 try-catch,会造成重复代码、状态码不统一,并可能暴露内部异常信息。
1. 定义业务异常
java
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
2. 使用 @RestControllerAdvice
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException exception) {
return ResponseEntity.badRequest().body(
new ErrorResponse(exception.getCode(), exception.getMessage())
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnknownException(
Exception exception) {
log.error("Unhandled exception", exception);
return ResponseEntity.internalServerError().body(
new ErrorResponse("INTERNAL_ERROR", "系统繁忙,请稍后重试")
);
}
}
Spring MVC 的异常处理建立在 HandlerExceptionResolver 链上。
3. @ControllerAdvice 能处理 Filter 异常吗?
通常不能直接处理。Filter 位于 DispatcherServlet 外部,如果异常发生在进入 Spring MVC 之前,就没有进入 MVC 的异常解析流程。
常见处理方法包括:
- 在 Filter 内捕获并写入统一响应;
- 委托给 Spring 的
HandlerExceptionResolver; - 鉴权场景交给 Spring Security 的
AuthenticationEntryPoint或AccessDeniedHandler。
建议同时使用 HTTP 状态码和业务码,不要让所有失败都返回 HTTP 200。
六、配置文件与多环境管理
Spring Boot 支持 application.properties 和 application.yml。
yaml
server:
port: 8080
app:
payment:
timeout: 3s
max-retries: 2
1. @Value 和 @ConfigurationProperties
少量独立配置可以使用 @Value:
java
@Value("${app.payment.timeout}")
private Duration timeout;
一组相关配置更适合使用 @ConfigurationProperties:
java
@ConfigurationProperties(prefix = "app.payment")
public record PaymentProperties(
Duration timeout,
int maxRetries,
URI callbackUrl) {
}
相比 @Value,@ConfigurationProperties 支持类型安全、批量绑定、嵌套对象、校验和更清晰的配置结构。
2. 多环境配置
text
application.yml
application-dev.yml
application-test.yml
application-prod.yml
激活生产环境:
bash
java -jar app.jar --spring.profiles.active=prod
公共配置放在 application.yml,环境差异放在对应的 profile 文件中。
3. 配置覆盖与敏感信息
Spring Boot 配置可以来自配置文件、环境变量、JVM 系统属性和命令行参数。高优先级配置源可以覆盖低优先级配置源。
生产环境不应把密码直接提交到 Git,可以使用环境变量、Kubernetes Secret、Vault、云厂商密钥管理系统或企业配置中心:
yaml
spring:
datasource:
password: ${DB_PASSWORD}
七、Spring Boot 集成 Redis
1. 引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Boot 默认通过 starter 提供 Redis 自动配置,并默认使用 Lettuce 客户端。
2. 配置连接
yaml
spring:
data:
redis:
host: localhost
port: 6379
database: 0
timeout: 2s
生产环境还需要考虑密码或 ACL、TLS、Sentinel 或 Cluster、连接池、命令超时以及 Redis 不可用时的降级策略。
3. 使用 StringRedisTemplate
java
@Service
public class VerificationCodeService {
private final StringRedisTemplate redisTemplate;
public VerificationCodeService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void saveCode(String phone, String code) {
String key = "verification-code:" + phone;
redisTemplate.opsForValue()
.set(key, code, Duration.ofMinutes(5));
}
}
简单字符串数据优先考虑 StringRedisTemplate。存储对象时,要显式考虑 JSON 序列化、类型安全、跨语言共享和版本兼容问题。
4. Cache Aside 模式
text
读取:先查 Redis
├─ 命中:直接返回
└─ 未命中:查数据库 → 写入 Redis → 返回
更新:更新数据库 → 删除 Redis 缓存
通常选择"更新数据库后删除缓存",而不是同时更新数据库和缓存,因为双写更容易产生不一致。严格场景还可以在事务提交后删除缓存,或者通过消息和重试机制实现最终一致性。
5. Redis 高频面试问题
缓存穿透
查询数据库中不存在的数据,每次都绕过缓存访问数据库。
解决方案:缓存空值、布隆过滤器、参数合法性校验。
缓存击穿
某个热点 Key 失效,大量请求同时访问数据库。
解决方案:互斥锁、逻辑过期、热点数据预热、后台刷新。
缓存雪崩
大量 Key 在相近时间失效,或者 Redis 整体不可用。
解决方案:过期时间增加随机值、热点 Key 分散过期、Redis 高可用、限流熔断和本地缓存兜底。
分布式锁
基本思想:
text
SET lockKey uniqueValue NX EX 30
释放锁时必须比较 value,只删除自己持有的锁,并通过 Lua 脚本保证"比较和删除"的原子性。生产项目通常优先使用 Redisson 等经过验证的实现。
八、把所有知识点串成一次请求
假设客户端请求:
http
GET /api/users/100
Authorization: Bearer xxx
完整过程可以描述为:
- Filter 为请求生成 traceId,记录基础访问日志;
DispatcherServlet接收请求并寻找 Controller;- Interceptor 检查登录信息和接口上下文;
- Controller 解析参数并调用 Service;
- Service 对象由 IoC 容器创建,DAO 通过构造器注入;
- Service 先查询 Redis;
- 缓存未命中时,通过 DAO 查询数据库;
- 查询结果写入 Redis,并设置过期时间;
- 如果用户不存在,Service 抛出业务异常;
@RestControllerAdvice将异常转换成统一 HTTP 响应;- AOP 围绕 Service 方法记录耗时、事务或审计信息;
- Interceptor 和 Filter 完成收尾处理。
这条链路同时涉及 Web MVC、IoC、AOP、异常体系、配置绑定和数据访问,是面试中最值得建立的整体模型。
九、面试速答版
Controller、Service、DAO 分别负责什么?
Controller 负责 HTTP 协议适配,Service 负责业务逻辑和事务编排,DAO 负责数据持久化。分层的核心是职责隔离,而不是形式上的目录划分。
Filter、Interceptor、AOP 的区别?
Filter 位于 Servlet 层,处理原始请求和响应;Interceptor 位于 Spring MVC 层,围绕 Controller 执行;AOP 位于 Bean 方法调用层,常用于事务、日志、缓存和审计。
@Configuration 与 @Bean 是什么关系?
@Configuration声明 Java 配置类,@Bean声明方法创建的对象需要交给 IoC 容器管理。@Bean特别适合注册第三方对象或定制创建过程。
@Autowired 按名称还是按类型?
核心上先按类型解析候选 Bean。存在多个候选时,可以通过
@Primary、@Qualifier等进一步确定。工程中推荐构造器注入。
为什么 @Transactional 有时不生效?
常见原因是同类内部调用没有经过 Spring AOP 代理;此外还有对象不是 Spring Bean、方法不能被代理、异常被捕获后没有继续抛出等情况。
全局异常如何处理?
使用
@RestControllerAdvice配合@ExceptionHandler,分别处理业务异常、参数异常和未知异常,并统一 HTTP 状态码、业务码和日志策略。
@Value 和 @ConfigurationProperties 如何选择?
单个简单配置可以使用
@Value;一组结构化配置优先使用@ConfigurationProperties,因为它支持类型安全、批量绑定、嵌套结构和校验。
Redis 缓存如何保证一致性?
常见方案是 Cache Aside:读取时缓存未命中再查询数据库并回填;更新时先更新数据库,再删除缓存。更严格场景还需要事务提交后删除、消息重试或订阅数据库变更来保证最终一致性。
结语
学习 Spring Boot 不能只记"哪个注解怎么写"。真正重要的是理解三个底层模型:
text
IoC:对象由容器创建和装配
AOP:通过代理增强 Bean 的方法调用
MVC:请求经过组件链,最终映射到 Controller
Controller、Service、DAO 是业务分层;Filter、Interceptor、AOP 是不同层级的横切机制;@Configuration、@Bean、@Autowired 是 IoC 的主要使用方式;异常处理、配置文件和 Redis 则体现了应用如何真正进入生产环境。
只要把这些知识点放进同一条请求链路里理解,面试时就不再是背诵零散注解,而是在解释一个完整系统如何运行。