跳转至

Spring MVC 与 WebFlux

Spring MVC 请求处理流程

Spring MVC 处理一个 HTTP 请求的完整流程

🔹 一句话概括 请求先到前端控制器 DispatcherServlet,然后它调度各种组件,最终返回响应。理解这个流程,就掌握了 Spring MVC 的精髓。

🔹 文字流程图(带图标说明)

用户请求
   ↓
① 🎯 DispatcherServlet(前端控制器)
   |  是整个流程的“总指挥”,接收请求并分派。
   ↓
② 🗺️ HandlerMapping(处理器映射器)
   |  根据请求 URL、方法等找到对应的处理器 Handler(即控制器方法)。
   ↓
③ ⚙️ HandlerAdapter(处理器适配器)
   |  负责真正调用 Handler。因为 Handler 有多种形式(@Controller、HttpRequestHandler 等),
   |  通过适配器统一调用,并处理参数绑定、返回值处理等。
   ↓
④ 🔧 Handler(开发者编写的 Controller 方法)
   |  执行业务逻辑,通常会返回一个 ModelAndView(传统)或直接用 @ResponseBody 返回对象。
   ↓
⑤ 🔁 如果有拦截器(Interceptor)的 postHandle,视图渲染前执行。
   ↓
⑥ 🖼️ ViewResolver(视图解析器) / HttpMessageConverter(消息转换器)
   |  如果返回视图名,则 ViewResolver 解析为具体 View 模板(如 JSP、Thymeleaf);
   |  如果是 @ResponseBody 或 REST 接口,则通过消息转换器(如 MappingJackson2HttpMessageConverter)把对象转为 JSON。
   ↓
⑦ 📄 视图渲染 / 响应体输出
   |  将模型数据填充到视图,生成 HTML;或者直接输出 JSON/XML 等。
   ↓
⑧ 🧹 拦截器的 afterCompletion 执行(视图渲染后,用于资源清理)。
   ↓
响应返回给用户

🔹 示例代码:体验 @ResponseBody 如何走消息转换器

@RestController
public class UserController {
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        // 模拟查询用户
        User user = new User(id, "老周", "laozhou@example.com");
        return user;  // 因为没有视图名,默认走 @ResponseBody -> JSON
    }
}

当请求 /users/1 时,DispatcherServlet 通过 HandlerMapping 找到 getUser 方法,HandlerAdapter 执行方法并得到 User 对象。因为没有返回视图,Spring MVC 会使用 HttpMessageConverter 将 User 转成 JSON 写入响应体。

🔹 面试加分小贴士 可以顺便提一下:在前后端分离时代,ViewResolver 环节经常会被弱化,核心变成了消息转换器。另外,流程中还会穿插拦截器、异常解析器等,体现了 Spring MVC 的可扩展性。

📌 这样梳理下来,是不是比死记硬背清晰多了?下次可以尝试画一画,面试时连画带讲,效果拔群。


拦截器(Interceptor)和过滤器(Filter)的区别

很多同学傻傻分不清,本质上是因为它们处于不同层级。我们用一张对比表 + 代码感受一下。

🔹 核心区别对比表

查看内嵌表格

🔹 代码对比:实现与执行顺序

过滤器示例:

@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter: 前处理");
        chain.doFilter(request, response); // 必须显式调用才能继续
        System.out.println("Filter: 后处理");
    }
}

拦截器示例:

@Component
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) {
        System.out.println("Interceptor: preHandle");
        // 可以强转 handler 为 HandlerMethod 拿到方法信息
        return true; // true 放行,false 中断
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) {
        System.out.println("Interceptor: postHandle");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        System.out.println("Interceptor: afterCompletion");
    }
}

配置拦截器(Spring Boot 中):

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/api/**");  // 只拦截 API 路径
    }
}

如果同时有 Filter 和 Interceptor,执行顺序通常是: Filter前 → Interceptor.pre → Controller → Interceptor.post → Interceptor.after → Filter后。 所以 Filter 是最外层的“壳”,Interceptor 是更贴近业务的一层。

🔹 选择建议

  • 如果只是做编码、过滤参数、跨域,用 Filter 更合适,和框架无关。

  • 如果要拿到控制器方法、参数,或者要统计哪个接口耗时最长,用 Interceptor 更方便。

💡 掌握了这些区别,面试时再从实际场景举例,就能把“背诵”变成“理解”了。


@ControllerAdvice + @ExceptionHandler 的原理与统一错误响应设计

这个组合是构建健壮 API 的基石,面试必考原理和落地。

🔹 工作原理剖析

  1. 异常发生点:控制器方法抛出异常(如 RuntimeException、自定义 BusinessException)。

  2. DispatcherServlet 捕获:DispatcherServlet 的 processHandlerException() 方法开始遍历所有注册的 HandlerExceptionResolver。

  3. ExceptionHandlerExceptionResolver 登场:这是专门处理 @ExceptionHandler 的解析器。它会先在本 Controller 中查找标注了 @ExceptionHandler 的方法,如果没找到,就去所有 @ControllerAdvice 类中查找。

  4. 匹配规则:根据异常类型匹配,会按继承关系找最接近的。比如抛出了 UserNotFoundException,会优先匹配声明处理该异常的方法,如果没有则匹配它的父类 RuntimeException。

  5. 执行处理方法并输出:找到匹配的方法后,通过反射调用,返回的 ResponseEntity 或 @ResponseBody 对象会经过消息转换器变成 JSON 响应。

🔹 设计统一的 API 错误响应

通常需要定义一个统一返回体,包含错误码、错误信息、请求路径、时间戳等,还可以携带详细错误列表(如参数校验失败)。

步骤1:定义错误响应结构

public class ApiError {
    private int code;
    private String message;
    private String path;
    private LocalDateTime timestamp;
    private List<String> details;   // 用于存放校验错误细节

    // 构造方法、getter、setter 省略,实际可用 Lombok
}

步骤2:自定义业务异常(可选但推荐)

public class BusinessException extends RuntimeException {
    private int code;
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    // getter
}

步骤3:全局异常处理器

@RestControllerAdvice   // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    // 处理自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiError> handleBusinessException(BusinessException ex,
                                                            HttpServletRequest request) {
        ApiError error = new ApiError();
        error.setCode(ex.getCode());
        error.setMessage(ex.getMessage());
        error.setPath(request.getRequestURI());
        error.setTimestamp(LocalDateTime.now());
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // 处理参数校验失败异常(@Valid 触发)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex,
                                                     HttpServletRequest request) {
        List<String> details = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .collect(Collectors.toList());

        ApiError error = new ApiError();
        error.setCode(400);
        error.setMessage("参数校验失败");
        error.setPath(request.getRequestURI());
        error.setTimestamp(LocalDateTime.now());
        error.setDetails(details);
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // 兜底处理未知异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleAll(Exception ex, HttpServletRequest request) {
        ApiError error = new ApiError();
        error.setCode(500);
        error.setMessage("服务器内部错误:" + ex.getLocalizedMessage());
        error.setPath(request.getRequestURI());
        error.setTimestamp(LocalDateTime.now());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

🔹 返回效果示例 当接口抛出 new BusinessException(1001, "用户不存在") 时,客户端收到:

{
    "code": 1001,
    "message": "用户不存在",
    "path": "/api/users/99",
    "timestamp": "2026-07-05T14:30:00",
    "details": null
}

🔹 进阶与注意

  • @ResponseStatus 注解也可以标在异常类上,但不如 ResponseEntity 灵活。

  • 可以结合 @ExceptionHandler 处理 AccessDeniedException 实现统一安全错误。

  • 对于微服务,错误码要全局规划,避免冲突。

  • 一定注意:@ExceptionHandler 方法内如果抛出异常,不会再被同一个 @ControllerAdvice 捕获,会直接抛给容器,所以在兜底处理里尽量不要再写复杂逻辑。

🛠️ 设计良好的错误响应,是 API 成熟度的重要标志。希望你能在项目中把它用起来,而不是只停留在面试的“标准答案”上。

🚀 Agent 的 SSE 流式输出接口如何用 Spring MVC 实现?与 WebSocket 如何选型?

很多 AI 对话、实时日志、消息推送场景,Agent 需要把生成的内容一个字一个字“吐”给前端。Spring MVC 里最契合的技术就是 SSE(Server-Sent Events),它本质是一种基于 HTTP 的单向流式响应,比 WebSocket 轻量得多。

🔧 Spring MVC 的实现方式

Spring 提供了两个主流工具:SseEmitter(阻塞式,适合 Tomcat 线程模型)和 响应式 Flux<ServerSentEvent>(WebFlux 专用,但我们这里限定 MVC,就用 SseEmitter)。

完整示例:创建一个 Agent SSE 接口

@RestController
@RequestMapping("/agent")
public class AgentController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamResponse(@RequestParam String prompt) {
        // 超时时间设为 5 分钟,避免长连接被断开
        SseEmitter emitter = new SseEmitter(300_000L);

        // 在异步线程中模拟 Agent 生成过程
        Executors.newSingleThreadExecutor().execute(() -> {
            try {
                String[] words = {"Hello", ", ", "this", " is", " a", " streaming", " response"};
                for (String word : words) {
                    // 发送一条事件,可以附带事件名和数据
                    emitter.send(SseEmitter.event()
                            .name("message")
                            .data(word));
                    Thread.sleep(500); // 模拟逐词生成延迟
                }
                // 正常完成
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });

        // 设置回调处理客户端断开
        emitter.onCompletion(() -> System.out.println("连接完成"));
        emitter.onTimeout(() -> System.out.println("连接超时"));
        return emitter;
    }
}

关键点:

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE 告诉浏览器这是 SSE 流。

  • 每次 send() 都会把数据以 data: ...\n\n 的格式写入响应体,浏览器端通过 EventSource API 接收。

  • 记得处理超时和异常,否则 Servlet 线程可能挂起。

配置线程池(避免阻塞主线程)

@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.initialize();
        return executor;
    }
}

这样,每个 SSE 连接消耗一个异步线程,Tomcat 主线程迅速释放,可以支撑更多并发。

⚖️ 和 WebSocket 的选型对比

查看内嵌表格

对于典型的 Agent 流式输出,浏览器只需要收数据,完全不需要往服务器发消息(除了最初的请求),SSE 简直为它量身定做。而且完全基于 HTTP,负载均衡、防火墙、认证都可以复用现有体系。除非你的 Agent 需要持续双向交互(比如可打断、可中途修改指令),否则 WebSocket 就是杀鸡用牛刀。

🎯 选型结论

  • Agent 流式输出,毫无疑问首选 SSE。它更简单、省资源,还能享受 HTTP/2 的多路复用。

  • 如果是需要“用户实时操作 → Agent 立即响应 + Agent 主动推送”这种双向高频场景,才上 WebSocket。

请求处理深入

⚙️ 1 HandlerMethodArgumentResolver 自定义:实现 @CurrentUser 自动注入当前用户

在实际项目里,很多接口方法都要拿到当前登录用户,传统做法是用 SecurityContextHolder.getContext().getAuthentication() 或者从 Request Header 里解析 token。一遍又一遍写重复代码,难看又容易出错。自定义参数解析器就是来消灭这种样板代码的。

🧩 工作原理

Spring MVC 在调用控制器方法前,会遍历所有 HandlerMethodArgumentResolver。对每个参数,supportsParameter() 被调用,如果返回 true,就由 resolveArgument() 负责创建参数实例。我们可以自己实现一个,识别带 @CurrentUser 注解的 User 参数,并自动填入当前用户。

📝 一步一步实现

第一步:定义 @CurrentUser 注解

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
    // 可以增加属性,比如 required 表示是否必须登录
}

第二步:实现参数解析器

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    // 这里假定你有一个从 token 解析用户的服务
    private final UserService userService;

    public CurrentUserArgumentResolver(UserService userService) {
        this.userService = userService;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 参数类型是 User 并且带有 @CurrentUser 注解
        return parameter.getParameterType().equals(User.class)
                && parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        // 从请求中获取 token(例如 Header 或 Cookie)
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            // 如果注解允许非必须,可返回 null 或匿名用户
            throw new AuthenticationException("未携带认证信息");
        }
        // 根据 token 获取用户,实际可调用已有安全框架
        User user = userService.getUserFromToken(token);
        return user;
    }
}

第三步:注册到 Spring MVC

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final CurrentUserArgumentResolver currentUserResolver;

    public WebMvcConfig(CurrentUserArgumentResolver currentUserResolver) {
        this.currentUserResolver = currentUserResolver;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        // 添加到最前面,优先处理
        resolvers.add(0, currentUserResolver);
    }
}

使用效果

@RestController
@RequestMapping("/api/user")
public class UserProfileController {

    @GetMapping("/profile")
    public ResponseEntity<Profile> getProfile(@CurrentUser User currentUser) {
        // 这里 currentUser 已经由解析器自动注入
        return ResponseEntity.ok(profileService.getProfile(currentUser.getId()));
    }
}

📈 执行时序图

请求 → DispatcherServlet
           ↓
  查找 HandlerMethod
           ↓
  获取参数列表,对每个参数遍历所有 HandlerMethodArgumentResolver
           ↓
  遇到 @CurrentUser + User 类型 → CurrentUserArgumentResolver.supportsParameter() 返回 true
           ↓
  resolveArgument() 执行 → 从 Header/Token 获取用户 → 返回 User 对象
           ↓
  所有参数解析完毕 → 调用 Controller 方法

🧠 进阶思考

  • 可以结合 Spring Security,直接从 SecurityContext 取用户,不必重复解析 token。

  • 如果有非必须登录的场景,可以让 @CurrentUser 有个 required 属性,未登录时返回 null 或一个匿名占位对象。

  • 同理,可以做出 @RequestIp 自动注入客户端 IP、@RequestHeaderToken 等通用解析器,让控制器的签名就是你的业务协议。

掌握了自定义参数解析器,你就能把横向关注点干净地抽离,让控制器只关注业务规则。


📦 2. @RequestBody 的参数解析原理与 HttpMessageConverter 工作机制

这个问题的本质是:当你在 Controller 方法参数上打上 @RequestBody,Spring MVC 是如何把 HTTP 请求体里的 JSON/XML/文本变成你的 Java 对象的?

🔍 从 DispatchServlet 到对象的过程

  1. 识别参数:DispatcherServlet 找到对应的 HandlerMethod 后,遍历参数。发现某个参数标了 @RequestBody,就会去找能处理它的解析器。

  2. 指定解析器:RequestResponseBodyMethodProcessor 就是专门处理 @RequestBody 和 @ResponseBody 的。它的 supportsParameter 会返回 true。

  3. 读取内容类型:解析器获取请求的 Content-Type 头,比如 application/json。

  4. 遍历消息转换器:Spring 容器中注册了一系列 HttpMessageConverter(启动时加载)。解析器拿着目标类型(比如 User.class)和 Content-Type,依次询问每个 Converter 的 canRead()。第一个返回 true 的会被使用。

  5. 执行转换:选中的 Converter 调用 read() 方法,从 HttpInputMessage 读取输入流,将字节序列化为 Java 对象。对 JSON 来说,底层就是 Jackson 的 ObjectMapper。

  6. 数据绑定与验证:得到对象后,如果还有 @Valid 注解,会触发验证。最终对象作为参数传给控制器方法。

⚙️ HttpMessageConverter 接口深度剖析

public interface HttpMessageConverter<T> {

    boolean canRead(Class<?> clazz, MediaType mediaType);
    boolean canWrite(Class<?> clazz, MediaType mediaType);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}
  • canRead:判断能否把请求体转换成目标 Java 类型,并支持指定的 MediaType。

  • read:真正干活的方法,输入是 HTTP 输入流,输出是对象。

  • write:用于 @ResponseBody 把对象写到输出流。

Spring 自带一些常用实现: MappingJackson2HttpMessageConverter → application/json StringHttpMessageConverter → text/plain FormHttpMessageConverter → application/x-www-form-urlencoded ByteArrayHttpMessageConverter → 处理二进制等。

默认序列化是如何支持的? 当我们引入 spring-boot-starter-web,自动配置会注册 MappingJackson2HttpMessageConverter,所以 REST 接口天然支持 JSON。

🧪 自定义一个消息转换器(例如支持 CSV)

假设我们需要让 @ResponseBody 能直接返回 List<User> 并输出 CSV 文件。

public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<User>> {

    public CsvHttpMessageConverter() {
        super(new MediaType("text", "csv"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return List.class.isAssignableFrom(clazz);
    }

    @Override
    protected List<User> readInternal(Class<? extends List<User>> clazz,
                                      HttpInputMessage inputMessage) {
        throw new UnsupportedOperationException("暂不实现读取");
    }

    @Override
    protected void writeInternal(List<User> users, HttpOutputMessage outputMessage)
            throws IOException {
        OutputStream outputStream = outputMessage.getBody();
        try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
            writer.println("id,name,email");
            for (User user : users) {
                writer.println(user.getId() + "," + user.getName() + "," + user.getEmail());
            }
        }
    }
}

注册:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new CsvHttpMessageConverter());
    }
}

现在 @GetMapping(produces = "text/csv") 并返回 List<User> 就能输出 CSV 了。这就是消息转换器给框架带来的扩展自由度。

⚠️ 注意事项与最佳实践

  • 请求体只能读取一次:如果在过滤器或拦截器中读取了 request.getInputStream(),到 @RequestBody 解析时流已经关闭,会报错。遇到这种情况要用 ContentCachingRequestWrapper 包装请求。

  • 大请求体流式处理:如果文件上传用 @RequestBody 不合适,内存会爆,应该用 MultipartFile 或流式转换(如 StreamingResponseBody 的逆操作)。

  • 性能:Jackson 默认会反序列化整个 JSON 到对象,如果只想取部分值,可以用 JsonNode 配合 @RequestBody 手动处理。

  • 定制 ObjectMapper:可以通过 Jackson2ObjectMapperBuilder 配置全局序列化规则,或通过 @JsonView 控制输出。

🧭 总结

@RequestBody 的背后是 参数解析器 + 消息转换器 的组合拳。理解这套机制,你就能自信地处理各种非标协议、定制数据格式,也能在遇到“415 Unsupported Media Type”或解析异常时快速定位问题。底层原理吃透了,上层框架的魔法就不再神秘。


RESTful API 设计与实践

1、基础题:RESTful API 设计的核心原则?@GetMapping/@PostMapping 等注解的语义区别?

难度级别:⭐⭐(HTTP 方法语义、资源命名、状态码规范)

Answer

REST 核心原则:资源导向、无状态、统一接口。

HTTP 方法语义:

  • GET:查询资源,幂等

  • POST:创建资源

  • PUT:全量更新资源,幂等

  • PATCH:部分更新

  • DELETE:删除资源,幂等

资源命名:使用名词复数,如 /users/{id},避免动词如 /getUser。

状态码:200 成功、201 创建成功、204 无内容、400 参数错误、401 未认证、403 无权限、404 不存在、500 服务器错误。

@GetMapping 等注解是 @RequestMapping(method=GET) 的快捷方式。


2、进阶题:Spring MVC 如何实现接口版本管理(v1/v2)?有哪些方案?

难度级别:⭐⭐⭐(URL 路径版本、请求头版本、自定义 RequestCondition)

1️⃣ Common Answer

接口版本管理有几种方式:URL 路径版本(/api/v1/users)、请求头版本(Accept 头里带版本)、请求参数版本(?version=2)。最常用的是 URL 路径版本,因为直观,Nginx 路由也方便。

2️⃣ Impressive Answer

我从 4 个维度对比:

  1. 方案对比:

查看内嵌表格

  1. 最佳实践:URL 路径版本最常用,配合 Swagger 分版本展示。生产环境建议:
  2. 版本号放在 URL 路径中(如 /api/v1/)
  3. 新版本并行发布,旧版本设置废弃时间
  4. Nginx 根据路径路由到不同服务

  5. 请求头版本实现:

@GetMapping(value = "/users", produces = "application/vnd.myapp.v1+json")
public List<User> getUsersV1() { /* v1 逻辑 */ }

@GetMapping(value = "/users", produces = "application/vnd.myapp.v2+json")
public List<UserDTO> getUsersV2() { /* v2 逻辑 */ }
  1. 自定义注解实现:继承 RequestMappingHandlerMapping,重写 getCustomMethodCondition,解析 @ApiVersion 注解。

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 平台需要同时支持 REST 和 SSE 两种调用模式,如何设计统一的 Controller 层?

难度级别:⭐⭐⭐(统一入口、策略模式、Accept 头协商)

1️⃣ Common Answer

可以写两个 Controller,一个返回 JSON,一个返回 SSE。或者同一个 Controller 根据 Accept 头判断,如果是 text/event-stream 就返回 SseEmitter,否则返回普通对象。

2️⃣ Impressive Answer

  1. 策略模式设计:
public interface ResponseStrategy {
    Object execute(Supplier<String> contentGenerator);
}

@Component
public class RestResponseStrategy implements ResponseStrategy {
    @Override
    public Object execute(Supplier<String> contentGenerator) {
        return Result.success(contentGenerator.get());
    }
}

@Component
public class SseResponseStrategy implements ResponseStrategy {
    @Override
    public Object execute(Supplier<String> contentGenerator) {
        SseEmitter emitter = new SseEmitter(120_000L);
        CompletableFuture.runAsync(() -> {
            try {
                String content = contentGenerator.get();
                emitter.send(SseEmitter.event().data(content));
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }
}
  1. 统一 Controller:
@RestController
@RequestMapping("/agent/chat")
public class AgentChatController {

    @Autowired
    private RestResponseStrategy restStrategy;

    @Autowired
    private SseResponseStrategy sseStrategy;

    @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE,
                            MediaType.TEXT_EVENT_STREAM_VALUE})
    public Object chat(@RequestParam String query,
                       @RequestHeader("Accept") String accept) {
        Supplier<String> generator = () -> agentService.generate(query);

        if (accept.contains(MediaType.TEXT_EVENT_STREAM_VALUE)) {
            return sseStrategy.execute(generator);
        }
        return restStrategy.execute(generator);
    }
}
  1. 生产考虑:
  2. SSE 模式需要异步处理,避免阻塞线程池
  3. REST 模式可以同步返回,响应更快
  4. 超时设置:SSE 设置较长超时(如 2 分钟),REST 设置较短超时(如 30 秒)
  5. 监控指标:分别统计两种模式的 QPS、延迟、错误率

3️⃣ Key Differences

查看内嵌表格



参数校验与数据绑定

1、基础题:Spring MVC 的参数校验机制?@Valid 和 @Validated 的区别?

难度级别:⭐⭐(JSR-303/380、校验注解、BindingResult)

Answer

校验机制:基于 JSR-303/380 标准,通过 LocalValidatorFactoryBean 集成 Hibernate Validator。

@Valid vs @Validated:

  • @Valid:JSR-303 标准注解,支持嵌套校验,不支持分组

  • @Validated:Spring 扩展,支持分组校验(groups),不支持嵌套校验

常用校验注解:@NotNull、@NotBlank、@Size、@Min、@Max、@Pattern、@Email。

BindingResult:紧跟被校验参数,用于获取校验错误信息。校验失败默认抛 MethodArgumentNotValidException,配合 @ControllerAdvice 统一处理。


2、进阶题:如何实现自定义校验注解?分组校验(groups)如何使用?

难度级别:⭐⭐⭐(ConstraintValidator、分组接口、@GroupSequence)

1️⃣ Common Answer

自定义校验注解需要定义注解类和实现 ConstraintValidator 接口。比如 @PhoneNumber 校验手机号格式。分组校验就是定义几个接口,比如 Create.class 和 Update.class,然后在注解上指定 groups,Controller 里用 @Validated(Create.class)。

2️⃣ Impressive Answer

  1. 自定义校验注解:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;  // @NotNull 负责判空
        return PATTERN.matcher(value).matches();
    }
}
  1. 分组校验:
public interface Create {}
public interface Update {}

