Java深入解析篇十六之Spring MVC与WebFlux

Java深入解析篇十六之Spring MVC与WebFlux

本文基于 Spring Framework 6.x / Spring Boot 3.x,深入分析 Spring MVC 请求处理机制与 WebFlux 响应式 Web 开发,配合完整可运行代码示例。


目录

  1. [Spring MVC架构与请求处理流程](#Spring MVC架构与请求处理流程)
  2. DispatcherServlet核心组件
  3. HandlerMapping与HandlerAdapter
  4. @Controller/@RestController/@RequestMapping
  5. 参数绑定与数据转换
  6. 拦截器(HandlerInterceptor)
  7. 异常处理(@ExceptionHandler/@ControllerAdvice)
  8. [RESTful API设计](#RESTful API设计)
  9. 跨域(CORS)处理
  10. 文件上传下载
  11. [WebFlux响应式Web(Reactor Netty)](#WebFlux响应式Web(Reactor Netty))
  12. RouterFunction/HandlerFunction
  13. [SSE(Server-Sent Events)](#SSE(Server-Sent Events))
  14. WebSocket
  15. [MVC vs WebFlux选型](#MVC vs WebFlux选型)
  16. 最佳实践

一、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
相关推荐
mifengxing2 小时前
LeetCode 41.缺失的第一个正数|Hard题O(n)+O(1)最优解法深度解析
java·算法·leetcode·排序算法
markinmarkin4 小时前
Spring 中Bean 的作用域有哪些?
java·后端·spring
山荷枝5 小时前
Java学习第十天
java·学习
matlabgoodboy6 小时前
计算机毕设代做|Java Python Matlab APP 全套开发设计
java·python·课程设计
程序员雷欧7 小时前
环形缓冲区深度解析:从基础原理到Disruptor源码的全面剖析
java
xiaoqiMikko7 小时前
Dependabot 面板全绿,不代表你的 Tomcat 没洞
java·spring boot
前端开发张小七7 小时前
Java 学习笔记 · 第三课:多线程与并发编程(线程、同步、死锁、Lock、乐观锁与悲观锁)
java·后端·程序员
花生了什么事o8 小时前
JVM 垃圾回收:对象如何被判定和回收
java·jvm
evans在进步8 小时前
HashMap 为什么线程不安全?ConcurrentHashMap 如何解决?
java·spring boot·spring
我命由我123458 小时前
匈牙利命名法
java·服务器·后端·学习·java-ee·kotlin·学习方法