+关注继续查看

背景

编写单元测试的时候,经常会需要 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);
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
【Kotlin】属性 与 幕后字段 ( 属性声明 | 属性初始化器 | 属性访问器 | field 属性幕后字段 | lateinit 延迟初始化属性 )
【Kotlin】属性 与 幕后字段 ( 属性声明 | 属性初始化器 | 属性访问器 | field 属性幕后字段 | lateinit 延迟初始化属性 )
一个工具类搞定 CRUD 的创建人、修改人、时间等字段赋值!
数据库设计过程中,我们往往会给数据库表添加一些通用字段,比如创建人、创建时间、修改人、修改时间,在一些公司的设计过程中有时会强制要求每个表都要包含这些基础信息,以便记录数据操作时的一些基本日志记录。