Java实体映射工具:MapStruct
????????MapStruct版本:1.3.1.Final。
????????當我們需要進行Java Model之間的拷貝時,或者項目要求Java Model需要嚴格區分為數據對象(DO)、數據傳輸對象(DTO)和展示對象(VO)的時候,我們就不得不把一個實體中的屬性映射到另一個實體中。最簡單的做法就是寫一個工具類,進行不斷的getter / setter,這樣雖然能完成要求但卻寫了很多冗余代碼,維護起來相當惡心。所以這個時候就需要一款能自動映射實體屬性的工具了。
????????Spring自帶的BeanUtils工具類算是一款,但是它卻不能自定義映射規則;ModelMapper也是一款映射工具框架,雖然它可以自定義映射規則,但寫法上卻復雜一些。而MapStruct作為一款優秀的Java實體映射工具來說,它也能夠自定義映射規則,并且是通過注解的方式來實現的,源和目標看得很清楚明白。不同于BeanUtils和ModelMapper是通過反射在運行期生成代碼從而導致性能不高,MapStruct是在編譯期生成實現類映射代碼,生成的代碼就是普通的getter / setter代碼,和原生使用的性能相差不大。
1 簡單使用
????????首先需要引入的依賴如下所示:
<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>????????除此之外如果使用的IDE是idea的話,還可以下載MapStruct的插件:
????????該插件可以動態地提示當前沒有進行映射的字段,以及其他一些對MapStruct的支持(和Lombok不同,該插件不是必須安裝的)。
????????接著準備兩個Java Model如下所示,一個DO,一個VO:
import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor;import java.io.Serializable;@Data @NoArgsConstructor @AllArgsConstructor @Builder public class PersonDO implements Serializable {private static final long serialVersionUID = -3483764417202514211L;private Long personId;private String name;private Integer sex;private Integer age;private String address; } import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor;import java.io.Serializable;@Data @NoArgsConstructor @AllArgsConstructor @Builder public class PersonVO implements Serializable {private static final long serialVersionUID = 7827081422917080855L;private Long id;private String name;private String sex;private Integer age;private String address; }????????其中用到了Lombok的注解來簡化編程,詳見我的另一篇文章《Lombok概述》。在完成了上述準備之后,就可以進行MapStruct的開發使用了。
????????我們現在是要把PersonDO轉成PersonVO,首先需要寫一個接口,如下所示:
import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers;@Mapper public interface PersonMapper {PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);PersonVO personDO2VO(PersonDO personDO); }????????之后需要進行打包編譯,在對屬性進行變動后都要進行打包編譯,以此來生成新的實現類。MapStruct并不能及時地反映出屬性的變更,比方說接口提供方變更了Model的屬性,而調用方只有等到打包編譯的時候才能提示出錯誤,這也許是MapStruct為數不多的缺點了吧,但介于MapStruct是在編譯期生成映射代碼的機制,這點也無可厚非,只要多加留意即可。
????????生成的實現類如下所示:
import javax.annotation.Generated;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T18:46:28+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();personVO.setName( personDO.getName() );if ( personDO.getSex() != null ) {personVO.setSex( String.valueOf( personDO.getSex() ) );}personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );return personVO;} }????????由上可以看到,生成的實現類就是通過getter / setter方法來實現的,不損失性能。由此可見我們只需要寫一個接口,并定義好映射規則的方法就行了,不需要再寫其他的代碼。相應的測試代碼如下所示:
PersonDO personDO = PersonDO.builder().personId(1L).name("Robert Hou").sex(1).age(24).address("Beijing").build(); PersonVO personVO = PersonMapper.INSTANCE.personDO2VO(personDO); System.out.println(personVO);????????運行結果如下:
PersonVO(id=null, name=Robert Hou, sex=1, age=24, address=Beijing)2 Spring注入
????????除了上節的在PersonMapper映射接口中聲明INSTANCE的方式來進行調用之外,MapStruct也同時支持Spring的依賴注入機制,如下所示:
import org.mapstruct.Mapper;@Mapper(componentModel = "spring") public interface PersonMapper {PersonVO personDO2VO(PersonDO personDO); }????????只需要在@Mapper注解中添加componentModel配置項,并設置為“spring”即可。生成的實現類如下:
import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T18:47:41+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();personVO.setName( personDO.getName() );if ( personDO.getSex() != null ) {personVO.setSex( String.valueOf( personDO.getSex() ) );}personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );return personVO;} }????????可以看到是對該類添加了@Component注解,注冊成為了一個Bean,之后就可以通過@Autowired注解來進行調用了。相應的測試代碼如下所示:
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest class TestApplicationTests {@Autowiredprivate PersonMapper personMapper;@Testvoid mapperTest() {PersonDO personDO = PersonDO.builder().personId(1L).name("Robert Hou").sex(1).age(24).address("Beijing").build();PersonVO personVO = personMapper.personDO2VO(personDO);System.out.println(personVO);} }3 自定義映射
????????由上面的映射規則可知,MapStruct默認只會對同名的屬性進行映射,對于不同名的屬性則不會映射,如PersonDO的personId屬性就沒有映射到PersonVO的id屬性中。這種情況下就需要手動選擇映射屬性,如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id")})PersonVO personDO2VO(PersonDO personDO); }????????如果映射屬性只有一個的話,則可以不用使用@Mappings注解而只使用@Mapping注解也都是可以的。這回再來看一下生成的實現類代碼:
import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T18:48:47+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );if ( personDO.getSex() != null ) {personVO.setSex( String.valueOf( personDO.getSex() ) );}personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );return personVO;} }????????在第20行可以看到對Id屬性也進行了映射。
4 映射集合
????????MapStruct也支持對集合的映射,寫起來也相當簡單。比如說我們現在需要將一個PersonDO的List集合轉換成PersonVO的List集合,MapStruct的寫法如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;import java.util.List;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id")})PersonVO personDO2VO(PersonDO personDO);List<PersonVO> personDOs2VOs(List<PersonDO> personDOList); }????????可以看到只需要在第15行添加一個對集合進行映射的方法就行了,并不需要顯示地調用personDO2VO方法,這完全得益于MapStruct的自動類型探測,生成的實現類代碼如下所示:
import java.util.ArrayList; import java.util.List; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T18:51:17+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );if ( personDO.getSex() != null ) {personVO.setSex( String.valueOf( personDO.getSex() ) );}personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );return personVO;}@Overridepublic List<PersonVO> personDOs2VOs(List<PersonDO> personDOList) {if ( personDOList == null ) {return null;}List<PersonVO> list = new ArrayList<PersonVO>( personDOList.size() );for ( PersonDO personDO : personDOList ) {list.add( personDO2VO( personDO ) );}return list;} }????????由上面的第41行代碼可以看到personDOs2VOs方法實現了對personDO2VO方法的調用。
5 忽略映射
????????在某些情況下我們不需要對某些字段進行映射,MapStruct也是支持的。比方說現在不需要對address字段進行映射,那么寫法如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(target = "address", ignore = true)})PersonVO personDO2VO(PersonDO personDO); }????????如第10行代碼所示,將ignore選項值賦為true即可。這樣的話生成的實現類就不會對address字段進行映射了。
6 多參數映射
????????有些時候我們的入參不止一個,而是有多個,這樣的情況MapStruct也是支持的,如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personDO1.personId", target = "id"),@Mapping(source = "personDO1.name", target = "name"),@Mapping(source = "personDO1.sex", target = "sex"),@Mapping(source = "personDO2.age", target = "age"),@Mapping(source = "personDO2.address", target = "address")})PersonVO personDO2VO(PersonDO personDO1, PersonDO personDO2); }????????入參有兩個PersonDO,將第一個PersonDO的personId、name和sex屬性賦值給PersonVO,而將第二個PersonDO的age和address屬性賦值給PersonVO。需要注意的是如果多個入參的屬性名相同,則需要起別名來進行區別,否則編譯會報錯。生成的實現類代碼如下:
import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T20:58:00+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO1, PersonDO personDO2) {if ( personDO1 == null && personDO2 == null ) {return null;}PersonVO personVO = new PersonVO();if ( personDO1 != null ) {personVO.setName( personDO1.getName() );personVO.setId( personDO1.getPersonId() );if ( personDO1.getSex() != null ) {personVO.setSex( String.valueOf( personDO1.getSex() ) );}}if ( personDO2 != null ) {personVO.setAddress( personDO2.getAddress() );personVO.setAge( personDO2.getAge() );}return personVO;} }7 映射規則
????????對于一些簡單的類型轉換,例如int轉String,boolean轉Boolean等等,MapStruct都可以自動完成。在上面的例子中也有所體現,例如PersonDO的sex字段是Integer類型的,而PersonVO的sex字段是String類型的,MapStruct通過String.valueOf的方式完成了轉換,不需要使用者操心。
????????而對于Date和String類型之間的相互轉換,MapStruct也是支持的,這里我們往PersonDO中新添加一個Date類型的birthday屬性,在PersonVO中新添加一個String類型的birthday屬性,兩者進行映射。然后MapStruct的接口類代碼如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")})PersonVO personDO2VO(PersonDO personDO); }????????通過dateFormat配置項可以配置日期的格式,生成的實現類如下:
import java.text.SimpleDateFormat; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T19:17:11+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();if ( personDO.getBirthday() != null ) {personVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( personDO.getBirthday() ) );}personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );if ( personDO.getSex() != null ) {personVO.setSex( String.valueOf( personDO.getSex() ) );}personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );return personVO;} }????????由上面第22行代碼可以看到,MapStruct是通過SimpleDateFormat的方式完成的轉換。另外MapStruct還提供了一個numberFormat關于數據精度的配置,讀者可自行嘗試,這里就不再演示了。
????????有些情況下可能需要完成更加復雜、更加定制化的映射規則,這時候就需要我們自己來寫映射代碼了,這在MapStruct中實現也非常容易?,F在我們不需要從PersonDO中的sex字段映射到PersonVO中的sex字段,而是通過身份證號的倒數第二位來進行判斷,如果為奇數,則為男,反之則為女。繼續往PersonDO中新添加一個String類型的idNumber屬性,然后需要寫一個轉換的工具方法:
public class PersonUtils {private PersonUtils() {}public static String getSex(String idNumber) {if (idNumber == null) {return null;}//截取身份證倒數第二位數字String in = idNumber.substring(idNumber.length() - 2, idNumber.length() - 1);int i = Integer.parseInt(in);//如果為奇數,則是男,反之則為女return (i & 1) == 1 ? "男" : "女";} }????????MapStruct的接口類改造如下:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring", imports = PersonUtils.class) public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss"),@Mapping(target = "sex", expression = "java(PersonUtils.getSex(personDO.getIdNumber()))")})PersonVO personDO2VO(PersonDO personDO); }????????首先需要在類上的@Mapper注解中加上imports選項來引入這個工具類,然后通過在@Mapping注解中添加expression選項來完成調用,如第11行所示。需要注意的是expression配置項中首先需要寫“java()”,然后再在其中寫具體的調用代碼。生成的實現類代碼如下所示:
import java.text.SimpleDateFormat; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T20:04:35+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();if ( personDO.getBirthday() != null ) {personVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( personDO.getBirthday() ) );}personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );personVO.setAge( personDO.getAge() );personVO.setAddress( personDO.getAddress() );personVO.setSex( PersonUtils.getSex(personDO.getIdNumber()) );return personVO;} }????????如上第29行所示,通過調用PersonUtils的工具類方法,完成了sex字段的映射。同時如果使用的Java版本是8或者以上,那么可以使用從Java 8開始支持的接口中的default語句來簡化編程,不用再單獨寫一個工具類了。比如說現在我們需要將PersonDO中的age字段的值加上10,然后賦值給PersonVO中的age字段。如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring", imports = PersonUtils.class) public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss"),@Mapping(target = "sex", expression = "java(PersonUtils.getSex(personDO.getIdNumber()))")})PersonVO personDO2VO(PersonDO personDO);default Integer addAge(Integer age) {return age + 10;} }????????不需要顯式地進行調用,MapStruct會自己推測出來。生成的實現類代碼如下:
import java.text.SimpleDateFormat; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T20:32:12+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();if ( personDO.getBirthday() != null ) {personVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( personDO.getBirthday() ) );}personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );personVO.setAge( addAge( personDO.getAge() ) );personVO.setAddress( personDO.getAddress() );personVO.setSex( PersonUtils.getSex(personDO.getIdNumber()) );return personVO;} }????????從第26行代碼可以看到,顯式調用了addAge方法完成了轉換的操作。但是有一點需要注意,MapStruct是通過入參和出參的類型進行判斷,從而進行賦值的。拿這個例子來說,MapStruct會把所有的源Model類型為Integer和目標Model類型為Integer的屬性都添加上這個addAge方法,這往往會造成把不想要轉換的屬性也給轉換了。在這種情況下就不能使用default語句了,而轉而使用上文所說的expression配置項,寫一個工具類方法,顯示地對屬性進行調用即可,這點需要多加留意。
8 默認值和常量
????????同時MapStruct也支持默認值和常量,如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings;@Mapper(componentModel = "spring") public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss"),@Mapping(target = "sex", constant = "男"),@Mapping(target = "address", defaultValue = "中國"),})PersonVO personDO2VO(PersonDO personDO); }????????如上第11行和第12行代碼所示,sex字段常量賦值為“男”,address字段使用了默認值,即該字段如果值為null的情況下賦值為“中國”,不為null則照常進行賦值。生成的實現類代碼如下:
import java.text.SimpleDateFormat; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T21:20:11+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {if ( personDO == null ) {return null;}PersonVO personVO = new PersonVO();if ( personDO.getBirthday() != null ) {personVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( personDO.getBirthday() ) );}personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );personVO.setAge( personDO.getAge() );if ( personDO.getAddress() != null ) {personVO.setAddress( personDO.getAddress() );}else {personVO.setAddress( "中國" );}personVO.setSex( "男" );return personVO;} }9 空Model返回
????????通過上面的例子可以看到,當入參Model本身為null的時候,則直接返回null賦值給出參Model。有些情況下我們想返回一個空屬性的Model而不是null,這樣可以不用再在后續進行空指針判斷。如下所示:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.NullValueMappingStrategy;@Mapper(componentModel = "spring", nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) public interface PersonMapper {@Mappings({@Mapping(source = "personId", target = "id"),@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss"),@Mapping(target = "sex", constant = "男"),@Mapping(target = "address", defaultValue = "中國"),})PersonVO personDO2VO(PersonDO personDO); }????????在類上的@Mapper注解中加上nullValueMappingStrategy配置項,并賦值為NullValueMappingStrategy.RETURN_DEFAULT即可,默認為RETURN_NULL,即返回null。生成的實現類代碼如下:
import java.text.SimpleDateFormat; import javax.annotation.Generated; import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2020-02-08T21:24:37+0800",comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) @Component public class PersonMapperImpl implements PersonMapper {@Overridepublic PersonVO personDO2VO(PersonDO personDO) {PersonVO personVO = new PersonVO();if ( personDO != null ) {if ( personDO.getBirthday() != null ) {personVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( personDO.getBirthday() ) );}personVO.setId( personDO.getPersonId() );personVO.setName( personDO.getName() );personVO.setAge( personDO.getAge() );if ( personDO.getAddress() != null ) {personVO.setAddress( personDO.getAddress() );}else {personVO.setAddress( "中國" );}}personVO.setSex( "男" );return personVO;} }????????可以看到如果PersonDO為null的話,會返回一個空屬性的PersonVO(賦值為常量的情況除外),而不是直接返回null。
總結
以上是生活随笔為你收集整理的Java实体映射工具:MapStruct的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: KALI利用MS17-010漏洞入侵
- 下一篇: Java Lambda 映射 map