1. 程式人生 > >spring method validation的優化【自己動手實現校驗切面】

spring method validation的優化【自己動手實現校驗切面】

建議首先閱讀筆者的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;
    }
}