public class UserDTO {
    @Null(groups = Update.class, message = "创建时 ID 必须为空")
    @NotNull(groups = Create.class, message = "更新时 ID 不能为空")
    private Long id;

    @NotBlank(groups = {Create.class, Update.class})
    @PhoneNumber(groups = {Create.class, Update.class})
    private String phone;
}

@PostMapping
public Result<Long> create(@Validated(Create.class) UserDTO dto) { /* 创建逻辑 */ }

@PutMapping
public Result<Void> update(@Validated(Update.class) UserDTO dto) { /* 更新逻辑 */ }
  1. @GroupSequence:控制分组校验顺序,前一组失败则不执行后续组:
@GroupSequence({Default.class, Create.class, Advanced.class})
public interface FullValidation {}
  1. 跨字段校验:类级别注解 + 类级别 Validator,如密码和确认密码一致性校验。

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 工具调用的参数校验如何设计?JSON Schema 校验 vs Bean Validation?

难度级别:⭐⭐⭐(动态校验、Schema 驱动、校验策略选型)

1️⃣ Common Answer

可以用 Bean Validation,在 DTO 上加校验注解。但 Agent 工具参数是动态的,可能用 JSON Schema 更合适。JSON Schema 可以根据工具定义动态校验。

2️⃣ Impressive Answer

  1. 选型对比:

查看内嵌表格

  1. 混合方案:
  2. 外层请求:用 Bean Validation(@Valid),校验通用参数(如 agentId、sessionId)
  3. 工具参数:用 JSON Schema,根据 Function Definition 动态校验

  4. JSON Schema 实现:

public class ToolParameterValidator {
    private final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);

    public ValidationResult validate(String schemaJson, Object parameters) {
        JsonSchema schema = factory.getSchema(new JSONObject(schemaJson));
        Set<ValidationMessage> errors = schema.validate(new JSONObject(parameters));

        return new ValidationResult(
            errors.isEmpty(),
            errors.stream()
                .map(ValidationMessage::getMessage)
                .collect(Collectors.toList())
        );
    }
}
  1. 错误信息设计:校验失败返回结构化错误,告诉 LLM 哪个参数不合法及原因,便于模型自动修正:
{
  "error": "VALIDATION_FAILED",
  "details": {
    "field": "max_tokens",
    "message": "must be greater than or equal to 1",
    "constraint": "minimum"
  }
}

3️⃣ Key Differences

查看内嵌表格



跨域与安全

1、基础题:Spring MVC 如何处理跨域(CORS)?@CrossOrigin 和全局配置的区别?

难度级别:⭐⭐(同源策略、预检请求、CORS 头)

Answer

跨域原因:浏览器同源策略限制,不同域名/端口/协议的请求被拦截。

处理方式:

  • @CrossOrigin:方法/类级别,细粒度控制

  • 全局配置:WebMvcConfigurer.addCorsMappings(),统一配置所有接口

  • Filter 方式:CorsFilter,在 Servlet 层处理,优先级最高

预检请求(OPTIONS):浏览器先发 OPTIONS 请求确认服务端是否允许跨域。

生产配置:不要用 *,明确指定允许的 origin、method、header。


2、进阶题:Spring Security 与 Spring MVC 的集成原理?FilterChain 的执行顺序?

难度级别:⭐⭐⭐(DelegatingFilterProxy、SecurityFilterChain、FilterChainProxy、核心 Filter)

1️⃣ Common Answer

Spring Security 是一组 Filter,通过 DelegatingFilterProxy 桥接到 Spring 容器。FilterChainProxy 管理多个 SecurityFilterChain,根据 URL 匹配不同的安全策略。Security Filter 在 DispatcherServlet 之前执行,先鉴权再路由。

2️⃣ Impressive Answer

  1. 集成原理:
  2. Spring Security 本质是一组 Servlet Filter
  3. DelegatingFilterProxy 作为 Servlet Filter 的代理,将请求委托给 Spring 容器中的 springSecurityFilterChain Bean
  4. FilterChainProxy 管理多个 SecurityFilterChain,根据 URL 匹配不同的安全策略

  5. 核心 Filter 执行顺序:

graph TD
    A[请求] --> B[SecurityContextPersistenceFilter]
    B --> C[UsernamePasswordAuthenticationFilter]
    C --> D[JwtAuthenticationFilter 自定义]
    D --> E[ExceptionTranslationFilter]
    E --> F[FilterSecurityInterceptor]
    F --> G[DispatcherServlet]
    G --> H[Controller]
  1. 和 Spring MVC 的关系:
  2. Security Filter 在 DispatcherServlet 之前执行
  3. 先鉴权,鉴权失败直接返回,不进入 Spring MVC 流程
  4. 鉴权成功后才路由到 Controller

  5. JWT 场景:自定义 JwtAuthenticationFilter 插入到 UsernamePasswordAuthenticationFilter 之前:

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) {
        String token = extractToken(request);
        if (token != null) {
            Authentication auth = jwtService.parseToken(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }
}

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http.addFilterBefore(jwtAuthenticationFilter(),
                           UsernamePasswordAuthenticationFilter.class);
        // ...
    }
}
  1. Agent 场景:API Key 认证 + JWT Token 双重认证,不同接口不同安全策略:
  2. 公开接口:无需认证
  3. Agent 调用接口:API Key 认证
  4. 用户操作接口:JWT Token 认证

3️⃣ Key Differences

查看内嵌表格


DispatcherServlet 深度解析

1、基础题:DispatcherServlet 的初始化流程?onRefresh() 初始化了哪 9 个核心组件?

难度级别:⭐⭐(DispatcherServlet 生命周期、9 个核心组件)

Answer

DispatcherServlet 继承自 FrameworkServlet,初始化流程:

Servlet 容器启动 → init() → HttpServletBean.init()
  → FrameworkServlet.initServletBean() → initWebApplicationContext()
  → DispatcherServlet.onRefresh() → 初始化 9 个核心组件

onRefresh() 初始化的 9 个核心组件:

  1. MultipartResolver:文件上传解析器

  2. LocaleResolver:国际化解析器

  3. ThemeResolver:主题解析器

  4. HandlerMapping:请求映射处理器(URL → Controller)

  5. HandlerAdapter:处理器适配器(执行 Controller 方法)

  6. HandlerExceptionResolver:异常解析器

  7. RequestToViewNameTranslator:视图名转换器

  8. ViewResolver:视图解析器

  9. FlashMapManager:Flash 属性管理器(Redirect 参数传递)


2、进阶题:HandlerMapping 的匹配优先级?RequestMappingHandlerMapping vs BeanNameUrlHandlerMapping 的区别?

难度级别:⭐⭐⭐(HandlerMapping 类型、匹配顺序、路由策略)

1️⃣ Common Answer

HandlerMapping 有好几种,RequestMappingHandlerMapping 用 @RequestMapping 注解,BeanNameUrlHandlerMapping 用 Bean 名称。匹配顺序好像是按顺序来的,具体不太清楚。

2️⃣ Impressive Answer

我从 3 个维度分析:

  1. HandlerMapping 类型与优先级: Spring MVC 默认按以下顺序匹配:

  2. RequestMappingHandlerMapping(最高优先级):基于 @RequestMapping/@GetMapping 等注解

  3. BeanNameUrlHandlerMapping:基于 Bean 名称(/controller → beanName="/controller")

  4. RouterFunctionMapping:Spring 5+ 的函数式路由

  5. SimpleUrlHandlerMapping:基于 URL 映射配置

匹配逻辑:遍历所有 HandlerMapping,找到第一个返回非 null Handler 的即停止。

  1. RequestMappingHandlerMapping vs BeanNameUrlHandlerMapping:

查看内嵌表格

  1. Agent 场景:自定义 HandlerMapping 实现版本路由
@Component
public class VersionedHandlerMapping extends RequestMappingHandlerMapping {

    @Override
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        String version = request.getHeader("X-Agent-Version");
        if (version == null) {
            version = "v1"; // 默认版本
        }

        // 根据版本路由到不同 Controller
        String path = request.getRequestURI();
        if (path.startsWith("/api/agent/")) {
            path = path.replace("/api/agent/", "/api/agent/" + version + "/");
        }

        return super.getHandlerInternal(new VersionedRequestWrapper(request, path));
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 平台需要根据请求头动态路由到不同版本的 Controller,如何自定义 HandlerMapping?

难度级别:⭐⭐⭐(自定义 HandlerMapping、版本路由、动态匹配)

1️⃣ Common Answer

可以用 @RequestMapping 的 params 属性,或者写个拦截器判断版本。不太清楚怎么自定义 HandlerMapping。

2️⃣ Impressive Answer

我给出完整的实现方案:

  1. 方案一:自定义 HandlerMapping(推荐)
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class VersionRoutingHandlerMapping extends RequestMappingHandlerMapping {

    private static final String VERSION_HEADER = "X-Agent-Version";
    private static final String DEFAULT_VERSION = "v1";

    @Override
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        String version = request.getHeader(VERSION_HEADER);
        if (version == null) {
            version = DEFAULT_VERSION;
        }

        // 包装请求,修改路径
        String originalPath = request.getRequestURI();
        String versionedPath = injectVersion(originalPath, version);

        return super.getHandlerInternal(new VersionedRequestWrapper(request, versionedPath));
    }

    private String injectVersion(String path, String version) {
        // /api/agent/chat → /api/agent/v1/chat
        return path.replaceFirst("/api/agent/", "/api/agent/" + version + "/");
    }
}

// 请求包装器
public class VersionedRequestWrapper extends HttpServletRequestWrapper {
    private final String versionedPath;

    public VersionedRequestWrapper(HttpServletRequest request, String versionedPath) {
        super(request);
        this.versionedPath = versionedPath;
    }

    @Override
    public String getRequestURI() {
        return versionedPath;
    }

    @Override
    public String getServletPath() {
        return versionedPath;
    }
}

// 不同版本的 Controller
@RestController
@RequestMapping("/api/agent/v1/chat")
public class AgentChatV1Controller {
    @PostMapping
    public String chat(@RequestBody ChatRequest request) {
        return "V1: " + request.getMessage();
    }
}

@RestController
@RequestMapping("/api/agent/v2/chat")
public class AgentChatV2Controller {
    @PostMapping
    public String chat(@RequestBody ChatRequest request) {
        return "V2 (Enhanced): " + request.getMessage();
    }
}
  1. 方案二:基于条件的路由(@Conditional)
@RestController
@RequestMapping("/api/agent/chat")
public class AgentChatController {

    @PostMapping
    @ConditionalOnHeader(name = "X-Agent-Version", value = "v1")
    public String chatV1(@RequestBody ChatRequest request) {
        return "V1: " + request.getMessage();
    }

    @PostMapping
    @ConditionalOnHeader(name = "X-Agent-Version", value = "v2")
    public String chatV2(@RequestBody ChatRequest request) {
        return "V2: " + request.getMessage();
    }
}
  1. Agent 场景实践:
  2. 灰度发布:新版本逐步放量,通过请求头控制流量
  3. A/B 测试:不同用户群体使用不同版本
  4. 回滚机制:遇到问题时快速切换回旧版本

3️⃣ Key Differences

查看内嵌表格



返回值处理与内容协商

1、基础题:@ResponseBody 和 @RestController 的区别?ResponseEntity 的使用场景?

难度级别:⭐⭐(注解区别、ResponseEntity)

Answer

@ResponseBody vs @RestController:

  • @ResponseBody:标注在方法上,表示返回值直接写入响应体(不经过视图解析器)

  • @RestController:标注在类上,相当于 @Controller + @ResponseBody 的组合

ResponseEntity 使用场景:

ResponseEntity 是 HttpEntity 的子类,可以完整控制 HTTP 响应:

