深入Spring官網系列(十七):Java資料校驗

Hi丶ImViper發表於2020-10-30

在前文中我們一起學習了Spring中的資料繫結,也就是整個DataBinder的體系,其中有提到DataBinder跟校驗相關。可能對於Spring中的校驗大部分同學跟我一一樣,都只是知道可以通過@Valid / @Validated來對介面的入參進行校驗,但是對於其底層的具體實現以及一些細節都不是很清楚,通過這篇文章我們就來徹底搞懂Spring中的校驗機制。

在學習Spring中某個功能時,往往要從Java本身出發。比如我們之前介紹過的Spring中的國際化(見《Spring官網閱讀(十一)》)、Spring中的ResolvableType(見《Spring雜談》系列文章)等等,它們都是對Java本身的封裝,沿著這個思路,我們要學習Spring中的資料校驗,必然要先對Java中的資料校驗有一定了解。

話不多說,開始正文!

Java中的資料校驗

在學習Java中的資料校驗前,我們需要先了解一個概念,即什麼是JSR?

JSR:全稱Java Specification Requests,意思是Java 規範提案。我們可以將其理解為Java為一些功能指定的一系列統一的規範。跟資料校驗相關的最新的JSRJSR 380

Bean Validation 2.0 是JSR第380號標準。該標準連線如下:https://www.jcp.org/en/egc/view?id=380
Bean Validation的主頁:http://beanvalidation.org
Bean Validation的參考實現:https://github.com/hibernate/hibernate-validator

Bean Validation(JSR 380)

在這裡插入圖片描述
從官網中的截圖我們可以看到,Bean Validation 2.0的唯一實現就是Hibernate Validator,對應版本為6.0.1.Final,同時在2.0版本之前還有1.1(JSR 349)及1.0(JSR 303)兩個版本,不過版本間的差異並不是我們關注的重點,而且Bean Validation 2.0本身也向下做了相容。

在上面的圖中,可以看到Bean Validation2.0的全稱為Jakarta Bean Validation2.0,關於Jakarta,感興趣的可以參考這個連結:https://www.oschina.net/news/94055/jakarta-ee-new-logo,就是Java換了個名字。

使用示例

匯入依賴:

<!--除了匯入hibernate-validator外,還需要匯入一個tomcat-embed-el包,用於提供EL表示式的功能
因為錯誤message是支援EL表示式計算的,所以需要匯入此包
-->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.16</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.14.Final</version>
    <scope>compile</scope>
</dependency>
<!--
如果你用的是一個SpringBoot專案只需要匯入下面這個依賴即可

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

-->

測試Demo:

@Data
public class Person {
    
    @NotEmpty
    private String name;
    
    @Positive
    @Max(value = 100)
    private int age;
}

public class SpringValidation {
    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(-1);
        Set<ConstraintViolation<Person>> result =
               Validation.buildDefaultValidatorFactory().getValidator().validate(person);
        // 對結果進行遍歷輸出
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                .forEach(System.out::println);
    }
    // 執行結果:
    // name 不能為空: null
    // age 必須是正數: -1
}

對於其中涉及的細節目前來說我不打算過多的探討,我們現在只需要知道Java提供了資料校驗的規範,同時Hibernate對其有一套實現就可以了,並且我們也驗證了使用其進行校驗是可行的。那麼接下來我們的問題就變成了Spring對Java的這套資料校驗的規範做了什麼支援呢?或者它又做了什麼擴充套件呢?

Spring對Bean Validation的支援

我們先從官網入手,看看Spring中如何使用資料校驗,我這裡就直接取官網中的Demo了

@Data
public class Person {
    private String name;
    private int age;
}

public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(-1);
        DirectFieldBindingResult errorResult = new DirectFieldBindingResult(person, "dmz");
        PersonValidator personValidator = new PersonValidator();
        personValidator.validate(person, errorResult);
        System.out.println(errorResult);
        // 程式列印:
