Spring MVC 与 WebFlux
Spring MVC 请求处理流程¶
Spring MVC 处理一个 HTTP 请求的完整流程¶
🔹 一句话概括
请求先到前端控制器 DispatcherServlet,然后它调度各种组件,最终返回响应。理解这个流程,就掌握了 Spring MVC 的精髓。
🔹 文字流程图(带图标说明)
用户请求
↓
① 🎯 DispatcherServlet(前端控制器)
| 是整个流程的“总指挥”,接收请求并分派。
↓
② 🗺️ HandlerMapping(处理器映射器)
| 根据请求 URL、方法等找到对应的处理器 Handler(即控制器方法)。
↓
③ ⚙️ HandlerAdapter(处理器适配器)
| 负责真正调用 Handler。因为 Handler 有多种形式(@Controller、HttpRequestHandler 等),
| 通过适配器统一调用,并处理参数绑定、返回值处理等。
↓
④ 🔧 Handler(开发者编写的 Controller 方法)
| 执行业务逻辑,通常会返回一个 ModelAndView(传统)或直接用 @ResponseBody 返回对象。
↓
⑤ 🔁 如果有拦截器(Interceptor)的 postHandle,视图渲染前执行。
↓
⑥ 🖼️ ViewResolver(视图解析器) / HttpMessageConverter(消息转换器)
| 如果返回视图名,则 ViewResolver 解析为具体 View 模板(如 JSP、Thymeleaf);
| 如果是 @ResponseBody 或 REST 接口,则通过消息转换器(如 MappingJackson2HttpMessageConverter)把对象转为 JSON。
↓
⑦ 📄 视图渲染 / 响应体输出
| 将模型数据填充到视图,生成 HTML;或者直接输出 JSON/XML 等。
↓
⑧ 🧹 拦截器的 afterCompletion 执行(视图渲染后,用于资源清理)。
↓
响应返回给用户
🔹 示例代码:体验 @ResponseBody 如何走消息转换器
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 模拟查询用户
User user = new User(id, "老周", "laozhou@example.com");
return user; // 因为没有视图名,默认走 @ResponseBody -> JSON
}
}
当请求 /users/1 时,DispatcherServlet 通过 HandlerMapping 找到 getUser 方法,HandlerAdapter 执行方法并得到 User 对象。因为没有返回视图,Spring MVC 会使用 HttpMessageConverter 将 User 转成 JSON 写入响应体。
🔹 面试加分小贴士
可以顺便提一下:在前后端分离时代,ViewResolver 环节经常会被弱化,核心变成了消息转换器。另外,流程中还会穿插拦截器、异常解析器等,体现了 Spring MVC 的可扩展性。
📌 这样梳理下来,是不是比死记硬背清晰多了?下次可以尝试画一画,面试时连画带讲,效果拔群。
拦截器(Interceptor)和过滤器(Filter)的区别¶
很多同学傻傻分不清,本质上是因为它们处于不同层级。我们用一张对比表 + 代码感受一下。
🔹 核心区别对比表
🔹 代码对比:实现与执行顺序
过滤器示例:
@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("Filter: 前处理");
chain.doFilter(request, response); // 必须显式调用才能继续
System.out.println("Filter: 后处理");
}
}
拦截器示例:
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
System.out.println("Interceptor: preHandle");
// 可以强转 handler 为 HandlerMethod 拿到方法信息
return true; // true 放行,false 中断
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
System.out.println("Interceptor: postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
System.out.println("Interceptor: afterCompletion");
}
}
配置拦截器(Spring Boot 中):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/api/**"); // 只拦截 API 路径
}
}
如果同时有 Filter 和 Interceptor,执行顺序通常是:
Filter前 → Interceptor.pre → Controller → Interceptor.post → Interceptor.after → Filter后。
所以 Filter 是最外层的“壳”,Interceptor 是更贴近业务的一层。
🔹 选择建议
-
如果只是做编码、过滤参数、跨域,用 Filter 更合适,和框架无关。
-
如果要拿到控制器方法、参数,或者要统计哪个接口耗时最长,用 Interceptor 更方便。
💡 掌握了这些区别,面试时再从实际场景举例,就能把“背诵”变成“理解”了。
@ControllerAdvice + @ExceptionHandler 的原理与统一错误响应设计¶
这个组合是构建健壮 API 的基石,面试必考原理和落地。
🔹 工作原理剖析
-
异常发生点:控制器方法抛出异常(如
RuntimeException、自定义BusinessException)。 -
DispatcherServlet 捕获:
DispatcherServlet的processHandlerException()方法开始遍历所有注册的HandlerExceptionResolver。 -
ExceptionHandlerExceptionResolver 登场:这是专门处理
@ExceptionHandler的解析器。它会先在本 Controller 中查找标注了@ExceptionHandler的方法,如果没找到,就去所有@ControllerAdvice类中查找。 -
匹配规则:根据异常类型匹配,会按继承关系找最接近的。比如抛出了
UserNotFoundException,会优先匹配声明处理该异常的方法,如果没有则匹配它的父类RuntimeException。 -
执行处理方法并输出:找到匹配的方法后,通过反射调用,返回的
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的格式写入响应体,浏览器端通过EventSourceAPI 接收。 -
记得处理超时和异常,否则 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 到对象的过程¶
-
识别参数:
DispatcherServlet找到对应的HandlerMethod后,遍历参数。发现某个参数标了@RequestBody,就会去找能处理它的解析器。 -
指定解析器:
RequestResponseBodyMethodProcessor就是专门处理@RequestBody和@ResponseBody的。它的supportsParameter会返回true。 -
读取内容类型:解析器获取请求的
Content-Type头,比如application/json。 -
遍历消息转换器:Spring 容器中注册了一系列
HttpMessageConverter(启动时加载)。解析器拿着目标类型(比如User.class)和Content-Type,依次询问每个 Converter 的canRead()。第一个返回 true 的会被使用。 -
执行转换:选中的 Converter 调用
read()方法,从HttpInputMessage读取输入流,将字节序列化为 Java 对象。对 JSON 来说,底层就是 Jackson 的ObjectMapper。 -
数据绑定与验证:得到对象后,如果还有
@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 个维度对比:
- 方案对比:
- 最佳实践:URL 路径版本最常用,配合 Swagger 分版本展示。生产环境建议:
- 版本号放在 URL 路径中(如
/api/v1/) - 新版本并行发布,旧版本设置废弃时间
-
Nginx 根据路径路由到不同服务
-
请求头版本实现:
@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 逻辑 */ }
- 自定义注解实现:继承
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
- 策略模式设计:
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;
}
}
- 统一 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);
}
}
- 生产考虑:
- SSE 模式需要异步处理,避免阻塞线程池
- REST 模式可以同步返回,响应更快
- 超时设置:SSE 设置较长超时(如 2 分钟),REST 设置较短超时(如 30 秒)
- 监控指标:分别统计两种模式的 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
- 自定义校验注解:
@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();
}
}
- 分组校验:
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) { /* 更新逻辑 */ }
- @GroupSequence:控制分组校验顺序,前一组失败则不执行后续组:
- 跨字段校验:类级别注解 + 类级别 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
- 选型对比:
- 混合方案:
- 外层请求:用 Bean Validation(
@Valid),校验通用参数(如 agentId、sessionId) -
工具参数:用 JSON Schema,根据 Function Definition 动态校验
-
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())
);
}
}
- 错误信息设计:校验失败返回结构化错误,告诉 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
- 集成原理:
- Spring Security 本质是一组 Servlet Filter
DelegatingFilterProxy作为 Servlet Filter 的代理,将请求委托给 Spring 容器中的springSecurityFilterChainBean-
FilterChainProxy管理多个SecurityFilterChain,根据 URL 匹配不同的安全策略 -
核心 Filter 执行顺序:
graph TD
A[请求] --> B[SecurityContextPersistenceFilter]
B --> C[UsernamePasswordAuthenticationFilter]
C --> D[JwtAuthenticationFilter 自定义]
D --> E[ExceptionTranslationFilter]
E --> F[FilterSecurityInterceptor]
F --> G[DispatcherServlet]
G --> H[Controller]
- 和 Spring MVC 的关系:
- Security Filter 在
DispatcherServlet之前执行 - 先鉴权,鉴权失败直接返回,不进入 Spring MVC 流程
-
鉴权成功后才路由到 Controller
-
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);
// ...
}
}
- Agent 场景:API Key 认证 + JWT Token 双重认证,不同接口不同安全策略:
- 公开接口:无需认证
- Agent 调用接口:API Key 认证
- 用户操作接口: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 个核心组件:
-
MultipartResolver:文件上传解析器
-
LocaleResolver:国际化解析器
-
ThemeResolver:主题解析器
-
HandlerMapping:请求映射处理器(URL → Controller)
-
HandlerAdapter:处理器适配器(执行 Controller 方法)
-
HandlerExceptionResolver:异常解析器
-
RequestToViewNameTranslator:视图名转换器
-
ViewResolver:视图解析器
-
FlashMapManager:Flash 属性管理器(Redirect 参数传递)
2、进阶题:HandlerMapping 的匹配优先级?RequestMappingHandlerMapping vs BeanNameUrlHandlerMapping 的区别?¶
难度级别:⭐⭐⭐(HandlerMapping 类型、匹配顺序、路由策略)
1️⃣ Common Answer
HandlerMapping 有好几种,RequestMappingHandlerMapping 用 @RequestMapping 注解,BeanNameUrlHandlerMapping 用 Bean 名称。匹配顺序好像是按顺序来的,具体不太清楚。
2️⃣ Impressive Answer
我从 3 个维度分析:
-
HandlerMapping 类型与优先级: Spring MVC 默认按以下顺序匹配:
-
RequestMappingHandlerMapping(最高优先级):基于 @RequestMapping/@GetMapping 等注解
-
BeanNameUrlHandlerMapping:基于 Bean 名称(/controller → beanName="/controller")
-
RouterFunctionMapping:Spring 5+ 的函数式路由
-
SimpleUrlHandlerMapping:基于 URL 映射配置
匹配逻辑:遍历所有 HandlerMapping,找到第一个返回非 null Handler 的即停止。
- RequestMappingHandlerMapping vs BeanNameUrlHandlerMapping:
- 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
我给出完整的实现方案:
- 方案一:自定义 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();
}
}
- 方案二:基于条件的路由(@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();
}
}
- Agent 场景实践:
- 灰度发布:新版本逐步放量,通过请求头控制流量
- A/B 测试:不同用户群体使用不同版本
- 回滚机制:遇到问题时快速切换回旧版本
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 个维度解析:
-
内容协商策略: Spring MVC 支持多种协商策略:
-
Accept 头:
Accept: application/json→ 返回 JSON -
URL 后缀:
/agent.json→ 返回 JSON -
请求参数:
/agent?format=json→ 返回 JSON -
固定策略:忽略客户端,强制返回指定格式
-
配置内容协商:
@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());
}
}
- 多格式响应示例:
@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);
}
}
- 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
我给出完整的多协议支持方案:
- 依赖配置:
<!-- pom.xml -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.21.0</version>
</dependency>
- Protobuf 消息定义:
// agent.proto
syntax = "proto3";
message AgentResponse {
string id = 1;
string name = 2;
repeated string tools = 3;
}
- 配置 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"));
}
}
- 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();
}
}
- Agent 场景实践:
- 性能优化:Protobuf 比 JSON 小 30-50%,序列化快 5-10 倍
- 兼容性:内部服务用 Protobuf,外部 API 用 JSON
- 渐进式迁移:逐步将关键接口迁移到 Protobuf
3️⃣ Key Differences
异步请求处理¶
1、基础题:DeferredResult 和 Callable 的区别?异步请求的线程模型是什么?¶
难度级别:⭐⭐(异步处理、线程模型)
Answer
DeferredResult vs Callable:
-
Callable:简单异步,返回 Callable
,Spring 自动在独立线程执行 -
DeferredResult:灵活异步,手动设置结果,支持回调、超时、错误处理
异步请求的线程模型:
示例:
// 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 个维度解析:
-
WebAsyncManager 工作原理: WebAsyncManager 是 Spring MVC 异步处理的核心组件:
-
初始化:在 HandlerAdapter 中检测到 Callable/DeferredResult 时创建
-
启动异步:调用 startAsyncProcessing(),将请求标记为异步
-
超时管理:注册超时回调,超时后调用 onTimeout()
-
结果设置:通过 setResult() 或 setErrorResult() 设置结果
-
请求分发:异步完成后,重新分发请求到 DispatcherServlet
-
异步请求处理流程:
请求 → DispatcherServlet
→ HandlerAdapter
→ 检测到 Callable/DeferredResult
→ WebAsyncManager.startAsyncProcessing()
→ 释放 Tomcat 线程
→ 任务线程执行
→ WebAsyncManager.setResult()
→ 重新分发请求
→ DispatcherServlet
→ 返回响应
- 超时和错误处理:
@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();
}
}
- 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
我给出完整的异步架构方案:
- 异步线程池配置:
@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;
}
}
- 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);
}
}
- 异步 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);
}
}
- 任务状态管理:
@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);
}
}
- 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 个维度解析:
- MultipartResolver 对比:
- 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;
}
}
- 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
- 分片上传方案:
@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());
}
}
- 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
我给出完整的流式处理方案:
- 流式上传 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;
}
}
- 流式文档处理服务:
@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());
}
}
- 文本提取器接口:
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();
}
}
- 文本分块工具:
@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;
}
}
- 异步线程池配置:
@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 个维度解析:
- @EnableWebMvc 的影响:
- 作用:启用 Spring MVC 的 Java Config 模式
- 副作用:禁用 WebMvcAutoConfiguration 的默认配置
-
结果:需要手动配置所有组件(拦截器、转换器等)
-
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 会禁用自动配置
-
三种配置方式对比:
- 推荐配置方式:
// ❌ 不推荐:禁用自动配置
@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/**");
}
}
- 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
我给出完整的统一配置方案:
- 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 {
}
- 组件自动发现配置:
@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());
}
}
- 统一 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); // 添加到开头,优先级最高
});
}
}
- 具体组件实现:
// 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;
}
}
- 使用示例:
@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 异常解析器链(按执行顺序):
-
ExceptionHandlerExceptionResolver:处理 @ExceptionHandler 注解的方法
-
ResponseStatusExceptionResolver:处理 @ResponseStatus 注解
-
DefaultHandlerExceptionResolver:处理 Spring MVC 内置异常
-
自定义 HandlerExceptionResolver:自定义异常解析器
执行顺序:
异常发生
→ ExceptionHandlerExceptionResolver
→ ResponseStatusExceptionResolver
→ DefaultHandlerExceptionResolver
→ 自定义 HandlerExceptionResolver
→ 如果都未处理,抛出给 Servlet 容器
2、进阶题:DefaultHandlerExceptionResolver 处理哪些内置异常?如何自定义 HandlerExceptionResolver?¶
难度级别:⭐⭐⭐(内置异常、自定义异常解析器)
1️⃣ Common Answer
DefaultHandlerExceptionResolver 处理一些 Spring 的异常,比如 404、405 之类的。自定义异常解析器要实现 HandlerExceptionResolver 接口。
2️⃣ Impressive Answer
我从 3 个维度解析:
- DefaultHandlerExceptionResolver 处理的内置异常:
- 自定义 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;
}
}
}
- @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
我给出完整的分层异常体系设计:
- 异常分层架构:
// 基础异常
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());
}
}
- 统一错误码规范:
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;
}
}
- 统一响应格式:
@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();
}
}
- 全局异常处理器:
@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));
}
}
- 使用示例:
@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 个维度解析:
- Tomcat 线程池参数:
- 线程池调优策略:
# 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 区别:
- 性能优化实践:
@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);
};
}
}
- 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
我给出完整的三维优化方案:
- 线程池优化:
@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;
}
}
- 连接数优化:
@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;
}
}
- 序列化优化:
@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)
)
);
}
}
- 性能监控:
@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()
);
}
}
- 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
- 核心区别:
- 编程模型:WebFlux 基于 Reactor(Mono/Flux),异步非阻塞;MVC 基于 Servlet API,同步阻塞。
- 线程模型:WebFlux 少量线程(事件循环)处理大量连接;MVC 线程池模型,每个请求占用一个线程。
- 容器支持:WebFlux 支持 Netty、Undertow、Servlet 3.1+;MVC 仅支持 Servlet 容器。
-
生态成熟度:MVC 生态成熟,WebFlux 相对较新。
-
适用场景:
- WebFlux:高并发 I/O 密集型(SSE、WebSocket、API 网关)、流式处理、低延迟要求。
-
Spring MVC:传统 CRUD、业务逻辑复杂、阻塞操作多(JDBC、文件上传)。
-
性能对比:
- WebFlux:高并发下吞吐量高,内存占用低,但编程复杂度高。
-
Spring MVC:简单场景性能足够,开发效率高,生态丰富。
-
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
- Mono 和 Flux:
- Mono:
Mono<T>表示 0 或 1 个元素的异步序列,类似Optional<T>+ 异步。 - Flux:
Flux<T>表示 0 到 N 个元素的异步序列,类似Stream<T>+ 异步。 -
操作符:
map(转换)、filter(过滤)、flatMap(异步转换)、zip(组合)、buffer(批处理)。 -
背压机制:
- 问题:生产者产生数据速度快于消费者处理速度,导致 OOM。
- 解决:消费者主动告诉生产者需要多少数据(
request(n))。 -
策略:
BUFFER(缓冲)、DROP(丢弃)、LATEST(最新)、ERROR(抛异常)、IGNORE(忽略)。 -
背压示例:
// 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); // 每处理完一个再请求一个
}
});
- Agent 场景:
- Mono:LLM 单次调用返回结果。
- Flux:SSE 流式输出 LLM Token。
- 背压:控制 SSE 推送速度,避免客户端处理不过来。
3️⃣ Key Differences
3、场景题:Agent 服务需要实现 SSE 流式输出 LLM 响应,如何用 WebFlux 实现?¶
难度级别:⭐⭐⭐(SSE、Flux 流式处理、异常处理)
1️⃣ Common Answer
可以用 WebFlux 的 Flux 返回 SSE 事件。在 Controller 里返回 Flux
2️⃣ Impressive Answer
- 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));
}
}
- 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);
}
});
}
}
- 异常处理和重试:
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() + "]");
});
}
- 前端连接:
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);
};
- 优势:WebFlux 的非阻塞模型可以同时处理大量 SSE 连接,不会因为长连接占满线程池。
3️⃣ Key Differences