我想知道在具有多个请求参数的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
}
}
}
使用这种端点似乎非常方便(参数的顺序无关紧要,所有参数都是可选的...),但是维护这样的东西就像一个地狱(组合的数量可能很大)。 / p>
我的问题是-处理这种事情的最佳方法是什么?在“专业” API中如何设计?
答案 0 :(得分:3)
处理这种事情的最佳方法是什么?
处理此问题的最佳方法是使用现有工具。当您使用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
答案 1 :(得分: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;
}
有关更多信息,请检查此@Valid usage in Spring
这种方式会将您的验证机制从控制器层分离到业务层。这样可以节省很多样板代码,但是正如您注意到的那样,更改了POST。
因此,总的来说,您的问题没有直接答案,简短的答案取决于具体情况,因此,选择具有功能强大且维护较少的简单方法是您的最佳实践
答案 2 :(得分:0)
美好的一天。我不能称自己是专业人士,但是这里有一些技巧可以使该控制器看起来更好。
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注释。
这是一个很好的指南-REST Query Language with Spring Data JPA Specifications
@GetMapping
public Page<Customer> findCustomersByFirstName(@Valid CustomerDTO customerDTO, BindingResult bindingResult, Pageable pageable) {
if (bindingResult.hasErrors()) {
// error handling
}
return customerService.findAllBySpecification(new CustomerSpecification(customerDTO));
}
您的控制器不应包含任何与实体或某些业务相关的逻辑。这只是关于处理请求/错误,重定向,视图等...
答案 3 :(得分:0)
最好有一个带有这种验证的 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 :(得分: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
模型进行更好的条件查询并将其与提供的解决方案相结合。