//Field error in object 'dmz' on field 'name': rejected value [null]; codes //[name.empty.dmz.name,name.empty.name,name.empty.java.lang.String,name.empty]; arguments //[]; default message [null]
//Field error in object 'dmz' on field 'age': rejected value [-1]; codes //[negativevalue.dmz.age,negativevalue.age,negativevalue.int,negativevalue]; arguments //[]; default message [null]
        
    }
}

在上面的例子中,PersonValidator實現了一個Validator介面,這個介面是Spring自己提供的,全稱:org.springframework.validation.Validator,我們看看這個介面的定義

Spring中的Validator

org.springframework.validation.Validator是專門用於應用相關的物件的校驗器。

這個介面完全從基礎設施或者上下文中脫離的,這意味著它沒有跟web層或者資料訪問層或者其餘任何的某一個層次發生耦合。所以它能用於應用中的任意一個層次,能對應用中的任意一個物件進行校驗。,

介面定義

public interface Validator {
	
    // 此clazz是否可以被validate
	boolean supports(Class<?> clazz);
	
    // 執行校驗,錯誤訊息放在Errors中
    // 如果能執行校驗,通常也意味著supports方法返回true
	// 可以參考ValidationUtils這個工具類
	void validate(Object target, Errors errors);
}

UML類圖

在這裡插入圖片描述

SmartValidator

對Validator介面進行了增強,能進行分組校驗

public interface SmartValidator extends Validator {

	// validationHints:就是啟動的校驗組
    // target:需要校驗的結果
    // errors:封裝校驗
    void validate(Object target, Errors errors, Object... validationHints);
	
    // 假設value將被繫結到指定物件中的指定欄位上,並進行校驗
    // @since 5.1  這個方法子類需要複寫 否則不能使用
    default void validateValue(Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
        throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
    }
}

SpringValidatorAdapter

在之前的介面我們會發現,到目前為止Spring中的校驗跟Bean Validation還沒有產生任何交集,而SpringValidatorAdapter就完成了到Bean Validation的對接

