设计一个POJO生成器-第1次迭代

设计一个POJO生成器-第1次迭代

实施增量开发过程,开发一个POJO生成器

需求

能够为一个只有常用类型成员的类型生成实例,用户可以提供一个字符串数组告诉生成器忽略指定字段

常用类型包括:

  • java.math.BigDecimal
  • java.util.Date
  • java.time.ZonedDateTime
  • Boolean
  • String
  • Double
  • Integer
  • Long

设计

提供一个接口A,将要生成的类型的Class作为参数传递给A,A首先创建该类型实例,然后通过反射遍历该Class,过滤非用户定义的成员、非基本类型成员及用户要求忽略的字段,为其他成员生成随机值,最后返回实例

字段过滤条件:

  • static或final修饰的字段:static修饰的变量为类所有实例共享,为某一个实例生成该字段的随机值没有意义;其次像实现了Serializable接口的类型,一般会有serialVersionUID这个static和final修饰的字段,像这样的字段也应该被过滤掉
  • 用户要求忽略
  • 编译器生成字段:非静态内部类会持有一个其外部类实例的引用,这个引用实际上会被存放在内部类中编译器生成的字段里,比较典型的如使用jacoco时,jacoco为统计测试覆盖率会修改class文件进行插桩,包括了在类中插入特殊的成员变量。可以通过Field.isSynthetic方法判断字段是否是这样的字段

该过程中需要针对不同类型的字段生成不同类型的值,有以下可选方案:

  • 使用if-else或者switch-case语句决定为字段生成什么类型的值
  • 针对每种类型实现一个生成器,以类型的Class为键,生成器作值构成一个map

方案三肯定是最优的,相比分支语句,每种类型一个生成器非常方便以后扩展类型,因为使用分支语句可能需要维护一个巨大臃肿的代码块,不利于维护;其次使用map组织生成器,查询性能优秀

编码

添加基本依赖:

    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>28.0-jre</version>
      <scope>compile</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.9</version>
    </dependency>

    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.2</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.5</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>1.0.11</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>

      <artifactId>logback-classic</artifactId>
      <version>1.0.11</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.20</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

创建自定义异常类型:

package org.luncert.objectmocker.exception;

public class GeneratorException extends RuntimeException {

  private static final long serialVersionUID = 2598986311953699268L;

  public GeneratorException() {
  }

  public GeneratorException(String message) {
    super(message);
  }

  public GeneratorException(Throwable cause) {
    super(cause);
  }

  public GeneratorException(String message, Throwable cause) {
    super(message, cause);
  }

}

定义字段生成器接口:

package org.luncert.objectmocker.core;

import org.luncert.objectmocker.exception.GeneratorException;

public abstract class AbstractGenerator<T> {

  public abstract T generate(Class<?> clazz) throws GeneratorException;
}

为需求指定的常用字段类型实现生成器(这里仅用BigDecimalGenerator作为示例):

package org.luncert.objectmocker.builtinGenerator;

import org.apache.commons.lang3.RandomUtils;
import org.luncert.objectmocker.core.AbstractGenerator;
import org.luncert.objectmocker.exception.GeneratorException;

import java.math.BigDecimal;

public class BigDecimalGenerator extends AbstractGenerator<BigDecimal> {

  @Override
  public BigDecimal generate(Class<?> clazz) throws GeneratorException {
    return BigDecimal.valueOf(RandomUtils.nextDouble());
  }
}

实现核心逻辑:

package org.luncert.objectmocker.core;

import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.luncert.objectmocker.builtinGenerator.*;
import org.luncert.objectmocker.exception.GeneratorException;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.*;

@Slf4j
public class ObjectGenerator {

  // 构建生成器字典
  private static final Map<Class, AbstractGenerator> BUILTIN_GENERATORS = ImmutableMap
      .<Class, AbstractGenerator>builder()
      .put(String.class, new StringGenerator())
      .put(ZonedDateTime.class, new ZonedDateTimeGenerator())
      .put(Date.class, new DateGenerator())
      .put(Boolean.class, new BooleanGenerator())
      .put(boolean.class, new BooleanGenerator())
      .put(Integer.class, new IntegerGenerator())
      .put(int.class, new IntegerGenerator())
      .put(Long.class, new LongGenerator())
      .put(long.class, new LongGenerator())
      .put(Double.class, new DoubleGenerator())
      .put(double.class, new DoubleGenerator())
      .put(BigDecimal.class, new BigDecimalGenerator())
      .build();

