spring method validation的優化【自己動手實現校驗切面】
阿新 • • 發佈:2018-12-31
建議首先閱讀筆者的Spring method validation的不足
在【Spring method validation的不足】的文章中,筆者提出瞭如下問題:
- spring method validation 不支援方法物件檢視的校驗;
- 方法級別的校驗規則是統一校驗方法引數和方法返回值,不支援動態設定校驗方式;
- 校驗過程沒有對應的快速失敗機制。
解決方案:實現自己的MethodValidationAspect
步驟1:自定義@EnableValidation註解
/** * @author Te * @date created at 2018/11/29 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface EnableValidation { @AliasFor("groups") Class<?>[] value() default {}; /** * validation groups * * @return groups default {} */ @AliasFor("value") Class<?>[] groups() default {}; }
自定義EnableValidation註解,該註解可以應用在類或者方法層級,標註在類上表示校驗類下的所有方法,標註在方法上表示僅對該方法進行校驗。
其中groups表示執行方法校驗時的校驗組。
同時該註解標註了@Inherited,表明該註解支援繼承。
在這裡簡單說明下校驗組的概念
假設資料庫有user表,有id和name屬性。
如果對應的javabean中id和name屬性都標註了對應的約束,比如@NotNull,那麼在刪除和新增操作的時候都會去校驗兩個屬性是否為空。但是其實刪除的時候我們只需要去校驗id屬性,新增的時候只需要校驗name屬性,那麼上面就會出問題。這時候就可以使用校驗組,進行分組校驗。
具體可以參考如下程式碼:
//刪除分組
public interface DeleteGroup {
}
//新增分組
public interface SaveGroup {
}
public class User{
@NotNull(groups = {DeleteGroup.class})
private Long id;
@NotNull(groups = {SaveGroup.class})
private Long id;
}
只需要在進行具體操作的時候指定對應的校驗組就OK了,如下程式碼所示
public interface UserService{ //新增的時候使用SaveGroup的規範去校驗 void save(@Validated(groups={SaveGroup.class}) User user); //刪除的時候使用DeleteGroup的規範去校驗 void delete(@Validated(groups={DeleteGroup.class}) User user); }
步驟2:自定義切面
package com.mtdp.mtdp.aspect;
import com.mtdp.mtdp.annotations.EnableValidation;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
/**
* @author wangte
* @date created at 2018/11/29
*/
@Aspect
@Component
public class MethodValidationAspect implements InitializingBean {
private final LocalValidatorFactoryBean localValidatorFactoryBean;
private final Class<? extends Annotation> ANNOTATION_TYPE = EnableValidation.class;
/**
* javax validator
*/
private Validator validator;
@Autowired
public MethodValidationAspect(LocalValidatorFactoryBean localValidatorFactoryBean) {
this.localValidatorFactoryBean = localValidatorFactoryBean;
}
@Pointcut(value = "@annotation(com.mtdp.mtdp.annotations.EnableValidation)")
private void methodLevelAnnotationPointcut() {
//methodLevel pointcut,nothing to do
}
/**
* with in annotation pointcut
*/
@Pointcut(value = "@within(com.mtdp.mtdp.annotations.EnableValidation)")
private void classLevelPointcut() {
//classLevel pointcut,nothing to do
}
@Before(value = "methodLevelAnnotationPointcut() || classLevelPointcut()")
public void before(JoinPoint joinPoint) {
//1、獲取方法引數列表
Object[] args = joinPoint.getArgs();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
EnableValidation enableValidation = ((EnableValidation) obtainValidation(method));
//2、獲取校驗組
Class<?>[] methodValidationGroups = determineValidationGroups(enableValidation);
//3、獲取方法校驗器
ExecutableValidator executableValidator = validator.forExecutables();
Set<ConstraintViolation<Object>> result = new HashSet<>();
//4、只有在方法引數列表不為空的情況下才會去校驗引數列表,如果方法引數列表為空,則沒有必要執行方法引數的驗證
if (ArrayUtils.isNotEmpty(args)) {
//只有開啟引數校驗,才會去校驗引數列表,在這裡讀者可以往註解裡定義新的屬性,比如validateParameter,獲取註解中validateParameter的值,如果為true,才進行下面的步驟
//4.1 首先進行方法級別的基本校驗
Set<ConstraintViolation<Object>> methodConstraintViolations = executableValidator.validateParameters(joinPoint.getThis(), method, args, methodValidationGroups);
result.addAll(methodConstraintViolations);
//4.2 進行方法Bean級別的校驗
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
if (ArrayUtils.isNotEmpty(parameterAnnotations)) {
for (int i = 0; i < parameterAnnotations.length; i++) {
//4.2.1 判斷是否需要執行bean級別的校驗
//校驗標準:判斷方法引數bean上是否標註了spring Validated註解
Annotation[] parameterAnnotation = parameterAnnotations[i];
for (Annotation annotation : parameterAnnotation) {
if (annotation.annotationType() == Validated.class) {
Class<?>[] beanValidationGroups = ((Validated) annotation).value();
if (args[i] == null) {
continue;
}
//spring 4.x不支援容器級別Bean檢視的校驗,在這裡只實現了基本Bean的校驗,如果要實現容器級別Bean檢視校驗,可以抽出一個方法,判斷bean的型別,如果是List,則校驗List泛型中的元素,Set、Map、Optional等等,也是如此,這裡讀者可以自己實現
Set<ConstraintViolation<Object>> beanConstraintViolations = validator.validate(args[i], beanValidationGroups);
result.addAll(beanConstraintViolations);
}
}
}
}
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
}
/**
* 獲取方法對應的校驗組
*/
private Class<?>[] determineValidationGroups(EnableValidation enableValidation) {
Class<?>[] groups = enableValidation.groups();
return ArrayUtils.isEmpty(groups) ? enableValidation.value() : groups;
}
@Override
public void afterPropertiesSet() throws Exception {
//如果通過Validation工廠方法獲取到的validator,底層的validator是使用Hibernate validator進行反射建立,不歸spring容器管理,不能在validator中使用依賴注入。
//使用以下這種方式得到的validator可以進行spring的依賴注入。這種方式其實是將validator的建立交給spring建立管理而已。
validator = localValidatorFactoryBean.getValidator();
}
private Annotation obtainValidation(Method method) {
Annotation validatedAnn = AnnotationUtils.findAnnotation(method, ANNOTATION_TYPE);
if (validatedAnn == null) {
validatedAnn = AnnotationUtils.findAnnotation(method.getClass(), ANNOTATION_TYPE);
}
return validatedAnn;
}
}