环境:SpringBoot3.2.5
1. 简介
构建健壮且高效的REST API是现代应用开发的关键。虽然Spring Boot简化了这一过程,但开发人员经常会犯一些错误,这些错误可能导致效率低下、安全漏洞或用户体验不佳。本篇文章,我们将探讨Spring Boot REST API中的十大常见错误,解释它们的影响,并使用最新的Spring Boot特性和最佳实践提供更新的解决方案。
图片
接下来,我们将详细介绍上面10点常见错误。
2. 错误详解
2.1 使用错误的HTTP状态码
- 错误:对所有响应(包括错误响应)都返回200 OK状态码。
- 影响:误导性的状态码会使API客户端感到困惑,并使调试变得困难。
- 解决方案:根据具体情况始终使用适当的HTTP状态码:
a.200 OK:请求成功。
b.201 Created:资源已成功创建。
c.400 Bad Request:输入无效或存在验证错误。
d.404 Not Found:资源不存在。
e.500 Internal Server Error:服务器内部错误(意外错误)。
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.findById(id)
.map(product -> ResponseEntity.ok(product))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).build()) ;
}
}
为何重要:正确的HTTP状态码可以提高客户端的理解能力和API的可靠性。
2.2 未验证输入数据
- 错误:未经验证就接受数据。
- 影响:会导致安全漏洞和后续错误。
- 解决方案:在数据传输对象(DTO)上使用@Valid注解进行输入验证,并优雅地处理错误。
@RestController
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public ResponseEntity<String> createProduct(@Valid @RequestBody ProductDTO productDTO) {
productService.save(productDTO) ;
return ResponseEntity.status(HttpStatus.CREATED)
.body("success");
}
}
public record ProductDTO(
@NotNull(message = "名称不能为空")
String name,
@Positive(message = "价格必须大于0")
Double price) {}
为何重要:验证输入可以确保数据的完整性,并防止滥用。
2.3 忽略API版本控制
- 错误:开发API时不进行版本控制会导致对客户端的破坏性更改。
- 影响:当API发展时,客户端可能会遇到兼容性问题。
- 解决方案:通过URI或自定义头部实现API版本控制。
@RestController
@RequestMapping("/api/v1/products")
public class ProductV1Controller {
@GetMapping("/{id}")
public Product getProductV1(@PathVariable Long id) {
return productService.findByIdV1(id);
}
}
@RestController
@RequestMapping("/api/v2/products")
public class ProductV2Controller {
@GetMapping("/{id}")
public ProductDTO getProductV2(@PathVariable Long id) {
return productService.findByIdV2(id);
}
}
为何重要:版本控制可以在引入新功能的同时确保向后兼容性。
2.4 对端点和配置进行硬编码
- 错误:在代码中硬编码URL或服务端点。
- 影响:使应用程序难以维护和配置。
- 解决方案:使用application.yml或application.properties将配置外部化。
配置文件
# application.yml
pack:
product:
service:
url: https://api.pack.com/products
代码注入
@Value("${pack.product.service.url}")
private String productServiceUrl;
为何重要:将配置外部化可以使您的应用程序更加灵活且易于维护。
2.5 异常处理不当
- 错误:允许异常未经适当格式化就传播到客户端。
- 影响:客户端会收到结构不清晰的错误信息,导致困惑。
- 解决方案:使用带有@ExceptionHandler的@ControllerAdvice进行集中式的错误处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("发生错误: " + ex.getMessage());
}
}
为何重要:适当的异常处理能够提升客户体验和调试效率。
2.6 直接暴露JAP实体对象
- 错误:直接在API响应中暴露数据库实体。
- 影响:导致数据库与API之间紧密耦合。
- 解决方案:使用DTO(数据传输对象)将API响应与数据库架构解耦。
public record ProductDTO(Long id, String name, Double price) {}
public ProductDTO mapToDTO(Product product) {
return new ProductDTO(product.getId(), product.getName(), product.getPrice());
}
为何重要:DTO(数据传输对象)提高了API的灵活性,并防止敏感数据泄露。
2.7 未实现分页和过滤功能
- 错误:在单个响应中返回大型数据集。
- 影响:导致性能瓶颈和客户端问题。
- 解决方案:使用Pageable实现分页和过滤功能。
@GetMapping
public Page<Product> getAllProducts(Pageable pageable) {
return productRepository.findAll(pageable);
}
为何重要:分页和过滤功能能够提高API的性能和可扩展性。
2.8 忽略接口安全
- 错误:未对REST API进行保护或暴露敏感数据。
- 影响:可能导致未经授权的访问和潜在的数据泄露。
- 解决方案:使用带有JWT或OAuth2的Spring Security。
@Bean
SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Throwable {
http.csrf(csrf -> csrf.disable()) ;
http.authorizeHttpRequests(registry -> {
registry.requestMatchers("/*/*.js", "/*/*.css", "*.css", "*.js", "*.html", "/*/*.html", "/login", "/logout").permitAll() ;
registry.requestMatchers("/**").authenticated() ;
}) ;
http.securityMatcher("/api/**", "/admin/**", "/login", "/logout", "/default-ui.css") ;
http.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class) ;
return http.build() ;
}
以上指定了api接口及其它资源的访问策略。
为何重要:安全性能够保护敏感数据并确保合规性。
2.9 忽略API文档
- 错误:跳过API文档编写。
- 影响:导致其他开发人员难以使用你的API。
- 解决方案:使用Knife4j进行自动生成的API文档编写。
引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
配置
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api,用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any()).build()
/* 设置安全模式,swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("Pack_接口文档")
// 描述
.description("Packxxx系统,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact("xxxooo", null, null))
// 版本
.version("1.0.0").build();
}
private List<SecurityScheme> securitySchemes() {
List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
return apiKeyList;
}
为何重要:文档能够提高开发者的生产力和协作效率。
2.10 API中忽略HATEOAS
- 错误:返回不带导航链接的纯JSON数据。
- 影响:客户端缺乏执行相关操作的指导。
- 解决方案:使用Spring HATEOAS来包含导航链接。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
使用
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public EntityModel<ProductDTO> queryProductById(@PathVariable Long id) {
ProductDTO productDTO = new ProductDTO(id, "鼠标", 66.6D) ;
EntityModel<ProductDTO> productModel = EntityModel.of(
productDTO,
linkTo(methodOn(ProductController.class).queryProductById(productDTO.id())).withSelfRel(),
linkTo(methodOn(ProductController.class).queryProducts()).withRel("all-products")
) ;
return productModel ;
}
@GetMapping("")
public List<EntityModel<ProductDTO>> queryProducts() {
List<EntityModel<ProductDTO>> list = List.of(
EntityModel.of(
new ProductDTO(1L, "鼠标", 66.6),
linkTo(methodOn(ProductController.class).queryProductById(1L)).withSelfRel(),
linkTo(methodOn(ProductController.class).queryProducts()).withRel("all-products")
),
EntityModel.of(
new ProductDTO(2L, "键盘", 88.8),
linkTo(methodOn(ProductController.class).queryProductById(2L)).withSelfRel(),
linkTo(methodOn(ProductController.class).queryProducts()).withRel("all-products")
)
) ;
return list ;
}
}
访问/products/666接口,输出结果.
图片
图片
以上是我们手动生成,如果你结合Spring REST Docs使用,那么你根本不需要自己写这些。