org.mapstruct.mapstruct 类型转换工具


参考文档:MapStruct 1.3.0.Final参考指南 (kailing.pub)
官方:https://mapstruct.org/
官方案例 github项目:https://github.com/mapstruct/mapstruct-examples

学习版本 :1.4.1.Final

各工具包-转换性能对比:图片来源知乎大佬

引入依赖




    org.mapstruct
    mapstruct
    1.4.1.Final



    org.mapstruct
    mapstruct-processor
    1.4.1.Final




    org.projectlombok
    lombok
    true

第一个案例

实体类
@Data
public class Cat {
    private Integer id_cat;// 测试 【字段名称不同】 的处理
    private Integer age; // age name 为 【基本数据类型】 字段
    private String name;
    private Date birthdate;// 【时间类型】
    private Master master; // 【引用数据类型】 - 动物的主人
}
@Data
public class CatDto {
    private Integer id;
    private Integer age;
    private String name;
    private Date birthdate;
    private MasterDto masterDto;
}
@Data
public class Master {
    private String name;
    private Integer age;
}
@Data
public class MasterDto {
    private String name;
    private Integer age;
}
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(uses = {MasterMapper.class}) // 引入嵌套的对象 - 【不引入会造成引用数据类型为浅拷贝】
public interface CatMapper {
    // Mappers工厂 创建 实例
    CatMapper INSTANCE = Mappers.getMapper(CatMapper.class);
    CatDto catToCatDto(Cat cat);
}

@Mapper
public interface MasterMapper {
    MasterMapper INSTANCE = Mappers.getMapper(MasterMapper.class);
    MasterDto masterToMasterDto(Master master);
}
调用
public class Test_01 {
    public static void main(String[] args) {

        Cat cat = Test_01.newInstanceCat();
        System.out.println(cat.toString());
        System.out.println(cat.getMaster().hashCode());

        CatDto catDto = CatMapper.INSTANCE.catToCatDto(cat);

        System.out.println(catDto.toString());
        System.out.println(catDto.getMasterDto().hashCode());
    }
    public static Master newInstanceMaster() {
        Master master = new Master();
        master.setName("小辉");
        master.setAge(18);
        return master;
    }
    public static Cat newInstanceCat() {
        Cat cat = new Cat();
        cat.setId_cat(1);
        cat.setName("小花猫");
        cat.setAge(3);
        cat.setMaster(Test_01.newInstanceMaster());
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date birthdate = formatter.parse("2020-01-01");
            cat.setBirthdate(birthdate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return cat;
    }
}

映射器使用方式

1. 映射器 + Mappers工厂 (推荐使用-简单易懂)

import org.mapstruct.Mapper;
@Mapper 
public interface CarMapper {
     CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
     CarDto carToCarDto(Car car);
}

// 调用
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );

2. 基于容器 的 "cdi","spring" 模式

spring模式案例, cdi模式测试时java类缺失(个人感觉不会使用,没有深究)

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
public interface CatMapper {
	CatDto catToCatDto(Cat cat);
}

// 注入到spring容器中Bean名称“catMapperImpl”,这里为注入示例
@Autowired
CatMapper catMapper;

映射器编写

1. 基础映射器

  • @Mapper 标记为映射器,
  • @Mapping 标记对象中的字段:source 源对象字段 ,target 目标字段
@Mapper
public interface CarMapper {

    @Mapping(source = "make", target = "manufacturer")
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);

    @Mapping(source = "name", target = "fullName")
    PersonDto personToPersonDto(Person person);
}

2. 自定义方法 不常用

自定义可以直接使用表达式来制定

这里付一个官方案例,可忽略
@Mapper
public interface SourceTargetMapper {

    SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping( target = "key", ignore = true )
    @Mapping( target = "descriptionArticle1", source = "brush.description" )
    @Mapping( target = "descriptionArticle2", source = "paste.description" )
    CombinedOfferingEntity toEntity(Toothbrush brush, ToothPaste paste, @Context ArticleRepository repo);

