Java深入解析篇十六之Spring MVC与WebFlux
本文基于 Spring Framework 6.x / Spring Boot 3.x,深入分析 Spring MVC 请求处理机制与 WebFlux 响应式 Web 开发,配合完整可运行代码示例。
目录
- [Spring MVC架构与请求处理流程](#Spring MVC架构与请求处理流程)
- DispatcherServlet核心组件
- HandlerMapping与HandlerAdapter
- @Controller/@RestController/@RequestMapping
- 参数绑定与数据转换
- 拦截器(HandlerInterceptor)
- 异常处理(@ExceptionHandler/@ControllerAdvice)
- [RESTful API设计](#RESTful API设计)
- 跨域(CORS)处理
- 文件上传下载
- [WebFlux响应式Web(Reactor Netty)](#WebFlux响应式Web(Reactor Netty))
- RouterFunction/HandlerFunction
- [SSE(Server-Sent Events)](#SSE(Server-Sent Events))
- WebSocket
- [MVC vs WebFlux选型](#MVC vs WebFlux选型)
- 最佳实践
一、Spring MVC架构与请求处理流程
1.1 核心设计思想
Spring MVC 基于前端控制器模式(Front Controller) ,所有请求统一由 DispatcherServlet 接收和分发,实现关注点分离。
客户端请求
│
▼
┌─────────────────────────────────────────────────────────┐
│ DispatcherServlet(前端控制器) │
│ │
│ ① HandlerMapping ──→ 查找处理器 │
│ ② HandlerAdapter ──→ 适配调用处理器 │
│ ③ Controller(Handler) ──→ 执行业务逻辑 │
│ ④ ViewResolver ──→ 解析视图 │
│ ⑤ View ──→ 渲染响应 │
└─────────────────────────────────────────────────────────┘
│
▼
客户端响应
1.2 完整请求处理流程(9步)
1. 客户端发送HTTP请求 → DispatcherServlet.doDispatch()
2. DispatcherServlet 调用 HandlerMapping.getHandler() 查找处理器
3. HandlerMapping 返回 HandlerExecutionChain(Handler + 拦截器链)
4. DispatcherServlet 调用 HandlerAdapter.handle() 适配执行
5. HandlerAdapter 解析参数、调用 Controller 方法
6. Controller 返回 ModelAndView(或@ResponseBody直接写入响应)
7. DispatcherServlet 调用 ViewResolver.resolveViewName() 解析视图
8. ViewResolver 返回 View 对象
9. View.render() 渲染模型数据,写入 HttpServletResponse
1.3 DispatcherServlet 源码核心入口
java
// DispatcherServlet.doDispatch() 简化流程
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 检查是否Multipart请求
processedRequest = checkMultipart(request);
// 2. 通过HandlerMapping查找Handler
mappedHandler = getHandler(processedRequest);
// 3. 获取HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 4. 执行拦截器 preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // 返回false则中断
}
// 5. 调用Handler
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 6. 执行拦截器 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 7. 处理结果(渲染视图或处理异常)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
二、DispatcherServlet核心组件
2.1 组件初始化
DispatcherServlet 在 onRefresh() 中初始化九大组件:
java
// DispatcherServlet.onRefresh() 源码
protected void onRefresh(ApplicationContext context) {
initMultipartResolver(context); // 文件上传解析器
initLocaleResolver(context); // 国际化解析器
initThemeResolver(context); // 主题解析器
initHandlerMappings(context); // 处理器映射器(核心)
initHandlerAdapters(context); // 处理器适配器(核心)
initHandlerExceptionResolvers(context); // 异常解析器
initRequestToViewNameTranslator(context);
initViewResolvers(context); // 视图解析器
initFlashMapManager(context);
}
2.2 Spring Boot 自动配置
java
// Spring Boot 中 DispatcherServlet 的自动注册
@Configuration
public class DispatcherServletAutoConfiguration {
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
servlet.setDispatchTraceRequest(false);
return servlet;
}
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistration(
DispatcherServlet dispatcherServlet) {
// 默认映射 "/",处理所有请求
DispatcherServletRegistrationBean registration =
new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registration.setLoadOnStartup(1);
return registration;
}
}
三、HandlerMapping与HandlerAdapter
3.1 HandlerMapping 体系
java
public interface HandlerMapping {
/**
* 根据请求查找Handler及关联的拦截器
*/
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
核心实现 --- RequestMappingHandlerMapping:
java
// 启动时扫描所有@Controller/@RestController,构建映射表
// URL模式 → HandlerMethod(Controller中的具体方法)
// 例如: GET /api/users/{id} → UserController.getUser()
// 路径匹配策略(Spring 6默认PathPattern)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// Spring 6 默认使用 PathPatternParser(性能更优)
configurer.setPatternParser(new PathPatternParser());
// 旧版 AntPathMatcher(兼容)
// configurer.setPathMatcher(new AntPathMatcher());
}
}
3.2 HandlerAdapter 体系
java
public interface HandlerAdapter {
// 是否支持该Handler
boolean supports(Object handler);
// 执行Handler,返回ModelAndView
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception;
}
RequestMappingHandlerAdapter 参数解析链:
java
// 内置的 HandlerMethodArgumentResolver 列表(按优先级)
// 1. RequestParamMethodArgumentResolver → @RequestParam
// 2. PathVariableMethodArgumentResolver → @PathVariable
// 3. RequestResponseBodyMethodProcessor → @RequestBody / @ResponseBody
// 4. RequestHeaderMethodArgumentResolver → @RequestHeader
// 5. ModelAttributeMethodProcessor → @ModelAttribute
// 6. ServletRequestMethodArgumentResolver → HttpServletRequest等原生对象
// 自定义参数解析器
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String token = request.getHeader("Authorization");
// 从token解析用户信息
return parseUserFromToken(token);
}
}
// 注册自定义解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
}
3.3 HttpMessageConverter 消息转换
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 自定义Jackson转换器
MappingJackson2HttpMessageConverter jacksonConverter =
new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// Long类型序列化为String(防止前端精度丢失)
objectMapper.registerModule(new SimpleModule()
.addSerializer(Long.class, ToStringSerializer.instance));
jacksonConverter.setObjectMapper(objectMapper);
converters.add(0, jacksonConverter);
}
}
四、@Controller/@RestController/@RequestMapping
4.1 控制器注解
java
// @Controller:返回视图名(传统MVC)
@Controller
@RequestMapping("/pages")
public class PageController {
@GetMapping("/home")
public String home(Model model) {
model.addAttribute("title", "首页");
return "home"; // 视图名,由ViewResolver解析
}
}
// @RestController:返回JSON/XML(RESTful API)
// 等价于 @Controller + @ResponseBody
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // 直接序列化为JSON
}
}
4.2 @RequestMapping 完整属性
java
@RestController
@RequestMapping(
value = "/api/v1/orders",
method = {RequestMethod.GET, RequestMethod.POST},
params = "version=2", // 必须包含参数 version=2
headers = "X-Api-Key=abc123", // 必须包含指定请求头
consumes = "application/json", // 请求Content-Type
produces = "application/json" // 响应Content-Type
)
public class OrderController {
// 组合注解简化写法
@GetMapping(value = "/{id}", produces = "application/json")
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id);
}
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Order createOrder(@RequestBody @Valid CreateOrderRequest request) {
return orderService.create(request);
}
@PutMapping("/{id}")
public Order updateOrder(@PathVariable Long id, @RequestBody UpdateOrderRequest request) {
return orderService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable Long id) {
orderService.delete(id);
}
@PatchMapping("/{id}/status")
public Order updateStatus(@PathVariable Long id, @RequestParam String status) {
return orderService.updateStatus(id, status);
}
}
五、参数绑定与数据转换
5.1 常用参数绑定注解
java
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
// @RequestParam:查询参数 ?name=xxx&page=1
@GetMapping("/search")
public List<Product> search(
@RequestParam String name,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) BigDecimal minPrice) {
return productService.search(name, page, size, minPrice);
}
// @PathVariable:路径变量 /products/123
@GetMapping("/{id}")
public Product getById(@PathVariable("id") Long productId) {
return productService.findById(productId);
}
// @RequestBody:请求体JSON自动反序列化
@PostMapping
public Product create(@RequestBody @Valid ProductCreateDTO dto) {
return productService.create(dto);
}
// @RequestHeader:获取请求头
@GetMapping("/recommend")
public List<Product> recommend(
@RequestHeader("User-Agent") String userAgent,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
return productService.recommend(userAgent);
}
// @CookieValue:获取Cookie
@GetMapping("/history")
public List<Product> history(@CookieValue("session_id") String sessionId) {
return productService.getHistory(sessionId);
}
// 原生Servlet对象注入
@GetMapping("/export")
public void export(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=products.xlsx");
// 写入response.getOutputStream()
}
}
5.2 表单对象绑定(@ModelAttribute)
java
// DTO对象
public class ProductQueryDTO {
private String name;
private String category;
private BigDecimal minPrice;
private BigDecimal maxPrice;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate createDateFrom;
// getter/setter...
}
@GetMapping("/advanced-search")
public List<Product> advancedSearch(@ModelAttribute ProductQueryDTO query) {
// Spring自动将查询参数绑定到DTO对象
// ?name=手机&category=电子&minPrice=1000&createDateFrom=2024-01-01
return productService.advancedSearch(query);
}
5.3 自定义类型转换
java
// 自定义Converter:字符串 → 枚举
@Component
public class StringToGenderConverter implements Converter<String, Gender> {
@Override
public Gender convert(String source) {
return switch (source.toUpperCase()) {
case "M", "MALE", "男" -> Gender.MALE;
case "F", "FEMALE", "女" -> Gender.FEMALE;
default -> throw new IllegalArgumentException("无效性别: " + source);
};
}
}
// 自定义Formatter:带格式化的转换
@Component
public class PhoneFormatter implements Formatter<String> {
@Override
public String parse(String text, Locale locale) {
// 去除空格和横线
return text.replaceAll("[\\s-]", "");
}
@Override
public String print(String object, Locale locale) {
// 格式化为 138-0000-0000
return object.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1-$2-$3");
}
}
// 注册转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToGenderConverter());
registry.addFormatter(new PhoneFormatter());
}
}
六、拦截器(HandlerInterceptor)
6.1 接口定义与执行时机
java
public interface HandlerInterceptor {
/**
* Handler执行前调用。返回false则中断后续处理。
* 典型用途:认证校验、权限检查、限流
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
return true;
}
/**
* Handler执行后、视图渲染前调用。
* 典型用途:修改ModelAndView、添加公共数据
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
/**
* 请求完成后调用(视图渲染后),无论是否异常。
* 典型用途:资源清理、性能日志
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable Exception ex) throws Exception {
}
}
6.2 完整拦截器实现示例
java
// 认证拦截器
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public AuthenticationInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// OPTIONS预检请求直接放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false;
}
LoginUser user = tokenService.validateToken(token.substring(7));
if (user == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"token已过期\"}");
return false;
}
// 将用户信息存入ThreadLocal供后续使用
UserContext.setCurrentUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理ThreadLocal,防止内存泄漏
UserContext.clear();
}
}
// 性能监控拦截器
@Component
public class PerformanceInterceptor implements HandlerInterceptor {
private static final String START_TIME_ATTR = "requestStartTime";
private static final Logger log = LoggerFactory.getLogger(PerformanceInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
Long startTime = (Long) request.getAttribute(START_TIME_ATTR);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("[{}] {} {} - {}ms", request.getMethod(), request.getRequestURI(),
response.getStatus(), duration);
if (duration > 1000) {
log.warn("慢请求告警: {} {} 耗时 {}ms", request.getMethod(),
request.getRequestURI(), duration);
}
}
}
}
6.3 注册拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthenticationInterceptor authInterceptor;
private final PerformanceInterceptor performanceInterceptor;
public WebConfig(AuthenticationInterceptor authInterceptor,
PerformanceInterceptor performanceInterceptor) {
this.authInterceptor = authInterceptor;
this.performanceInterceptor = performanceInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 性能监控:拦截所有请求
registry.addInterceptor(performanceInterceptor)
.addPathPatterns("/**");
// 认证拦截:排除公开接口
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/public/**"
);
}
}
6.4 多拦截器执行顺序
请求 → Interceptor1.preHandle → Interceptor2.preHandle → Handler
响应 ← Interceptor2.postHandle ← Interceptor1.postHandle ← 视图渲染
完成 ← Interceptor2.afterCompletion ← Interceptor1.afterCompletion
注意:preHandle正序,postHandle和afterCompletion逆序
若某个preHandle返回false,则后续拦截器和Handler都不执行,
但已执行的拦截器的afterCompletion仍会逆序调用。
七、异常处理(@ExceptionHandler/@ControllerAdvice)
7.1 统一错误响应结构
java
// 统一响应体
@Data
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private String traceId;
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setCode(200);
resp.setMessage("success");
resp.setData(data);
resp.setTimestamp(System.currentTimeMillis());
return resp;
}
public static <T> ApiResponse<T> error(int code, String message) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setCode(code);
resp.setMessage(message);
resp.setTimestamp(System.currentTimeMillis());
return resp;
}
}
// 业务异常基类
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
// 具体业务异常
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resource, Long id) {
super(404, String.format("%s(id=%d)不存在", resource, id));
}
}
7.2 全局异常处理器
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResponse<Void> handleBusinessException(BusinessException ex, HttpServletRequest request) {
log.warn("业务异常: {} - {}", request.getRequestURI(), ex.getMessage());
return ApiResponse.error(ex.getCode(), ex.getMessage());
}
/**
* 资源不存在
*/
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiResponse<Void> handleNotFound(ResourceNotFoundException ex) {
return ApiResponse.error(404, ex.getMessage());
}
/**
* 参数校验异常(@Valid触发)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ApiResponse<Map<String, String>> resp = ApiResponse.error(400, "参数校验失败");
resp.setData(errors);
return resp;
}
/**
* 请求参数绑定异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleMissingParam(MissingServletRequestParameterException ex) {
return ApiResponse.error(400, "缺少必要参数: " + ex.getParameterName());
}
/**
* 类型转换异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
return ApiResponse.error(400,
String.format("参数'%s'类型错误,期望%s", ex.getName(),
ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "未知"));
}
/**
* HTTP方法不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ApiResponse<Void> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
return ApiResponse.error(405, "不支持的请求方法: " + ex.getMethod());
}
/**
* 限流异常(配合Sentinel/自定义限流)
*/
@ExceptionHandler(RateLimitException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ApiResponse<Void> handleRateLimit(RateLimitException ex) {
return ApiResponse.error(429, "请求过于频繁,请稍后重试");
}
/**
* 兜底:未知异常(生产环境不暴露堆栈)
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<Void> handleUnexpected(Exception ex, HttpServletRequest request) {
log.error("未处理异常: {} ", request.getRequestURI(), ex);
return ApiResponse.error(500, "服务器内部错误,请联系管理员");
}
}
7.3 @ResponseStatus 注解式
java
// 自定义异常 + @ResponseStatus
@ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateResourceException extends RuntimeException {
public DuplicateResourceException(String message) {
super(message);
}
}
// 抛出即返回409状态码
@PostMapping("/api/v1/users")
public User createUser(@RequestBody UserCreateDTO dto) {
if (userService.existsByEmail(dto.getEmail())) {
throw new DuplicateResourceException("邮箱已注册: " + dto.getEmail());
}
return userService.create(dto);
}
八、RESTful API设计
8.1 标准RESTful控制器
java
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserRestController {
private final UserService userService;
public UserRestController(UserService userService) {
this.userService = userService;
}
/**
* 分页查询用户列表
* GET /api/v1/users?page=0&size=20&sort=createTime,desc&status=ACTIVE
*/
@GetMapping
public ApiResponse<PageResult<UserVO>> listUsers(
@PageableDefault(size = 20, sort = "createTime", direction = Sort.Direction.DESC)
Pageable pageable,
@RequestParam(required = false) UserStatus status,
@RequestParam(required = false) String keyword) {
Page<UserVO> page = userService.listUsers(status, keyword, pageable);
return ApiResponse.success(PageResult.from(page));
}
/**
* 获取单个用户
* GET /api/v1/users/123
*/
@GetMapping("/{id}")
public ApiResponse<UserVO> getUser(@PathVariable Long id) {
UserVO user = userService.getUserById(id);
return ApiResponse.success(user);
}
/**
* 创建用户
* POST /api/v1/users
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<UserVO> createUser(@RequestBody @Valid UserCreateDTO dto) {
UserVO user = userService.createUser(dto);
return ApiResponse.success(user);
}
/**
* 全量更新用户
* PUT /api/v1/users/123
*/
@PutMapping("/{id}")
public ApiResponse<UserVO> updateUser(@PathVariable Long id,
@RequestBody @Valid UserUpdateDTO dto) {
UserVO user = userService.updateUser(id, dto);
return ApiResponse.success(user);
}
/**
* 部分更新
* PATCH /api/v1/users/123
*/
@PatchMapping("/{id}")
public ApiResponse<UserVO> patchUser(@PathVariable Long id,
@RequestBody Map<String, Object> fields) {
UserVO user = userService.patchUser(id, fields);
return ApiResponse.success(user);
}
/**
* 删除用户
* DELETE /api/v1/users/123
*/
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
/**
* 批量操作
* POST /api/v1/users/batch-delete
*/
@PostMapping("/batch-delete")
public ApiResponse<Integer> batchDelete(@RequestBody List<Long> ids) {
int count = userService.batchDelete(ids);
return ApiResponse.success(count);
}
}
8.2 ResponseEntity 精细控制
java
@GetMapping("/{id}/avatar")
public ResponseEntity<Resource> getAvatar(@PathVariable Long id) {
Resource avatar = userService.getAvatar(id);
if (avatar == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(CacheControl.maxAge(1, TimeUnit.DAYS))
.eTag("\"" + avatar.contentLength() + "\"")
.body(avatar);
}
@PostMapping("/import")
public ResponseEntity<ApiResponse<ImportResult>> importUsers(
@RequestParam("file") MultipartFile file) {
ImportResult result = userService.importFromExcel(file);
URI location = URI.create("/api/v1/users/import-tasks/" + result.getTaskId());
return ResponseEntity.created(location)
.body(ApiResponse.success(result));
}
8.3 参数校验(JSR-380)
java
// 创建用户DTO
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 32, message = "用户名长度2-32个字符")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 64, message = "密码长度8-64个字符")
private String password;
@Min(value = 0, message = "年龄不能为负数")
@Max(value = 150, message = "年龄不合法")
private Integer age;
@NotNull(message = "角色不能为空")
private List<@NotBlank String> roles;
}
九、跨域(CORS)处理
9.1 CORS原理
简单请求(GET/HEAD/POST + 简单Content-Type):
浏览器直接发送,附带 Origin 头 → 服务器返回 Access-Control-Allow-Origin
预检请求(PUT/DELETE/自定义头等):
① 浏览器发送 OPTIONS 预检请求
→ Access-Control-Request-Method: PUT
→ Access-Control-Request-Headers: Content-Type, Authorization
② 服务器返回允许的配置
→ Access-Control-Allow-Methods: GET, POST, PUT, DELETE
→ Access-Control-Allow-Headers: Content-Type, Authorization
→ Access-Control-Max-Age: 3600(预检缓存1小时)
③ 浏览器发送实际请求
9.2 三种配置方式
java
// 方式一:注解(方法/类级别)
@CrossOrigin(origins = "https://frontend.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
// ...
}
// 方式二:全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("https://*.example.com", "http://localhost:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Request-Id")
.allowCredentials(true)
.maxAge(3600);
}
}
// 方式三:CorsFilter(优先级最高,适用于Spring Security场景)
@Configuration
public class CorsFilterConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://*.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
十、文件上传下载
10.1 文件上传
java
// application.yml 配置
// spring:
// servlet:
// multipart:
// max-file-size: 50MB
// max-request-size: 100MB
// file-size-threshold: 2KB # 超过此大小写入磁盘
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private final FileStorageService storageService;
// 单文件上传
@PostMapping("/upload")
public ApiResponse<FileInfo> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "default") String category) throws IOException {
// 校验文件
validateFile(file);
String fileId = storageService.store(file, category);
FileInfo info = new FileInfo(fileId, file.getOriginalFilename(),
file.getSize(), file.getContentType());
return ApiResponse.success(info);
}
// 多文件上传
@PostMapping("/upload/batch")
public ApiResponse<List<FileInfo>> uploadFiles(
@RequestParam("files") MultipartFile[] files) throws IOException {
if (files.length > 10) {
throw new BusinessException(400, "单次最多上传10个文件");
}
List<FileInfo> results = new ArrayList<>();
for (MultipartFile file : files) {
validateFile(file);
String fileId = storageService.store(file, "batch");
results.add(new FileInfo(fileId, file.getOriginalFilename(),
file.getSize(), file.getContentType()));
}
return ApiResponse.success(results);
}
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException(400, "文件不能为空");
}
String contentType = file.getContentType();
List<String> allowed = List.of("image/jpeg", "image/png", "application/pdf");
if (!allowed.contains(contentType)) {
throw new BusinessException(400, "不支持的文件类型: " + contentType);
}
}
}
10.2 文件下载
java
// 普通下载
@GetMapping("/download/{fileId}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
StoredFile storedFile = storageService.load(fileId);
Resource resource = new FileSystemResource(storedFile.getPath());
String encodedFilename = URLEncoder.encode(storedFile.getOriginalName(),
StandardCharsets.UTF_8)
.replace("+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodedFilename)
.contentLength(storedFile.getSize())
.body(resource);
}
// 大文件流式下载(避免OOM)
@GetMapping("/download/stream/{fileId}")
public ResponseEntity<StreamingResponseBody> streamDownload(@PathVariable String fileId) {
StoredFile storedFile = storageService.load(fileId);
StreamingResponseBody body = outputStream -> {
try (InputStream is = new FileInputStream(storedFile.getPath())) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + storedFile.getOriginalName() + "\"")
.body(body);
}
十一、WebFlux响应式Web(Reactor Netty)
11.1 核心架构
Spring WebFlux 运行模型:
客户端请求 → Reactor Netty (EventLoop线程,默认CPU核数)
│
▼
HttpHandler(非阻塞处理)
│
▼
WebFilter链(全局过滤器)
│
▼
DispatcherHandler(类似DispatcherServlet)
│
├── HandlerMapping(路由匹配)
├── HandlerAdapter(调用处理函数)
└── 返回 Mono<ServerResponse> / Flux<T>
│
▼
非阻塞写回响应
关键:全程无阻塞,少量线程(EventLoop)处理海量并发连接
11.2 依赖与配置
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 排除spring-boot-starter-web,避免冲突 -->
yaml
# application.yml
server:
port: 8080
netty:
connection-timeout: 5000
spring:
codec:
max-in-memory-size: 256KB
11.3 注解式控制器
java
@RestController
@RequestMapping("/api/v1/reactive/users")
public class ReactiveUserController {
private final ReactiveUserService userService;
public ReactiveUserController(ReactiveUserService userService) {
this.userService = userService;
}
// 返回单个对象
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userService.findById(id);
}
// 返回列表(流式输出)
@GetMapping
public Flux<User> listUsers() {
return userService.findAll();
}
// 创建
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> createUser(@RequestBody Mono<UserCreateDTO> dtoMono) {
return dtoMono.flatMap(userService::create);
}
// 带完整响应控制
@GetMapping("/{id}/profile")
public Mono<ResponseEntity<UserProfile>> getProfile(@PathVariable Long id) {
return userService.findProfile(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
// 删除
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteUser(@PathVariable Long id) {
return userService.delete(id);
}
}
11.4 WebFilter 全局过滤器
java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ReactiveLoggingFilter implements WebFilter {
private static final Logger log = LoggerFactory.getLogger(ReactiveLoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
long startTime = System.nanoTime();
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String method = request.getMethod().name();
// 生成TraceId
String traceId = UUID.randomUUID().toString().replace("-", "");
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-Trace-Id", traceId)
.build();
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return chain.filter(mutatedExchange)
.then(Mono.fromRunnable(() -> {
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
int status = exchange.getResponse().getStatusCode() != null
? exchange.getResponse().getStatusCode().value() : 0;
log.info("[{}] {} {} - {}ms", traceId, method, path, duration);
}));
}
}
十二、RouterFunction/HandlerFunction
12.1 函数式端点核心概念
java
// RouterFunction:定义路由规则(URL + 方法 → Handler)
// HandlerFunction:处理请求,返回 Mono<ServerResponse>
// ServerRequest:请求抽象(参数、Body、Header)
// ServerResponse:响应构建器
12.2 完整函数式路由示例
java
// Handler 类(业务处理)
@Component
public class ProductHandler {
private final ReactiveProductRepository repository;
public ProductHandler(ReactiveProductRepository repository) {
this.repository = repository;
}
// 查询列表
public Mono<ServerResponse> listProducts(ServerRequest request) {
// 获取查询参数
String category = request.queryParam("category").orElse(null);
int page = request.queryParam("page").map(Integer::parseInt).orElse(0);
int size = request.queryParam("size").map(Integer::parseInt).orElse(20);
Flux<Product> products = (category != null)
? repository.findByCategory(category)
: repository.findAll();
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(products.skip((long) page * size).take(size), Product.class);
}
// 查询单个
public Mono<ServerResponse> getProduct(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return repository.findById(id)
.flatMap(product -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(product))
.switchIfEmpty(ServerResponse.notFound().build());
}
// 创建
public Mono<ServerResponse> createProduct(ServerRequest request) {
Mono<Product> productMono = request.bodyToMono(ProductCreateDTO.class)
.flatMap(dto -> {
Product product = new Product();
product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setCategory(dto.getCategory());
return repository.save(product);
});
return ServerResponse.created(URI.create("/api/v1/products"))
.contentType(MediaType.APPLICATION_JSON)
.body(productMono, Product.class);
}
// 更新
public Mono<ServerResponse> updateProduct(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
Mono<Product> updated = request.bodyToMono(ProductUpdateDTO.class)
.flatMap(dto -> repository.findById(id)
.flatMap(product -> {
product.setName(dto.getName());
product.setPrice(dto.getPrice());
return repository.save(product);
}));
return updated.flatMap(product -> ServerResponse.ok().bodyValue(product))
.switchIfEmpty(ServerResponse.notFound().build());
}
// 删除
public Mono<ServerResponse> deleteProduct(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return repository.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
// Router 配置类
@Configuration
public class ProductRouter {
@Bean
public RouterFunction<ServerResponse> productRoutes(ProductHandler handler) {
return RouterFunctions.route()
// 嵌套路由:统一前缀
.nest(path("/api/v1/products"), builder -> builder
.GET("", handler::listProducts)
.GET("/{id}", handler::getProduct)
.POST("", handler::createProduct)
.PUT("/{id}", handler::updateProduct)
.DELETE("/{id}", handler::deleteProduct)
)
// 路由级过滤器
.filter((request, next) -> {
// 可在此做认证、日志等
return next.handle(request);
})
.build();
}
}
12.3 RequestPredicates 请求谓词
java
@Bean
public RouterFunction<ServerResponse> advancedRoutes(ProductHandler handler) {
return RouterFunctions.route()
// 组合谓词
.GET("/api/v1/products",
accept(MediaType.APPLICATION_JSON).and(queryParam("format", "json")),
handler::listProducts)
// 限定Content-Type
.POST("/api/v1/products",
contentType(MediaType.APPLICATION_JSON),
handler::createProduct)
// 自定义谓词
.route(request -> request.path().startsWith("/api/v1/admin")
&& request.headers().header("X-Admin-Key").contains("secret"),
handler::adminPanel)
.build();
}
十三、SSE(Server-Sent Events)
13.1 协议格式
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
event: message
id: 1
data: {"type":"notification","content":"新订单"}
event: heartbeat
id: 2
data: ping
13.2 Spring MVC 实现 SSE(SseEmitter)
java
@RestController
@RequestMapping("/api/v1/sse")
public class SseController {
// 存储所有活跃的Emitter(生产环境用Redis/消息队列)
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
/**
* 客户端订阅SSE
*/
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(@RequestParam String userId) {
// 超时时间30分钟
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
emitters.put(userId, emitter);
// 注册回调
emitter.onCompletion(() -> emitters.remove(userId));
emitter.onTimeout(() -> {
emitters.remove(userId);
emitter.complete();
});
emitter.onError(ex -> emitters.remove(userId));
// 发送初始连接成功事件
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("SSE连接建立成功")
.id(UUID.randomUUID().toString()));
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
/**
* 向指定用户推送消息(由业务层调用)
*/
public void pushToUser(String userId, String eventName, Object data) {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data, MediaType.APPLICATION_JSON)
.id(String.valueOf(System.currentTimeMillis())));
} catch (IOException e) {
emitters.remove(userId);
}
}
}
/**
* 广播消息给所有订阅者
*/
@PostMapping("/broadcast")
public ApiResponse<Integer> broadcast(@RequestBody BroadcastMessage message) {
AtomicInteger successCount = new AtomicInteger(0);
emitters.forEach((userId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name("broadcast")
.data(message));
successCount.incrementAndGet();
} catch (IOException e) {
emitters.remove(userId);
}
});
return ApiResponse.success(successCount.get());
}
}
13.3 Spring WebFlux 实现 SSE
java
@RestController
@RequestMapping("/api/v1/reactive/sse")
public class ReactiveSseController {
/**
* 流式推送(天然支持背压)
*/
@GetMapping(value = "/stock/{symbol}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<StockPrice>> streamStockPrice(@PathVariable String symbol) {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> {
StockPrice price = generateMockPrice(symbol);
return ServerSentEvent.<StockPrice>builder()
.id(String.valueOf(seq))
.event("price-update")
.data(price)
.comment("stock: " + symbol)
.build();
});
}
/**
* 系统通知流
*/
@GetMapping(value = "/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> notifications() {
// 合并心跳 + 业务事件
Flux<ServerSentEvent<String>> heartbeat = Flux.interval(Duration.ofSeconds(30))
.map(i -> ServerSentEvent.<String>builder()
.event("heartbeat")
.data("ping")
.build());
Flux<ServerSentEvent<String>> businessEvents = eventBus.subscribe()
.map(event -> ServerSentEvent.<String>builder()
.id(event.getId())
.event(event.getType())
.data(event.getPayload())
.build());
return Flux.merge(heartbeat, businessEvents);
}
private StockPrice generateMockPrice(String symbol) {
// 模拟价格波动
double base = "AAPL".equals(symbol) ? 150.0 : 3000.0;
double change = (ThreadLocalRandom.current().nextDouble() - 0.5) * 2;
return new StockPrice(symbol, base + change, LocalDateTime.now());
}
}
13.4 前端 EventSource 客户端
javascript
// JavaScript 客户端
const eventSource = new EventSource('/api/v1/sse/subscribe?userId=user123');
// 监听默认message事件
eventSource.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 监听自定义事件
eventSource.addEventListener('price-update', (event) => {
const price = JSON.parse(event.data);
updateStockDisplay(price);
});
eventSource.addEventListener('connected', (event) => {
console.log('SSE连接成功:', event.data);
});
// 错误处理(浏览器自动重连)
eventSource.onerror = (err) => {
console.error('SSE连接异常,将自动重连...', err);
if (eventSource.readyState === EventSource.CLOSED) {
// 连接已关闭,需手动重建
reconnect();
}
};
// 关闭连接
function disconnect() {
eventSource.close();
}
十四、WebSocket
14.1 Spring MVC WebSocket(原生)
java
// 启用WebSocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler(), "/ws/chat")
.addInterceptors(new WebSocketAuthInterceptor())
.setAllowedOrigins("https://frontend.example.com");
}
@Bean
public ChatWebSocketHandler chatWebSocketHandler() {
return new ChatWebSocketHandler();
}
}
// 握手拦截器
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 从URL参数获取token进行认证
String token = UriComponentsBuilder.fromUri(request.getURI())
.build().getQueryParams().getFirst("token");
LoginUser user = tokenService.validate(token);
if (user == null) {
return false; // 拒绝握手
}
attributes.put("currentUser", user);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
// WebSocket处理器
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 在线用户Session管理
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
LoginUser user = (LoginUser) session.getAttributes().get("currentUser");
sessions.put(user.getUserId(), session);
broadcast(new SystemMessage(user.getUsername() + " 加入了聊天"));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
LoginUser user = (LoginUser) session.getAttributes().get("currentUser");
ChatMessage chatMsg = objectMapper.readValue(message.getPayload(), ChatMessage.class);
chatMsg.setSender(user.getUsername());
chatMsg.setTimestamp(LocalDateTime.now());
// 广播给所有在线用户
broadcast(chatMsg);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
LoginUser user = (LoginUser) session.getAttributes().get("currentUser");
if (user != null) {
sessions.remove(user.getUserId());
broadcast(new SystemMessage(user.getUsername() + " 离开了聊天"));
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
session.getAttributes().clear();
sessions.values().remove(session);
}
private void broadcast(Object message) throws IOException {
String json = objectMapper.writeValueAsString(message);
TextMessage textMessage = new TextMessage(json);
for (WebSocketSession session : sessions.values()) {
if (session.isOpen()) {
session.sendMessage(textMessage);
}
}
}
}
14.2 STOMP 子协议(消息代理模式)
java
// STOMP配置
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 客户端订阅前缀(服务器→客户端)
config.enableSimpleBroker("/topic", "/queue");
// 客户端发送前缀(客户端→服务器)
config.setApplicationDestinationPrefixes("/app");
// 点对点用户前缀
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/stomp")
.setAllowedOriginPatterns("https://*.example.com")
.withSockJS(); // 兼容不支持WebSocket的浏览器
}
}
// STOMP消息控制器
@Controller
public class StompChatController {
private final SimpMessagingTemplate messagingTemplate;
public StompChatController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
/**
* 处理群聊消息
* 客户端发送到 /app/chat.send
* 订阅者监听 /topic/chat.room.{roomId}
*/
@MessageMapping("/chat.send")
@SendTo("/topic/chat.room.{roomId}")
public ChatMessage sendMessage(@DestinationVariable String roomId,
@Payload ChatMessage message,
Principal principal) {
message.setSender(principal.getName());
message.setTimestamp(LocalDateTime.now());
return message;
}
/**
* 点对点消息
* 发送到 /app/chat.private
* 接收方监听 /user/queue/messages
*/
@MessageMapping("/chat.private")
public void sendPrivateMessage(@Payload PrivateMessage message, Principal principal) {
message.setSender(principal.getName());
messagingTemplate.convertAndSendToUser(
message.getRecipient(),
"/queue/messages",
message
);
}
/**
* 业务层主动推送(如订单状态变更)
*/
public void pushOrderUpdate(Long userId, OrderStatusUpdate update) {
messagingTemplate.convertAndSendToUser(
String.valueOf(userId),
"/queue/orders",
update
);
}
}
14.3 WebFlux WebSocket(完全非阻塞)
java
@Configuration
public class ReactiveWebSocketConfig {
@Bean
public HandlerMapping webSocketMapping(ReactiveChatHandler chatHandler) {
Map<String, WebSocketHandler> map = Map.of(
"/ws/reactive/chat", chatHandler
);
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1);
return mapping;
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@Component
public class ReactiveChatHandler implements WebSocketHandler {
private final Sinks.Many<String> chatSink = Sinks.many().multicast().onBackpressureBuffer();
@Override
public Mono<Void> handle(WebSocketSession session) {
// 接收消息 → 广播
Mono<Void> input = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.doOnNext(msg -> chatSink.tryEmitNext(msg))
.then();
// 订阅广播 → 发送
Mono<Void> output = session.send(
chatSink.asFlux()
.map(session::textMessage)
);
// 双向通信
return Mono.zip(input, output).then();
}
@Override
public List<String> getSubProtocols() {
return List.of();
}
}
14.4 前端 WebSocket 客户端
javascript
// 原生WebSocket
const ws = new WebSocket('wss://api.example.com/ws/chat?token=xxx');
ws.onopen = () => {
console.log('WebSocket连接建立');
ws.send(JSON.stringify({ type: 'JOIN', room: 'general' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
appendMessage(msg);
};
ws.onclose = (event) => {
console.log(`连接关闭: code=${event.code}, reason=${event.reason}`);
// 自动重连
setTimeout(() => connectWebSocket(), 3000);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
// STOMP客户端(使用SockJS + STOMP.js)
const stompClient = Stomp.over(new SockJS('/ws/stomp'));
stompClient.connect({}, (frame) => {
// 订阅群聊
stompClient.subscribe('/topic/chat.room.general', (message) => {
appendMessage(JSON.parse(message.body));
});
// 订阅个人消息
stompClient.subscribe('/user/queue/messages', (message) => {
showNotification(JSON.parse(message.body));
});
});
十五、MVC vs WebFlux选型
15.1 对比总览
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| IO模型 | 同步阻塞 (Servlet) | 异步非阻塞 (Reactor Netty) |
| 线程模型 | 一请求一线程(线程池) | EventLoop(少量线程) |
| 并发能力 | 受线程数限制(通常200-500) | 少量线程支撑数万并发 |
| 编程复杂度 | 低(直觉式同步代码) | 高(响应式链路、操作符) |
| 调试难度 | 低(堆栈清晰) | 高(异步堆栈不连续) |
| 生态兼容 | JDBC/JPA/所有阻塞库 | 需R2DBC/响应式驱动 |
| 适用场景 | CRUD、传统Web、微服务 | 网关、流式处理、高并发IO |
| 性能(低负载) | 延迟略低 | 差异不大 |
| 性能(高负载) | 线程耗尽后急剧下降 | 平稳降级 |
15.2 选型决策树
是否需要高并发长连接(>5000并发)?
├── 是 → 是否涉及大量阻塞IO(数据库/文件)?
│ ├── 是 → 是否有响应式驱动(R2DBC)?
│ │ ├── 是 → WebFlux
│ │ └── 否 → MVC + 异步(@Async/CompletableFuture)
│ └── 否 → WebFlux(如API网关、代理、SSE推送)
└── 否 → 团队是否熟悉响应式编程?
├── 是 → 可用WebFlux获得更好资源利用率
└── 否 → Spring MVC(开发效率优先)
15.3 典型场景推荐
java
// 场景1:API网关 → WebFlux(Spring Cloud Gateway基于WebFlux)
// 大量转发请求,IO密集,无需阻塞调用
// 场景2:实时数据推送(股票行情/监控) → WebFlux + SSE/WebSocket
// 长连接多,数据流式输出
// 场景3:传统CRUD后台管理 → Spring MVC
// 并发不高,JPA/JDBC生态成熟,开发效率高
// 场景4:文件处理服务 → Spring MVC + 异步
// 文件IO本身阻塞,WebFlux无优势
// 场景5:混合架构 → MVC为主 + WebClient调用下游
@RestController
public class OrderController {
private final WebClient webClient; // 非阻塞HTTP客户端
@GetMapping("/api/v1/orders/{id}/detail")
public OrderDetail getOrderDetail(@PathVariable Long id) {
// 并行调用多个微服务
CompletableFuture<User> userFuture = webClient.get()
.uri("http://user-service/api/users/{id}", userId)
.retrieve().bodyToMono(User.class)
.toFuture();
CompletableFuture<List<Product>> productsFuture = webClient.get()
.uri("http://product-service/api/products?orderId={id}", id)
.retrieve().bodyToFlux(Product.class)
.collectList().toFuture();
// 等待所有结果
return new OrderDetail(userFuture.join(), productsFuture.join());
}
}
十六、最佳实践
16.1 Controller层设计
java
// 原则:Controller保持轻薄,只做参数接收、校验、调用Service、返回结果
@RestController
@RequestMapping("/api/v1/orders")
@Validated
@Tag(name = "订单管理", description = "订单CRUD接口") // SpringDoc
public class OrderController {
private final OrderService orderService;
// 统一响应包装
@GetMapping("/{id}")
@Operation(summary = "查询订单详情")
public ApiResponse<OrderVO> getOrder(
@Parameter(description = "订单ID") @PathVariable Long id) {
return ApiResponse.success(orderService.getOrder(id));
}
// 参数校验 + 分组
@PostMapping
@Operation(summary = "创建订单")
public ApiResponse<OrderVO> createOrder(
@RequestBody @Validated(Create.class) OrderCreateDTO dto) {
return ApiResponse.success(orderService.createOrder(dto));
}
}
16.2 接口幂等性设计
java
// 幂等注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
long timeout() default 5; // 幂等窗口(秒)
String key() default ""; // SpEL表达式
}
// 幂等拦截器(基于Redis)
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
private final StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
Idempotent idempotent = handlerMethod.getMethodAnnotation(Idempotent.class);
if (idempotent == null) {
return true;
}
// 从请求头获取幂等Key
String idempotentKey = request.getHeader("X-Idempotent-Key");
if (idempotentKey == null) {
throw new BusinessException(400, "缺少幂等Key请求头");
}
String redisKey = "idempotent:" + idempotentKey;
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", idempotent.timeout(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(acquired)) {
throw new BusinessException(409, "重复请求,请勿重复提交");
}
return true;
}
}
// 使用
@PostMapping("/create")
@Idempotent(timeout = 10)
public ApiResponse<OrderVO> createOrder(@RequestBody OrderCreateDTO dto) {
return ApiResponse.success(orderService.createOrder(dto));
}
16.3 WebFlux 最佳实践
java
// 1. 绝对不要在响应式链路中阻塞
// 错误示范:
@GetMapping("/bad")
public Mono<String> bad() {
return Mono.fromCallable(() -> {
Thread.sleep(1000); // 阻塞EventLoop线程!
return "result";
});
}
// 正确:使用subscribeOn隔离阻塞操作到弹性线程池
@GetMapping("/good")
public Mono<String> good() {
return Mono.fromCallable(() -> {
// 阻塞操作(如调用遗留JDBC代码)
return legacyBlockingCall();
})
.subscribeOn(Schedulers.boundedElastic()); // 隔离到弹性线程池
}
// 2. 使用Context传递上下文(替代ThreadLocal)
@GetMapping("/with-context")
public Mono<String> withContext() {
return Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
return Mono.just("Hello, " + userId);
});
}
// WebFilter中写入Context
@Component
public class UserContextFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
return chain.filter(exchange)
.contextWrite(Context.of("userId", userId != null ? userId : "anonymous"));
}
}
// 3. 错误处理链路
@GetMapping("/resilient")
public Mono<Data> resilient() {
return remoteService.getData()
.timeout(Duration.ofSeconds(3))
.retryWhen(Retry.backoff(3, Duration.ofMillis(500))
.filter(ex -> ex instanceof TimeoutException))
.onErrorResume(ex -> {
log.warn("降级处理", ex);
return Mono.just(getCachedData()); // 降级返回缓存
});
}
// 4. 背压策略
@GetMapping("/stream")
public Flux<Event> stream() {
return eventSource.getEvents()
.onBackpressureBuffer(1000, BufferOverflowStrategy.DROP_OLDEST)
.publishOn(Schedulers.parallel());
}
16.4 日志链路追踪
java
// MVC:通过MDC + 拦截器
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
MDC.put("spanId", UUID.randomUUID().toString().substring(0, 8));
response.setHeader("X-Trace-Id", traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
MDC.clear();
}
}
// logback-spring.xml 配置
// <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
16.5 API文档(SpringDoc/OpenAPI 3)
xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
java
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("电商平台API")
.version("v1.0")
.description("基于Spring Boot 3的RESTful API")
.contact(new Contact().name("Dev Team").email("dev@example.com")))
.addSecurityItem(new SecurityRequirement().addList("Bearer"))
.components(new Components()
.addSecuritySchemes("Bearer",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
// 访问地址:http://localhost:8080/swagger-ui.html
// OpenAPI JSON:http://localhost:8080/v3/api-docs
16.6 安全实践清单
java
// 1. 输入校验:永远不信任客户端数据
// 2. SQL注入:使用参数化查询(JPA/MyBatis #{})
// 3. XSS防护:输出编码 + CSP头
// 4. CSRF:REST API使用Token认证可禁用CSRF
// 5. 限流:网关层 + 应用层双重限流
// 6. 敏感数据:密码BCrypt加密,日志脱敏
// 7. HTTPS:生产环境强制TLS
// 请求限流注解示例
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int permits() default 100; // 时间窗口内允许次数
int window() default 60; // 时间窗口(秒)
String key() default ""; // 限流维度(IP/用户)
}
总结
| 技术点 | 核心要点 |
|---|---|
| DispatcherServlet | 前端控制器,协调HandlerMapping/Adapter/ViewResolver |
| HandlerMapping | URL→Handler映射,RequestMappingHandlerMapping为核心 |
| HandlerAdapter | 适配调用Handler,参数解析+返回值处理+消息转换 |
| 拦截器 | preHandle/postHandle/afterCompletion,正序/逆序执行 |
| 异常处理 | @RestControllerAdvice + @ExceptionHandler 全局统一 |
| RESTful | 资源URI + HTTP方法语义 + 状态码 + 统一响应体 |
| CORS | allowedOriginPatterns + allowCredentials + maxAge |
| WebFlux | Reactor Netty + Mono/Flux + 非阻塞全链路 |
| RouterFunction | 函数式路由,RouterFunctions.route() + HandlerFunction |
| SSE | MVC用SseEmitter,WebFlux用Flux |
| WebSocket | 全双工,STOMP子协议简化开发,WebFlux完全非阻塞 |
| 选型 | CRUD用MVC,高并发IO/流式/网关用WebFlux |