// 设置状态码、响应头、响应体
@GetMapping("/agent/{id}")
public ResponseEntity<Agent> getAgent(@PathVariable Long id) {
    Agent agent = agentService.findById(id);
    if (agent == null) {
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok()
        .header("X-Custom-Header", "value")
        .body(agent);
}

// 自定义状态码
@PostMapping("/agent")
public ResponseEntity<Agent> createAgent(@RequestBody Agent agent) {
    Agent created = agentService.create(agent);
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

2、进阶题:Spring MVC 内容协商(Content Negotiation)机制?如何根据 Accept 头返回不同格式?

难度级别:⭐⭐⭐(ContentNegotiation、Accept 头、多格式响应)

1️⃣ Common Answer

Content Negotiation 就是根据请求的 Accept 头返回不同格式,比如 JSON 或 XML。好像要配置 HttpMessageConverter。

2️⃣ Impressive Answer

我从 4 个维度解析:

  1. 内容协商策略: Spring MVC 支持多种协商策略:

  2. Accept 头:Accept: application/json → 返回 JSON

  3. URL 后缀:/agent.json → 返回 JSON

  4. 请求参数:/agent?format=json → 返回 JSON

  5. 固定策略:忽略客户端,强制返回指定格式

  6. 配置内容协商:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            // 启用 Accept 头协商
            .favorParameter(false)
            // 启用 URL 后缀协商(/agent.json)
            .favorPathExtension(true)
            // 忽略 Accept 头,强制返回 JSON
            // .ignoreAcceptHeader(true)
            // 默认内容类型
            .defaultContentType(MediaType.APPLICATION_JSON)
            // 指定后缀映射
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("protobuf", MediaType.parseMediaType("application/x-protobuf"));
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加 Protobuf 转换器
        converters.add(new ProtobufHttpMessageConverter());
    }
}
  1. 多格式响应示例:
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    // 根据 Accept 头返回 JSON 或 XML
    @GetMapping("/{id}")
    public Agent getAgent(@PathVariable Long id) {
        return agentService.findById(id);
        // Accept: application/json → JSON
        // Accept: application/xml → XML
    }

    // ResponseEntity 精确控制
    @GetMapping("/{id}/detail")
    public ResponseEntity<Agent> getAgentDetail(@PathVariable Long id) {
        Agent agent = agentService.findById(id);
        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(agent);
    }
}
  1. Agent 场景:多协议支持
// Agent 平台同时支持 JSON 和 Protobuf
@GetMapping("/agent/tools")
public ResponseEntity<List<Tool>> listTools(
    @RequestHeader("Accept") String acceptHeader) {

    List<Tool> tools = toolService.getAll();

    if (acceptHeader.contains("application/x-protobuf")) {
        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType("application/x-protobuf"))
            .body(tools);
    }

    return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_JSON)
        .body(tools);
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 接口需要同时支持 JSON 和 Protobuf 响应格式,如何配置 HttpMessageConverter?

难度级别:⭐⭐⭐(HttpMessageConverter、Protobuf、多协议)

1️⃣ Common Answer

添加 Protobuf 的依赖,然后配置转换器。具体怎么配不太清楚,好像要实现 HttpMessageConverter 接口。

2️⃣ Impressive Answer

我给出完整的多协议支持方案:

  1. 依赖配置:
<!-- pom.xml -->
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.21.0</version>
</dependency>
  1. Protobuf 消息定义:
// agent.proto
syntax = "proto3";

message AgentResponse {
    string id = 1;
    string name = 2;
    repeated string tools = 3;
}
  1. 配置 HttpMessageConverter:
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 保留默认转换器
        converters.add(new MappingJackson2HttpMessageConverter());

        // 添加 Protobuf 转换器
        ProtobufHttpMessageConverter protobufConverter = new ProtobufHttpMessageConverter();
        converters.add(protobufConverter);
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true)
            .parameterName("format")
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("protobuf", MediaType.parseMediaType("application/x-protobuf"));
    }
}
  1. Controller 实现:
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    // 方案一:根据 Accept 头自动选择
    @GetMapping("/{id}")
    public ResponseEntity<Agent> getAgent(@PathVariable Long id) {
        Agent agent = agentService.findById(id);
        // Accept: application/json → JSON
        // Accept: application/x-protobuf → Protobuf
        return ResponseEntity.ok(agent);
    }

    // 方案二:手动指定格式
    @GetMapping("/{id}/json")
    public ResponseEntity<Agent> getAgentJson(@PathVariable Long id) {
        Agent agent = agentService.findById(id);
        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(agent);
    }

    @GetMapping("/{id}/protobuf")
    public ResponseEntity<AgentResponse> getAgentProtobuf(@PathVariable Long id) {
        Agent agent = agentService.findById(id);

        // 转换为 Protobuf 消息
        AgentResponse.Builder builder = AgentResponse.newBuilder()
            .setId(agent.getId())
            .setName(agent.getName());
        agent.getTools().forEach(builder::addTools);

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType("application/x-protobuf"))
            .body(builder.build());
    }

    // 方案三:统一接口,内部转换
    @GetMapping("/{id}/unified")
    public ResponseEntity<?> getAgentUnified(
        @PathVariable Long id,
        @RequestHeader("Accept") String accept) {

        Agent agent = agentService.findById(id);

        if (accept.contains("application/x-protobuf")) {
            AgentResponse protobuf = convertToProtobuf(agent);
            return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType("application/x-protobuf"))
                .body(protobuf);
        }

        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(agent);
    }

    private AgentResponse convertToProtobuf(Agent agent) {
        AgentResponse.Builder builder = AgentResponse.newBuilder()
            .setId(agent.getId())
            .setName(agent.getName());
        agent.getTools().forEach(builder::addTools);
        return builder.build();
    }
}
  1. Agent 场景实践:
  2. 性能优化:Protobuf 比 JSON 小 30-50%,序列化快 5-10 倍
  3. 兼容性:内部服务用 Protobuf,外部 API 用 JSON
  4. 渐进式迁移:逐步将关键接口迁移到 Protobuf

3️⃣ Key Differences

查看内嵌表格


异步请求处理

1、基础题:DeferredResult 和 Callable 的区别?异步请求的线程模型是什么?

难度级别:⭐⭐(异步处理、线程模型)

Answer

DeferredResult vs Callable:

  • Callable:简单异步,返回 Callable,Spring 自动在独立线程执行

  • DeferredResult:灵活异步,手动设置结果,支持回调、超时、错误处理

异步请求的线程模型:

Tomcat 线程(IO 线程)
  ↓ 接收请求,释放线程
  ↓
任务线程(业务线程池)
  ↓ 执行耗时任务
  ↓
Tomcat 线程(复用)
  ↓ 写回响应

示例:

// Callable 方式
@GetMapping("/async/callable")
public Callable<String> asyncCallable() {
    return () -> {
        Thread.sleep(3000); // 耗时任务
        return "Hello from Callable";
    };
}

// DeferredResult 方式
@GetMapping("/async/deferred")
public DeferredResult<String> asyncDeferred() {
    DeferredResult<String> result = new DeferredResult<>(5000L); // 5秒超时

    // 在另一个线程设置结果
    new Thread(() -> {
        try {
            Thread.sleep(3000);
            result.setResult("Hello from DeferredResult");
        } catch (Exception e) {
            result.setErrorResult(e);
        }
    }).start();

    return result;
}

2、进阶题:WebAsyncManager 的工作原理?异步请求的超时和错误处理机制?

难度级别:⭐⭐⭐(WebAsyncManager、超时处理、错误处理)

1️⃣ Common Answer

WebAsyncManager 是管理异步请求的,超时了就返回错误。错误处理可以用 @ExceptionHandler。

2️⃣ Impressive Answer

我从 4 个维度解析:

  1. WebAsyncManager 工作原理: WebAsyncManager 是 Spring MVC 异步处理的核心组件:

  2. 初始化:在 HandlerAdapter 中检测到 Callable/DeferredResult 时创建

  3. 启动异步:调用 startAsyncProcessing(),将请求标记为异步

  4. 超时管理:注册超时回调,超时后调用 onTimeout()

  5. 结果设置:通过 setResult() 或 setErrorResult() 设置结果

  6. 请求分发:异步完成后,重新分发请求到 DispatcherServlet

  7. 异步请求处理流程:

请求 → DispatcherServlet
  → HandlerAdapter
  → 检测到 Callable/DeferredResult
  → WebAsyncManager.startAsyncProcessing()
  → 释放 Tomcat 线程
  → 任务线程执行
  → WebAsyncManager.setResult()
  → 重新分发请求
  → DispatcherServlet
  → 返回响应
  1. 超时和错误处理:
@RestController
@RequestMapping("/api/agent/async")
public class AgentAsyncController {

    // 超时处理
    @GetMapping("/timeout")
    public DeferredResult<String> asyncWithTimeout() {
        DeferredResult<String> result = new DeferredResult<>(3000L); // 3秒超时

        // 超时回调
        result.onTimeout(() -> {
            result.setErrorResult("Request timeout");
        });

        // 错误回调
        result.onError((Throwable t) -> {
            result.setErrorResult("Error: " + t.getMessage());
        });

        // 完成回调
        result.onCompletion(() -> {
            log.info("Async request completed");
        });

        // 异步执行
        CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(5000); // 超过超时时间
                return "Success";
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).whenComplete((res, ex) -> {
            if (ex != null) {
                result.setErrorResult(ex);
            } else {
                result.setResult(res);
            }
        });

        return result;
    }

    // 全局异常处理
    @ExceptionHandler
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    public String handleTimeout(AsyncRequestTimeoutException ex) {
        return "Async request timeout: " + ex.getMessage();
    }
}
  1. Agent 场景:长时间运行任务
@Service
public class AgentTaskService {

