48_Spring Boot RESTful API开发

Spring Boot RESTful API开发

文章目录

  • [Spring Boot RESTful API开发](#Spring Boot RESTful API开发)
    • 前言
    • [一、RESTful API设计规范](#一、RESTful API设计规范)
      • [1.1 什么是RESTful](#1.1 什么是RESTful)
      • [1.2 URL与HTTP方法设计](#1.2 URL与HTTP方法设计)
      • [1.3 命名规范](#1.3 命名规范)
    • 二、Controller注解详解
      • [2.1 @RestController与@Controller](#2.1 @RestController与@Controller)
      • [2.2 请求映射注解](#2.2 请求映射注解)
      • [2.3 参数获取方式](#2.3 参数获取方式)
    • 三、参数校验
      • [3.1 引入依赖](#3.1 引入依赖)
      • [3.2 实体类校验注解](#3.2 实体类校验注解)
      • [3.3 Controller校验](#3.3 Controller校验)
      • [3.4 常用校验注解](#3.4 常用校验注解)
      • [3.5 自定义校验注解](#3.5 自定义校验注解)
    • 四、统一返回格式
      • [4.1 通用返回类](#4.1 通用返回类)
      • [4.2 状态码枚举](#4.2 状态码枚举)
      • [4.3 在Controller中使用](#4.3 在Controller中使用)
    • 五、全局异常处理
      • [5.1 自定义业务异常](#5.1 自定义业务异常)
      • [5.2 全局异常处理器](#5.2 全局异常处理器)
      • [5.3 使用业务异常](#5.3 使用业务异常)
    • 六、跨域配置
    • 七、接口文档
    • 总结
    • [✅ 亮点总结](#✅ 亮点总结)
    • 适用场景
    • 扩展方向

前言

在现代Web开发中,RESTful API已成为前后端分离架构的标准通信方式。Spring Boot提供了强大的注解和工具支持,让开发者可以轻松构建规范的RESTful接口。

为什么RESTful很重要? 在前后端分离的架构中,后端只负责提供API接口、返回JSON数据,前端(Vue/React/小程序)通过HTTP请求获取数据并渲染页面。如果没有统一的API设计规范,每个人的接口风格都不一样------有人用GET创建资源、有人用POST查询数据------前后端联调时会耗费大量时间在沟通上。RESTful规范提供了一套统一的设计准则,让API"可预测"。在面试中,RESTful设计原则、HTTP方法语义、统一返回格式等也是常见考点。

本文将详细介绍Controller层的开发技巧,包括参数校验、统一返回格式、全局异常处理等实战内容,帮你构建健壮、规范的API。

一、RESTful API设计规范

1.1 什么是RESTful

REST(Representational State Transfer) 是一种软件架构风格,核心思想是:将服务器上的所有资源都抽象为URL,通过HTTP方法对资源进行增删改查操作。

1.2 URL与HTTP方法设计

以用户管理为例,标准的RESTful API设计如下:

HTTP方法的语义化使用

  • GET:获取资源,幂等且安全(多次请求结果相同,不会修改资源)
  • POST:创建资源,不幂等(多次请求会创建多个资源)
  • PUT:全量更新资源,幂等(多次请求结果相同)
  • PATCH:部分更新资源,不保证幂等
  • DELETE:删除资源,幂等

面试中经常问到的"GET和POST的区别"在RESTful语境下主要是语义不同:GET用于读取,POST用于创建。技术上GET的请求参数在URL中(有长度限制),POST的请求数据在请求体中(无长度限制)。

功能 方法 URL 请求体
查询所有用户 GET /api/users
查询单个用户 GET /api/users/{id}
新增用户 POST /api/users 用户JSON
更新用户 PUT /api/users/{id} 用户JSON
部分更新 PATCH /api/users/{id} 部分字段JSON
删除用户 DELETE /api/users/{id}

1.3 命名规范

复制代码
好的示例:
GET    /api/users              # 获取用户列表
GET    /api/users/1            # 获取ID为1的用户
GET    /api/users/1/orders     # 获取用户1的订单
POST   /api/users              # 新增用户
DELETE /api/users/1            # 删除用户

不好的示例:
GET /api/getUsers              # 不要在URL中使用动词
POST /api/addUser              # 动词由HTTP方法表达
GET /api/user_list             # 使用复数名词

二、Controller注解详解

2.1 @RestController与@Controller

java 复制代码
// @Controller:返回视图名,配合@ResponseBody使用
@Controller
public class UserController {
    @ResponseBody
    @GetMapping("/users")
    public List<User> list() {
        return userService.listAll();
    }
}

// @RestController = @Controller + @ResponseBody(推荐)
@RestController
@RequestMapping("/api/users")
public class UserController {
    // 所有方法返回值都会被序列化为JSON
}

2.2 请求映射注解

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping                    // GET /api/users
    public List<User> list() {}
    
    @GetMapping("/{id}")           // GET /api/users/{id}
    public User getById(@PathVariable Long id) {}
    
    @PostMapping                   // POST /api/users
    public User create(@RequestBody User user) {}
    
    @PutMapping("/{id}")           // PUT /api/users/{id}
    public User update(@PathVariable Long id, @RequestBody User user) {}
    
    @DeleteMapping("/{id}")        // DELETE /api/users/{id}
    public void delete(@PathVariable Long id) {}
}

2.3 参数获取方式

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // 路径参数:/api/users/5
    @GetMapping("/{id}")
    public User getById(@PathVariable("id") Long userId) {}
    
    // 查询参数:/api/users?name=张三&page=1&size=10
    @GetMapping
    public List<User> list(
        @RequestParam(required = false) String name,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "10") int size) {}
    
    // 请求体参数(JSON)
    @PostMapping
    public User create(@RequestBody @Validated User user) {}
    
    // 请求头参数
    @GetMapping("/auth")
    public String auth(@RequestHeader("Authorization") String token) {}
    
    // HttpservletRequest原生对象
    @GetMapping("/info")
    public String info(HttpServletRequest request) {
        return request.getRemoteAddr();
    }
}

三、参数校验

3.1 引入依赖

为什么需要服务端校验? 前端可以做表单验证,但前端验证可以被绕过(通过Postman、curl等工具直接发送请求)。服务端校验是最后一道防线,必须做到位。Spring Boot Validation基于JSR 380(Bean Validation 2.0)标准,通过注解即可实现声明式参数校验,比手写if-else检查每个字段要简洁和规范得多。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3.2 实体类校验注解

java 复制代码
import javax.validation.constraints.*;
import lombok.Data;

@Data
public class UserCreateRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度需在2~20之间")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
    
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
}

3.3 Controller校验

java 复制代码
@PostMapping
public Result<User> create(@Validated @RequestBody UserCreateRequest request) {
    // 通过@Validated触发校验,校验失败会自动抛出MethodArgumentNotValidException
    User user = new User();
    BeanUtils.copyProperties(request, user);
    userService.create(user);
    return Result.success(user);
}

3.4 常用校验注解

注解 说明
@NotNull 不能为null
@NotEmpty 不能为null且长度>0(字符串/集合)
@NotBlank 不能为null且trim后长度>0(字符串)
@Size(min, max) 字符串/集合长度范围
@Min / @Max 数值最小/最大值
@Email 邮箱格式
@Pattern 正则表达式匹配
@Positive / @Negative 正数/负数
@Past / @Future 过去/未来日期

3.5 自定义校验注解

java 复制代码
// 自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {StatusValidator.class})
public @interface ValidStatus {
    String message() default "状态值不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    int[] allowedValues() default {};
}

// 校验器实现
public class StatusValidator implements ConstraintValidator<ValidStatus, Integer> {
    private int[] allowedValues;
    
    @Override
    public void initialize(ValidStatus annotation) {
        this.allowedValues = annotation.allowedValues();
    }
    
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        if (value == null) return true; // null由@NotNull处理
        for (int v : allowedValues) {
            if (v == value) return true;
        }
        return false;
    }
}

// 使用
@ValidStatus(allowedValues = {0, 1, 2}, message = "状态值只能是0、1、2")
private Integer status;

四、统一返回格式

前后端分离项目中,统一的API返回格式便于前端统一处理响应和异常。

为什么需要统一返回格式? 如果没有统一的返回格式,每个接口的返回值格式可能都不一样------有的是直接返回对象,有的是返回Map,出错时的响应格式更是五花八门。前端需要为每种情况编写不同的处理逻辑,代码变得臃肿且易出错。统一返回格式让前端可以只需要一个拦截器(如axios的response拦截器)就能处理所有接口的成功和失败情况,大大简化了前端代码。

4.1 通用返回类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp;
    
    // 成功响应
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data, System.currentTimeMillis());
    }
    
    public static <T> Result<T> success() {
        return success(null);
    }
    
    // 失败响应
    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null, System.currentTimeMillis());
    }
    
    public static <T> Result<T> error(String message) {
        return error(500, message);
    }
}

4.2 状态码枚举

java 复制代码
public enum ResultCode {
    SUCCESS(200, "操作成功"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "无权限"),
    NOT_FOUND(404, "资源不存在"),
    INTERNAL_ERROR(500, "服务器内部错误");
    
    private final int code;
    private final String message;
    
    ResultCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
    // getter方法省略
}

4.3 在Controller中使用

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public Result<List<User>> list() {
        List<User> users = userService.listAll();
        return Result.success(users);
    }
    
    @PostMapping
    public Result<User> create(@Validated @RequestBody UserCreateRequest request) {
        User user = userService.create(request);
        return Result.success(user);
    }
    
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success();
    }
}

五、全局异常处理

5.1 自定义业务异常

为什么需要全局异常处理? 在一个没有全局异常处理的项目中,异常处理代码散落在各个Controller方法中,到处都是try-catch,代码冗余且容易遗漏。更糟糕的是,未被捕获的异常会暴露给前端------用户可能看到500错误页面、堆栈信息等,既不友好也不安全。全局异常处理器通过@RestControllerAdvice统一拦截所有异常,根据异常类型返回规范的错误响应,让Controller代码保持干净。

java 复制代码
public class BusinessException extends RuntimeException {
    private int code;
    
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    
    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }
    
    public BusinessException(ResultCode resultCode) {
        super(resultCode.getMessage());
        this.code = resultCode.getCode();
    }
    // getter省略
}

5.2 全局异常处理器

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining("; "));
        return Result.error(400, message);
    }
    
    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException ex) {
        return Result.error(ex.getCode(), ex.getMessage());
    }
    
    // 处理运行时异常
    @ExceptionHandler(RuntimeException.class)
    public Result<Void> handleRuntimeException(RuntimeException ex) {
        // 生产环境建议只返回"服务器内部错误",详细信息记日志
        return Result.error(500, "服务器内部错误: " + ex.getMessage());
    }
    
    // 处理所有未被捕获的异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception ex) {
        return Result.error(500, "系统异常,请联系管理员");
    }
}

异常处理的优先级:Spring会按照@ExceptionHandler中声明的异常类型从具体到泛化进行匹配。如果你同时定义了BusinessException的处理器和RuntimeException的处理器,当抛出BusinessException时,会优先匹配BusinessException的处理器。这就是为什么要把具体的异常处理器放在前面、通用的异常处理器放在后面------确保每个异常都被最合适的处理器处理。建议顺序:自定义业务异常 → 参数校验异常 → 运行时异常 → Exception兜底。

5.3 使用业务异常

java 复制代码
@Service
public class UserService {
    
    public User getById(Long id) {
        User user = userMapper.findById(id);
        if (user == null) {
            throw new BusinessException(ResultCode.NOT_FOUND);
        }
        return user;
    }
}

六、跨域配置

前后端分离开发中,需要处理CORS跨域问题:

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")          // 允许跨域的路径
            .allowedOriginPatterns("*")          // 允许的来源
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .allowedHeaders("*")                 // 允许的请求头
            .allowCredentials(true)              // 是否允许携带Cookie
            .maxAge(3600);                       // 预检请求缓存时间
    }
}

