Spring Boot 面试知识(二):分层、IoC、异常、配置与 Redis

很多 Spring Boot 面试题看起来是在问注解,实际上考察的是三件事:

  1. 你是否理解一次请求如何在系统中流转;
  2. 你是否理解对象由谁创建、依赖如何注入;
  3. 你是否知道框架能力的边界,以及生产环境中的常见问题。

本文以 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:请求完成后,用于清理资源。

对于 @ResponseBodyResponseEntity,响应体可能在 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 的 AuthenticationEntryPointAccessDeniedHandler

建议同时使用 HTTP 状态码和业务码,不要让所有失败都返回 HTTP 200。


六、配置文件与多环境管理

Spring Boot 支持 application.propertiesapplication.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

完整过程可以描述为:

  1. Filter 为请求生成 traceId,记录基础访问日志;
  2. DispatcherServlet 接收请求并寻找 Controller;
  3. Interceptor 检查登录信息和接口上下文;
  4. Controller 解析参数并调用 Service;
  5. Service 对象由 IoC 容器创建,DAO 通过构造器注入;
  6. Service 先查询 Redis;
  7. 缓存未命中时,通过 DAO 查询数据库;
  8. 查询结果写入 Redis,并设置过期时间;
  9. 如果用户不存在,Service 抛出业务异常;
  10. @RestControllerAdvice 将异常转换成统一 HTTP 响应;
  11. AOP 围绕 Service 方法记录耗时、事务或审计信息;
  12. 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 则体现了应用如何真正进入生产环境。

只要把这些知识点放进同一条请求链路里理解,面试时就不再是背诵零散注解,而是在解释一个完整系统如何运行。

相关推荐
泡茶喝茶写代码42 分钟前
量化数据开发实战系列(第 25 篇):基金基础接口实战:基金列表、ETF-LOF 分类、基金概况、净值数据
java·数据库·人工智能·python·mysql
shehuiyuelaiyuehao1 小时前
算法46,分治快排,排序数组
java·算法·排序算法·排序
野生技术架构师2 小时前
1200 道 JAVA 面试题(各大企业常见面试题及答案)
java·开发语言
careathers2 小时前
【数据结构】栈
java·数据结构
邪修king2 小时前
Re:Linux系统篇(二十六):文件系统(二):Ext 文件系统底层详解:从 inode、块组到软硬链接,结合 Windows 讲透文件管理本质
android·java·linux
Yyyyyy~2 小时前
【java】类和对象
java·开发语言
重生之小比特3 小时前
【Java SE】抽象类和接口
java·开发语言
Lyyaoo.3 小时前
【设计模型】策略模式
java
譕痕3 小时前
Idea 集成 智能体开发工具“千问(qwen)”
java·ide·spring·语言模型