微服务中VO、DTO、Entity间的相互转换处理

微服务架构下,服务拆分会产生VO、DTO、Entity三类pojo:

  • VO 用于前端接口参数传递,例如用于http接口接收请求参数。可以继承扩展DTO,或者直接使用DTO。
  • DTO 用于rpc接口参数传递。单独定义,或者继承扩展其他rpc接口的DTO。
  • Entity 用于orm映射处理,与表结构一一对应,只在服务内部使用,不能对外。

1. 问题

微服务架构面向不同场景的pojo定义,引入了一个问题:一个前端请求的处理,需要在这三者之间进行转换

  • 请求:VO => DTO => Entity
  • 返回:Entity => DTO => VO

Entity

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName(value = "orders", schema = "crazy1984")
public class Order {
@TableId(type = IdType.AUTO)
private int id;
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
private Date updatedAt;
}

DTO

1
2
3
4
5
6
7
@Data
public class OrderDTO {
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
}

VO

1
2
3
4
5
@Data
public class OrderVO extends OrderDTO{
private String orderTypeName;
private String orderStatusName;
}

2. 手动转换

直接的方法,是通过代码对pojo属性进行逐个拷贝:

1
2
3
4
5
OrderDTO dto = new OrderDTO();
dto.setOrderId(entity.getOrderId());
dto.setOrderType(entity.getOrderType());
dto.setOrderStatus(entity.getOrderStatus());
dto.setCreatedAt(entity.getCreatedAt());

这样的方式太低效,给开发人员增加许多低效的重复劳动,也不易维护(比如新增字段时,所有相关处都要同步修改)。借助IDE工具自动生成代码,可以减少重复工作。

3. 工具类辅助拷贝转换

改进一点的方法是,使用工具类进行同名属性的自动拷贝,例如使用spring的BeanUtils

1
2
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(entity, dto);

这样可以大量减少代码工作,但也有一些不足之处:

  1. 不支持属性名映射,属性名必须完全一致。
  2. 不支持自动类型转换,spring的BeanUtils要求源属性与目标属性的类型是相互assignable的。
  3. 属性拷贝中的属性名匹配、类型检查、写权限检查都是动态判断,有性能损耗。

4. Mapping框架

除了以上方法,还可以用开源的mapping框架,如:

这些框架都支持:

  • 不同属性名的映射
  • 自动类型转换
  • 递归mapping自定义pojo的属性

例如 ModelMapper:

1
2
ModelMapper mapper = new ModelMapper();
OrderDTO dto = modelMapper.map(entity, OrderDTO.class);

这些框架的实现原理,可以归结为以下几点:

  • 基于java反射机制,根据源和目标的class自动进行属性get、set的调用。
  • 基于注解、配置,来进行不同属性名的映射、类型转换。

以上这些框架的性能对比MapStructJMapper性能较好。原因是,它们都使用代码生成将map的过程静态化了,所以实际性能和自己手写get、set一样。在开发过程中,可以通过检查生成的代码来确保mapping转换没有错误,相比ModelMapper的黑盒实现更加可靠。对于grpc协议protobuf对象和entity的互相转换,也能很好的支持。

5. MapStruct的使用

maven安装

在项目pom.xml中增加配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.crazy1984.mapper</groupId>
<artifactId>mapper-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>mapper-demo</artifactId>
<packaging>jar</packaging>
<properties>
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>
<!-- provided -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

上述配置,能让lombok和mapstruct一起工作。

示例:Entity和DTO的双向mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* demo: entity
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
@TableName(value = "transfer_flow", schema = "sale_fund")
public class TransferFlow {

@TableId(type = IdType.AUTO)
private int id;
@TableField(updateStrategy = FieldStrategy.NEVER)
private String flowId;// 插入后不可更新
@TableField(updateStrategy = FieldStrategy.NEVER)
private String orderId;// 插入后不可更新
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
@TableField(updateStrategy = FieldStrategy.NEVER)
private Date createdAt;// 插入后不可更新
private Date updatedAt;
/**
* 非数据库字段:demo注释
*/
@TableField(exist = false)
private String comment;
}

/**
* demo: dto
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
public class TransferFlowDTO implements Serializable {

private static final long serialVersionUID = -1256479071103445488L;
private int id;
private String flowId;
private String orderId;
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
private LocalDate createdAt;
private Date updatedAt;
/**
* demo注释
*/
private String comment;
}

