简化mapstruct代码: mapstruct-spring-plus

mapstruct

MapStruct 是一个属性映射工具,只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。MapStruct官网地址: //mapstruct.org/
MapStruct 使用APT生成映射代码,其在效率上比使用反射做映射的框架要快很多。

mapstruct spring

MapStruct 结合spring使用,设定componentModel = "spring"即可,如下Mapper接口:

@Mapper(componentModel = "spring")
public interface CarDtoMapper{
    Car dtoToEntity(CarDto dto);
}

生成的映射代码如下,发现实现类上添加了@Component注解


@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-04-26T11:02:50+0800",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-6.8.jar, environment: Java 1.8.0_211 (Oracle Corporation)"
)
@Component
public class CarDtoMapperImpl implements CarDtoMapper {

    @Override
    public Car dtoToEntity(CarDto dto) {
        if ( dto == null ) {
            return null;
        }

        Car car = new Car();

        car.setName( dto.getName() );

        return car;
    }
}

mapstruct spring 使用的缺点

mapstruct结合spring,在使用方式上主要是需要编写接口文件和定义函数所带来编码工作量:

  1. 需要创建mapper接口文件,这个是mapstruct框架的必须要经历的过程,代码量增加
  2. Dto和Entity之间互相转换,需要在接口中添加一个方法,并且添加上InheritInverseConfiguration注解,如下
@InheritInverseConfiguration(name = "dtoToEntity")    
CarDto entityToDto(Car dto);
  1. service 中依赖多个mapper转换,增加构造函数注入个数
  2. 覆盖已有对象,需要添加如下map方法,如下
Car dtoMapToEntity(CarDto dto, @MappingTarget Car car)

反向映射,同样需要添加如下方法

CarDto entityMapToDto(Car dto, @MappingTarget CarDto car);

理想的映射工具

对于对象映射,有一种理想的使用方式,伪代码如下

Car car = mapper.map(dto, Car.class);
// or
Car car = new Car();
mapper.map(dto, car);

//  反向映射
CarDto dto = mapper.map(entity, CarDto.class);
// or
CarDto dto = new CarDto();
mapper.map(entity, dto);

只使用mapper对象,就可以解决任何对象之间的映射。

mapstruct 官方解决方案: mapstruct-spring-extensions

官方地址如下: //github.com/mapstruct/mapstruct-spring-extensions
其思路是使用spring 的 Converter接口,官方用法如下



@Mapper(config = MapperSpringConfig.class)
public interface CarMapper extends Converter<Car, CarDto> {
    @Mapping(target = "seats", source = "seatConfiguration")
    CarDto convert(Car car);
}

  @ComponentScan("org.mapstruct.extensions.spring")
  @Component
  static class AdditionalBeanConfiguration {
    @Bean
    ConfigurableConversionService getConversionService() {
      return new DefaultConversionService();
    }
  }

  @BeforeEach
  void addMappersToConversionService() {
    conversionService.addConverter(carMapper);
    conversionService.addConverter(seatConfigurationMapper);
    conversionService.addConverter(wheelMapper);
    conversionService.addConverter(wheelsMapper);
    conversionService.addConverter(wheelsDtoListMapper);
  }

  @Test
  void shouldKnowAllMappers() {
    then(conversionService.canConvert(Car.class, CarDto.class)).isTrue();
    then(conversionService.canConvert(SeatConfiguration.class, SeatConfigurationDto.class)).isTrue();
    then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
    then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
    then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
  }

  @Test
  void shouldMapAllAttributes() {
    // Given
    final Car car = new Car();
    car.setMake(TEST_MAKE);
    car.setType(TEST_CAR_TYPE);
    final SeatConfiguration seatConfiguration = new SeatConfiguration();
    seatConfiguration.setSeatMaterial(TEST_SEAT_MATERIAL);
    seatConfiguration.setNumberOfSeats(TEST_NUMBER_OF_SEATS);
    car.setSeatConfiguration(seatConfiguration);
    final Wheels wheels = new Wheels();
    final ArrayList<Wheel> wheelsList = new ArrayList<>();
    final Wheel wheel = new Wheel();
    wheel.setDiameter(TEST_DIAMETER);
    wheel.setPosition(TEST_WHEEL_POSITION);
    wheelsList.add(wheel);
    wheels.setWheelsList(wheelsList);
    car.setWheels(wheels);

    // When
    final CarDto mappedCar = conversionService.convert(car, CarDto.class);
}

使用 mapstruct-spring-extensions,使用 ConfigurableConversionService, 虽然解决了使用同一个对象映射,但是代码量没有解决,同时,没有提供覆盖已有对象的使用方式

推荐 mapstruct-spring-plus

地址: //github.com/ZhaoRd/mapstruct-spring-plus
这个项目参考了mapstruct-spring-extensions项目,同时使用APT技术,动态生成Mapper接口,解决编写接口的问题,提供IObejctMapper接口,提供所有的map方法。

maven引入

<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
    <io.github.zhaord.version>1.0.1.RELEASE</io.github.zhaord.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>io.github.zhaord</groupId>
        <artifactId>mapstruct-spring-plus-boot-starter</artifactId>
        <version>${io.github.zhaord.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <path>
                        <groupId>io.github.zhaord</groupId>
                        <artifactId>mapstruct-spring-plus-processor</artifactId>
                        <version>${io.github.zhaord.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

gradle 引入

dependencies {
    ...
    compile 'org.mapstruct:mapstruct:1.4.2.Final'
    compile 'io.github.zhaord:mapstruct-spring-plus-boot-starter:1.0.1.RELEASE'

    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
    testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' // if you are using mapstruct in test code

    annotationProcessor 'io.github.zhaord:mapstruct-spring-plus-processor:1.0.1.RELEASE'
    testAnnotationProcessor 'io.github.zhaord:mapstruct-spring-plus-processor:1.0.1.RELEASE' // if you are using mapstruct in test code
    ...
}

使用案例

使用代码如下


public enum CarType {
    SPORTS, OTHER
}

@Data
public class Car {
    private String make;
    private CarType type;
}


@Data
@AutoMap(targetType = Car.class)
public class CarDto {
    private String make;

    private String type;

}


@ExtendWith(SpringExtension.class)
@ContextConfiguration(
        classes = {AutoMapTests.AutoMapTestConfiguration.class})
public class AutoMapTests {

    @Autowired
    private IObjectMapper mapper;

    @Test
    public void testDtoToEntity() {

        var dto = new CarDto();
        dto.setMake("M1");
        dto.setType("OTHER");

        Car entity = mapper.map(dto, Car.class);

        assertThat(entity).isNotNull();
        assertThat(entity.getMake()).isEqualTo("M1");
        assertThat(entity.getCarType()).isEqualTo("OTHER");

    }


    @ComponentScan("io.github.zhaord.mapstruct.plus")
    @Configuration
    @Component
    static class AutoMapTestConfiguration {


    }


}

AutoMap 生成的接口代码


@Mapper(
    config = AutoMapSpringConfig.class,
    uses = {}
)
public interface CarDtoToCarMapper extends BaseAutoMapper<CarDto, Car> {
  @Override
  @Mapping(
      ignore = false
  )
  Car map(final CarDto source);

  @Override
  @Mapping(
      ignore = false
  )
  Car mapTarget(final CarDto source, @MappingTarget final Car target);
}

mapstruct-spring-plus 带来的便捷

  1. 使用AutoMap注解,减少了重复代码的编写,尤其是接口文件和映射方法
  2. 依赖注入,只需要注入IObjectMapper接口即可,具体实现细节和调用方法,对客户端友好
  3. 没有丢失mapstruct的功能和效率
  4. @Mapping注解,都可以使用@AutoMapField来完成字段的映射设置,因为@AutoMapField继承自@Mapping,比如字段名称不一致、跳过映射等