乖乖,咱不用BeanUtil.copy了,咱試試這款神級工具(超詳細)

码农Academy發表於2024-03-05

引言

在現代Java應用程式開發中,處理物件之間的對映是一個常見而且必不可少的任務。隨著專案規模的增長,手動編寫繁瑣的對映程式碼不僅耗時且容易出錯,因此開發者們一直在尋找更高效的解決方案。比如基於Dozer封裝的或者Spring自帶的BeanUtil.copyProperties對應物件之間的屬性複製。但是Dozer採用執行時對映的方式,透過反射在執行時動態生成對映程式碼。這意味著在每次對映時都需要進行反射操作,Dozer在處理複雜對映時可能需要額外的配置和自定義轉換器,可能導致一定的效能開銷,尤其在大型專案中可能表現不佳。另外在處理處理複雜對映(例如欄位名稱不一致,某些欄位不需要對映)時可能需要額外的配置和自定義轉換器,使用起來並不是那麼的便捷。那麼此時MapStruct變應用而生,成為簡化Java Bean對映的利器。

MapStruct是一款基於註解和編譯時程式碼生成的工具,旨在簡化Java Bean之間的對映過程。透過在編譯時生成高效的對映程式碼,避免了執行時的效能開銷,使得對映過程更加高效。MapStruct不僅消除了手寫對映程式碼的痛苦,還提供了效能優勢。它支援在Java Bean之間進行對映,並透過使用註解標記對映方法和類,提供了一種宣告性的方式定義對映規則,簡化了對映程式碼的編寫。使得開發者能夠專注於業務邏輯而不必過多關注物件之間的轉換。並且它還支援自定義轉換器和表示式,適用於處理各種複雜的對映場景。

下面我們就開始介紹如何使用MapStruct來高效的完成物件之間的對映。

如何MapStruct使用

使用MapStruct進行Java Bean對映通常包括幾個基本步驟,包括專案配置、註解標記、自定義轉換器等。以下是詳細的使用步驟:

1、依賴

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

同時在 pom.xml 需要正確配置MapStruct的依賴和註解處理器外掛。例如:

<build>  
    <plugins>        
	    <plugin>            
		    <groupId>org.apache.maven.plugins</groupId>  
            <artifactId>maven-compiler-plugin</artifactId>  
            <configuration>                 
                <annotationProcessorPaths>                   
	                <path>                        
						<groupId>org.mapstruct</groupId>  
						<artifactId>mapstruct-processor</artifactId>  
						<version>1.5.5.Final</version>  
	                </path>                    
	                <path>                        
						<groupId>org.projectlombok</groupId>  
						<artifactId>lombok</artifactId>  
						<version>1.18.22</version>  
	                </path>                    
	                <path>                        
		                <groupId>org.projectlombok</groupId>  
						<artifactId>lombok-mapstruct-binding</artifactId>  
						<version>0.2.0</version>  
					</path>                
				</annotationProcessorPaths>            
			</configuration>        
		</plugin>    
	</plugins>
</build>

當然如果你同時使用了lombok,也需要同時配置lombok編譯生成程式碼的外掛。

2、建立對映介面

建立一個Java介面,並使用@Mapper註解標記它。例如:

@Mapper
public interface MyMapper {
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

    TargetObject sourceToTarget(SourceObject source);
    // 定義其他對映方法
}

上述程式碼定義了一個對映介面MyMapper,其中有一個對映方法sourceToTarget用於將SourceObject對映為TargetObjectINSTANCE欄位用於獲取對映器的例項。

此時我們編譯專案之後,可以看見生成的MyMapper實現類中的程式碼:

@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  
  
    TargetObject targetObject = new TargetObject();  
  
    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    targetObject.setSex( source.getSex() );  
  
    return targetObject;  
}

這樣就省去了我們自己手寫兩個物件之間的欄位對映,避免了大量的重複工作,大大增加了開發效率,其次也是最重要的一點就是我們可以很直觀的看見兩個物件之間的欄位對映關係,不像Dozer那樣每次基於反射區實現對映,我們無法看見兩邊的欄位的對映,出現問題後不方便排查,功能上不可控。

很重要的一點提示:我們要養成在寫完一個對映方法後,要養成一定一定提前編譯看一下生成的實現類方法是否正確,同時也看看是否存在欄位對映關係設定錯誤導致編譯不透過。

