Java?MapStruct優(yōu)雅地實現對象轉換
1、什么是MapStruct
1.1 JavaBean 的困擾
對于代碼中 JavaBean之間的轉換, 一直是困擾我很久的事情。在開發(fā)的時候我看到業(yè)務代碼之間有很多的 JavaBean 之間的相互轉化, 非常的影響觀感,卻又不得不存在。我后來想的一個辦法就是通過反射,或者自己寫很多的轉換器。
第一種通過反射的方法確實比較方便,但是現在無論是 BeanUtils, BeanCopier 等在使用反射的時候都會影響到性能。雖然我們可以進行反射信息的緩存來提高性能。但是像這種的話,需要類型和名稱都一樣才會進行映射,有很多時候,由于不同的團隊之間使用的名詞不一樣,還是需要很多的手動 set/get 等功能。
第二種的話就是會很浪費時間,而且在添加新的字段的時候也要進行方法的修改。不過,由于不需要進行反射,其性能是很高的。
1.2 MapStruct 帶來的改變
MapSturct 是一個生成類型安全,高性能且無依賴的 JavaBean 映射代碼的注解處理器(annotation processor)。
- 注解處理器
- 可以生成 JavaBean 之間那的映射代碼
- 類型安全,高性能,無依賴性
2、MapStruct 入門
2.1 添加依賴
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>2.2 po類
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
}2.3 dto類
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
}2.4 創(chuàng)建轉換接口
//可以使用abstract class代替接口
@Mapper
publicinterface UserMapper {
UserDto userToUserDto(User user);
//集合
List<UserDto> userToUserDto(List<User> users);
}2.5 測試方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}2.6 運行效果

2.7 查看編譯的class
底層通過自動取值賦值操作完成

