MapperStruct的使用教程

描述

 

前言

相信绝大多数的业务开发同学,日常的工作都离不开写 getter、setter 方法。要么是将下游的 RPC 结果通过getter、setter 方法进行获取组装。要么就是将自己系统内部的处理结果通过 getter、setter 方法处理成前端所需要的 VO 对象。

 

public UserInfoVO originalCopyItem(UserDTO userDTO){
    UserInfoVO userInfoVO = new UserInfoVO();
    userInfoVO.setUserName(userDTO.getName());
    userInfoVO.setAge(userDTO.getAge());
    userInfoVO.setBirthday(userDTO.getBirthday());
    userInfoVO.setIdCard(userDTO.getIdCard());
    userInfoVO.setGender(userDTO.getGender());
    userInfoVO.setIsMarried(userDTO.getIsMarried());
    userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
    userInfoVO.setAddress(userDTO.getAddress());
    return userInfoVO;
}

 

传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些 BeanUtil 简约代码的方式:

 

public UserInfoVO utilCopyItem(UserDTO userDTO){
    UserInfoVO userInfoVO = new UserInfoVO();
    //采用反射、内省机制实现拷贝
    BeanUtils.copyProperties(userDTO, userInfoVO);
    return userInfoVO;
}

 

但是,像 BeanUtils 这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的 setter 方法、BeanUtils 的拷贝以及本次需要介绍的 mapperStruct 进行了一次对比。得到的耗时结果如下所示:(具体的运行代码请见附录)

运行次数 setter方法耗时 BeanUtils拷贝耗时 MapperStruct拷贝耗时
1 2921528(1) 3973292(1.36) 2989942(1.023)
10 2362724(1) 66402953(28.10) 3348099(1.417)
100 2500452(1) 71741323(28.69) 2120820(0.848)
1000 3187151(1) 157925125(49.55) 5456290(1.711)
10000 5722147(1) 300814054(52.57) 5229080(0.913)
100000 19324227(1) 244625923(12.65) 12932441(0.669)

以上单位均为毫微秒。括号内的为当前组件同 Setter 比较的比值。可以看到 BeanUtils 的拷贝耗时基本为 setter 方法的十倍、二十倍以上。而 MapperStruct 方法拷贝的耗时,则与 setter 方法相近。由此可见,简单的 BeanUtils 确实会给服务的性能带来很大的压力。而 MapperStruct 拷贝则可以很好的解决这个问题。

下面我们就来介绍一下 MapperStruct 这个能够很好提升我们代码效率的工具。

使用教程

maven依赖

首先要导入 mapStruct 的 maven 依赖,这里我们选择最新的版本 1.5.0.RC1。

 

...

    1.5.0.RC1

...

//mapStruct maven依赖

    
        org.mapstruct
        mapstruct
        ${org.mapstruct.version}
    

...
    
//编译的组件需要配置

    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.8.1
            
                1.8  
                1.8  
                
                    
                        org.mapstruct
                        mapstruct-processor
                        ${org.mapstruct.version}
                    
                     
                
            
        
    

 

在引入 maven 依赖后,我们首先来定义需要转换的 DTO 及 VO 信息,主要包含的信息是名字、年龄、生日、性别等信息。

 

@Data
public class UserDTO {
    private String name;

    private int age;

    private Date birthday;

    //1-男 0-女
    private int gender;

    private String idCard;

    private String phoneNumber;

    private String address;

    private Boolean isMarried;
}
@Data
public class UserInfoVO {
    private String userName;

    private int age;

    private Date birthday;

    //1-男 0-女
    private int gender;

    private String idCard;

    private String phoneNumber;

    private String address;

    private Boolean isMarried;
}

 

紧接着需要编写相应的mapper类,以便生成相应的编译类。

 

@Mapper
public interface InfoConverter {

    InfoConverter INSTANT = Mappers.getMapper(InfoConverter.class);

    @Mappings({
            @Mapping(source = "name", target = "userName")
    })
    UserInfoVO convert(UserDTO userDto);
}

 

需要注意的是,因为 DTO 中的 name 对应的其实是 VO 中的 userName。因此需要在 converter 中显式声明。在编写完对应的文件之后,需要执行 maven 的 complie 命令使得 IDE 编译生成对应的 Impl 对象。(自动生成)