3、對映介面使用

在業務程式碼或者其他程式碼方法中,我們可以直接使用MyConverter.INSTANCE.sourceToTarget(source)進行sourcetarget之間的轉換。

TargetObject handleObject(SourceObject source){  
    return MyConverter.INSTANCE.sourceToTarget(source);  
}

怎麼樣?是不是很簡單。接下來讓我們繼續介紹MapStruct的詳細功能,揭開它神秘的面紗。。。。。

MapStruct常用註解

瞭解MapStruct的註解及其屬性是非常重要的,因為它們定義了對映規則和行為。以下是MapStruct中常用的註解及其屬性:

1.@Mapper

用於標記一個介面或抽象類,用於定義物件之間的對映規則。它有多個屬性可以配置對映器的功能。以下是 @Mapper 註解的一些常用屬性:

1.1 componentModel

指定生成的對映器例項的元件模型,以便與應用框架整合。他有"default"(預設值)、"cdi"、"spring"等可選值(具體參考MappingConstants.ComponentModel)。我們著重介紹一下default以及spring:

  • default:MapStruct的預設元件模型
    在預設模式下,MapStruct 會生成一個無引數的建構函式的對映器例項。對映器例項的建立和管理由 MapStruct自動處理。例項通常透過 Mappers.getMapper(Class)獲取。適用於簡單的對映場景,無需額外的依賴注入或容器管理。

  • spring:使用Spring Framework的元件模型
    在 Spring 模式下,MapStruct 會生成一個使用 @Component 註解標記的對映器例項,從而允許透過 Spring 的 IoC 容器進行管理和依賴注入。適用於 Spring 框架中的應用,可以利用 Spring 的依賴注入功能。稍後我們會介紹這種模型的使用,也是我們日常使用SpringBoot開發時用的比較多的模型。比如上例中,我們使用spring的模型,則生成的程式碼:

@Component  
public class MySpringMapperImpl implements MySpringMapper {  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
        targetObject.setSex( source.getSex() );  
  
        return targetObject;  
    }  
}

可以看見實現類中自動加上了@Component,注入到Spring的容器中管理。

  • cdi:使用 Contexts and Dependency Injection (CDI) 的元件模型。
    在 CDI 模式下,MapStruct 會生成一個使用 @Dependent 註解標記的對映器例項,允許透過 CDI 容器進行管理和依賴注入。適用於Java EEJakarta EE中使用 CDI 的應用,可以利用 CDI 容器進行管理。

其餘的大家感興趣的可以去閱讀原始碼,平時使用不多,這裡就不過多介紹了。

1.2 uses

指定對映器使用的自定義轉換器。自定義轉換器是在對映過程中呼叫的方法,用於處理特定型別之間的自定義對映邏輯。如果我們兩個物件之間有一個欄位的屬性值需要特殊處理之後在進行對映,即需要加上一些轉換邏輯,我們就可以自定義一個轉換器,然後在對映器中使用轉換器中的方法。例如:SoureObject中的有一個列舉值,但是轉換到TargetObject中時需要轉換為具體的說明,那麼此時我們就可以使用自定義轉換器。

我們自定義一個轉換器,並且定義一個轉換方法:

public class MyConverter {  
  
    @Named("convertSexDesc")  
    public String convertSexDesc(Integer sex){  
        return SexEnum.descOfCode(sex);  
    }  
}

然後再對映器MyMapper中使用uses指定轉換器,同時使用@Mapping註解指定兩個欄位的對映規則:

@Mapper(uses = {MyConverter.class})  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);  
}

編譯後可以看見實現類中生成的程式碼:

public class MyMapperImpl implements MyMapper {  
  
    private final MyConverter myConverter = new MyConverter();  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
  
        return targetObject;  
    }  
}

當然假如你的轉換器或者轉換方法,是你這個對映器獨有,其他對映器不會使用這個轉換方法,那麼你可以直接在MyMapper中定義一個default的轉換方法,就不必使用uses引入轉換器:

@Mapper  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);  
  
  
    @Named("convertSexDesc")  
    default String convertSexDesc(Integer sex){  
        return SexEnum.descOfCode(sex);  
    }  
}

編譯後生成的實現類中,直接可以呼叫到這個方法:

public class MyMapperImpl implements MyMapper {  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        targetObject.setSex( convertSexDesc( source.getSex() ) );  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
  
        return targetObject;  
    }  
}

在Java中,介面可以包含預設方法(Default Methods)。預設方法是在介面中提供一個預設的實現,這樣在介面的實現類中就不需要強制性地實現該方法了。預設方法使用關鍵字 default 進行宣告。

1.3 imports

匯入其他類的全限定名,使其在生成的對映器介面中可見。比如我們可以匯入其他的工具類去處理我們的欄位,例如:StringUtils, CollectionUtilsMapUtils,或者一些列舉類等。同常運用@Mapping中的expression上。

@Mapper(imports = {StringUtils.class, SexEnum.class})  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex",  expression = "java(SexEnum.descOfCode(source.getSex()))")  
    TargetObject sourceToTarget(SourceObject source);
}    

編譯後生成的實現類中直接importimports中定義的類:

import com.springboot.code.mapstruct.SexEnum;
import org.springframework.util.StringUtils;

public class MyMapperImpl implements MyMapper {  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
  
        targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  
  
        return targetObject;  
    }  
}

當然我們也可以不使用imports去匯入其他的類,那我們在使用這些類的方法時,必須寫上他們的全路徑:

@Mapper  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex",  expression = "java(com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()))")  
    TargetObject sourceToTarget(SourceObject source);
}    

編譯後生成的實現類中,就不會import類了:

public class MyMapperImpl implements MyMapper {  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
  
        targetObject.setSex( com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()) );  
  
        return targetObject;  
    }  
}
1.4 config

config 屬性允許你指定一個對映器配置類,該配置類用於提供全域性的配置選項。透過配置類,你可以定義一些全域性行為,例如處理 null 值的策略、對映器名稱、對映器元件模型等。

我們使用@MapperConfig定義一個對映器配置類 MyMapperConfig

@MapperConfig(  
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,  
        componentModel = "default",  
        uses = MyConverter.class,  
        unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然後再MyMapper中指定config:

@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);
 }   

我們可以集中管理對映器的一些全域性行為,而不需要在每個對映器中重複配置。
在實際應用中,你可以根據專案需求定義不同的對映器配置類,用於管理不同的全域性配置選項。這有助於提高程式碼的組織性和可維護性。

1.5 nullValueCheckStrategy

用於指定對映器對源物件欄位的null值進行檢查的策略。檢查策略列舉類NullValueCheckStrategy值如下:

  • ALWAYS:始終對源值進行NULL檢查。
    生成的實現類中,都是源值進行判NULL
@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  
  
    TargetObject targetObject = new TargetObject();  
  
    if ( source.getSex() != null ) {  
        targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  
    }  
    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  
  
    return targetObject;  
}
  • ON_IMPLICIT_CONVERSION:不檢查NULL值,直接將源值賦值給目標值

除了上述的屬性值之外,還有一些其他的屬性值,例如:

  • unmappedSourcePolicy: 未對映源物件欄位的處理策略。
  • unmappedTargetPolicy: 未對映目標物件欄位的處理策略。
    可選值:ReportingPolicy.IGNORE(忽略未對映欄位,預設)、ReportingPolicy.WARN(警告)、ReportingPolicy.ERROR(丟擲錯誤)。

以及其他的一些屬性值,如果需要用到的同學,可以看一下原始碼中的介紹,這裡就不過多敘述了。

2.@MapperConfig

註解用於定義對映器配置類,它允許在一個單獨的配置類中集中管理對映器的全域性配置選項。可以將一些全域性的配置選項集中在一個配置類中,使得對映器的配置更為清晰和可維護。在實際應用中,可以根據需要定義不同的對映器配置類,以便在不同的場景中使用。配置類可以在對映器中透過@Mapperconfig屬性引入。它大部分的屬性值跟@Mapper一致。

@MapperConfig(  
        nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION,  
        componentModel = "default",  
        uses = MyConverter.class,  
        unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然後再MyMapper中指定config:

@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);
 } 

3.@Mapping

用於自定義對映器方法中的對映規則。它允許你指定源物件和目標物件之間欄位的對映關係。

3.1 sourcetarget:
  • source 含義: 源物件欄位的名稱或表示式。
  • target 含義: 目標物件欄位的名稱。
@Mapping(target = "sourceField", source = "sourceField")  
TargetObject sourceToTarget(SourceObject source);

或者使用表示式的方式:

@Mapping(expression = "java(source.getSourceField())", target = "targetField")
TargetObject sourceToTarget(SourceObject source);
3.2 qualifiedByNamequalifiedBy:
  • qualifiedByName: 指定使用自定義轉換器方法進行對映。

定義一個轉換器MyNameConverter:

public class MyNameConverter {  
    
    @Named("convertUserName")  
    public String convertUserName(String userName){  
        return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  
    }  
}

使用自定義轉換器的方法:

@Mapper( uses = {MyNameConverter.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {  
  
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  
  
    @Mapping(target = "userName", source = "userName", qualifiedByName = "convertUserName")  
    TargetObject sourceToTarget(SourceObject source);
  • qualifiedBy: 指定使用基於@qualifier註解的轉換方法

先定義一個基於@qualifier(mapstruct包下)的作用於轉換器類上的註解@StrConverter:

@Qualifier  
@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.CLASS)  
public @interface StrConverter {  
}

再定義一個基於@qualifier(mapstruct包下)的作用於轉換器方法上的註解@NameUpper:

@Qualifier  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.CLASS)  
public @interface NameUpper {  
}

最後定義一個自定義轉換器MyNameConverter:

@StrConverter  
public class MyNameConverter {  
  
  
    @NameUpper  
    public String convertUserName(String userName){  
        return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  
    }  
}

然後我們在@Mappinbg中透過使用:

@Mapper(uses = {MyNameConverter.class} ,nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {
	@Mapping(target = "userName", source = "userName", qualifiedBy = NameUpper.class) 
	TargetObject sourceToTarget(SourceObject source);
}

最終兩種方式編譯後的結果是一致的:

public class MyMapperImpl implements MyMapper {  
  
    private final MyNameConverter myNameConverter = new MyNameConverter();  
  
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  
  
        TargetObject targetObject = new TargetObject();  
  
        if ( source.getUserName() != null ) {  
            targetObject.setUserName( myNameConverter.convertUserName( source.getUserName() ) );  
        }  
        if ( source.getUserId() != null ) {  
            targetObject.setUserId( source.getUserId() );  
        }  
  
        return targetObject;  
    }  
}

以上基於qualifiedBy的使用示例參考自@Qualifier原始碼文件。

3.3 ignore

是否忽略某欄位的對映。為true時忽略。

@Mapping(target = "sex", source = "sex", ignore = true)
TargetObject sourceToTarget(SourceObject source);

編譯後實現類中不會對這個欄位進行賦值:

@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  
  
    TargetObject targetObject = new TargetObject();  
  
    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  
  
    return targetObject;  
}
3.4 defaultExpression

指定預設表示式,當源物件欄位為 null 時使用。

@Mapping(target = "sex", source = "sex", defaultExpression = "java(SexEnum.MAN.desc)")
TargetObject sourceToTarget(SourceObject source);

編譯後實現類:

 if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
  }  
  else {  
    targetObject.setSex( SexEnum.MAN.desc );  
  } 

defaultExpression不能與expression,defaultValue,constant一起使用。

3.5 defaultValue

指定預設值,當源物件欄位為 null 時使用。

@Mapping(target = "sex", source = "sex", defaultValue = "男人")  
TargetObject sourceToTarget(SourceObject source);

編譯後:

if ( source.getSex() != null ) {  
    targetObject.setSex( String.valueOf( source.getSex() ) );  
}  
else {  
    targetObject.setSex( "男人" );  
}

defaultValue不能與expression,defaultExpression,constant一起使用。

3.6 constant

將目標物件的欄位設定為該常量。不從源物件中對映值。

@Mapping(target = "source", constant = "API")  
TargetObject sourceToTarget(SourceObject source);

編譯後:

targetObject.setSource( "API" );

constant不能與defaultExpression,expression,defaultValue,constant, source一起使用。

3.7 expression

透過表示式完成對映。要基於該字串設定指定的目標屬性。目前,Java 是唯一受支援的“表示式語言”,表示式必須使用以下格式以 Java 表示式的形式給出:java(<EXPRESSION>)

@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
TargetObject sourceToTarget(SourceObject source);

編譯後:

targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

expression不能與source, defaultValue, defaultExpression, qualifiedBy, qualifiedByName 以及constant 一起使用

3.8 dateFormat

指定日期格式化模式,僅適用於日期型別的欄位。可以實現String型別時間和Date相互轉換,基於SimpleDateFormat實現。

@Data  
public class TargetObject {
	private String createTime;  
  
	private Date loginDate;
}

@Data  
public class SourceObject {
	private Date createTime;  
  
	private String loginDate;
}


@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")  
@Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd")  
TargetObject sourceToTarget(SourceObject source);

編譯後:

if ( source.getCreateTime() != null ) {  
    targetObject.setCreateTime( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( source.getCreateTime() ) );  
}  
try {  
    if ( source.getLoginDate() != null ) {  
        targetObject.setLoginDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( source.getLoginDate() ) );  
    }  
}  
catch ( ParseException e ) {  
    throw new RuntimeException( e );  
}
3.9 numberFormat

指定數值格式化格式,僅適用Number型別的欄位。可以實現String型別數值與Number相互轉換,基於DecimalFormat實現。

@Data  
public class TargetObject {
	private double amountDouble;  
	  
	private String amountStr;
}

@Data  
public class SourceObject {
	private String amountStr;  
  
	private double amountDouble;
}

@Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00")  
@Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
TargetObject sourceToTarget(SourceObject source);

編譯後:

try {  
    if ( source.getAmountStr() != null ) {  
        targetObject.setAmountDouble( new DecimalFormat( "#,###.00" ).parse( source.getAmountStr() ).doubleValue() );  
    }  
}  
catch ( ParseException e ) {  
    throw new RuntimeException( e );  
}  
targetObject.setAmountStr( new DecimalFormat( "#,###.00" ).format( source.getAmountDouble() ) );

還有其他的屬性,這裡就不過多敘述了,有興趣或者需要的可以閱讀原始碼。

4.@Mappings

包含多個@Mapping註解,將多個欄位對映規則組合在一起,使程式碼更清晰。

@Mappings({  
        @Mapping(target = "source", constant = "API"),  
        @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  
        @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),  
        @Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd"),  
        @Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00"),  
        @Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
})  
TargetObject sourceToTarget(SourceObject source);

5.@Named:

用於標記自定義轉換器或者對映器中的某個方法的名稱。一般配合qualifiedByName 使用:

/**
* 標記對映方法名稱
*/
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  

/**
* 標記轉換器方法名稱
*/
@Named("convertSexDesc")  
default String convertSexDesc(Integer sex){  
    return SexEnum.descOfCode(sex);  
}

我們在定義自己的轉換器方法時,最好把方法都加上@Named的註解標記你的方法名稱,否則如果後續程式碼中再寫一個同型別的不同方法名的轉換方法時編譯報錯:不明確的對映方法。

image.png

6. @IterableMapping

用於集合對映,定義集合元素的對映規則。其中一些屬性例如:`qualifiedByName`,`qualifiedBy`以及`dateFormat`,`numberFormat`參考`@Mapping`中的用法。
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  
  
@IterableMapping(qualifiedByName = "sourceToTarget")  
List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

編譯後的實現類程式碼:

@Override  
public List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList) {  
    if ( sourceObjectList == null ) {  
        return null;  
    }  
  
    List<TargetObject> list = new ArrayList<TargetObject>( sourceObjectList.size() );  
    for ( SourceObject sourceObject : sourceObjectList ) {  
        list.add( sourceToTarget( sourceObject ) );  
    }  
  
    return list;  
}

可看出它內部迴圈呼叫sourceToTarget的方法完成list的轉換。

需要特別注意,在寫集合型別的轉換時一定要配合IterableMappingqualifiedByNameNamed使用,如果不使用@IterableMapping中顯示宣告迴圈使用的方法時,它的內部會重新生成一個對映方法去使用。這樣會在開發過程中出現一些莫名其妙的忽然就不好使的錯誤。。。。。

    @Named("sourceToTarget")  
    TargetObject sourceToTarget(SourceObject source);  
  
    @Named("sourceToTarget2")  
    TargetObject sourceToTarget2(SourceObject source);  
  
//    @IterableMapping(qualifiedByName = "sourceToTarget")  
    List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

編譯後,實現類中程式碼可以看出並沒有使用以上兩個方法,而是重新生成的:

image.png
image.png

7.@MappingTarget

標記在對映方法的目標物件引數上,允許在對映方法中修改目標物件的屬性。當目標物件已經建立了,此時可以將目標物件也當做引數傳遞到對映器方法中。

@Mapping(target = "source", constant = "API")  
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
@Named("sourceToTarget3")
void sourceToTarget3(@MappingTarget TargetObject targetObject, SourceObject source);

編譯後實現類程式碼:

@Override  
public void sourceToTarget3(TargetObject targetObject, SourceObject source) {  
    if ( source == null ) {  
        return;  
    }  
  
    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
  
    targetObject.setSource( "API" );  
    targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  
}

8.@InheritConfiguration

它用於在對映介面中引用另一個對映方法的配置。主要用於減少程式碼重複,提高對映方法的可維護性。

	@Mappings({  
            @Mapping(target = "source", constant = "API"),  
            @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")
    })  
    @Named("sourceToTarget")  
    TargetObject sourceToTarget(SourceObject source);

	@InheritConfiguration(name = "sourceToTarget")  
	@Named("sourceToTarget2")  
	TargetObject sourceToTarget2(SourceObject source);

	@InheritConfiguration(name = "sourceToTarget")  
	void sourceToTarget4(@MappingTarget TargetObject targetObject, SourceObject source);

sourceToTarget2sourceToTarget4就可以直接繼承使用sourceToTarget的規則了。避免了再次定義一份相同的規則。

9. @BeanMapping

用於配置對映方法級別的註解,它允許在單個對映方法上指定一些特定的配置。例如忽略某些屬性、配置對映條件等(開始我們在@Mapper中定義)。它提供了一種在方法級別自定義對映行為的方式。

@BeanMapping(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
@Named("sourceToTarget2")  
TargetObject sourceToTarget2(SourceObject source);

編譯後實現的程式碼:

@Override  
public TargetObject sourceToTarget2(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  
  
    TargetObject targetObject = new TargetObject();  
  
    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  
  
    return targetObject;  
}

校驗了源物件值的null

10.@ValueMapping

用於自定義列舉型別或其他可對映型別的值對映。該註解允許在列舉型別對映時,定義自定義的值對映規則,使得在對映中可以轉換不同的列舉值。他只有兩個屬性值:

  • source:只能取值:列舉值名稱,MappingConstants.NULLMappingConstants.ANY_REMAININGMappingConstants.ANY_UNMAPPED
  • target: 只能取值:列舉值名稱MappingConstants.NULLMappingConstants.ANY_UNMAPPED
  public enum OrderType { RETAIL, B2B, C2C, EXTRA, STANDARD, NORMAL }
 
  public enum ExternalOrderType { RETAIL, B2B, SPECIAL, DEFAULT }

  @ValueMappings({  
        @ValueMapping(target = "SPECIAL", source = "EXTRA"),  
        @ValueMapping(target = "DEFAULT", source = "STANDARD"),  
        @ValueMapping(target = "DEFAULT", source = "NORMAL"),  
        @ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = "C2C" )  
})  
ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType);	

編譯後實現類程式碼:

@Override  
public ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType) {  
    if ( orderType == null ) {  
        return null;  
    }  
  
    ExternalOrderTypeEnum externalOrderTypeEnum;  
  
    switch ( orderType ) {  
        case EXTRA: externalOrderTypeEnum = ExternalOrderTypeEnum.SPECIAL;  
        break;  
        case STANDARD: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  
        break;  
        case NORMAL: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  
        break;  
        case C2C: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  
        case RETAIL: externalOrderTypeEnum = ExternalOrderTypeEnum.RETAIL;  
        break;  
        case B2B: externalOrderTypeEnum = ExternalOrderTypeEnum.B2B;  
        break;  
        default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  
    }  
  
    return externalOrderTypeEnum;  
}

11.@Context

@Context註解在MapStruct框架中用於標記對映方法的引數,使得這些引數作為對映上下文來處理。被標註為@Context的引數會在適用的情況下傳遞給其他對映方法、@ObjectFactory方法或者@BeforeMapping@AfterMapping方法,從而可以在自定義程式碼中使用它們。

具體作用如下:

  • 傳遞上下文資訊: 當MapStruct執行對映操作時,它會將帶有@Context註解的引數值向下傳遞到關聯的方法中。這意味著你可以在不同的對映階段(包括屬性對映、物件工廠方法呼叫以及對映前後的處理方法)共享和利用這些上下文資料。

  • 呼叫相關方法: MapStruct還會檢查帶有@Context註解的引數型別上是否宣告瞭@BeforeMapping@AfterMapping方法,並在適用時對提供的上下文引數值呼叫這些方法。

  • 空值處理: 注意,MapStruct不會在呼叫與@Context註解引數相關的對映前後方法或物件工廠方法之前進行空值檢查。呼叫者需要確保在這種情況下不傳遞null值。

  • 生成程式碼的要求: 為了使生成的程式碼能夠正確呼叫帶有@Context引數的方法,正在生成的對映方法宣告必須至少包含那些相同型別(或可賦值型別)的@Context引數。MapStruct不會為缺失的@Context引數建立新例項,也不會以null代替它們傳遞。

因此,@Context註解提供了一種機制,允許開發者在對映過程中攜帶並傳播額外的狀態或配置資訊,增強了對映邏輯的靈活性和定製能力。

一個簡單的用法示例:

	@Named("sourceToTarget5")  
	@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
	TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  
	  
	@Named("formatDate")  
	default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
	DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
	return dateTimeFormatter.format(createTime);  
}

生成的實現類程式碼:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
	if ( source == null ) {  
		return null;  
	}  
	  
	TargetObject targetObject = new TargetObject();  
	  
	targetObject.setUserName( source.getUserName() );  
	targetObject.setUserId( source.getUserId() );  
	if ( source.getSex() != null ) {  
		targetObject.setSex( String.valueOf( source.getSex() ) );  
	}  
	  
	targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  
	  
	return targetObject;  
}

12.@BeforeMapping

這個註解可以標註在一個沒有返回值的方法上,該方法會在執行實際對映操作前被呼叫。在此方法中可以透過@Context注入上下文物件,並根據需要對源物件或上下文進行修改或預處理。

	@Named("sourceToTarget5")  
	@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
	TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  
	  
	@Named("formatDate")  
	default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
		DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
		return dateTimeFormatter.format(createTime);  
	}  
	  
	@BeforeMapping  
	default void beforeFormatDate(@Context ContextObject context) {  
		// 在對映之前初始化或更新上下文中的資訊  
		context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}

編譯後生成的實現類程式碼中,會發現在sourceToTarget5的方法第一行會呼叫beforeFormatDate這個方法:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
	// 第一行呼叫@BeforeMapping的方法
	beforeFormatDate( contextObject );  
	  
	if ( source == null ) {  
		return null;  
	}  
	  
	TargetObject targetObject = new TargetObject();  
	  
	targetObject.setUserName( source.getUserName() );  
	targetObject.setUserId( source.getUserId() );  
	if ( source.getSex() != null ) {  
		targetObject.setSex( String.valueOf( source.getSex() ) );  
	}  
	  
	targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  
	  
	return targetObject;  
}

13.@AfterMapping

這個註解同樣可以標註在一個沒有返回值的方法上,但它會在完成所有屬性對映後被呼叫。你可以在這裡執行一些額外的轉換邏輯或者基於對映結果和上下文進行後期處理。

@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  
  
@Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
	DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
	return dateTimeFormatter.format(createTime);  
}  
  
@BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  
// 在對映之前初始化或更新上下文中的資訊  
	context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  
  
@AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  
	targetObject.setContext(contextObject.getContext());  
}

編譯後,可以發現在sourceTarget5的實現方法中的最後會呼叫afterHandler方法:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
	beforeFormatDate( contextObject );  
	  
	if ( source == null ) {  
		return null;  
	}  
	  
	TargetObject targetObject = new TargetObject();  
	  
	targetObject.setUserName( source.getUserName() );  
	targetObject.setUserId( source.getUserId() );  
	if ( source.getSex() != null ) {  
		targetObject.setSex( String.valueOf( source.getSex() ) );  
	}  
	  
	targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  
	  
	afterHandler( source, targetObject, contextObject );  
	  
	return targetObject;  
}

@BeforeMapping@AfterMapping 註解的方法預設會作用於在同一介面內使用了相同引數型別的對映方法上。如果想要在一個地方定義一個通用的前置或後置處理邏輯,並讓它應用於多個對映方法,可以編寫一個不帶具體對映源和目標引數的方法,並在需要應用這些邏輯的所有對映方法上保持相同的@Context引數型別。

14.@ObjectFactory

此註解用於宣告一個工廠方法,該方法在目標物件例項化階段被呼叫。這裡也可以透過@Context獲取到上下文資訊,以便在建立目標物件時就考慮到某些上下文依賴。

@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  
  
@Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
	DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
	return dateTimeFormatter.format(createTime);  
}  
  
@BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  
	// 在對映之前初始化或更新上下文中的資訊  
	context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  
  
@AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  
	targetObject.setContext(contextObject.getContext());  
}  
  
@ObjectFactory  
default TargetObject createTargetObject(@Context ContextObject contextObject){  
	TargetObject targetObject = new TargetObject();  
	// 根據上下文初始化dto的一些屬性  
	targetObject.setContext(contextObject.getContext());  
	return targetObject;  
}

編譯後生成的實現類中,會看見TargetObject會透過createTargetObject方法建立:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
	beforeFormatDate( contextObject );  
	  
	if ( source == null ) {  
	return null;  
	}  
	  
	TargetObject targetObject = createTargetObject( contextObject );  
	  
	targetObject.setUserName( source.getUserName() );  
	targetObject.setUserId( source.getUserId() );  
	if ( source.getSex() != null ) {  
		targetObject.setSex( String.valueOf( source.getSex() ) );  
	}  
	  
	targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  
	  
	afterHandler( source, targetObject, contextObject );  
	  
	return targetObject;  
}

@ObjectFactory 標記的方法則更具有針對性,它通常用於為特定的目標物件建立例項。如果你定義了一個@ObjectFactory方法且沒有指定具體對映方法,則這個工廠方法會作為預設的例項化方式,在所有未明確提供例項化方法的對映目標物件時被呼叫。

SpringBoot整合

上面我們說到了@Mapper註解以及他的屬性componentModel,將該值設定為Spring也就是MappingConstants.ComponentModel.SPRING值時,這個對映器生成的實現類就可以被Spring容器管理,這樣就可以在使用時就可以注入到其他元件中了。

@Mapper(uses = {MyNameConverter.class}, imports = {SexEnum.class}, componentModel = MappingConstants.ComponentModel.SPRING)  
public interface MyMapper {
	@Mappings({  
	@Mapping(target = "source", constant = "API"),  
	@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  
	})  
	@Named("sourceToTarget")  
	TargetObject sourceToTarget(SourceObject source);
}	

生成的實現類自動加上@Component註解,並將其註冊為Spring Bean,:

@Component  
public class MyMapperImpl implements MyMapper {
	@Override  
	public TargetObject sourceToTarget(SourceObject source) {  
		if ( source == null ) {  
			return null;  
		}  
		  
		TargetObject targetObject = new TargetObject();  
		  
		targetObject.setUserName( source.getUserName() );  
		targetObject.setUserId( source.getUserId() );  
		if ( source.getCreateTime() != null ) {  
			targetObject.setCreateTime( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( source.getCreateTime() ) );  
		}  
		  
		targetObject.setSource( "API" );  
		targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  
		  
		return targetObject;  
	}
}

這樣就可以在其他元件中注入MyMapper

@SpringBootTest  
public class SpringbootCodeApplicationTests {
	private MyMapper mapper;

	@Test  
	void testMapper(){  
		TargetObject targetObject = mapper.sourceToTarget(new SourceObject());  
		System.out.println(targetObject.getSex());  
	}

	@Autowired  
	public void setMapper(MyMapper mapper) {  
	this.mapper = mapper;  
}

總結

MapStruct是一個利用註解和編譯時程式碼生成技術的Java Bean對映工具,透過在介面上定義對映規則並自動建立實現類,極大地簡化了物件轉換過程。相比於手動編寫對映程式碼及執行時反射工具如Dozer,MapStruct提供了更高的效能、更好的可讀性和易於維護性。它支援靈活的欄位對映配置、自定義轉換邏輯,並可透過元件模型適應不同框架,是提升開發效率與降低維護成本的理想物件對映解決方案。

寫在最後:可能大家覺得要防禦性程式設計,但是咱可以把編譯後實現類的程式碼CV到你的程式碼裡面就可以了,這樣免去了自己手寫getset方法對映,這樣不出錯,還可以節省時間摸魚。。。。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章