简体   繁体   English

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

[英]Spring REST API multiple RequestParams vs controller implementation

I'm wondering about proper way of implementating of controller in case of GET request with multiple request params given.我想知道在给定多个请求参数的 GET 请求的情况下实现控制器的正确方法。 In my understanding of REST it's much better to have one endpoint with additional parameters for filtering/sorting than several endpoints (one for each case).根据我对 REST 的理解,与多个端点(每种情况一个)相比,拥有一个带有额外参数用于过滤/排序的端点要好得多。 I'm just wondering about maintanance and extensibility of such endpoint.我只是想知道此类端点的可维护性和可扩展性。 Please have a look on example below :请看下面的例子:

@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
        }
    }
}

Such endpoint seems to be very convenient to use (order of parameters doesn't matter, all of them are optional...), but maintaining something like this looks like a hell (number of combinations can be enormous).这样的端点使用起来似乎非常方便(参数的顺序无关紧要,它们都是可选的......),但是维护这样的东西看起来很糟糕(组合的数量可能很大)。

My question is - what is the best way to deal with something like this?我的问题是 - 处理此类事情的最佳方法是什么? How is it designed in "professional" APIs?它是如何在“专业”API 中设计的?

What is the best way to deal with something like this?处理这样的事情的最佳方法是什么?

The best way to deal with it is to use the tools already available.处理它的最好方法是使用现有的工具。 As you are using Spring Boot and, I assume therefore, Spring Data JPA then enable the QueryDsl suppport and web support extensions for Spring Data JPA.由于您使用的是 Spring Boot,因此我假设 Spring Data JPA 会为 Spring Data JPA 启用 QueryDsl 支持和 Web 支持扩展。

You controller then simply becomes:你的控制器然后简单地变成:

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

and your repository is simply extended to support QueryDsl:并且您的存储库只是扩展为支持 QueryDsl:

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

}

You can now query by any combination of params without writing any further code.您现在可以通过参数的任意组合进行查询,而无需编写任何进一步的代码。

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

Good day.再会。 I can't call myself a professional, but here are some tips which can make this controller looks better.我不能称自己为专业人士,但这里有一些技巧可以让这个控制器看起来更好。

  • Use DTO instead of using a group of parameters使用 DTO 而不是使用一组参数
public class CustomerDTO {

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

}

With this class your method's signature will look like this:使用此类,您的方法签名将如下所示:

@GetMapping
public Page<Customer> findCustomersByFirstName(CustomerDTO customerDTO, Pageable pageable) {
    ...
}
  • Use validation if you need one如果需要,请使用验证

For example, you can make some of these fields are required:例如,您可以将其中一些字段设为必填:

public class CustomerDTO {

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

}

Don't forget to add @Valid annotation before the DTO parameter in your controller.不要忘记在控制器中的 DTO 参数之前添加 @Valid 注释。

  • Use specification instead of this block with if-else使用规范而不是带有 if-else 的块

Here is a great guide on how to do it - REST Query Language with Spring Data JPA Specifications这是有关如何执行此操作的出色指南 - REST Query Language with Spring Data JPA Specifications

  • Use the service layer, don't need to call repository from the controller使用服务层,不需要从控制器调用repository
@GetMapping
public Page<Customer> findCustomersByFirstName(@Valid CustomerDTO customerDTO, BindingResult bindingResult, Pageable pageable) {
    if (bindingResult.hasErrors()) {
        // error handling
    }
    return customerService.findAllBySpecification(new CustomerSpecification(customerDTO));
}

Your controller should not contain any logic about working with entities or some business stuff.您的控制器不应包含任何有关处理实体或某些业务内容的逻辑。 It's only about handling request/errors, redirects, views, etc...它只是关于处理请求/错误、重定向、视图等......

Its good to have a POST request with such validations instead of a GET request.You can use following method for the controller.拥有带有此类验证的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);

} }

use the DTO as follows.使用 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() {

}

} }

And add a controller level exception advice to return the response in errors.并添加控制器级异常建议以返回错误响应。

@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);
}

} }

Actually, you answered half of the answer yourself, Query Params are used for filtration purposes, and as you can see in your code this will be allowed via GET request.实际上,您自己回答了一半的答案,查询参数用于过滤目的,正如您在代码中看到的那样,这将通过 GET 请求被允许。 But your question regarding validations is something a trade-off.但是您关于验证的问题是一种权衡。

For example;例如; if you don't want to have this kind of check, you can depend on mandatory required = true which is the default in @RequestParam , and handle it in the response immediately.如果您不想进行这种检查,您可以依赖于@RequestParam的默认值required = true ,并立即在响应中处理它。

Or you can use alternatively @RequestBody with support of @Valid for more clear info for what wrong had occurred;或者,您可以在支持@Valid 的情况下使用@RequestBody以获取更清晰的错误信息; for example例如

@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;
}

For more information check this @Valid usage in Spring有关更多信息,请查看Spring 中的 @Valid 用法

This way will decouple your validation mechanism from Controller layer to Business layer.这种方式将您的验证机制从控制器层解耦到业务层。 which in turns will save lots of boilerplate code, but as you noticed with change to POST instead.这反过来会节省大量样板代码,但正如您注意到的那样,改为 POST。

So in general, there is no direct answer to your question, and the short answer is it depends, so choose whatever easy for you with good capabilities and less maintainance will be the best practice所以总的来说,你的问题没有直接的答案,简短的答案是它取决于你,所以选择任何对你来说容易、功能好、维护少的东西将是最佳实践

As an alternative solution besides other ones, you can use JpaSpecificationExecutor<T> in your repository and create a specification object based on your arguments and pass it to the findAll method.作为除其他解决方案之外的替代解决方案,您可以在存储库中使用JpaSpecificationExecutor<T>并根据您的参数创建规范对象并将其传递给findAll方法。

So, your repository should extend from the JpaSpecificationExecutor<Customer> interface as follows:因此,您的存储库应该从JpaSpecificationExecutor<Customer>接口扩展,如下所示:

@Repository
interface CustomerRepository extends JpaSpecificationExecutor<Customer> {

}

Your controller should get the required arguments as Map<String, String to gain dynamic behavior.您的控制器应该获得所需的参数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);
    }
}

And, you should define a method to convert provided arguments to Specification<Customer> :并且,您应该定义一个方法将提供的参数转换为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;
    }
}

Also, You can use the Meta model to make better criteria query and combine it with the provided solution.此外,您可以使用Meta模型进行更好的条件查询并将其与提供的解决方案结合起来。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM