前言

声明:
1、DO(业务实体对象),DTO(数据传输对象)。
在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。
- 在我们对外暴露的Dubbo接口,一般这样定义接口类
/**
* 获取营销信息
*
* @param hallId 场馆编号
* @param modelCode 车型代码
* @return
*/
BrandMarketInfoDTO getBrandMarketInfo(String hallId, String modelCode);
- 在和数据持久层映射实现的接口中,一般我们会这么写
/**
* 获取水牌营销信息
*
* @param hallId
* @param modelCode
* @return
*/
HallCarManageDO getBoardMarketInfo(@Param("hallId") String hallId, @Param("modelCode") String modelCode);
但是我们HallCarManageDO和BrandMarketInfoDTO中的属性和属性类型不是相等的。这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。
常见工具类及实现
以下列举被广大开发工程师常用的Bean属性复制工具
- Spring.BeanUtils
- Cglib.BeanCopier
- MapStruct
以下选取属性赋值的功能来对比每个工具类的不同
BeanUtils
import org.springframework.beans.BeanUtils;
/**
* @author james mu
* @date 2019/10/22
*/
public class PojoUtils {
/**
*
* @param source 源对象
* @param clazz 目标对象
*
* @return 复制属性后的对象
*/
public static <T> T copyProperties(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T target;
try {
target = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("通过反射创建对象失败");
}
BeanUtils.copyProperties(source, target);
return target;
}
}
springframework
的BeanUtils也是通过java内省机制
获取getter/setter,然后通过反射调用从而实现属性复制。
BeanCopier
依赖引用
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
工具类实现
import net.sf.cglib.beans.BeanCopier;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author james mu
* @date 2019/10/22
*/
public class BeanCopierUtils {
/**
* BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。
* 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能
*/
public static Map<String, BeanCopier> beanCopierMap = new ConcurrentHashMap<String, BeanCopier>();
/**
* cp 对象赋值
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyProperties(Object source, Object target) {
String beanKey = generateKey(source.getClass(), target.getClass());
BeanCopier copier = null;
if (!beanCopierMap.containsKey(beanKey)) {
copier = BeanCopier.create(source.getClass(), target.getClass(), false);
beanCopierMap.put(beanKey, copier);
} else {
copier = beanCopierMap.get(beanKey);
}
copier.copy(source, target, null);
}
private static String generateKey(Class<?> class1, Class<?> class2) {
return class1.toString() + class2.toString();
}
}
-
使用动态代理,生成字节码类,再通过Java反射成Class,调用其copy方法。
-
大家可以看到这里用到了ConcurrentHashMap存取
copier
,因为BeanCopier.create使用了缓存,该过程也消耗资源,建议全局只初始化一次。 -
支持自定义转换器。
MapStruct
MapSturct
是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。
抓一下重点:
- 注解处理器
- 可以生成
JavaBean
之间那的映射代码 - 类型安全, 高性能, 无依赖性
从字面的理解, 我们可以知道, 该工具可以帮我们实现 JavaBean
之间的转换, 通过注解的方式。
同时, 作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。
入门
依赖引用
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.0.Final</version>
<scope>provided</scope>
</dependency>
案例演示
UserDO
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
* @author james mu
* @date 2019/10/22
*/
@Data
@ToString
public class UserDO {
private String name;
private String password;
private Integer age;
private Date birthday;
private String sex;
}
UserDTO
import lombok.Data;
import lombok.ToString;
/**
* @author james mu
* @date 2019/10/22
*/
@Data
@ToString
public class UserDTO {
private String name;
private String age;
private String birthday;
private String gender;
}
UserConvertUtils
import com.sanshengshui.javabeanconvert.DO.UserDO;
import com.sanshengshui.javabeanconvert.DTO.UserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
/**
* @author james mu
* @date 2019/10/22
*/
@Mapper
public interface UserConvertUtils {
UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class);
/**
* 类型转换
*
* @param userDO UserDO数据持久层类
* @return 数据传输类
*/
@Mappings({
@Mapping(target = "gender", source = "sex")
})
UserDTO doToDTO(UserDO userDO);
}
- DTO与DO中属性名相同时候默认映射,(比如name),属性名相同属性类型不同也会映射,(比如birthday,一个Data,一个String)
- DTO与DO中属性名不同的,需要通过
@Mapping
明确关系来形成映射(如sex对应gender) - 无映射关系属性被忽略(如UserEntity的password)
结果如下:
UserDO(name=snow, password=123, age=20, birthday=Tue Oct 22 17:10:19 CST 2019, sex=男)
+-+-+-+-+-+-+-+-+-+-+-
UserDTO(name=snow, age=20, birthday=19-10-22 下午5:10, gender=男)
MapStruct分析
上面中, 我写了3个步骤来实现了从 UserDTO
到 UserDO
的转换。
那么, 作为一个注解处理器, 通过MapStruct
生成的代码具有怎么样的优势呢?
高性能
Java反射原理和反射低的原因:juejin.cn/post/684490…
这是相对反射来说的, 反射需要去读取字节码的内容, 花销会比较大。 而通过 MapStruct
来生成的代码, 其类似于人手写。 速度上可以得到保证。
前面例子中生成的代码可以在编译后看到。 在 target/generated-sources/annotations 里可以看到。

对应的代码
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-10-22T17:10:17+0800",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Azul Systems, Inc.)"
)
public class UserConvertUtilsImpl implements UserConvertUtils {
@Override
public UserDTO doToDTO(UserDO userDO) {
if ( userDO == null ) {
return null;
}
UserDTO userDTO = new UserDTO();
userDTO.setGender( userDO.getSex() );
userDTO.setName( userDO.getName() );
if ( userDO.getAge() != null ) {
userDTO.setAge( String.valueOf( userDO.getAge() ) );
}
if ( userDO.getBirthday() != null ) {
userDTO.setBirthday( new SimpleDateFormat().format( userDO.getBirthday() ) );
}
return userDTO;
}
}
可以看到其生成了一个实现类, 而代码也类似于我们手写, 通俗易懂。
性能比较
测试在两个简单的Bean之间转换的耗时,执行次数分别为10、100、1k、10k、100k,时间单位为ms。

总结
虽然反射效率低,但这个时间是很小很小的。根据不同工具的性能及功能维度,个人建议当对象转换操作较少或者应用对性能要求较高时,尽量不采用工具,而是手写getter/setter;在不考虑性能的情况下,普通的对象转换可以使用Cglib.BeanCopier,复杂的对象转换使用MapStruct。