1. 概述
本文将深入探讨MapStruct库中的@Context
注解,该注解能帮助我们利用外部数据源或服务来填充目标POJO属性,同时也可用于传递状态变量。
在常规的POJO到POJO映射中,@Mapping
注解的source
和target
属性已足够。但某些场景下,我们需要更精细的控制——比如向自定义服务传递额外参数来推导目标属性值。这时,mapper类中的@Context
属性就派上用场了。
2. 映射场景
考虑一个需要额外参数的映射场景:
假设有个证券交易应用,它接收上游客户端的交易请求,需要将Trade Request
中的Security ID
转换为Standard ID
(如CUSIP/ISIN/SEDOL标准标识符)后,再转发给下游交易所。
由于原始交易请求中不包含这些标准标识符,映射程序必须依赖外部的Security Lookup Service
。最终,Mapper
程序将Trade Request
对象转换为Trade Dto
对象,再由Trade Service
发送到交易所执行订单。
下文我们将源POJO称为Trade
,目标POJO称为TradeDto
:
Mapper#toTradeDto()
方法中的上下文可以是标识类型或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()
是模拟实现,实际应用中会调用下游服务获取正确标识符。
最后调用mapper验证效果:
void givenBeforeMappingMethod_whenSecurityIdInTradeObject_thenSetSecurityIdentifierInTradeDto() {
Trade trade = createTradeObject();
TradeDto tradeDto = TradeMapperWithBeforeMapping.getInstance()
.toTradeDto(trade, "CUSIP", 6464);
assertEquals("037833100", tradeDto.getSecurityIdentifier());
}
先通过createTradeObject()
创建示例Trade
对象,其securityIdentifier
、quantity
和price
属性分别设为"AAPL"、"100"和"150.0":
Trade createTradeObject() {
return new Trade("AAPL", 100, 150.0);
}
mapper成功将Trade#SecurityID
的CUSIP等效值填充到TradeDto#SecurityIdentifier
。
后续示例将复用此createTradeObject()
方法创建示例Trade
对象。
4. 在@AfterMapping注解方法中使用上下文
有时我们需要在完成初始映射后,根据外部上下文填充属性。这时可用@AfterMapping
注解方法,它们在映射操作结束时调用。重要的是,这些方法能访问mapper类原始映射方法中声明的上下文参数:
@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;
}
}
本例中对convertToIdentifier()
方法应用了@AfterMapping
注解。除参数identifierType
的@Context
注解外,参数tradeDto
还带有@MappingTarget
注解。
@MappingTarget
注解提供对源Trade
对象的访问。方法调用SecurityService#getSecurityIdentifierOfType()
填充TradeDto#securityIdentifier
属性。
运行mapper验证结果:
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
,然后通过构造函数创建TradeDto
对象并返回。
在mapper类中需将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
填充。
运行mapper程序验证结果:
void whenGivenSecurityIDInTradeObject_thenUseObjectFactoryToCreateTradeDto() {
Trade trade = createTradeObject();
TradeDto tradeDto = TradeMapperUsingObjectFactory.getInstance()
.toTradeDto(trade, "SEDOL");
assertEquals("B1Y8QX7", tradeDto.getSecurityIdentifier());
}
如预期,程序成功将TradeDto#SecurityIdentifier
属性填充为Trade#securityID
的SEDOL等效值。
6. 总结
本文学习了如何使用MapStruct的@Context
注解映射依赖外部上下文的目标属性。该特性对实现细粒度映射控制至关重要,能帮助向外部服务传递上下文或状态参数,从其他数据源获取目标属性值。
此外,它还补充增强了现有特性,如@AfterMapping
、@BeforeMapping
、@ObjectFactory
注解和@Mapping#expression
属性。
本文使用的源代码可在GitHub获取。