  @SuppressWarnings("unchecked")
  public static <T> T generate(Class<T> clazz, String ...tmpIgnores) {
    String className = clazz.getSimpleName();

    // try create new instance for target class
    Object target;
    try {
      target = clazz.getConstructor().newInstance();
    } catch (Exception e) {
      throw new GeneratorException("Failed to create a new instance of target class " +
          className + ".");
    }

    // 将tmpIgnores从数组转为集合,提高查询性能
    // 如果tmpIgnores为空数组,则直接赋值Collections.EMPTY_SET,EMPTY_SET相较于空的HashSet性能更好(要好一丢丢),因为EMPTY_SET.contains方法直接返回false,如果使用HashSet还要将key进行hash然后作查询
    Set<String> tmpIgnoreSet = tmpIgnores.length == 0 ?
        Collections.EMPTY_SET : new HashSet<>(Arrays.asList(tmpIgnores));

    try {
      // 遍历成员变量,使用getDeclaredFields才可以获取到私有变量,getFields不行
      for (Field field : clazz.getDeclaredFields()) {
        String fieldName = field.getName();
        Class<?> fieldType = field.getType();

        // 过滤static或final修饰的或synthetic字段
        int modifiers = field.getModifiers();
        if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers) || field.isSynthetic()) {
          log.debug("{}.{} - Field has been skipped because it has static or final modifier, or generated by compiler.",
              className, fieldName);
          continue;
        }

        // 过滤用户要求忽略的字段
        if (tmpIgnoreSet.contains(fieldName)) {
          continue;
        }

        // 对于私有变量,必须先设置其为可访问的,然后才能进行设值
        field.setAccessible(true);

        // 查询生成器,生成并设置值
        AbstractGenerator generator = BUILTIN_GENERATORS.get(fieldType);
        if (generator != null) {
          // generate field value using built-in generator
          field.set(target, generator.generate(fieldType));
        } else {
          throw new GeneratorException("Couldn't find a appropriate generator for type " + fieldType);
        }
      }
    } catch (IllegalAccessException e) {
      throw new GeneratorException(
          "Failed to set field value for instance of class " + className + ".", e);
    }
    return clazz.cast(target);
  }

}

测试

首先编写成功的case,因为是随机生成值,无法使用断言判断测试是否成功,这里只能用人眼判断了,这个test case主要针对以下几点进行测试:

  • 测试是否任意可见性的字段都能被处理到
  • 测试所有生成器能否正常工作
  • 测试用户指定忽略字段是否起作用
package org.luncert.objectmocker.core;

import lombok.Data;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Date;

@RunWith(JUnit4.class)
public class ObjectGeneratorTest {

  @Data
  private static class TestClass {
    protected BigDecimal bigDecimalField;
    public boolean booleanField;
    private Date dateField;
    private double doubleField;
    private int integerField;
    private long longField;
    private String stringField;
    private ZonedDateTime zonedDateTimeField;
    private String shouldBeIgnored;
  }

  @Test
  public void successCase() {
    TestClass ins = ObjectGenerator.generate(TestClass.class, "shouldBeIgnored");
    System.out.println(ins);
  }
}

测试输出:

在这里插入图片描述

测试通过

编写反例进行测试,测试目标:

  • 测试当目标类型包含不支持类型的字段时,是否抛出正确的异常
  @Data
  private static class ComplexClass {
    private TestClass customTypeField;
  }

  @Test
  public void nonSupportedType() {
    try {
      ObjectGenerator.generate(ComplexClass.class, "shouldBeIgnored");
      Assert.fail("Catch no exception");
    } catch (GeneratorException e) {
      Assert.assertNull("Incorrect exception type", e.getCause());
    }
  }

测试通过

POJOGenerator(POJO代码生成器 v1.3.3) 本POJO代码生成器采用Java的Swing技术编码实现,是绿色免费工具,可以自由传播。 由于本工具的内部实现较烂,所以还请反编译高手手下留情,让我留几分颜面。^_^ 由于本人只用过Oracle、DB2、MySQL、MS SQL Server这四款数据库产品,所以制作 成exe可执行文件时只添入了这四款数据库的驱动支持。如果您需要使用这款工具从 其它数据库中生成POJO,那么您可以联系我(Email:CodingMouse@gmail.com), 我会添加其它数据库的驱动支持后通过电子邮件发送给您。 简单的使用说明: 1、先将压缩档解压到任意文件夹,必须保留配置文件cmsdk4j.cfg.xml和generator .cfg.xml与可执行文件POJOGenerator.exe在同一目录,否则无法运行。 2、可以预先在配置档cmsdk4j.cfg.xml中设定您的数据库服务器配置,配置档中已经 提供了默认的配置信息,您仅需在此基础上修改部分参数(如:IP地址、端口号、 用户名、密码、数据库名等),这些参数将作为生成器的预设数据库连接配置参数。 3、可以预先在配置档generator.cfg.xml中设定您的数据类型映射方案,配置档中已经 提供了MS SQL Server/MySQL/DB2和Oracle两种映射方案,当然,可能有不太完整的地方 ,您可以根据实际情况稍作修改即可。需要注意的一点是ref属性表示引用同一映射方案 的另一映射,这样您便可以简化同一映射数据类型的配置;而import属性是指定需要在 最终生成的源代码中作为类最开始的package类型导入声明部分的导入类型名称,因此, 这个名称是完整带包名的类名称,否则不能正确生成最终代码。配置档中提供的默认配 置如果不能满足你的需要,也可以自行根据实际情况进行修改。最后,需要大家注意的 一点就是由于最终生成的代码要调用包装类型的equals和hashCode方法,因此,配置的 数据类型必须是包装类型,如果用基本类型生成POJO代码是无法通过编译的。 4、所有配置档仅在工具启动初始读取一并缓存到内存中,因此,如果您是在工具运行 时修改的配置档,请重新启动本工具以使新的配置生效。并且,所有配置档的XML结构均 不能修改,只能修改其节点间的文本值或属性值,以及添加新的标签组,否则会导致本 工具无法工作。选择“界面皮肤方案”后,默认会在当前目录生成名为skin.dat的文件, 这是一个Properties属性文件,用于保存您最后选择的皮肤名称,以便下打开此工具 时加载您所选择的皮肤来渲染工具UI界面。 5、所有最终代码生成效果都可以在左边的代码预览区域中查看,可点击滑动箭头显示出 被隐藏的POJO代码卡片。点击“写入磁盘文件”按钮即可将POJO代码的Java源码文件写入 到指定文件夹中。POJO代码的equals方法重写完全符合《Core Java》所述规范,同时, 其中的hashCode方法重写则参考了Netbeans中JavaBean转换器的写法。为保障原有代码安 全,通常更好的做法是将最终代码生成后拷贝到您的项目对应文件夹中。最好不要直接指 向您的项目文件夹,因为本工具会直接覆盖掉指定目录中同名的文件。最终生成的代码文 件以.java为扩展名。 6、从1.3版开始生成POJO代码目录中可自动添加一个名为pojo.ntf.xml的POJO映射通 知档,其中,ID列名默认使用主键名称(若为复合主键则采用序排首位的主键列名) ,而Oracle环境下的sequence对象名称则为“seq_表名_id”格式的默认名称,请根据 实际情况修改。该配置档用于CmSdk4j-Core框架的ORM映射,不需要则请不要勾选此项或 在生成后直接删除即可。 7、目前1.3.3版与1.3版差异不大,仅修改了POJO类名与成员变量名的大小写处理策略。 即目标数据库服务器为Oracle时,才将表名除首字母外全部小写处理成POJO类名,同理, 成员变量名也只在Oracle数据库情况下才全小写处理。其余数据库如:DB2、MySQL、 MS SQL Server则直接处理为除首字母大写外,其余全部保留原始大小写。其中,对于 表名的处理还直接去掉了空格符和下划线,并且若为Oracle数据库时,下划线亦作为首 字母大写的分隔标志,如:HRM_HUMAN_RESOURCE,最终生成POJO类名将直接去掉串中 的下划线,并以下划线作为首字母大写的起始,即:HrmHumanResource + POJO类名后缀。 同理,成员变量名的处理也是采用了相同的处理策略。最终处理效果详见生成写入到磁盘 的pojo.ntf.xml配置档。 8、此小工具一直均只写来自用,以便与自己的O/R Mapping简易版工具配套使用,目前 1.3.3这个版本已经能满足自己的需要,同时为了方便预览POJO代码生成的效果,特意添 加了语法着色功能,其着色色调搭配和关键字字典数据来源于EmEditor这款带语法着色的 纯文本编辑器,并且该色调搭配方案也被多款JS版本的语法着色器采用,色调可读性较高。 此小工具虽然GUI、功能这些都相对较弱,但自用已经足够。因此,后期可能就不再考虑 功能更新了,请见谅! 如果您有好的建议,请发送留言到作者博客:http://blog.csdn.net/CodingMouse 或发送邮件到:CodingMouse@gmail.com 本工具已经打包成exe可执行文件,便于在Window环境下运行,但仍需要你的机器上 安装至少1.6版本的jre环境(受打包工具的jre版本不兼容限制影响)。 By CodingMouse 2010年5月22日
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值