Spring Boot 2中如何复用订单ID存在性校验逻辑?
问题
我们有多个获取订单信息的REST接口:
/order/{id}/order/{id}/positions/order/{id}/invoice/order/{id}/shipment
这些接口分散在Spring Boot 2应用的OrderController、InvoiceController等多个控制器中,每个控制器都通过OrderRepository校验指定ID的订单是否存在,不存在就抛出异常,导致大量重复代码。示例代码如下:
@RestController public class OrderController { // ... @GetMapping("order/{id}") public Order getCustomer(@PathVariable final Integer id) { return this.orderRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("order not found")); } }
请问Spring框架是否提供回调机制,让订单ID的校验逻辑只需编写一次?另外我发现了AntPathMatcher,但它仅提供布尔型接口,似乎并非合适的解决方案。
回答
当然有办法解决这个重复校验的问题!Spring提供了几种优雅的方式让你把订单ID的校验逻辑只写一次,不用在每个控制器里重复造轮子,完全不用依赖AntPathMatcher(它确实只是用来做路径匹配的,和业务校验无关)。下面给你两种最常用的实现方案:
方案一:使用@ControllerAdvice + @ModelAttribute全局预处理
这种方式适合所有和订单相关的控制器,我们可以创建一个全局的通知类,在匹配的控制器方法执行前自动校验订单ID并加载订单对象,然后注入到方法参数中。
实现步骤:
- 创建全局控制器通知类,指定作用于哪些订单相关的控制器:
@ControllerAdvice(assignableTypes = {OrderController.class, InvoiceController.class, ShipmentController.class}) public class OrderPreloadAdvice { private final OrderRepository orderRepository; // 构造注入Repository public OrderPreloadAdvice(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @ModelAttribute public Order preloadOrder(@PathVariable Integer id) { // 统一的校验逻辑:找不到订单就抛出异常 return orderRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("order not found")); } }
- 修改控制器方法,直接使用注入好的
Order对象:
@RestController public class OrderController { @GetMapping("/order/{id}") public Order getOrder(Order order) { // 不用自己查订单了,直接返回 return order; } } @RestController public class InvoiceController { @GetMapping("/order/{id}/invoice") public Invoice getInvoice(Order order) { // 直接从预加载的Order对象中拿发票信息 return order.getInvoice(); } }
优点:实现简单,代码量少,不需要额外配置;注意点:所有目标控制器的路径变量名称必须是id,如果有不同的名称,需要调整@PathVariable的参数名。
方案二:自定义HandlerMethodArgumentResolver参数解析器
这种方式更灵活,你可以通过自定义注解来标记需要自动校验的订单参数,不受路径变量名称或控制器范围的限制。
实现步骤:
- 定义一个自定义注解,用来标记需要自动加载的订单参数:
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ExistingOrder { // 可以添加属性,比如指定路径变量名称,默认是"id" String value() default "id"; }
- 实现参数解析器,处理带有
@ExistingOrder注解的参数:
@Component public class ExistingOrderArgumentResolver implements HandlerMethodArgumentResolver { private final OrderRepository orderRepository; public ExistingOrderArgumentResolver(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public boolean supportsParameter(MethodParameter parameter) { // 只处理带有@ExistingOrder注解的Order类型参数 return parameter.hasParameterAnnotation(ExistingOrder.class) && Order.class.isAssignableFrom(parameter.getParameterType()); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { // 获取注解中的路径变量名称 ExistingOrder annotation = parameter.getParameterAnnotation(ExistingOrder.class); String pathVarName = annotation.value(); // 从请求中获取路径变量值 String idStr = webRequest.getAttribute(pathVarName, NativeWebRequest.SCOPE_REQUEST); Integer id = Integer.parseInt(idStr); // 统一校验并返回订单 return orderRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("order not found")); } }
- 注册参数解析器到Spring MVC配置中:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final ExistingOrderArgumentResolver existingOrderArgumentResolver; public WebMvcConfig(ExistingOrderArgumentResolver existingOrderArgumentResolver) { this.existingOrderArgumentResolver = existingOrderArgumentResolver; } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(existingOrderArgumentResolver); } }
- 在控制器方法中使用自定义注解:
@RestController public class ShipmentController { @GetMapping("/order/{id}/shipment") public Shipment getShipment(@ExistingOrder Order order) { return order.getShipment(); } // 如果路径变量名称不是id,比如orderId,也可以指定: @GetMapping("/order/{orderId}/positions") public List<Position> getPositions(@ExistingOrder("orderId") Order order) { return order.getPositions(); } }
优点:灵活性极高,支持自定义路径变量名称,不受控制器范围限制;注意点:需要编写和注册解析器,代码量稍多,但扩展性更好。
两种方案都能完美解决你的重复代码问题,你可以根据自己的业务场景选择合适的方式~
内容的提问来源于stack exchange,提问作者sschmeck
相关产品推荐
相关产品推荐