    @ObjectFactory
    default  T lookup(Toothbrush brush, ToothPaste paste, @Context ArticleRepository repo,
        @TargetType Class targetType ) {
        ComposedKey key = new ComposedKey(brush.getName(), paste.getName() );
        CombinedOfferingEntity entity = repo.lookup( key );
        if ( entity == null ) {
            entity = new CombinedOfferingEntity();
        }
        return (T) entity;
    }
}
public class SourceTargetMapperTest {
    @Test
    public void testToTarget() {

        ToothPaste paste = new ToothPaste();
        paste.setName( "Paradontax" );
        paste.setDescription( "with active enzimes" );

        Toothbrush brush = new Toothbrush();
        brush.setName( "Oral-B" );
        brush.setDescription( "rotating head" );

        ArticleRepository repo = new ArticleRepository();

        CombinedOfferingEntity entity = SourceTargetMapper.MAPPER.toEntity( brush, paste, repo );

        //results
        assertThat( entity ).isNotNull();
        assertThat( entity.getKey() ).isNotNull();
        assertThat( entity.getKey().getName1() ).isEqualTo( "Oral-B" );
        assertThat( entity.getKey().getName2() ).isEqualTo( "Paradontax" );
        assertThat( entity.getDescriptionArticle1() ).isEqualTo( "rotating head" );
        assertThat( entity.getDescriptionArticle2() ).isEqualTo( "with active enzimes" );
    }
}
public class ArticleRepository {
    public CombinedOfferingEntity lookup(ComposedKey key) {
        // do some DB lookups here.
        CombinedOfferingEntity entity = new CombinedOfferingEntity();
        entity.setKey( key );
        return entity;
    }
}
public interface Entity {
    ComposedKey getKey();
}
public class ComposedKey {
    private final String name1;
    private final String name2;
    public ComposedKey(String name1, String name2) {
        this.name1 = name1;
        this.name2 = name2;
    }
    public String getName1() {
        return name1;
    }
    public String getName2() {
        return name2;
    }
}
public class CombinedOfferingEntity implements Entity {
    private ComposedKey key;
    private String descriptionArticle1;
    private String descriptionArticle2;
    @Override
    public ComposedKey getKey() {
        return key;
    }
    public void setKey(ComposedKey key) {
        this.key = key;
    }
    public String getDescriptionArticle1() {
        return descriptionArticle1;
    }
    public void setDescriptionArticle1(String descriptionArticle1) {
        this.descriptionArticle1 = descriptionArticle1;
    }
    public String getDescriptionArticle2() {
        return descriptionArticle2;
    }
    public void setDescriptionArticle2(String descriptionArticle2) {
        this.descriptionArticle2 = descriptionArticle2;
    }
}
public class Toothbrush {
    private String name;
    private String description;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}
public class ToothPaste {
    private String name;
    private String description;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}

3. 多源映射(入参)

  • 源对象出现相同字段时必须用@Mapping指定
@Mapper
public interface AddressMapper {

    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "address.houseNo", target = "houseNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
// 特殊用法
@Mapper
public interface AddressMapper {

    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "hn", target = "houseNumber") // 这里可以直接注入到指定字段中
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}

4. 传入目标对象

  • MappingTarget 用了标记目标对象
@Mapper
public interface CarMapper {
    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}

5. 自定义逻辑

  • 见下 官方案例 1

6. map对象 映射类

public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map longDateMapToStringStringMap(Map source);
}

7. 枚举映射

@Mapper
public interface SpecialOrderMapper {

    SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );

    @ValueMappings({
        @ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ),
        @ValueMapping( source = "STANDARD", target = MappingConstants.NULL ),
        @ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" )
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}

8. 高级映射

    // 1.默认值和常量
    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    @Mapping(target = "stringConstant", constant = "Constant Value")
    @Mapping(target = "integerConstant", constant = "14")
    @Mapping(target = "longWrapperConstant", constant = "3001")
    @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
    Target sourceToTarget(Source s);

    // 2. 表达式
    // @Mapper( imports = TimeAndFormat.class )   // 可以结合 Mapper 引入类
    // @Mapping(target = "timeAndFormat", 
    //     expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    @Mapping(target = "timeAndFormat",
         expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);

    // 3. 默认表达式
    // 当 sourceId 为null时,会使用默认表达式
    @Mapping(target="id", source="sourceId", defaultExpression = "java( java.util.UUID.randomUUID().toString() )")
    Target sourceToTarget(Source s);

官方案例

1. 自定义逻辑处理 Mapping#qualifiedBy

    /**
     * A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple
     * mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found'
     * error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method.
     * 

* Note that {@link #defaultValue()} usage will also be converted using this qualifier. * * @return the qualifiers * @see Qualifier */ Class<? extends Annotation>[] qualifiedBy() default { };

自定义逻辑处理(枚举)
// 用于标识
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface LastElement {}

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface FirstElement {}

// 具体逻辑类
public class IterableNonInterableUtil {

    @FirstElement
    public  T first( List in ) {
        if ( in != null && !in.isEmpty() ) {
            return in.get( 0 );
        }
        else {
            return null;
        }
    }

    @LastElement
    public  T last( List in ) {
        if ( in != null && !in.isEmpty() ) {
            return in.get( in.size() - 1 );
        }
        else {
            return null;
        }
    }
}
public class Source {
    private List myIntegers;
    private List myStrings;
}
public class Target {
    private Integer myInteger;
    private String myString;
}
// uses = IterableNonInterableUtil.class 引入对象方式
@Mapper( uses = IterableNonInterableUtil.class )
public interface SourceTargetMapper {

    SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

    // qualifiedBy = FirstElement.class  指定具体方法
    @Mapping( source = "myIntegers", target = "myInteger", qualifiedBy = FirstElement.class )
    @Mapping( source = "myStrings", target = "myString", qualifiedBy = LastElement.class )
    Target toTarget( Source s );
}

