簡體   English   中英

Spring REST API 多個 RequestParams 與控制器實現

[英]Spring REST API multiple RequestParams vs controller implementation

我想知道在給定多個請求參數的 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 中設計的?

處理這樣的事情的最佳方法是什么?

處理它的最好方法是使用現有的工具。 由於您使用的是 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

再會。 我不能稱自己為專業人士,但這里有一些技巧可以讓這個控制器看起來更好。

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

您的控制器不應包含任何有關處理實體或某些業務內容的邏輯。 它只是關於處理請求/錯誤、重定向、視圖等......

擁有帶有此類驗證的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);
}

}

實際上,您自己回答了一半的答案,查詢參數用於過濾目的,正如您在代碼中看到的那樣,這將通過 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;
}

有關更多信息,請查看Spring 中的 @Valid 用法

這種方式將您的驗證機制從控制器層解耦到業務層。 這反過來會節省大量樣板代碼,但正如您注意到的那樣,改為 POST。

所以總的來說,你的問題沒有直接的答案,簡短的答案是它取決於你,所以選擇任何對你來說容易、功能好、維護少的東西將是最佳實踐

作為除其他解決方案之外的替代解決方案,您可以在存儲庫中使用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模型進行更好的條件查詢並將其與提供的解決方案結合起來。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM