按照日常开发习惯,在现在多模块多层级的项目中,应用于应用之间,模块于模块之间数据模型一般都不通用,每层都有自己的数据模型。对于不同领域层使用不同JavaBean对象传输数据,避免相互影响。比如传输对象DTO、业务普通封装对象BO、数据库映射对象DO等。于是在不同层之间进行数据传输时,不可避免地需要将这些对象的属性进行互相转换操作。
目录:
属性映射工具——MapStruct(一)
属性映射工具——MapStruct(二)
属性映射工具——MapStruct(三)
属性映射工具——MapStruct(四)
属性映射工具——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地址
在全栈的道路上,积极向上、成熟稳重、谦虚好学、怀着炽热的心向前方的走得更远。