前言
RESTful API 是现代 Web 服务的标准设计方式。Spring Boot 提供了强大的支持,让 RESTful API 开发变得简单高效。本文将介绍 RESTful API 的完整开发流程和最佳实践。
RESTful 设计规范
资源命名
推荐:使用名词复数,小写,连字符分隔
// ✅ 推荐
GET /api/users
GET /api/user-profiles
GET /api/order-items
// ❌ 不推荐
GET /api/getUsers
GET /api/user
GET /api/UserProfiles
HTTP 方法
| 方法 | 说明 | 幂等性 |
|---|---|---|
| GET | 查询资源 | 是 |
| POST | 创建资源 | 否 |
| PUT | 更新资源(全量) | 是 |
| PATCH | 更新资源(部分) | 否 |
| DELETE | 删除资源 | 是 |
状态码
| 状态码 | 说明 |
|---|---|
| 200 OK | 请求成功 |
| 201 Created | 资源创建成功 |
| 204 No Content | 删除成功 |
| 400 Bad Request | 请求参数错误 |
| 401 Unauthorized | 未授权 |
| 403 Forbidden | 禁止访问 |
| 404 Not Found | 资源不存在 |
| 500 Internal Server Error | 服务器错误 |
基础 Controller 开发
1. 创建实体类
package com.example.demo.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
2. 创建 DTO
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class UserDTO {
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
3. 创建 Controller
package com.example.demo.controller;
import com.example.demo.dto.UserDTO;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 获取用户列表
*/
@GetMapping
public ResponseEntity<List<UserDTO>> getUsers() {
List<UserDTO> users = userService.findAll();
return ResponseEntity.ok(users);
}
/**
* 获取单个用户
*/
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok(user);
}
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO dto) {
UserDTO created = userService.create(dto);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(created);
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserDTO dto
) {
UserDTO updated = userService.update(id, dto);
return ResponseEntity.ok(updated);
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
统一响应格式
1. 定义响应体
package com.example.demo.common;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class R<T> {
private int code;
private String message;
private T data;
private LocalDateTime timestamp;
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.setCode(200);
r.setMessage("success");
r.setData(data);
r.setTimestamp(LocalDateTime.now());
return r;
}
public static <T> R<T> ok() {
return ok(null);
}
public static <T> R<T> error(String message) {
R<T> r = new R<>();
r.setCode(500);
r.setMessage(message);
r.setTimestamp(LocalDateTime.now());
return r;
}
public static <T> R<T> error(int code, String message) {
R<T> r = new R<>();
r.setCode(code);
r.setMessage(message);
r.setTimestamp(LocalDateTime.now());
return r;
}
}
2. 使用统一响应
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public R<List<UserDTO>> getUsers() {
return R.ok(userService.findAll());
}
@GetMapping("/{id}")
public R<UserDTO> getUser(@PathVariable Long id) {
return R.ok(userService.findById(id));
}
@PostMapping
public R<UserDTO> createUser(@Valid @RequestBody UserDTO dto) {
return R.ok(userService.create(dto));
}
}
分页查询
1. 分页请求
@Data
public class PageRequest {
@Min(value = 1, message = "页码最小值为 1")
private int page = 1;
@Min(value = 1, message = "每页大小最小值为 1")
@Max(value = 100, message = "每页大小最大值为 100")
private int size = 10;
private String sortField;
private String sortOrder = "asc";
}
2. 分页响应
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int size;
private int pages;
public static <T> PageResult<T> of(List<T> list, long total, int page, int size) {
return new PageResult<>(list, total, page, size, (int) Math.ceil((double) total / size));
}
}
3. 分页接口
@GetMapping
public R<PageResult<UserDTO>> getUsers(
@ModelAttribute PageRequest pageRequest
) {
Pageable pageable = PageRequest.of(
pageRequest.getPage() - 1,
pageRequest.getSize(),
Sort.by(Sort.Direction.fromString(pageRequest.getSortOrder()), pageRequest.getSortField())
);
Page<UserDTO> page = userService.findAll(pageable);
return R.ok(PageResult.of(
page.getContent(),
page.getTotalElements(),
pageRequest.getPage(),
pageRequest.getSize()
));
}
版本控制
1. URI 版本
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// v1 版本接口
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// v2 版本接口
}
2. 请求头版本
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(produces = "application/vnd.demo.v1+json")
public UserDTO getUserV1(@PathVariable Long id) {
// v1 版本
}
@GetMapping(produces = "application/vnd.demo.v2+json")
public UserDTO getUserV2(@PathVariable Long id) {
// v2 版本
}
}
文件上传
@PostMapping("/avatar")
public R<String> uploadAvatar(
@RequestParam("file") MultipartFile file
) {
if (file.isEmpty()) {
return R.error("文件不能为空");
}
// 验证文件类型
String contentType = file.getContentType();
if (!List.of("image/jpeg", "image/png", "image/gif").contains(contentType)) {
return R.error("只支持 JPG、PNG、GIF 格式图片");
}
// 验证文件大小(2MB)
if (file.getSize() > 2 * 1024 * 1024) {
return R.error("文件大小不能超过 2MB");
}
// 保存文件
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get("uploads/" + filename);
try {
Files.write(path, file.getBytes());
return R.ok("/uploads/" + filename);
} catch (IOException e) {
return R.error("上传失败:" + e.getMessage());
}
}
最佳实践
1. 使用 ResponseEntity
// ✅ 推荐
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
// ❌ 不推荐
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.findById(id);
}
2. 使用路径变量
// ✅ 推荐
@GetMapping("/users/{userId}/orders/{orderId}")
public ResponseEntity<OrderDTO> getOrder(
@PathVariable Long userId,
@PathVariable Long orderId
) {
return ResponseEntity.ok(orderService.findById(orderId));
}
// ❌ 不推荐
@GetMapping("/users/{userId}/orders")
public ResponseEntity<OrderDTO> getOrder(
@PathVariable Long userId,
@RequestParam Long orderId
) {
return ResponseEntity.ok(orderService.findById(orderId));
}
3. 使用查询参数过滤
@GetMapping
public ResponseEntity<List<UserDTO>> getUsers(
@RequestParam(required = false) String status,
@RequestParam(required = false) String role,
@RequestParam(required = false) LocalDateTime startDate,
@RequestParam(required = false) LocalDateTime endDate
) {
List<UserDTO> users = userService.findByFilters(status, role, startDate, endDate);
return ResponseEntity.ok(users);
}
4. 使用 HATEOAS
@GetMapping("/{id}")
public ResponseEntity<EntityModel<UserDTO>> getUser(@PathVariable Long id) {
UserDTO user = userService.findById(id);
EntityModel<UserDTO> resource = EntityModel.of(user);
resource.add(linkTo(methodOn(UserController.class).getUser(id)).withSelfRel());
resource.add(linkTo(methodOn(UserController.class).getUsers()).withRel("users"));
return ResponseEntity.ok(resource);
}
总结
RESTful API 开发要点:
- ✅ 资源命名 - 名词复数,小写,连字符
- ✅ HTTP 方法 - GET/POST/PUT/PATCH/DELETE
- ✅ 状态码 - 使用合适的 HTTP 状态码
- ✅ 统一响应 - 统一的响应格式
- ✅ 分页查询 - 支持分页和排序
- ✅ 版本控制 - URI 版本或请求头版本
- ✅ 文件上传 - 验证类型和大小
遵循 RESTful 设计规范,能让 API 更加规范、易用、易维护。