"本文原文鏈接地址:http://nullpointer.pw/mapstruct%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html" 前言 按照日常開發習慣,對於不同領域層使用不同JavaBean對象傳輸數據,避免相互影響,因此基於資料庫實體對象User衍生出比如U ...
本文原文鏈接地址:http://nullpointer.pw/mapstruct%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html
前言
按照日常開發習慣,對於不同領域層使用不同JavaBean對象傳輸數據,避免相互影響,因此基於資料庫實體對象User衍生出比如UserDto、UserVo等對象,於是在不同層之間進行數據傳輸時,不可避免地需要將這些對象進行互相轉換操作。
常見的轉換方式有:
- 調用getter/setter方法進行屬性賦值
- 調用BeanUtil.copyPropertie進行反射屬性賦值
第一種方式不必說,屬性多了就需要寫一大坨getter/setter代碼。第二種方式比第一種方式要簡便很多,但是坑巨多,比如sources與target寫反,難以定位某個欄位在哪裡進行的賦值,同時因為用到反射,導致性能也不佳。
鑒於此,今天寫一寫第三種對象轉換方式,本文使用的是 MapStruct 工具進行轉換,MapStruct 原理也很簡單,就是在代碼編譯階段生成對應的賦值代碼,底層原理還是調用getter/setter方法,但是這是由工具替我們完成,MapStruct在不影響性能的情況下,解決了前面兩種方式弊端,很贊~
準備工作
為了講解 MapStruct 工具的使用,本文使用常見的 User 類以及對應 UserDto 對象來演示。
@Data
@Accessors(chain = true)
public class User {
private Long id;
private String username;
private String password; // 密碼
private Integer sex; // 性別
private LocalDate birthday; // 生日
private LocalDateTime createTime; // 創建時間
private String config; // 其他擴展信息,以JSON格式存儲
private String test; // 測試欄位
}
@Data
@Accessors(chain = true)
public class UserVo {
private Long id;
private String username;
private String password;
private Integer gender;
private LocalDate birthday;
private String createTime;
private List<UserConfig> config;
private String test; // 測試欄位
@Data
public static class UserConfig {
private String field1;
private Integer field2;
}
}
註意觀察這兩個類的區別。
一、MapStruct 配置以及基礎使用
項目中引入 MapStruct 的依賴
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</dependency>
因為項目中的對象轉換操作基本都一樣,因此抽取除了一個轉換基類,不同對象如果只是簡單轉換可以直接繼承該基類,而無需覆寫基類任何方法,即只需要一個空類即可。如果子類覆寫了基類的方法,則基類上的 @Mapping 會失效。
@MapperConfig
public interface BaseMapping<SOURCE, TARGET> {
/**
* 映射同名屬性
*/
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
TARGET sourceToTarget(SOURCE var1);
/**
* 反向,映射同名屬性
*/
@InheritInverseConfiguration(name = "sourceToTarget")
SOURCE targetToSource(TARGET var1);
/**
* 映射同名屬性,集合形式
*/
@InheritConfiguration(name = "sourceToTarget")
List<TARGET> sourceToTarget(List<SOURCE> var1);
/**
* 反向,映射同名屬性,集合形式
*/
@InheritConfiguration(name = "targetToSource")
List<SOURCE> targetToSource(List<TARGET> var1);
/**
* 映射同名屬性,集合流形式
*/
List<TARGET> sourceToTarget(Stream<SOURCE> stream);
/**
* 反向,映射同名屬性,集合流形式
*/
List<SOURCE> targetToSource(Stream<TARGET> stream);
}
實現 User 與 UserVo 對象的轉換器
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface UserMapping extends BaseMapping<User, UserVo> {
@Mapping(target = "gender", source = "sex")
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Override
UserVo sourceToTarget(User var1);
@Mapping(target = "sex", source = "gender")
@Mapping(target = "password", ignore = true)
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Override
User targetToSource(UserVo var1);
default List<UserConfig> strConfigToListUserConfig(String config) {
return JSON.parseArray(config, UserConfig.class);
}
default String listUserConfigToStrConfig(List<UserConfig> list) {
return JSON.toJSONString(list);
}
}
本文示例使用的是 Spring 的方式,@Mapper 註解的 componentModel 屬性值為 spring,不過應該大多數都用的此模式進行開發。
@Mapping用於配置對象的映射關係,示例中 User 對象性別屬性名為 sex,而UserVo對象性別屬性名為gender,因此需要配置 target 與 source 屬性。
password 欄位不應該返回到前臺,可以採取兩種方式不進行轉換,第一種就是在vo對象中不出現password欄位,第二種就是在@Mapping中設置該欄位 ignore = true。
MapStruct 提供了時間格式化的屬性 dataFormat,支持Date、LocalDate、LocalDateTime等時間類型與String的轉換。示例中birthday 屬性為 LocalDate 類型,可以無需指定dataFormat自動完成轉換,而LocalDateTime類型預設使用的是ISO格式時間,在國內往往不符合需求,因此需要手動指定一下 dataFormat。
二、自定義屬性類型轉換方法
一般常用的類型欄位轉換 MapStruct都能替我們完成,但是有一些是我們自定義的對象類型,MapStruct就不能進行欄位轉換,這就需要我們編寫對應的類型轉換方法,筆者使用的是JDK8,支持介面中的預設方法,可以直接在轉換器中添加自定義類型轉換方法。
示例中User對象的config屬性是一個JSON字元串,UserVo對象中是List
default List<UserConfig> strConfigToListUserConfig(String config) {
return JSON.parseArray(config, UserConfig.class);
}
default String listUserConfigToStrConfig(List<UserConfig> list) {
return JSON.toJSONString(list);
}
如果是 JDK8以下的,不支持預設方法,可以另外定義一個 轉換器,然後再當前轉換器的 @Mapper 中通過 uses = XXX.class 進行引用。
定義好方法之後,MapStruct當匹配到合適類型的欄位時,會調用我們自定義的轉換方法進行轉換。
三、單元測試
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapStructTest {
@Resource
private UserMapping userMapping;
@Test
public void tetDomain2DTO() {
User user = new User()
.setId(1L)
.setUsername("zhangsan")
.setSex(1)
.setPassword("abc123")
.setCreateTime(LocalDateTime.now())
.setBirthday(LocalDate.of(1999, 9, 27))
.setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]");
UserVo userVo = userMapping.sourceToTarget(user);
log.info("User: {}", user);
// User: User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-01-17T17:46:20.316, config=[{"field1":"Test Field1","field2":500}])
log.info("UserVo: {}", userVo);
// UserVo: UserVo(id=1, username=zhangsan, gender=1, birthday=1999-09-27, createTime=2020-01-17 17:46:20, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
}
@Test
public void testDTO2Domain() {
UserConfig userConfig = new UserConfig();
userConfig.setField1("Test Field1");
userConfig.setField2(500);
UserVo userVo = new UserVo()
.setId(1L)
.setUsername("zhangsan")
.setGender(2)
.setCreateTime("2020-01-18 15:32:54")
.setBirthday(LocalDate.of(1999, 9, 27))
.setConfig(Collections.singletonList(userConfig));
User user = userMapping.targetToSource(userVo);
log.info("UserVo: {}", userVo);
// UserVo: UserVo(id=1, username=zhangsan, gender=2, birthday=1999-09-27, createTime=2020-01-18 15:32:54, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
log.info("User: {}", user);
// User: User(id=1, username=zhangsan, password=null, sex=2, birthday=1999-09-27, createTime=2020-01-18T15:32:54, config=[{"field1":"Test Field1","field2":500}])
}
四、常見問題
- 當兩個對象屬性不一致時,比如User對象中某個欄位不存在與UserVo當中時,在編譯時會有警告提示,可以在@Mapping中配置 ignore = true,當欄位較多時,可以直接在@Mapper中設置unmappedTargetPolicy屬性或者unmappedSourcePolicy屬性為 ReportingPolicy.IGNORE即可。
- 如果項目中也同時使用到了 Lombok,一定要註意 Lombok的版本要等於或者高於1.18.10,否則會有編譯不通過的情況發生,筆者掉進這個坑很久才爬了出來,希望各位不要重覆踩坑。
代碼下載
本文涉及代碼已上傳到 Github,以供參考。
參考
- 官方文檔:https://mapstruct.org/documentation/stable/reference/html/
- 官方FAQ:https://mapstruct.org/faq/
- 官方Example:https://github.com/mapstruct/mapstruct-examples
- 機翻中文版文檔:http://www.kailing.pub/MapStruct1.3/index.html
- 5種常見Bean映射工具的性能比對:https://www.cnblogs.com/javaguide/p/11861749.html