Mapstruct的具体介绍

Mapstruct的具体介绍



问题

微服务架构下,实例类基本上会拆分成 VO、DTO、Entity三类 POJO,各个公司的规范大同小异
VO: 用于前端接口参数传递,例如用于 http 接口接收请求参数,或者接口返回参数
DTO:用于 rpc 接口参数传递。单独定义,或者继承扩展其他 rpc 接口的 DTO,或者上线文内使用
Entity(PO): 用于 orm 映射处理,与表结构对应,只在服务内部使用,不能对外使用和暴露
往往在使用的时候会涉及到实体和实体之间的转换


前言

官方文档往往是最好的学习参考资料
官网地址https://mapstruct.org/documentation/stable/reference/html/

一、MapStruct是什么?

MapStruct 是一个 Java 注解处理器,用于生成类型安全的 Bean 映射类。

您所要做的就是定义一个映射器接口,该接口声明任何必需的映射方法。在编译过程中,MapStruct 将生成此接口的实现。此实现使用纯 Java 方法调用在源对象和目标对象之间进行映射,即无反射或类似。

与手动编写映射代码相比,MapStruct 通过生成繁琐且容易出错的代码来节省时间。遵循约定而不是配置方法,MapStruct 使用合理的默认值,但在配置或实现特殊行为时会不妨碍您。

与动态映射框架相比,MapStruct 具有以下优势:

  • 通过使用普通方法调用而不是反射来快速执行
  • 编译时类型安全:只能映射相互映射的对象和属性,不会意外地将订单实体映射到客户实体等。
  • 在构建时清除错误报告
    • 如果映射不完整(并非所有目标属性都映射)
    • 映射不正确(找不到正确的映射方法或类型转换)

二、依赖maven

...

<properties>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>
...
<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>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
...

三、定义映射器

3.1 基本映射

要创建映射器,只需使用所需的映射方法定义一个 Java 接口,然后使用 org.mapstruct.Mapper 注释对其进行注释:


/**
  * 1. 用于像静态方式使用
  * 2. 用于spring方式注入使用
  */
@Mapper
public interface CarMapper {
    // 用于静态导入
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    Car carCovertCarVO(CarVO carVO);
}

