Converter的实现与管理

MapStrut实现Converter

日常的开发中,在分层的架构中,对象在跨域边界时,难免需要需要转换成对应层次的模型对象,例如Entity、DO、DTO等。实现这些Converter也有不同的处理方案,如动态的BeanCopy、静态的MapStrut。

假设我们现在有一个OrderEntity和OrderDO需要实现互转,这里避免篇幅过长引入了lombok,以下涉及的maven依赖如下:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
</dependency>

两个对象的定义如下,

OrderEntity:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrderEntity {
    private long id;

    private long price;

    private OrderAddress address;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrderAddress {
    private String country;

    private String city;

    private String street;
}

OrderDO:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrderDO {
    private long id;

    private long price;

    private String country;

    private String city;

    private String street;
}

MapStruct实现的Converter如下:

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

    @Mapping(target = "country", source = "address.country")
    @Mapping(target = "city", source = "address.city")
    @Mapping(target = "street", source = "address.street")
    OrderDO toDO(OrderEntity orderEntity);

    @Mapping(target = "address.country", source = "country")
    @Mapping(target = "address.city", source = "city")
    @Mapping(target = "address.street", source = "street")
    OrderEntity toEntity(OrderDO orderDO);
}

只需定义一个接口,在编译的过程中,MapStruct会生成此接口的实现。

由于idprice的类型和命名都是相同的,便不需要额外的Mapping了。

映射的规则是这样的:

  1. 当target和source对象中属性名相同,则直接转换
  2. 当target和source对象中属性名不同,名字的映射可以通过@Mapping注解来指定。

使用Mappers.getMapper则是为了获取此接口的实现实例。

@Mapping还有更加复杂的用法,可以查阅官方说明:https://mapstruct.org/documentation/1.3/reference/html/

我们来测试一下是否达到了预期:

@Test
public void testMapStructConverter() {
    // given
    OrderDO orderDO = OrderDO.builder()
        .id(10001)
        .price(520)
        .country("China")
        .city("Wuhan")
        .street("Baker Street")
        .build();

    OrderAddress orderAddress = OrderAddress.builder()
        .country("China")
        .city("Wuhan")
        .street("Baker Street")
        .build();

    OrderEntity orderEntity = OrderEntity.builder().id(10001).price(520).address(orderAddress).build();

    // when
    OrderEntity actualEntity = OrderConverter.INSTANCE.toEntity(orderDO);

    // then
    assertThat(actualEntity).isEqualTo(orderEntity);

    // when
    OrderDO actualDO = OrderConverter.INSTANCE.toDO(orderEntity);

    // then
    assertThat(actualDO).isEqualTo(orderDO);

以上用例是可以通过的,说明Converter是可以工作的。

从这个用例中,我们发现一点小小让人不爽的地方,我们需要调用OrderConverter.INSTANCE来完成转化,这里的命名其实不够恰当,贴切的命名应该是OrderEntityDOConverter,因为此外还会存在OrderEntityDTOConverter,这样一来,大量的业务对象会伴随着大量的Converter对象混杂在我们边界转换的代码中,如何更好的管理这里Converter呢?

管理你的Converter

这里提供一个基于Spring的管理方案,如果不使用Spring也可以参考Spring的实现方案。

针对对象转换,Spring提供了三种转换器接口

  1. Converter
  2. ConverterFactory
  3. GenericConverter

而且提供了ConversionService接口,该接口的默认实现提供了多种基础类型的转化。

跟我们相关的是,Spring支持将自定义的Converter注册到容器中。

我们修改下OrderConverter接口的写法,让其实现Converter

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

    class DOToEntity implements Converter<OrderDO, OrderEntity> {

        @Override
        public OrderEntity convert(OrderDO orderDO) {
            return INSTANCE.toEntity(orderDO);
        }
    }

    class EntityToDO implements Converter<OrderEntity, OrderDO> {

        @Override
        public OrderDO convert(OrderEntity orderEntity) {
            return INSTANCE.toDO(orderEntity);
        }
    }

    @Mapping(target = "country", source = "address.country")
    @Mapping(target = "city", source = "address.city")
    @Mapping(target = "street", source = "address.street")
    OrderDO toDO(OrderEntity orderEntity);

    @Mapping(target = "address.country", source = "country")
    @Mapping(target = "address.city", source = "city")
    @Mapping(target = "address.street", source = "street")
    OrderEntity toEntity(OrderDO orderDO);
}

然后在xml文件中声明ConversionServiceFactoryBean的bean

<bean class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean class="com.huawei.geek.spring.converter.OrderConverter$DOToEntity"/>
            <bean class="com.huawei.geek.spring.converter.OrderConverter$EntityToDO"/>
        </list>
    </property>
</bean>

然后就可以使用ConversionService了。

验证一下看看

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:convert.xml"})
public class ConverterTest {
    @Autowired
    ConversionService conversionService;

    @Test
    public void testSpringConverter() {
        // given
        OrderDO orderDO = OrderDO.builder()
            .id(10001)
            .price(520)
            .country("China")
            .city("Wuhan")
            .street("Baker Street")
            .build();

        OrderAddress orderAddress = OrderAddress.builder()
            .country("China")
            .city("Wuhan")
            .street("Baker Street")
            .build();

        OrderEntity orderEntity = OrderEntity.builder().id(10001).price(520).address(orderAddress).build();

        // when
        OrderEntity actualEntity = conversionService.convert(orderDO, OrderEntity.class);

        // then
        assertThat(actualEntity).isEqualTo(orderEntity);

        // when
        OrderDO actualDO = conversionService.convert(orderEntity, OrderDO.class);

        // then
        assertThat(actualDO).isEqualTo(orderDO);
    }
}

用例OK。

之后我们只需在需要转换的地方调用conversionService的convert方法即可了,Spring会帮我们找到对应的Converter,前提是你注册了

conversionService还有些其他能力:

public interface ConversionService {
	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

	@Nullable
	<T> T convert(@Nullable Object source, Class<T> targetType);

	@Nullable
	Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

也是能同步工作的:

@Test
public void testCanConverter() {
    boolean canConvert = conversionService.canConvert(OrderDO.class, OrderEntity.class);
    assertThat(canConvert).isTrue();
}

很明显这样也有些额外的代码,MapStruct的接口中多了一层委托,Spring查找Converter也是有些许的性能损耗。

好处是我们可以把Entity-DO,Entity-DTO这些转换都放到一个Converter接口中,完成注册之后,在调用的地方我们便不关心散落的Converter了。

上一篇:ES常用的存储mapping配置项说明 与 doc_values详细介绍


下一篇:do_generic_file_read()函数