1. 程式人生 > >讓Controller支援對平鋪引數執行@Valid資料校驗

讓Controller支援對平鋪引數執行@Valid資料校驗

每篇一句

在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多程式設計框架都只是教你碎片化的實踐)

相關閱讀

【小家Java】深入瞭解資料校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】@Validated和@Valid的區別?教你使用它完成Controller引數校驗(含級聯屬性校驗)以及原理分析
【小家Spring】Spring方法級別資料校驗:@Validated + MethodValidationPostProcessor優雅的完成資料校驗動作


對Spring感興趣可掃碼加入wx群:`Java高工、架構師3群`(文末有二維碼)

前言

我們知道Spring MVC層是預設可以支援Bean Validation的,但是我在實際使用起來有很多不便之處(相信我的使用痛點也是小夥伴的痛點),就感覺它是個半拉子:只支援對JavaBean的驗證,而並不支援對Controller處理方法的平鋪引數的校驗。

上篇文章一起了解了Spring MVC中對Controller處理器入參校驗的問題,但也僅侷限於對JavaBean的驗證。不可否認對JavaBean的校驗是我們實際專案使用中較為常見、使用頻繁的case,關於此部分詳細內容可參見:【小家Spring】@Validated和@Valid的區別?教你使用它完成Controller引數校驗(含級聯屬性校驗)以及原理分析

在上文我也提出了使用痛點:我們Controller控制器方法中入參,其實大部分情況下都是平鋪引數而非JavaBean的。然而對於平鋪引數我們並不能使用@Validated像校驗JavaBean一樣去做,並且Spring MVC也並沒有提供源生的解決方案(其實提供了,哈哈)。
那怎麼辦?難道真的只能自己書寫重複的if else去完成嗎?當然不是,那麼本文將對此常見的痛點問題(現象)提供兩種思路,供給使用者參考~

Controller層平鋪引數的校驗

因為Spring MVC並不天然支援對控制器方法平鋪引數的資料校驗,但是這種case的卻有非常的常見,因此針對這種常見現象提供一些可靠的解決方案,對你的專案的收益是非常高的。

方案一:藉助Spring對方法級別資料校驗的能力

首先必須明確一點:此能力屬於Spring框架的,而部分web框架Spring MVC。
Spring對方法級別資料校驗的能力非常重要(它能對Service層、Dao層的校驗等),前面也重點分析過,具體使用方式參考本文:【小家Spring】Spring方法級別資料校驗:@Validated + MethodValidationPostProcessor優雅的完成資料校驗動作

使用此種方案來解決問題的步驟比較簡單,使用起來也非常方便。下面我寫個簡單示例作為參考:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Bean
    public MethodValidationPostProcessor mvcMethodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

Controller中 類 上使用@Validated標註,然後方法上正常使用約束註解標註平鋪的屬性:

@RestController
@RequestMapping
@Validated
public class HelloController {
    @PutMapping("/hello/id/{id}/status/{status}")
    public Object helloGet(@Max(5) @PathVariable Integer id, @Min(5) @PathVariable Integer status) {
        return "hello world";
    }
}

請求:/hello/id/6/status/4 可看見拋異常:

注意一下:這裡arg0 arg1並沒有按照順序來,欄位可別對應錯了~~~

由此可見,校驗生效了。丟擲了javax.validation.ConstraintViolationException異常,這樣我們再結合一個全域性異常的處理程式,也就能達到我們預定的效果了~

這種方案一樣有一個非常值得注意但是很多人都會忽略的地方:因為我們希望能夠代理Controller這個Bean,所以僅僅只在父容器中配置MethodValidationPostProcessor是無效的,必須在子容器(web容器)的配置檔案中再配置一個MethodValidationPostProcessor,請務必注意~

有小夥伴問我了,為什麼它的專案裡只配置了一個MethodValidationPostProcessor也生效了呢? 我的回答是:檢查一下你是否是用的SpringBoot。

其實關於配置一個還是多個MethodValidationPostProcessor的case,其實是個Bean覆蓋有很大關係的,這方面內容可參考:【小家Spring】聊聊Spring的bean覆蓋(存在同名name/id問題),介紹Spring名稱生成策略介面BeanNameGenerator

方案二:自己實現,藉助HandlerInterceptor做攔截處理(輕量)

方案一的使用已經很簡單了,但我個人總還覺得怪怪的,因為我一直不喜歡Controller層被代理(可能是潔癖吧)。因此針對這個現象,我自己接下來提供一個自定義攔截器HandlerInterceptor的處理方案來實現,大家不一定要使用,也是供以參考嘛~
設計思路:Controller攔截器 + @Validated註解 + 自定義校驗器(當然這裡面涉及到不少細節的:比如入參解析、繫結等等內建的API)

1、準備一個攔截器ValidationInterceptor用於處理校驗邏輯:

// 注意:此處只支援@RequesrMapping方式~~~~
public class ValidationInterceptor implements HandlerInterceptor, InitializingBean {

    @Autowired
    private LocalValidatorFactoryBean validatorFactoryBean;
    @Autowired
    private RequestMappingHandlerAdapter adapter;
    private List<HandlerMethodArgumentResolver> argumentResolvers;

    @Override
    public void afterPropertiesSet() throws Exception {
        argumentResolvers = adapter.getArgumentResolvers();
    }

    // 快取
    private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
    private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 只處理HandlerMethod方式
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            Validated valid = method.getMethodAnnotation(Validated.class); //
            if (valid != null) {
                // 根據工廠,拿到一個校驗器
                ValidatorImpl validatorImpl = (ValidatorImpl) validatorFactoryBean.getValidator();

                // 拿到該方法所有的引數們~~~  org.springframework.core.MethodParameter
                MethodParameter[] parameters = method.getMethodParameters();
                Object[] parameterValues = new Object[parameters.length];

                //遍歷所有的入參:給每個引數做賦值和資料繫結
                for (int i = 0; i < parameters.length; i++) {
                    MethodParameter parameter = parameters[i];
                    // 找到適合解析這個引數的處理器~
                    HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
                    Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");

                    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
                    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));

                    WebDataBinderFactory webDataBinderFactory = getDataBinderFactory(method);
                    Object value = resolver.resolveArgument(parameter, mavContainer, new ServletWebRequest(request, response), webDataBinderFactory);
                    parameterValues[i] = value; // 賦值
                }

                // 對入參進行統一校驗
                Set<ConstraintViolation<Object>> violations = validatorImpl.validateParameters(method.getBean(), method.getMethod(), parameterValues, valid.value());
                // 若存在錯誤訊息,此處也做丟擲異常處理 javax.validation.ConstraintViolationException
                if (!violations.isEmpty()) {
                    System.err.println("方法入參校驗失敗~~~~~~~");
                    throw new ConstraintViolationException(violations);
                }
            }

        }

        return true;
    }

    private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
        Class<?> handlerType = handlerMethod.getBeanType();
        Set<Method> methods = this.initBinderCache.get(handlerType);
        if (methods == null) {
            // 支援到@InitBinder註解
            methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS);
            this.initBinderCache.put(handlerType, methods);
        }
        List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            initBinderMethods.add(new InvocableHandlerMethod(bean, method));
        }
        return new ServletRequestDataBinderFactory(initBinderMethods, adapter.getWebBindingInitializer());
    }

    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
            for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
                if (methodArgumentResolver.supportsParameter(parameter)) {
                    result = methodArgumentResolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
    
}

2、配置攔截器到Web容器裡(攔截所有請求),並且自己配置一個LocalValidatorFactoryBean

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    // 自己配置校驗器的工廠  自己隨意定製化哦~
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
        return new LocalValidatorFactoryBean();
    }

    // 配置用於校驗的攔截器
    @Bean
    public ValidationInterceptor validationInterceptor() {
        return new ValidationInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(validationInterceptor()).addPathPatterns("/**");
    }
}

3、Controller的方法(只需要在方法上標註即可)上標註@Validated註解:

    @Validated // 只需要方法處標註註解即可 非常簡便
    @GetMapping("/hello/id/{id}/status/{status}")
    public Object helloGet(@Max(5) @PathVariable("id") Integer id, @Min(5) @PathVariable("status") Integer status) {
        return "hello world";
    }

訪問/hello/id/6/status/4 能看到如下異常:

同樣的完美完成了我們的校驗需求。針對我自己書寫的這一套,這裡繼續有必要再說說兩個小細節:

  1. 本例的@PathVariable("id")是指定的value值的,因為在處理@PathVariable過程中我並沒有去分析位元組碼來得到形參名,所以為了簡便此處寫上value值,當然這裡是可以優化的,有興趣的小夥伴可自行定製
  2. 因為制定了value值,錯誤資訊中也能正確識別出欄位名了~
  3. Spring MVC的自動資料封裝體系中,value值不是必須的,只要欄位名對應上了也是ok的(這裡面運用了位元組碼技術,後文有講解)。但是在資料校驗中,它可並沒有用到位元組碼結束,請注意做出區分~~~

    總結

    本文介紹了兩種方案來處理我們平時遇到Controller中對處理方法平鋪型別的資料校驗問題,至於具體你選擇哪種方案當然是仁者見仁了。(方案一簡便,方案二需要你對Spring MVC的處理流程API很熟練,可炫技)

資料校驗相關知識介紹至此,不管是Java上的資料校驗,還是Spring上的資料校驗,都可以統一使用優雅的Bean Validation來完成了。希望這麼長時間來講的內容能對你的專案有實地的作用,真的能讓你的工程變得更加的簡介,甚至高能。畢竟真正做技術的人都是追求一定的極致性,甚至是存在程式碼潔癖,甚至是偏執的~

此種潔癖據我瞭解表現在多個方面:比如沒使用的變數一定要刪除、程式碼格式不好看一定要格式化、看到重複程式碼一定要提取公因子等等~

知識交流

若文章格式混亂,可點選:原文連結-原文連結-原文連結-原文連結-原文連結

==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被作者本人許可的~==

若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。並且備註:"java入群" 字樣,會手動邀請入