Spring REST API 多个 RequestParams 与控制器实现

Posted

技术标签:

【中文标题】Spring REST API 多个 RequestParams 与控制器实现【英文标题】:Spring REST API multiple RequestParams vs controller implementation 【发布时间】:2020-03-20 12:26:43 【问题描述】:

我想知道在给出多个请求参数的 GET 请求的情况下执行控制器的正确方法。在我对 REST 的理解中,拥有一个带有用于过滤/排序的附加参数的端点比多个端点(每种情况一个)要好得多。我只是想知道这种端点的维护和可扩展性。请看下面的例子:

@RestController
@RequestMapping("/customers")
public class CustomerController 

    @Autowired
    private CustomerRepository customerRepo;

    @GetMapping
    public Page<Customer> findCustomersByFirstName(
                @RequestParam("firstName") String firstName,
                @RequestParam("lastName") String lastName,
                @RequestParam("status") Status status, Pageable pageable) 

        if (firstName != null) 
            if (lastName != null) 
                if (status != null) 
                    return customerRepo.findByFirstNameAndLastNameAndStatus(
                                                    firstName, lastName, status, pageable);
                 else 
                    return customerRepo.findByFirstNameAndLastName(
                                                    firstName, lastName, pageable);
                
             else 
                // other combinations omitted for sanity
            
         else 
            // other combinations omitted for sanity
        
    

这样的端点使用起来似乎很方便(参数的顺序无关紧要,所有这些都是可选的......),但维护这样的东西看起来就像地狱一样(组合的数量可以是巨大的)。

我的问题是 - 处理此类事情的最佳方法是什么? “专业”的 API 是如何设计的?

【问题讨论】:

好问题,但从我的角度来看,它有点太宽泛了。 如何以专业的方式进行设计可以通过许多不同的解决方案获得许多答案。最好问一个更具体的问题以获得特定案例的良好答案。举个例子:您可以将一个参数作为 JSON 发送,它代表您的客户 POJO,并使用 queryDSL 生成用于构建动态查询的谓词。 【参考方案1】:

作为除其他解决方案之外的替代解决方案,您可以在存储库中使用JpaSpecificationExecutor&lt;T&gt;,并根据您的参数创建一个规范对象并将其传递给findAll 方法。

因此,您的存储库应从JpaSpecificationExecutor&lt;Customer&gt; 接口扩展,如下所示:

@Repository
interface CustomerRepository extends JpaSpecificationExecutor<Customer> 


您的控制器应该获得所需的参数作为Map&lt;String, String 以获得动态行为。

@RestController
@RequestMapping("/customers")
public class CustomerController 
    private final CustomerRepository repository;

    @Autowired
    public CustomerController(CustomerRepository repository) 
        this.repository = repository;
    

    @GetMapping
    public Page<Customer> findAll(@RequestBody HashMap<String, String> filters, Pageable pageable) 
        return repository.findAll(QueryUtils.toSpecification(filters), pageable);
    

而且,您应该定义一个方法来将提供的参数转换为Specification&lt;Customer&gt;

class QueryUtils 
    public static Specification<Customer> toSpecification(Map<String, String> filters) 
        Specification<Customer> conditions = null;

        for (Map.Entry<String, String> entry : filters.entrySet()) 
            Specification<Customer> condition = Specification.where((root, query, cb) -> cb.equal(root.get(entry.getKey()), entry.getValue()));
            if (conditions == null) 
                conditions = condition;
             else 
                conditions = conditions.and(condition);
            
        

        return conditions;
    

另外,您可以使用Meta 模型进行更好的条件查询并将其与提供的解决方案相结合。

【讨论】:

【参考方案2】:

实际上,您自己回答了一半,查询参数用于过滤目的,正如您在代码中看到的那样,这将通过 GET 请求被允许。但是您关于验证的问题是一种权衡。

例如;如果你不想进行这种检查,你可以依赖@RequestParam中默认的强制required = true,并在响应中立即处理。

或者您也可以在 @Valid 的支持下使用 @RequestBody 以获得更清晰的错误信息;例如

@PostMapping(value = "/order")
public ResponseEntity<?> submitRequest(@RequestBody @Valid OrderRequest requestBody, 
            Errors errors) throws Exception 

        if (errors.hasErrors())
            throw new BusinessException("ERR-0000", "", HttpStatus.NOT_ACCEPTABLE);

        return new ResponseEntity<>(sendOrder(requestBody), HttpStatus.CREATED);


// Your Pojo
public class OrderRequest 
    @NotNull(message = "channel is required")
    private String channel;

    @NotNull(message = "Party ID is required")
    private long partyId;

更多信息请查看@Valid usage in Spring

这种方式将您的验证机制从控制器层解耦到业务层。这反过来会节省大量样板代码,但正如您注意到的那样,改为 POST。

所以一般来说,您的问题没有直接的答案,而简短的答案取决于您,所以选择对您来说容易且功能强大且维护较少的最佳实践

【讨论】:

【参考方案3】:

最好有一个带有此类验证的 POST 请求,而不是 GET 请求。您可以对控制器使用以下方法。

