Spring Boot Rest API十大常見錯誤及避免方法
環境: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使用,那么你根本不需要自己寫這些。