一般情况下,DO是用来映射数据库记录的实体类,DTO是用来在网络上传输的实体类。两者的不同除了适用场景不同外还有就是DTO需要实现序列化接口。从DB查询到数据之后,ORM框架会把数据转换成DO对象,通常我们需要再把DO对象转换为DTO对象。同样的,插入数据到DB之前需要将DTO对象转换为DO对象然后交给ORM框架去执行JDBC。
通常用到的转换工具类BeanUtils是通过反射来实现的,实现源码如下
public static <T> T convertObject(Object sourceObj, Class<T> targetClz) {
if (sourceObj == null) {
return null;
}
if (targetClz == null) {
throw new IllegalArgumentException("parameter clz shoud not be null");
}
try {
Object targetObj = targetClz.newInstance();
BeanUtils.copyProperties(sourceObj, targetObj);
return (T) targetObj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void copyProperties(Object source, Object target, Class<?> editable, String[] ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null) ? Arrays.asList(ignoreProperties) : null;
for (PropertyDescriptor targetPd : targetPds) {
if (targetPd.getWriteMethod() != null &&
(ignoreProperties == null || (!ignoreList.contains(targetPd.getName())))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null && sourcePd.getReadMethod() != null) {
try {
Method readMethod = sourcePd.getReadMethod();
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
Method writeMethod = targetPd.getWriteMethod();
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException("Could not copy properties from source to target", ex);
}
}
}
}
}
也可以通过mapstruct来实现,这种方式是在Mapper接口的包中生成一个对应mapper的实现类,实现类的源码如下。显然这种方式的实现更为普通,看起来没有BeanUtils的实现那么复杂。不过BeanUtils通过反射实现更为通用,可以为各种类型的DTO实现转换。而mapstruct只是帮我们生产了我们不想写的代码。
public Task doToDTO(TaskDO taskDO) {
if (taskDO == null) {
return null;
} else {
Task task = new Task();
task.setId(taskDO.getId());
//其他字段的set
task.setGmtCreate(taskDO.getGmtCreate());
task.setGmtModified(taskDO.getGmtModified());
return task;
}
}
对比以上两种方式,显然使用BeanUtils来进行转换时需要写的代码更少,内部的通过反射便可以进行get/set操作。而mapstruct实现上需要写的代码稍微多一点,但是这种方式的性能比通过反射实现更好。下面写一段代码来测试两种方式实现的性能差别。
public void testConvert() {
System.out.println("####testConvert");
int num = 100000;
TaskDO taskDO = new TaskDO();
long start = System.currentTimeMillis();
for (int i = 0; i < num; i ++) {
Task task = ObjectConvertor.convertObject(taskDO, Task.class);
}
System.out.println(System.currentTimeMillis() - start);
//---------------------------------------------
start = System.currentTimeMillis();
for (int i = 0; i < num; i ++) {
Task task = taskMapper.doToDTO(taskDO);
}
System.out.println(System.currentTimeMillis() - start);
}
以上测试代码中分别使用两种方式对同一个DO对象进行n次转换,两次转换的耗时统计如下。单位:ms
次数 | 1 | 10 | 100 | 1000 | 10000 | 100000 | 1000000 | 10000000 |
---|---|---|---|---|---|---|---|---|
Mapstruct | 0 | 1 | 1 | 1 | 2 | 4 | 8 | 8 |
BeanUtil | 9 | 7 | 11 | 26 | 114 | 500 | 1469 | 14586 |
可见当转换数量级增加时,使用BeanUtil的耗时急剧上升,而使用Mapstruct的耗时则保持在比较低的水平。
在一个系统中,ORM对DB的各种操作几乎都会涉及到DO和DTO之间的转换,参考以上表格的统计结果,更推荐使用Mapstruct。