遇到的问题
在日常开发过程中我们往往有这样的需求:生成随机对象,特别在编写单元测试或集成测试的时候。这样的对象,我们往往不关心其字段的具体是什么值,只要里面的字段符合一定的规则要求即可(避免空指针、满足数据库表非空约束、参数校验约束等)。
java.utiil.random 包给我们提供了 nextInt(), nextLong(), nextDouble(), nextFloat(), nextBytes(), nextBoolean() 和 nextGaussian()
这样的函数,用来生成原始类型对象,但是对于其他对象、字符串等,则没有提供方便的 API。
例如,我们有以下这个 User 对象
@Data
public class User {
@NotBlank
private String username;
@Valid
private Address address;
@Pattern("^1(3|4|5|6|7|8|9)d{9}$");
private String phone;
@Email
private String email;
}
@Data
public class Address {
private String province;
private String city;
private String street;
}
这时我们不得不手动设置每个字段的值
User user = new User();
user.setUsername("张三"):
Address address = new Address();
address.setProvince("浙江");
...
user.setAddress(address);
user.setPhone("13811110000");
user.setEmail("dadiyang@aliyun.com");
当这样的冗余代码充斥在单元测试中时,我们的单元测试会越来越难以阅读和维护,最终导致很难坚持下去,变成烂尾测试。
神器来求场——EasyRandom
EasyRandom,一个简单到傻子都会用的 Java 对象生成器
The simple, stupid random Java™ beans generator
github地址:https://github.com/j-easy/easy-random
用它来生成随机对象,一行代码搞定:
User user = new EasyRandom().nextObject(User.class);
对于一般的场景,我们直接使用即可。而对于一些个性化的需求,这个生成器还提供了很多个性化的配置选项。
如何使用?
引入maven依赖
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-random-core</artifactId>
<version>4.3.0</version>
<exclusions>
<!-- 跟 SpringAOP 引入的 objenesis 有冲突,要排除 -->
<exclusion>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 支持根据参数校验逻辑生成对象字段 -->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-random-bean-validation</artifactId>
<version>4.3.0</version>
</dependency>
注:4.3.0 之前
- 对 @Positive @Nagetive 这类数字类型的支持有bug
- 对于泛型的支持较弱,所以请使用 4.3.0 及以上版本
封装工具类
import org.jeasy.Random.EasyRandom;
import org.jeasy.Random.EasyRandomParameters;
import org.jeasy.Random.api.Randomizer;
import org.jeasy.Random.api.RandomizerRegistry;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.stream.*;
/**
* 随机工具,封装 EasyRandom 提供的对象和 List 的随机生成方法
* @author dadiyang
* @since 2020/11/15
*/
public class RandomUtil {
private static final EasyRandom easyRandom
private RandomUtil() {
throw new UnsupportedOperationException("静态工具类不允许被实例化");
}
static {
EasyRandomParameters param = new EasyRandomParameters();
param.setStringLengthRange(new EasyRandomParameters.Range<>(5, 10));
// 注册自定义随机生成器
param.RandomizerRegistry(new BigDecimalRegistry());
// 生成的对象是一个是接口或者抽象类,则扫描类路径找到它的一个具体实现类
param.setScanClasspathForConcreteTypes(true);
param.setObjectPoolSize(100);
param.setRandomizationDepth(12);
easyRandom = new EasyRandom(param);
}
/**
* 根据给定的类型生成一个随机的对象
*/
public static <T> T nextObject(Class<T> clz) {
return easyRandom.nextObject(clz);
}
/**
* 根据给定的类型和大小生成一个随机对象的列表
*/
public static <T> List<T> nextList(Class<T> clz, int size) {
return objects(clz, size).collect(Collectors.toList());
}
/**
* 根据给定的类型和大小生成一个随机对象的集合
*/
public static <T> Set<T> nextSet(Class<T> clz, int size) {
return objects(clz, size).collect(Collectors.toSet());
}
/**
* 根据给定的类型和大小生成一个随机对象流
*/
public static <T> Stream<T> objects(Class<T> clz, int size) {
return easyRandom.objects(clz, size);
}
/**
* 默认生成的 BigDecimal 实例精度过大,将会导致用于插入数据库时超过精度而报错,所以我们默认精度取为5
*/
private static class BigDecimalRegistry implements RandomizerRegistry {
static final BigDecimalRandomizer bigDecimalRandomizer = new BigDecimalRandomizer();
@Override
public void init(EasyRandomParameters easyRandomParameters) { }
@Override
public Randomizer<?> getRandomizer(Field field) {
if (field.getType() == BigDecimal.class) {
return bigDecimalRandomizer;
} else {
return null;
}
}
@Override
public Randomizer<?> getRandomizer(Class<?> aClass) {
if (aClass == BigDecimal.class) {
return bigDecimalRandomizer;
} else {
return null;
}
}
}
private static class BigDecimalRandomizer implements Randomizer<BigDecimal> {
@Override
public BigDecimal getRandomValue() {
return BigDecimal.valueOf(nextDouble(Integer.MIN_VALUE, Integer.MAX_VALUE))
.setScale(5, BigDecimal.ROUND_HALF_UP);
}
}
}
使用工具类
// 随机生成一个 user 对象
User user = RandomUtil.nextObjet(User.class);
// 随机生成一个 user 对象列表
List<User> users = RandomUtil.nextList(User.class, 10);
One More Thing
不用构造器就能生成对象
学习这个工具时,我发现一个很有意思的点。
这个工具使用反射来生成对象并设置里面的值,所以一般我们认为,一个对象应包含一个默认的构造器,这样框架才能正确地使用 clz.newInstance()
来生成一个对象。但是它的强大之处在于,就算是没有默认构造器的对象,它也能够正确地生成对象!
换句话说,在没有默认构造器的场景下,它可以在不需要构造器的情况下,生成一个对象! 是不是非常神奇?原来,它底层是使用了 Objenesis 提供的能力来实现的。这个 Objenesis 在 SpringAOP 的底层也发挥了很大的作用,有兴趣的同学可以继续深入了解一下哦。