分层设计中上下游间接口调用时,如果接口方法参数很多最好的方式是包装一个对象。但每层都有自己专用的对象,比如vo、bo、po等。在对象转换时需要写大量的getter和setter方法或是用deepCopy简化代码,但无论哪种都少不了大量的代码。
这里我们会引用一个叫mapStruct的三方框架,它可以实现负责的对象间deepCopy操作,笔者试用了一段时间后已经集成到生产环境中了,这个框架不夸张的说可以100%覆盖日常开发需要。原理也非常简单,通过注释方式在编译期间生成源码。
本章代码任务:我们会在 https://blog.51cto.com/arch/5379866 这篇文档上的代码再进行优化,以一个简单的示例展示mapStruct的便利性。然后再以附加章节的方式扩展其它高级使用功能。
一、集成
集成也非常简单需引入两个jar包和一个编译插件(这个插件主要是解决和lombok插件的冲突的)。在maven中添加依赖,这里笔者用的是版本是 1.4.2.Final 。
<!--mapStruct依赖,代码拷贝-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>compile</scope>
</dependency>
添加插件解决与lombok冲突(原因是lombok也是同样的原理实现的),因为mapStruct会用到对象的setter和getter方法,所以同时使用时要保证lombok先执行编译,配置如下:
<properties>
<java.version>1.8</java.version>
<maven.compile.source>1.8</maven.compile.source>
<maven.compile.target>1.8</maven.compile.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.20</lombok.version>
<mapstruct.version>1.4.2.Final</mapstruct.version>
<lombok.mapstruct.binding.version>0.1.0</lombok.mapstruct.binding.version>
</properties>
<!--处理lombok和mapstruft 加载顺序的问题 防止在生成的实体没有属性-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- additional annotation processor required as of Lombok 1.18.16 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok.mapstruct.binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
二、简单使用
这里我们只需要修改【base-grpc-framework-core】模块。修改后的工程文件如下,文件名称带颜色的是本次修改的文件(git版本控制的默认色)。
2.1、创建请求包装对象
新创建包装对象,源码如下,源码中的的@Data和@JsonNaming是lombok的注解:
- @Data:自动生成getter和setter方法;
- @JsonNaming:会把java中的骆峰参数名转换为_连接的参数名,比如bizId会转成biz_id;
/**
* @Title: com.zd.baseframework.core.controller.core.request.SystemLogRequest
* @Description 查询请求参数包装对象
* @author liudong
* @date 2022/6/14 1:54 PM
*/
@Data
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SystemLogQueryRequest {
private String bizId;
private Long userId;
private String code;
}
2.2、创建对象转换类
下面就是mapstruct的用法,httpReqObjectToGrpcReqObject()方法表示把SystemLogRequest转换成CreateSysRecordRequest对象。有@Mapper注解的参数解释如下:
- collectionMappingStrategy:集合处理方式;
- nullValueCheckStrategy:转换前是否做为空判断
- imports:手动工具类,需用expression表达式人工处理,后续会讲到;
- uses:自动工具类,mapstruct会根据类型自动匹配相应方法;
/**
* @Title: com.zd.baseframework.core.controller.core.translator.SystemLogTranslator
* @Description http层与grpc层对象转换工具
* @author liudong
* @date 2022/6/15 8:52 PM
*/
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
imports = {MapStructMapperUtil.class},
uses = BaseMapStructMapper.class)
public interface SystemLogTranslator {
SystemLogTranslator INSTANCE = Mappers.getMapper(SystemLogTranslator.class);
/*查询请求参数转换*/
ListSystemLogRequest toGrpcQueryReqObject(SystemLogQueryRequest var);
}
编译后在target/generated-sources/annotations/会找到如下源码:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-06-14T14:25:42+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_144 (Oracle Corporation)"
)
public class SystemLogTranslatorImpl implements SystemLogTranslator {
@Override
public ListSystemLogRequest toGrpcQueryReqObject(SystemLogQueryRequest var) {
if ( var == null ) {
return null;
}
Builder createSysRecordRequest = ListSystemLogRequest.newBuilder();
if ( var.getBizId() != null ) {
createSysRecordRequest.setBizId( BaseMapStructMapper.javaStringToStringValue( var.getBizId() ) );
}
if ( var.getUserId() != null ) {
createSysRecordRequest.setUserId( BaseMapStructMapper.longToInt64( var.getUserId() ) );
}
if ( var.getTrackUid() != null ) {
createSysRecordRequest.setTrackUid( BaseMapStructMapper.javaStringToStringValue( var.getTrackUid() ) );
}
if ( var.getCode() != null ) {
createSysRecordRequest.setCode( BaseMapStructMapper.javaStringToStringValue( var.getCode() ) );
}
if ( var.getCustomCode() != null ) {
createSysRecordRequest.setCustomCode( BaseMapStructMapper.javaStringToStringValue( var.getCustomCode() ) );
}
return createSysRecordRequest.build();
}
}
2.3、实现包含类型请求和自动对象转换
修改 com.zd.baseframework.core.controller.core.SystemLogController.java,添加如下方法,源代码的第5行和行为新增的内容。
@PostMapping("/v1/list_systemlog")
public BaseResponse listSystemLog(@RequestBody SystemLogQueryRequest systemLogRequest){
try{
//构建查询参数
ListSystemLogRequest listSystemLogRequest = SystemLogTranslator.INSTANCE.toGrpcQueryReqObject(systemLogRequest);
ListSystemLogResponse listSystemLogResponse = iSysRecordServiceBlockingStub.listSystemLogByCondition(listSystemLogRequest);
List<SystemLogVo> vos = null;
return BaseResponse.success(vos);
}catch(StatusRuntimeException e){
log.error("ErrorCode : " + e.getStatus().getCode());
return BaseResponse.error(e.getMessage());
}catch(Exception e){
e.printStackTrace();
return BaseResponse.error(e.getMessage());
}
}
最后启动测试下。
三、辅助工具类源码
3.1、BaseMapStructMapper
注意这个类需要用@Mapper注解一下,而手动的工具类则不需要。
@Mapper
public class BaseMapStructMapper {
/*Boolean---Integer*/
public static int boolToInteger(Boolean source) {
if (source) {
return 1;
}
return 0;
}
public static Boolean integerToBool(Integer source) {
if (source != null && source.intValue() != 0) {
return true;
}
return false;
}
/*java.lang.String --- com.google.protobuf.StringValue*/
public static String stringValueToString(StringValue value) {
return value.getValue();
}
public static StringValue javaStringToStringValue(String value) {
return StringValue.of(value);
}
/*java.lang.Integer --- com.google.protobuf.Int32Value*/
public static Int32Value integerToInt32(Integer value) {
return Int32Value.of(value);
}
public static Integer int32ToInteger(Int32Value value) {
return value.getValue();
}
/*java.lang.Long --- com.google.protobuf.Int64Value*/
public static Int64Value longToInt64(Long value) {
return Int64Value.of(value);
}
public static Long int64ToLong(Int64Value value) {
return value.getValue();
}
/*java.lang.Float --- com.google.protobuf.FloatValue*/
public static FloatValue floatToFloatValue(Float value) {
return FloatValue.of(value);
}
public static Float floatValueToFloat(FloatValue value) {
return value.getValue();
}
/*java.util.Date---com.google.protobuf.util.Timestamps*/
public static Timestamp javaDateToGoogleTimestamp(Date date) {
if (date == null) {
return null;
}
return Timestamps.fromMillis(date.getTime());
}
public static Date googleTimestampToJavaDate(Timestamp timestamp) {
if (timestamp == null || timestamp.getSeconds()<=0) {
return null;
}
return new Date(timestamp.getSeconds() * 1000);
}
public static Date localDateTimeToDate(LocalDateTime time) {
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = time.atZone(zoneId);
Date date = Date.from(zdt.toInstant());
return date;
}
/*java.util.LocalDateTime---com.google.protobuf.util.Timestamps*/
public static Timestamp localDateTimetoGoogleTimestamp(LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
return Timestamps.fromMillis(localDateTimeToDate(localDateTime).getTime());
// return Timestamp.newBuilder().setSeconds(localDateTime.getSecond()).setNanos(localDateTime.getNano()).build();
// return Timestamps.fromMillis(localDateTime.getSecond());
}
/*
*
* 2022.04.18 update by shaocx timestamp.getSeconds()<= 0 -> timestamp.getSeconds()< 0 ,解决1970-01-01 入参 返回null问题
* */
public static LocalDateTime googleTimestampToLocalDateTime(Timestamp timestamp) {
if (timestamp == null || timestamp.getSeconds()< 0) {
return null;
}
Date date = new Date(timestamp.getSeconds() * 1000);
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
return localDateTime;
}
/*java.util.LocalDate---com.google.protobuf.util.Timestamps*/
public static Timestamp localDatetoGoogleTimestamp(LocalDate localDate) {
if (localDate == null) {
return null;
}
LocalDateTime localDateTime = localDate.atStartOfDay();
return BaseMapStructMapper.localDateTimetoGoogleTimestamp(localDateTime);
}
public static LocalDate googleTimestampToLocalDate(Timestamp timestamp) {
if (timestamp == null || timestamp.getSeconds()<=0) {
return null;
}
return BaseMapStructMapper.googleTimestampToLocalDateTime(timestamp).toLocalDate();
}
/*jsonString---com.google.protobuf.util.Value*/
public static String valueToJson(Value value) {
String json = null;
try {
json = JsonFormat.printer().includingDefaultValueFields().print(value);
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
return json;
}
public static Value jsonToValue(String json) {
Value.Builder builder = Value.newBuilder();
try {
JsonFormat.parser().merge(json, builder);
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
return builder.build();
}
/*jsonString---com.google.protobuf.util.Struct*/
public static String structToJson(Struct struct) {
String json = null;
try {
json = JsonFormat.printer().includingDefaultValueFields().print(struct);
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
return json;
}
public static Struct jsonToStruct(String json) {
Struct.Builder builder = Struct.newBuilder();
try {
JsonFormat.parser().merge(json, builder);
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
return builder.build();
}
//LocalDateTime -> String
public static String localDateTimeToStr(LocalDateTime time, String format) {
if (null == time) {
return "";
}
DateTimeFormatter df = DateTimeFormatter.ofPattern(format);
String timeStr = df.format(time);
return timeStr;
}
//String -> LocalDateTime
public static LocalDateTime strToLocalDateTime(String timeStr, String format) {
if (StringUtils.isEmpty(timeStr)) {
return null;
}
DateTimeFormatter df = DateTimeFormatter.ofPattern(format);
LocalDateTime ldt = LocalDateTime.parse(timeStr, df);
return ldt;
}
}
3.2、MapStructMapperUtil
public class MapStructMapperUtil {
//localdatetime to date and time
public static String localDateToDate (LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return dtf2.format(localDateTime);
}
public static String localDateToTime (LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("HH:mm:ss");
return dtf2.format(localDateTime);
}
//localdatetime to date
public static LocalDateTime dateToLocalDate (Date date) {
if (date == null) {
return null;
}
Instant it = date.toInstant();
ZoneId zid = ZoneId.systemDefault();
return LocalDateTime.ofInstant(it, zid);
}
public Date localDateTimeToDate(LocalDateTime localDateTime) {
ZoneId zone = ZoneId.systemDefault();
Instant instant = localDateTime.atZone(zone).toInstant();
return Date.from(instant);
}
}
后面章节,笔者会对mapstruct展开讨论,如果能用好可以省去好多工作量,然虽然只是一个对象deepCopy工具,但足够复杂,还是需要下点精力研究和试验的。
笔者要使用这个插件时就踩过好多坑,所以学习没有捷径。