    @Async("agentTaskExecutor")
    public CompletableFuture<String> executeLongRunningTask(String taskId) {
        // Agent 深度搜索、多步推理等长时间任务
        try {
            // 模拟长时间任务
            Thread.sleep(10000);
            return CompletableFuture.completedFuture("Task " + taskId + " completed");
        } catch (InterruptedException e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

@RestController
@RequestMapping("/api/agent/task")
public class AgentTaskController {

    @PostMapping("/{taskId}")
    public DeferredResult<TaskResult> executeTask(@PathVariable String taskId) {
        DeferredResult<TaskResult> result = new DeferredResult<>(30000L); // 30秒超时

        result.onTimeout(() -> {
            result.setErrorResult(TaskResult.error("Task timeout"));
        });

        agentTaskService.executeLongRunningTask(taskId)
            .thenAccept(res -> {
                result.setResult(TaskResult.success(res));
            })
            .exceptionally(ex -> {
                result.setErrorResult(TaskResult.error(ex.getMessage()));
                return null;
            });

        return result;
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 长时间运行的任务(如深度搜索、多步推理)如何用 DeferredResult 实现异步响应,避免 Tomcat 线程耗尽?

难度级别:⭐⭐⭐⭐(异步架构、线程池优化、Agent 场景)

1️⃣ Common Answer

用 DeferredResult 就可以,或者用 @Async。线程池配置大一点,避免线程不够用。

2️⃣ Impressive Answer

我给出完整的异步架构方案:

  1. 异步线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("agentTaskExecutor")
    public Executor agentTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("agent-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Bean("agentCallbackExecutor")
    public Executor agentCallbackExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("agent-callback-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}
  1. Agent 任务服务:
@Service
public class AgentTaskService {

    @Autowired
    @Qualifier("agentTaskExecutor")
    private Executor taskExecutor;

    // 深度搜索任务
    public CompletableFuture<SearchResult> deepSearch(String query) {
        return CompletableFuture.supplyAsync(() -> {
            // 1. 向量检索
            List<Document> docs = vectorSearch(query);

            // 2. 多步推理
            String reasoning = multiStepReasoning(query, docs);

            // 3. 结果汇总
            return SearchResult.builder()
                .query(query)
                .documents(docs)
                .reasoning(reasoning)
                .build();
        }, taskExecutor);
    }

    // 多步推理任务
    public CompletableFuture<ReasoningResult> multiStepReasoning(String query, List<Document> docs) {
        return CompletableFuture.supplyAsync(() -> {
            // Step 1: 理解问题
            String understanding = llmService.understand(query);

            // Step 2: 规划步骤
            List<String> steps = llmService.plan(understanding);

            // Step 3: 执行步骤
            List<String> results = new ArrayList<>();
            for (String step : steps) {
                String result = executeStep(step, docs);
                results.add(result);
            }

            // Step 4: 综合答案
            String answer = llmService.synthesize(results);

            return ReasoningResult.builder()
                .steps(steps)
                .results(results)
                .answer(answer)
                .build();
        }, taskExecutor);
    }
}
  1. 异步 Controller:
@RestController
@RequestMapping("/api/agent")
public class AgentAsyncController {

    @Autowired
    private AgentTaskService agentTaskService;

    // 深度搜索(异步)
    @PostMapping("/search/deep")
    public DeferredResult<SearchResult> deepSearch(@RequestBody SearchRequest request) {
        DeferredResult<SearchResult> result = new DeferredResult<>(60000L); // 60秒超时

        // 超时处理
        result.onTimeout(() -> {
            result.setErrorResult(SearchResult.error("Search timeout"));
        });

        // 错误处理
        result.onError(ex -> {
            result.setErrorResult(SearchResult.error(ex.getMessage()));
        });

        // 异步执行
        agentTaskService.deepSearch(request.getQuery())
            .thenAccept(result::setResult)
            .exceptionally(ex -> {
                result.setErrorResult(SearchResult.error(ex.getMessage()));
                return null;
            });

        return result;
    }

    // 多步推理(异步)
    @PostMapping("/reasoning")
    public DeferredResult<ReasoningResult> reasoning(@RequestBody ReasoningRequest request) {
        DeferredResult<ReasoningResult> result = new DeferredResult<>(90000L); // 90秒超时

        result.onTimeout(() -> {
            result.setErrorResult(ReasoningResult.error("Reasoning timeout"));
        });

        agentTaskService.multiStepReasoning(request.getQuery(), request.getDocs())
            .thenAccept(result::setResult)
            .exceptionally(ex -> {
                result.setErrorResult(ReasoningResult.error(ex.getMessage()));
                return null;
            });

        return result;
    }

    // 任务状态查询(轮询)
    @GetMapping("/task/{taskId}/status")
    public TaskStatus getTaskStatus(@PathVariable String taskId) {
        return agentTaskService.getTaskStatus(taskId);
    }
}
  1. 任务状态管理:
@Service
public class AgentTaskService {

    private final Map<String, TaskStatus> taskStatusMap = new ConcurrentHashMap<>();

    public String createTaskId() {
        return UUID.randomUUID().toString();
    }

    public void updateTaskStatus(String taskId, String status, int progress) {
        taskStatusMap.put(taskId, TaskStatus.builder()
            .taskId(taskId)
            .status(status)
            .progress(progress)
            .timestamp(System.currentTimeMillis())
            .build());
    }

    public TaskStatus getTaskStatus(String taskId) {
        return taskStatusMap.get(taskId);
    }
}
  1. Tomcat 线程池优化:
# application.yml
server:
  tomcat:
    threads:
      max: 200
      min-spare: 20
    accept-count: 100
    connection-timeout: 30000

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


文件上传与下载

1、基础题:Spring MVC 如何处理文件上传?MultipartFile 的核心方法?

难度级别:⭐⭐(文件上传、MultipartFile)

Answer

文件上传配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(10 * 1024 * 1024); // 10MB
        resolver.setMaxInMemorySize(1024 * 1024); // 1MB
        resolver.setDefaultEncoding("UTF-8");
        return resolver;
    }
}

MultipartFile 核心方法:

@RestController
@RequestMapping("/api/file")
public class FileUploadController {

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        // 获取文件名
        String originalFilename = file.getOriginalFilename();

        // 获取文件类型
        String contentType = file.getContentType();

        // 获取文件大小
        long size = file.getSize();

        // 获取文件字节数组
        byte[] bytes = file.getBytes();

        // 获取输入流
        InputStream inputStream = file.getInputStream();

        // 保存文件
        File dest = new File("/tmp/" + originalFilename);
        file.transferTo(dest);

        return "Upload success: " + originalFilename;
    }
}

2、进阶题:大文件上传的分片处理方案?MultipartResolver 的两种实现(CommonsMultipartResolver vs StandardServletMultipartResolver)?

难度级别:⭐⭐⭐(分片上传、MultipartResolver 对比)

1️⃣ Common Answer

大文件可以分片上传,前端切成小块,后端合并。MultipartResolver 有两种,Commons 和 Standard,区别不太清楚。

2️⃣ Impressive Answer

我从 3 个维度解析:

  1. MultipartResolver 对比:

查看内嵌表格

  1. CommonsMultipartResolver 配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(100 * 1024 * 1024); // 100MB
        resolver.setMaxUploadSizePerFile(10 * 1024 * 1024); // 单文件 10MB
        resolver.setMaxInMemorySize(1024 * 1024); // 1MB 内存缓冲
        resolver.setDefaultEncoding("UTF-8");
        return resolver;
    }
}
  1. StandardServletMultipartResolver 配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public MultipartResolver multipartResolver() {
        return new StandardServletMultipartResolver();
    }
}

// application.yml
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 100MB
      file-size-threshold: 1MB
  1. 分片上传方案:
@RestController
@RequestMapping("/api/file/chunk")
public class ChunkUploadController {

    // 上传分片
    @PostMapping("/upload")
    public ResponseEntity<ChunkUploadResult> uploadChunk(
        @RequestParam("file") MultipartFile file,
        @RequestParam("chunkNumber") int chunkNumber,
        @RequestParam("totalChunks") int totalChunks,
        @RequestParam("identifier") String identifier) throws IOException {

        String chunkPath = "/tmp/chunks/" + identifier + "/" + chunkNumber;
        File chunkFile = new File(chunkPath);
        chunkFile.getParentFile().mkdirs();
        file.transferTo(chunkFile);

        return ResponseEntity.ok(ChunkUploadResult.builder()
            .chunkNumber(chunkNumber)
            .identifier(identifier)
            .uploaded(true)
            .build());
    }

    // 合并分片
    @PostMapping("/merge")
    public ResponseEntity<MergeResult> mergeChunks(
        @RequestParam("identifier") String identifier,
        @RequestParam("filename") String filename,
        @RequestParam("totalChunks") int totalChunks) throws IOException {

        String outputPath = "/tmp/uploads/" + filename;
        File outputFile = new File(outputPath);
        outputFile.getParentFile().mkdirs();

        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            for (int i = 1; i <= totalChunks; i++) {
                File chunkFile = new File("/tmp/chunks/" + identifier + "/" + i);
                Files.copy(chunkFile.toPath(), fos);
                chunkFile.delete(); // 删除分片
            }
        }

        return ResponseEntity.ok(MergeResult.builder()
            .filename(filename)
            .path(outputPath)
            .success(true)
            .build());
    }
}
  1. Agent 场景:知识库文档上传
@Service
public class AgentDocumentService {

    @Async("docUploadExecutor")
    public CompletableFuture<UploadResult> uploadAndIndex(
        MultipartFile file,
        String agentId) {

        try {
            // 1. 保存文件
            String filePath = saveFile(file);

            // 2. 提取文本
            String text = extractText(filePath);

            // 3. 向量化
            List<Float> embedding = embeddingService.embed(text);

            // 4. 入库
            documentService.index(Document.builder()
                .agentId(agentId)
                .filename(file.getOriginalFilename())
                .content(text)
                .embedding(embedding)
                .build());

            return CompletableFuture.completedFuture(
                UploadResult.success(file.getOriginalFilename()));

        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 知识库文档上传(PDF/Word)如何实现流式解析,边上传边向量化入库?

难度级别:⭐⭐⭐⭐(流式处理、向量化、Agent 场景)

1️⃣ Common Answer

上传后解析 PDF/Word,提取文本,然后调用向量化接口入库。可以用 Apache POI 解析 Word,PDFBox 解析 PDF。

2️⃣ Impressive Answer

我给出完整的流式处理方案:

  1. 流式上传 Controller:
@RestController
@RequestMapping("/api/agent/kb")
public class KnowledgeBaseController {

    @Autowired
    private AgentDocumentService documentService;

    // 流式上传并处理
    @PostMapping("/upload/stream")
    public DeferredResult<UploadResult> uploadStream(
        @RequestParam("file") MultipartFile file,
        @RequestParam("agentId") String agentId) {

        DeferredResult<UploadResult> result = new DeferredResult<>(60000L);

        result.onTimeout(() -> {
            result.setErrorResult(UploadResult.error("Upload timeout"));
        });

        // 异步流式处理
        documentService.processStream(file.getInputStream(), agentId, file.getOriginalFilename())
            .thenAccept(result::setResult)
            .exceptionally(ex -> {
                result.setErrorResult(UploadResult.error(ex.getMessage()));
                return null;
            });

        return result;
    }
}
  1. 流式文档处理服务:
@Service
public class AgentDocumentService {

    @Autowired
    private EmbeddingService embeddingService;

    @Autowired
    private VectorStore vectorStore;

    @Async("docProcessExecutor")
    public CompletableFuture<UploadResult> processStream(
        InputStream inputStream,
        String agentId,
        String filename) {

        try {
            // 1. 检测文件类型
            String fileType = detectFileType(filename);

            // 2. 流式提取文本
            List<TextChunk> chunks = new ArrayList<>();
            TextExtractor extractor = getExtractor(fileType);

            extractor.extractStream(inputStream, (text, pageIndex) -> {
                // 3. 分块处理
                List<TextChunk> pageChunks = chunkText(text, 500, 50);
                chunks.addAll(pageChunks);

                // 4. 边提取边向量化
                for (TextChunk chunk : pageChunks) {
                    processChunk(chunk, agentId, filename, pageIndex);
                }
            });

            return CompletableFuture.completedFuture(
                UploadResult.success(filename, chunks.size()));

        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }

    private void processChunk(TextChunk chunk, String agentId, String filename, int pageIndex) {
        // 1. 向量化
        List<Float> embedding = embeddingService.embed(chunk.getText());

        // 2. 入库
        vectorStore.insert(Document.builder()
            .agentId(agentId)
            .filename(filename)
            .pageIndex(pageIndex)
            .chunkIndex(chunk.getIndex())
            .content(chunk.getText())
            .embedding(embedding)
            .metadata(Map.of(
                "filename", filename,
                "pageIndex", String.valueOf(pageIndex),
                "chunkIndex", String.valueOf(chunk.getIndex())
            ))
            .build());
    }
}
  1. 文本提取器接口:
public interface TextExtractor {
    void extractStream(InputStream inputStream, BiConsumer<String, Integer> callback) throws IOException;
}

@Component
public class PdfTextExtractor implements TextExtractor {

    @Override
    public void extractStream(InputStream inputStream, BiConsumer<String, Integer> callback) throws IOException {
        try (PDDocument document = PDDocument.load(inputStream)) {
            PDFTextStripper stripper = new PDFTextStripper();

            for (int i = 0; i < document.getNumberOfPages(); i++) {
                stripper.setStartPage(i + 1);
                stripper.setEndPage(i + 1);

                String text = stripper.getText(document);
                callback.accept(text.trim(), i);
            }
        }
    }
}

@Component
public class WordTextExtractor implements TextExtractor {

    @Override
    public void extractStream(InputStream inputStream, BiConsumer<String, Integer> callback) throws IOException {
        XWPFDocument document = new XWPFDocument(inputStream);

        for (int i = 0; i < document.getParagraphs().size(); i++) {
            XWPFParagraph paragraph = document.getParagraphs().get(i);
            String text = paragraph.getText();

            // 按段落分页(模拟)
            int pageIndex = i / 10;
            callback.accept(text, pageIndex);
        }

        document.close();
    }
}
  1. 文本分块工具:
@Component
public class TextChunker {

    public List<TextChunk> chunkText(String text, int chunkSize, int overlap) {
        List<TextChunk> chunks = new ArrayList<>();

        String[] sentences = text.split("(?<=[.!?。!?])\\s+");
        StringBuilder currentChunk = new StringBuilder();
        int chunkIndex = 0;

        for (String sentence : sentences) {
            if (currentChunk.length() + sentence.length() > chunkSize) {
                if (currentChunk.length() > 0) {
                    chunks.add(TextChunk.builder()
                        .index(chunkIndex++)
                        .text(currentChunk.toString())
                        .build());

                    // 保留重叠部分
                    String overlapText = currentChunk.substring(
                        Math.max(0, currentChunk.length() - overlap));
                    currentChunk = new StringBuilder(overlapText);
                }
            }

            currentChunk.append(sentence).append(" ");
        }

        if (currentChunk.length() > 0) {
            chunks.add(TextChunk.builder()
                .index(chunkIndex)
                .text(currentChunk.toString().trim())
                .build());
        }

        return chunks;
    }
}
  1. 异步线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("docProcessExecutor")
    public Executor docProcessExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("doc-process-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


WebMvcConfigurer 扩展点体系

1、基础题:WebMvcConfigurer 有哪些常用扩展点?各自的作用?

难度级别:⭐⭐(WebMvcConfigurer、扩展点)

Answer

WebMvcConfigurer 常用扩展点:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 1. 路径匹配配置
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseTrailingSlashMatch(true);
    }

    // 2. 内容协商配置
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true);
    }

    // 3. 消息转换器配置
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }

    // 4. 拦截器配置
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor())
            .addPathPatterns("/**");
    }

    // 5. 跨域配置
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("*");
    }

    // 6. 视图控制器配置
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }

    // 7. 静态资源配置
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/");
    }

    // 8. 参数解析器配置
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AgentContextResolver());
    }
}

2、进阶题:@EnableWebMvc 和 Spring Boot 自动配置的冲突问题?WebMvcAutoConfiguration 的加载条件?

难度级别:⭐⭐⭐(自动配置、加载条件、冲突处理)

1️⃣ Common Answer

@EnableWebMvc 会禁用 Spring Boot 的自动配置,好像是因为它会覆盖默认配置。WebMvcAutoConfiguration 有一些条件注解。

2️⃣ Impressive Answer

我从 4 个维度解析:

  1. @EnableWebMvc 的影响:
  2. 作用:启用 Spring MVC 的 Java Config 模式
  3. 副作用:禁用 WebMvcAutoConfiguration 的默认配置
  4. 结果:需要手动配置所有组件(拦截器、转换器等)

  5. WebMvcAutoConfiguration 加载条件:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) // 关键条件
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    // ...
}

关键条件:@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

  • 当容器中存在 WebMvcConfigurationSupport Bean 时,WebMvcAutoConfiguration 不加载

  • @EnableWebMvc 会导入 DelegatingWebMvcConfiguration(继承自 WebMvcConfigurationSupport)

  • 因此,使用 @EnableWebMvc 会禁用自动配置

  • 三种配置方式对比:

查看内嵌表格

  1. 推荐配置方式:
// ❌ 不推荐:禁用自动配置
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    // 需要手动配置所有组件
}

// ✅ 推荐:保留自动配置,扩展自定义配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
    // 只配置需要自定义的部分
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AgentInterceptor())
            .addPathPatterns("/api/agent/**");
    }
}
  1. Agent 场景:统一配置 Agent 平台组件
@Configuration
public class AgentWebConfig implements WebMvcConfigurer {

    @Autowired
    private AgentProperties agentProperties;

    // 统一注册 Agent 参数解析器
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AgentContextResolver());
        resolvers.add(new AgentTokenResolver());
        resolvers.add(new AgentVersionResolver());
    }

    // 统一注册 Agent 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AgentAuthInterceptor())
            .addPathPatterns("/api/agent/**")
            .excludePathPatterns("/api/agent/public/**");

        registry.addInterceptor(new AgentLoggingInterceptor())
            .addPathPatterns("/api/agent/**");

        registry.addInterceptor(new AgentRateLimitInterceptor())
            .addPathPatterns("/api/agent/**");
    }

    // 统一注册 Agent 消息转换器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // Protobuf 转换器
        converters.add(0, new ProtobufHttpMessageConverter());

        // 自定义 JSON 转换器(Agent 特定格式)
        converters.add(1, new AgentJsonHttpMessageConverter());
    }

    // 统一配置跨域
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/agent/**")
            .allowedOrigins(agentProperties.getAllowedOrigins())
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 平台如何通过 WebMvcConfigurer 统一注册所有工具的参数解析器、拦截器和消息转换器?

难度级别:⭐⭐⭐⭐(统一配置、Agent 平台、扩展点组合)

1️⃣ Common Answer

在 WebMvcConfigurer 里分别添加参数解析器、拦截器和消息转换器。可以用循环注册多个。

2️⃣ Impressive Answer

我给出完整的统一配置方案:

  1. Agent 组件自动注册机制:
// Agent 组件接口
public interface AgentComponent {
    String getName();
    int getOrder();
}

// Agent 参数解析器接口
public interface AgentArgumentResolver extends HandlerMethodArgumentResolver, AgentComponent {
}

// Agent 拦截器接口
public interface AgentInterceptor extends HandlerInterceptor, AgentComponent {
}

// Agent 消息转换器接口
public interface AgentMessageConverter extends HttpMessageConverter<?>, AgentComponent {
}
  1. 组件自动发现配置:
@Configuration
public class AgentComponentAutoConfig {

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public List<AgentArgumentResolver> agentArgumentResolvers() {
        return applicationContext.getBeansOfType(AgentArgumentResolver.class)
            .values()
            .stream()
            .sorted(Comparator.comparingInt(AgentComponent::getOrder))
            .collect(Collectors.toList());
    }

    @Bean
    public List<AgentInterceptor> agentInterceptors() {
        return applicationContext.getBeansOfType(AgentInterceptor.class)
            .values()
            .stream()
            .sorted(Comparator.comparingInt(AgentComponent::getOrder))
            .collect(Collectors.toList());
    }

    @Bean
    public List<AgentMessageConverter> agentMessageConverters() {
        return applicationContext.getBeansOfType(AgentMessageConverter.class)
            .values()
            .stream()
            .sorted(Comparator.comparingInt(AgentComponent::getOrder))
            .collect(Collectors.toList());
    }
}
  1. 统一 WebMvcConfigurer:
@Configuration
public class AgentWebConfig implements WebMvcConfigurer {

    @Autowired
    private List<AgentArgumentResolver> agentArgumentResolvers;

    @Autowired
    private List<AgentInterceptor> agentInterceptors;

    @Autowired
    private List<AgentMessageConverter> agentMessageConverters;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        log.info("Registering {} Agent argument resolvers", agentArgumentResolvers.size());
        agentArgumentResolvers.forEach(resolver -> {
            log.info("  - {}", resolver.getName());
            resolvers.add(resolver);
        });
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("Registering {} Agent interceptors", agentInterceptors.size());
        agentInterceptors.forEach(interceptor -> {
            log.info("  - {}", interceptor.getName());
            registry.addInterceptor(interceptor)
                .addPathPatterns("/api/agent/**");
        });
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("Registering {} Agent message converters", agentMessageConverters.size());
        agentMessageConverters.forEach(converter -> {
            log.info("  - {}", converter.getName());
            converters.add(0, converter); // 添加到开头,优先级最高
        });
    }
}
  1. 具体组件实现:
// Agent 上下文解析器
@Component
@Slf4j
public class AgentContextResolver implements AgentArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(AgentContext.class) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        String agentId = webRequest.getHeader("X-Agent-Id");
        String agentVersion = webRequest.getHeader("X-Agent-Version");

        return AgentContext.builder()
            .agentId(agentId)
            .version(agentVersion)
            .timestamp(System.currentTimeMillis())
            .build();
    }

    @Override
    public String getName() {
        return "AgentContextResolver";
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

// Agent 认证拦截器
@Component
@Slf4j
public class AgentAuthInterceptor implements AgentInterceptor {

    @Autowired
    private AgentAuthService authService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("X-Agent-Token");

        if (!authService.validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }

    @Override
    public String getName() {
        return "AgentAuthInterceptor";
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

// Agent Protobuf 消息转换器
@Component
@Slf4j
public class AgentProtobufConverter implements AgentMessageConverter {

    private final ProtobufHttpMessageConverter delegate = new ProtobufHttpMessageConverter();

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return Message.class.isAssignableFrom(clazz) &&
               delegate.canRead(clazz, mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return Message.class.isAssignableFrom(clazz) &&
               delegate.canWrite(clazz, mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return delegate.getSupportedMediaTypes();
    }

    @Override
    public Object read(Class<?> clazz, HttpInputMessage inputMessage) throws IOException {
        return delegate.read(clazz, inputMessage);
    }

    @Override
    public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException {
        delegate.write(t, contentType, outputMessage);
    }

    @Override
    public String getName() {
        return "AgentProtobufConverter";
    }

    @Override
    public int getOrder() {
        return 100;
    }
}
  1. 使用示例:
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    // 自动注入 AgentContext
    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request, @AgentContext AgentContext context) {
        log.info("Agent ID: {}, Version: {}", context.getAgentId(), context.getVersion());
        return agentService.chat(request, context);
    }
}

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


Spring MVC 异常处理体系

1、基础题:Spring MVC 的异常解析器链有哪些?执行顺序?

难度级别:⭐⭐(异常解析器、执行顺序)

Answer

Spring MVC 异常解析器链(按执行顺序):

  1. ExceptionHandlerExceptionResolver:处理 @ExceptionHandler 注解的方法

  2. ResponseStatusExceptionResolver:处理 @ResponseStatus 注解

  3. DefaultHandlerExceptionResolver:处理 Spring MVC 内置异常

  4. 自定义 HandlerExceptionResolver:自定义异常解析器

执行顺序:

异常发生
  → ExceptionHandlerExceptionResolver
  → ResponseStatusExceptionResolver
  → DefaultHandlerExceptionResolver
  → 自定义 HandlerExceptionResolver
  → 如果都未处理,抛出给 Servlet 容器

2、进阶题:DefaultHandlerExceptionResolver 处理哪些内置异常?如何自定义 HandlerExceptionResolver?

难度级别:⭐⭐⭐(内置异常、自定义异常解析器)

1️⃣ Common Answer

DefaultHandlerExceptionResolver 处理一些 Spring 的异常,比如 404、405 之类的。自定义异常解析器要实现 HandlerExceptionResolver 接口。

2️⃣ Impressive Answer

我从 3 个维度解析:

  1. DefaultHandlerExceptionResolver 处理的内置异常:

查看内嵌表格

  1. 自定义 HandlerExceptionResolver:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AgentExceptionHandlerResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {

        // Agent 工具调用异常
        if (ex instanceof AgentToolCallException) {
            AgentToolCallException toolEx = (AgentToolCallException) ex;
            return buildErrorResponse(response,
                HttpStatus.SERVICE_UNAVAILABLE,
                toolEx.getCode(),
                toolEx.getMessage());
        }

        // LLM 限流异常
        if (ex instanceof AgentRateLimitException) {
            AgentRateLimitException rateEx = (AgentRateLimitException) ex;
            response.setHeader("X-RateLimit-Limit", String.valueOf(rateEx.getLimit()));
            response.setHeader("X-RateLimit-Remaining", "0");
            response.setHeader("X-RateLimit-Reset", String.valueOf(rateEx.getResetTime()));

            return buildErrorResponse(response,
                HttpStatus.TOO_MANY_REQUESTS,
                rateEx.getCode(),
                rateEx.getMessage());
        }

        // 参数校验异常
        if (ex instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException validEx = (MethodArgumentNotValidException) ex;
            String errorMessage = validEx.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining(", "));

            return buildErrorResponse(response,
                HttpStatus.BAD_REQUEST,
                "VALIDATION_ERROR",
                errorMessage);
        }

        // 其他异常不处理,交给下一个解析器
        return null;
    }

    private ModelAndView buildErrorResponse(HttpServletResponse response,
                                           HttpStatus status,
                                           String code,
                                           String message) {
        try {
            response.setStatus(status.value());
            response.setContentType("application/json;charset=UTF-8");

            ErrorResponse errorResponse = ErrorResponse.builder()
                .code(code)
                .message(message)
                .timestamp(System.currentTimeMillis())
                .build();

            response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));

            return new ModelAndView();
        } catch (IOException e) {
            return null;
        }
    }
}
  1. @ExceptionHandler 全局异常处理:
@ControllerAdvice
@Slf4j
public class AgentGlobalExceptionHandler {

    // Agent 工具调用超时
    @ExceptionHandler(AgentToolTimeoutException.class)
    public ResponseEntity<ErrorResponse> handleToolTimeout(AgentToolTimeoutException ex) {
        log.error("Agent tool timeout: {}", ex.getMessage(), ex);

        return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
            .body(ErrorResponse.builder()
                .code("TOOL_TIMEOUT")
                .message(ex.getMessage())
                .timestamp(System.currentTimeMillis())
                .build());
    }

    // LLM 限流
    @ExceptionHandler(AgentRateLimitException.class)
    public ResponseEntity<ErrorResponse> handleRateLimit(AgentRateLimitException ex) {
        log.warn("Agent rate limit: {}", ex.getMessage());

        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .header("X-RateLimit-Limit", String.valueOf(ex.getLimit()))
            .header("X-RateLimit-Remaining", "0")
            .header("X-RateLimit-Reset", String.valueOf(ex.getResetTime()))
            .body(ErrorResponse.builder()
                .code("RATE_LIMIT_EXCEEDED")
                .message(ex.getMessage())
                .timestamp(System.currentTimeMillis())
                .build());
    }

    // 参数校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String errorMessage = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining(", "));

        return ResponseEntity.badRequest()
            .body(ErrorResponse.builder()
                .code("VALIDATION_ERROR")
                .message(errorMessage)
                .timestamp(System.currentTimeMillis())
                .build());
    }

    // 通用异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        log.error("Unexpected error: {}", ex.getMessage(), ex);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.builder()
                .code("INTERNAL_ERROR")
                .message("An unexpected error occurred")
                .timestamp(System.currentTimeMillis())
                .build());
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 工具调用超时、LLM 限流(429)、参数校验失败,如何设计分层异常体系和统一错误码规范?

难度级别:⭐⭐⭐⭐(分层异常、错误码规范、Agent 场景)

1️⃣ Common Answer

定义几个异常类,用 @ExceptionHandler 处理。错误码用枚举定义,统一返回格式。

2️⃣ Impressive Answer

我给出完整的分层异常体系设计:

  1. 异常分层架构:
// 基础异常
public abstract class AgentBaseException extends RuntimeException {
    private final String code;
    private final int httpStatus;

    public AgentBaseException(String code, String message, int httpStatus) {
        super(message);
        this.code = code;
        this.httpStatus = httpStatus;
    }

    public String getCode() {
        return code;
    }

    public int getHttpStatus() {
        return httpStatus;
    }
}

// 工具调用异常(5xx)
public class AgentToolException extends AgentBaseException {
    public AgentToolException(String code, String message) {
        super(code, message, HttpStatus.INTERNAL_SERVER_ERROR.value());
    }
}

// 工具调用超时
public class AgentToolTimeoutException extends AgentToolException {
    private final String toolName;
    private final long timeout;

    public AgentToolTimeoutException(String toolName, long timeout) {
        super("TOOL_TIMEOUT",
              String.format("Tool %s timeout after %dms", toolName, timeout),
              HttpStatus.GATEWAY_TIMEOUT.value());
        this.toolName = toolName;
        this.timeout = timeout;
    }
}

// LLM 限流异常(429)
public class AgentRateLimitException extends AgentBaseException {
    private final int limit;
    private final long resetTime;

    public AgentRateLimitException(int limit, long resetTime) {
        super("RATE_LIMIT_EXCEEDED",
              "Rate limit exceeded, please retry later",
              HttpStatus.TOO_MANY_REQUESTS.value());
        this.limit = limit;
        this.resetTime = resetTime;
    }

    public int getLimit() {
        return limit;
    }

    public long getResetTime() {
        return resetTime;
    }
}

// 参数校验异常(4xx)
public class AgentValidationException extends AgentBaseException {
    private final Map<String, String> fieldErrors;

    public AgentValidationException(Map<String, String> fieldErrors) {
        super("VALIDATION_ERROR", "Parameter validation failed",
              HttpStatus.BAD_REQUEST.value());
        this.fieldErrors = fieldErrors;
    }

    public Map<String, String> getFieldErrors() {
        return fieldErrors;
    }
}

// 业务逻辑异常(4xx)
public class AgentBusinessException extends AgentBaseException {
    public AgentBusinessException(String code, String message) {
        super(code, message, HttpStatus.BAD_REQUEST.value());
    }
}
  1. 统一错误码规范:
public enum AgentErrorCode {
    // 通用错误码(1000-1999)
    SUCCESS("0000", "Success"),
    INTERNAL_ERROR("1000", "Internal server error"),
    INVALID_REQUEST("1001", "Invalid request"),
    VALIDATION_ERROR("1002", "Parameter validation failed"),

    // Agent 工具错误码(2000-2999)
    TOOL_TIMEOUT("2000", "Tool execution timeout"),
    TOOL_ERROR("2001", "Tool execution error"),
    TOOL_NOT_FOUND("2002", "Tool not found"),
    TOOL_DISABLED("2003", "Tool is disabled"),

    // LLM 错误码(3000-3999)
    LLM_RATE_LIMIT("3000", "LLM rate limit exceeded"),
    LLM_QUOTA_EXCEEDED("3001", "LLM quota exceeded"),
    LLM_ERROR("3002", "LLM processing error"),
    LLM_TIMEOUT("3003", "LLM request timeout"),

    // 知识库错误码(4000-4999)
    KB_NOT_FOUND("4000", "Knowledge base not found"),
    KB_INDEX_ERROR("4001", "Knowledge base index error"),
    KB_SEARCH_ERROR("4002", "Knowledge base search error");

    private final String code;
    private final String message;

    AgentErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}
  1. 统一响应格式:
@Data
@Builder
public class ApiResponse<T> {
    private String code;
    private String message;
    private T data;
    private long timestamp;

    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
            .code(AgentErrorCode.SUCCESS.getCode())
            .message(AgentErrorCode.SUCCESS.getMessage())
            .data(data)
            .timestamp(System.currentTimeMillis())
            .build();
    }

    public static <T> ApiResponse<T> error(AgentErrorCode errorCode) {
        return ApiResponse.<T>builder()
            .code(errorCode.getCode())
            .message(errorCode.getMessage())
            .timestamp(System.currentTimeMillis())
            .build();
    }

    public static <T> ApiResponse<T> error(String code, String message) {
        return ApiResponse.<T>builder()
            .code(code)
            .message(message)
            .timestamp(System.currentTimeMillis())
            .build();
    }
}
  1. 全局异常处理器:
@ControllerAdvice
@Slf4j
public class AgentGlobalExceptionHandler {

    // 工具调用超时
    @ExceptionHandler(AgentToolTimeoutException.class)
    public ResponseEntity<ApiResponse<Void>> handleToolTimeout(AgentToolTimeoutException ex) {
        log.error("Tool timeout: tool={}, timeout={}ms", ex.getToolName(), ex.getTimeout(), ex);

        return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
            .body(ApiResponse.error(AgentErrorCode.TOOL_TIMEOUT));
    }

    // LLM 限流
    @ExceptionHandler(AgentRateLimitException.class)
    public ResponseEntity<ApiResponse<Void>> handleRateLimit(AgentRateLimitException ex) {
        log.warn("Rate limit: limit={}, resetTime={}", ex.getLimit(), ex.getResetTime());

        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .header("X-RateLimit-Limit", String.valueOf(ex.getLimit()))
            .header("X-RateLimit-Remaining", "0")
            .header("X-RateLimit-Reset", String.valueOf(ex.getResetTime()))
            .body(ApiResponse.error(AgentErrorCode.LLM_RATE_LIMIT));
    }

    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidation(
        MethodArgumentNotValidException ex) {

        Map<String, String> fieldErrors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (existing, replacement) -> existing
            ));

        return ResponseEntity.badRequest()
            .body(ApiResponse.<Map<String, String>>builder()
                .code(AgentErrorCode.VALIDATION_ERROR.getCode())
                .message(AgentErrorCode.VALIDATION_ERROR.getMessage())
                .data(fieldErrors)
                .timestamp(System.currentTimeMillis())
                .build());
    }

    // Agent 业务异常
    @ExceptionHandler(AgentBusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusiness(AgentBusinessException ex) {
        log.warn("Business error: code={}, message={}", ex.getCode(), ex.getMessage());

        return ResponseEntity.badRequest()
            .body(ApiResponse.error(ex.getCode(), ex.getMessage()));
    }

    // 通用异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
        log.error("Unexpected error", ex);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error(AgentErrorCode.INTERNAL_ERROR));
    }
}
  1. 使用示例:
@Service
public class AgentToolService {

    public String callTool(String toolName, Map<String, Object> params) {
        // 检查工具是否存在
        if (!toolRegistry.exists(toolName)) {
            throw new AgentBusinessException(
                AgentErrorCode.TOOL_NOT_FOUND.getCode(),
                String.format("Tool %s not found", toolName));
        }

        // 检查工具是否启用
        if (!toolRegistry.isEnabled(toolName)) {
            throw new AgentBusinessException(
                AgentErrorCode.TOOL_DISABLED.getCode(),
                String.format("Tool %s is disabled", toolName));
        }

        try {
            // 执行工具调用(带超时)
            return CompletableFuture.supplyAsync(() -> {
                return toolExecutor.execute(toolName, params);
            }).get(30, TimeUnit.SECONDS);

        } catch (TimeoutException e) {
            throw new AgentToolTimeoutException(toolName, 30000);
        } catch (Exception e) {
            throw new AgentToolException(
                AgentErrorCode.TOOL_ERROR.getCode(),
                String.format("Tool execution error: %s", e.getMessage()));
        }
    }

    public String callLLM(String prompt) {
        try {
            return llmService.generate(prompt);
        } catch (RateLimitException e) {
            throw new AgentRateLimitException(e.getLimit(), e.getResetTime());
        }
    }
}

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


Spring MVC 性能优化

1、基础题:Spring MVC 的静态资源缓存如何配置?ResourceHandler 的作用?

难度级别:⭐⭐(静态资源、缓存配置)

Answer

静态资源配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(3600); // 缓存 1 小时
    }
}

ResourceHandler 作用:

  • 映射 URL 路径到静态资源位置

  • 配置缓存策略

  • 支持版本号控制(防止缓存问题)


2、进阶题:Tomcat 线程池参数(maxThreads、minSpareThreads、acceptCount)如何调优?连接器(Connector)的 NIO vs NIO2 区别?

难度级别:⭐⭐⭐(线程池调优、NIO 模型)

1️⃣ Common Answer

Tomcat 线程池参数调大一点,maxThreads 设置 200,acceptCount 设置 100。NIO 比 NIO2 快,具体区别不太清楚。

2️⃣ Impressive Answer

我从 4 个维度解析:

  1. Tomcat 线程池参数:

查看内嵌表格

  1. 线程池调优策略:
# application.yml
server:
  tomcat:
    threads:
      max: 500
      min-spare: 50
      max-queue: 200
    accept-count: 200
    max-connections: 10000
    connection-timeout: 30000

调优公式:

  • maxThreads = CPU 核数 × (1 + 等待时间 / 计算时间)

  • minSpareThreads = maxThreads × 10%

  • acceptCount = maxThreads × 40%

  • NIO vs NIO2 区别:

查看内嵌表格

  1. 性能优化实践:
@Configuration
public class TomcatConfig {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> {
            // 自定义连接器
            Connector connector = new Connector("org.apache.coyote.http11.Http11Nio2Protocol");
            connector.setPort(8080);
            connector.setMaxThreads(500);
            connector.setMinSpareThreads(50);
            connector.setAcceptCount(200);
            connector.setMaxConnections(10000);
            connector.setConnectionTimeout(30000);

            // 启用压缩
            connector.setProperty("compression", "on");
            connector.setProperty("compressionMinSize", "1024");
            connector.setProperty("compressableMimeType",
                "text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json");

            factory.addAdditionalTomcatConnectors(connector);
        };
    }
}
  1. Agent 场景:高并发 Agent 调用优化
@Configuration
public class AgentPerformanceConfig {

    // Tomcat 线程池优化
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> {
            factory.addConnectorCustomizers(connector -> {
                Http11Nio2Protocol protocol = (Http11Nio2Protocol) connector.getProtocolHandler();
                protocol.setMaxThreads(500);
                protocol.setMinSpareThreads(50);
                protocol.setAcceptCount(200);
                protocol.setMaxConnections(10000);
            });
        };
    }

    // 异步任务线程池
    @Bean("agentTaskExecutor")
    public Executor agentTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("agent-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    // 连接池优化
    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory =
            new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(30000);
        factory.setConnectionRequestTimeout(5000);

        // 连接池配置
        PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(200);
        connectionManager.setDefaultMaxPerRoute(50);

        CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .build();

        factory.setHttpClient(httpClient);
        return new RestTemplate(factory);
    }
}

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 高并发场景下(1000+ QPS),如何从线程池、连接数、序列化三个维度优化 Spring MVC 性能?

难度级别:⭐⭐⭐⭐(性能优化、高并发、Agent 场景)

1️⃣ Common Answer

线程池调大一点,连接数也调大。序列化用 Protobuf 比 JSON 快。

2️⃣ Impressive Answer

我给出完整的三维优化方案:

  1. 线程池优化:
@Configuration
public class ThreadPoolOptimizationConfig {

    // Tomcat 线程池(IO 线程)
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> {
            factory.addConnectorCustomizers(connector -> {
                Http11Nio2Protocol protocol = (Http11Nio2Protocol) connector.getProtocolHandler();

                // 根据 CPU 核数计算
                int cpuCores = Runtime.getRuntime().availableProcessors();
                int maxThreads = cpuCores * 100; // 8核 → 800线程

                protocol.setMaxThreads(maxThreads);
                protocol.setMinSpareThreads(maxThreads / 10); // 80线程
                protocol.setAcceptCount(maxThreads / 4); // 200队列
                protocol.setMaxConnections(10000);
                protocol.setConnectionTimeout(30000);
            });
        };
    }

    // Agent 任务线程池(业务线程)
    @Bean("agentTaskExecutor")
    public Executor agentTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        int cpuCores = Runtime.getRuntime().availableProcessors();
        int corePoolSize = cpuCores * 10; // 8核 → 80线程
        int maxPoolSize = cpuCores * 50; // 8核 → 400线程

        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("agent-task-");
        executor.setKeepAliveSeconds(60);
        executor.setAllowCoreThreadTimeOut(true);

        // 拒绝策略:调用者运行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }

    // LLM 调用线程池(IO 密集型)
    @Bean("llmExecutor")
    public Executor llmExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // IO 密集型:更多线程
        int cpuCores = Runtime.getRuntime().availableProcessors();
        int maxPoolSize = cpuCores * 200; // 8核 → 1600线程

        executor.setCorePoolSize(maxPoolSize / 2);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(2000);
        executor.setThreadNamePrefix("llm-call-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        executor.initialize();
        return executor;
    }
}
  1. 连接数优化:
@Configuration
public class ConnectionOptimizationConfig {

    // HTTP 客户端连接池
    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory =
            new HttpComponentsClientHttpRequestFactory();

        // 超时配置
        factory.setConnectTimeout(3000); // 连接超时 3秒
        factory.setReadTimeout(30000); // 读取超时 30秒
        factory.setConnectionRequestTimeout(1000); // 获取连接超时 1秒

        // 连接池配置
        PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(500); // 最大连接数
        connectionManager.setDefaultMaxPerRoute(100); // 单路由最大连接数
        connectionManager.setValidateAfterInactivity(30000); // 30秒后验证连接

        CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setKeepAliveStrategy((response, context) -> 60000) // 60秒 Keep-Alive
            .evictIdleConnections(30000, TimeUnit.SECONDS) // 30秒清理空闲连接
            .build();

        factory.setHttpClient(httpClient);
        return new RestTemplate(factory);
    }

    // 数据库连接池(HikariCP)
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    // Redis 连接池(Lettuce)
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory();

        LettuceClientConfiguration config = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(5))
            .shutdownTimeout(Duration.ZERO)
            .poolConfig(new GenericObjectPoolConfig<>() {{
                setMaxTotal(200);
                setMaxIdle(50);
                setMinIdle(10);
                setTestOnBorrow(true);
                setTestWhileIdle(true);
            }})
            .build();

        factory.setClientConfiguration(config);
        return factory;
    }
}
  1. 序列化优化:
@Configuration
public class SerializationOptimizationConfig implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 1. Protobuf 转换器(优先级最高)
        ProtobufHttpMessageConverter protobufConverter = new ProtobufHttpMessageConverter();
        converters.add(0, protobufConverter);

        // 2. 优化后的 JSON 转换器
        MappingJackson2HttpMessageConverter jsonConverter =
            new MappingJackson2HttpMessageConverter();

        ObjectMapper objectMapper = new ObjectMapper();

        // 性能优化配置
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.registerModule(new JavaTimeModule());

        // 使用 FastJson2(可选)
        // objectMapper = new com.alibaba.fastjson2.JSONMapper();

        jsonConverter.setObjectMapper(objectMapper);
        converters.add(1, jsonConverter);
    }

    // Kryo 序列化(用于缓存)
    @Bean
    public SerializationPair<Object> kryoSerializationPair() {
        return SerializationPair.fromSerializer(new KryoSerializer());
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new KryoRedisSerializer<>(Object.class)
                )
            );
    }
}
  1. 性能监控:
@Configuration
public class PerformanceMonitorConfig {

    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config()
            .commonTags("application", "agent-platform");
    }

    @Bean
    public TomcatMetricsBinder tomcatMetricsBinder() {
        return new TomcatMetricsBinder();
    }

    @Bean
    public ExecutorServiceMetrics executorServiceMetrics(
        @Qualifier("agentTaskExecutor") Executor executor) {
        return new ExecutorServiceMetrics(
            (ThreadPoolTaskExecutor) executor,
            "agent.task.executor",
            Collections.emptyList()
        );
    }
}
  1. Agent 场景优化总结:

查看内嵌表格

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


4.5 WebFlux 响应式编程

1、基础题:Spring WebFlux 和 Spring MVC 的核心区别是什么?什么场景下选 WebFlux?

难度级别:⭐⭐(编程模型、线程模型、适用场景)

1️⃣ Common Answer

WebFlux 是响应式的,Spring MVC 是传统的。WebFlux 用少量线程处理高并发,Spring MVC 每个请求一个线程。高并发场景选 WebFlux。

2️⃣ Impressive Answer

  1. 核心区别:
  2. 编程模型:WebFlux 基于 Reactor(Mono/Flux),异步非阻塞;MVC 基于 Servlet API,同步阻塞。
  3. 线程模型:WebFlux 少量线程(事件循环)处理大量连接;MVC 线程池模型,每个请求占用一个线程。
  4. 容器支持:WebFlux 支持 Netty、Undertow、Servlet 3.1+;MVC 仅支持 Servlet 容器。
  5. 生态成熟度:MVC 生态成熟,WebFlux 相对较新。

  6. 适用场景:

  7. WebFlux:高并发 I/O 密集型(SSE、WebSocket、API 网关)、流式处理、低延迟要求。
  8. Spring MVC:传统 CRUD、业务逻辑复杂、阻塞操作多(JDBC、文件上传)。

  9. 性能对比:

  10. WebFlux:高并发下吞吐量高,内存占用低,但编程复杂度高。
  11. Spring MVC:简单场景性能足够,开发效率高,生态丰富。

  12. Agent 场景:SSE 流式输出 LLM 响应、大量并发工具调用、实时事件推送,适合 WebFlux。

3️⃣ Key Differences

查看内嵌表格


2、进阶题:Reactor 的 Mono 和 Flux 是什么?背压(Backpressure)机制如何工作?

难度级别:⭐⭐⭐(Reactor 核心、背压机制、流式处理)

1️⃣ Common Answer

Mono 是处理 0-1 个元素的,Flux 是处理 0-N 个元素的。背压就是控制数据流的速度,防止消费者处理不过来。

2️⃣ Impressive Answer

  1. Mono 和 Flux:
  2. Mono:Mono<T> 表示 0 或 1 个元素的异步序列,类似 Optional<T> + 异步。
  3. Flux:Flux<T> 表示 0 到 N 个元素的异步序列,类似 Stream<T> + 异步。
  4. 操作符:map(转换)、filter(过滤)、flatMap(异步转换)、zip(组合)、buffer(批处理)。

  5. 背压机制:

  6. 问题:生产者产生数据速度快于消费者处理速度,导致 OOM。
  7. 解决:消费者主动告诉生产者需要多少数据(request(n))。
  8. 策略:BUFFER(缓冲)、DROP(丢弃)、LATEST(最新)、ERROR(抛异常)、IGNORE(忽略)。

  9. 背压示例:

// Flux 背压示例
Flux.range(1, 1000)
    .log()
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnSubscribe(Subscription subscription) {
            request(10); // 初始请求 10 个元素
        }

        @Override
        protected void hookOnNext(Integer value) {
            try {
                Thread.sleep(100); // 模拟慢速处理
            } catch (InterruptedException e) {
                // ignore
            }
            request(1); // 每处理完一个再请求一个
        }
    });
  1. Agent 场景:
  2. Mono:LLM 单次调用返回结果。
  3. Flux:SSE 流式输出 LLM Token。
  4. 背压:控制 SSE 推送速度,避免客户端处理不过来。

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 服务需要实现 SSE 流式输出 LLM 响应,如何用 WebFlux 实现?

难度级别:⭐⭐⭐(SSE、Flux 流式处理、异常处理)

1️⃣ Common Answer

可以用 WebFlux 的 Flux 返回 SSE 事件。在 Controller 里返回 Flux,用 MediaType.TEXTEVENTSTREAM_VALUE。LLM 调用返回流,然后转成 Flux。

2️⃣ Impressive Answer

  1. Controller 实现:
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    @Autowired
    private LLMService llmService;

    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> chatStream(@RequestParam String prompt) {
        return llmService.streamChat(prompt)
            .map(token -> ServerSentEvent.<String>builder()
                .data(token)
                .build())
            .doOnComplete(() -> log.info("Stream completed"))
            .doOnError(e -> log.error("Stream error", e));
    }
}
  1. LLM Service 实现:
@Service
public class LLMService {

    public Flux<String> streamChat(String prompt) {
        return Flux.create(sink -> {
            try {
                // 调用 LLM API 流式接口
                CompletionStreamRequest request = CompletionStreamRequest.builder()
                    .model("gpt-4")
                    .messages(List.of(Message.user(prompt)))
                    .build();

                client.stream(request)
                    .onErrorResume(e -> {
                        sink.error(e);
                        return Mono.empty();
                    })
                    .subscribe(token -> sink.next(token),
                        e -> sink.error(e),
                        () -> sink.complete());

            } catch (Exception e) {
                sink.error(e);
            }
        });
    }
}
  1. 异常处理和重试:
public Flux<String> streamChatWithRetry(String prompt) {
    return streamChat(prompt)
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
            .filter(e -> e instanceof TimeoutException)
            .doBeforeRetry(signal -> log.warn("Retrying...")))
        .onErrorResume(e -> {
            log.error("All retries failed", e);
            return Flux.just("[ERROR: " + e.getMessage() + "]");
        });
}
  1. 前端连接:
const eventSource = new EventSource('/api/agent/chat/stream?prompt=hello');
eventSource.onmessage = (event) => {
    console.log(event.data);
};
eventSource.onerror = (error) => {
    console.error('SSE error:', error);
};
  1. 优势:WebFlux 的非阻塞模型可以同时处理大量 SSE 连接,不会因为长连接占满线程池。

3️⃣ Key Differences

查看内嵌表格