Skip to content

RESTful API 设计

REST (Representational State Transfer) 是一种软件架构风格,用于设计网络应用程序。RESTful API 是遵循 REST 原则的应用程序接口,它已成为现代 Web 应用开发的标准。Spring MVC 提供了强大的支持,使开发人员能够轻松构建符合 REST 规范的 API。

REST 架构原则

REST 由 Roy Fielding 在他的博士论文中提出,它基于以下核心原则:

  1. 资源标识:通过 URI 唯一标识每个资源
  2. 统一接口:使用标准的 HTTP 方法操作资源
  3. 自描述消息:包含足够信息以处理请求
  4. 无状态通信:服务器不存储客户端状态
  5. 超媒体驱动:客户端通过超链接发现可用操作(HATEOAS)

RESTful API 的 HTTP 方法

RESTful API 利用 HTTP 方法对资源执行操作,主要方法包括:

HTTP 方法CRUD 操作描述幂等性安全性
GETRead获取资源
POSTCreate创建资源
PUTUpdate/Replace完全替换资源
PATCHUpdate/Modify部分更新资源
DELETEDelete删除资源

幂等性:多次重复执行同一请求产生的效果与执行一次相同

安全性:请求不会修改服务器上的资源

使用 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。