3、MapStruct優(yōu)點分析
3.1 性能高
這是相對反射來說的,反射需要去讀取字節(jié)碼的內容,花銷會比較大。而通過 MapStruct 來生成的代碼,其類似于人手寫。速度上可以得到保證。
3.2 使用簡單
如果是完全映射的,使用起來肯定沒有反射簡單。用類似 BeanUtils 這些工具一條語句就搞定了。但是,如果需要進行特殊的匹配(特殊類型轉換,多對一轉換等),其相對來說也是比較簡單的。
基本上,使用的時候,我們只需要聲明一個接口,接口下寫對應的方法,就可以使用了。當然,如果有特殊情況,是需要額外處理的。
3.3 代碼獨立
生成的代碼是對立的,沒有運行時的依賴。
3.4 易于 debug
在我們生成的代碼中,我們可以輕易的進行 debug。
4、MapStruct使用案例
4.1 屬性名稱相同
在實現類的時候,如果屬性名稱相同,則會進行對應的轉化。通過此種方式,我們可以快速的編寫出轉換的方法。(入門案例)
4.2 屬性名不相同
屬性名不相同,在需要進行互相轉化的時候,則我們可以通過@Mapping 注解來進行轉化。
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
}
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
publicinterface UserMapper {
//單個屬性
//@Mapping(source = "pwd",target = "password")
//多個屬性
@Mappings({
@Mapping(source = "pwd",target = "password")
})
UserDto userToUserDto(User user);
}- source 需要轉換的對接,通常是入參
- target 轉換的對接,通常是出參
- ignore 忽略,默認false不忽略,需要忽略設置為true
- defaultValue 默認值
- expressions 可以通過表達式來構造一些簡單的轉化關系。雖然設計的時候想兼容很多語言,不過目前只能寫Java代碼。
@Mappings({
@Mapping(source = "birthdate", target = "birth"),//屬性名不一致映射
@Mapping(target = "birthformat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthdate(),"yyyy-MM-dd HH:mm:ss"))"),//自定義屬性通過java代碼映射
})
public PersonVo PersonToPersonVo(Person person);這里用到演示了如何使用TimeAndFormat對time和format操作,這里必須要指定需要使用的Java類的完整包名,不然編譯的時候不知道你使用哪個Java類,會報錯。
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
user.setPwd("123456");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
4.3 轉換非基礎類型屬性
如果subUser與subUserDto字段名稱相同直接配置即可完成(對象類型,包括list)
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private List<SubUserDto> subUserDto;
}
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private List<SubUser> subUser;
}
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);4.4 Mapper 中使用自定義的轉換
有時候,對于某些類型,無法通過代碼生成器的形式來進行處理。那么, 就需要自定義的方法來進行轉換。這時候,我們可以在接口(同一個接口,后續(xù)還有調用別的 Mapper 的方法)中定義默認方法(Java8及之后)。
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private SubUserDto subUserDto;
}
@Data
publicclass SubUserDto {
private Boolean result;
private String name;
}
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private SubUser subUser;
}
@Data
publicclass SubUser {
private Integer deleted;
private String name;
}
@Mapper
publicinterface UserMapper {
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);
default SubUserDto subSource2subTarget(SubUser subUser) {
if (subUser == ) {
return;
}
SubUserDto subUserDto = new SubUserDto();
subUserDto.setResult(!subUser.getDeleted().equals(0));
subUserDto.setName(subUser.getName()==?"":subUser.getName());
return subUserDto;
}
}只能存在一個default修飾的方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
user.setSubUser(subUser);
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
4.5 多轉一
我們在實際的業(yè)務中少不了將多個對象轉換成一個的場景。MapStruct 當然也支持多轉一的操作。
@Data
publicclass SubUser {
private Integer deleted;
private String name;
}
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
publicinterface UserMapper {
@Mappings({
@Mapping(source = "user.pwd",target = "password"),
@Mapping(source = "subUser.name", target = "name")
})
NewUserDto userToUserDto(User user,SubUser subUser);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToUserDto(user,subUser);
System.out.println(userDto);
}
4.5.1 遵循原則
- 當多個對象中, 有其中一個為 , 則會直接返回
- 如一對一轉換一樣, 屬性通過名字來自動匹配。因此, 名稱和類型相同的不需要進行特殊處理
- 當多個原對象中,有相同名字的屬性時,需要通過 @Mapping 注解來具體的指定, 以免出現歧義(不指定會報錯)。如上面的 name
屬性也可以直接從傳入的參數來賦值
@Mapping(source = "person.description", target = "description") @Mapping(source = "name", target = "name") DeliveryAddress personAndAddressToDeliveryAddressDto(Person person, String name);
4.6 更新 Bean 對象
有時候,我們不是想返回一個新的 Bean 對象,而是希望更新傳入對象的一些屬性。這個在實際的時候也會經常使用到。
@Mapper
publicinterface UserMapper {
NewUserDto userToNewUserDto(User user);
/**
* 更新, 注意注解 @MappingTarget
* 注解 @MappingTarget后面跟的對象會被更新。
*/
void updateDeliveryAddressFromAddress(SubUser subUser,@MappingTarget NewUserDto newUserDto);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToNewUserDto(user);
mapper.updateDeliveryAddressFromAddress(subUser,userDto);
System.out.println(userDto);
}4.7 map映射
@MapMapping(valueDateFormat ="yyyy-MM-dd HH:mm:ss")
public Map<String ,String> DateMapToStringMap(Map<String,Date> sourceMap);
@Test
public void mapMappingTest(){
Map<String,Date> map=new HashMap<>();
map.put("key1",new Date());
map.put("key2",new Date(new Date().getTime()+9800000));
Map<String, String> stringObjectMap = TestMapper.MAPPER.DateMapToStringMap(map);
}4.8 多級嵌套
只需要在mapper接口中定義相關的類型轉換方法即可,list類型也適用
4.8.1 方式1
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
private List<SubUser> user;
}
@Data
publicclass SubUser {
private Integer deleted;
private String name;
private List<SubSubUser> subUser;
}
@Data
publicclass SubSubUser {
private String aaa;
private String ccc;
}
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
private List<SubUserDto> user;
}
@Data
publicclass SubUserDto {
private Integer deleted;
private String name;
private List<SubSubUserDto> subUser;
}
@Data
publicclass SubSubUserDto {
private String aaa;
private String bbb;
}
@Mapper
publicinterface UserMapper {
UserDto userToNewUserDto(User user);
//子集字段相同方法不用編寫會自動生成
//孫子集字段不相同(list會自動讀取此方法生成list)
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}4.8.2 方式2
通過uses配置類型轉換
@Mapper(uses = {TestMapper.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
@Mapper
publicinterface TestMapper {
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}5、獲取 mapper
5.1 通過 Mapper 工廠獲取
我們都是通過 Mappers.getMapper(xxx.class) 的方式來進行對應 Mapper 的獲取。此種方法為通過 Mapper 工廠獲取。
如果是此種方法,約定俗成的是在接口內定義一個接口本身的實例 INSTANCE, 以方便獲取對應的實例。
@Mapper
publicinterface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
// ......
}這樣在調用的時候,我們就不需要在重復的去實例化對象了。類似下面
Target target = SourceMapper.INSTANCE.source2target(source);
5.2 使用依賴注入
對于 Web 開發(fā),依賴注入應該很熟悉。MapSturct 也支持使用依賴注入,同時也推薦使用依賴注入。

@Mapper(componentModel = "spring")
5.3 依賴注入策略
可以選擇是通過構造方法或者屬性注入,默認是屬性注入。
publicenum InjectionStrategy {
/** Annotations are written on the field **/
FIELD,
/** Annotations are written on the constructor **/
CONSTRUCTOR
}類似如此使用
@Mapper(componentModel = "cdi" injectionStrategy = InjectionStrategy.CONSTRUCTOR)
5.4 自定義類型轉換
有時候,在對象轉換的時候可能會出現這樣一個問題,就是源對象中的類型是Boolean類型,而目標對象類型是String類型,這種情況可以通過@Mapper的uses屬性來實現:
@Data
publicclass User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
}
@Data
publicclass UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
}
@Mapper(uses = {BooleanStrFormat.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
publicclass BooleanStrFormat {
public String toStr(Boolean isDisable) {
if (isDisable) {
return"Y";
} else {
return"N";
}
}
public Boolean toBoolean(String str) {
if (str.equals("Y")) {
returntrue;
} else {
returnfalse;
}
}
}要注意的是,如果使用了例如像spring這樣的環(huán)境,Mapper引入uses類實例的方式將是自動注入,那么這個類也應該納入Spring容器
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北滄州");
user.setBirth(new Date());
user.setIsDisable(true);
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToNewUserDto(user);
System.out.println(userDto);
}以上就是Java MapStruct優(yōu)雅地實現對象轉換的詳細內容,更多關于Java MapStruct的資料請關注腳本之家其它相關文章!
相關文章
SpringMVC如何獲取表單數據(radio和checkbox)
這篇文章主要介紹了SpringMVC如何獲取表單數據(radio和checkbox)的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

