设计一个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());
    }
  }

测试通过

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Mybatis-Plus是一个基于Mybatis的增强工具,它提供了一些实用的功能,其中包括自动生成POJO的功能。使用Mybatis-Plus可以方便地生成POJO类,减少手动编写POJO的工作量,提高开发效率。生成的POJO类可以根据数据库表结构自动生成,可以通过注解或XML配置进行自定义。 ### 回答2: MyBatis-Plus是在MyBatis框架的基础上增加了一系列的扩展功能,其中之一就是自动生成pojo的功能。 MyBatis-Plus的代码生成器可以自动生成数据库中每个表对应的实体类(POJO)、Mapper接口以及XML文件。这个功能可以大大降低程序员的开发量,提高开发效率。 使用MyBatis-Plus代码生成器生成POJO的步骤如下: 1. 配置MyBatis-Plus的代码生成器。在pom.xml文件中添加MyBatis-Plus和MySQL驱动的依赖,然后在resources目录下创建generator.properties配置文件,配置数据库连接信息、包名、作者名等信息。 2. 编写生成器代码。在同一个配置文件中,配置输出路径、生成哪些模块(Controller、Service、ServiceImpl、Mapper等)、生成哪些表等信息。 3. 运行代码生成器。在generator文件夹下,运行MybatisPlusGeneratorApplication类即可开始生成POJO。 生成的POJO代码默认是不包含注释的,可以在generator.properties文件中配置添加注释的规则。 总之,MyBatis-Plus自动生成POJO的功能可以大大减少程序员的开发工作量,提高开发效率,在写Java代码时已经成为了一个很常见的实践,非常值得推荐。 ### 回答3: MyBatis-Plus是针对MyBatis框架的增强工具,提供了许多方便使用的功能。其中,自动生成POJO是MyBatis-Plus的一个重要特性。 自动化生成POJO,可以帮助我们简化程序开发的过程,提高开发效率。 MyBatis-Plus自动生成POJO主要分为两种方式:基于代码生成器和基于IDE插件。 基于代码生成器,我们可以通过配置文件配置输出路径、父包名、数据库连接等参数,然后自动生成对应的实体类、mapper接口、mapper.xml文件等一系列文件,这样就省去了我们手写这些繁琐的代码的工作。 基于IDE插件,我们可以在IDE中安装MyBatis-Plus插件,通过插件提供的代码生成功能,自动根据数据库表结构生成对应的实体类、mapper接口、mapper.xml文件等文件。 无论是哪种方式,MyBatis-Plus自动生成POJO都需要我们在配置文件中对数据库进行配置。在配置文件中,我们需要提供数据库连接信息,以及实体类所在的包名、映射文件所在的包名、数据表名等参数。MyBatis-Plus还提供了一些高级配置选项,如是否支持AR模式、生成的注释模板等,可以根据不同的需求进行调整。 需要注意的是,MyBatis-Plus自动生成POJO虽然可以大大简化我们的开发工作,但是有些情况下会存在一些问题。比如,数据库表结构发生变化时,我们需要手动修改对应的实体类和映射文件。此外,在生成过程中,MyBatis-Plus可能无法满足我们的需求,需要我们手动修改生成代码。因此,我们需要在使用MyBatis-Plus自动生成POJO的过程中,谨慎对待,灵活运用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luncert

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值