/**
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public interface TransferFlowConverter {

TransferFlowConverter INSTANCE = Mappers.getMapper(TransferFlowConverter.class);

TransferFlowDTO toDTO(TransferFlow entity);

List<TransferFlowDTO> toDTO(List<TransferFlow> lst);

TransferFlow fromDTO(TransferFlowDTO dto);

}

生成的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class TransferFlowConverterImpl implements TransferFlowConverter {

@Override
public TransferFlowDTO toDTO(TransferFlow entity) {
if ( entity == null ) {
return null;
}

TransferFlowDTO transferFlowDTO = new TransferFlowDTO();

transferFlowDTO.setId( entity.getId() );
transferFlowDTO.setFlowId( entity.getFlowId() );
transferFlowDTO.setOrderId( entity.getOrderId() );
transferFlowDTO.setTransferStatus( entity.getTransferStatus() );
if ( entity.getCreatedAt() != null ) {
// Date to LocalDate
transferFlowDTO.setCreatedAt( LocalDateTime.ofInstant( entity.getCreatedAt().toInstant(), ZoneOffset.UTC ).toLocalDate() );
}
transferFlowDTO.setUpdatedAt( entity.getUpdatedAt() );
transferFlowDTO.setComment( entity.getComment() );

return transferFlowDTO;
}

@Override
public List<TransferFlowDTO> toDTO(List<TransferFlow> lst) {
if ( lst == null ) {
return null;
}

List<TransferFlowDTO> list = new ArrayList<TransferFlowDTO>( lst.size() );
for ( TransferFlow transferFlow : lst ) {
list.add( toDTO( transferFlow ) );
}

return list;
}

@Override
public TransferFlow fromDTO(TransferFlowDTO dto) {
if ( dto == null ) {
return null;
}

TransferFlow transferFlow = new TransferFlow();

transferFlow.setId( dto.getId() );
transferFlow.setFlowId( dto.getFlowId() );
transferFlow.setOrderId( dto.getOrderId() );
transferFlow.setTransferStatus( dto.getTransferStatus() );
if ( dto.getCreatedAt() != null ) {
// LocalDate to Date
transferFlow.setCreatedAt( Date.from( dto.getCreatedAt().atStartOfDay( ZoneOffset.UTC ).toInstant() ) );
}
transferFlow.setUpdatedAt( dto.getUpdatedAt() );
transferFlow.setComment( dto.getComment() );

return transferFlow;
}
}

示例:带递归和额外信息补充的mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Data
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor
@Builder
public class TaskListDTO implements Serializable {
private static final long serialVersionUID = -2125356351064981209L;

private String taskId;
private String taskName;
private Integer taskState;
private String taskStateText;
private List<ApproverlDTO> approverlDTOList;

}

@Data
public class TaskList {
private String taskId;
private String taskName;
private Integer taskState;
private Integer rejectFlag;
private List<ApproverList> approverList;
}

/**
* 带递归和额外信息补充的mapping
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public abstract class TaskListConverter {

public static TaskListConverter INSTANCE = Mappers.getMapper(TaskListConverter.class);

@Mapping(source = "approverList", target = "approverlDTOList")
public abstract TaskListDTO toDTO(TaskList task);

public abstract List<TaskListDTO> toDTO(List<TaskList> lst);

// 递归转换主pojo内的
public abstract ApproverlDTO toDTO(ApproverList approver);

// 主pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(TaskList task,
@MappingTarget TaskListDTO.TaskListDTOBuilder target) {
if (task == null || target == null) {
return;
}
// 审批状态,1:未处理,2:已处理
if (task.getTaskState() != null) {
target.taskStateText(task.getTaskState() == 1 ? "未处理" : "已处理");
}
}

// 内部pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(ApproverList approver,
@MappingTarget ApproverlDTO.ApproverlDTOBuilder target) {
if (approver == null || target == null) {
return;
}
// 审批结果 1:同意 2:驳回
if (approver.getApproveResult() != null) {
target.approveResultText(approver.getApproveResult() == 1 ? "同意" : "驳回");
}
}

}

示例:字符串List自动转json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Data
public class CustomerNoteDTO implements Serializable {
private static final long serialVersionUID = 1783984956317260001L;
private Long id;
private Long customerId;
private List<String> images;
}

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName(value = "customer_notes", schema = "crm")
public class CustomerNotes {

@TableId(type = IdType.AUTO)
private Long id;
private Long customerId;
private String images;
}

/**
* @author zhangjy
* @date 2020-03-25
*/
@ApplicationScope
@Component
@Slf4j
public class SpringMapper {
@Autowired
private Gson gson;

String map(List<String> values) {
try {
return gson.toJson(values);
} catch (Exception e) {
log.warn("list to json", e);
return null;
}
}

List<String> map(String json) {
try {
final TypeToken<?> tt = TypeToken.getParameterized(List.class, String.class);
return gson.fromJson(json, tt.getType());
} catch (Exception e) {
log.warn("json to list: {}", json, e);
return null;
}
}

}

/**
* DTO vs entity 转换器
*
* @author zhangjy
* @date 2020-03-23
*/
@Mapper(componentModel = "spring", uses = SpringMapper.class)
public interface CustomerNoteConverter {
CustomerNoteConverter INSTANCE = Mappers.getMapper(CustomerNoteConverter.class);

CustomerNotes fromCDTO(CustomerNoteDTO noteDTO);

CustomerNoteDTO toDTO(CustomerNotes note);

}

生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
public class CustomerNoteConverterImpl implements CustomerNoteConverter {

@Autowired
private SpringMapper springMapper;

@Override
public CustomerNotes fromCDTO(CustomerNoteDTO noteDTO) {
if ( noteDTO == null ) {
return null;
}

CustomerNotes customerNotes = new CustomerNotes();

customerNotes.setId( noteDTO.getId() );
customerNotes.setCustomerId( noteDTO.getCustomerId() );
customerNotes.setImages( springMapper.map( noteDTO.getImages() ) );

return customerNotes;
}

@Override
public CustomerNoteDTO toDTO(CustomerNotes note) {
if ( note == null ) {
return null;
}

CustomerNoteDTO customerNoteDTO = new CustomerNoteDTO();

customerNoteDTO.setId( note.getId() );
customerNoteDTO.setCustomerId( note.getCustomerId() );
customerNoteDTO.setImages( springMapper.map( note.getImages() ) );

return customerNoteDTO;
}
}

6. 参考

  1. Java Bean Copy框架性能对比
  2. 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
  3. Performance of Java Mapping Frameworks
  4. MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
  5. MapStruct Installation
  6. JMapper Philosophy