springboot

RESTful API 与 Web 开发

By AI-Writer 11 min read

RESTful API 与 Web 开发

Spring MVC 是构建 Web 应用的基石,而 RESTful API 是现代前后端分离架构的标准接口形式。本篇文章将系统讲解从控制器注解到参数绑定的全部核心技能,并涵盖参数校验与统一响应封装。

控制器注解体系

@RestController vs @Controller

java
// @RestController = @Controller + @ResponseBody
// 返回值直接写入 HTTP 响应体(JSON/XML)
@RestController
@RequestMapping("/api/users")
public class UserApiController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // 返回 User 对象,Spring 自动序列化为 JSON
        return userService.findById(id);
    }
}
java
// 如果需要返回视图(Thymeleaf/Freemarker),使用 @Controller
@Controller
@RequestMapping("/users")
public class UserViewController {

    @GetMapping("/list")
    public String listPage(Model model) {
        model.addAttribute("users", userService.findAll());
        return "user/list";  // 返回视图名,由视图解析器渲染模板
    }

    // 如果 @Controller 中某个方法需要返回 JSON,使用 @ResponseBody
    @GetMapping("/api/list")
    @ResponseBody
    public List<User> listApi() {
        return userService.findAll();
    }
}

请求映射 @RequestMapping 系列

@RequestMapping 是最通用的映射注解,Spring 提供了更语义化的快捷方式:

java
@RestController
@RequestMapping("/api/users")  // 类级别:所有方法共享的前缀
public class UserController {

    // @GetMapping 是 @RequestMapping(method = RequestMethod.GET) 的快捷写法
    @GetMapping           // 查询列表
    public List<User> list() { ... }

    @GetMapping("/{id}")  // 查询单个
    public User get(@PathVariable Long id) { ... }

    @PostMapping          // 创建
    public User create(@RequestBody @Valid CreateUserRequest request) { ... }

    @PutMapping("/{id}")  // 完整更新
    public User update(@PathVariable Long id,
                       @RequestBody @Valid UpdateUserRequest request) { ... }

    @PatchMapping("/{id}") // 部分更新
    public User patch(@PathVariable Long id,
                      @RequestBody Map<String, Object> fields) { ... }

    @DeleteMapping("/{id}") // 删除
    public void delete(@PathVariable Long id) { ... }
}

映射变体速查

java
// Ant 风格路径(Spring MVC 支持)
@GetMapping("/api/**/users")       // 匹配 /api/aaa/users、/api/bbb/users
@GetMapping("/api/users/*.json")    // 匹配 /api/users/1.json
@GetMapping("/api/users/?")         // 匹配 /api/users/1、/api/users/2

// PathVariable 支持正则(Spring MVC 支持)
@GetMapping("/api/users/{id:\\d+}") // 仅匹配数字 ID
@GetMapping("/api/users/{name:[a-z]+}") // 仅匹配小写字母 name

// 请求头 / Content-Type 条件映射
@GetMapping(value = "/data", produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

// 请求头条件映射
@GetMapping(value = "/preview", headers = "X-Preview=true")
public String preview() { ... }

参数绑定

@PathVariable — 路径变量

java
// 单个路径变量
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    // @PathVariable("id") 或省略名称(自动推断)
    return userService.findById(id);
}

// 多个路径变量
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
        @PathVariable Long userId,
        @PathVariable Long orderId) {
    return orderService.findByUserAndOrder(userId, orderId);
}

// 路径变量重命名(适配 snake_case URL)
@GetMapping("/users/{user-id}")
public User getByUserId(@PathVariable("user-id") Long id) {
    // 访问 /users/123 -> id = 123
    return userService.findById(id);
}

@RequestParam — 查询参数

java
// ?page=0&size=20&sort=createdAt,desc
@GetMapping("/users")
public Page<User> list(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(required = false) String sort,
        @RequestParam(required = false) String keyword) {
    return userService.search(keyword, page, size, sort);
}

// 多值参数(?tags=spring&tags=java)
@GetMapping("/articles")
public List<Article> search(@RequestParam List<String> tags) {
    return articleService.findByTagsIn(tags);
}

// JSON 请求体(POST / PUT)
@PostMapping("/users")
public User create(@RequestBody CreateUserRequest request) {
    return userService.create(request);
}

@RequestBody — 请求体自动反序列化

Spring Boot 默认使用 Jackson 将 JSON 反序列化为 Java 对象:

java
// 创建请求 DTO(使用 record,Spring Boot 3 推荐)
public record CreateUserRequest(
        String username,
        String email,
        @Email(message = "邮箱格式不正确") String emailField,
        Integer age
) {}

// @Valid 触发 Jakarta Bean Validation(Spring Boot Starter Validation 已自动配置)
@PostMapping("/users")
public User create(@RequestBody @Valid CreateUserRequest request,
                   BindingResult result) {
    if (result.hasErrors()) {
        throw new ValidationException(result.getFieldErrors()
                .stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .collect(Collectors.joining("; ")));
    }
    return userService.create(request);
}

@RequestAttribute 与 @SessionAttribute

在非同步请求或拦截器场景中获取请求属性和会话属性:

java
@GetMapping("/profile")
public UserProfile profile(
        @RequestAttribute("tenantId") String tenantId,
        @SessionAttribute("userId") Long userId,
        @CookieValue(value = "SESSION", required = false) String sessionId) {
    return userService.getProfile(tenantId, userId);
}

