跳到主要内容

Web 开发

本章将深入讲解 Spring Boot Web 开发的核心知识,包括 RESTful API 设计、请求处理、参数验证、异常处理等内容。

RESTful API 设计

REST 架构风格

REST(Representational State Transfer)是一种软件架构风格,强调资源的状态转移。

RESTful 设计原则

原则说明
统一接口使用标准的 HTTP 方法操作资源
无状态每个请求包含所有必要信息
可缓存响应应标明是否可缓存
分层系统客户端无需知道连接的是哪一层

HTTP 方法映射

HTTP 方法操作示例 URL说明
GET查询/api/users获取用户列表
GET查询/api/users/{id}获取单个用户
POST创建/api/users创建新用户
PUT更新/api/users/{id}更新用户(完整)
PATCH更新/api/users/{id}更新用户(部分)
DELETE删除/api/users/{id}删除用户

RESTful 控制器示例

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

// 获取用户列表
@GetMapping
public Result<List<User>> list() {
return Result.success(userService.findAll());
}

// 获取单个用户
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.success(userService.findById(id));
}

// 创建用户
@PostMapping
public Result<User> create(@RequestBody @Valid UserDTO userDTO) {
return Result.success(userService.save(userDTO));
}

// 更新用户
@PutMapping("/{id}")
public Result<User> update(@PathVariable Long id, @RequestBody @Valid UserDTO userDTO) {
return Result.success(userService.update(id, userDTO));
}

// 删除用户
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.deleteById(id);
return Result.success();
}
}

请求处理

获取请求参数

@RequestParam:查询参数

@GetMapping("/search")
public Result<List<User>> search(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return Result.success(userService.search(name, page, size));
}

请求示例GET /api/users/search?name=张三&page=0&size=10

属性说明

属性说明
value / name参数名
required是否必需(默认 true)
defaultValue默认值

@PathVariable:路径变量

@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.success(userService.findById(id));
}

// 多个路径变量
@GetMapping("/{userId}/orders/{orderId}")
public Result<Order> getOrder(
@PathVariable Long userId,
@PathVariable Long orderId
) {
return Result.success(orderService.findByUserIdAndOrderId(userId, orderId));
}

@RequestBody:请求体

@PostMapping
public Result<User> create(@RequestBody @Valid UserDTO userDTO) {
return Result.success(userService.save(userDTO));
}

请求示例

POST /api/users
Content-Type: application/json

{
"name": "张三",
"email": "[email protected]",
"age": 25
}

@RequestHeader:请求头

@GetMapping("/info")
public Result<Map<String, String>> getInfo(
@RequestHeader("User-Agent") String userAgent,
@RequestHeader(value = "X-Token", required = false) String token
) {
Map<String, String> info = new HashMap<>();
info.put("userAgent", userAgent);
info.put("token", token);
return Result.success(info);
}

@CookieValue:Cookie 值

@GetMapping("/session")
public Result<String> getSession(@CookieValue("JSESSIONID") String sessionId) {
return Result.success(sessionId);
}

请求方法映射

@RestController
@RequestMapping("/api")
public class MethodController {

// GET 请求
@GetMapping("/resource")
public String get() {
return "GET 请求";
}

// POST 请求
@PostMapping("/resource")
public String post(@RequestBody String body) {
return "POST 请求: " + body;
}

// PUT 请求
@PutMapping("/resource/{id}")
public String put(@PathVariable Long id, @RequestBody String body) {
return "PUT 请求: " + id + ", " + body;
}

// PATCH 请求
@PatchMapping("/resource/{id}")
public String patch(@PathVariable Long id, @RequestBody String body) {
return "PATCH 请求: " + id + ", " + body;
}

// DELETE 请求
@DeleteMapping("/resource/{id}")
public String delete(@PathVariable Long id) {
return "DELETE 请求: " + id;
}

// 多方法映射
@RequestMapping(value = "/multi", method = {RequestMethod.GET, RequestMethod.POST})
public String multiMethod() {
return "GET 或 POST 请求";
}
}

响应处理

统一响应格式

定义统一的响应结构:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {

private Integer code; // 状态码
private String message; // 提示信息
private T data; // 数据

// 成功响应
public static <T> Result<T> success() {
return new Result<>(200, "success", null);
}

public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}

public static <T> Result<T> success(String message, T data) {
return new Result<>(200, message, data);
}

// 失败响应
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}

public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
}

响应示例

{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "张三",
"email": "[email protected]"
}
}

分页响应

@Data
public class PageResult<T> {

private List<T> list; // 数据列表
private Long total; // 总记录数
private Integer page; // 当前页
private Integer size; // 每页大小
private Integer totalPages; // 总页数

public static <T> PageResult<T> of(List<T> list, Long total, Integer page, Integer size) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setSize(size);
result.setTotalPages((int) Math.ceil((double) total / size));
return result;
}
}

ResponseEntity

对于需要设置响应头或状态码的场景:

@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header("X-Custom-Header", "value")
.body(user);
}

@PostMapping
public ResponseEntity<User> create(@RequestBody UserDTO userDTO) {
User user = userService.save(userDTO);
// 返回 201 Created 状态码和 Location 头
return ResponseEntity
.created(URI.create("/api/users/" + user.getId()))
.body(user);
}

参数验证

添加验证依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证注解

注解作用示例
@NotNull不能为 null@NotNull
@NotBlank不能为空字符串@NotBlank
@NotEmpty不能为空集合/字符串@NotEmpty
@Size字符串/集合长度范围@Size(min = 2, max = 10)
@Min / @Max数值最小/最大值@Min(0) @Max(100)
@Email邮箱格式@Email
@Pattern正则表达式@Pattern(regexp = "^1[3-9]\\d{9}$")
@Past / @Future过去/未来日期@Past
@Digits数字位数@Digits(integer = 5, fraction = 2)

使用验证

验证请求体

@Data
public class UserDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String name;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄必须小于150")
private Integer age;

@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}

@RestController
public class UserController {

@PostMapping
public Result<User> create(@RequestBody @Valid UserDTO userDTO) {
return Result.success(userService.save(userDTO));
}
}

验证路径参数和查询参数

@GetMapping("/{id}")
public Result<User> getById(
@PathVariable @Min(1) Long id,
@RequestParam @NotBlank String name
) {
return Result.success(userService.findById(id));
}

注意:需要在类级别添加 @Validated 注解:

@RestController
@Validated
public class UserController {
// ...
}

嵌套验证

@Data
public class OrderDTO {

@NotBlank(message = "订单号不能为空")
private String orderNo;

@Valid // 启用嵌套验证
@NotNull(message = "收货地址不能为空")
private AddressDTO address;
}

@Data
public class AddressDTO {

@NotBlank(message = "省不能为空")
private String province;

@NotBlank(message = "市不能为空")
private String city;

@NotBlank(message = "详细地址不能为空")
private String detail;
}

自定义验证器

自定义注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {

String message() default "手机号格式不正确";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

自定义验证器

public class PhoneValidator implements ConstraintValidator<Phone, String> {

private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 空值交给 @NotBlank 处理
}
return PHONE_PATTERN.matcher(value).matches();
}
}

使用自定义验证

@Data
public class UserDTO {

@Phone(message = "请输入正确的手机号")
private String phone;
}

异常处理

全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}

/**
* 处理约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}

/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}

/**
* 处理资源未找到异常
*/
@ExceptionHandler(EntityNotFoundException.class)
public Result<Void> handleEntityNotFoundException(EntityNotFoundException e) {
return Result.error(404, e.getMessage());
}

/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
logger.error("系统异常", e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}

自定义业务异常

@Getter
public class BusinessException extends RuntimeException {

private final Integer code;

public BusinessException(String message) {
super(message);
this.code = 500;
}

public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}

// 使用示例
public User findById(Long id) {
User user = userRepository.findById(id).orElse(null);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
return user;
}

错误码枚举

@Getter
@AllArgsConstructor
public enum ErrorCode {

SUCCESS(200, "成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_ERROR(500, "服务器内部错误");

private final Integer code;
private final String message;
}

跨域处理

方式一:@CrossOrigin 注解

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*", maxAge = 3600)
public class ApiController {
// ...
}

方式二:全局配置

@Configuration
public class CorsConfig {

@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();

// 允许的域名
config.addAllowedOriginPattern("*");

// 允许的请求头
config.addAllowedHeader("*");

// 允许的请求方法
config.addAllowedMethod("*");

// 允许携带 Cookie
config.setAllowCredentials(true);

// 预检请求缓存时间
config.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsFilter(source);
}
}

方式三:WebMvcConfigurer

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

文件上传下载

文件上传

@RestController
@RequestMapping("/api/files")
public class FileController {

@Value("${file.upload-dir:./uploads}")
private String uploadDir;

/**
* 单文件上传
*/
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new BusinessException("请选择要上传的文件");
}

// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String filename = UUID.randomUUID().toString() + extension;

// 创建上传目录
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}

// 保存文件
Path filePath = uploadPath.resolve(filename);
file.transferTo(filePath);

return Result.success(filename);
}

/**
* 多文件上传
*/
@PostMapping("/uploads")
public Result<List<String>> uploads(@RequestParam("files") MultipartFile[] files) throws IOException {
List<String> filenames = new ArrayList<>();

for (MultipartFile file : files) {
if (!file.isEmpty()) {
String filename = saveFile(file);
filenames.add(filename);
}
}

return Result.success(filenames);
}

