Zacard's Notes

dubbo service单元测试参数校验的问题

背景

当我们编写dubbo service端的单元测试的时候,并且设置了dubbo的validation是在客户端(服务消费端)校验的话,那么测试类中的基于注解的参数校验将不会生效。

解决办法

最简单的办法就是直接编写一个aop类,在方法调用前先做参数校验,代码如下:

/**
 * 参数校验aop切面
 * 在dubbo service test类中,使参数校验生效
 *
 * @author zacard
 * @since 2016-01-29 13:56
 */
@Component
@Aspect
public class ValidatorAspect {

    private static final Logger logger = LoggerFactory.getLogger(ValidatorAspect.class);

    /**
     * 参数校验类
     */
    private static Validator validator;

    /**
     * 内部类连接符
     */
    public static final String INNERCLASSSTR = "$";


    // 定义切入点:dubbo service
    @Pointcut(value = "execution(public * com.zacard.*.apis.*.*(..))")
    private void dubboServicePointcut() {
    }

     // 前置aop
    @Before(value = "dubboServicePointcut()")
    public void beforeAdvice(JoinPoint pj) throws Throwable {
        // 1.校验方法入参
        validateMethodParams(pj);
        // 2.校验入参内的属性
        validateParamsProperty(pj);
    }


    /**
     * 校验方法入参
     *
     * @param pj aop切面入参
     */
    private void validateMethodParams(JoinPoint pj) throws Exception {
        MethodSignature signature = (MethodSignature) pj.getSignature();
        // 代理的方法
        Method method = signature.getMethod();
        // 是否需要校验方法入参
        if (isNeedValidateMethod(method)) {
            // 代理类
            Class aopClass = pj.getTarget().getClass();
            // 方法参数列表
            Object[] parms = pj.getArgs();
            Set<ConstraintViolation<Object>> violations = thegetValidateResultForMethod(aopClass.newInstance(), method, parms);
            if (!CollectionUtils.isEmpty(violations)) {
                throw new ValidateException("TEST_ERROR_001", "参数校验未通过=>{" + getErrorMsg(violations) + "}");
            }
        }
    }

    /**
     * 校验入参属性
     *
     * @param pj aop切面入参
     */
    private void validateParamsProperty(JoinPoint pj) {
        // 方法参数列表
        Object[] params = pj.getArgs();
        if (params == null || params.length < 1) {
            return;
        }
        // 分组校验注解类
        Class methodAnnotation = getMethodGroupAnnotation(pj.getTarget().getClass(), pj.getSignature().getName());
        for (Object param : params) {
            if (param == null) {
                continue;
            }
            Set<ConstraintViolation<Object>> violations = getValidateResultForParam(param, methodAnnotation);
            if (!CollectionUtils.isEmpty(violations)) {
                throw new ValidateException("TEST_ERROR_001", "参数校验未通过=>{" + getErrorMsg(violations) + "}");
            }
        }
    }

    /**
     * 判断是否需要先验证方法中的入参
     *
     * @param method aop方法类
     */
    private boolean isNeedValidateMethod(Method method) {
        for (Annotation[] annotations : method.getParameterAnnotations()) {
            if (annotations.length > 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取aop方法对应的分组校验注解类
     *
     * @param aopClass   aop类
     * @param methodName aop方法名称
     * @return 分组校验注解类
     */
    private Class getMethodGroupAnnotation(Class aopClass, String methodName) {
        Class[] interfaces = aopClass.getInterfaces();
        if (interfaces.length > 0) {
            String interfaceName = interfaces[0].getName();
            if (logger.isInfoEnabled()) {
                logger.info("aop method=>{" + aopClass.getName() + "." + methodName + "()}");
            }
            // 判断是否有对应方法的分组校验注解
            try {
                String annotationName = interfaceName + INNERCLASSSTR + getDubboTypeAnnonName(methodName);
                return Class.forName(annotationName);
            } catch (ClassNotFoundException e) {
                // 不存在这个注解
            }
        }
        return null;
    }

    /**
     * 获取dubbo验证规则下的分组校验的注解名称
     *
     * @param methodName aop的方法名称
     * @return 注解名称
     */
    private String getDubboTypeAnnonName(String methodName) {
        return methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
    }

    /**
     * 校验方法中的入参,并返回验证结果
     *
     * @param classInstance 方法所在类的实例
     * @param method        方法类
     * @param params        方法值数组
     * @return 校验结果
     */
    private Set<ConstraintViolation<Object>> getValidateResultForMethod(Object classInstance, Method method, Object[] params) {
        return getValidator().forExecutables().validateParameters(classInstance, method, params);
    }

    /**
     * 校验入参
     *
     * @param param            入参
     * @param methodAnnotation 分组注解类
     * @return 校验结果
     */
    private Set<ConstraintViolation<Object>> getValidateResultForParam(Object param, Class methodAnnotation) {
        if (methodAnnotation == null) {
            return getValidator().validate(param);
        }
        return getValidator().validate(param, methodAnnotation);
    }

    /**
     * 从校验结果中格式化出错误信息
     *
     * @param violations 参数校验结果
     * @return 错误信息集合, 格式:[a:reason]
     */
    private List<String> getErrorMsg(Set<ConstraintViolation<Object>> violations) {
        List<String> result = new ArrayList<>();
        if (violations == null || violations.isEmpty()) {
            return result;
        }
        return violations.stream()
                .map(violation -> violation.getPropertyPath() + ":" + violation.getMessage())
                .collect(Collectors.toList());
    }

    /**
     * 获取validator
     *
     * @return 参数校验类
     */
    private Validator getValidator() {
        if (validator == null) {
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            validator = factory.getValidator();
        }
        return validator;
    }

xml需要开启aop注解支持:

 <!--开启aop注解支持-->
<aop:aspectj-autoproxy />

同时需要扫描ValidatorAspect类所在的包,例如:

<context:component-scan base-package="com.zacard.core.test" />

依赖的jar包,pom.xml配置:

<!--AspectJ-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.6.10</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.6.10</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
    <scope>test</scope>
</dependency>

需要注意的一个地方

如果在单元测试类中,同时使用了Mock一类的包(例如:mockito),可能会使Mock失效。

失效原因

由于测试类是被aop代理的类,使用mock注入的一些bean或者属性会注入到代理类中,所以会失败.

解决办法

编写一个工具类,将mock对象注入到真实的类中,代码如下:

/**
 * Mock注入工具类
 *
 * @author zacard
 * @since 2016-01-29 14:26
 */
public class MockWithAopUtils {

    private static final Logger logger = LoggerFactory.getLogger(MockWithAopUtils.class);

    /**
     * 注入mock对象到被aop代理的原bean中
     *
     * @param target       真实的bean
     * @param propertyName 被mock属性名称
     * @param mock         mock对象
     */
    public static void setMocks(Object target, String propertyName, Object mock) {
        // 1.获取被aop代理的原始bean
        Object realBean = target;
        try {
            realBean = unwrapProxy(target);
        } catch (Exception e) {
            logger.error("获取被aop代理的原始bean失败!", e);
        }
        // 2.注入mock对象
        ReflectionTestUtils.setField(realBean, propertyName, mock);
    }


    /**
     * 获取真实的被代理类
     *
     * @param bean 代理类
     * @return 真实bean
     * @throws Exception
     */
    private static Object unwrapProxy(Object bean) throws Exception {
        if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
            Advised advised = (Advised) bean;
            bean = advised.getTargetSource().getTarget();
        }
        return bean;
    }
}
坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章