mapstruct使用与原理

731 阅读5分钟

一、mapstruct简介

mapstruct是一种实体类映射框架,能够通过Java注解将一个实体类的属性安全地赋值给另一个实体类。有了mapstruct,只需要定义一个映射器接口,声明需要映射的方法,在编译过程中,mapstruct会自动生成该接口的实现类,实现将源对象映射到目标对象的效果。

官网:mapstruct.org/

git:github.com/mapstruct/m…

二、快速使用

1、引入依赖

    <org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
    <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>

2、编写接口

@Mapper
public interface PersonMapper {
 
    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
 
    PersonDTO convert(Person person);
}
 
@Data
public class PersonDTO {
 
    private String describe;
 
    private String id;
 
    private String name;
 
    private int age;
 
    private BigDecimal source;
 
    private double height;
 
    private Date createTime;
 
}
 
@Data
public class Person {
 
    private String describe;
 
    private String id;
 
    private String name;
 
    private int age;
 
    private BigDecimal source;
 
    private double height;
 
    private Date createTime;
}

3、使用

@Test
public void convertTest() {
    Person person = new Person();
    person.setId("123");
    person.setAge(1);
    person.setName("wgj");
    person.setDescribe("描述");
    person.setHeight(123.12);
    person.setCreateTime(new Date());
    person.setSource(new BigDecimal("3.14"));
    PersonDTO convert = PersonMapper.INSTANCT.convert(person);
    System.out.println(JSON.toJSONString(convert));
}

三、用法

1、属性名称不同

@Data
public class Person {
    private String name;
}

@Data
public class PersonDTO {
    private String personName;
}

@Mapper
public interface PersonMapper {

    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "personName", source = "name")
    PersonDTO convert(Person person);

}

2、属性类型不同(1)

@Data
public class Person {
    private Date createTime;
}

@Data
public class PersonDTO {
    private String createTime;
}

@Mapper
public interface PersonMapper {

    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "createTime" ,source = "createTime", dateFormat = "yyyy-MM-dd")
    PersonDTO convert(Person person);

}

3、属性类型不同(2)

@Data
public class Person {
    private String age;
}
@Data
public class PersonDTO {
    private int age;
}

@Mapper
public interface PersonMapper {

    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);

    PersonDTO convert(Person person);
}

    @Test
    public void convertTest() {
        Person person = new Person();
        person.setAge("abc");
    }

4、指定默认值

@Mapping(target = "personName", source = "name", defaultValue = "金网络")
PersonDTO convert(Person person);

5、忽略属性

@Mapping(target = "id", ignore = true)// 忽略id,不进行映射
PersonDTO convert(Person person);

6、自定义类型转换方法

@Data
public class Person {
    private String createTime;
    private Date updateTime;
}

@Data
public class PersonDTO {
    private Date createTime;
    private String updateTime;
}

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);
        }
    }
}

@Mapper(uses = DateMapper.class)
public interface PersonMapper {
    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
    PersonDTO convert(Person person);
}

public class PersonMapperImpl implements PersonMapper {
    private final DateMapper dateMapper = new DateMapper();

    public PersonMapperImpl() {
    }

    public PersonDTO convert(Person person) {
        personDTO.setCreateTime(this.dateMapper.asDate(person.getCreateTime()));
        personDTO.setUpdateTime(this.dateMapper.asString(person.getUpdateTime()));
        return personDTO;
    }
}

7、使用其他的值

@Data
public class Person {
}

@Data
public class PersonDTO {
    private String other;
}

@Mapper(uses = DateMapper.class)
public interface PersonMapper {

    PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
    PersonDTO convert(Person person, String other);
}

PersonDTO personDTO = PersonMapper.INSTANCT.convert(person, "other");

8、@Qualifier:标记的自定义注解标记

public class DateFormtUtil {

    @DateFormat
    public static String plusDate(String date) {
        return DateUtils.plusDate(date, DateUtils.YYYY_MM_DD, 1);
    }

    @Qualifier
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.CLASS)
    public @interface DateFormat {
    }
}

@Mapping(target = "endBizTime", source = "bizEndDate", qualifiedBy = DateFormtUtil.DateFormat.class)
@Mapping(target = "endPaymentTime", source = "paymentEndDate", qualifiedBy = DateFormtUtil.DateFormat.class)
PaymentVO convertToPaymentVO(FeePaymentQueryRequest request);

9、@namd

@Named("plusDate")
public static String plusDate_(String date){
    return DateUtils.plusDate(date, DateUtils.YYYY_MM_DD, 1);
}

@Mapping(target = "endBizTime", source = "bizEndDate", qualifiedByName = "plusDate")
@Mapping(target = "endPaymentTime", source = "paymentEndDate", qualifiedByName = "plusDate")
PaymentVO convertToPaymentVO(FeePaymentQueryRequest request);

10、使用表达式

@Mapping(target = "endPaymentTime", expression = "java(DateUtils.plusDate(request.getPaymentEndDate(), DateUtils.YYYY_MM_DD, 1))")
PaymentVO convertToPaymentVO(FeePaymentQueryRequest request);

@Mapper(uses = {DateFormtUtil.class}, imports = {DateUtils.class})

11、组合映射-多个源对象

@Data
public class PersonDTO {

    private String describe;

    private String id;

    private String personName;

    private int age;

    private BigDecimal source;

    private double height;

    private Date createTime;

    private String other;
}

@Data
public class BasicEntity {

    private Date createTime;

    private String createBy;

    private Date updateTime;

    private String updateBy;

    private int _ROW;

}

@Mapping(target = "createTime", source = "basicEntity.createTime")
PersonDTO convertToPersonDTO(Person person, BasicEntity basicEntity);

12、嵌套映射

@Data
public class Person {
    private Child personChild;
}

@Data
public class PersonDTO {
    private Child child;
}

@Mapping(target = "child", source = "personChild")
PersonDTO convert(Person person);

13、numberFormat()

public class Person {
    private int age;
}

@Data
public class PersonDTO {
    private String age;
}

@Mapping(target = "age", source = "age", numberFormat = "#0.00")
PersonDTO convert(Person person);

14、逆映射

@Mapping(target = "child", source = "personChild")
@Mapping(target = "age", source = "age", numberFormat = "#0.00")
PersonDTO convert(Person person);


@InheritInverseConfiguration
Person convert(PersonDTO personDTO);

15、集合映射

@Data
public class Person {
    private String describe;
    private String id;
    private String name;
    private int age;
    private BigDecimal source;
    private double height;
    private String createTime;
    private Date updateTime;
    private Child personChild;
}

@Data
public class PersonDTO {
    private String describe;
    private String id;
    private String personName;
    private String age;
    private BigDecimal source;
    private double height;
    private Date createTime;
//    private String updateTime;
    private String other;
    private ChildDTO childDTO;
}

@Data
public class ChildDTO {
    private String childName;
    private String descDto;
}

@Data
public class Child {
    private String childName;
    private String desc;
}

@Data
public class PagePersonDTO {
    private List<PersonDTO> personDTOS;
    private int total;
    private int curr;
    private int size;
}

@Data
public class PagePerson {
    private List<Person> personList;
    private int total;
    private int curr;
    private int size;
}
@Mapper
public interface PagePersonConverter {
    PagePersonConverter INSTANCT = Mappers.getMapper(PagePersonConverter.class);


    @Mapping(target = "personDTOS", source = "personList")
    PagePersonDTO convert(PagePerson pagePerson);


    @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
    @Mapping(target = "childDTO", source = "personChild")
    @Mapping(target = "childDTO.descDto", source = "personChild.desc")
    PersonDTO personToPersonDTO(Person person);
}

16、集成到 spring

// 接口
@Mapper(componentModel = "spring")
public interface FeeBusinessConverter {
    OpenAccountVO convert(OpenAccountRequest request);

    PaymentAccountDTO convert(PaymentAccount paymentAccount);
}

// 使用
@RestController
@Slf4j
public class FeeController implements FeeClient {

    // 注入
    @Resource
    private FeeBusinessConverter feeBusinessConverter;

    // 使用
    @Override
    public R<PaymentAccountDTO> openAccount(OpenAccountRequest request) {
        // 省略
        OpenAccountVO openAccountVO = feeBusinessConverter.convert(request);
        // 省略
    }

}

四、原理

1、mapstruct原理

mapstruct是基于JSR 269实现的,JSR 269是JDK引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:

1、继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。
2、在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor

执行过程

image.png (1)、生成抽象语法树。Java编译器对Java源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。
(2)、调用实现了JSR 269 API的程序。只要程序实现了JSR 269 API,就会在编译期间调用实现的注解处理器。
(3)、修改抽象语法树。在实现JSR 269 API的程序中,可以修改抽象语法树,插入自己的实现逻辑。
(4)、生成字节码。修改完抽象语法树后,Java编译器会生成修改后的抽象语法树对应的字节码文件件。

2、mapstruct源码

image.png

public class MappingProcessor extends AbstractProcessor {


    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
        // nothing to do in the last round
        if ( !roundEnvironment.processingOver() ) {
            RoundContext roundContext = new RoundContext( annotationProcessorContext );

            // process any mappers left over from previous rounds
            Set<TypeElement> deferredMappers = getAndResetDeferredMappers();
            processMapperElements( deferredMappers, roundContext );

            // get and process any mappers from this round
            Set<TypeElement> mappers = getMappers( annotations, roundEnvironment );
            processMapperElements( mappers, roundContext );
        }
        else if ( !deferredMappers.isEmpty() ) {
            // If the processing is over and there are deferred mappers it means something wrong occurred and
            // MapStruct didn't generate implementations for those
            for ( DeferredMapper deferredMapper : deferredMappers ) {

                TypeElement deferredMapperElement = deferredMapper.deferredMapperElement;
                Element erroneousElement = deferredMapper.erroneousElement;
                String erroneousElementName;

                if ( erroneousElement instanceof QualifiedNameable ) {
                    erroneousElementName = ( (QualifiedNameable) erroneousElement ).getQualifiedName().toString();
                }
                else {
                    erroneousElementName =
                        erroneousElement != null ? erroneousElement.getSimpleName().toString() : null;
                }

                // When running on Java 8 we need to fetch the deferredMapperElement again.
                // Otherwise the reporting will not work properly
                deferredMapperElement = annotationProcessorContext.getElementUtils()
                    .getTypeElement( deferredMapperElement.getQualifiedName() );

                processingEnv.getMessager()
                    .printMessage(
                        Kind.ERROR,
                        "No implementation was created for " + deferredMapperElement.getSimpleName() +
                            " due to having a problem in the erroneous element " + erroneousElementName + "." +
                            " Hint: this often means that some other annotation processor was supposed to" +
                            " process the erroneous element. You can also enable MapStruct verbose mode by setting" +
                            " -Amapstruct.verbose=true as a compilation argument.",
                        deferredMapperElement
                    );
            }

        }

        return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
    }

}

3、lombok

image.png