// 可以看到,這個介面同時實現了Spring中的SmartValidator介面跟JSR中的Validator介面
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
	
    //@NotEmpty,@NotNull等註解都會有這三個屬性
	private static final Set<String> internalAnnotationAttributes = new HashSet<>(4);
	static {
		internalAnnotationAttributes.add("message");
		internalAnnotationAttributes.add("groups");
		internalAnnotationAttributes.add("payload");
	}
	
    // targetValidator就是實際完成校驗的物件
	@Nullable
	private javax.validation.Validator targetValidator;
	public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
		Assert.notNull(targetValidator, "Target Validator must not be null");
		this.targetValidator = targetValidator;
	}
	SpringValidatorAdapter() {
	}
	void setTargetValidator(javax.validation.Validator targetValidator) {
		this.targetValidator = targetValidator;
	}

    // 支援對所有型別的Bean的校驗
	@Override
	public boolean supports(Class<?> clazz) {
		return (this.targetValidator != null);
	}
	
    // 呼叫targetValidator完成校驗,並通過processConstraintViolations方法封裝校驗後的結果到Errors中
	@Override
	public void validate(Object target, Errors errors) {
		if (this.targetValidator != null) {
			processConstraintViolations(this.targetValidator.validate(target), errors);
		}
	}
	
    // 完成分組校驗
	@Override
	public void validate(Object target, Errors errors, Object... validationHints) {
		if (this.targetValidator != null) {
			processConstraintViolations(
					this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
		}
	}
	
    // 完成對物件上某一個欄位及給定值的校驗
	@SuppressWarnings("unchecked")
	@Override
	public void validateValue(
			Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {

		if (this.targetValidator != null) {
			processConstraintViolations(this.targetValidator.validateValue(
					(Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
		}
	}


	// @since 5.1
	// 將validationHints轉換成JSR中的分組
	private Class<?>[] asValidationGroups(Object... validationHints) {
		Set<Class<?>> groups = new LinkedHashSet<>(4);
		for (Object hint : validationHints) {
			if (hint instanceof Class) {
				groups.add((Class<?>) hint);
			}
		}
		return ClassUtils.toClassArray(groups);
	}

	// 省略對校驗錯誤的封裝
    // .....


	
    // 省略對JSR中validator介面的實現,都是委託給targetValidator完成的
    // ......

}

ValidatorAdapter

跟SpringValidatorAdapter同一級別的類,但是不同的是他沒有實現JSR中的Validator介面。一般不會使用這個類

CustomValidatorBean

public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean {
	
    // JSR中的介面,校驗器工廠
	@Nullable
	private ValidatorFactory validatorFactory;
	
    // JSR中的介面,用於封裝校驗資訊
	@Nullable
	private MessageInterpolator messageInterpolator;
	
     // JSR中的介面,用於判斷屬效能否被ValidatorProvider訪問
	@Nullable
	private TraversableResolver traversableResolver;

	// 忽略setter方法
	
    // 在SpringValidatorAdapter的基礎上實現了InitializingBean,在Bean初始化時呼叫,用於給上面三個屬性進行配置
	@Override
	public void afterPropertiesSet() {
		if (this.validatorFactory == null) {
			this.validatorFactory = Validation.buildDefaultValidatorFactory();
		}

		ValidatorContext validatorContext = this.validatorFactory.usingContext();
		MessageInterpolator targetInterpolator = this.messageInterpolator;
		if (targetInterpolator == null) {
			targetInterpolator = this.validatorFactory.getMessageInterpolator();
		}
		validatorContext.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator));
		if (this.traversableResolver != null) {
			validatorContext.traversableResolver(this.traversableResolver);
		}

		setTargetValidator(validatorContext.getValidator());
	}

}

LocalValidatorFactoryBean

public class LocalValidatorFactoryBean extends SpringValidatorAdapter
    implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
		//......
}

可以看到,這個類額外實現了ValidatorFactory介面,所以通過它不僅能完成校驗,還能獲取一個校驗器validator。

OptionalValidatorFactoryBean

public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {

	@Override
	public void afterPropertiesSet() {
		try {
			super.afterPropertiesSet();
		}
		catch (ValidationException ex) {
			LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
		}
	}

}

繼承了LocalValidatorFactoryBean,區別在於讓校驗器的初始化成為可選的,即使校驗器沒有初始化成功也不會報錯。

使用示例

在對整個體系有一定了解之後,我們通過一個例子來體會下Spring中資料校驗

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        // 將CustomValidatorBean註冊到容器中,主要是為了讓它經過初始化階段完成對校驗器的配置
        ac.register(CustomValidatorBean.class);
        // 重新整理啟動容器
        ac.refresh();
        // 獲取到容器中的校驗器
        CustomValidatorBean cb = ac.getBean(CustomValidatorBean.class);

        // 校驗simple組的校驗
        Person person = new Person();
        DirectFieldBindingResult simpleDbr = new DirectFieldBindingResult(person, "person");
        cb.validate(person, simpleDbr, Person.Simple.class);

        // 校驗Complex組的校驗
        DirectFieldBindingResult complexDbr = new DirectFieldBindingResult(person, "person");
        person.setStart(new Date());
        cb.validate(person, complexDbr, Person.Complex.class);
        System.out.println(complexDbr);
    }
}

執行結果我這裡就不貼出來了,大家可以自行測試


到目前為止,我們所接觸到的校驗的內容跟實際使用還是有很大區別,我相信在絕大多數情況下大家都不會採用前文所採用的這種方式去完成校驗,而是通過@Validated或者@Valid來完成校驗。

@Validated跟@Valid的區別

關於二者的區別網上有很多文章,但是實際二者的區別大家不用去記,我們只要看一看兩個註解的申明變一目瞭然了。

@Validated

