属性映射工具——MapStruct(一)
目录:
一、背景
按照日常开发习惯,在现在多模块多层级的项目中,应用于应用之间,模块于模块之间数据模型一般都不通用,每层都有自己的数据模型。对于不同领域层使用不同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地址