什么是JSR303?
JSR 303 – Bean Validation 是一個數據驗證的規范,2009 年 11 月確定最終方案。
Hibernate Validator 是 Bean Validation 的最佳實踐。
為什么使用JSR,松耦合,讓業務代碼的職責更加清晰。
松耦合就是職責更加清晰,每個人都有自己的職責,如果你的代碼進行改動,我不用改動或者僅僅少量改動就可以發布和部署。
准備工作
maven 配置
<!-- JSR 303 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- Hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>
SpringMVC 配置
<mvc:annotation-driven validator="validator">
</mvc:annotation-driven>
<!-- 配置校驗器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<!-- 校驗器,使用Hibernate校驗器 -->
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<!-- 指定校驗使用的資源文件,在文件中配置校驗錯誤信息,如果不指定則默認使用classpath下面的ValidationMessages.properties文件, -->
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<!-- 校驗錯誤信息配置文件,也可以不配置,直接使用注解中的message即可 -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<!-- 資源文件名 -->
<property name="basenames">
<list>
<value>classpath:messageSource</value>
</list>
</property>
<!-- 資源文件編碼格式 -->
<property name="fileEncodings" value="utf-8"/>
<!-- 對資源文件內容緩存時間,單位秒 -->
<property name="cacheSeconds" value="120"/>
</bean>
常用校驗注解
注解 | 運行時檢查 |
---|---|
@AssertFalse | 被注解的元素必須為false |
@AssertTrue | 被注解的元素必須為true |
@DecimalMax(value) | 被注解的元素必須為一個數字,其值必須小於等於指定的最大值 |
@DecimalMin(Value) | 被注解的元素必須為一個數字,其值必須大於等於指定的最小值 |
@Digits(integer=, fraction=) | 被注解的元素必須為一個數字,其值必須在可接受的范圍內 |
@Future | 被注解的元素必須是日期,檢查給定的日期是否比現在晚 |
@Max(value) | 被注解的元素必須為一個數字,其值必須小於等於指定的最大值 |
@Min(value) | 被注解的元素必須為一個數字,其值必須大於等於指定的最小值 |
@NotNull | 被注解的元素必須不為null |
@Null | 被注解的元素必須為null |
@Past(java.util.Date/Calendar) | 被注解的元素必須過去的日期,檢查標注對象中的值表示的日期比當前早 |
@Pattern(regex=, flag=) | 被注解的元素必須符合正則表達式,檢查該字符串是否能夠在match指定的情況下被regex定義的正則表達式匹配 |
@Size(min=, max=) | 被注解的元素必須在制定的范圍(數據類型:String, Collection, Map and arrays) |
@Valid | 遞歸的對關聯對象進行校驗, 如果關聯對象是個集合或者數組, 那么對其中的元素進行遞歸校驗,如果是一個map,則對其中的值部分進行校驗 |
@CreditCardNumber | 對信用卡號進行一個大致的驗證 |
被注釋的元素必須是電子郵箱地址 | |
@Length(min=, max=) | 被注解的對象必須是字符串的大小必須在制定的范圍內 |
@NotBlank | 被注解的對象必須為字符串,不能為空,檢查時會將空格忽略 |
@NotEmpty | 被注釋的對象必須不為空(數據:String,Collection,Map,arrays) |
@Range(min=, max=) | 被注釋的元素必須在合適的范圍內 (數據:BigDecimal, BigInteger, String, byte, short, int, long and 原始類型的包裝類 ) |
@URL(protocol=, host=, port=, regexp=, flags=) | 被注解的對象必須是字符串,檢查是否是一個有效的URL,如果提供了protocol,host等,則該URL還需滿足提供的條件 |
Bean驗證
首先向我們的bean中添加注解。
public class User {
@NotEmpty(message = "用戶名不為空")
private String username;
@NotEmpty(message = "密碼不為空")
private String password;
// getter 和 setter
}
Controller
中配置:
@RestController
public class UserAction {
@PostMapping("/login")
public Result login(@Validated User user, Errors errors) {
System.out.println(user);
if (errors.hasErrors()) {
return ResultUtil.messageResult(errors);
}
return ResultUtil.SUCCESS_RESULT;
}
}
我們只需要在要校驗的bean前面添加@Validated
,在需要校驗的bean后面添加Errors
對象來接收校驗出錯信息即可,然后根據錯誤信息進行判斷和返回錯誤信息給前端。
注意:
@Validated
和 Errors errors
是成對出現的,並且形參順序是固定的(一前一后)。也就是所每一個@Validated
后面必須跟一個Errors
,需要驗證多個bean,后面就跟多個Errors
。
AOP處理Errors
如果我們通過JSR來驗證bean對象,那么在每個需要驗證的方法中都需要處理Error對象,很容易想到可以通過AOP的方式來統一處理錯誤對象,並且組織錯誤信息,返回給前端。
通過一個環繞通知對所有的action方法盡心攔截,如果發現有Errors對象存在,就獲取所有的錯誤信息,封裝為一個list返回前端。
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import java.util.ArrayList;
import java.util.List;
/**
* @author 李文浩
* @version 2017/10/8.
*/
public class ValidationAdvice {
/**
* 切點處理
*
* @param pjp
* @return
* @throws Throwable
*/
public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
Errors errors = null;
Object[] args = pjp.getArgs();
if (null != args && args.length != 0) {
for (Object object : args) {
if (object instanceof BindingResult) {
errors = (BindingResult) object;
break;
}
}
}
if (errors != null && errors.hasErrors()) {
List<ObjectError> allErrors = errors.getAllErrors();
List<String> messages = new ArrayList<String>();
for (ObjectError error : allErrors) {
messages.add(error.getDefaultMessage());
}
return ResultUtil.messageResult(messages);
}
return pjp.proceed();
}
}
Spring配置:
<bean id="validationAdvice" class="studio.jikewang.util.ValidationAdvice" />
<aop:config>
<aop:pointcut id="validation1" expression="execution(public * studio.jikewang.action.*.*(..))" />
<aop:aspect id="validationAspect" ref="validationAdvice">
<aop:around method="aroundMethod" pointcut-ref="validation1" />
</aop:aspect>
</aop:config>
@Validated
和 @Valid
@Valid
是使用Hibernate Validation的時候使用。
Java的JSR303聲明了這類接口,然后hibernate-validator對其進行了實現。@Validated
是只用Spring Validator校驗機制使用。
方法參數驗證
Spring提供了MethodValidationPostProcessor
類,用於對方法的校驗。
Controller
中配置:
@RestController
@Validated
public class UserAction {
@PostMapping("/login")
public Result login(@NotEmpty(message = "用戶名不為空") String username,
@NotEmpty(message = "密碼不為空") String password) {
return ResultUtil.SUCCESS_RESULT;
}
}
xml配置(最好是配置在<mvc:annotation-driven validator="validator" />
上面,不然會有未知錯誤)如下:
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
</bean>
在校驗遇到非法的參數時會拋出ConstraintViolationException
,可以通過getConstraintViolations
獲得所有沒有通過的校驗ConstraintViolation
集合,可以通過它們來獲得對應的消息。
我們同樣使用 @ExceptionHandler
捕捉ConstraintViolationException
異常處理全局異常信息。
然后將所有的錯誤信息封裝好返回給前端。
@RestControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolationException(ConstraintViolationException e) {
List<String> list = new ArrayList<String>();
for (ConstraintViolation<?> s : e.getConstraintViolations()) {
System.out.println(s.getInvalidValue() + ": " + s.getMessage());
list.add(s.getMessage());
}
Result result = new Result();
result.setStatus("0");
result.setMessage(list);
return result;
}
}
使用@Validated
驗證list
現在我遇到一個新的需求,我需要前端給我傳遞一個對象數組,於是我使用一個list去接收,但是無法獲得驗證信息。
於是將list重新包裝一下。
public class ValidList<E> {
@Valid
private List<E> list;
public List<E> getList() {
return list;
}
public void setList(List<E> list) {
this.list = list;
}
}
Controller
中配置:
@RestController
public class UserAction {
@PostMapping("/login")
public Result login(@Validated ValidList<User> users, Errors errors) {
System.out.println(users.getList());
if (errors.hasErrors()) {
return ResultUtil.messageResult(errors);
}
return ResultUtil.SUCCESS_RESULT;
}
}
然后為了只返回第一個驗證失敗的信息(如果不更改,就會將所有的出錯信息返回給前端),更改ValidationAdvice
如下:
public class ValidationAdvice {
/**
* 切點處理
*
* @param pjp
* @return
* @throws Throwable
*/
public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
boolean isValidList = false;
Errors errors = null;
if (null != args && args.length != 0) {
for (Object object : args) {
if (object instanceof ValidList) {
isValidList = true;
}
if (object instanceof BindingResult) {
errors = (BindingResult) object;
break;
}
}
}
if (errors != null && errors.hasErrors()) {
List<ObjectError> allErrors = errors.getAllErrors();
List<String> messages = new ArrayList<String>();
for (ObjectError error : allErrors) {
if (isValidList) {
messages.add(error.getDefaultMessage());
break;
} else {
messages.add(error.getDefaultMessage());
}
}
return ResultUtil.messageResult(messages);
}
return pjp.proceed();
}
}
這樣即可驗證list
。
總結
AOP的思想是貫穿我們的開發的,使用AOP的思想可以大大提高我們的開發效率,減少重復代碼。
參考文檔: