Spring Boot Rest API十大常见错误及避免方法

开发 前端
本篇文章,我们将探讨Spring Boot REST API中的十大常见错误,解释它们的影响,并使用最新的Spring Boot特性和最佳实践提供更新的解决方案。

环境: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使用,那么你根本不需要自己写这些。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2009-10-22 09:52:37

2011-04-29 10:37:01

CRM

2009-10-14 09:42:14

2013-09-13 14:43:16

2023-05-11 12:40:00

Spring控制器HTTP

2024-09-26 08:03:37

2022-10-17 07:35:52

Kubernetes工具日志

2018-03-12 13:25:51

2010-08-16 13:51:22

DIV+CSS

2013-07-03 09:42:32

网络管理系统升级故障排查

2013-08-15 09:47:07

云迁移云技术

2024-03-04 13:23:00

数字化转型

2020-04-29 14:37:24

JavaScript前端技术

2022-01-10 06:43:27

ATT&CK网络安全攻击

2013-10-23 14:34:15

2015-06-08 13:51:56

WiFi

2023-11-06 13:56:20

2019-07-29 15:15:45

2019-07-31 10:59:36

2024-12-06 11:42:33

点赞
收藏

51CTO技术栈公众号