前言
本文Spring版本為 SpringBoot-2.0.7票腰,所有源碼相關(guān)類、方法封救、代碼行都以此版本為基礎(chǔ)拇涤。
代碼行數(shù): 使用 IDEA 的同學(xué)通過Maven Projects -> Donwload Sources and Documentation
下載源碼及注釋文檔,保證行數(shù)的準確誉结。非常歡迎您指正在文章中出現(xiàn)的錯誤鹅士,包括但不限于 語句錯誤、描述錯誤惩坑、示例錯誤掉盅、代碼理解錯誤。
參數(shù)校驗是代碼開發(fā)中必不可少的一環(huán)以舒,一個方法中參數(shù)校驗套了一個又一個 if-else趾痘,繁瑣的操作讓廣大程序員詬病。
本文我們就講一下 SpringBoot 結(jié)合 Hibernate-Validtor 校驗參數(shù)蔓钟、簡化工作永票。
開始
spring-boot-starter-web 已經(jīng)默認整合、提供了 Hibernate-Validator 的功能滥沫,只待我們?nèi)ナ褂谩?/p>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
創(chuàng)建一個實體類侣集,并添加校驗注解。
本小節(jié)只做簡單的使用演示兰绣。
常用注解列表世分、注解說明、注解用法缀辩,以及·自定義校驗注解·的教程罚攀。JSR 303 - Bean Validation 介紹及最佳實踐
public class Student{
@NotNull
private String name;
@NotNull
private String sex;
@Min(0)
@Max(150)
private int age;
...get,set...
}
接著編寫 Controller 代碼。
// @RestController
// DemoController
@GetMapping("/student")
public String validator(@Validated Student student, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
啟動程序后訪問http://{host:prot}/student
雌澄,將會返回:
must not be null
訪問http://{host:prot}/student?name=zhangsan&sex=Male&age=22
斋泄,將會返回:
ok
到這,本期的教程結(jié)束...是不可能的镐牺。
進階
上面的教程還太簡單炫掐,很多事情都很朦朧。
- 書寫有沒有什么規(guī)則睬涧?
- 我怎么知道‘must not be null’是指哪個參數(shù)募胃?跟沒提示一樣。
- 每個 Controller 方法都要判斷 BindingResult 還是好麻煩畦浓!我懶得寫痹束!
- 校驗規(guī)則太少了,能不能自己寫規(guī)則讶请?
- 我想手動校驗怎么辦祷嘶?
書寫規(guī)則
@Validated 和 @Valid 的異同
@Validated 是 Spring 實現(xiàn)的JSR-303的變體 @Valid ,支持驗證組的規(guī)范。 設(shè)計用于方便使用Spring的JSR-303支持论巍,但不支持JSR-303特定烛谊。
@Valid JSR-303標準實現(xiàn)的校驗注解。
注解 | 范圍 | 嵌套 | 校驗組 |
---|---|---|---|
@Validated | 可以標記類嘉汰、方法丹禀、方法參數(shù),不能用在成員屬性(字段)上 | 不支持 | 支持 |
@Valid | 可以標記方法鞋怀、構(gòu)造函數(shù)双泪、方法參數(shù)和成員屬性(字段)上 | 支持 | 不支持 |
兩者都可以用在方法入?yún)⑸希紵o法單獨提供嵌套驗證功能密似,都能配合嵌套驗證注解@Valid進行嵌套驗證焙矛。
嵌套驗證示例:
public class ClassRoom{
@NotNull
String name;
@Valid // 嵌套校驗,校驗參數(shù)內(nèi)部的屬性
@NotNull
Student student;
}
@GetMapping("/room") // 此處可使用 @Valid 或 @Validated, 將會進行嵌套校驗
public String validator(@Validated ClassRoom classRoom, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
BindingResult 的使用
BindingResult
必須跟在被校驗參數(shù)之后,若被校驗參數(shù)之后沒有BindingResult
對象辛友,將會拋出BindException
薄扁。
@GetMapping("/room")
public String validator(@Validated ClassRoom classRoom, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
不要使用 BindingResult
接收,String等簡單對象的錯誤信息废累。簡單對象校驗失敗邓梅,會拋出 ConstraintViolationException
。
主要就是接不著邑滨,你要寫也算是沒關(guān)系...
// ? 錯誤用法日缨,也沒有特別的錯,只是 result 是接不到值掖看。
@GetMapping("/room")
@Validated // 啟用校驗
public String validator(@NotNull String name, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
修改校驗失敗的提示信息
可以通過各個校驗注解的message
屬性設(shè)置更友好的提示信息匣距。
public class ClassRoom{
@NotNull(message = "Classroom name must not be null")
String name;
@Valid
@NotNull
Student student;
}
@GetMapping("/room")
@Validated
public String validator(ClassRoom classRoom, BindingResult result, @NotNull(message = "姓名不能為空") String name) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
message
屬性配置國際化的消息也可以的,message
中填寫國際化消息的code
哎壳,在拋出異常時根據(jù)code
處理一下就好了毅待。
@GetMapping("/room")
@Validated
public String validator(@NotNull(message = "demo.message.notnull") String name) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
// message_zh_CN.properties
demo.message.notnull=xxx消息不能為空
// message_en_US.properties
demo.message.notnull=xxx message must no be null
省略 Controller 中的校驗判斷
可以利用參數(shù)校驗失敗后拋出異常這點,配置·統(tǒng)一異常攔截·归榕,進行異常統(tǒng)一的處理尸红,合理的將錯誤信息返回給前端。
拋磚(僅做示例):
// @RestControllerAdvice
/* 數(shù)據(jù)校驗處理 */
@ExceptionHandler({BindException.class, ConstraintViolationException.class})
public String validatorExceptionHandler(Exception e) {
String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())
: msgConvertor(((ConstraintViolationException) e).getConstraintViolations());
return msg;
}
/**
* 校驗消息轉(zhuǎn)換拼接
*
* @param bindingResult
* @return
*/
public static String msgConvertor(BindingResult bindingResult) {
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
StringBuilder sb = new StringBuilder();
fieldErrors.forEach(fieldError -> sb.append(fieldError.getDefaultMessage()).append(","));
return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
}
private String msgConvertor(Set<ConstraintViolation<?>> constraintViolations) {
StringBuilder sb = new StringBuilder();
constraintViolations.forEach(violation -> sb.append(violation.getMessage()).append(","));
return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
}
注:getMessage
和getDefaultMessage
都是直接獲取注解上message
屬性的值刹泄,
擴展校驗注解外里、校驗規(guī)則
常用注解列表、注解說明特石、注解用法盅蝗,以及·自定義校驗注解·的教程。JSR 303 - Bean Validation 介紹及最佳實踐
手動校驗
若沒有手動配置Validator
對象姆蘸,自然需要從 Spring 容器中獲取校驗器對象墩莫,注入使用芙委。
此處給出一個手動校驗的工具類,供大家參考贼穆。(lay了...寫的自閉题山,如果對代碼有疑問請聯(lián)系我..持續(xù)更新)
代碼中提到的與 Spring 集成兰粉,主要是對代碼返回值的統(tǒng)一故痊。(不支持普通對象...)
若都以注解的message
屬性來獲取提示消息,可以刪除 Spring 相關(guān)的代碼玖姑。
若不以message
屬性作為消息愕秫,那么可以從bindingResult
中獲取字段、類焰络、注解信息戴甩,拼裝成消息碼。
拋磚:
// config
// @Configuration
@Bean
public Validator validator() {
return ValidatorUtils.getValidator();
}
import org.hibernate.validator.HibernateValidator;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
/**
* hibernate-validator校驗工具類
*/
public class ValidatorUtils {
private static Validator validator;
private static SmartValidator validatorAdapter;
static {
// 快速返回模式
validator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
public static Validator getValidator() {
return validator;
}
private static SmartValidator getValidatorAdapter(Validator validator) {
if (validatorAdapter == null) {
validatorAdapter = new SpringValidatorAdapter(validator);
}
return validatorAdapter;
}
/**
* 校驗參數(shù)闪彼,用于普通參數(shù)校驗 [未測試甜孤!]
*
* @param
*/
public static void validateParams(Object... params) {
Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params);
if (!constraintViolationSet.isEmpty()) {
throw new ConstraintViolationException(constraintViolationSet);
}
}
/**
* 校驗對象
*
* @param object
* @param groups
* @param <T>
*/
public static <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups);
if (!constraintViolationSet.isEmpty()) {
throw new ConstraintViolationException(constraintViolationSet);
}
}
/**
* 校驗對象
* 使用與 Spring 集成的校驗方式。
*
* @param object 待校驗對象
* @param groups 待校驗的組
* @throws BindException
*/
public static <T> void validateBySpring(T object, Class<?>... groups)
throws BindException {
DataBinder dataBinder = getBinder(object);
dataBinder.validate((Object[]) groups);
if (dataBinder.getBindingResult().hasErrors()) {
throw new BindException(dataBinder.getBindingResult());
}
}
private static <T> DataBinder getBinder(T object) {
DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass()));
dataBinder.setValidator(getValidatorAdapter(validator));
return dataBinder;
}
}
源碼經(jīng)驗寶寶[拓展]
為什么 BindingResult
接收不到簡單對象的校驗信息畏腕?
跟進 Spring MVC 源碼缴川,發(fā)現(xiàn):SpringMVC 在進行方法參數(shù)的注入(將 Http請求參數(shù)封裝成方法所需的參數(shù))時,不同的對象使用不同的解析器注入對象描馅。
聽著好像沒什么關(guān)系把夸。但其實就是,注入實體對象時使用ModelAttributeMethodProcessor
中的校驗方法铭污,而注入 String 對象使用AbstractNamedValueMethodArgumentResolver
中的校驗方法恋日。正是這個差異導(dǎo)致了BindingResult
無法接受到簡單對象(簡單的入?yún)?shù)類型)的校驗信息。
班谀岂膳?你問我什么是簡單對象?emm...
八大基礎(chǔ)類型再加上不同解析器支持的類型對象(不同的參數(shù)類型)磅网,需要看各解析器實現(xiàn)的supportsParameter()
方法谈截,文中提到的簡單對象,意思是ModelAttributeMethodProcessor
不支持的所有對象知市。
獲取參數(shù)注入解析器
的源碼位于HandlerMethodArgumentResolverComposite#resolveArgument():120
:
// HandlerMethodArgumentResolverComposite.class
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 獲取 parameter 參數(shù)的解析器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
// 調(diào)用解析器獲取參數(shù)
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 獲取 parameter 參數(shù)的解析器
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
// 從緩存中獲取參數(shù)對應(yīng)的解析器
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
// 解析器是否支持該參數(shù)類型
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
return result;
}
注入 String 參數(shù)時傻盟,在AbstractNamedValueMethodArgumentResolver#resolveArgument()
中,不會拋出BindException/ConstraintViolationException
異常嫂丙、也不會將 BindingResult 傳入到方法中娘赴。
注入對象時在ModelAttributeMethodProcessor#resolveArgument():154
行的 validateIfApplicable(binder, parameter)
語句,進行了參數(shù)校驗,校驗不通過并且實體對象后不存在BindingResult
對象跟啤,則會在this#resolveArgument():156
拋出BindException
诽表。
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// bean 參數(shù)綁定和校驗
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
// 參數(shù)校驗
validateIfApplicable(binder, parameter);
// 校驗結(jié)果包含錯誤唉锌,并且該對象后不存在 BindingResult 對象,就拋出異常
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
// 在對象后注入 BindingResult 對象
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
}
在哪里拋出ConstraintViolationException
竿奏?
可能有同學(xué)發(fā)現(xiàn)了袄简,簡單對象注入后并沒有拋出異常,那這個參數(shù)在哪里被校驗?zāi)兀?/p>
被方法級的攔截器攔住了泛啸。
這里的方法攔截器是 MethodValidationInterceptor
:
// MethodValidationInterceptor.class
public Object invoke(MethodInvocation invocation) throws Throwable {
ExecutableValidator execVal = this.validator.forExecutables();
// 校驗參數(shù)
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// 解決參數(shù)錯誤異常绿语、再次校驗
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// 執(zhí)行結(jié)果
Object returnValue = invocation.proceed();
// 校驗返回值
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
over.
本文到此結(jié)束。
非常歡迎您指正在文章中出現(xiàn)的錯誤候址,包括但不限于 語句錯誤吕粹、描述錯誤、示例錯誤岗仑、代碼理解錯誤匹耕。