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会生成此接口的实现。
由于id
和price
的类型和命名都是相同的,便不需要额外的Mapping了。
映射的规则是这样的:
- 当target和source对象中属性名相同,则直接转换
- 当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提供了三种转换器接口
- Converter
- ConverterFactory
- 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了。