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/**"
);
}
}
小结
本章我们学习了:
- RESTful API 设计:资源设计和 HTTP 方法映射
- 请求处理:各种参数获取方式
- 响应处理:统一响应格式和 ResponseEntity
- 参数验证:验证注解和自定义验证器
- 异常处理:全局异常处理器
- 跨域处理:多种跨域解决方案
- 文件上传下载:文件处理和配置
- 拦截器:请求拦截和预处理
练习
- 设计并实现一个完整的用户管理 RESTful API
- 为 API 添加参数验证
- 实现全局异常处理
- 配置 CORS 允许前端跨域访问
- 实现文件上传和下载功能