背景
当我们编写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;
}
}