背景:html
在代码里面 常常会有一些DO到BO,或者DTO的转换。java
例如:数据库查询出来的用户信息(表的映射模型)是UserDO,可是咱们须要传递给客户端的是UserVO,这时候就须要把UserDO实例的属性一个一个赋值到UserVO实例中。git
在这些数据结构之间很大一部分属性均可能会相同,也可能不一样。github
记得在大二的时候写代码最常使用的一种方式自定义一个convert转换方法或者一个转换类声明一堆静态方法实现特定对象转换。spring
public xxxBO xxxDOToxxxBO(xxxDO xxxdo){ xxxBO xxxbo=new xxxBO; xxxbo.setXXX(xxxdo.getXXX()); ...... ...... ...... }复制代码
这样实现的一个好处实现了代码的复用可是在方法编写的时候须要十分当心,稍微不注意就可能遗漏某个字段。若是这个字段是当前不重要,但后续开发十分重要的属性。一段时间后排查问题每每很难发现。数据库
解决方法1:在Set/Get方法的时候从底部开始,这样已经get过的属性就会放在最上方。不断get,直到第一个重复的对象这样就没有缺失的问题。api
解决方法2:使用idea的插件自动生成好比GenerateAllSetter、codehelper.generator等缓存
发现的问题:有部分字段转换的时候可能须要通过必定的逻辑映射,好比枚举类型转换为string等。或者通过必定的业务处理,使得转换类的通用性下降。其次转换类数量不断增长。安全
最近发现有许多工具类能够实现自动拷贝 首先给性能图数据结构

此外还有orika等其余框架。整体来讲底层使用技术三种使用反射、使用动态代理修改字节码文件、直接生成字节码文件,效率上来讲 直接生成字节码文件>动态代理(cglib)> 反射
使用方式来讲 手动get/set >mapstruct>orika>cglib>spring(BeanUtils)还有像Dozen(底层使用反射进行实现)
下面记录一下工具的demo
Spring中的BeanUtils,其中实现的方式很简单,就是对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。
能够看到, 成员变量赋值是基于目标对象的成员列表, 而且会跳过ignore的以及在源对象中不存在的, 因此这个方法是安全的, 不会由于两个对象之间的结构差别致使错误, 可是必须保证同名的两个成员变量类型相同。
咱们把数据库查询出来的UserDO.java 拷贝到 UserVO.java。直接使用BeanUtils.copyProperties()方法。
@Test public void commonCopy() { UserDO userDO = new UserDO(1L, "Van", 18, 1); UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); log.info("userVO:{}",userVO); } 复制代码复制代码
.... userVO:UserVO(userId=1, userName=Van, age=18, sex=null) 复制代码复制代码
刚刚拷贝的是一个对象,可是有时候咱们想拷贝一组UerDO.java,是一个集合的时候就不能这样直接赋值了。若是还按照这种逻辑,以下:
@Test public void listCopyFalse() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 18, 2)); List<UserVO> userVOList = new ArrayList(); BeanUtils.copyProperties(userDOList, userVOList); log.info("userVOList:{}",userVOList); } 复制代码复制代码
.... userVOList:[] 复制代码复制代码
经过日志能够发现,直接拷贝集合是无效的,那么怎么解决呢?
将须要拷贝的集合遍历,暴力拷贝。
@Test public void listCopyCommon() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = new ArrayList(); userDOList.forEach(userDO ->{ UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); }); log.info("userVOList:{}",userVOList); } 复制代码复制代码
.... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)] 复制代码复制代码
虽然该方式能够解决,可是一点都不优雅,特别是写起来麻烦。
经过JDK 8 的函数式接口封装org.springframework.beans.BeanUtils
函数式接口里是能够包含默认方法,这里咱们定义默认回调方法。
@FunctionalInterface public interface BeanUtilCopyCallBack <S, T> { /** * 定义默认回调方法 * @param t * @param s */ void callBack(S t, T s); } 复制代码复制代码
public class BeanUtilCopy extends BeanUtils { /** * 集合数据的拷贝 * @param sources: 数据源类 * @param target: 目标类::new(eg: UserVO::new) * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target) { return copyListProperties(sources, target, null); } /** * 带回调函数的集合数据的拷贝(可自定义字段拷贝规则) * @param sources: 数据源类 * @param target: 目标类::new(eg: UserVO::new) * @param callBack: 回调函数 * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target, BeanUtilCopyCallBack<S, T> callBack) { List<T> list = new ArrayList<>(sources.size()); for (S source : sources) { T t = target.get(); copyProperties(source, t); list.add(t); if (callBack != null) { // 回调 callBack.callBack(source, t); } } return list; } } 复制代码复制代码
@Test public void listCopyUp() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new); log.info("userVOList:{}",userVOList); } 复制代码复制代码
.... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)] 复制代码复制代码
经过如上方法,咱们基本实现了集合的拷贝,可是从返回结果咱们能够发现:属性不一样的字段没法拷贝。
注意: UserDO.java 和UserVO.java 最后一个字段sex类型不同,分别是:Integer/String
优化一下
public enum SexEnum { UNKNOW("未设置",0), MEN("男生", 1), WOMAN("女生",2), ; private String desc; private int code; SexEnum(String desc, int code) { this.desc = desc; this.code = code; } public static SexEnum getDescByCode(int code) { SexEnum[] typeEnums = values(); for (SexEnum value : typeEnums) { if (code == value.getCode()) { return value; } } return null; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } } 复制代码复制代码
@Test public void listCopyUpWithCallback() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, userVO) -> { // 这里能够定义特定的转换规则 userVO.setSex(SexEnum.getDescByCode(userDO.getSex()).getDesc()); }); log.info("userVOList:{}",userVOList); } 复制代码复制代码
... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=男生), UserVO(userId=2, userName=VanVan, age=20, sex=女生)] 复制代码复制代码
经过打印结果能够发现,UserDO.java 中Integer类型的sex复制到UserVO.java成了String类型的男生/女生。
该方法是咱们用的最多的方案,这里简单封装下,能够方便集合类型对象的拷贝,日常使用基本够用,仅供参考。
BeanCopier是用于在两个bean之间进行属性拷贝的。BeanCopier支持两种方式:
@Test public void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); // 第一个参数:源对象, 第二个参数:目标对象,第三个参数:是否使用自定义转换器(下面会介绍),下同 BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false); UserDTO userDTO = new UserDTO(); b.copy(userDO, userDTO, null); log.info("拷贝后:userDTO:{}", userDTO); } 复制代码复制代码
...... 拷贝前:userDO:UserDO(id=1, userName=Van, sex=0, gmtBroth=2019-11-02T18:24:24.077, balance=100) ...... 拷贝后:userDTO:UserDTO(id=1, userName=Van, sex=null) 复制代码复制代码
经过结果发现:UserDO的int类型的sex没法拷贝到UserDTO的Integer的sex。
即:BeanCopier只拷贝名称和类型都相同的属性。
即便源类型是原始类型(int, short和char等),目标类型是其包装类型(Integer, Short和Character等),或反之:都不会被拷贝。
经过3.1可知,当源和目标类的属性类型不一样时,不能拷贝该属性,此时咱们能够经过实现Converter接口来自定义转换器
@Data public class UserDomain { private Integer id; private String userName; /** * 如下两个字段用户模拟自定义转换 */ private String gmtBroth; private String balance; } 复制代码复制代码
public class UserDomainConverter implements Converter { /** * 时间转换的格式 */ DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * 自定义属性转换 * @param value 源对象属性类 * @param target 目标对象里属性对应set方法名,eg.setId * @param context 目标对象属性类 * @return */ @Override public Object convert(Object value, Class target, Object context) { if (value instanceof Integer) { return value; } else if (value instanceof LocalDateTime) { LocalDateTime date = (LocalDateTime) value; return dtf.format(date); } else if (value instanceof BigDecimal) { BigDecimal bd = (BigDecimal) value; return bd.toPlainString(); } // 更多类型转换请自定义 return value; } } 复制代码复制代码
/** * 类型不一样,使用Converter */ @Test public void converterTest() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true); UserDomainConverter converter = new UserDomainConverter(); UserDomain userDomain = new UserDomain(); copier.copy(userDO, userDomain, converter); log.info("拷贝后:userDomain:{}", userDomain); } 复制代码复制代码
...... 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100) ...... 拷贝后:userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100) 复制代码复制代码
BeanCopier拷贝速度快,性能瓶颈出如今建立BeanCopier实例的过程当中。 因此,把建立过的BeanCopier实例放到缓存中,下次能够直接获取,提高性能。
@Test public void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(10000); long start = System.currentTimeMillis(); List<UserDTO> userDTOS = new ArrayList<>(); userDOList.forEach(userDO -> { UserDTO userDTO = new UserDTO(); copy(userDO, userDTO); userDTOS.add(userDTO); }); } /** * 缓存 BeanCopier */ private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIERS = new ConcurrentHashMap<>(); public void copy(Object srcObj, Object destObj) { String key = genKey(srcObj.getClass(), destObj.getClass()); BeanCopier copier = null; if (!BEAN_COPIERS.containsKey(key)) { copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false); BEAN_COPIERS.put(key, copier); } else { copier = BEAN_COPIERS.get(key); } copier.copy(srcObj, destObj, null); } private String genKey(Class<?> srcClazz, Class<?> destClazz) { return srcClazz.getName() + destClazz.getName(); } 复制代码复制代码
Orika 是 Java Bean 映射框架,能够实现从一个对象递归拷贝数据至另外一个对象。它的优势是:名字相同类型不一样也能直接复制。
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.4</version> </dependency> 复制代码复制代码
使用枚举实现的单例模式建立一个映射工具类,便于测试。
public enum MapperUtils { /** * 实例 */ INSTANCE; /** * 默认字段工厂 */ private static final MapperFactory MAPPER_FACTORY = new DefaultMapperFactory.Builder().build(); /** * 默认字段实例 */ private static final MapperFacade MAPPER_FACADE = MAPPER_FACTORY.getMapperFacade(); /** * 默认字段实例集合 */ private static Map<String, MapperFacade> CACHE_MAPPER_FACADE_MAP = new ConcurrentHashMap<>(); /** * 映射实体(默认字段) * * @param toClass 映射类对象 * @param data 数据(对象) * @return 映射类对象 */ public <E, T> E map(Class<E> toClass, T data) { return MAPPER_FACADE.map(data, toClass); } /** * 映射实体(自定义配置) * * @param toClass 映射类对象 * @param data 数据(对象) * @param configMap 自定义配置 * @return 映射类对象 */ public <E, T> E map(Class<E> toClass, T data, Map<String, String> configMap) { MapperFacade mapperFacade = this.getMapperFacade(toClass, data.getClass(), configMap); return mapperFacade.map(data, toClass); } /** * 映射集合(默认字段) * * @param toClass 映射类对象 * @param data 数据(集合) * @return 映射类对象 */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data) { return MAPPER_FACADE.mapAsList(data, toClass); } /** * 映射集合(自定义配置) * * @param toClass 映射类 * @param data 数据(集合) * @param configMap 自定义配置 * @return 映射类对象 */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data, Map<String, String> configMap) { T t = data.stream().findFirst().orElseThrow(() -> new ExceptionInInitializerError("映射集合,数据集合为空")); MapperFacade mapperFacade = this.getMapperFacade(toClass, t.getClass(), configMap); return mapperFacade.mapAsList(data, toClass); } /** * 获取自定义映射 * * @param toClass 映射类 * @param dataClass 数据映射类 * @param configMap 自定义配置 * @return 映射类对象 */ private <E, T> MapperFacade getMapperFacade(Class<E> toClass, Class<T> dataClass, Map<String, String> configMap) { String mapKey = dataClass.getCanonicalName() + "_" + toClass.getCanonicalName(); MapperFacade mapperFacade = CACHE_MAPPER_FACADE_MAP.get(mapKey); if (Objects.isNull(mapperFacade)) { MapperFactory factory = new DefaultMapperFactory.Builder().build(); ClassMapBuilder classMapBuilder = factory.classMap(dataClass, toClass); configMap.forEach(classMapBuilder::field); classMapBuilder.byDefault().register(); mapperFacade = factory.getMapperFacade(); CACHE_MAPPER_FACADE_MAP.put(mapKey, mapperFacade); } return mapperFacade; } } 复制代码复制代码
@Test public void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); // 第一个参数:源对象, 第二个参数:目标对象,第三个参数:是否使用自定义转换器(下面会介绍),下同 UserDTO userDTO = MapperUtils.INSTANCE.map(UserDTO.class, userDO);; log.info("拷贝后:userDTO:{}", userDTO); } 复制代码复制代码
@Test public void converterTest() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); Map<String, String> config = new HashMap<>(); // 自定义配置(balance 转 balances) config.put("balance", "balances"); log.info("拷贝前:userDO:{}", userDO); UserDomain userDomain = MapperUtils.INSTANCE.map(UserDomain.class, userDO, config); log.info("拷贝后:userDomain:{}", userDomain); } 复制代码复制代码
@Test public void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(3); log.info("拷贝前:userDOList:{}", userDOList); List<UserDTO> userDTOS = MapperUtils.INSTANCE.mapAsList(UserDTO.class,userDOList); log.info("拷贝后:userDTOS:{}", userDTOS); } 复制代码复制代码
MapStruct 是一个自动生成 bean 映射类的代码生成器。MapStruct 还可以在不一样的数据类型之间进行转换。
包含所需的注释,例如@Mapping。
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</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> 复制代码复制代码
您所要作的就是定义一个mapper接口,该接口声明任何所需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用普通的Java方法调用来在源对象和目标对象之间进行映射。
利用@Mapper注解标注该接口/抽象类是被MapStruct自动映射的,只有存在该注解才会将内部的接口方法自动实现。
MapStruct为咱们提供了多种的获取Mapper的方式,习惯用默认配置:采用Mappers经过动态工厂内部反射机制完成Mapper实现类的获取。
UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class); 复制代码复制代码
完整的一个转换器demo:
@Mapper public interface UserConvertUtils { UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class); /** * 普通的映射 * * @param userDO UserDO数据持久层类 * @return 数据传输类 */ UserDTO doToDTO(UserDO userDO); /** * 类型转换的映射 * * @param userDO UserDO数据持久层类 * @return 数据传输类 */ @Mappings({ @Mapping(target = "gmtBroth", source = "gmtBroth", dateFormat = "yyyy-MM-dd HH:mm:ss"), @Mapping(target = "balances", source = "balance"), }) UserDTO doToDtoWithConvert(UserDO userDO); } 复制代码复制代码
/** * 通常拷贝 */ @Test public void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDTO(userDO); log.info("拷贝后:userDTO:{}", userDTO); } /** * 包含类型转换的拷贝 */ @Test public void doToDtoWithConvert() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDtoWithConvert(userDO); log.info("拷贝后:userDTO:{}", userDTO); } 复制代码复制代码
通常拷贝: ...拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balance=100) ...拷贝后:userDTO:UserDTO(id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balances=null) 包含类型转换的拷贝: ...拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2020-04-21T21:05:19.282, balance=100) ...拷贝后:userDTO:UserDTO(id=1, userName=Van, gmtBroth=2020-04-21 21:05:19, balances=100) 复制代码复制代码
经过打印结果能够发现:相较于前者,包含类型转换的拷贝能够自定义转换属性和时间格式等。
MapStruct 能够将几种类型的对象映射为另一种类型,好比将多个 DO 对象转换为 DTO。
详见:
UserDTO doAndInfoToDto(UserDO userDO, UserInfoDO userInfoDO); 复制代码复制代码
与手工编写映射代码相比,MapStruct经过生成繁琐且易于编写的代码来节省时间。遵循约定优于配置方法,MapStruct使用合理的默认值,但在配置或实现特殊行为时会采起措施。
与动态映射框架相比,MapStruct具备如下优点:
经过四种属性拷贝的方式,加上本身手动get/set,仅给出如下建议: