背景
在代碼開發(fā)中匙头,我們通常都會使用分層架構(gòu)属拾,在分層架構(gòu)中都會使用模型轉(zhuǎn)換,在不同的層使用不同的模型胀蛮。以 DDD 分層模型為例院刁,如下:
模型分類
DO
DataObject,數(shù)據(jù)庫映射對象粪狼,通常用于基礎(chǔ)設(shè)施層退腥,與數(shù)據(jù)庫字段完全對應(yīng)任岸。
Entity
領(lǐng)域?qū)ο螅ǔS糜趹?yīng)用層和領(lǐng)域?qū)樱ㄓ幸恍?DDD 代碼模型在應(yīng)用層使用的是 DTO狡刘,但是基于應(yīng)用層是業(yè)務(wù)編排的職責(zé)享潜,可能會直接使用 Entity 的行為進(jìn)行邏輯編排,那么個人建議應(yīng)用層應(yīng)該使用 Entity)嗅蔬。不只是指實體剑按、還包括值對象。通常是充血模型澜术,包括屬性和行為吕座。
DTO
數(shù)據(jù)傳輸對象,通常用于用戶接口層(用戶接口層瘪板,通常指的是流量入口,包括web 流量漆诽、服務(wù)消費者 RPC 調(diào)用侮攀、消息輸入等)。所以 DTO 通常用于Controller中的輸入輸出參數(shù)厢拭、打到二方包里的輸入輸出參數(shù)(例如兰英,Dubbo 接口的輸入輸出參數(shù))以及消息消費者中的消息模型。
根據(jù)實際需要供鸠,有時候在 web 中畦贸,我們也會使用 vo。
轉(zhuǎn)換器
DTOAssembler
DTO 和 Entity 的轉(zhuǎn)換器
DOConverter
DO 和 Entity 的轉(zhuǎn)換器
現(xiàn)有 Bean 轉(zhuǎn)換工具的比較
目前的轉(zhuǎn)化器有:手寫轉(zhuǎn)換器楞捂、Apache BeanUtils薄坏、Spring BeanUtils、Dozer寨闹、Orika胶坠、ModelMapper、JMapper繁堡、MapStruct 等沈善。其中手寫轉(zhuǎn)換器帶來的人工成本較高,尤其是當(dāng)轉(zhuǎn)換對象屬性較多椭蹄,或者有嵌套屬性時闻牡,費時費力,且容易遺漏出錯绳矩,而且隨著對象的迭代罩润,轉(zhuǎn)換器中的代碼也要變動,所以通常我們還是會采用自動化的轉(zhuǎn)換器埋酬。
根據(jù) 這篇文章 的性能壓測來看哨啃,JMapper 和 MapStruct 的性能最好烧栋,根據(jù)易用性來講 MapStruct 最好,所以我們就使用 MapStruct 來實現(xiàn)轉(zhuǎn)換器拳球。
MapStruct 使用
<properties>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties>
<dependencies>
<!-- mapStruct 核心注解 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- mapStruct 根據(jù)接口生成實現(xiàn)類 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
<!-- mapStruct 支持 lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
最簡示例
DTO & Entity
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String name;
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private String name;
}
轉(zhuǎn)換類
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
測試類
public class Test {
public static void main(String[] args) {
testFromSource();
testToSource();
}
private static void testFromSource(){
Source source = new Source(1, "測試基礎(chǔ)轉(zhuǎn)換");
Target target = Converter.INSTANCE.fromSource(source);
System.out.println(target);
}
private static void testToSource(){
Target target = new Target(1, "測試基礎(chǔ)轉(zhuǎn)換");
Source source = Converter.INSTANCE.toSource(target);
System.out.println(source);
}
}
不同名稱的屬性關(guān)聯(lián)
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String name; // 映射 Target 中的 targetName
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private String targetName; // 映射 Source 中的 name
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
@Mapping(source = "name", target = "targetName")
Target fromSource(Source source);
@InheritInverseConfiguration
Source toSource(Target target);
}
- 使用 @Mapping 手動映射屬性审姓;
- 使用 @InheritInverseConfiguration 表示繼承反方向的配置,例如祝峻,上例中的 toSource 方法的注解可以硬編碼為 @Mapping(source = "targetName", target = "name")魔吐,效果相同
不同類型的屬性關(guān)聯(lián)
@Data
@AllArgsConstructor
public class Source {
private Integer id; // 對應(yīng) Target 的 Long id
private String price; // 對應(yīng) Target 的 Double price
}
@Data
@AllArgsConstructor
public class Target {
private Long id;
private Double price;
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
屬性名相同的屬性如果類型不同,會直接進(jìn)行類型自動轉(zhuǎn)換
內(nèi)嵌屬性關(guān)聯(lián)
@Data
@AllArgsConstructor
public class Source {
private Integer id; // 對應(yīng) Target.TargetId.id
private String name;
}
@Data
@AllArgsConstructor
public class Target {
private TargetId targetId;
private String name;
}
@Data
@AllArgsConstructor
public class TargetId {
private Integer id;
public static TargetId of(Integer id) {
if (id == null) {
throw new RuntimeException("id 不能為 null");
}
return new TargetId(id);
}
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
@Mapping(source = "id", target = "targetId.id")
Target fromSource(Source source);
@InheritInverseConfiguration
Source toSource(Target target);
}
直接在 Mapping 中做屬性嵌套轉(zhuǎn)換
枚舉類關(guān)聯(lián)(屬性抽壤痴摇)
簡單枚舉類
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String type;
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private SimpleEnumType type;
}
public enum SimpleEnumType {
HAHA, HEHE
}
or
public enum SimpleEnumType {
HAHA("HAHA"), HEHE("HEHE");
private String desc;
SimpleEnumType(String desc) {
this.desc = desc;
}
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
簡單枚舉類:單個參數(shù)的枚舉類會自動進(jìn)行類型轉(zhuǎn)換
復(fù)雜枚舉類
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String name; // 映射 Target.targetName
private Integer typeCode; // 映射 Target.type.code
private String typeName; // 映射 Target.type.name
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private String targetName;
private ComplexEnumType type;
}
@Getter
public enum ComplexEnumType {
HAHA(1, "haha"), HEHE(2, "hehe");
private Integer code;
private String name;
ComplexEnumType(Integer code, String name) {
this.code = code;
this.name = name;
}
public static ComplexEnumType getByCode(Integer code) {
return Arrays.stream(values()).filter(x->x.getCode().equals(code)).findFirst().orElse(null);
}
}
Java 表達(dá)式
@Mapper
public interface ConverterWithExpression {
ConverterWithExpression INSTANCE = Mappers.getMapper(ConverterWithExpression.class);
@Mapping(source = "name", target = "targetName")
@Mapping(target = "type", expression = "java(ComplexEnumType.getByCode(source.getTypeCode()))")
Target fromSource(Source source);
@InheritInverseConfiguration
@Mapping(target = "typeCode", source = "type.code")
@Mapping(target = "typeName", source = "type.name")
Source toSource(Target target);
}
- expression:
格式:java(xxx)
酬姆,其中的 xxx 是 Java 語法,其計算出來的值會填充到 target 中奥溺。當(dāng) IDEA 安裝了 MapStruct Support 插件時辞色,在編寫 xxx 時會有提示。上述的 toSource 直接使用了嵌套屬性獲取方式浮定,也可以使用@Mapping(target = "typeName", expression = "java(target.getType().getName())")
這樣的格式相满。- @InheritInverseConfiguration:特殊值特殊處理,比如這里的枚舉相關(guān)值桦卒,其他屬性依舊使用逆轉(zhuǎn)繼承即可立美。
Qualifier 注解
import org.mapstruct.Qualifier;
public class ComplexEnumTypeUtil {
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TypeCode {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TypeName {
}
@TypeCode
public Integer typeCode(ComplexEnumType type) {
return type.getCode();
}
@TypeName
public String typeName(ComplexEnumType type) {
return type.getName();
}
}
@Mapper(uses = ComplexEnumTypeUtil.class)
public interface ConverterWithQualifier {
ConverterWithQualifier INSTANCE = Mappers.getMapper(ConverterWithQualifier.class);
@Mapping(source = "name", target = "targetName")
@Mapping(target = "type", expression = "java(ComplexEnumType.getByCode(source.getTypeCode()))")
Target fromSource(Source source);
@InheritInverseConfiguration
@Mapping(source = "type", target = "typeCode", qualifiedBy = ComplexEnumTypeUtil.TypeCode.class)
@Mapping(source = "type", target = "typeName", qualifiedBy = ComplexEnumTypeUtil.TypeName.class)
Source toSource(Target target);
}
轉(zhuǎn)換類上
@Mapper(uses ={xxx.class}
可以指定使用的轉(zhuǎn)換輔助類
Name 注解
@Mapper
public interface ConverterWithName {
ConverterWithName INSTANCE = Mappers.getMapper(ConverterWithName.class);
@Mapping(source = "name", target = "targetName")
@Mapping(target = "type", expression = "java(ComplexEnumType.getByCode(source.getTypeCode()))")
Target fromSource(Source source);
@InheritInverseConfiguration
@Mapping(source = "type", target = "typeCode", qualifiedByName = "typeCodeUtil")
@Mapping(source = "type", target = "typeName", qualifiedByName = "typeNameUtil")
Source toSource(Target target);
@Named("typeCodeUtil")
default Integer typeCode(ComplexEnumType type) {
return type.getCode();
}
@Named("typeNameUtil")
default String typeName(ComplexEnumType type) {
return type.getName();
}
}
三種方式:Java Expression 最簡單,推薦使用
null 值映射時忽略或者填充默認(rèn)值
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
nullValuePropertyMappingStrategy 的解釋
public enum NullValuePropertyMappingStrategy {
/**
* If a source bean property equals {@code null} the target bean property will be set explicitly to {@code null}.
*/
SET_TO_NULL,
/**
* If a source bean property equals {@code null} the target bean property will be set to its default value.
* <p>
* This means:
* <ol>
* <li>For {@code List} MapStruct generates an {@code ArrayList}</li>
* <li>For {@code Map} a {@code HashMap}</li>
* <li>For arrays an empty array</li>
* <li>For {@code String} {@code ""}</li>
* <li>for primitive / boxed types a representation of {@code 0} or {@code false}</li>
* <li>For all other objects an new instance is created, requiring an empty constructor.</li>
* </ol>
* <p>
* Make sure that a {@link Mapping#defaultValue()} is defined if no empty constructor is available on
* the default value.
*/
SET_TO_DEFAULT,
/**
* If a source bean property equals {@code null} the target bean property will be ignored and retain its
* existing value.
*/
IGNORE;
}
指定不映射某些值
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String name;
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private String name;
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
// name 值不做映射
@Mapping(source = "name", target = "name", ignore = true)
Target fromSource(Source source);
@InheritInverseConfiguration
Source toSource(Target target);
}
通過
@Mapping#ignore=true
來指定不需要做映射的值
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private List<SourceItem> itemList;
}
@Data
@AllArgsConstructor
public class SourceItem {
private String identifier;
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private List<TargetItem> itemList;
}
@Data
@AllArgsConstructor
public class TargetItem {
private String identifier;
}
@Mapper
public interface SourceItemConverter {
SourceItemConverter INSTANCE = Mappers.getMapper(SourceItemConverter.class);
TargetItem fromSourceItem(SourceItem sourceItem);
SourceItem toSourceItem(TargetItem targetItem);
}
@Mapper
public interface SourceConverter {
SourceConverter INSTANCE = Mappers.getMapper(SourceConverter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
public class Test {
public static void main(String[] args) {
testFromSource();
testToSource();
}
private static void testFromSource(){
Target target = SourceConverter.INSTANCE.fromSource(new Source(1, Arrays.asList(new SourceItem("111"), new SourceItem("112"))));
System.out.println(target);
}
private static void testToSource(){
Source source = SourceConverter.INSTANCE.toSource(new Target(2, Arrays.asList(new TargetItem("222"), new TargetItem("223"))));
System.out.println(source);
}
}
各寫各的映射器方灾,應(yīng)用的時候是需要調(diào)用最外層的映射器即可建蹄。
更新目標(biāo)類而不是新建目標(biāo)類
@Data
@AllArgsConstructor
public class Source {
private Integer id;
private String name;
}
@Data
@AllArgsConstructor
public class Target {
private Integer id;
private String name;
}
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
/**
* id 不做更新,其他 source 的屬性更新到 target
* @param source
* @param target
*/
@Mapping(target = "id", ignore = true)
void fromSource(Source source, @MappingTarget Target target);
}
public class Test {
public static void main(String[] args) {
testFromSource();
}
private static void testFromSource(){
Source source = new Source(1, "sourceName");
Target target = new Target(2, "targetName");
Converter.INSTANCE.fromSource(source, target);
System.out.println(target);
}
}
MapStruct 原理
以上述的最簡示例為例裕偿,在項目編譯時洞慎,會把如下轉(zhuǎn)換接口動態(tài)編譯出實現(xiàn)類(底層使用了 APT 技術(shù),APT 示例見這里)击费。實現(xiàn)類與手寫的轉(zhuǎn)換器類似拢蛋,使用構(gòu)造器或者 setter/getter 進(jìn)行操作。
在運行時蔫巩,直接執(zhí)行該實現(xiàn)類谆棱,所以性能與手寫幾乎相同。
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
Target fromSource(Source source);
Source toSource(Target target);
}
其實現(xiàn)類如下:
package xxx; // 與接口所在的包名相同
import javax.annotation.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-01-21T21:19:02+0800",
comments = "version: 1.4.1.Final, compiler: javac, environment: Java 1.8.0_151 (Oracle Corporation)"
)
public class ConverterImpl implements Converter {
@Override
public Target fromSource(Source source) {
if ( source == null ) {
return null;
}
Integer id = null;
String name = null;
id = source.getId();
name = source.getName();
Target target = new Target( id, name );
return target;
}
@Override
public Source toSource(Target target) {
if ( target == null ) {
return null;
}
Integer id = null;
String name = null;
id = target.getId();
name = target.getName();
Source source = new Source( id, name );
return source;
}
}