简化属性拷贝插件 MapStructs 使用指北

发布时间 2023-12-26 16:14:28作者: charler。

MapStruct 使用指南

1、安装与介绍

what?

mapstruct 是一个代码生成器,可以简化实现java bean 之间的转换的配置方法

生成的代码使用传统的方法实现get set属性,比起反射更快、更简单、更安全,易于理解

why?

基于多层的应用经常需要映射不同的对象模型 如VO -> TDO 等;属性转换的代码重复且容易出错。

与其他映射框架相比,MapStruct在编译时生成bean映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。

how?

MapStruct是一个注释处理器,插入Java编译器,通过在命令行构建(Maven,Gradle等)时使用。并且实现了默认的映射关系,故便于使用。

非spring环境下安装使用

参考官网设置:

MapStruct – Java bean mappings, the easy way!

spring环境下安装使用

pom.xml中增加mapstruct 的相关依赖

        <!-- mapstruct 实体转换 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.5.5.Final</version>
        </dependency>

在pom文件的<plugins>标签中新增配置注解处理路径,项目中如果使用了lombok 需要注意其版本,如果版本高于 1.18.16,需要新增 lombok-mapstruct-binding 配置,兼容两者,低于 1.18.16 则不需要配置。

<build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.2.6.RELEASE</version>
            <configuration>
                <executable>true</executable>
                <mainClass>org.test.application</mainClass>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>1.18.16</version>
                    </path>
                    <!-- This is needed when using Lombok 1.18.16 and above -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

最后是在IDEA 中安装mapstrut支持插件 mapStructSupport,该插件提供未映射字段提醒,写好映射文件后自动生成实现类,无需手动编译查看实现类的逻辑是否正确的功能

至此安装完成。

2、简单对象映射

简单对象映射定义为映射前后对象的基本成员都是基础数据类型,该类对象映射只需要映射转换前后对象的成员名称即可,详见以下示例:

现有转换之前的对象 OriginalOcrEntity

@Data
public class OriginalOcrEntity {
    private String bill_number;
    private String bill_code;
    private String total_words;
    private String payer;
    List<ItemOriginal> items = new ArrayList<>();
}

需要将其转换为以下对象 ConvertOcrEntity 并且 orientation 字段值需要通过其他字段计算得到

@Data
@ToString
public class ConvertOcrEntity {
    private String code;
    private String number;
    private String total_cn;
    private String buyer;
    /**
     * 待计算字段
     */
    private String orientation;

    List<ItemConvert> items = new ArrayList<>();

需要编写一个映射接口:

1、接口上增加 @Mapper(componentModel = "spring" ) 将其交于spring 进行管理,这样可以支持直接@Autowired 获取实例,否则需要手动获取该接口的实例使用;

2、写转换方法,mapstruct 默认会将转换前后字段名一致的字段 get 到并 set 进新的对象中,只需要配置字段名不同的字段值即可;

使用 @Mappings({ }) 标注转换信息,内填写具体映射信息 @Mapping(target = "xx", source = "xx") ;

3、如果对象的成员对象是实例,或者List 则需要实现 对应实体 -> 新的实体的映射接口 以及List转换到新的List的接口,如 List items 转换为 List items,则需要写两个映射接口:

List<ItemConvert> ITEM_CONVERT_LIST(List<ItemOriginal> itemOriginals);

上面实现类的会生成调用下面接口的实现类的方法去做转换。

@Mappings({
        @Mapping(target = "name", source = "project_name"),
        @Mapping(target = "unit", source = "uom"),
        @Mapping(target = "amount", source = "total")
})
ItemConvert ITEM_CONVERT(ItemOriginal itemOriginal);

最后完整的映射接口如下:

/**
 * @ClassName Converter
 * @Description 简单的对象转换
 * @Date 2023/10/11
 */
@Mapper(componentModel = "spring" )
public interface CommonConverter {

    @Mappings({
            @Mapping(target = "code", source = "bill_code"),
            @Mapping(target = "number", source = "bill_number"),
            @Mapping(target = "total_cn", source = "total_words"),
            @Mapping(target = "buyer", source = "payer"),
            @Mapping(target = "items", source = "items")

    })
    ConvertOcrEntity CONVERT_OCR_ENTITY(OriginalOcrEntity entity);


    List<ItemConvert> ITEM_CONVERT_LIST(List<ItemOriginal> itemOriginals);
    
    @Mappings({
            @Mapping(target = "name", source = "project_name"),
            @Mapping(target = "unit", source = "uom"),
            @Mapping(target = "amount", source = "total")
    })
    ItemConvert ITEM_CONVERT(ItemOriginal itemOriginal);

}

编写完成并保存后,idea如果安装了插件,会自动生成对应的代码,否则可能需要手动执行 maven complie 生成对应实现类;点击接口实现类图标,能够跳转到生成的实现类,该实现类的内容就是简单的get与 set 逻辑:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-10-12T17:32:27+0800",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 1.8.0_45 (Oracle Corporation)"
)
@Component
public class CommonConverterImpl implements CommonConverter {

    @Override
    public ConvertOcrEntity CONVERT_OCR_ENTITY(OriginalOcrEntity entity) {
        if ( entity == null ) {
            return null;
        }

        ConvertOcrEntity convertOcrEntity = new ConvertOcrEntity();

        convertOcrEntity.setCode( entity.getBill_code() );
        convertOcrEntity.setNumber( entity.getBill_number() );
        convertOcrEntity.setTotal_cn( entity.getTotal_words() );
        convertOcrEntity.setBuyer( entity.getPayer() );
        // list对象转换 调用的 设置的 ITEM_CONVERT_LIST 接口的实现方法
        convertOcrEntity.setItems( ITEM_CONVERT_LIST( entity.getItems() ) );

        return convertOcrEntity;
    }

    @Override
    public List<ItemConvert> ITEM_CONVERT_LIST(List<ItemOriginal> itemOriginals) {
        if ( itemOriginals == null ) {
            return null;
        }

        List<ItemConvert> list = new ArrayList<ItemConvert>( itemOriginals.size() );
        for ( ItemOriginal itemOriginal : itemOriginals ) {
            // list 实际上 为单个转换方法 ITEM_CONVERT ,汇总后并返回
            list.add( ITEM_CONVERT( itemOriginal ) );
        }

        return list;
    }

    @Override
    public ItemConvert ITEM_CONVERT(ItemOriginal itemOriginal) {
        if ( itemOriginal == null ) {
            return null;
        }

        ItemConvert itemConvert = new ItemConvert();

        itemConvert.setName( itemOriginal.getProject_name() );
        itemConvert.setUnit( itemOriginal.getUom() );
        itemConvert.setAmount( itemOriginal.getTotal() );

        return itemConvert;
    }
}

可以看到,使用mapstruct 生成的实现类,无自己写的转换方法是一致的,能够减少大量写 get set方法的时间。

3、复杂对象映射

财务ocr识别结果转换的场景下存在复杂对象映射,主要针对,由于接口返回数据通常使用json 接收,并且部分字段值可能需要进行简单处理后才能保存到数据库中,json可以直接转为jsonObject ,jsonObject 底层使用的LinkedHashMap<String, Object> 存储的,故考虑直接传入LinkedHashMap<String,Object> 转换为所需对象,详见以下示例:

现通过接口返回的发票识别结果json串如下:

{
  "code": "6300161320",
  "number": "15064112",
  "code_confirm": "333",
  "number_confirm": "444",
  "date": "111",
  "pretax_amount": "222",
  "total": "444",
  "total_cn": "4123",
  "tax": "123",
  "check_code": "123",
  "machine_code": "123",
  "seller": "123",
  "seller_tax_id": "123",
  "seller_addr_tel": "123",
  "seller_bank_account": "123",
  "buyer": "123",
  "buyer_tax_id": "123123",
  "buyer_bank_account": "123123",
  "buyer_addr_tel": "31231",
  "company_seal": "123",
  "form_type": "12313",
  "form_name": "123",
  "kind": "123",
  "ciphertext": "123123",
  "travel_tax": "123123",
  "receiptor": "123",
  "reviewer": "12313",
  "issuer": "123",
  "place": "123",
  "province": "1231",
  "city": "123",
  "service_name": "132",
  "remark": "123",
  "item_names": "123",
  "agent_mark": "12312",
  "acquisition_mark": "123",
  "block_chain": "12313",
  "electronic_mark": "1231",
  "transit_mark": "1231",
  "oil_mark": "1231",
  "vehicle_mark": "12312",
  "title": "111",
  "items": [
    {
      "name": "222",
      "specification": "333",
      "unit": "1231",
      "quantity": "123",
      "price": "123",
      "total": "123",
      "tax_rate": "123",
      "tax": "1231"
    }
  ]
}

需要将该json串转换为以下实体

@Data
public class Invoice10101 {
    private String code;
    private String number;
    private String code_confirm;
    private String number_confirm;
    private String date;
    private String pretax_amount;
    private String total;
    private String total_cn;
    private String tax;
    private String check_code;
    private String machine_code;
    private String seller;
    private String seller_tax_id;
    private String seller_addr_tel;
    private String seller_bank_account;
    private String buyer;
    private String buyer_tax_id;
    private String buyer_bank_account;
    private String buyer_addr_tel;
    private String company_seal;/
    private String form_type;
    private String form_name;
    private String kind;
    private String ciphertext;
    private String travel_tax;
    private String receiptor;
    private String reviewer;
    private String issuer;
    private String place;
    private String province;
    private String city;
    private String service_name;
    private String remark;
    private String item_names;
    private String agent_mark;
    private String acquisition_mark;
    private String block_chain;
    private String electronic_mark;
    private String transit_mark;
    private String oil_mark;
    private String vehicle_mark;
    private String title;
    private List<Invoice10101Item> items;

}

Invoice10101Item 的结构如下:

@Data
public class Invoice10101Item {

    /** "*保险服务*保费", --货物或应税劳务、服务名称 */
    private String name;
    private String specification;
    private String unit;
    private String quantity;
    private String price;
    private String total;
    private String tax_rate;
    private String tax;
}

自定义converter内容如下:

