讓Controller支援對平鋪引數執行@Valid資料校驗
每篇一句
在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多程式設計框架都只是教你碎片化的實踐)
相關閱讀
【小家Java】深入瞭解資料校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】@Validated和@Valid的區別?教你使用它完成Controller引數校驗(含級聯屬性校驗)以及原理分析
【小家Spring】Spring方法級別資料校驗:@Validated + MethodValidationPostProcessor優雅的完成資料校驗動作
前言
我們知道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
能看到如下異常:
同樣的完美完成了我們的校驗需求。針對我自己書寫的這一套,這裡繼續有必要再說說兩個小細節:
- 本例的
@PathVariable("id")
是指定的value
值的,因為在處理@PathVariable
過程中我並沒有去分析位元組碼來得到形參名,所以為了簡便此處寫上value值,當然這裡是可以優化的,有興趣的小夥伴可自行定製 - 因為制定了
value
值,錯誤資訊中也能正確識別出欄位名了~ 在
Spring MVC
的自動資料封裝體系中,value
值不是必須的,只要欄位名對應上了也是ok
的(這裡面運用了位元組碼技術,後文有講解)。但是在資料校驗中,它可並沒有用到位元組碼結束,請注意做出區分~~~總結
本文介紹了兩種方案來處理我們平時遇到
Controller
中對處理方法平鋪型別的資料校驗問題,至於具體你選擇哪種方案當然是仁者見仁了。(方案一簡便,方案二需要你對Spring MVC
的處理流程API
很熟練,可炫技)
資料校驗相關知識介紹至此,不管是Java
上的資料校驗,還是Spring
上的資料校驗,都可以統一使用優雅的Bean Validation
來完成了。希望這麼長時間來講的內容能對你的專案有實地的作用,真的能讓你的工程變得更加的簡介,甚至高能。畢竟真正做技術的人都是追求一定的極致性,甚至是存在程式碼潔癖,甚至是偏執的~
此種潔癖據我瞭解表現在多個方面:比如沒使用的變數一定要刪除、程式碼格式不好看一定要格式化、看到重複程式碼一定要提取公因子等等~
知識交流
若文章格式混亂,可點選
:原文連結-原文連結-原文連結-原文連結-原文連結
==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被作者本人許可的~
==
若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群
。
若群二維碼失效,請加wx號:fsx641385712
(或者掃描下方wx二維碼)。並且備註:"java入群"
字樣,會手動邀請入