跳转至

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 会使用 HttpMessageConverterUser 转成 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.preControllerInterceptor.postInterceptor.afterFilter后。 所以 Filter 是最外层的“壳”,Interceptor 是更贴近业务的一层。

🔹 选择建议

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

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

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


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

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

🔹 工作原理剖析

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

  2. DispatcherServlet 捕获:DispatcherServletprocessHandlerException() 方法开始遍历所有注册的 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. MonoMono<T> 表示 0 或 1 个元素的异步序列,类似 Optional<T> + 异步。
  3. FluxFlux<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

查看内嵌表格