/**
* 文件下载
*/
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) throws IOException {
Path filePath = Paths.get(uploadDir).resolve(filename);
Resource resource = new FileSystemResource(filePath);

if (!resource.exists()) {
throw new BusinessException(404, "文件不存在");
}

// 设置响应头
String contentType = Files.probeContentType(filePath);
String disposition = "attachment; filename=\"" + URLEncoder.encode(filename, "UTF-8") + "\"";

return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
.body(resource);
}
}

文件上传配置

spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB # 单个文件最大大小
max-request-size: 100MB # 总请求最大大小
file-size-threshold: 0 # 超过此大小的文件写入临时目录

拦截器

创建拦截器

@Component
public class AuthInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求预处理
String token = request.getHeader("Authorization");

if (token == null || !validateToken(token)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未授权\"}");
return false;
}

return true; // 继续执行
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 请求处理后处理
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后处理
}
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private AuthInterceptor authInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns( // 排除路径
"/api/auth/login",
"/api/auth/register",
"/api/public/**"
);
}
}

HTTP 客户端

在微服务架构中,服务间调用是常见需求。Spring Boot 提供了多种 HTTP 客户端选择,其中 RestClient 是 Spring 6.1 引入的现代同步 HTTP 客户端,也是官方推荐的替代 RestTemplate 的方案。

RestClient 概述

RestClient 是一个功能强大的同步 HTTP 客户端,提供流畅的 API 风格。相比传统的 RestTemplate,它有以下优势:

特性RestClientRestTemplate
API 风格流畅、函数式模板方法模式
类型安全更好的泛型支持较弱
可读性链式调用,更直观方法调用较多
推荐程度官方推荐维护模式(不推荐新项目使用)

RestClient 基本使用

Spring Boot 自动配置了 RestClient.Builder,推荐注入使用:

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class UserService {

private final RestClient restClient;

// 注入自动配置的 Builder
public UserService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder
.baseUrl("https://api.example.com")
.build();
}

/**
* GET 请求示例
*/
public User getUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}

/**
* 带请求头的 GET 请求
*/
public User getUserWithAuth(Long id, String token) {
return restClient.get()
.uri("/users/{id}", id)
.header("Authorization", "Bearer " + token)
.retrieve()
.body(User.class);
}

/**
* POST 请求示例
*/
public User createUser(UserDTO userDTO) {
return restClient.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.body(userDTO)
.retrieve()
.body(User.class);
}

/**
* PUT 请求示例
*/
public User updateUser(Long id, UserDTO userDTO) {
return restClient.put()
.uri("/users/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(userDTO)
.retrieve()
.body(User.class);
}

/**
* DELETE 请求示例
*/
public void deleteUser(Long id) {
restClient.delete()
.uri("/users/{id}", id)
.retrieve()
.toBodilessEntity(); // 忽略响应体
}
}

解释

  • RestClient.Builder 是原型作用域,每次注入都是新实例
  • retrieve() 方法触发实际请求
  • body() 方法将响应体反序列化为指定类型
  • toBodilessEntity() 用于忽略响应体

处理响应和错误

获取完整响应信息

import org.springframework.http.ResponseEntity;

public User getUserWithResponse(Long id) {
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);

// 获取响应信息
int statusCode = response.getStatusCode().value();
HttpHeaders headers = response.getHeaders();
User user = response.getBody();

return user;
}

错误处理

import org.springframework.web.client.RestClientResponseException;

public User getUserSafe(Long id) {
try {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
} catch (RestClientResponseException e) {
// 服务器返回错误状态码(4xx、5xx)
int statusCode = e.getStatusCode().value();
String responseBody = e.getResponseBodyAsString();
log.error("请求失败,状态码: {}, 响应: {}", statusCode, responseBody);
throw new BusinessException("获取用户失败: " + e.getMessage());
}
}

// 自定义错误处理
public User getUserWithHandler(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(status -> status.is4xxClientError(), (request, response) -> {
// 4xx 错误处理
throw new BusinessException("客户端错误: " + response.getStatusCode());
})
.onStatus(status -> status.is5xxServerError(), (request, response) -> {
// 5xx 错误处理
throw new BusinessException("服务器错误: " + response.getStatusCode());
})
.body(User.class);
}

处理列表响应

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

public List<User> getUsers() {
// 方式一:使用数组
User[] users = restClient.get()
.uri("/users")
.retrieve()
.body(User[].class);
return Arrays.asList(users);

// 方式二:使用 ParameterizedTypeReference(推荐)
List<User> userList = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
return userList;
}

请求定制

使用 UriBuilder 构建 URI