REST 风格 URL 设计规范

良好的 REST API 设计遵循以下原则:

plaintext
# 资源命名:使用复数名词,名词而非动词
✅ GET    /api/users          # 用户列表
✅ GET    /api/users/123      # 单个用户
✅ POST   /api/users          # 创建用户
✅ PUT    /api/users/123      # 完整更新用户
✅ PATCH  /api/users/123      # 部分更新用户
✅ DELETE /api/users/123     # 删除用户

❌ GET /getUser?id=123        # 避免动词
❌ POST /createUser           # 避免动词
❌ GET /api/getAllUsers       # 避免动词

嵌套资源反映资源间的从属关系:

plaintext
GET    /api/users/123/orders           # 用户123的所有订单
GET    /api/users/123/orders/456       # 用户123的订单456
POST   /api/users/123/orders           # 为用户123创建订单
DELETE /api/users/123/orders/456        # 删除用户123的订单456

统一响应封装

每个 API 返回统一的响应结构,便于前端处理:

java
// 响应封装类(使用 record)
public record ApiResponse<T>(
        int code,
        String message,
        T data,
        long timestamp
) {
    // 静态工厂方法
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "OK", data, System.currentTimeMillis());
    }

    public static <T> ApiResponse<T> created(T data) {
        return new ApiResponse<>(201, "Created", data, System.currentTimeMillis());
    }

    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null, System.currentTimeMillis());
    }
}

// 全局异常处理(@ControllerAdvice)
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleNotFound(UserNotFoundException ex) {
        return ApiResponse.error(404, ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> errors = ex.getBindingResult().getFieldErrors()
                .stream()
                .collect(Collectors.toMap(
                        FieldError::getField,
                        e -> e.getDefaultMessage() != null ? e.getDefaultMessage() : "Invalid value",
                        (a, b) -> a
                ));
        return new ApiResponse<>(400, "Validation failed", errors, System.currentTimeMillis());
    }
}

控制器改写:

java
@PostMapping
public ResponseEntity<ApiResponse<User>> create(
        @Valid @RequestBody CreateUserRequest request) {
    User user = userService.create(request);
    return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(ApiResponse.created(user));
}

@Validated 参数校验

Spring Boot 已自动配置 Hibernate Validator(Jakarta Bean Validation 的实现),无需额外依赖。

常用校验注解

java
public record CreateUserRequest(
        @NotBlank(message = "用户名不能为空")           // 非空字符串
        @Size(min = 3, max = 20, message = "用户名3-20位")
        String username,

        @NotBlank
        @Email(message = "邮箱格式不正确")
        String email,

        @NotNull(message = "年龄不能为空")
        @Min(value = 0, message = "年龄不能为负数")
        @Max(value = 150, message = "年龄不合理")
        Integer age,

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

        @NotBlank
        @Size(min = 6, max = 20)
        String password,

        // 分组校验:不同场景用不同分组
        @NotNull(groups = {UpdateGroup.class})
        Long id
) {}

分组校验

java
// 定义校验分组
public interface UpdateGroup {}
public interface CreateGroup {}

// 请求对象使用分组
public record UpdateUserRequest(
        @NotNull(groups = UpdateGroup.class) Long id,
        @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}) String name
) {}

// 控制器中指定分组
@PostMapping
public void create(@Validated(CreateGroup.class) @RequestBody UpdateUserRequest req) { }

@PutMapping
public void update(@Validated(UpdateGroup.class) @RequestBody UpdateUserRequest req) { }

自定义校验注解

java
// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
@NotNull
public @interface EnumValue {
    String message() default "值不在允许范围内";
    Class<? extends Enum<?>[]> enumClass();
    String method() default "name";
}

// 2. 实现校验器
public class EnumValidator implements ConstraintValidator<EnumValue, String> {
    private Enum<?>[] enumConstants;

    @Override
    public void initialize(EnumValue annotation) {
        enumConstants = annotation.enumClass().getEnumConstants();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;  // @NotNull 负责空值校验
        return Arrays.stream(enumConstants)
                .anyMatch(e -> e.name().equals(value));
    }
}

// 3. 使用
public record CreateArticleRequest(
        @EnumValue(enumClass = Category.class) String category
) {}

小结

  • @RestController = @Controller + @ResponseBody,适合前后端分离的 REST API
  • @GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMapping 是标准 HTTP 方法映射
  • @PathVariable 绑定 URL 路径变量,@RequestParam 绑定查询参数,@RequestBody 绑定请求体 JSON
  • REST API 命名使用复数名词和标准 HTTP 方法,避免在 URL 中使用动词
  • 使用 @Validated + Jakarta Bean Validation 注解实现声明式参数校验
  • @ControllerAdvice + @ExceptionHandler 实现全局统一异常处理和响应封装

下一篇文章我们将学习 数据访问:JPA 与 MyBatis,掌握持久层的完整开发技能。

#springboot #rest-api #spring-mvc #validation

评论

A

Written by

AI-Writer

Related Articles

springboot
#2

Spring Boot 自动配置原理

深入理解 @SpringBootApplication 注解组合、自动配置生效机制(spring.factories 与 AutoConfiguration.imports)、自定义 Starter 的编写方法

Read More
springboot
#9

Docker 容器化与生产部署

Spring Boot fat jar 原理、分层 Docker 镜像构建(layered jar)、docker-compose 多容器编排、GitHub Actions CI/CD 流水线与生产环境配置示例

Read More