RPC

到此,mapperStruct 的接入就算是完成了~。我们就可以在我们的代码中使用这个拷贝类了。

 

public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
    UserInfoVO userInfoVO = new UserInfoVO();
    userInfoVO = InfoConverter.INSTANT.convert(userDTO);
    return userInfoVO;
}

 

怎么样,接入是不是很简单~

FAQ

1、接入项目时,发现并没有生成对应的编译对象class,这个是什么原因?

答:可能的原因有如下几个:

忘记编写对应的 @Mapper 注解,因而没有生成

没有配置上述提及的插件 maven-compiler-plugin

没有执行 maven 的 Compile,IDE 没有进行相应编译

2、接入项目后发现,我项目内的 Lombok、@Data 注解不好使了,这怎么办呢?

由于 Lombok 本身是对 AST 进行修改实现的,但是 mapStruct 在执行的时候并不能检测到 Lombok 所做的修改,因此需要额外的引入 maven 依赖lombok-mapstruct-binding。

 

......
    1.5.0.RC1
    0.2.0
    1.18.20
......

......

    org.mapstruct
    mapstruct
    ${org.mapstruct.version}


    org.projectlombok
    lombok-mapstruct-binding
    ${lombok-mapstruct-binding.version}


    org.projectlombok
    lombok
    ${lombok.version}

 

更详细的,mapperStruct 在官网中还提供了一个实现 Lombok 及 mapStruct 同时并存的案例

「3、更多问题:」

欢迎查看MapStruct官网文档,里面对各种问题都有更详细的解释及解答。

实现原理

在聊到 mapstruct 的实现原理之前,我们就需要先回忆一下 JAVA 代码运行的过程。大致的执行生成的流程如下所示:

RPC

可以直观的看到,如果我们想不通过编码的方式对程序进行修改增强,可以考虑对抽象语法树进行相应的修改。而mapstruct 也正是如此做的。具体的执行逻辑如下所示:

RPC

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

继承 AbstractProcessor 类,并且重写 process 方法,在 process 方法中实现自己的注解处理逻辑。

在 META-INF/services 目录下创建 javax.annotation.processing.Processor 文件注册自己实现的 Annotation Processor。

通过实现AbstractProcessor,在程序进行 compile 的时候,会对相应的 AST 进行修改。从而达到目的。

 

public void compile(List sourceFileObjects,
                    List classnames,
                    Iterable processors)
{
    if (processors != null && processors.iterator().hasNext())
        explicitAnnotationProcessingRequested = true;
    // as a JavaCompiler can only be used once, throw an exception if
    // it has been used before.
    if (hasBeenUsed)
        throw new AssertionError("attempt to reuse JavaCompiler");
    hasBeenUsed = true;

    // forcibly set the equivalent of -Xlint:-options, so that no further
    // warnings about command line options are generated from this point on
    options.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");
    options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);

    start_msec = now();

    try {
        initProcessAnnotations(processors);

        //此处会调用到mapStruct中的processor类的方法.
        delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);

        delegateCompiler.compile2();
        delegateCompiler.close();
        elapsed_msec = delegateCompiler.elapsed_msec;
    } catch (Abort ex) {
        if (devVerbose)
            ex.printStackTrace(System.err);
    } finally {
        if (procEnvImpl != null)
            procEnvImpl.close();
    }
}

 

关键代码,在mapstruct-processor包中,有个对应的类MappingProcessor继承了 AbstractProcessor,并实现其 process 方法。通过对 AST 进行相应的代码增强,从而实现对最终编译的对象进行修改的方法。

 

