使用 SpringAOP + hibernate-validator 完美实现自动参数校验

在前面的文章《Spring 参数校验最佳实践》 中,我们介绍过 SpringMVC 如何做自动参数校验并通过统一异常处理机制在校验不通过时返回统一的异常。然而这并不完美,如果我们用的是 RequestBody 来接收的参数,一旦校验失败,我们在统一异常处理中并不能获取到完整的参数列表。另外,有些时候我们用的框架可能没有包含参数校验的功能,例如一些 RPC 框架。

这种情况下,我们可以通过 SpringAOP + hibernate-validator 来自己实现参数校验的功能。

加入 maven 依赖


<dependency>
  <groupId>org.hibernate.validatorgroupId>
    <artifactId>hibernate-validatorartifactId>
    <version>6.0.17.Finalversion>
dependency>
<dependency>
    <groupId>org.glassfishgroupId>
    <artifactId>javax.elartifactId>
    <version>3.0.1-b08version>
dependency>

<dependency>
    <groupId>org.aspectjgroupId>
    <artifactId>aspectjrtartifactId>
    <version>1.9.1version>
dependency>
<dependency>
    <groupId>org.aspectjgroupId>
    <artifactId>aspectjweaverartifactId>
    <version>1.9.1version>
dependency>
<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-aopartifactId>
    <version>5.1.8.RELEASEversion>
dependency>

编译插件

在 pom.xml 文件中添加编译插件,开启 true,这样在校验不通过时,可以从结果中获取到不通过参数名称,具体参考:《深度分析如何获取方法参数名》

<build>
	<plugins>
	     <plugin>
	         <groupId>org.apache.maven.pluginsgroupId>
	         <artifactId>maven-compiler-pluginartifactId>
	         <version>3.8.0version>
	         <configuration>
	             <source>1.8source>
	             <target>1.8target>
	             <parameters>trueparameters>
	             <encoding>UTF-8encoding>
	         configuration>
	     plugin>
	plugins>
build>

开启 AOP 功能

直接在带有 Configuration 注解的类上添加 EnableAspectJAutoProxy 注解

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

实现校验功能

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.hibernate.validator.HibernateValidator;
import org.hibernate.validator.internal.engine.DefaultParameterNameProvider;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.annotation.PostConstruct;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 使用AOP做统一参数校验
 *
 * @author huangxuyang
 * @since 2019-09-28
 */
@Aspect
@Component
public class ValidationAspect {
    private static Set<Class<?>> SIMPLE_TYPES = new HashSet<>(Arrays.asList(
            Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class,
            Boolean.class, String.class, Enum.class, Date.class, Void.class, LocalDateTime.class, LocalDate.class));
    private ExecutableValidator executableValidator;
    private Validator validator;

    @PostConstruct
    public void init() {
        // 这里初始化校验器,可以对校验器进行一些定制
        validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
                .ignoreXmlConfiguration()
                .parameterNameProvider(new DefaultParameterNameProvider())
                .buildValidatorFactory()
                .getValidator();
        // 这个 validator 可以直接校验参数,
        executableValidator = validator.forExecutables();
    }

    /**
     * 定义需要拦截的切面 —— 方法或类上包含 {@link Validated} 注解
     */
    @Pointcut("@target(org.springframework.validation.annotation.Validated)" +
            " ||  @within(org.springframework.validation.annotation.Validated)")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void before(JoinPoint point) {
        if (point.getArgs() == null || point.getArgs().length <= 0) {
            return;
        }
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        // 执行普通参数校验,获得校验结果
        Set<ConstraintViolation<Object>> validResult = executableValidator.validateParameters(point.getTarget(), method, point.getArgs());
        if (validResult == null || validResult.isEmpty()) {
            // 找出所有带 @Validated 注解的参数
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Annotation[] annotation = parameterAnnotations[i];
                for (Annotation ann : annotation) {
                    if (ann.annotationType().equals(Validated.class)) {
                        validResult = validateBean(point.getArgs()[i]);
                    }
                }
            }
        }
        // 如果有校验不通过的,直接抛出参数校验不通过的异常
        if (validResult != null && !validResult.isEmpty()) {
            String msg = validResult.stream()
                    .map(cv -> {
                        // 如果是普通类型带上具体校验不通过的值
                        if (isSimpleType(cv.getInvalidValue())) {
                            return cv.getPropertyPath() + " " + cv.getMessage() + ": " + cv.getInvalidValue();
                        } else {
                            return cv.getPropertyPath() + " " + cv.getMessage();
                        }
                    }).collect(Collectors.joining(", "));
            // 若校验不通过,则直接抛出异常,若有必要,可以通过 point.getArgs() 获取并记录完整的参数列表
            throw new IllegalArgumentException(msg);
        }
    }

    /**
     * 校验参数,支持数据和集合
     */
    private Set<ConstraintViolation<Object>> validateBean(Object arg) {
        Class<?> clz = arg.getClass();
        if (clz.isArray()) {
            // 遍历数组
            int length = Array.getLength(clz);
            for (int i = 0; i < length; i++) {
                Object obj = Array.get(arg, i);
                Set<ConstraintViolation<Object>> tmp = validator.validate(obj);
                if (tmp != null && !tmp.isEmpty()) {
                    return tmp;
                }
            }
        } else if (arg instanceof Collection) {
            // 遍历集合
            Collection collection = (Collection) arg;
            for (Object item : collection) {
                Set<ConstraintViolation<Object>> tmp = validator.validate(item);
                if (tmp != null && !tmp.isEmpty()) {
                    return tmp;
                }
            }
        } else {
            return validator.validate(arg);
        }
        return Collections.emptySet();
    }

    /**
     * 是否为普通类型,即原始数据类型及其包装类、字符串、日期、枚举等
     */
    private static boolean isSimpleType(Object obj) {
        if (obj == null) {
            return true;
        }
        Class<?> clazz = obj.getClass();
        if (clazz.isPrimitive()) {
            return true;
        }
        return SIMPLE_TYPES.contains(clazz);
    }
}

这个实现几乎考虑到了所有的校验场景,支持普通校验也支持自定义类型的参数、数组和集合类型。在需要校验参数的类或方法上添加 @Validated 注解,就可以通过切面进行校验。如果是复杂对象参数,则需要在参数上也添加 @Validated 注解。

举个例子

@Data
public class SystemUser {
	  @NotBlank
	  private String username;
	  @Min(0)
	  @Max(150)
	  private int age;

接口

public interface ISystemUserService {
	/**
	 * 校验普通参数,直接添加校验注解即可
	 */
    SystemUser getUserById(@Positive long id) throws Exception;
    
    /**
     * 复杂参数,可以当成普通参数进行非空校验,然后添加 @Validated 注解声明要进行字段校验
     */
    long insertUser(@NotNull @Validated SystemUser user) throws Exception;

	/**
	 * 对集合也可以做基本校验,然后添加 @Validated 注解声明对集合中的每个对象进行字段校验
	 */
    List<Long> insertUsers(@NotNull @Size(min = 1, max = 200) @Validated List<SystemUser> users) throws Exception;
}

实现类,要添加 @Validated 注解让校验切面对这个类中的所有方法都生效。接口和实现方法上的参数注解必须一致,否则可能会出错或校验不生效。

@Validated
public class SystemUserServiceImpl implements ISystemUserService {
    private final SystemUserMapper systemUserMapper;

    @Override
    public SystemUser getUserById(@Positive long id) {
        // 这里实现查询逻辑
    }

    @Override
    public long insertUser(@NotNull @Validated SystemUser user) {
        // 这里实现插入逻辑
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> insertUsers(@NotNull @Size(min = 1, max = 200) @Validated List<SystemUser> users) {
        // 这里实现插入逻辑
    }
}

写个测试用例看下:
使用 SpringAOP + hibernate-validator 完美实现自动参数校验_第1张图片
可以看到,切面生效了,我们对方法进行了统一的参数校验!

你可能感兴趣的:(Java,AOP实战)