import org.springframework.web.util.UriBuilder;

public List<User> searchUsers(String name, Integer age, String email) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/users/search")
.queryParam("name", name)
.queryParam("age", age)
.queryParamIfPresent("email", Optional.ofNullable(email))
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}

添加请求拦截器

@Configuration
public class RestClientConfig {

@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("User-Agent", "MyApp/1.0")
.defaultHeader("Accept", "application/json")
.requestInterceptor((request, body, execution) -> {
// 记录请求日志
log.info("请求: {} {}", request.getMethod(), request.getURI());
long start = System.currentTimeMillis();

try {
ClientHttpResponse response = execution.execute(request, body);
long duration = System.currentTimeMillis() - start;
log.info("响应: {}, 耗时: {}ms", response.getStatusCode(), duration);
return response;
} catch (IOException e) {
log.error("请求失败", e);
throw e;
}
})
.build();
}
}

设置超时

import java.time.Duration;

@Configuration
public class RestClientConfig {

@Bean
public RestClient restClient(RestClient.Builder builder) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULT
.withConnectTimeout(Duration.ofSeconds(5))
.withReadTimeout(Duration.ofSeconds(30));

return builder
.baseUrl("https://api.example.com")
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
}

SSL 配置

当需要调用使用自签名证书或自定义证书的 HTTPS 服务时,可以使用 SSL Bundles:

# application.yml
spring:
ssl:
bundle:
jks:
mybundle:
key:
alias: "myalias"
location: "classpath:keystore.jks"
password: "secret"
options:
protocol: "TLS"
import org.springframework.boot.autoconfigure.web.client.RestClientSsl;
import org.springframework.boot.ssl.SslBundles;

@Service
public class SecureApiService {

private final RestClient restClient;

// 方式一:使用 RestClientSsl
public SecureApiService(RestClient.Builder builder, RestClientSsl ssl) {
this.restClient = builder
.baseUrl("https://secure-api.example.com")
.apply(ssl.fromBundle("mybundle"))
.build();
}

// 方式二:使用 SslBundles
public SecureApiService(RestClient.Builder builder, SslBundles sslBundles) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULT
.withSslBundle(sslBundles.getBundle("mybundle"))
.withReadTimeout(Duration.ofMinutes(2));

this.restClient = builder
.baseUrl("https://secure-api.example.com")
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
}

全局 HTTP 客户端配置

Spring Boot 支持全局配置 HTTP 客户端属性:

spring:
http:
client:
connect-timeout: 5s # 连接超时
read-timeout: 30s # 读取超时
redirects: follow # 重定向策略: follow, dont-follow
factory: jdk # 指定 HTTP 客户端: jdk, jetty, httpcomponents, reactor

支持的 HTTP 客户端(按优先级):

客户端依赖说明
Apache HttpClienthttpclient5功能丰富,推荐生产环境使用
Jetty HttpClientjetty-client轻量级
Reactor Nettyreactor-netty-http支持 HTTP/2
JDK HttpClientJava 11+ 内置无额外依赖
Simple JDKJava 内置功能有限,不推荐

RestClient vs RestTemplate

如果已有项目使用 RestTemplate,可以逐步迁移:

// RestTemplate 方式(不推荐新项目使用)
User user = restTemplate.getForObject("/users/{id}", User.class, id);

// RestClient 方式(推荐)
User user = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);

迁移建议

  1. 新项目直接使用 RestClient
  2. 旧项目可以共存,逐步替换
  3. RestTemplate 处于维护模式,不会添加新功能

WebClient(响应式客户端)

如果需要非阻塞的响应式客户端,可以使用 WebClient:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class ReactiveUserService {

private final WebClient webClient;

public ReactiveUserService(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://api.example.com")
.build();
}

public Mono<User> getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
}

public Flux<User> getAllUsers() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class);
}
}

选择建议

场景推荐客户端
传统 Spring MVC 应用RestClient
响应式 Spring WebFlux 应用WebClient
高并发、非阻塞需求WebClient
简单的同步调用RestClient

WebSocket 实时通信

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,适用于实时聊天、消息推送、股票行情等场景。Spring Boot 提供了完善的 WebSocket 支持。

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocket 配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker // 启用 WebSocket 消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

/**
* 配置消息代理
* - enableSimpleBroker: 启用简单的内存消息代理,用于向客户端推送消息
* - setApplicationDestinationPrefixes: 设置应用目的地前缀
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单的内存消息代理,客户端订阅以 /topic 开头的目的地
config.enableSimpleBroker("/topic");
// 客户端发送消息的目的地前缀
config.setApplicationDestinationPrefixes("/app");
}

/**
* 注册 STOMP 端点
* - 客户端通过此端点建立 WebSocket 连接
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws") // WebSocket 端点路径
.setAllowedOriginPatterns("*") // 允许跨域
.withSockJS(); // 启用 SockJS 降级支持
}
}

配置说明

配置项说明
/topic消息代理前缀,用于广播消息
/app应用目的地前缀,控制器处理的消息
/wsWebSocket 连接端点
withSockJS()启用 SockJS 降级,兼容不支持 WebSocket 的浏览器

消息模型

/**
* 接收的消息
*/
@Data
public class ChatMessage {
private String from; // 发送者
private String content; // 消息内容
private String type; // 消息类型:CHAT, JOIN, LEAVE
}

/**
* 响应的消息
*/
@Data
@AllArgsConstructor
public class ChatResponse {
private String from;
private String content;
private LocalDateTime timestamp;
}

WebSocket 控制器

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

/**
* 处理聊天消息
* @MessageMapping: 客户端发送消息的目的地(/app/chat.sendMessage)
* @SendTo: 处理后发送到的目的地(/topic/public)
*/
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatResponse sendMessage(@Payload ChatMessage message) {
return new ChatResponse(
message.getFrom(),
message.getContent(),
LocalDateTime.now()
);
}

/**
* 处理用户加入
*/
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatResponse addUser(@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
// 将用户名存入 WebSocket Session
headerAccessor.getSessionAttributes().put("username", message.getFrom());

return new ChatResponse(
"系统",
message.getFrom() + " 加入了聊天",
LocalDateTime.now()
);
}
}

注解说明

注解作用
@MessageMapping映射 WebSocket 消息到处理方法
@SendTo将方法返回值发送到指定目的地
@Payload绑定消息体到参数
@DestinationVariable绑定目的地变量(类似 @PathVariable

向特定用户发送消息

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

@Autowired
private SimpMessagingTemplate messagingTemplate;

/**
* 向特定用户发送消息
* @param userId 用户ID
* @param message 消息内容
*/
public void sendToUser(String userId, Object message) {
// 发送到 /user/{userId}/queue/notifications
messagingTemplate.convertAndSendToUser(
userId,
"/queue/notifications",
message
);
}

/**
* 向所有订阅者广播消息
*/
public void broadcast(Object message) {
messagingTemplate.convertAndSend("/topic/notifications", message);
}
}

配置用户目的地

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue"); // 添加 /queue 支持点对点消息
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user"); // 用户目的地前缀
}

// ... 其他配置
}

前端连接示例(JavaScript)

// 使用 STOMP.js 连接 WebSocket
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

// 建立连接
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);

// 连接成功回调
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);

// 订阅公共消息
stompClient.subscribe('/topic/public', function(message) {
const data = JSON.parse(message.body);
console.log('收到消息:', data);
});

// 订阅个人消息(需要认证)
stompClient.subscribe('/user/queue/notifications', function(message) {
const data = JSON.parse(message.body);
console.log('收到个人消息:', data);
});
});

// 发送消息
function sendMessage(from, content) {
stompClient.send('/app/chat.sendMessage', {}, JSON.stringify({
from: from,
content: content,
type: 'CHAT'
}));
}

WebSocket 安全配置

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("http://localhost:*")
.withSockJS();
}
}

WebSocket 认证拦截器

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

@Component
public class AuthHandshakeInterceptor implements HandshakeInterceptor {

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 从请求中获取认证信息
if (request instanceof ServletServerHttpRequest servletRequest) {
String token = servletRequest.getServletRequest().getParameter("token");
// 验证 token 并获取用户信息
if (token != null && validateToken(token)) {
attributes.put("user", getUserFromToken(token));
return true;
}
}
return false;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}

SSE 服务器推送

Server-Sent Events(SSE)是一种服务器向客户端推送数据的技术,与 WebSocket 不同,SSE 是单向通信(仅服务器到客户端),适合实时通知、数据监控等场景。

SSE vs WebSocket 对比

特性SSEWebSocket
通信方向单向(服务器→客户端)双向
协议HTTPWebSocket 协议
断线重连浏览器自动重连需要手动实现
数据格式文本(UTF-8)文本和二进制
浏览器支持IE 不支持广泛支持
适用场景实时通知、数据监控聊天、游戏、协作编辑

使用 SseEmitter

Spring MVC 提供了 SseEmitter 类来实现 SSE:

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@RestController
@RequestMapping("/api/sse")
public class SseController {

// 存储所有连接的客户端
private final CopyOnWriteArraySet<SseEmitter> emitters = new CopyOnWriteArraySet<>();

/**
* 建立 SSE 连接
* 客户端通过此接口建立长连接,接收服务器推送的事件
*/
@GetMapping(path = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
// 创建 SseEmitter,设置超时时间(30分钟)
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);

// 添加到集合
emitters.add(emitter);

// 设置回调
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(e -> emitters.remove(emitter));

// 发送初始连接成功消息
try {
emitter.send(SseEmitter.event()
.name("connect")
.data("连接成功"));
} catch (IOException e) {
emitter.completeWithError(e);
}

return emitter;
}

