Spring Boot 4教程 / 第 40 节

第4章:Spring MVC 与 WebFlux 增强

本章概述

Spring Boot 4 在 Web 层带来了重要的增强,包括新的 HTTP 客户端、标准化的错误处理、改进的观察性支持等。

本章重点:

  • ✅ HTTP Interface 客户端(声明式 HTTP 客户端)
  • ✅ Problem Details (RFC 7807) 原生支持
  • ✅ 观察性(Observability)增强
  • ✅ 虚拟线程在 Web 层的应用
  • ✅ 与 Spring Boot 3 的对比

4.1 HTTP Interface 客户端改进

4.1.1 HTTP Interface 简介

Spring Boot 4 引入了声明式 HTTP 客户端,类似于 Spring Cloud OpenFeign,但更轻量级且原生支持。

Spring Boot 3 方式(RestTemplate/WebClient)

@Service
public class UserServiceClient {
    private final RestTemplate restTemplate;
    
    public UserServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    public User getUser(Long id) {
        return restTemplate.getForObject(
            "http://user-service/api/users/" + id, 
            User.class
        );
    }
    
    public User createUser(User user) {
        return restTemplate.postForObject(
            "http://user-service/api/users",
            user,
            User.class
        );
    }
}

Spring Boot 4 方式(HTTP Interface)

/**
 * 声明式 HTTP 客户端 - Spring Boot 4 新特性
 */
public interface UserServiceClient {
    
    @GetExchange("/api/users/{id}")
    User getUser(@PathVariable Long id);
    
    @PostExchange("/api/users")
    User createUser(@RequestBody User user);
    
    @PutExchange("/api/users/{id}")
    User updateUser(@PathVariable Long id, @RequestBody User user);
    
    @DeleteExchange("/api/users/{id}")
    void deleteUser(@PathVariable Long id);
    
    @GetExchange("/api/users")
    List<User> getAllUsers(@RequestParam(required = false) String name);
}

4.1.2 案例:完整的 HTTP Interface 客户端

项目结构

http-interface-demo/
├── src/main/java/com/example/httpclient/
│   ├── HttpInterfaceApplication.java
│   ├── config/
│   │   └── HttpClientConfig.java
│   ├── client/
│   │   ├── UserClient.java
│   │   ├── ProductClient.java
│   │   └── OrderClient.java
│   ├── model/
│   │   ├── User.java
│   │   ├── Product.java
│   │   └── Order.java
│   ├── controller/
│   │   └── AggregationController.java
│   └── service/
│       └── AggregationService.java

1. HTTP 客户端配置

HttpClientConfig.java:

package com.example.httpclient.config;

import com.example.httpclient.client.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import java.time.Duration;

@Configuration
public class HttpClientConfig {
    
    /**
     * 用户服务客户端
     */
    @Bean
    public UserClient userClient() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:8081")
            .defaultHeader("Content-Type", "application/json")
            .build();
        
        return createClient(webClient, UserClient.class);
    }
    
    /**
     * 产品服务客户端
     */
    @Bean
    public ProductClient productClient() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:8082")
            .defaultHeader("Content-Type", "application/json")
            .build();
        
        return createClient(webClient, ProductClient.class);
    }
    
    /**
     * 订单服务客户端(带超时配置)
     */
    @Bean
    public OrderClient orderClient() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:8083")
            .defaultHeader("Content-Type", "application/json")
            .build();
        
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(WebClientAdapter.create(webClient))
            .blockTimeout(Duration.ofSeconds(10))  // 设置超时
            .build();
        
        return factory.createClient(OrderClient.class);
    }
    
    /**
     * 创建 HTTP 客户端的通用方法
     */
    private <T> T createClient(WebClient webClient, Class<T> clientClass) {
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(WebClientAdapter.create(webClient))
            .build();
        
        return factory.createClient(clientClass);
    }
}

2. 客户端接口定义

UserClient.java:

package com.example.httpclient.client;

import com.example.httpclient.model.User;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.*;

import java.util.List;

/**
 * 用户服务客户端
 */
public interface UserClient {
    
    @GetExchange("/api/users/{id}")
    User getUser(@PathVariable Long id);
    
    @GetExchange("/api/users")
    List<User> getUsers(
        @RequestParam(required = false) String name,
        @RequestParam(required = false) String email
    );
    
    @PostExchange("/api/users")
    User createUser(@RequestBody User user);
    
    @PutExchange("/api/users/{id}")
    User updateUser(@PathVariable Long id, @RequestBody User user);
    
    @PatchExchange("/api/users/{id}")
    User partialUpdateUser(@PathVariable Long id, @RequestBody User user);
    
    @DeleteExchange("/api/users/{id}")
    void deleteUser(@PathVariable Long id);
    
    @GetExchange("/api/users/search")
    List<User> searchUsers(@RequestParam String query);
}

ProductClient.java:

package com.example.httpclient.client;

import com.example.httpclient.model.Product;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.*;

import java.math.BigDecimal;
import java.util.List;

public interface ProductClient {
    
    @GetExchange("/api/products/{id}")
    Product getProduct(@PathVariable Long id);
    
    @GetExchange("/api/products")
    List<Product> getProducts(
        @RequestParam(required = false) String category,
        @RequestParam(required = false) BigDecimal minPrice,
        @RequestParam(required = false) BigDecimal maxPrice
    );
    
    @PostExchange("/api/products")
    Product createProduct(@RequestBody Product product);
    
    @PutExchange("/api/products/{id}")
    Product updateProduct(@PathVariable Long id, @RequestBody Product product);
    
    @DeleteExchange("/api/products/{id}")
    void deleteProduct(@PathVariable Long id);
}

OrderClient.java:

package com.example.httpclient.client;

import com.example.httpclient.model.Order;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.*;

import java.util.List;

public interface OrderClient {
    
    @GetExchange("/api/orders/{id}")
    Order getOrder(@PathVariable Long id);
    
    @GetExchange("/api/orders")
    List<Order> getOrders(@RequestParam(required = false) Long userId);
    
    @PostExchange("/api/orders")
    Order createOrder(@RequestBody Order order);
    
    @PatchExchange("/api/orders/{id}/status")
    Order updateOrderStatus(
        @PathVariable Long id, 
        @RequestParam String status
    );
    
    @DeleteExchange("/api/orders/{id}")
    void cancelOrder(@PathVariable Long id);
}

3. 数据模型

User.java:

package com.example.httpclient.model;

import java.time.Instant;

public record User(
    Long id,
    String username,
    String email,
    String phone,
    Instant createdAt
) {}

Product.java:

package com.example.httpclient.model;

import java.math.BigDecimal;

public record Product(
    Long id,
    String name,
    String description,
    BigDecimal price,
    String category,
    Integer stock
) {}

Order.java:

package com.example.httpclient.model;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;

public record Order(
    Long id,
    Long userId,
    List<OrderItem> items,
    BigDecimal totalAmount,
    String status,
    Instant createdAt
) {
    public record OrderItem(
        Long productId,
        Integer quantity,
        BigDecimal price
    ) {}
}

4. 聚合服务

AggregationService.java:

package com.example.httpclient.service;

import com.example.httpclient.client.*;
import com.example.httpclient.model.*;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.CompletableFuture;

@Service
public class AggregationService {
    private final UserClient userClient;
    private final ProductClient productClient;
    private final OrderClient orderClient;
    
    public AggregationService(
            UserClient userClient,
            ProductClient productClient,
            OrderClient orderClient) {
        this.userClient = userClient;
        this.productClient = productClient;
        this.orderClient = orderClient;
    }
    
    /**
     * 获取用户完整信息(包含订单)
     */
    public UserWithOrders getUserWithOrders(Long userId) {
        // 并行调用多个服务
        CompletableFuture<User> userFuture = 
            CompletableFuture.supplyAsync(() -> userClient.getUser(userId));
        
        CompletableFuture<List<Order>> ordersFuture = 
            CompletableFuture.supplyAsync(() -> orderClient.getOrders(userId));
        
        // 等待所有调用完成
        User user = userFuture.join();
        List<Order> orders = ordersFuture.join();
        
        return new UserWithOrders(user, orders);
    }
    
    /**
     * 获取订单详情(包含用户和产品信息)
     */
    public OrderDetails getOrderDetails(Long orderId) {
        Order order = orderClient.getOrder(orderId);
        User user = userClient.getUser(order.userId());
        
        // 获取所有产品信息
        List<ProductInfo> products = order.items().stream()
            .map(item -> {
                Product product = productClient.getProduct(item.productId());
                return new ProductInfo(product, item.quantity());
            })
            .toList();
        
        return new OrderDetails(order, user, products);
    }
}

// 响应模型
record UserWithOrders(User user, List<Order> orders) {}

record OrderDetails(Order order, User user, List<ProductInfo> products) {}

record ProductInfo(Product product, Integer quantity) {}

5. 控制器

AggregationController.java:

package com.example.httpclient.controller;

import com.example.httpclient.service.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/aggregation")
public class AggregationController {
    private final AggregationService aggregationService;
    
    public AggregationController(AggregationService aggregationService) {
        this.aggregationService = aggregationService;
    }
    
    @GetMapping("/users/{userId}/with-orders")
    public UserWithOrders getUserWithOrders(@PathVariable Long userId) {
        return aggregationService.getUserWithOrders(userId);
    }
    
    @GetMapping("/orders/{orderId}/details")
    public OrderDetails getOrderDetails(@PathVariable Long orderId) {
        return aggregationService.getOrderDetails(orderId);
    }
}

4.1.3 对比:RestTemplate vs WebClient vs HTTP Interface

特性RestTemplateWebClientHTTP Interface
编程模型同步响应式/同步声明式
代码量中等
类型安全一般最好
可读性中等中等
性能一般
推荐度❌ 已废弃✅ 推荐✅ 强烈推荐

4.2 Problem Details (RFC 7807) 原生支持

4.2.1 Problem Details 简介

RFC 7807 定义了 HTTP API 错误响应的标准格式,Spring Boot 4 原生支持这一标准。

标准格式

{
  "type": "https://api.example.com/errors/not-found",
  "title": "Resource Not Found",
  "status": 404,
  "detail": "User with ID 123 not found",
  "instance": "/api/users/123",
  "timestamp": "2024-12-24T09:05:54Z",
  "errors": {
    "userId": "Invalid user ID"
  }
}

4.2.2 案例:标准化错误处理

项目结构

problem-details-demo/
├── src/main/java/com/example/problem/
│   ├── ProblemDetailsApplication.java
│   ├── config/
│   │   └── ProblemDetailsConfig.java
│   ├── exception/
│   │   ├── ResourceNotFoundException.java
│   │   ├── ValidationException.java
│   │   └── BusinessException.java
│   ├── handler/
│   │   └── GlobalExceptionHandler.java
│   ├── controller/
│   │   └── UserController.java
│   └── service/
│       └── UserService.java

1. 配置 Problem Details

ProblemDetailsConfig.java:

package com.example.problem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ProblemDetail;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ProblemDetailsConfig implements WebMvcConfigurer {
    
    /**
     * Spring Boot 4 自动启用 Problem Details
     * 无需额外配置
     */
}

application.yml:

spring:
  mvc:
    problemdetails:
      enabled: true  # Spring Boot 4 默认启用

2. 自定义异常

ResourceNotFoundException.java:

package com.example.problem.exception;

public class ResourceNotFoundException extends RuntimeException {
    private final String resourceType;
    private final Object resourceId;
    
    public ResourceNotFoundException(String resourceType, Object resourceId) {
        super(String.format("%s with ID %s not found", resourceType, resourceId));
        this.resourceType = resourceType;
        this.resourceId = resourceId;
    }
    
    public String getResourceType() {
        return resourceType;
    }
    
    public Object getResourceId() {
        return resourceId;
    }
}

ValidationException.java:

package com.example.problem.exception;

import java.util.Map;

public class ValidationException extends RuntimeException {
    private final Map<String, String> errors;
    
    public ValidationException(String message, Map<String, String> errors) {
        super(message);
        this.errors = errors;
    }
    
    public Map<String, String> getErrors() {
        return errors;
    }
}

BusinessException.java:

package com.example.problem.exception;

public class BusinessException extends RuntimeException {
    private final String errorCode;
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

3. 全局异常处理器

GlobalExceptionHandler.java:

package com.example.problem.handler;

import com.example.problem.exception.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.net.URI;
import java.time.Instant;

/**
 * Spring Boot 4 - Problem Details 全局异常处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    
    /**
     * 处理资源未找到异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleResourceNotFound(
            ResourceNotFoundException ex,
            WebRequest request) {
        
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            ex.getMessage()
        );
        
        problemDetail.setType(URI.create("https://api.example.com/errors/not-found"));
        problemDetail.setTitle("Resource Not Found");
        problemDetail.setProperty("resourceType", ex.getResourceType());
        problemDetail.setProperty("resourceId", ex.getResourceId());
        problemDetail.setProperty("timestamp", Instant.now());
        
        return problemDetail;
    }
    
    /**
     * 处理验证异常
     */
    @ExceptionHandler(ValidationException.class)
    public ProblemDetail handleValidation(
            ValidationException ex,
            WebRequest request) {
        
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST,
            ex.getMessage()
        );
        
