Mapstruct it can replace BeanUtil to perform conversions between DTO, VO, PO. It uses the annotation processor mechanism of Java compilation period, to put it bluntly, it is a code generator, instead of you manually type conversion during the take value assignment operation.

1
2
3
4
5
@Mapper(componentModel = "spring")
public interface AreaMapping {

    List<AreaInfoListVO> toVos(List<Area> areas);
}

In just a few lines, a collection of PO is transformed into a collection of corresponding VO.

1
2
3
4
5
6
7
8
// spring bean 
@Autowired
AreaMapping areaMapping
    
// 转换源 areas    
List<Area> areas = ……;
// 转换目标 vos 
List<AreaInfoListVO> vos = areaMapping.toVos(areas)

But it’s still not very pleasant to write this way, and you have to inject the corresponding Mapper class every time.

Converter

Spring framework provides a Converter<S,T> interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S source);

    default <U> Converter<S, U> andThen(Converter<? super T, ? extends U> after) {
        Assert.notNull(after, "After Converter must not be null");
        return (s) -> {
            T initialResult = this.convert(s);
            return initialResult != null ? after.convert(initialResult) : null;
        };
    }
}

Its role is to convert S to T, which coincides with the role of Mapstruct.

The Converter will be registered to the ConversionService through the ConverterRegistry registration interface, and then you can do the conversion through the convert method of the ConversionService.

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

MapStruct Spring Extensions

Based on the above mechanism, the official MapStruct Spring Extensions plugin was introduced, which implements a mechanism where all Mapstruct mapping interfaces ( Mapper ) that implement Converter are automatically registered to ConversionService. We only need to go through the ConversionService to do any conversion operation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * @author felord.cn
 * @since 1.0.0
 */
@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {

    @Mapping(target = "seats", source = "seatConfiguration")
    CarDto convert(Car car);
}

Usage..

1
2
3
4
5
6

@Autowired
private ConversionService conversionService;

Car car = ……;
CarDto carDto = conversionService.convert(car,CarDto.class);

MapStruct Spring Extensions will automatically generate an adapter class to handle Mapper Registration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.mapstruct.extensions.spring.converter;

import cn.felord.mapstruct.entity.Car;
import cn.felord.mapstruct.entity.CarDto;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
/**
 * @author felord.cn
 * @since 1.0.0
 */
@Component
public class ConversionServiceAdapter {
    private final ConversionService conversionService;

    public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public CarDto mapCarToCarDto(final Car source) {
        return (CarDto)this.conversionService.convert(source, CarDto.class);
    }
}

Customization

Customize the package path and name of the adapter class

By default, the generated adapter class will be located in the package org.mapstruct.extensions.spring.converter with a fixed name of ConversionServiceAdapter. If you wish to change the package path or name, you can do so.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package cn.felord.mapstruct.config;

import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;

/**
 * @author felord.cn
 * @since 1.0.0
 */
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(conversionServiceAdapterPackage = "cn.felord.mapstruct.config",
        conversionServiceAdapterClassName = "MapStructConversionServiceAdapter")
public class MapperSpringConfig {
}

Without specifying the conversionServiceAdapterPackage element, the generated Adapter class will reside in the same package as the annotated Config, so the path above can be omitted.

Specifying a ConversionService

If you have more than one ConversionService in your Spring IoC container, you can specify it via the conversionServiceBeanName parameter of the @SpringMapperConfig annotation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package cn.felord.mapstruct.config;

import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;

/**
 * @author felord.cn
 * @since 1.0.0
 */
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(conversionServiceAdapterPackage = "cn.felord.mapstruct.config",
        conversionServiceAdapterClassName = "MapStructConversionServiceAdapter",
                   conversionServiceBeanName = "myConversionService")
public class MapperSpringConfig {
}

Integrating Spring’s built-in conversions

Spring provides a lot of nice Converter<S,T> implementations internally, some of them are not directly open, if you want to use them with the Mapstruct mechanism, you can register them with externalConversions of the @SpringMapperConfig annotation.

1
2
3
4
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
   externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
public interface MapstructConfig {}

The corresponding conversions are automatically generated in the adapter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Component
public class ConversionServiceAdapter {
  private final ConversionService conversionService;

  public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public Locale mapStringToLocale(final String source) {
    return conversionService.convert(source, Locale.class);
  }
}

Summary

mapstruct-spring-annotations enables developers to use the defined Mapstruct mappers via ConversionService without having to import each Mapper separately, thus allowing loose coupling between Mappers. It does not affect the Mapstruct mechanism itself.

Reference https://mp.weixin.qq.com/s/oZpZgqftZ0xeEFdrnUXgAw