@SupportedAnnotationTypes({"org.mapstruct.Mapper"})
@SupportedOptions({"mapstruct.suppressGeneratorTimestamp", "mapstruct.suppressGeneratorVersionInfoComment", "mapstruct.unmappedTargetPolicy", "mapstruct.unmappedSourcePolicy", "mapstruct.defaultComponentModel", "mapstruct.defaultInjectionStrategy", "mapstruct.disableBuilders", "mapstruct.verbose"})
public class MappingProcessor extends AbstractProcessor {
    public boolean process(Set annotations, RoundEnvironment roundEnvironment) {
        if (!roundEnvironment.processingOver()) {
            RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
            Set deferredMappers = this.getAndResetDeferredMappers();
            this.processMapperElements(deferredMappers, roundContext);
            Set mappers = this.getMappers(annotations, roundEnvironment);
            this.processMapperElements(mappers, roundContext);
        } else if (!this.deferredMappers.isEmpty()) {
            Iterator var8 = this.deferredMappers.iterator();

            while(var8.hasNext()) {
                MappingProcessor.DeferredMapper deferredMapper = (MappingProcessor.DeferredMapper)var8.next();
                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;
                }

                deferredMapperElement = this.annotationProcessorContext.getElementUtils().getTypeElement(deferredMapperElement.getQualifiedName());
                this.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 false;
    }
}

 

「如何断点调试:」

因为这个注解处理器是在解析->编译的过程完成,跟普通的 jar 包调试不太一样,maven 框架为我们提供了调试入口,需要借助 maven 才能实现 debug。所以需要在编译过程打开 debug 才可调试。

在项目的 pom 文件所在目录执行 mvnDebug compile

接着用 idea 打开项目,添加一个 remote,端口为 8000

打上断点,debug 运行 remote 即可调试。

RPC

附录

测试代码如下,采用Spock框架 + JAVA代码 实现。Spock框架作为当前最火热的测试框架,你值得学习一下。Spock框架初体验:更优雅地写好你的单元测试

 

//    @Resource
    @Shared
    MapperStructService mapperStructService

    def setupSpec() {
        mapperStructService = new MapperStructService()
    }

    @Unroll
    def "test mapperStructTest times = #times"() {
        given: "初始化数据"
        UserDTO dto = new UserDTO(name: "笑傲菌", age: 20, idCard: "1234",
                phoneNumber: "18211932334", address: "北京天安门", gender: 1,
                birthday: new Date(), isMarried: false)

        when: "调用方法"
//        传统的getter、setter拷贝
        long startTime = System.nanoTime();
        UserInfoVO oldRes = mapperStructService.originalCopyItem(dto, times)
        Duration originalWasteTime = Duration.ofNanos(System.nanoTime() - startTime);

//        采用工具实现反射类的拷贝
        long startTime1 = System.nanoTime();
        UserInfoVO utilRes = mapperStructService.utilCopyItem(dto, times)
        Duration utilWasteTime = Duration.ofNanos(System.nanoTime() - startTime1);

        long startTime2 = System.nanoTime();
        UserInfoVO mapStructRes = mapperStructService.newCopyItem(dto, times)
        Duration mapStructWasteTime = Duration.ofNanos(System.nanoTime() - startTime2);

        then: "校验数据"
        println("times = "+ times)
        println("原始拷贝的消耗时间为: " + originalWasteTime.getNano())
        println("BeanUtils拷贝的消耗时间为: " + utilWasteTime.getNano())
        println("mapStruct拷贝的消耗时间为: " + mapStructWasteTime.getNano())
        println()

        where: "比较不同次数调用的耗时"
        times || ignore
        1     || null
        10    || null
        100   || null
        1000  || null
    }

 

测试的Service如下所示:

 

public class MapperStructService {

    public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
        UserInfoVO userInfoVO = new UserInfoVO();
        for (int i = 0; i < times; i++) {
            userInfoVO = InfoConverter.INSTANT.convert(userDTO);
        }
        return userInfoVO;
    }

    public UserInfoVO originalCopyItem(UserDTO userDTO, int times) {
        UserInfoVO userInfoVO = new UserInfoVO();
        for (int i = 0; i < times; i++) {
            userInfoVO.setUserName(userDTO.getName());
            userInfoVO.setAge(userDTO.getAge());
            userInfoVO.setBirthday(userDTO.getBirthday());
            userInfoVO.setIdCard(userDTO.getIdCard());
            userInfoVO.setGender(userDTO.getGender());
            userInfoVO.setIsMarried(userDTO.getIsMarried());
            userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
            userInfoVO.setAddress(userDTO.getAddress());
        }
        return userInfoVO;
    }

    public UserInfoVO utilCopyItem(UserDTO userDTO, int times) {
        UserInfoVO userInfoVO = new UserInfoVO();
        for (int i = 0; i < times; i++) {
            BeanUtils.copyProperties(userDTO, userInfoVO);
        }
        return userInfoVO;
    }
}

 

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分