@Mapper(componentModel = "spring") //用于spring方式注入
public interface CarMapper {
    // 用于静态导入
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    Car carCovertCarVO(CarVO carVO);
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Car implements Serializable {
    private Long id;
    private String name;
    private Integer age;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CarVO implements Serializable {
    private Long id;
    private String name;
    private Integer age;
}

效果

public class CarMapperImpl implements CarMapper {
    @Override
    public Car carCovertCarVO(CarVO carVO) {
        if ( carVO == null ) {
            return null;
        }
        Car.CarBuilder car = Car.builder();
        car.id( carVO.getId() );
        car.name( carVO.getName() );
        car.age( carVO.getAge() );
        return car.build();
    }
}

3.2 名称对不上的映射

名称对不上的映射需要使用target代码需要转换后的值,source代表源数据

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CarDTO implements Serializable {
    private Long ids;
    private String userName;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    Car carCovertCarVO(CarVO carVO);
    @Mappings({
            @Mapping(target = "id", source = "ids"),
            @Mapping(target = "name", source = "userName")
    })
    Car carCovertCarDTO(CarDTO carDTO);
}

最后效果

    @Override
    public Car carCovertCarDTO(CarDTO carDTO) {
        if ( carDTO == null ) {
            return null;
        }
        Car.CarBuilder car = Car.builder();
        car.id( carDTO.getIds() );
        car.name( carDTO.getUserName() );
        return car.build();
    }

@Mapper 注释会导致 MapStruct 代码生成器在构建时创建 CarMapper 接口的实现。

在生成的方法实现中,源类型(例如 Car )中的所有可读属性都将复制到目标类型(例如 CarDto )中的相应属性中:
当属性与其目标实体对应项具有相同的名称时,它将被隐式映射。

必须在 @Mapping 注释中指定 JavaBeans 规范中定义的属性名称,例如,对于具有访问器方法 getSeatCount() 和 setSeatCount() 的属性,必须指定 seatCount。

通过 @BeanMapping(ignoreByDefault = true) ,默认行为将是显式映射,这意味着所有映射都必须通过 @Mapping 指定,并且不会对缺少的目标属性发出警告。这允许忽略所有字段,但通过 @Mapping 显式定义的字段除外

3.3 list对象的映射

    @Mappings({
            @Mapping(target = "id", source = "ids"),
            @Mapping(target = "name", source = "userName")
    })
    @Named("carCovertCarDTO")
    Car carCovertCarDTO(CarDTO carDTO);
	// 普通list转换
    List<Car> carCovertCarVOList(List<CarVO> carVO);
    
    // list自定义转换方式
    @IterableMapping(qualifiedByName = "carCovertCarDTO")
    List<Car> carCovertCarDTOList (List<CarDTO> carVO);

效果
    @Override
    public List<Car> carCovertCarVOList(List<CarVO> carVO) {
        if ( carVO == null ) {
            return null;
        }

        List<Car> list = new ArrayList<Car>( carVO.size() );
        for ( CarVO carVO1 : carVO ) {
            list.add( carCovertCarVO( carVO1 ) );
        }

        return list;
    }

    @Override
    public List<Car> carCovertCarDTOList(List<CarDTO> carVO) {
        if ( carVO == null ) {
            return null;
        }

        List<Car> list = new ArrayList<Car>( carVO.size() );
        for ( CarDTO carDTO : carVO ) {
            list.add( carCovertCarDTO( carDTO ) );
        }

        return list;
    }

3.4 更多基础映射

// 多参数映射

// 具有多个源参数的映射方法
@Mapper
public interface AddressMapper {
    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "address.houseNo")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

// 直接引用源参数的映射方法
@Mapper
public interface AddressMapper {
    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "hn")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
// 如果不想显式命名嵌套源 Bean 中的所有属性,则可以使用 . 作为目标。这将告诉 MapStruct 将每个属性从源 Bean 映射到目标对象。示例如下
// 使用“Target This”注释”。
// 生成的代码将直接将每个属性从 CustomerDto.record 映射到 Customer ,而无需手动命名其中任何一个。 Customer.account 也是如此。
 @Mapper
 public interface CustomerMapper {
     @Mapping( target = "name", source = "record.name" )
     @Mapping( target = ".", source = "record" )
     @Mapping( target = ".", source = "account" )
     Customer customerDtoToCustomer(CustomerDto customerDto);
 }
// 当存在冲突时,可以通过显式定义映射来解决这些问题。
// 例如,在上面的例子中。 name 出现在 CustomerDto.record 和 CustomerDto.account 中。
// 映射 @Mapping( target = "name", source = "record.name" ) 解决了此冲突。

3.5 高级映射

这里介绍了几个高级选项,这些选项允许根据需要微调生成的映射代码的行为


// 例:具有默认值和常量的映射方法
@Mapper(uses = StringListMapper.class)
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 = "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);
}

// Expressions 表达式
// 使用表达式的映射方法
@Mapper
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    @Mapping(target = "timeAndFormat", expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}
// 该示例演示如何将源属性 time 和 format 组合成一个目标属性 TimeAndFormat 。
// 请注意,指定了完全限定的包名称,因为 MapStruct 不负责 TimeAndFormat 类的导入
// (除非在 SourceTargetMapper 类中以其他方式显式使用)。这可以通过在 @Mapper 注释上定义 imports 来解决。
// 例如
@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    @Mapping(target = "timeAndFormat", expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}

//  默认表达式
// 使用默认表达式的映射方法
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    Target sourceToTarget(Source s);
}