或者使用 @CrossOrigin 注解(适合少数接口):

java 复制代码
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {}

七、接口文档

推荐使用Swagger(SpringDoc)自动生成接口文档:

xml 复制代码
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>
java 复制代码
@Operation(summary = "查询用户列表", description = "支持分页和条件筛选")
@GetMapping
public Result<PageInfo<User>> list(
    @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
    @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") int size) {
    // ...
}

项目启动后访问 http://localhost:8080/swagger-ui.html 即可看到接口文档。

总结

本文详细介绍了Spring Boot开发RESTful API的核心内容:URL设计规范、常用注解、参数校验、统一返回格式和全局异常处理。一个规范的API应该具备以下特质:URL命名语义化参数校验完备返回格式统一异常处理友好。掌握这些技能,你就能构建出前后端协作顺畅的API服务了。

RESTful API开发核心清单

  1. URL设计:名词复数、层级关系、HTTP方法语义化
  2. 参数获取:@PathVariable(路径参数)、@RequestParam(查询参数)、@RequestBody(JSON请求体)、@RequestHeader(请求头)
  3. 参数校验:@Validated + Bean Validation注解链,必要时自定义校验器
  4. 统一返回:Result<T>(code + message + data + timestamp)
  5. 异常处理:@RestControllerAdvice分层捕获,业务异常→校验异常→运行时异常→兜底异常
  6. 跨域配置:CorsConfig + WebMvcConfigurer 或 @CrossOrigin注解

✅ 亮点总结

  • RESTful URL规范:名词复数、HTTP方法语义化(GET查/POST增/PUT改/DELETE删)
  • @Validated + 校验注解(@NotBlank/@Email/@Pattern等)实现声明式参数校验
  • 统一返回格式Result<T>:封装code/message/data/timestamp,前端统一处理
  • @RestControllerAdvice全局异常处理:分类捕获参数校验异常、业务异常、系统异常
  • CORS跨域配置 + Swagger/SpringDoc接口文档自动生成,提升前后端协作效率

适用场景

  • 前后端分离项目:Vue/React前端通过Axios调用RESTful API获取JSON数据
  • 移动端后端服务:Android/iOS App通过网络请求调用统一格式的API接口
  • 开放平台API设计:对外提供规范的RESTful接口,配合Swagger文档方便第三方接入

扩展方向

  • 学习Spring Security + JWT实现无状态认证和接口鉴权
  • 了解OpenAPI 3.0规范,使用SpringDoc生成标准化的接口文档
  • 推荐阅读下一篇文章:MySQL基础与JDBC操作,夯实数据库底层知识