微服务架构下,服务拆分会产生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);
|
这样可以大量减少代码工作,但也有一些不足之处:
- 不支持属性名映射,属性名必须完全一致。
- 不支持自动类型转换,spring的
BeanUtils
要求源属性与目标属性的类型是相互assignable的。
- 属性拷贝中的属性名匹配、类型检查、写权限检查都是动态判断,有性能损耗。
4. Mapping框架
除了以上方法,还可以用开源的mapping框架,如:
这些框架都支持:
- 不同属性名的映射
- 自动类型转换
- 递归mapping自定义pojo的属性
例如 ModelMapper:
1 2
| ModelMapper mapper = new ModelMapper(); OrderDTO dto = modelMapper.map(entity, OrderDTO.class);
|
这些框架的实现原理,可以归结为以下几点:
- 基于java反射机制,根据源和目标的class自动进行属性get、set的调用。
- 基于注解、配置,来进行不同属性名的映射、类型转换。
以上这些框架的性能对比,MapStruct
和JMapper
性能较好。原因是,它们都使用代码生成将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> <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
|
@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;
private int transferStatus; @TableField(updateStrategy = FieldStrategy.NEVER) private Date createdAt; private Date updatedAt;
@TableField(exist = false) private String comment; }
@Data public class TransferFlowDTO implements Serializable {
private static final long serialVersionUID = -1256479071103445488L; private int id; private String flowId; private String orderId;
private int transferStatus; private LocalDate createdAt; private Date updatedAt;
private String comment; }
@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 ) { 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 ) { 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; }
@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);
public abstract ApproverlDTO toDTO(ApproverList approver);
@AfterMapping protected void calledWithSourceAndTarget(TaskList task, @MappingTarget TaskListDTO.TaskListDTOBuilder target) { if (task == null || target == null) { return; } if (task.getTaskState() != null) { target.taskStateText(task.getTaskState() == 1 ? "未处理" : "已处理"); } }
@AfterMapping protected void calledWithSourceAndTarget(ApproverList approver, @MappingTarget ApproverlDTO.ApproverlDTOBuilder target) { if (approver == null || target == null) { return; } 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; }
@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; } }
}
@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. 参考
- Java Bean Copy框架性能对比
- 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
- Performance of Java Mapping Frameworks
- MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
- MapStruct Installation
- JMapper Philosophy