/**
* 向所有客户端推送消息
*/
public void broadcast(String message) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(message));
} catch (IOException e) {
// 发送失败,移除连接
emitters.remove(emitter);
}
}
}

/**
* 推送带ID的事件
*/
public void sendEvent(String eventId, Object data) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.id(eventId)
.name("update")
.data(data, MediaType.APPLICATION_JSON));
} catch (IOException e) {
emitters.remove(emitter);
}
}
}
}

SseEmitter 方法说明

方法说明
send(Object)发送数据
send(SseEventBuilder)发送完整事件
complete()正常结束连接
completeWithError(Throwable)异常结束连接
onCompletion(Runnable)连接完成回调
onTimeout(Runnable)超时回调
onError(Consumer<Throwable>)错误回调

SSE 事件构建

// 构建复杂的 SSE 事件
emitter.send(SseEmitter.event()
.id("123") // 事件ID
.name("notification") // 事件名称
.reconnectTime(5000) // 重连时间(毫秒)
.comment("这是一个注释") // 注释
.data("{\"message\":\"Hello\"}", MediaType.APPLICATION_JSON) // 数据
);

// 发送多行数据
emitter.send(SseEmitter.event()
.name("multi-data")
.data("第一行数据\n")
.data("第二行数据\n")
.data("第三行数据")
);

实时数据推送示例

@Service
public class StockPriceService {

private final SseEmitter emitter = new SseEmitter();

/**
* 模拟股票价格实时推送
*/
@Scheduled(fixedRate = 1000) // 每秒推送一次
public void pushStockPrice() {
try {
StockPrice price = new StockPrice(
"AAPL",
Math.random() * 100 + 150,
LocalDateTime.now()
);

emitter.send(SseEmitter.event()
.name("stock-price")
.data(price, MediaType.APPLICATION_JSON));
} catch (IOException e) {
emitter.completeWithError(e);
}
}

public SseEmitter getEmitter() {
return emitter;
}
}

前端接收 SSE

// 创建 EventSource 连接
const eventSource = new EventSource('/api/sse/connect');

// 监听连接打开
eventSource.addEventListener('connect', function(event) {
console.log('SSE 连接成功:', event.data);
});

// 监听消息事件
eventSource.addEventListener('message', function(event) {
console.log('收到消息:', event.data);
});

// 监听自定义事件
eventSource.addEventListener('stock-price', function(event) {
const data = JSON.parse(event.data);
console.log('股票价格更新:', data);
});

// 监听错误
eventSource.onerror = function(event) {
console.error('SSE 错误:', event);
// 浏览器会自动重连
};

// 关闭连接
function closeConnection() {
eventSource.close();
}

SSE 最佳实践

1. 心跳机制

@Scheduled(fixedRate = 30000)  // 每30秒发送心跳
public void sendHeartbeat() {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.comment("heartbeat") // 注释作为心跳
);
} catch (IOException e) {
emitters.remove(emitter);
}
}
}

2. 连接管理

@Component
public class SseConnectionManager {

private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();

public SseEmitter createEmitter(String clientId) {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);

emitter.onCompletion(() -> emitters.remove(clientId));
emitter.onTimeout(() -> emitters.remove(clientId));

emitters.put(clientId, emitter);
return emitter;
}

public void sendToClient(String clientId, Object data) {
SseEmitter emitter = emitters.get(clientId);
if (emitter != null) {
try {
emitter.send(data);
} catch (IOException e) {
emitters.remove(clientId);
}
}
}
}

异步请求处理

在高并发场景下,异步请求处理可以显著提高服务器的吞吐量。Spring MVC 提供了多种异步处理方式。

异步处理原理

┌─────────────────────────────────────────────────────────────────────┐
│ 异步请求处理流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端请求 ──> Servlet线程(释放)──> 容器线程池 ──> 响应客户端 │
│ │ │ │
│ │ ▼ │
│ │ 异步任务执行 │
│ │ (不占用Servlet线程) │
│ │ │ │
│ └─────────────────────┘ │
│ │
│ 优势: │
│ - Servlet 线程快速释放,处理更多请求 │
│ - 长时间任务在独立线程执行,不阻塞容器 │
│ - 提高服务器并发处理能力 │
│ │
└─────────────────────────────────────────────────────────────────────┘

Callable 异步处理

Callable 是最简单的异步处理方式,Spring 会将返回的 Callable 提交到线程池执行:

import java.util.concurrent.Callable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/async")
public class AsyncController {

/**
* 使用 Callable 进行异步处理
* Servlet 线程立即释放,Callable 在单独线程中执行
*/
@GetMapping("/callable")
public Callable<String> callableExample() {
// 返回 Callable,Servlet 线程立即释放
return () -> {
// 此代码在独立线程中执行
Thread.sleep(2000); // 模拟耗时操作
return "异步处理完成";
};
}

/**
* 带业务的 Callable 示例
*/
@GetMapping("/users/{id}")
public Callable<User> getUser(@PathVariable Long id) {
return () -> {
// 异步查询用户
return userService.findById(id);
};
}
}

解释

  1. 控制器返回 Callable 后,Servlet 线程立即释放
  2. Spring 将 Callable 提交到 AsyncTaskExecutor 执行
  3. 执行完成后,Spring 将结果写回响应

DeferredResult 异步处理

DeferredResult 提供了更灵活的异步处理方式,可以在任意线程中设置结果:

import org.springframework.web.context.request.async.DeferredResult;
import java.util.concurrent.CompletableFuture;

@RestController
@RequestMapping("/api/async")
public class DeferredResultController {

@Autowired
private OrderService orderService;

/**
* 使用 DeferredResult 进行异步处理
* 适合需要在其他线程(如消息队列、定时任务)中设置结果的场景
*/
@GetMapping("/deferred")
public DeferredResult<String> deferredExample() {
// 创建 DeferredResult,设置超时时间
DeferredResult<String> deferredResult = new DeferredResult<>(5000L);

// 设置超时回调
deferredResult.onTimeout(() -> {
deferredResult.setErrorResult("请求超时");
});

// 设置完成回调
deferredResult.onCompletion(() -> {
System.out.println("请求处理完成");
});

// 在其他线程中设置结果
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000);
// 设置成功结果
deferredResult.setResult("异步处理完成");
} catch (InterruptedException e) {
// 设置错误结果
deferredResult.setErrorResult("处理失败");
}
});

return deferredResult;
}

/**
* 结合消息队列的示例
*/
@PostMapping("/orders")
public DeferredResult<Order> createOrder(@RequestBody OrderDTO orderDTO) {
DeferredResult<Order> deferredResult = new DeferredResult<>(30000L);

// 发送订单到消息队列
orderService.submitOrder(orderDTO, deferredResult);

return deferredResult;
}
}

// 订单服务
@Service
public class OrderService {

private final Map<String, DeferredResult<Order>> pendingOrders = new ConcurrentHashMap<>();

public void submitOrder(OrderDTO orderDTO, DeferredResult<Order> deferredResult) {
String orderId = UUID.randomUUID().toString();

// 保存 DeferredResult,等待处理结果
pendingOrders.put(orderId, deferredResult);

// 发送到消息队列
rabbitTemplate.convertAndSend("order.queue", orderDTO);
}

/**
* 消息队列消费者调用此方法设置结果
*/
public void completeOrder(String orderId, Order order) {
DeferredResult<Order> deferredResult = pendingOrders.remove(orderId);
if (deferredResult != null) {
deferredResult.setResult(order);
}
}
}

Callable vs DeferredResult

特性CallableDeferredResult
执行方式Spring 线程池执行任意线程设置结果
适用场景简单异步计算复杂异步流程、消息队列
灵活性较低较高
超时处理需要手动处理内置超时支持
结果设置返回值调用 setResult()

ResponseBodyEmitter 流式响应

ResponseBodyEmitter 用于向响应中多次写入数据,适合流式传输场景:

import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.io.IOException;

@RestController
@RequestMapping("/api/stream")
public class StreamController {

/**
* 使用 ResponseBodyEmitter 进行流式响应
* 可以多次发送数据
*/
@GetMapping("/events")
public ResponseBodyEmitter streamEvents() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter(60000L);

// 异步发送数据
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
// 发送数据
emitter.send("Event " + i + "\n");
Thread.sleep(1000);
}
// 完成发送
emitter.complete();
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e);
}
}).start();

return emitter;
}

/**
* 发送 JSON 数据
*/
@GetMapping("/json-stream")
public ResponseBodyEmitter streamJson() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();

new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
EventData data = new EventData(i, "Event " + i, LocalDateTime.now());
emitter.send(data, MediaType.APPLICATION_JSON);
Thread.sleep(500);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();

return emitter;
}
}

StreamingResponseBody 文件下载

对于大文件下载,使用 StreamingResponseBody 可以避免一次性加载到内存:

import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
@RequestMapping("/api/download")
public class DownloadController {

/**
* 使用 StreamingResponseBody 下载大文件
* 流式写入,不占用大量内存
*/
@GetMapping("/large-file")
public ResponseEntity<StreamingResponseBody> downloadLargeFile() {
StreamingResponseBody stream = outputStream -> {
try (InputStream inputStream = new FileInputStream("large-file.dat")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
};

return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"large-file.dat\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(stream);
}

/**
* 动态生成 CSV 导出
*/
@GetMapping("/export")
public ResponseEntity<StreamingResponseBody> exportData() {
StreamingResponseBody stream = outputStream -> {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
// 写入 CSV 头
writer.println("ID,Name,Email,CreatedAt");

// 分批查询并写入数据
int page = 0;
List<User> users;
do {
users = userRepository.findAll(PageRequest.of(page++, 1000)).getContent();
for (User user : users) {
writer.printf("%d,%s,%s,%s%n",
user.getId(),
user.getName(),
user.getEmail(),
user.getCreatedAt()
);
}
writer.flush();
} while (!users.isEmpty());
}
};

return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"users.csv\"")
.contentType(MediaType.parseMediaType("text/csv;charset=UTF-8"))
.body(stream);
}
}

异步配置

@Configuration
public class AsyncConfig implements WebMvcConfigurer {

/**
* 配置异步请求处理
*/
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 设置默认超时时间(30秒)
configurer.setDefaultTimeout(30000);

// 配置异步任务执行器
configurer.setTaskExecutor(mvcTaskExecutor());

// 注册 DeferredResult 拦截器
configurer.registerDeferredResultInterceptors(new DeferredResultInterceptor());

// 注册 Callable 拦截器
configurer.registerCallableInterceptors(new CallableInterceptor());
}

/**
* 自定义异步任务执行器
*/
@Bean
public AsyncTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("mvc-async-");
executor.initialize();
return executor;
}
}

异步请求拦截器

import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;

@Component
public class AsyncRequestInterceptor implements CallableProcessingInterceptor, DeferredResultProcessingInterceptor {

@Override
public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
// 异步处理前调用(在 Servlet 线程中)
System.out.println("异步处理开始");
}

@Override
public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object result) {
// 异步处理完成后调用
System.out.println("异步处理完成,结果: " + result);
}

@Override
public <T> Object handleError(NativeWebRequest request, Callable<T> task, Throwable t) {
// 错误处理
return "异步处理错误: " + t.getMessage();
}

@Override
public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) {
// 超时处理
return "请求超时";
}
}

异步处理最佳实践

1. 合理配置线程池

@Bean
public AsyncTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 根据 CPU 核心数和任务类型配置
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}

2. 设置合理的超时时间

// 全局超时配置
configurer.setDefaultTimeout(30000);

// 单个请求超时
DeferredResult<String> result = new DeferredResult<>(5000L);
result.onTimeout(() -> result.setErrorResult("请求超时"));

3. 错误处理

DeferredResult<String> result = new DeferredResult<>();
result.onTimeout(() -> result.setErrorResult(
new ErrorResponse("TIMEOUT", "请求超时")));
result.onError(e -> result.setErrorResult(
new ErrorResponse("ERROR", "处理失败")));

4. 避免阻塞

// 不推荐:在 Callable 中执行阻塞操作
return () -> {
Thread.sleep(10000); // 阻塞异步线程池
return result;
};

// 推荐:使用非阻塞 API
return () -> {
return CompletableFuture.supplyAsync(() -> {
// 使用独立线程池
});
};

小结

本章我们学习了:

  1. RESTful API 设计:资源设计和 HTTP 方法映射
  2. 请求处理:各种参数获取方式
  3. 响应处理:统一响应格式和 ResponseEntity
  4. 参数验证:验证注解和自定义验证器
  5. 异常处理:全局异常处理器
  6. 跨域处理:多种跨域解决方案
  7. 文件上传下载:文件处理和配置
  8. 拦截器:请求拦截和预处理
  9. HTTP 客户端:RestClient 的使用、配置和错误处理
  10. WebSocket 实时通信:STOMP 消息、广播和点对点消息、安全配置
  11. SSE 服务器推送:SseEmitter 的使用、事件构建、实时数据推送
  12. 异步请求处理:Callable、DeferredResult、ResponseBodyEmitter、StreamingResponseBody 的使用场景和最佳实践

练习

  1. 设计并实现一个完整的用户管理 RESTful API
  2. 为 API 添加参数验证
  3. 实现全局异常处理
  4. 配置 CORS 允许前端跨域访问
  5. 实现文件上传和下载功能
  6. 实现一个简单的聊天室(使用 WebSocket)
  7. 实现股票价格实时推送(使用 SSE)
  8. 实现异步长时间任务处理(使用 DeferredResult)
  9. 实现大文件流式下载(使用 StreamingResponseBody)