1. 概述

本文将深入探讨MapStruct库中的@Context注解,该注解能帮助我们利用外部数据源或服务来填充目标POJO属性,同时也可用于传递状态变量

在常规的POJO到POJO映射中,@Mapping注解的sourcetarget属性已足够。但某些场景下,我们需要更精细的控制——比如向自定义服务传递额外参数来推导目标属性值。这时,mapper类中的@Context属性就派上用场了。

2. 映射场景

考虑一个需要额外参数的映射场景:

sequence 300x131

假设有个证券交易应用,它接收上游客户端的交易请求,需要将Trade Request中的Security ID转换为Standard ID(如CUSIP/ISIN/SEDOL标准标识符)后,再转发给下游交易所。

由于原始交易请求中不包含这些标准标识符,映射程序必须依赖外部的Security Lookup Service。最终,Mapper程序将Trade Request对象转换为Trade Dto对象,再由Trade Service发送到交易所执行订单。

下文我们将源POJO称为Trade,目标POJO称为TradeDto

class e1744135579498 300x85

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对象,其securityIdentifierquantityprice属性分别设为"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#quantityTrade#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获取。


原始标题:Mastering Context in MapStruct: Leveraging @Context for Complex Source Mappings | Baeldung