// Target代表這個註解能使用在類/介面/列舉上,方法上以及方法的引數上
// 注意注意!!!! 它不能註解到欄位上
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
// 在執行時期仍然生效(註解不僅被儲存到class檔案中,jvm載入class檔案之後,仍然存在)
@Retention(RetentionPolicy.RUNTIME)
// 這個註解應該被 javadoc工具記錄. 預設情況下,javadoc是不包括註解的. 但如果宣告註解時指定了 @Documented,則它會被 javadoc 之類的工具處理, 所以註解型別資訊也會被包括在生成的文件中,是一個標記註解,沒有成員。
@Documented
public @interface Validated {
	// 校驗時啟動的分組
	Class<?>[] value() default {};

}

@Valid

// 可以作用於類,方法,欄位,建構函式,引數,以及泛型型別上(例如:Main<@Valid T> )
// 簡單來說,哪裡都可以放
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
    //沒有提供任何屬性
}

我們通過上面兩個註解的定義就能很快的得出它們的區別:

  1. 來源不同@ValidJSR的規範,來源於javax.validation包下,而@Validated是Spring自身定義的註解,位於org.springframework.validation.annotation包下

  2. 作用範圍不同@Validated無法作用在欄位上,正因為如此它就無法完成對級聯屬性的校驗。而@Valid

    沒有這個限制。

  3. 註解中的屬性不同@Validated註解中可以提供一個屬性去指定校驗時採用的分組,而@Valid沒有這個功能,因為@Valid不能進行分組校驗

我相信通過這個方法的記憶遠比看部落格死記要好~

實際生產應用

我們將分為兩部分討論

  1. 對Java的校驗
  2. 對普通引數的校驗

這裡說的普通引數的校驗是指引數沒有被封裝到JavaBean中,而是直接使用,例如:

test(String name,int age),這裡的name跟age就是簡單的引數。

而將name跟age封裝到JavaBean中,則意味著這是對JavaBean的校驗。

同時,按照校驗的層次,我們可以將其分為

  1. 對controller層次(介面層)的校驗
  2. 對普通方法的校驗

接下來,我們就按這種思路一一進行分析

