3

我想知道在给出多个请求参数的 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 中设计的?

4

5 回答 5

4

处理这样的事情的最佳方法是什么?

处理它的最佳方法是使用已有的工具。当您使用 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/当前/参考/html/#core.extensions.querydsl

于 2019-11-25T09:09:27.133 回答
1

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

  • 使用 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));
}

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

于 2019-11-25T09:20:08.250 回答
1

最好有一个带有此类验证的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);
}

}

于 2019-11-25T11:25:04.440 回答
0

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

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

或者,您可以在@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;
}

有关更多信息,请查看Spring 中的 @Valid 用法

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

所以一般来说,你的问题没有直接的答案,而简短的答案取决于你,所以选择任何对你来说容易且具有良好功能和较少维护的东西将是最佳实践

于 2019-11-25T09:07:42.023 回答
0

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

因此,您的存储库应从JpaSpecificationExecutor<Customer>接口扩展如下:

@Repository
interface CustomerRepository extends JpaSpecificationExecutor<Customer> {

}

Map<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<Customer>

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模型进行更好的条件查询并将其与提供的解决方案相结合。

于 2021-02-08T06:19:49.817 回答