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,它有以下优势:
| 特性 | RestClient | RestTemplate |
|---|---|---|
| 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 HttpClient | httpclient5 | 功能丰富,推荐生产环境使用 |
| Jetty HttpClient | jetty-client | 轻量级 |
| Reactor Netty | reactor-netty-http | 支持 HTTP/2 |
| JDK HttpClient | Java 11+ 内置 | 无额外依赖 |
| Simple JDK | Java 内置 | 功能有限,不推荐 |
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);
迁移建议:
- 新项目直接使用 RestClient
- 旧项目可以共存,逐步替换
- 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 | 应用目的地前缀,控制器处理的消息 |
/ws | WebSocket 连接端点 |
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 对比
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | WebSocket 协议 |
| 断线重连 | 浏览器自动重连 | 需要手动实现 |
| 数据格式 | 文本(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);
};
}
}
解释:
- 控制器返回
Callable后,Servlet 线程立即释放 - Spring 将
Callable提交到AsyncTaskExecutor执行 - 执行完成后,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:
| 特性 | Callable | DeferredResult |
|---|---|---|
| 执行方式 | 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(() -> {
// 使用独立线程池
});
};
小结
本章我们学习了:
- RESTful API 设计:资源设计和 HTTP 方法映射
- 请求处理:各种参数获取方式
- 响应处理:统一响应格式和 ResponseEntity
- 参数验证:验证注解和自定义验证器
- 异常处理:全局异常处理器
- 跨域处理:多种跨域解决方案
- 文件上传下载:文件处理和配置
- 拦截器:请求拦截和预处理
- HTTP 客户端:RestClient 的使用、配置和错误处理
- WebSocket 实时通信:STOMP 消息、广播和点对点消息、安全配置
- SSE 服务器推送:SseEmitter 的使用、事件构建、实时数据推送
- 异步请求处理:Callable、DeferredResult、ResponseBodyEmitter、StreamingResponseBody 的使用场景和最佳实践
练习
- 设计并实现一个完整的用户管理 RESTful API
- 为 API 添加参数验证
- 实现全局异常处理
- 配置 CORS 允许前端跨域访问
- 实现文件上传和下载功能
- 实现一个简单的聊天室(使用 WebSocket)
- 实现股票价格实时推送(使用 SSE)
- 实现异步长时间任务处理(使用 DeferredResult)
- 实现大文件流式下载(使用 StreamingResponseBody)