Appearance
RESTful API 设计
REST (Representational State Transfer) 是一种软件架构风格,用于设计网络应用程序。RESTful API 是遵循 REST 原则的应用程序接口,它已成为现代 Web 应用开发的标准。Spring MVC 提供了强大的支持,使开发人员能够轻松构建符合 REST 规范的 API。
REST 架构原则
REST 由 Roy Fielding 在他的博士论文中提出,它基于以下核心原则:
- 资源标识:通过 URI 唯一标识每个资源
- 统一接口:使用标准的 HTTP 方法操作资源
- 自描述消息:包含足够信息以处理请求
- 无状态通信:服务器不存储客户端状态
- 超媒体驱动:客户端通过超链接发现可用操作(HATEOAS)
RESTful API 的 HTTP 方法
RESTful API 利用 HTTP 方法对资源执行操作,主要方法包括:
HTTP 方法 | CRUD 操作 | 描述 | 幂等性 | 安全性 |
---|---|---|---|---|
GET | Read | 获取资源 | 是 | 是 |
POST | Create | 创建资源 | 否 | 否 |
PUT | Update/Replace | 完全替换资源 | 是 | 否 |
PATCH | Update/Modify | 部分更新资源 | 是 | 否 |
DELETE | Delete | 删除资源 | 是 | 否 |
幂等性:多次重复执行同一请求产生的效果与执行一次相同
安全性:请求不会修改服务器上的资源
使用 Spring MVC 构建 RESTful API
Spring MVC 提供了丰富的功能来支持 RESTful API 开发。
1. @RestController 注解
@RestController
是 @Controller
和 @ResponseBody
的组合注解。它表示该控制器的所有方法返回值都直接作为响应体,而不是视图名。
java
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> getAllProducts() {
return productService.findAll();
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@RequestBody Product product) {
return productService.save(product);
}
@PutMapping("/{id}")
public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
return productService.update(product);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id) {
productService.deleteById(id);
}
}
2. 请求映射注解
Spring MVC 提供了针对不同 HTTP 方法的注解:
@GetMapping
:处理 GET 请求@PostMapping
:处理 POST 请求@PutMapping
:处理 PUT 请求@PatchMapping
:处理 PATCH 请求@DeleteMapping
:处理 DELETE 请求
这些注解是 @RequestMapping
的便捷版本。
3. 路径变量和请求参数
路径变量:使用 @PathVariable
从 URL 路径中提取参数。
java
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
// 处理逻辑
}
// 多个路径变量
@GetMapping("/{categoryId}/products/{productId}")
public Product getProductInCategory(
@PathVariable Long categoryId,
@PathVariable Long productId) {
// 处理逻辑
}
请求参数:使用 @RequestParam
处理查询参数。
java
@GetMapping
public List<Product> getAllProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name,asc") String sort) {
// 处理逻辑
}
4. 请求体
使用 @RequestBody
注解将请求体转换为 Java 对象:
java
@PostMapping
public Product createProduct(@RequestBody Product product) {
// 处理逻辑
}
@RequestBody
与 Spring 的 HttpMessageConverter
接口配合工作,根据请求的 Content-Type
头选择合适的转换器。
5. 响应状态
可以使用 @ResponseStatus
注解设置响应状态码:
java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@RequestBody Product product) {
// 处理逻辑
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id) {
// 处理逻辑
}
也可以使用 ResponseEntity
更灵活地控制响应:
java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(product -> ResponseEntity.ok().body(product))
.orElse(ResponseEntity.notFound().build());
}
6. 内容协商
Spring MVC 支持基于请求的 Accept
头进行内容协商,自动选择合适的消息转换器:
java
@GetMapping(value = "/{id}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public Product getProduct(@PathVariable Long id) {
// 根据请求的 Accept 头,同一方法可以返回 JSON 或 XML
return productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
}
7. 异常处理
对于 RESTful API,异常处理通常使用 @RestControllerAdvice
和 @ExceptionHandler
:
java
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException ex) {
return new ErrorResponse("Resource not found", ex.getMessage());
}
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationException(ValidationException ex) {
return new ErrorResponse("Validation error", ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception ex) {
return new ErrorResponse("Server error", "An unexpected error occurred");
}
}
@Data
@AllArgsConstructor
class ErrorResponse {
private String error;
private String message;
private Instant timestamp = Instant.now();
public ErrorResponse(String error, String message) {
this.error = error;
this.message = message;
}
}
RESTful API 设计最佳实践
1. URL 设计
RESTful API 的 URL 应该清晰地表达资源的层次结构:
资源集合和单个资源
GET /api/products # 获取产品集合
GET /api/products/{id} # 获取单个产品
子资源关系
GET /api/products/{id}/reviews # 获取产品的评论
GET /api/users/{id}/orders # 获取用户的订单
查询参数用于过滤和分页
GET /api/products?category=electronics # 按类别过滤
GET /api/products?page=1&size=10 # 分页
GET /api/products?sort=price,desc # 排序
避免动词,使用名词
# 不推荐
GET /api/getProducts
POST /api/createProduct
# 推荐
GET /api/products
POST /api/products
2. HTTP 状态码
正确使用 HTTP 状态码传达请求处理结果:
状态码 | 描述 | 示例场景 |
---|---|---|
200 OK | 请求成功 | 获取资源成功 |
201 Created | 创建资源成功 | 创建新资源 |
204 No Content | 成功但无返回数据 | 删除资源成功 |
400 Bad Request | 请求格式错误 | 提交的数据无效 |
401 Unauthorized | 未认证 | 需要登录 |
403 Forbidden | 权限不足 | 尝试访问禁止的资源 |
404 Not Found | 资源不存在 | 请求不存在的资源 |
405 Method Not Allowed | 不支持的 HTTP 方法 | 对资源使用不支持的方法 |
409 Conflict | 资源状态冲突 | 并发更新冲突 |
415 Unsupported Media Type | 不支持的媒体类型 | 提交了不支持的格式 |
422 Unprocessable Entity | 无法处理的实体 | 语义错误,如验证失败 |
429 Too Many Requests | 请求过多 | 超出速率限制 |
500 Internal Server Error | 服务器错误 | 服务器发生异常 |
3. 版本控制
API 版本控制有多种策略:
URL 路径版本控制
/api/v1/products
/api/v2/products
查询参数版本控制
/api/products?version=1
/api/products?version=2
请求头版本控制
Accept: application/json; version=1
Accept: application/json; version=2
自定义请求头版本控制
X-API-Version: 1
X-API-Version: 2
内容协商版本控制
Accept: application/vnd.company.v1+json
Accept: application/vnd.company.v2+json
4. 数据格式
JSON 格式约定
- 使用驼峰命名法命名属性
- 日期和时间使用 ISO 8601 格式
- 布尔值使用
true
/false
,不使用1
/0
- 保持数据类型一致性
json
{
"id": 12345,
"name": "Product Name",
"price": 99.99,
"inStock": true,
"categories": ["electronics", "gadgets"],
"createdAt": "2023-01-15T14:30:00Z",
"details": {
"weight": 0.5,
"dimensions": {
"length": 10,
"width": 5,
"height": 2
}
}
}
错误响应格式
标准化错误响应格式有助于客户端处理错误:
json
{
"status": 400,
"error": "Bad Request",
"message": "Invalid product data",
"timestamp": "2023-01-15T14:30:00Z",
"details": [
"Product name cannot be empty",
"Price must be positive"
],
"path": "/api/products"
}
5. 分页和排序
分页和排序是 API 的重要功能:
GET /api/products?page=0&size=20&sort=price,desc&sort=name,asc
分页响应应包含元数据:
json
{
"content": [
// 产品数据
],
"pageable": {
"page": 0,
"size": 20,
"sort": [
{"property": "price", "direction": "DESC"},
{"property": "name", "direction": "ASC"}
]
},
"totalElements": 100,
"totalPages": 5,
"first": true,
"last": false,
"number": 0,
"numberOfElements": 20,
"size": 20
}
6. HATEOAS
HATEOAS (Hypermedia as the Engine of Application State) 是 REST 的一个约束,它使 API 能够通过超链接指示可用操作。
Spring 提供了 Spring HATEOAS 库来支持这一功能:
java
@GetMapping("/{id}")
public EntityModel<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
return EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products"),
linkTo(methodOn(ReviewController.class).getReviewsForProduct(id)).withRel("reviews"),
linkTo(methodOn(CategoryController.class).getCategory(product.getCategoryId())).withRel("category")
);
}
响应示例:
json
{
"id": 12345,
"name": "Product Name",
"price": 99.99,
"_links": {
"self": {
"href": "http://api.example.com/products/12345"
},
"products": {
"href": "http://api.example.com/products"
},
"reviews": {
"href": "http://api.example.com/products/12345/reviews"
},
"category": {
"href": "http://api.example.com/categories/5"
}
}
}
7. 文档
好的 API 文档对开发人员十分重要。Spring 生态系统支持多种 API 文档工具:
Swagger/OpenAPI
与 Spring Boot 集成 Swagger:
java
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.api"))
.paths(PathSelectors.ant("/api/**"))
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Product API")
.description("API for product management")
.version("1.0.0")
.contact(new Contact("API Team", "https://example.com", "api@example.com"))
.build();
}
}
在控制器和模型中添加 Swagger 注解:
java
@RestController
@RequestMapping("/api/products")
@Api(tags = "Product Management")
public class ProductController {
@GetMapping
@ApiOperation(value = "Get all products", notes = "Retrieves the list of all products")
@ApiResponses({
@ApiResponse(code = 200, message = "Successfully retrieved products"),
@ApiResponse(code = 500, message = "Server error")
})
public List<Product> getAllProducts(
@ApiParam(value = "Category filter", example = "electronics")
@RequestParam(required = false) String category) {
// ...
}
}
@Data
@ApiModel(description = "Product information")
public class Product {
@ApiModelProperty(value = "Unique product identifier", example = "12345")
private Long id;
@ApiModelProperty(value = "Product name", example = "Smartphone", required = true)
private String name;
@ApiModelProperty(value = "Product price", example = "599.99", required = true)
private BigDecimal price;
// ...
}
Spring REST Docs
Spring REST Docs 结合了手写文档和自动生成的片段:
java
@WebMvcTest(ProductController.class)
@AutoConfigureRestDocs
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
public void getProductShouldReturnProduct() throws Exception {
Product product = new Product(1L, "Product Name", BigDecimal.valueOf(99.99));
given(productService.findById(1L)).willReturn(Optional.of(product));
mockMvc.perform(get("/api/products/{id}", 1L)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Product Name"))
.andExpect(jsonPath("$.price").value(99.99))
.andDo(document("get-product",
pathParameters(
parameterWithName("id").description("Product identifier")
),
responseFields(
fieldWithPath("id").description("Product identifier"),
fieldWithPath("name").description("Product name"),
fieldWithPath("price").description("Product price")
)
));
}
}
高级主题
1. 请求验证
使用 Bean Validation (JSR-380) 验证请求数据:
java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@Valid @RequestBody ProductDTO productDTO) {
// 如果验证失败,会抛出 MethodArgumentNotValidException
return productService.save(convertToEntity(productDTO));
}
@Data
public class ProductDTO {
@NotBlank(message = "Product name is required")
@Size(min = 2, max = 100, message = "Product name must be between 2 and 100 characters")
private String name;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
private BigDecimal price;
@Size(max = 500, message = "Description must not exceed 500 characters")
private String description;
@NotNull(message = "Category is required")
private Long categoryId;
// ...
}
2. 安全性
使用 Spring Security 保护 RESTful API:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
或者使用 JWT 认证:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
3. 速率限制
使用 Spring Cloud Gateway 或自定义实现进行 API 速率限制:
java
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimiter rateLimiter;
public RateLimitInterceptor() {
// 允许每秒 10 个请求
this.rateLimiter = RateLimiter.create(10);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!rateLimiter.tryAcquire()) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too many requests, please try again later");
return false;
}
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RateLimitInterceptor rateLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/api/**");
}
}
4. 缓存
使用 HTTP 缓存头控制客户端缓存:
java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(product -> ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES))
.eTag(Integer.toString(product.hashCode()))
.body(product))
.orElse(ResponseEntity.notFound().build());
}
使用条件请求减少数据传输:
java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(
@PathVariable Long id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
Optional<Product> productOpt = productService.findById(id);
if (productOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
Product product = productOpt.get();
String eTag = Integer.toString(product.hashCode());
if (eTag.equals(ifNoneMatch)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(eTag)
.build();
}
return ResponseEntity.ok()
.eTag(eTag)
.body(product);
}
实战示例:完整的产品 API
下面是一个完整的产品 API 实现示例,展示了 Spring MVC 中 RESTful API 设计的最佳实践:
java
@RestController
@RequestMapping("/api/products")
@Validated
public class ProductController {
private final ProductService productService;
private final PagedResourcesAssembler<Product> pagedResourcesAssembler;
@Autowired
public ProductController(ProductService productService,
PagedResourcesAssembler<Product> pagedResourcesAssembler) {
this.productService = productService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
}
@GetMapping
public ResponseEntity<PagedModel<EntityModel<Product>>> getAllProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name,asc") String[] sort) {
Pageable pageable = createPageable(page, size, sort);
Page<Product> products = productService.findProducts(category, minPrice, maxPrice, pageable);
Link selfLink = linkTo(methodOn(ProductController.class)
.getAllProducts(category, minPrice, maxPrice, page, size, sort))
.withSelfRel();
PagedModel<EntityModel<Product>> pagedModel = pagedResourcesAssembler
.toModel(products, product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(product.getId())).withSelfRel()));
pagedModel.add(selfLink);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
.body(pagedModel);
}
@GetMapping("/{id}")
public ResponseEntity<EntityModel<Product>> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(product -> {
EntityModel<Product> model = EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(null, null, null, 0, 10, new String[]{"name,asc"})).withRel("products"),
linkTo(methodOn(ReviewController.class).getReviewsForProduct(id)).withRel("reviews"));
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES))
.eTag(Integer.toString(product.hashCode()))
.body(model);
})
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<EntityModel<Product>> createProduct(@Valid @RequestBody ProductDTO productDTO) {
Product newProduct = productService.save(convertToEntity(productDTO));
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(newProduct.getId())
.toUri();
EntityModel<Product> model = EntityModel.of(newProduct,
linkTo(methodOn(ProductController.class).getProduct(newProduct.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(null, null, null, 0, 10, new String[]{"name,asc"})).withRel("products"));
return ResponseEntity.created(location).body(model);
}
@PutMapping("/{id}")
public ResponseEntity<EntityModel<Product>> updateProduct(
@PathVariable Long id,
@Valid @RequestBody ProductDTO productDTO) {
if (!productService.existsById(id)) {
return ResponseEntity.notFound().build();
}
Product updatedProduct = productService.update(id, convertToEntity(productDTO));
EntityModel<Product> model = EntityModel.of(updatedProduct,
linkTo(methodOn(ProductController.class).getProduct(updatedProduct.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(null, null, null, 0, 10, new String[]{"name,asc"})).withRel("products"));
return ResponseEntity.ok().body(model);
}
@PatchMapping("/{id}")
public ResponseEntity<EntityModel<Product>> partialUpdateProduct(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
if (!productService.existsById(id)) {
return ResponseEntity.notFound().build();
}
Product updatedProduct = productService.partialUpdate(id, updates);
EntityModel<Product> model = EntityModel.of(updatedProduct,
linkTo(methodOn(ProductController.class).getProduct(updatedProduct.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(null, null, null, 0, 10, new String[]{"name,asc"})).withRel("products"));
return ResponseEntity.ok().body(model);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (!productService.existsById(id)) {
return ResponseEntity.notFound().build();
}
productService.deleteById(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/reviews")
public ResponseEntity<CollectionModel<EntityModel<Review>>> getProductReviews(@PathVariable Long id) {
if (!productService.existsById(id)) {
return ResponseEntity.notFound().build();
}
List<Review> reviews = productService.findReviewsByProductId(id);
List<EntityModel<Review>> reviewModels = reviews.stream()
.map(review -> EntityModel.of(review,
linkTo(methodOn(ReviewController.class).getReview(id, review.getId())).withSelfRel(),
linkTo(methodOn(ReviewController.class).getReviewsForProduct(id)).withRel("productReviews"),
linkTo(methodOn(ProductController.class).getProduct(id)).withRel("product")))
.collect(Collectors.toList());
return ResponseEntity.ok(
CollectionModel.of(reviewModels,
linkTo(methodOn(ProductController.class).getProductReviews(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getProduct(id)).withRel("product"))
);
}
// Helper methods
private Pageable createPageable(int page, int size, String[] sort) {
List<Sort.Order> orders = new ArrayList<>();
if (sort[0].contains(",")) {
for (String sortItem : sort) {
String[] parts = sortItem.split(",");
Sort.Direction direction = parts.length > 1 && parts[1].equalsIgnoreCase("desc") ?
Sort.Direction.DESC : Sort.Direction.ASC;
orders.add(new Sort.Order(direction, parts[0]));
}
} else {
// 默认按名称升序排序
orders.add(new Sort.Order(Sort.Direction.ASC, "name"));
}
return PageRequest.of(page, size, Sort.by(orders));
}
private Product convertToEntity(ProductDTO dto) {
Product product = new Product();
product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setDescription(dto.getDescription());
product.setCategoryId(dto.getCategoryId());
// 设置其他属性
return product;
}
}
总结
Spring MVC 提供了强大而灵活的支持,使开发人员能够轻松构建符合 REST 原则的 API。通过遵循本文中讨论的最佳实践,可以创建出一个易于使用、可维护且可扩展的 RESTful API。
关键要点包括:
- 使用恰当的 HTTP 方法和状态码
- 设计清晰、一致的 URL 结构
- 实现有效的错误处理
- 提供分页、排序和过滤功能
- 考虑版本控制策略
- 使用 HATEOAS 提高 API 的可发现性
- 提供全面的 API 文档
通过将这些原则与 Spring MVC 的功能相结合,可以设计出满足现代应用需求的高质量 RESTful API。