目錄:
一、背景
按照日常開發習慣,在現在多模塊多層級的項目中,應用於應用之間,模塊於模塊之間數據模型一般都不通用,每層都有自己的數據模型。對於不同領域層使用不同JavaBean對象傳輸數據,避免相互影響。比如傳輸對象DTO、業務普通封裝對象BO、數據庫映射對象DO等。於是在不同層之間進行數據傳輸時,不可避免地需要將這些對象的屬性進行互相轉換操作。
常見的轉換方式有:
-
- 調用getter/setter方法進行屬性賦值:一大堆‘巨簡單’的代碼,不美觀;
- 調用BeanUtil.copyPropertie進行反射屬性賦值:坑巨多,比如sources與target寫反,難以定位某個字段在哪里進行的賦值,不利於debug,同時因為用到反射,導致性能也不佳。
而本文介紹的MapStruct規避了上述的缺點。
二、簡介
MapStruct是一個代碼生成器,它基於約定優於配置的方法極大地簡化了Java bean類型之間映射的實現。
通過上面的介紹我們應該能夠理解到這么幾點,首先它是一個代碼生成器,就是用來幫開發者自動生成代碼的工具,只需要通過簡單的代碼就可以實現原來手工編寫的樣板代碼,因為它采用約定大於配置的設計思想,所以開發者只需要掌握簡單的代碼編寫就可以了。也就是說人家框架幫你自動生成了原先手工編寫的代碼,但實際上那些手工編寫的代碼還是存在的,只不過你沒有編寫,框架幫你自動生成了而已。這其實也回到框架的本質,事情還是那些事,就看你來做,還是它來做,它如果多做,你就少做,甚至可以不做。這里提到的它指的是各種框架,它的本質就是幫開發者做了一些事情。
優點:
-
-
- 通過使用普通方法調用而不是反射來快速執行
- 速度快:由於MapStruct不采用所謂的反射機制,而是像開發者原來手工逐個賦值那樣編碼,所以沒有額外的性能損失,跟你自己寫的代碼是一樣的。
- 編譯時類型安全性
- 展示生成報告:在生成代碼過程中如果發現映射不完整、不正確會立即輸出日志。
-
工作原理(使用java apt技術,該技術也用於lombok的實現)
-
- 在代碼編譯時會觸發MapStruct插件運行
- 當MapStruct運起來之后會掃描它自己特定注解的類
- 解析類中的方法按照自己的策略在項目編譯目錄(build)下生成實現類,如果生成過程中出現異常則會輸出日志,並中斷當前整個項目編譯工作。
三、簡單實踐
項目背景:Spring Boot+Maven項目,UserDAO——數據庫映射對象,UserDTO——數據傳輸對象,
3.1依賴
maven項目 pom.xml
<dependencies> <!--MapStruct--> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.2.0.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.2.0.Final</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.2.0.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
gradle項目
dependencies { implementation "org.mapstruct:mapstruct:${mapstructVersion}" annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" // If you are using mapstruct in test code testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" }
3.2 UserDAO
import lombok.Data; import java.sql.Timestamp; @Data public class UserDAO { // 主鍵 private Long id; // 姓名 private String name; // 性別 private Integer sex; // 描述 private String remark; // 創建時間 private Timestamp createTime; }
3.3 UserDTO
import com.gougou.mapstruct.enums.SexEnum; import lombok.Data; import java.io.Serializable; /** * dto:網絡傳輸對象 */ @Data public class UserDTO implements Serializable { private static final long serialVersionUID = -2767215193284523251L; // 主鍵 private String id; // 姓名 private String name; // 性別 private SexEnum sex; // 描述 private String desc; // 創建時間 private String createTime; }
3.4 SexEnum
import lombok.Getter; import lombok.Setter; public enum SexEnum { man(1, "男"), woman(0, "女"); @Setter @Getter private Integer code; @Setter @Getter private String name; SexEnum(Integer code, String name) { this.code = code; this.name = name; } public static SexEnum of(Integer code){ for(SexEnum sexEnum:SexEnum.values()){ if(sexEnum.code.equals(code)){ return sexEnum; } } return null; } }
3.5 transfer
Mapper
即映射器, 一般來說就是寫 xxxMapper
接口。 當然, 不一定是以 Mapper
結尾的。 只是官方是這么寫的。
簡單的映射(字段和類型都匹配), 只有一個要求, 在接口上寫 @Mapper
注解即可。 然后方法上入參對應要被轉化的對象, 返回值對應轉化后的對象, 方法名稱可任意。在實現類的時候, 如果屬性名稱相同, 則會進行對應的轉化(隱式轉化)。屬性名不相同, 可通過 @Mapping 注解進行指定轉化。否則沒有值。
import com.gougou.mapstruct.dao.UserDAO; import com.gougou.mapstruct.dto.UserDTO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.util.List; /** * UserDTO與UserDAO之間的轉化類 */ @Mapper(uses = { SexEnumIntegerMapper.class, StringTimestampMapper.class }) public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mappings({ @Mapping(source = "sex", target = "sex",qualifiedByName = {"SexEnumIntegerMapper", "integerBySexEnum"}), @Mapping(source = "desc", target = "remark"), @Mapping(source = "createTime", target = "createTime",qualifiedByName = {"StringTimestampMapper", "timestampByString"}) }) UserDAO toDO(UserDTO userDTO); List<UserDAO> toDOs(List<UserDTO> userDTOList); @Mappings({ @Mapping(source = "sex", target = "sex",qualifiedByName = {"SexEnumIntegerMapper", "sexEnumByInteger"}), @Mapping(source = "remark", target = "desc"), @Mapping(source = "createTime", target = "createTime",qualifiedByName = {"StringTimestampMapper", "stringByTimestamp"}) }) UserDTO toDTO(UserDAO userDAO); List<UserDTO> toDTOs(List<UserDAO> userDAOList); }
import com.gougou.mapstruct.enums.SexEnum; import org.mapstruct.Named; /** * SexEnum與Integer之間的轉化 */ @Named("SexEnumIntegerMapper") public class SexEnumIntegerMapper { @Named("sexEnumByInteger") public SexEnum sexEnumByInteger(Integer intParam){ return SexEnum.of(intParam); } @Named("integerBySexEnum") public Integer integerBySexEnum(SexEnum sexEnum){ return sexEnum.getCode(); } }
import org.mapstruct.Named; import java.sql.Timestamp; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; /** * String與timestamp之間的轉化 */ @Named("StringTimestampMapper") public class StringTimestampMapper { private static final String dateFormatStr = "yyyy-MM-dd HH:mm:ss"; @Named("timestampByString") public Timestamp timestampByString(String strParam) { SimpleDateFormat sf = new SimpleDateFormat(dateFormatStr); java.util.Date date = null; try { date = sf.parse(strParam); } catch (ParseException e) { e.printStackTrace(); } return new java.sql.Timestamp(date.getTime()); } @Named("stringByTimestamp") public String stringByTimestamp(Timestamp timestamp) { DateFormat df = new SimpleDateFormat(dateFormatStr); return df.format(timestamp); } }
3.6 測試
import com.gougou.mapstruct.dao.UserDAO; import com.gougou.mapstruct.dto.UserDTO; import com.gougou.mapstruct.enums.SexEnum; import com.gougou.mapstruct.transfer.UserMapper; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.List; public class MainTest { private UserDTO userDTO = new UserDTO(); private List<UserDTO> userDTOList = new ArrayList<>(2); private UserDAO userDAO = new UserDAO(); private List<UserDAO> userDAOList = new ArrayList<>(2); @Before public void initUserDTO() { userDTO.setId("1122"); userDTO.setDesc("這是張三"); userDTO.setName("張三"); userDTO.setSex(SexEnum.man); userDTO.setCreateTime("2020-05-06 19:00:00"); userDAO.setId(3377L); userDAO.setRemark("這是李梅梅"); userDAO.setName("李梅梅"); userDAO.setSex(0); userDAO.setCreateTime(new java.sql.Timestamp(1588765009399L)); UserDTO userDTO2 = new UserDTO(); userDTO2.setId("2211"); userDTO2.setDesc("這是張三2"); userDTO2.setName("張三2"); userDTO2.setSex(SexEnum.man); userDTO2.setCreateTime("2020-05-06 19:49:00"); UserDAO userDAO2 = new UserDAO(); userDAO2.setId(7733L); userDAO2.setRemark("這是李梅梅2"); userDAO2.setName("李梅梅2"); userDAO2.setSex(0); userDAO2.setCreateTime(new java.sql.Timestamp(1588766094618L)); userDAOList.add(userDAO); userDAOList.add(userDAO2); userDTOList.add(userDTO); userDTOList.add(userDTO2); } /** * DAO -> DTO */ @Test public void test1() { UserDTO userDTO1 = UserMapper.INSTANCE.toDTO(userDAO); // UserDTO(id=3377, name=李梅梅, sex=woman, desc=這是李梅梅, createTime=2020-05-06 19:36:49) System.out.println(userDTO1.toString()); } /** * DTO -> DAO */ @Test public void test2() { UserDAO userDAO1 = UserMapper.INSTANCE.toDO(userDTO); // UserDAO(id=1122, name=張三, sex=1, remark=這是張三, createTime=2020-05-06 19:00:00.0) System.out.println(userDAO1.toString()); } /** * List<DAO> -> List<DTO> */ @Test public void test3() { List<UserDTO> userDTOList1 = UserMapper.INSTANCE.toDTOs(userDAOList); /** * UserDTO(id=3377, name=李梅梅, sex=woman, desc=這是李梅梅, createTime=2020-05-06 19:36:49) * UserDTO(id=7733, name=李梅梅2, sex=woman, desc=這是李梅梅2, createTime=2020-05-06 19:54:54) */ userDTOList1.stream().forEach(x -> System.out.println(x)); } /** * List<DTO> -> List<DAO> */ @Test public void test4() { List<UserDAO> userDAOList1 = UserMapper.INSTANCE.toDOs(userDTOList); /** * UserDAO(id=1122, name=張三, sex=1, remark=這是張三, createTime=2020-05-06 19:00:00.0)——————這里的格式是TimeStamp的toString方法默認的實現,與本次轉換無關 * UserDAO(id=2211, name=張三2, sex=1, remark=這是張三2, createTime=2020-05-06 19:49:00.0) */ userDAOList1.stream().forEach(x -> System.out.println(x)); userDAOList1.stream().forEach(x -> System.out.println(x.getCreateTime())); } }
3.7 編譯后的代碼
通過 MapStruct
來生成的代碼, 其類似於人手寫。 速度上可以得到保證。本例子中生成的代碼可以在編譯后在 target/generated-sources/annotations 里看到。如下。所以說MapStruct生成的代碼易於Debug,在使用反射的時候,如果出現了問題, 很多時候是很難找到是什么原因的,因為不直觀。
import com.gougou.mapstruct.dao.UserDAO; import com.gougou.mapstruct.dto.UserDTO; import java.util.ArrayList; import java.util.List; import javax.annotation.Generated; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2020-05-06T19:40:20+0800", comments = "version: 1.2.0.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) public class UserMapperImpl implements UserMapper { private final SexEnumIntegerMapper sexEnumIntegerMapper = new SexEnumIntegerMapper(); private final StringTimestampMapper stringTimestampMapper = new StringTimestampMapper(); @Override public UserDAO toDO(UserDTO userDTO) { if ( userDTO == null ) { return null; } UserDAO userDAO = new UserDAO(); userDAO.setRemark( userDTO.getDesc() ); userDAO.setCreateTime( stringTimestampMapper.timestampByString( userDTO.getCreateTime() ) ); userDAO.setSex( sexEnumIntegerMapper.integerBySexEnum( userDTO.getSex() ) ); if ( userDTO.getId() != null ) { userDAO.setId( Long.parseLong( userDTO.getId() ) ); } userDAO.setName( userDTO.getName() ); return userDAO; } @Override public List<UserDAO> toDOs(List<UserDTO> userDTOList) { if ( userDTOList == null ) { return null; } List<UserDAO> list = new ArrayList<UserDAO>( userDTOList.size() ); for ( UserDTO userDTO : userDTOList ) { list.add( toDO( userDTO ) ); } return list; } @Override public UserDTO toDTO(UserDAO userDAO) { if ( userDAO == null ) { return null; } UserDTO userDTO = new UserDTO(); userDTO.setCreateTime( stringTimestampMapper.stringByTimestamp( userDAO.getCreateTime() ) ); userDTO.setSex( sexEnumIntegerMapper.sexEnumByInteger( userDAO.getSex() ) ); userDTO.setDesc( userDAO.getRemark() ); if ( userDAO.getId() != null ) { userDTO.setId( String.valueOf( userDAO.getId() ) ); } userDTO.setName( userDAO.getName() ); return userDTO; } @Override public List<UserDTO> toDTOs(List<UserDAO> userDAOList) { if ( userDAOList == null ) { return null; } List<UserDTO> list = new ArrayList<UserDTO>( userDAOList.size() ); for ( UserDAO userDAO : userDAOList ) { list.add( toDTO( userDAO ) ); } return list; } }
四、其他
附上兩個地址 MapStruct官網 MapStruct Git地址