第4章:Spring MVC 与 WebFlux 增强
第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
| 特性 | RestTemplate | WebClient | HTTP 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) | 提升 |
|---|---|---|---|
| 100 | 850 | 920 | 8% |
| 500 | 780 | 4500 | 477% |
| 1000 | 650 | 8900 | 1269% |
| 5000 | 420 | 18000 | 4186% |
4.5 小结
本章我们学习了 Spring Boot 4 在 Web 层的增强:
✅ HTTP Interface 客户端
- 声明式 HTTP 调用
- 更简洁的代码
- 更好的类型安全
✅ Problem Details 支持
- RFC 7807 标准
- 统一的错误格式
- 更好的互操作性
✅ 观察性增强
- 自动追踪
- 指标收集
- @Observed 注解
✅ 虚拟线程集成
- 显著的性能提升
- 简单的配置
下一步
下一章我们将学习 WebSocket 与 Server-Sent Events 改进。
导航: