当前位置 : 首页 » 文章分类 :  开发  »  MapStruct

MapStruct

bean 转换工具 MapStruct 使用笔记

https://mapstruct.org/


官方入门示例

public class Car {
    private String make;
    private int numberOfSeats;
    private CarType type;

    //constructor, getters, setters etc.
}
public class CarDto {
    private String make;
    private int seatCount;
    private String type;

    //constructor, getters, setters etc.
}

将 Car 转换为 CarDto 的转换器如下

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

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

@Mapper 标识这是一个转换器
carToCarDto() 是实际转换的方法,将入参 Car 转换为返回类型 CarDto, 方法名任意。
对于在原类型和目标类型中名字不同的字段,使用 @Mapping 注解进行映射
通过 Mappers.getMapper() 可以获取一个 mapper 实例

使用

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

获取 Mapper

componentModel 属性

componentModel 属性用于指定自动生成的接口实现类的组件类型,这个属性支持四个值:
default 这是默认的情况,mapstruct 不使用任何组件类型, 可以通过 Mappers.getMapper(Class) 方式获取自动生成的实例对象。
cdi the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
spring 生成的实现类上面会自动添加一个 @Component 注解,可以通过 Spring 的 @Autowired 方式进行注入
jsr330 生成的实现类上会添加 @javax.inject.Named@Singleton 注解,可以通过 @Inject 注解获取

mappingControl = DeepClone.class 深拷贝

普通的 Mapper 只是浅拷贝,增加 mappingControl = DeepClone.class 可实现深拷贝

@Mapper(mappingControl = DeepClone.class)
public interface CloningMapper {
     CloningMapper INSTANCE = Mappers.getMapper( CloningMapper.class );
     FridgeDTO clone(FridgeDTO in);
}

定义 Mapper

MapStruct 的类型转换规则

1、如果 source 和 target 属性类型相同,直接将 source 的值拷贝到 target,如果 source 是集合,将集合的 copy 放到 target 中。
2、如果 source 和 target 属性类型不同,寻找是否有以 source 类型为入参、 target 类型为返回值的方法,如果有,则会自动调用次方法进行类型转换。
3、查看是否可以进行内置类型转换

5.2. Mapping object references
https://mapstruct.org/documentation/stable/reference/html/#mapping-object-references

隐式类型转换

MapStruct 可自动处理下面的类型转换:
1、全部 Java 原子类型和其对应包装类型之间都可自动转换,例如 int 和 Integer 之间,将包装类型转换为原子类型时,会自动检测处理 null。
2、全部 Java 原子数据类型和包装类型,例如 int, long, byte 和 Integer 之间。
3、全部 Java 原子类型(包括启包装类型)和 String 之间可自动转换。
等等

5.1. Implicit type conversions
https://mapstruct.org/documentation/stable/reference/html/#implicit-type-conversions


嵌套字段映射

可通过 . 指定嵌套字段间的映射

@Mapper
public interface FishTankMapper {
    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", ignore = true)
    @Mapping(target = "ornament", source = "interior.ornament")
    @Mapping(target = "material.materialType", source = "material")
    @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
    FishTankDto map(FishTank source );
}

5.3. Controlling nested bean mappings
https://mapstruct.org/documentation/stable/reference/html/#controlling-nested-bean-mappings


ignore 忽略字段

@Mapping(target = "id", ignore = true)
@Mapping(target = "ruleId", source = "rulesDO.id")
MessageDO voRule2Do(MessageVO messageVO, RulesDO rulesDO);

uses 引用其他 Mapper

通过 @Mapper(uses = OtherMapper.class) 引用其他 Mapper 中的映射方法,可自动找到引用的 Mapper 中的自定义方法或 MapStruct 自动生成的方法。

例如定义一个 Date 自动转换 Mapper

public class DateMapper {
    public String asString(Date date) {
        return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;
    }

    public Date asDate(String date) {
        try {
            return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null;
        } catch ( ParseException e ) {
            throw new RuntimeException( e );
        }
    }
}

在 CarMapper 中通过 uses 引用 DateMapper,然后 MapStruct 生成的转换方法中会自动调用 DateMapper 的 String 和 Date 互转方法。

@Mapper(uses = DateMapper.class)
public interface CarMapper {
    CarDto carToCarDto(Car car);
}

qualifiedByName 自定义类型转换方法

自定义类型转换方法,加 @Named 注解,@Mapping 中通过 qualifiedByName 指定自定义方法名。

@Mapper
public interface MovieMapper {
    @Mapping(target = "title", qualifiedByName = { "EnglishToGerman" } )
    @Mapping(target = "dictCatalogs", source = "dictConfig", qualifiedByName = "dictConfig2Catalogs")
    GermanRelease toGerman(OriginalRelease movies);

    @Named("EnglishToGerman")
    static String translateTitleEG(String title) {
        // some mapping logic
    }

    @Named("dictConfig2Catalogs")
    default List<CatalogVO> dictConfig2Catalogs(String json) {
        return Optional.ofNullable(json)
                       .filter(StringUtils::isNotBlank)
                       .map(j -> {
                           try {
                               return new ObjectMapper().readValue(j, new TypeReference<List<CatalogVO>>() {
                               });
                           } catch (JsonProcessingException e) {
                               throw new RuntimeException(e);
                           }
                       })
                       .orElse(List.of());
    }
}

qualifiedByName 指定的方法的入参类型就是 source 字段的类型,如果没有 source,默认就是和 target 同名字段的类型。


@Context 传递额外的上下文参数

可用于给 qualifiedByName 指定的方法传入多个参数
但是注意标注了 @Context 的参数不能再作为 source 参数。



constant 常数

@Mapper
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @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")
    Target sourceToTarget(Source s);
}

target 的 stringConstant 字段被设为固定常数 “Constant Value”
target 的 integerConstant 和 longWrapperConstant 都被设为固定常数,MapStruct 会自动转换为对应的数字类型。
target 的 dateConstant 被设为固定的日期常数,通过 dateFormat 指定日期格式


defaultValue 默认值

当 source 对象的对应字段为 null 时,defaultValue 指定的默认值会被放入 target 的对应字段。
通过字符串类型指定默认值,MapStruct 会自动转换基础类型以及基础类型的包装类型,其他类型会自动寻找是否存在匹配的类型转换方法。

比如可以写一个 Request2DO 的转换方法,直接通过 Mapper 上的 defaultValue 来指定接口入参的默认值

@Mapper
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    @Mapping(target = "booleanProperty", defaultValue = "false")
    Target sourceToTarget(Source s);
}

如果 s.getStringProp() == null 则 target 的 stringProperty 会被设置为 undefined
如果 s.getLongProp() == null 则 target 的 longProperty 会被设置为-1
如果 s.getBooleanProperty() == null 则 target 的 booleanProperty 字段会被设为 false


@MappingTarget 更新已存在的target对象

如果不想自动生成一个新的 target 实例,而是更新参数传入的 target 实例,可以给 target 增加 @MappingTarget 注解,此时返回的是传入的 target 对象

@Mapper
interface DeliveryAddressMapper {
    @Mapping(source = "address.postalcode", target = "postalcode")
    @Mapping(source = "address.county", target = "county")
    DeliveryAddress updateAddress(@MappingTarget DeliveryAddress deliveryAddress, Address address);
}

3.6. Updating existing bean instances
https://mapstruct.org/documentation/stable/reference/html/#updating-bean-instances

Using Multiple Source Objects with MapStruct
https://www.baeldung.com/mapstruct-multiple-source-objects


expression java表达式求值

使用 expression 可以写脚本表达式,目前只支持 java 语言,在表达式中可以对 source 对象进行操作。
注意必须保证 expression 表达式中 java 代码的正确性,MapStruct 生成时不对代码进行检查。

示例:
enabled 字段的值使用 source 的 enabled() 方法结果

@Mapper
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target = "nodes", expression = "java(new java.util.ArrayList())")
    @Mapping(target = "enabled", expression = "java(s.enabled())")
    @Mapping(target = "timeAndFormat", expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}

问题

MapStruct结合lombok @Builder导致丢失父类字段

目标 DO 类如下:

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user")
public class UserDO extends DefaultIdDO {
    private String name;
}

UserDO 继承的 DefaultIdDO 中有个 protected Long id; 公共属性, UserDO 的 builder 中并不会有这个属性(除非父子类都加 @SuperBuilder 才会有)。

MapStruct 产生的映射方法如下,使用了 lombok @Builder 产生的 builder,丢了 UserDO 继承的 id 字段。

UserDO updateReq2Do(UserUpdateRequest userUpdateRequest);

@Override
public UserDO updateReq2Do(UserUpdateRequest userUpdateRequest) {
    if ( userUpdateRequest == null ) {
        return null;
    }
    UserDOBuilder userDO = UserDO.builder();
    userDO.name( userUpdateRequest.getName() );
    return userDO.build();
}

解决:
1、去掉实体上的 @Builder , 迫使 MapStruct 使用 gettr/setter 实现映射。
2、目标父子类都加 @SuperBuilder , 使 lombok 自动生成的 builder 中有父类中的 id 字段。但如果父类是三方库中的则无法改动。
3、@Mapper 可以配置是否使用 builder 属性,关闭即可,关闭后会使用 gettr/setter 实现映射。(推荐)

@Mapper(builder = @Builder(disableBuilder = true))
public interface UserConverter {
}

Mapstruct中使用lombok@Builder的坑
https://www.jianshu.com/p/4f7b4f4bf689

3.8. Using builders
https://mapstruct.org/documentation/stable/reference/html/#mapping-with-builders


上一篇 Spring-Utils

下一篇 Spring-Data-MongoDB

阅读
评论
2k
阅读预计9分钟
创建日期 2021-05-27
修改日期 2023-07-22
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论