每篇一句
在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多編程框架都只是教你碎片化的實踐)
相關閱讀
【小家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入群"
字樣,會手動邀請入群