podam mock 对象部分字段没有赋值问题

简介: 本文主要分析使用 podam mock 对象时,部分字段无法自动赋值的原因,并给出解决方案。

背景

编写单元测试的时候,经常会需要 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<?> attributeStrategyBeanValidationStrategy ,再查看 BeanValidationStrategygetValue 方法,我们终于找到了答案:

    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 @Maxjavax.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);
相关文章
|
对象存储 开发者
对象OSS生命周期(LifeCycle)管理功能|学习笔记
快速学习对象 OSS 生命周期(LifeCycle)管理功能
3079 0
对象OSS生命周期(LifeCycle)管理功能|学习笔记
|
Cloud Native Java 应用服务中间件
Docker容器实战【三】搭建Docker镜像私服Harbor
每个企业都有自己的镜像私服仓库,和nexus一样,公司内部的镜像制品都存放在自己的私服仓库中,今天我们来学习Harbor
1267 1
Docker容器实战【三】搭建Docker镜像私服Harbor
|
8月前
|
API 数据处理 开发者
获取淘宝分类详情:深入解析taobao.cat_get API接口
淘宝开放平台推出的`taobao.cat_get` API接口,帮助开发者和商家获取淘宝、天猫的商品分类详情。该接口支持获取类目列表、属性及父类目信息,通过指定分类ID(cid)实现精准查询,并提供灵活的参数设置和高效的数据处理。使用流程包括注册账号、创建应用、获取App Key/Secret、构造请求、发送并解析响应。示例代码展示了如何用Python调用此API。开发者可借此为电商项目提供数据支持。
|
8月前
|
存储 缓存 负载均衡
如何设计一个注册中心?以Zookeeper为例
本文介绍了分布式系统中注册中心的设计与工作原理,重点讲解了Zookeeper作为注册中心的实现。注册中心需具备服务注册、注销、心跳检测、服务查询等功能,确保高可用性。Zookeeper通过层次命名空间和znode存储数据,支持服务注册与发现,并采用发布-订阅模式通知消费者服务变更。然而,Zookeeper存在选举期间不可用的问题,不太适合作为注册中心,因其CP模型优先保证一致性而非可用性。
466 78
|
10月前
|
供应链 安全 数据挖掘
深度剖析区块链技术在金融科技领域的创新应用与挑战####
本文旨在探讨区块链技术于金融科技(FinTech)领域的革新性应用,分析其如何重塑传统金融服务模式,并深入剖析面临的技术与监管挑战。通过案例研究与数据分析,揭示区块链在提升金融效率、增强安全性及促进金融包容性方面的潜力,同时强调构建健全的法律法规框架与技术创新之间的平衡对于推动行业健康发展的重要性。本文不涉及具体代码实现或技术细节,而是聚焦于区块链应用的战略意义与实践挑战。 ####
|
10月前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
361 1
|
缓存 NoSQL Java
分布式系列教程(01) -Ehcache缓存架构
分布式系列教程(01) -Ehcache缓存架构
574 0
@RequiredArgsConstructor(onConstructor=@_(@Autowired))是什么语法?
@RequiredArgsConstructor(onConstructor=@_(@Autowired))是什么语法?
444 0
|
分布式计算 DataWorks MaxCompute
DataWorks产品使用合集之在DataWorks中,如何进行批量复制操作来将一个业务流程复制到另一个业务流程
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
183 0
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的智慧校园管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的智慧校园管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
189 0