掌握 MapStruct 中的上下文:利用 @Context 進行複雜的來源映射
1.概述
在本教程中,我們將討論 MapStruct 庫中的@Context
註釋,它有助於使用外部來源或服務填充目標 POJO 屬性。此外,它還可用於傳遞狀態變數。
在大多數直接的 POJO 到 POJO 映射中, @Mapping
註釋的source
和target
屬性就足夠了。然而,在某些情況下,我們需要更精細的控制,將額外的參數傳遞給自訂服務以衍生出一些目標屬性值。
這就是映射器類別中的@Context
屬性變得有價值的地方。讓我們詳細討論一下這個問題。
2. 映射用例
讓我們從映射邏輯依賴於來源 POJO 之外的附加參數的場景開始:
讓我們想像一個股票交易應用程序,它接收來自上游客戶端的交易請求。將Trade Request
中的Security ID
解析為Standard ID
後,將其轉送至下游交易所。因此,它會取得與Security ID
對應的標準標識符,例如CUSIP
、 ISIN
或SEDOL
。
由於該資訊在原始交易請求中無法直接取得,因此應用程式的對應程式必須依賴外部Security Lookup Service
。最終, Mapper
程式從Trade Request
物件建立一個Trade Dto
物件。最後, Trade Service
程式將Trade Dto
傳送給交易所執行訂單。
此外,在本文的其餘部分,我們將來源 POJO 稱為Trade
,將目標 POJO 稱為TradeDto
:
Mapper#toTradeDto()
方法中的context
可以是標識符類型,也可以是查找服務類,如SecurityService
。
讓我們探索一下@Context
註解如何幫助實現這個用例。
3. 使用@BeforeMapping
註解修飾的方法中的上下文
上下文值可用於@BeforeMapping
方法和@Mapping#expression
屬性中宣告的 Java 表達式。 BeforeMapping
方法在映射實作開始時被呼叫。理想情況下,我們使用它來執行初始化過程。對於我們的用例,我們將使用交換程式碼初始化安全服務類別:
@Mapper
public abstract class TradeMapperWithBeforeMapping {
protected SecurityService securityService;
public static TradeMapperWithBeforeMapping getInstance() {
return Mappers.getMapper(TradeMapperWithBeforeMapping.class);
}
@BeforeMapping
protected void initialize(@Context Integer exchangeCode) {
securityService = new SecurityService(exchangeCode);
}
@Mapping(target="securityIdentifier",
expression = "java(securityService.getSecurityIdentifierOfType(trade.getSecurityID(), identifierType))")
protected abstract TradeDto toTradeDto(Trade trade, @Context String identifierType, @Context Integer exchangeCode);
}
首先,我們在toTradeDto()
方法中使用了兩個上下文參數。第二個上下文參數exchangeCode
可用於使用@BeforeMapping
註解修飾的initialize()
方法。上下文參數也適用於@Mapping#expression
屬性中定義的 Java 表達式。
因此,我們在@Mapping#expression
屬性中使用了第一個上下文參數identifierType,
。此外,為了填入目標 POJO 的TradeDto#securityIdentifier
屬性,我們在表達式中呼叫了SecurityService#getSecurityIdentifierOfType()
:
public String getSecurityIdentifierOfType(String securityID, String identifierType) {
return switch (identifierType.toUpperCase()) {
case "ISIN" -> "US0378331005";
case "CUSIP" -> "037833100";
case "SEDOL" -> "B1Y8QX7";
default -> null;
};
}
getSecurityIdentifierOfType()
只是一個模擬,在實際應用中,它會進行下游呼叫來取得正確的識別碼。
最後,讓我們呼叫映射器來查看它是否有效:
void givenBeforeMappingMethod_whenSecurityIdInTradeObject_thenSetSecurityIdentifierInTradeDto() {
Trade trade = createTradeObject();
TradeDto tradeDto = TradeMapperWithBeforeMapping.getInstance()
.toTradeDto(trade, "CUSIP", 6464);
assertEquals("037833100", tradeDto.getSecurityIdentifier());
}
首先,我們透過呼叫createTradeObject()
方法來建立一個範例來源Trade
物件。此方法傳回一個Trade
對象,其securityIdentifier
、 quantity
和price
屬性分別設定為AAPL
、 100
和150.0
:
Trade createTradeObject() {
return new Trade("AAPL", 100, 150.0);
}
隨後,映射器成功將Trade#SecurityID
的CUSIP
等效項填入TradeDto#SecurityIdentifier
。
我們將在其他範例中重複使用此createTradeObject()
來建立範例Trade
物件。
4. 使用@AfterMapping
註解修飾的方法中的上下文
有時,我們可能希望在完成初始映射後根據外部上下文填入屬性。在這種情況下,我們可以在方法上使用@AfterMapping
註解來在映射操作結束時呼叫它們。重要的是,這些方法可以存取映射器類別中原始映射方法中聲明的上下文參數:
@Mapper
public abstract class TradeMapperWithAfterMapping {
public static TradeMapperWithAfterMapping getInstance() {
return Mappers.getMapper(TradeMapperWithAfterMapping.class);
}
protected abstract TradeDto toTradeDto(Trade trade, @Context String identifierType);
@AfterMapping
protected TradeDto convertToIdentifier(Trade trade,
@MappingTarget TradeDto tradeDto, @Context String identifierType) {
SecurityService securityService = new SecurityService();
tradeDto.setSecurityIdentifier(
securityService.getSecurityIdentifierOfType(trade.getSecurityID(), identifierType)
);
return tradeDto;
}
}
在這個範例中,我們將@AfterMapping
註解套用到convertToIdentifier()
方法。除了參數indentifierType
上的@Context
註解之外,它還在參數tradeDto
上具有@MappingTarget
註解。
此外, @MappingTarget
註解有助於提供對來源Trade
物件的存取。此方法呼叫SecurityService#getSecurityIdentifierOfType()
填入TradeDto#securityIdentifier
屬性。
最後,讓我們運行映射器並驗證結果:
void givenAfterMappingMethod_whenSecurityIdInTradeObject_thenSetSecurityIdentifierInTradeDto() {
Trade trade = createTradeObject();
TradeDto tradeDto = TradeMapperWithAfterMapping.getInstance()
.toTradeDto(trade, "CUSIP");
assertEquals("037833100", tradeDto.getSecurityIdentifier());
}
如預期的那樣,目標TradeDto#securityIdentifier
屬性將使用來源Trade#SecurityID
的 CUSIP 等效項進行填入。
5. 使用@ObjectFactory
註解修飾的方法中的上下文
MapStruct 提供了細粒度的控制,可以使用@ObjectFactory
註釋來客製化映射操作。與其他功能一樣,上下文參數也適用於用@ObjectFactory
註解的方法。
接下來,讓我們在建立目標TradeDto
物件的類別中定義一個帶有@ObjectFactory
註解的方法:
public class TradeDtoFactory {
private static final Logger logger = LoggerFactory.getLogger(TradeFactory.class);
@ObjectFactory
public TradeDto createTradeDto(Trade trade, @Context String identifierType) {
SecurityService securityService = new SecurityService();
String securityIdentifier = securityService.getSecurityIdentifierOfType(trade.getSecurityID(), identifierType);
TradeDto tradeDto = new TradeDto(securityIdentifier);
return tradeDto;
}
}
首先, TradeFactory#createTradeDto()
實例化SecurityService
類,然後呼叫其getSecurityIdentifierOfType()
方法來取得SecurityIdentifier
。最後,將securityIdentifier
傳遞給建構函式來建立TradeDto
物件後,傳回Trade
物件。
現在,在映射器類別中,我們必須將TradeFactory.class
指派給@Mapper#uses
屬性:
@Mapper(uses = TradeDtoFactory.class)
public abstract class TradeMapperUsingObjectFactory {
public static TradeMapperUsingObjectFactory getInstance() {
return Mappers.getMapper(TradeMapperUsingObjectFactory.class);
}
protected abstract TradeDto toTradeDto(Trade trade, @Context String identifierType);
}
編譯後,產生的實作TradeMapperUsingObjectFactoryImpl#toTradeDto()
方法將呼叫工廠而不是TradeDto
的建構子。稍後,在實作方法中,其餘目標屬性由Trade#quantity
和Trade#price
屬性填入。
接下來,讓我們執行映射程式並驗證結果:
void whenGivenSecurityIDInTradeObject_thenUseObjectFactoryToCreateTradeDto() {
Trade trade = createTradeObject();
TradeDto tradeDto = TradeMapperUsingObjectFactory.getInstance()
.toTradeDto(trade, "SEDOL");
assertEquals("B1Y8QX7", tradeDto.getSecurityIdentifier());
}
如預期的那樣,程式TradeMapperUsingObjectFactory#toTradeDto()
成功地使用Trade#securityID
的 SEDOL 等效項填入了TradeDto#SecurityIdentifier
屬性。
6. 結論
在本文中,我們學習如何使用 MapStruct 的@Context
註釋來映射依賴外部上下文的目標屬性。此功能對於提供對映射操作的細粒度控制極為重要。它有助於將上下文或狀態參數傳遞給從其他來源檢索目標屬性值的外部服務。
此外,它們增強並補充了目前的功能,例如@AfterMapping
、 @BeforeMapping
、 @ObjectFactory
註解和@Mapping#expression
屬性。
與往常一樣,本文中使用的源代碼可在 GitHub 上找到。