public class Main {
    public static void main( String[] args ) {
        Source s = new Source();
        s.setMyIntegers( Arrays.asList( 5, 3, 7 ) );
        s.setMyStrings( Arrays.asList( "five", "three", "seven " ) );

        Target t = SourceTargetMapper.MAPPER.toTarget( s );
        System.out.println( t.getMyInteger() ); // 5
        System.out.println( t.getMyString() ); // seven
    }
}

2. 反向注入 @InheritInverseConfiguration

@InheritInverseConfiguration 会根据正向转换生成对应的反向Mapping字段映射。

实体类(省略 get、set)
public class Customer {
    private Long id;
    private String name;
    private Collection orderItems;
}
public class CustomerDto {
    public Long id;
    public String customerName;
    public List orders;
}
public class OrderItem {
    private String name;
    private Long quantity;
}
public class OrderItemDto {
    public String name;
    public Long quantity;
}
@Mapper(uses = { OrderItemMapper.class })
public interface CustomerMapper {

    CustomerMapper MAPPER = Mappers.getMapper( CustomerMapper.class );

    @Mapping(source = "orders", target = "orderItems")
    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto customerDto);

    @InheritInverseConfiguration // 这里相当于正向 toCustomer 方法的 Mapping,等价于注释的Mapping
    // @Mapping(source = "orderItems", target = "orders")
    // @Mapping(source = "name", target = "customerName")
    CustomerDto fromCustomer(Customer customer);
}
@Mapper
public interface OrderItemMapper {

    OrderItemMapper MAPPER = Mappers.getMapper(OrderItemMapper.class);

    OrderItem toOrder(OrderItemDto orderItemDto);

    @InheritInverseConfiguration // 当正向没有不同时,可以忽略不写
    OrderItemDto fromOrder(OrderItem orderItem);
}

3. 克隆 clone ( 1.4 可用)

    /**
     * Allows detailed control over the mapping process.
     *
     * @return the mapping control
     *
     * @since 1.4
     *
     * @see org.mapstruct.control.DeepClone
     * @see org.mapstruct.control.NoComplexMapping
     * @see org.mapstruct.control.MappingControl
     */
    Class<? extends Annotation> mappingControl() default MappingControl.class;
实体类(省略 get、set)
public class CustomerDto {
    private Long id;
    private String customerName;
    private List orders;
    private Map stock;
}
public class OrderItemDto {
    private String name;
    private Long quantity;
}
public class OrderItemKeyDto {
    private long stockNumber;
}
@Mapper(mappingControl = DeepClone.class)
public interface Cloner {

    Cloner MAPPER = Mappers.getMapper( Cloner.class );

    CustomerDto clone(CustomerDto customerDto);
}
调用测试
public class ClonerTest {

    @Test
    public void testMapDtoToEntity() {

        CustomerDto customerDto = new CustomerDto();
        customerDto.setId( 10L );
        customerDto.setCustomerName("Jaques" );
        OrderItemDto order1 = new OrderItemDto();
        order1.setName ("Table" );
        order1.setQuantity( 2L );
        customerDto.setOrders( new ArrayList<>( Collections.singleton( order1 ) ) );
        OrderItemKeyDto key = new OrderItemKeyDto();
        key.setStockNumber( 5 );
        Map stock = new HashMap(  );
        stock.put( key, order1 );
        customerDto.setStock( stock );

        CustomerDto customer = Cloner.MAPPER.clone( customerDto );

        System.out.println(customer); // org.mapstruct.example.dto.CustomerDto@504bae78
        System.out.println(customerDto); // org.mapstruct.example.dto.CustomerDto@3b764bce

        // check if cloned
        assertThat( customer.getId() ).isEqualTo( 10 );
        assertThat( customer.getCustomerName() ).isEqualTo( "Jaques" );
        assertThat( customer.getOrders() )
            .extracting( "name", "quantity" )
            .containsExactly( tuple( "Table", 2L ) );
        assertThat( customer.getStock()  ).isNotNull();
        assertThat( customer.getStock() ).hasSize( 1 );

        Map.Entry entry = customer.getStock().entrySet().iterator().next();
        assertThat( entry.getKey().getStockNumber() ).isEqualTo( 5 );
        assertThat( entry.getValue().getName() ).isEqualTo( "Table" );
        assertThat( entry.getValue().getQuantity() ).isEqualTo( 2L );

        // check mapper really created new objects
        assertThat( customer ).isNotSameAs( customerDto );
        assertThat( customer.getOrders().get( 0 ) ).isNotEqualTo( order1 );
        assertThat( entry.getKey() ).isNotEqualTo( key );
        assertThat( entry.getValue() ).isNotEqualTo( order1 );
        assertThat( entry.getValue() ).isNotEqualTo( customer.getOrders().get( 0 ) );
    }
}