子所以按照層次劃分是因為Spring在對介面上的引數進行校驗時,跟對普通的方法上的引數進行校驗採用的是不同的形式(雖然都是依賴於JSR的實現來完成的,但是呼叫JSR的手段不一樣

對JavaBean的校驗

待校驗的類

@Data
public class Person {

    // 錯誤訊息message是可以自定義的
    @NotNull//(groups = Simple.class)
    public String name;

    @Positive//(groups = Default.class)
    public Integer age;

    @NotNull//(groups = Complex.class)
    @NotEmpty//(groups = Complex.class)
    private List<@Email String> emails;

    // 定義兩個組 Simple組和Complex組
    public interface Simple {
    }

    public interface Complex {

    }
}

// 用於進行巢狀校驗
@Data
public class NestPerson {
    @NotNull
    String name;

    @Valid
    Person person;
}

對controller(介面)層次上方法引數的校驗

用於測試的介面

// 用於測試的介面
@RestController
@RequestMapping("/test")
public class Main {
	
    // 測試 @Valid對JavaBean的校驗效果
    @RequestMapping("/valid")
    public String testValid(
            @Valid @RequestBody Person person) {
        System.out.println(person);
        return "OK";
    }

    // 測試 @Validated對JavaBean的校驗效果
    @RequestMapping("/validated")
    public String testValidated(
            @Validated @RequestBody Person person) {
        System.out.println(person);
        return "OK";
    }
    
    // 測試 @Valid對JavaBean巢狀屬性的校驗效果
    @RequestMapping("/validNest")
    public String testValid(@Valid @RequestBody NestPerson person) {
        System.out.println(person);
        return "OK";
    }
	
    // 測試 @Validated對JavaBean巢狀屬性的校驗效果
    @RequestMapping("/validatedNest")
    public String testValidated(@Validated @RequestBody NestPerson person) {
        System.out.println(person);
        return "OK";
    }
}

測試用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class MainTest {

    @Autowired
    private WebApplicationContext context;

    @Autowired
    ObjectMapper objectMapper;

    MockMvc mockMvc;

    Person person;

    NestPerson nestPerson;

    @Before
    public void init() {
        person = new Person();
        person.setAge(-1);
        person.setName("");
        person.setEmails(new ArrayList<>());
        nestPerson = new NestPerson();
        nestPerson.setPerson(person);
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    public void testValid() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/valid")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(person));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }

    @Test
    public void testValidated() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validated")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(person));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }

    @Test
    public void testValidNest() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validatedNest")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(nestPerson));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }

    @Test
    public void testValidatedNest() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validatedNest")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(nestPerson));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }

}

測試結果

在這裡插入圖片描述

我們執行用例時會發現,四個用例均斷言失敗並且控制檯列印:Validation failed for argument …。

另外細心的同學可以發現,Spring預設有一個全域性異常處理器DefaultHandlerExceptionResolver

同時觀察日誌我們可以發現,全域性異常處理器處理的異常型別為:org.springframework.web.bind.MethodArgumentNotValidException

使用注意要點
  1. 如果想使用分組校驗的功能必須使用@Validated
  2. 不考慮分組校驗的情況,@Validated@Valid沒有任何區別
  3. 網上很多文章說@Validated不支援對巢狀的屬性進行校驗,這種說法是不準確的,大家可以對第三,四個介面方法做測試,執行的結果是一樣的。更準確的說法是@Validated不能作用於欄位上,而@Valid可以。

對普通方法的校驗

待測試的方法

@Service
//@Validated
//@Valid
public class DmzService {
    public void testValid(@Valid Person person) {
        System.out.println(person);
    }

    public void testValidated(@Validated Person person) {
        System.out.println(person);
    }
}

測試用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class DmzServiceTest {

    @Autowired
    DmzService dmzService;

    Person person;

    @Before
    public void init(){
        person = new Person();
        person.setAge(-1);
        person.setName("");
        person.setEmails(new ArrayList<>());
    }

    @Test
    public void testValid() {
        dmzService.testValid(person);
    }

    @Test
    public void testValidated() {
        dmzService.testValidated(person);
    }
}

我們分為三種情況測試

  1. 類上不新增任何註解

在這裡插入圖片描述

  1. 類上新增@Validated註解

在這裡插入圖片描述

  1. 類上新增@Valid註解

在這裡插入圖片描述

使用注意要點

通過上面的例子,我們可以發現,只有類上新增了@Vlidated註解,並且待校驗的JavaBean上新增了@Valid的情況下校驗才會生效。

所以當我們要對普通方法上的JavaBean引數進行校驗必須滿足下面兩個條件

  1. 方法所在的類上新增@Vlidated
  2. 待校驗的JavaBean引數上新增@Valid

對簡單引數校驗

對普通方法的校驗

用於測試的方法

@Service
@Validated
//@Valid
public class IndexService {
    public void testValid(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }

    public void testValidated(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }

    public void testValidNest(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }

    public void testValidatedNest(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }
}

測試用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class IndexServiceTest {
    @Autowired
    IndexService indexService;

    int age;

    String name;

    @Before
    public void init(){
        age=100;
        name = "";
    }
    @Test
    public void testValid() {
        indexService.testValid(age,name);
    }
    @Test
    public void testValidated() {
        indexService.testValidated(age,name);
    }
    @Test
    public void testValidNest() {
        indexService.testValidNest(age,name);
    }
    @Test
    public void testValidatedNest() {
        indexService.testValidatedNest(age,name);
    }
}

這裡的測試結果我就不再放出來了,大家猜也能猜到答案

使用注意要點
  1. 方法所在的類上新增@Vlidated@Valid註解無效),跟JavaBean的校驗是一樣的

對controller(介面)層次的校驗

@RestController
@RequestMapping("/test/simple")
// @Validated
public class ValidationController {

    @RequestMapping("/valid")
    public String testValid(
            @Valid @Max(10) int age, @Valid @NotBlank String name) {
        System.out.println(age + "      " + name);
        return "OK";
    }

    @RequestMapping("/validated")
    public String testValidated(
            @Validated @Max(10) int age, @Valid @NotBlank String name) {
        System.out.println(age + "      " + name);
        return "OK";
    }
}

在測試過程中會發現,不過是在引數前新增了@Valid或者@Validated校驗均不生效。這個時候不得不借助Spring提供的普通方法的校驗功能來完成資料校驗,也就是在類級別上新增@Valiv=dated(引數前面的@Valid或者@Validated可以去除)

使用注意要點

對於介面層次簡單引數的校驗需要藉助Spring對於普通方法校驗的功能,必須在類級別上新增@Valiv=dated註解。

注意

在上面的所有例子中我都是用SpringBoot進行測試的,如果在單純的SpringMVC情況下,如果對於普通方法的校驗不生效請新增如下配置:

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    return new MethodValidationPostProcessor();
}

