跳到主要内容

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/**"
);
}
}

小结

本章我们学习了:

  1. RESTful API 设计:资源设计和 HTTP 方法映射
  2. 请求处理:各种参数获取方式
  3. 响应处理:统一响应格式和 ResponseEntity
  4. 参数验证:验证注解和自定义验证器
  5. 异常处理:全局异常处理器
  6. 跨域处理:多种跨域解决方案
  7. 文件上传下载:文件处理和配置
  8. 拦截器:请求拦截和预处理

练习

  1. 设计并实现一个完整的用户管理 RESTful API
  2. 为 API 添加参数验证
  3. 实现全局异常处理
  4. 配置 CORS 允许前端跨域访问
  5. 实现文件上传和下载功能