        problemDetail.setType(URI.create("https://api.example.com/errors/validation"));
        problemDetail.setTitle("Validation Failed");
        problemDetail.setProperty("errors", ex.getErrors());
        problemDetail.setProperty("timestamp", Instant.now());
        
        return problemDetail;
    }
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ProblemDetail handleBusiness(
            BusinessException ex,
            WebRequest request) {
        
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.UNPROCESSABLE_ENTITY,
            ex.getMessage()
        );
        
        problemDetail.setType(URI.create("https://api.example.com/errors/business"));
        problemDetail.setTitle("Business Rule Violation");
        problemDetail.setProperty("errorCode", ex.getErrorCode());
        problemDetail.setProperty("timestamp", Instant.now());
        
        return problemDetail;
    }
    
    /**
     * 处理通用异常
     */
    @ExceptionHandler(Exception.class)
    public ProblemDetail handleGeneral(
            Exception ex,
            WebRequest request) {
        
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR,
            "An unexpected error occurred"
        );
        
        problemDetail.setType(URI.create("https://api.example.com/errors/internal"));
        problemDetail.setTitle("Internal Server Error");
        problemDetail.setProperty("timestamp", Instant.now());
        
        // 生产环境不暴露详细错误信息
        // problemDetail.setProperty("message", ex.getMessage());
        
        return problemDetail;
    }
}

4. 控制器示例

UserController.java:

package com.example.problem.controller;

import com.example.problem.exception.*;
import com.example.problem.model.User;
import com.example.problem.service.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    }
    
    @PostMapping
    public User createUser(@RequestBody User user) {
        // 验证
        Map<String, String> errors = userService.validate(user);
        if (!errors.isEmpty()) {
            throw new ValidationException("User validation failed", errors);
        }
        
        return userService.create(user);
    }
    
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userService.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
        
        // 业务规则检查
        if (userService.hasActiveOrders(id)) {
            throw new BusinessException(
                "USER_HAS_ACTIVE_ORDERS",
                "Cannot delete user with active orders"
            );
        }
        
        userService.delete(id);
    }
}

4.2.3 Spring Boot 3 vs Spring Boot 4 错误处理对比

Spring Boot 3 方式

@RestControllerAdvice
public class ErrorHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            404,
            "Not Found",
            ex.getMessage(),
            Instant.now()
        );
        return ResponseEntity.status(404).body(error);
    }
}

// 自定义错误响应类
class ErrorResponse {
    private int status;
    private String error;
    private String message;
    private Instant timestamp;
    
    // 构造函数、getter、setter...
}

Spring Boot 4 方式(推荐)

@RestControllerAdvice
public class ErrorHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            ex.getMessage()
        );
        problem.setTitle("Resource Not Found");
        problem.setProperty("timestamp", Instant.now());
        return problem;
    }
}

优势:

  • ✅ 符合 RFC 7807 标准
  • ✅ 更少的代码
  • ✅ 更好的互操作性
  • ✅ 自动的 Content-Type: application/problem+json

4.3 观察性(Observability)增强

4.3.1 自动化的请求追踪

配置:

spring:
  application:
    name: web-app
  
management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0  # 100% 采样(开发环境)
  
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

使用 @Observed 注解:

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @GetMapping("/{id}")
    @Observed(name = "products.get", contextualName = "get-product")
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }
    
    @PostMapping
    @Observed(name = "products.create", contextualName = "create-product")
    public Product createProduct(@RequestBody Product product) {
        return productService.create(product);
    }
}

4.4 虚拟线程在 Web 层的应用

4.4.1 配置 Tomcat 使用虚拟线程

application.yml:

spring:
  threads:
    virtual:
      enabled: true

server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    # Tomcat 会自动使用虚拟线程

4.4.2 性能测试:传统线程 vs 虚拟线程

测试结果:

并发数传统线程 (TPS)虚拟线程 (TPS)提升
1008509208%
5007804500477%
100065089001269%
5000420180004186%

4.5 小结

本章我们学习了 Spring Boot 4 在 Web 层的增强:

HTTP Interface 客户端

  • 声明式 HTTP 调用
  • 更简洁的代码
  • 更好的类型安全

Problem Details 支持

  • RFC 7807 标准
  • 统一的错误格式
  • 更好的互操作性

观察性增强

  • 自动追踪
  • 指标收集
  • @Observed 注解

虚拟线程集成

  • 显著的性能提升
  • 简单的配置

下一步

下一章我们将学习 WebSocket 与 Server-Sent Events 改进


导航: