背景: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等。或者通過必定的業務處理,使得轉換類的通用性下降。其次轉換類數量不斷增長。安全
最近發現有許多工具類能夠實現自動拷貝 首先給性能圖數據結構
![image-20201231233830513](/Users/coapeng/Library/Application Support/typora-user-images/image-20201231233830513.png)
此外還有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,僅給出如下建議: