摘要:方案一借助對方法級別數(shù)據(jù)校驗的能力首先必須明確一點此能力屬于框架的,而部分框架。
每篇一句
在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現(xiàn)在很多編程框架都只是教你碎片化的實踐)相關(guān)閱讀
【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】@Validated和@Valid的區(qū)別?教你使用它完成Controller參數(shù)校驗(含級聯(lián)屬性校驗)以及原理分析
【小家Spring】Spring方法級別數(shù)據(jù)校驗:@Validated + MethodValidationPostProcessor優(yōu)雅的完成數(shù)據(jù)校驗動作
我們知道Spring MVC層是默認可以支持Bean Validation的,但是我在實際使用起來有很多不便之處(相信我的使用痛點也是小伙伴的痛點),就感覺它是個半拉子:只支持對JavaBean的驗證,而并不支持對Controller處理方法的平鋪參數(shù)的校驗。
上篇文章一起了解了Spring MVC中對Controller處理器入?yún)⑿r灥膯栴},但也僅局限于對JavaBean的驗證。不可否認對JavaBean的校驗是我們實際項目使用中較為常見、使用頻繁的case,關(guān)于此部分詳細內(nèi)容可參見:【小家Spring】@Validated和@Valid的區(qū)別?教你使用它完成Controller參數(shù)校驗(含級聯(lián)屬性校驗)以及原理分析
在上文我也提出了使用痛點:我們Controller控制器方法中入?yún)?,其實大部分情況下都是平鋪參數(shù)而非JavaBean的。然而對于平鋪參數(shù)我們并不能使用@Validated像校驗JavaBean一樣去做,并且Spring MVC也并沒有提供源生的解決方案(其實提供了,哈哈)。
那怎么辦?難道真的只能自己書寫重復的if else去完成嗎?當然不是,那么本文將對此常見的痛點問題(現(xiàn)象)提供兩種思路,供給使用者參考~
因為Spring MVC并不天然支持對控制器方法平鋪參數(shù)的數(shù)據(jù)校驗,但是這種case的卻有非常的常見,因此針對這種常見現(xiàn)象提供一些可靠的解決方案,對你的項目的收益是非常高的。
首先必須明確一點:此能力屬于Spring框架的,而部分web框架Spring MVC。
Spring對方法級別數(shù)據(jù)校驗的能力非常重要(它能對Service層、Dao層的校驗等),前面也重點分析過,具體使用方式參考本文:【小家Spring】Spring方法級別數(shù)據(jù)校驗:@Validated + MethodValidationPostProcessor優(yōu)雅的完成數(shù)據(jù)校驗動作
使用此種方案來解決問題的步驟比較簡單,使用起來也非常方便。下面我寫個簡單示例作為參考:
@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并沒有按照順序來,字段可別對應(yīng)錯了~~~
由此可見,校驗生效了。拋出了javax.validation.ConstraintViolationException異常,這樣我們再結(jié)合一個全局異常的處理程序,也就能達到我們預定的效果了~
這種方案一樣有一個非常值得注意但是很多人都會忽略的地方:因為我們希望能夠代理Controller這個Bean,所以僅僅只在父容器中配置MethodValidationPostProcessor是無效的,必須在子容器(web容器)的配置文件中再配置一個MethodValidationPostProcessor,請務(wù)必注意~
有小伙伴問我了,為什么它的項目里只配置了一個MethodValidationPostProcessor也生效了呢? 我的回答是:檢查一下你是否是用的SpringBoot。
其實關(guān)于配置一個還是多個MethodValidationPostProcessor的case,其實是個Bean覆蓋有很大關(guān)系的,這方面內(nèi)容可參考:【小家Spring】聊聊Spring的bean覆蓋(存在同名name/id問題),介紹Spring名稱生成策略接口BeanNameGenerator
方案一的使用已經(jīng)很簡單了,但我個人總還覺得怪怪的,因為我一直不喜歡Controller層被代理(可能是潔癖吧)。因此針對這個現(xiàn)象,我自己接下來提供一個自定義攔截器HandlerInterceptor的處理方案來實現(xiàn),大家不一定要使用,也是供以參考嘛~
設(shè)計思路:Controller攔截器 + @Validated注解 + 自定義校驗器(當然這里面涉及到不少細節(jié)的:比如入?yún)⒔馕?、綁定等等?nèi)置的API)
1、準備一個攔截器ValidationInterceptor用于處理校驗邏輯:
// 注意:此處只支持@RequesrMapping方式~~~~ public class ValidationInterceptor implements HandlerInterceptor, InitializingBean { @Autowired private LocalValidatorFactoryBean validatorFactoryBean; @Autowired private RequestMappingHandlerAdapter adapter; private ListargumentResolvers; @Override public void afterPropertiesSet() throws Exception { argumentResolvers = adapter.getArgumentResolvers(); } // 緩存 private final Map argumentResolverCache = new ConcurrentHashMap<>(256); private final Map , Set > 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) { // 根據(jù)工廠,拿到一個校驗器 ValidatorImpl validatorImpl = (ValidatorImpl) validatorFactoryBean.getValidator(); // 拿到該方法所有的參數(shù)們~~~ org.springframework.core.MethodParameter MethodParameter[] parameters = method.getMethodParameters(); Object[] parameterValues = new Object[parameters.length]; //遍歷所有的入?yún)ⅲ航o每個參數(shù)做賦值和數(shù)據(jù)綁定 for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; // 找到適合解析這個參數(shù)的處理器~ 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; // 賦值 } // 對入?yún)⑦M行統(tǒng)一校驗 Set > violations = validatorImpl.validateParameters(method.getBean(), method.getMethod(), parameterValues, valid.value()); // 若存在錯誤消息,此處也做拋出異常處理 javax.validation.ConstraintViolationException if (!violations.isEmpty()) { System.err.println("方法入?yún)⑿r炇~~~~~~"); throw new ConstraintViolationException(violations); } } } return true; } private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) { Class> handlerType = handlerMethod.getBeanType(); Set methods = this.initBinderCache.get(handlerType); if (methods == null) { // 支持到@InitBinder注解 methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS); this.initBinderCache.put(handlerType, methods); } List 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 能看到如下異常:
同樣的完美完成了我們的校驗需求。針對我自己書寫的這一套,這里繼續(xù)有必要再說說兩個小細節(jié):
本例的@PathVariable("id")是指定的value值的,因為在處理@PathVariable過程中我并沒有去分析字節(jié)碼來得到形參名,所以為了簡便此處寫上value值,當然這里是可以優(yōu)化的,有興趣的小伙伴可自行定制
因為制定了value值,錯誤信息中也能正確識別出字段名了~
在Spring MVC的自動數(shù)據(jù)封裝體系中,value值不是必須的,只要字段名對應(yīng)上了也是ok的(這里面運用了字節(jié)碼技術(shù),后文有講解)。但是在數(shù)據(jù)校驗中,它可并沒有用到字節(jié)碼結(jié)束,請注意做出區(qū)分~~~
總結(jié)本文介紹了兩種方案來處理我們平時遇到Controller中對處理方法平鋪類型的數(shù)據(jù)校驗問題,至于具體你選擇哪種方案當然是仁者見仁了。(方案一簡便,方案二需要你對Spring MVC的處理流程API很熟練,可炫技)
數(shù)據(jù)校驗相關(guān)知識介紹至此,不管是Java上的數(shù)據(jù)校驗,還是Spring上的數(shù)據(jù)校驗,都可以統(tǒng)一使用優(yōu)雅的Bean Validation來完成了。希望這么長時間來講的內(nèi)容能對你的項目有實地的作用,真的能讓你的工程變得更加的簡介,甚至高能。畢竟真正做技術(shù)的人都是追求一定的極致性,甚至是存在代碼潔癖,甚至是偏執(zhí)的~
此種潔癖據(jù)我了解表現(xiàn)在多個方面:比如沒使用的變量一定要刪除、代碼格式不好看一定要格式化、看到重復代碼一定要提取公因子等等~知識交流
若文章格式混亂,可點擊:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==
**若對技術(shù)內(nèi)容感興趣可以加入wx群交流:Java高工、架構(gòu)師3群。
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會手動邀請入群**
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75695.html
摘要:畢竟永遠相信本文能給你帶來意想不到的收獲使用示例關(guān)于數(shù)據(jù)校驗這一塊在中的使用案例,我相信但凡有點經(jīng)驗的程序員應(yīng)該沒有不會使用的,并且還不乏熟練的選手。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術(shù) 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validati...
摘要:如果說要使用數(shù)據(jù)校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的環(huán)境。具體使用可參考小家讓支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗默認使用只能對進行校驗級聯(lián)校驗什么叫級聯(lián)校驗,其實就是帶校驗的成員里存在級聯(lián)對象時,也要對它完成校驗。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術(shù) 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(...
摘要:就這樣借助相關(guān)約束注解,就非常簡單明了,語義清晰的優(yōu)雅的完成了方法級別入?yún)⑿r灧祷刂敌r灥男r?。但倘若是返回值校驗?zhí)行了即使是失敗了,方法體也肯定被執(zhí)行了只能哪些類型上提出這個細節(jié)的目的是約束注解并不是能用在所有類型上的。 每篇一句 在《深度工作》中作者提出這么一個公式:高質(zhì)量產(chǎn)出=時間*專注度。所以高質(zhì)量的產(chǎn)出不是靠時間熬出來的,而是效率為王 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校...
摘要:否則非法請求參數(shù)小則影響用戶體驗或者產(chǎn)生垃圾數(shù)據(jù),大則會拖跨整個系統(tǒng)其次,手工對所有的參數(shù)進行校驗相當繁瑣,容易出錯,而且最后,通過工具來完成其實是比較好的方式,但是必須讓工具變得優(yōu)雅一些。 聲明:本文屬原創(chuàng)文章,始發(fā)于公號:程序員自學之道,同步發(fā)布到 sf,轉(zhuǎn)載請注明出處。 不夠好的方案 在 Web 開發(fā)中, 我們經(jīng)常需要校驗各種參數(shù),這是一件繁瑣又重要的事情,對于很多人來說,在做參...
摘要:例如,將請求信息中的字符串格式參數(shù)轉(zhuǎn)換為對應(yīng)方法中的類類型入?yún)⒖赏ㄟ^的屬性注冊自定義轉(zhuǎn)換器。 1. 處理流程 請求提交給DispatchServlet 查找HandlerMapping 調(diào)用由HandlerAdapter封裝后的Handler 返回ModelAndView到DispatcherServlet 借由ViewResolver完成邏輯視圖到真實視圖的轉(zhuǎn)換 返回響應(yīng) 2. ...
閱讀 3461·2019-08-30 15:55
閱讀 2058·2019-08-30 15:44
閱讀 1464·2019-08-30 12:47
閱讀 752·2019-08-30 11:05
閱讀 1637·2019-08-30 10:54
閱讀 663·2019-08-29 16:07
閱讀 3575·2019-08-29 14:17
閱讀 2234·2019-08-23 18:31