簡化mapstruct程式碼: mapstruct-spring-plus

諸葛小亮發表於2021-05-07

mapstruct

MapStruct 是一個屬性對映工具,只需要定義一個 Mapper 介面,MapStruct 就會自動實現這個對映介面,避免了複雜繁瑣的對映實現。MapStruct官網地址: http://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

官方地址如下: https://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

地址: https://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,比如欄位名稱不一致、跳過對映等

關注我的公眾號,一起探索新技術

相關文章