背景
编写单元测试的时候,经常会需要 mock 一些测试用的对象。我们采用 podam 来负责 mock 对象。按照官方文档,mock 对象非常简单:
// Simplest scenario. Will delegate to Podam all decisions
// 最简单的场景,会提供一个默认实现
PodamFactory factory = new PodamFactoryImpl();
// This will use constructor with minimum arguments and
// then setters to populate POJO
// 这个方法会使用最少参数的构造函数,然后使用 setter 进行填充
Pojo myPojo = factory.manufacturePojo(Pojo.class);
但是在使用过程中,笔者却发现生成的对象缺少了部分字段。下面是一段简单的示例代码:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp = "^1[3456789]\\d{9}$", message = "手机号格式错误")
@Constraint(validatedBy = {})
public @interface PhoneNumber {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Data
public static class UserDTO {
@PhoneNumber
private String phone;
@Max(4)
@Min(1)
private Integer age;
@Length(min = 2, max = 40)
private String name;
@Email
private String email;
}
我们需要 mock 这个类用来测试代码。根据官方文档,很容易可以写出以下代码:
@Test
public void testCustomAnnotation() {
UserDTO userDTO = podamFactory.manufacturePojo(UserDTO.class);
// 简单看一下效果
System.out.println("userDTO : " + JSON.toJSONString(userDTO);
}
结果控制台的输出是:
userDTO : {"age":1}
也就是说有部分字段没有注入。
当把 debug 日志打开之后,看到了更多的内容:
20:16:46.798 [main] WARN uk.co.jemos.podam.typeManufacturers.TypeManufacturerUtil - Please, register AttributeStratergy for custom constraint @com.xxx.xxx.xxx.annotation.PhoneNumber(message=, groups=[], payload=[]), in DataProviderStrategy! Value will be left to null
看来问题出现在自定义的字段校验的 @PhoneNumber
这里。
源码分析
那么为什么 @PhoneNumber
会影响字段的填充呢 ?来看源码。
首先一进来就是这个方法。很明显前两行是准备 ManufacturingContext
而已,没有实质性操作:
@Override
public <T> T manufacturePojo(Class<T> pojoClass, Type... genericTypeArgs) {
// 环境准备
ManufacturingContext manufacturingCtx = new ManufacturingContext();
manufacturingCtx.getPojos().put(pojoClass, 1);
// 真正的 mock 方法
return doManufacturePojo(pojoClass, manufacturingCtx, genericTypeArgs);
}
继续跟踪,依旧是准备工作:
private <T> T doManufacturePojo(Class<T> pojoClass,
ManufacturingContext manufacturingCtx, Type... genericTypeArgs) {
try {
Class<?> declaringClass = null;
Object declaringInstance = null;
AttributeMetadata pojoMetadata = new AttributeMetadata(pojoClass,
pojoClass, genericTypeArgs, declaringClass, declaringInstance);
// 这里是实际的处理方法
return this.manufacturePojoInternal(pojoClass, pojoMetadata,
manufacturingCtx, genericTypeArgs);
} catch (InstantiationException e) {
// 此处省略异常处理逻辑
}
}
进入 manufacturePojoInternal
方法:这里主要是根据不同的情况,采用不同的方法获取这个对象:如果从缓存获取,就直接返回,否则创建对象,并进行初始化工作。
private <T> T manufacturePojoInternal(Class<T> pojoClass,
AttributeMetadata pojoMetadata, ManufacturingContext manufacturingCtx,
Type... genericTypeArgs)
throws InstantiationException, IllegalAccessException,
InvocationTargetException, ClassNotFoundException {
// reuse object from memoization table
// 先从缓存中查找已经 mock 过的实例
@SuppressWarnings("unchecked")
T objectToReuse = (T) strategy.getMemoizedObject(pojoMetadata);
if (objectToReuse != null) {
LOG.debug("Fetched memoized object for {} with parameters {}",
pojoClass, Arrays.toString(genericTypeArgs));
return objectToReuse;
} else {
LOG.debug("Manufacturing {} with parameters {}",
pojoClass, Arrays.toString(genericTypeArgs));
}
// 找不到已有的实例,继续往下走
final Map<String, Type> typeArgsMap = new HashMap<String, Type>();
Type[] genericTypeArgsExtra = TypeManufacturerUtil.fillTypeArgMap(typeArgsMap,
pojoClass, genericTypeArgs);
T retValue = (T) strategy.getTypeValue(pojoMetadata, typeArgsMap, pojoClass);
if (null == retValue) {
if (pojoClass.isInterface()) {
return getValueForAbstractType(pojoClass, pojoMetadata,
manufacturingCtx, typeArgsMap, genericTypeArgs);
}
try {
// 尝试创建这个对象
retValue = instantiatePojo(pojoClass, manufacturingCtx, typeArgsMap,
genericTypeArgsExtra);
} catch (SecurityException e) {
throw new PodamMockeryException(
"Security exception while applying introspection.", e);
}
}
if (retValue == null) {
return getValueForAbstractType(pojoClass, pojoMetadata,
manufacturingCtx, typeArgsMap, genericTypeArgs);
} else {
// update memoization cache with new object
// the reference is stored before properties are set so that recursive
// properties can use it
strategy.cacheMemoizedObject(pojoMetadata, retValue);
List<Annotation> annotations = null;
// 对象创建成功,但是还没给字段赋值,这里开始赋值
populatePojoInternal(retValue, annotations, manufacturingCtx,
typeArgsMap, genericTypeArgsExtra);
}
return retValue;
}
在 populatePojoInternal
方法中,根据不同的具体类型,调用不同的是字段赋值方法:
private <T> T populatePojoInternal(T pojo, List<Annotation> annotations,
ManufacturingContext manufacturingCtx,
Map<String, Type> typeArgsMap,
Type... genericTypeArgs)
throws InstantiationException, IllegalAccessException,
InvocationTargetException, ClassNotFoundException {
LOG.debug("Populating pojo {}", pojo.getClass());
// 先判断要填充的对象的类型,对数组,Collection,Map 使用不同方法进行填充。此处省略
Class<?> pojoClass = pojo.getClass();
if (pojoClass.isArray()) {
...
} else if (pojo instanceof Collection) {
...
} else if (pojo instanceof Map) {
...
}
// 如果是 数组,Collection,或者Map类型,先填充 数组,Collection 或者 Map 的公共字段
// 下面补充剩余的内容
ClassInfo classInfo = classInfoStrategy.getClassInfo(pojo.getClass());
Set<ClassAttribute> classAttributes = classInfo.getClassAttributes();
// attribute 就是类拥有的字段,普通的 pojo 类的填充从这里开始
for (ClassAttribute attribute : classAttributes) {
// 填充普通的可读写字段。 ClassAttribute 中包含了字段的 getter 和 setter 方法列表。这里就是我们要找的方法了
if (!populateReadWriteField(pojo, attribute, typeArgsMap, manufacturingCtx)) {
populateReadOnlyField(pojo, attribute, typeArgsMap, manufacturingCtx, genericTypeArgs);
}
}
// It executes any extra methods
// 执行其他方法(应该是类似 @PostConstruct 之类的)
Collection<Method> extraMethods = classInfoStrategy.getExtraMethods(pojoClass);
if (null != extraMethods) {
for (Method extraMethod : extraMethods) {
Object[] args = getParameterValuesForMethod(extraMethod, pojoClass,
manufacturingCtx, typeArgsMap, genericTypeArgs);
extraMethod.invoke(pojo, args);
}
}
return pojo;
}
在populateReadWriteField
中对字段进行赋值:
private <T> boolean populateReadWriteField(T pojo, ClassAttribute attribute,
Map<String, Type> typeArgsMap, ManufacturingContext manufacturingCtx)
throws InstantiationException, IllegalAccessException,
InvocationTargetException, ClassNotFoundException {
Method setter = PodamUtils.selectLatestMethod(attribute.getSetters());
if (setter == null) {
return false;
}
// 此处省略对 setter 的校验代码
...
// 获取字段上的注解
List<Annotation> pojoAttributeAnnotations
= PodamUtils.getAttributeAnnotations(
attribute.getAttribute(), setter);
// 根据注解获取字段填充策略
// 这里面是判断注解类型的方法,比较简单,就不详细分析了
AttributeStrategy<?> attributeStrategy
= TypeManufacturerUtil.findAttributeStrategy(strategy, pojoAttributeAnnotations, attributeType);
Object setterArg = null;
if (null != attributeStrategy) {
setterArg = TypeManufacturerUtil.returnAttributeDataStrategyValue(
attributeType, pojoAttributeAnnotations, attributeStrategy);
} else {
// 此处省略没有获取到策略时的生成逻辑
...
}
// 这里调用字段的 setter,把字段填充策略生成的值赋值给字段
try {
setter.invoke(pojo, setterArg);
} catch(IllegalAccessException e) {
LOG.warn("{} is not accessible. Setting it to accessible."
+ " However this is a security hack and your code"
+ " should really adhere to JavaBeans standards.",
setter.toString());
setter.setAccessible(true);
setter.invoke(pojo, setterArg);
}
return true;
}
打断点可以看到获取到的 AttributeStrategy<?> attributeStrategy
是 BeanValidationStrategy
,再查看 BeanValidationStrategy
的 getValue
方法,我们终于找到了答案:
public Object getValue(Class<?> attrType, List<Annotation> annotations) throws PodamMockeryException {
// 省略无关逻辑,主要是判断是否是 @Min @Max 之类的注解,并且根据注解返回对应的符合要求的值
...
Pattern pattern = findTypeFromList(annotations, Pattern.class);
if (null != pattern) {
LOG.warn("At the moment PODAM doesn't support @Pattern({}),"
+ " returning null", pattern.regexp());
return null;
}
// 此处省略无关逻辑,主要是判断是否是 @Min @Max 之类的注解,并且根据注解返回对应的符合要求的值
...
return null;
}
到这里我们找到答案:加了 @Pattern
注解的字段,在 getValue 的时候,获取的是 null
。
总结
根据官方文档, podam 会对 @Min
@Max
等 javax.validation.constraints
中的部分注解进行处理,能够支持直接生成符合要求的值。但是对于 @Pattern
、或者包含 @Pattern
的其他自定义注解,以及其他注解(比如 org.hibernate.validator.constraints
中的 @Length
)则无法处理,对应的注解类需要提供自定义的 AttributeStrategy
。推测是因为 @Pattern
之类的比较复杂校验,比较难以生成对应的符合要求的字符串。
对于这种 @Pattern
或者自定义的校验,要编写一个对应的数值生成方法不是一个简单的事情。几经搜索,没有找到相关的案例。
最终根据官方文档提供的解决方案,需要给 podamFactory
提供一个对应的 AttributeStrategy
:
protected PodamFactory podamFactory = new PodamFactoryImpl();
if (podamFactory.getStrategy() instanceof RandomDataProviderStrategy) {
// 根据实际情况提供一个随机字符串策略,这里只是简单举个例子
AttributeStrategy<?> phoneNumberStrategy = (attrType, attrAnnotations) -> "13244443322";
// 针对 @PhoneNumber 添加一个 AttributeStrategy
randomDataProviderStrategy.addOrReplaceAttributeStrategy(PhoneNumber.class, phoneNumberStrategy);
// 其他的注解也按照这种方式添加即可
}
UserDTO userDTO = factory.manufacturePojo(UserDTO.class);