// 使用项目中某个工具类的某个方法
// 例:使用StringUtils isEmptyIfStr方法
public class StringUtils{
	   public static boolean isEmptyIfStr(Object obj) {
        if (null == obj) {
            return true;
        } else if (obj instanceof CharSequence) {
            return 0 == ((CharSequence)obj).length();
        } else {
            return false;
        }
    }
}

@Mapper( imports = StringUtils.class ) // 这里引入class
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    @Mapping(target="id", source="sourceId", defaultExpression = "java( StringUtils.isEmptyIfStr() )")
    Target sourceToTarget(Source s);
}

// 指定水果映射的子类映射
@Mapper
public interface FruitMapper {
	// 当输入类型和结果类型都具有继承关系时,您希望将正确的专用化映射到匹配的专用化。
	// 假设一个 Apple 和一个 Banana ,它们都是 Fruit 的子类
    @SubclassMapping( source = AppleDto.class, target = Apple.class )
    @SubclassMapping( source = BananaDto.class, target = Banana.class )
    Fruit map( FruitDto source );
}

// 条件映射
@Mapper
public interface CarMapper {
    CarDto carToCarDto(Car car);
    @Condition
    default boolean isNotEmpty(String value) {
        return value != null && !value.isEmpty();
    }
}

// 生成的条件映射
public class CarMapperImpl implements CarMapper {
    @Override
    public CarDto carToCarDto(Car car) {
        if ( car == null ) {
            return null;
        }
        CarDto carDto = new CarDto();
        if ( isNotEmpty( car.getOwner() ) ) {
            carDto.setOwner( car.getOwner() );
        }
        // Mapping of other properties
        return carDto;
    }
}

// 带@BeforeMapping和@AfterMapping
// 在自定义映射器时,装饰器可能并不总是满足需求。
// 例如,如果不仅需要对少数选定方法执行自定义,还需要对映射特定超类型的所有方法执行自定义:
// 在这种情况下,可以使用在映射开始之前或映射完成后调用的回调方法。
@Mapper
public abstract class VehicleMapper {
    @BeforeMapping
    protected void flushEntity(AbstractVehicle vehicle) {
        // I would call my entity manager's flush() method here to make sure my entity
        // is populated with the right @Version before I let it map into the DTO
    }
    @AfterMapping
    protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
        result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
    }
    public abstract CarDto toCarDto(Car car);
}

public class VehicleMapperImpl extends VehicleMapper {
    public CarDto toCarDto(Car car) {
        flushEntity( car );
        if ( car == null ) {
            return null;
        }
        CarDto carDto = new CarDto();
        // attributes mapping ...

        fillTank( car, carDto );

        return carDto;
    }
}

问题

mapstruct编译后的实现类未进行get Set处理

public class CarMapperImpl implements CarMapper {

    @Override
    public Car carCovertCarVO(CarVO carVO) {
        if ( carVO == null ) {
            return null;
        }
        Car.CarBuilder car = Car.builder();
        return car.build();
    }
}

解决方案:
项目中是否引入了lombok ,如果引入了lombok,就是lombok版本和mapstruct版本不一致造成的,修改版本即可
从MapStruct 1.2.0. Beta1和Lombok 1.16.14版本开始,并分两种情况
1、如果您使用的是Lombok 1.18.16或更新版本,您还需要添加lombok-mapstruct-binding才能使Lombok和MapStruct一起工作。
2、如果您使用的是较旧的MapStruct或Lombok版本,解决方案是将要由Lombok修改的JavaBeans和要由MapStruct处理的映射器接口分成您项目的两个单独模块。然后Lombok将在第一个模块的编译期间运行,在第二个模块的编译期间运行MapStruct时,使bean类完整。
3. 或者升级mapStuct版本,我从1.5.3.final升级到1.5.5.final解决


END: 本文仅仅简单介绍了mapstruct的使用,更多的往往还是需要看官网的例子

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值