實際上對於普通方法的校驗,就是通過這個後置處理器來完成的,它會生成一個代理物件幫助我們完成校驗。SpringBoot中預設載入了這個後置處理器,而SpringMVC需要手動配置

結合BindingResult使用

在上面的例子中我們可以看到,當對於介面層次的JavaBean進行校驗時,如果校驗失敗將會丟擲org.springframework.web.bind.MethodArgumentNotValidException異常,這個異常將由Spring預設的全域性異常處理器進行處理,但是有時候我們可能想在介面中拿到具體的錯誤進行處理,這個時候就需要用到BindingResult

如下:

在這裡插入圖片描述

可以發現,錯誤資訊已經被封裝到了BindingResult,通過BindingResult我們能對錯誤資訊進行自己的處理。請注意,這種做法只對介面中JavaBean的校驗生效,對於普通引數的校驗是無效的。


實際上經過上面的學習我們會發現,其實Spring中的校驗就是兩種(前面的分類是按場景分的)

  1. Spring在介面上對JavaBean的校驗
  2. Spring在普通方法上的校驗

第一種校驗失敗將丟擲org.springframework.web.bind.MethodArgumentNotValidException異常,而第二種校驗失敗將丟擲javax.validation.ConstraintViolationException異常

為什麼會這樣呢?

這是因為,對於介面上JavaBean的校驗是Spring在對引數進行繫結時做了一層封裝,大家可以看看org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument這段程式碼

	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
            // 獲取一個DataBinder
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
                // 進行校驗,實際上就是呼叫DataBinder完成校驗
				validateIfApplicable(binder, parameter);
                // 如果校驗出錯並且沒有提供BindingResult直接丟擲一個MethodArgumentNotValidException
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);
	}

但是對於普通方法的校驗時,Spring完全依賴於動態代理來完成引數的校驗。具體細節在本文中不多贅述,大家可以關注我後續文章,有興趣的同學可以看看這個後置處理器:MethodValidationPostProcessor

結合全域性異常處理器使用

在實際應用中,更多情況下我們結合全域性異常處理器來使用資料校驗的功能,實現起來也非常簡單,如下:

@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
	// 另外還有一個javax.validation.ConstraintViolationException異常處理方式也類似,這裡不再贅述
    // 關於全域性異常處理器的部分因為是跟SpringMVC相關的,另外牽涉到動態代理,所以目前我也不想做過多介紹
    // 大家只要知道能這麼用即可,實際的使用可自行百度,非常簡單
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();

        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError error : bindingResult.getFieldErrors()) {
            String field = error.getField();
            Object value = error.getRejectedValue();
            String msg = error.getDefaultMessage();
            String message = String.format("錯誤欄位:%s,錯誤值:%s,原因:%s;", field, value, msg);
            stringBuilder.append(message).append("\r\n");
        }
        return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
    }
}

總結

關於資料校驗我們就介紹到這裡了,其實我自己之前對Spring中具體的資料校驗的使用方法及其原理都非常的模糊,但是經過這一篇文章的學習,現在可以說知道自己用了什麼了並且知道怎麼用,也知道為什麼。這也是我寫這篇文章的目的。按照慣例,我們還是總結了一張圖,如下:

在這裡插入圖片描述

相關文章