使用 @Mapping(target = "total", expression = "java( xxx )" 指定该字段的调用java 函数处理,该处需要指定处理类的全限定名进行调用

如 ConversionUtil.removeRmbSymbol() 传入String ,去掉符号¥ ,将返回值设置成指定的target字段的值

/**
 * @InterfaceName Invoice10101Converter
 * @Description 增值税发票 转换器
 * @Date 2023/10/13
 */
@Mapper(componentModel = "spring")
public interface Invoice10101Converter {

    @Mappings({
            @Mapping(target = "pretax_amount", expression = "java( org.test.invoice.util.ConversionUtil.removeRmbSymbol((String) map.get(\"pretax_amount\")) )"),
            @Mapping(target = "total", expression = "java( org.test.invoice.util.ConversionUtil.removeRmbSymbol((String) map.get(\"total\")) )"),
            @Mapping(target = "tax", expression = "java( org.test.invoice.util.ConversionUtil.removeRmbSymbol((String) map.get(\"tax\")) )"),
            @Mapping(target = "items", source = "items")
    })
    Invoice10101 CONVERT_OCR_ENTITY (LinkedHashMap<String, Object> map);

    @Mappings({
            @Mapping(target = "name", expression = "java( org.test.invoice.util.ConversionUtil.removeRmbSymbol((String) map.get(\"total\")) )"),
            @Mapping(target = "tax", expression = "java( org.test.invoice.util.ConversionUtil.removeRmbSymbol((String) map.get(\"tax\")) )")
    })
    Invoice10101Item ITEM_CONVERT(Map<String, Object> map);

    //默认情况使用 String  直接返回
    default String map(Object o) {
        return String.valueOf(o);
    }

    // 设置 List<item> 对象 需要自定义逻辑
    default List<Invoice10101Item> CONVERTS(Object o) {
        List<Invoice10101Item> itemConverts = new ArrayList<>();
        JSONArray array =  (JSONArray) o;
        for (Object o1 : array) {
            Map map = ((JSONObject) o1).toJavaObject(Map.class);
            Invoice10101Item itemConvert = this.ITEM_CONVERT(map);
            itemConverts.add(itemConvert);
        }
        return itemConverts;
    }
}

测试转换结果:

--- 转换前
{
  "code": "111",
  "number": "222",
  "code_confirm": "333",
  "number_confirm": "444",
  "date": "111",
  ...
  "items": [
    {
      "name": "222",
      "specification": "333",
      "unit": "1231",
      "quantity": "123",
      "price": "123",
      "total": "123",
      "tax_rate": "123",
      "tax": "1231"
    }
  ]
}
--- 转换后
Invoice10101(code=111, number=222, code_confirm=333, number_confirm=444, date=111, pretax_amount=222, total=444, total_cn=4123, tax=123, check_code=123, machine_code=123, seller=123, seller_tax_id=123, seller_addr_tel=123, seller_bank_account=123, buyer=123, buyer_tax_id=123123, buyer_bank_account=123123, buyer_addr_tel=31231, company_seal=123, form_type=12313, form_name=123, kind=123, ciphertext=123123, travel_tax=123123, receiptor=123, reviewer=12313, issuer=123, place=123, province=1231, city=123, service_name=132, remark=123, item_names=123, agent_mark=12312, acquisition_mark=123, block_chain=12313, electronic_mark=1231, transit_mark=1231, oil_mark=1231, vehicle_mark=12312, title=111, items=[Invoice10101Item(name=123, specification=333, unit=1231, quantity=123, price=123, total=123, tax_rate=123, tax=1231)])

5、复用映射关系

有时部分映射关系可能被多个映射转换器使用,这时候可以提取该部分映射关系为一个新的converter,在其余使用到的接口文件引入提取的接口类,从而复用该部分映射关系。

提取item 转换关系到 ItemConverter ,并在CommonConverter 中uses 引入ItemConverter

/**
 * @InterfaceName ItemConverter
 * @Description ItemConverter
 * @Date 2023/10/13
 */
@Mapper(componentModel = "spring" )
public interface ItemConverter {
    @Mappings({
            @Mapping(target = "name", source = "project_name"),
            @Mapping(target = "unit", source = "uom"),
            @Mapping(target = "amount", source = "total")
    })
    ItemConvert ITEM_CONVERT(ItemOriginal itemOriginal);
}
/**
 * @ClassName Converter
 * @Description 简单的对象转换
 * @Date 2023/10/11
 */
@Mapper(componentModel = "spring" , uses = ItemConverter.class)
public interface CommonConverter {

    @Mappings({
            @Mapping(target = "code", source = "bill_code"),
            @Mapping(target = "number", source = "bill_number"),
            @Mapping(target = "total_cn", source = "total_words"),
            @Mapping(target = "buyer", source = "payer"),
            @Mapping(target = "items", source = "items")

    })
    ConvertOcrEntity CONVERT_OCR_ENTITY(OriginalOcrEntity entity);

    List<ItemConvert> ITEM_CONVERT_LIST(List<ItemOriginal> itemOriginals);

检查生成的class 文件,可以看到 自动autowired了所需的ItemConverter

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-10-13T15:44:29+0800",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 1.8.0_45 (Oracle Corporation)"
)
@Component
public class CommonConverterImpl implements CommonConverter {

    @Autowired
    private ItemConverter itemConverter;

    @Override
    public ConvertOcrEntity CONVERT_OCR_ENTITY(OriginalOcrEntity entity) {
        if ( entity == null ) {
            return null;
        }

        ConvertOcrEntity convertOcrEntity = new ConvertOcrEntity();

        convertOcrEntity.setCode( entity.getBill_code() );
        convertOcrEntity.setNumber( entity.getBill_number() );
        convertOcrEntity.setTotal_cn( entity.getTotal_words() );
        convertOcrEntity.setBuyer( entity.getPayer() );
        convertOcrEntity.setItems( ITEM_CONVERT_LIST( entity.getItems() ) );

        return convertOcrEntity;
    }

    @Override
    public List<ItemConvert> ITEM_CONVERT_LIST(List<ItemOriginal> itemOriginals) {
        if ( itemOriginals == null ) {
            return null;
        }

        List<ItemConvert> list = new ArrayList<ItemConvert>( itemOriginals.size() );
        for ( ItemOriginal itemOriginal : itemOriginals ) {
            list.add( itemConverter.ITEM_CONVERT( itemOriginal ) );
        }

        return list;
    }
}