@PostMapping(value = "/findCustomer",produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> findCustomersByFirstName(@Valid @RequestBody Customer customer)
   return customerRepo.findByFirstNameAndLastNameAndStatus(customer.getFirstName, customer.getLastName(), customer.getStatus(), pageable);

按如下方式使用 DTO。

public class Customer 

private String firstName;
private String lastName;
private String status;

public String getFirstName() 
    return firstName;


public void setFirstName(String firstName) 
    this.firstName= firstName;


public String getLastName() 
    return lastName;


public void setLastName(String lastName) 
    this.lastName= lastName;


public String getStatus() 
    return status;


public void setStatus(String status) 
    this.status= status;


public LivenessInputModel(String firstName, String lastName, String status) 
    this.firstName= firstName;
    this.lastName= lastName;
    this.status= status;


public LivenessInputModel() 


并添加控制器级别的异常建议以返回错误响应。

@ControllerAdvice
public class ControllerExceptionAdvice 

private static final String EXCEPTION_TRACE = "Exception Trace:";

private static Logger log = LoggerFactory.getLogger(ControllerExceptionAdvice.class);

public ControllerExceptionAdvice() 
    super();


@ExceptionHandler( BaseException.class )
public ResponseEntity<String> handleResourceException(BaseException e, HttpServletRequest request,
                                                      HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(e);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, e.getHttpStatus());



@ExceptionHandler( Exception.class )
public ResponseEntity<String> handleException(Exception e, HttpServletRequest request,
                                              HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
            ExceptionMessages.INTERNAL_DEFAULT);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);



@ExceptionHandler( MethodArgumentNotValidException.class )
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException e,
                                                        HttpServletRequest request, HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    ValidationException validationEx = new ValidationException(e);
    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(validationEx);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, validationEx.getHttpStatus());



@ExceptionHandler( HttpMediaTypeNotSupportedException.class, InvalidMimeTypeException.class,
        InvalidMediaTypeException.class, HttpMessageNotReadableException.class )
public ResponseEntity<String> handleMediaTypeNotSupportException(Exception e, HttpServletRequest request,
                                                                 HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
            ExceptionMessages.BAD_REQUEST_DEFAULT);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);



@ExceptionHandler( HttpRequestMethodNotSupportedException.class )
public ResponseEntity<String> handleMethodNotSupportException(Exception e, HttpServletRequest request,
                                                              HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    HttpStatus httpStatus = HttpStatus.METHOD_NOT_ALLOWED;

    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
            ExceptionMessages.METHOD_NOT_ALLOWED);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);


@ExceptionHandler( MissingServletRequestParameterException.class )
public ResponseEntity<String> handleMissingServletRequestParameterException(Exception e, HttpServletRequest request,
                                                                            HttpServletResponse response) 

    log.error(EXCEPTION_TRACE, e);

    HttpHeaders responseHeaders = new HttpHeaders();

    responseHeaders.setContentType(MediaType.APPLICATION_JSON);

    HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

    BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
            ExceptionMessages.BAD_REQUEST_DEFAULT);

    return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);

【讨论】:

【参考方案4】:

美好的一天。我不能称自己为专业人士,但这里有一些技巧可以让这个控制器看起来更好。

使用 DTO 而不是使用一组参数
public class CustomerDTO 

    private String firstName;
    private String lastName;
    private String status;


使用这个类,您的方法的签名将如下所示:

@GetMapping
public Page<Customer> findCustomersByFirstName(CustomerDTO customerDTO, Pageable pageable) 
    ...

如果需要,请使用验证

例如,您可以将其中一些字段设为必填:

public class CustomerDTO 

    @NotNull(message = "First name is required")
    private String firstName;
    private String lastName;
    private String status;


不要忘记在控制器中的 DTO 参数之前添加 @Valid 注解。

在 if-else 中使用规范而不是这个块

这里有一个很好的指南 - REST Query Language with Spring Data JPA Specifications

使用服务层,不需要从控制器调用repository
@GetMapping
public Page<Customer> findCustomersByFirstName(@Valid CustomerDTO customerDTO, BindingResult bindingResult, Pageable pageable) 
    if (bindingResult.hasErrors()) 
        // error handling
    
    return customerService.findAllBySpecification(new CustomerSpecification(customerDTO));

您的控制器不应包含有关处理实体或某些业务内容的任何逻辑。它只是关于处理请求/错误、重定向、视图等......

【讨论】:

【参考方案5】:

处理此类问题的最佳方法是什么?

处理它的最佳方法是使用已有的工具。当您使用 Spring Boot 时,我假设 Spring Data JPA 然后为 Spring Data JPA 启用 QueryDsl 支持和 Web 支持扩展。

然后你的控制器就变成了:

@GetMapping
public Page<Customer> searchCustomers( 
        @QuerydslPredicate(root = Customer.class) Predicate predicate, Pageable pageable) 
   return customerRepo.findBy(predicate, pageable);

您的存储库被简单地扩展为支持 QueryDsl:

public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long>, 
            QueryDslPredicateExecutor<Customer>


您现在可以通过任何参数组合进行查询,而无需编写任何进一步的代码。

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web.type-safe https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl

【讨论】:

这正是我想要的。谢谢!

以上是关于Spring REST API 多个 RequestParams 与控制器实现的主要内容,如果未能解决你的问题,请参考以下文章

在具有多个连接的 Spring Boot Rest API 中公开自定义查询

负载平衡 (ribbon) 和路由 (zuul) Spring REST API (Spring Data JPA) 请求到同一服务的多个副本

使用 Spring Boot 的多个 REST 调用

使用jersey客户端消耗spring REST api

多个字段解析器使用不同的查询参数解析相同的 REST API

如何保护 Spring Boot Web 应用程序中的 REST API?