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