前语

为了更好的进行开发和维护,咱们都会对程序进行分层规划,例如常见的三层,四层,每层各司其职,相互配合。也随着分层,呈现了 VO,BO,PO,DTO,每层都会处理自己的数据目标,然后向上传递,这就防止不了常常要将一个目标的特点复制给另一个目标。

例如我有一个 User 目标和一个 UserVO 目标,要将 User 目标的10个特点赋值个 UserVO 的同名特点:

  • 一种办法是手写,一个特点一个特点赋值,相信大家最开端学习时都是这么干的,这种办法便是太低效了。
  • 在 idea 中能够装置插件帮咱们快速生成 set 特点代码,尽管仍是逐个特点赋值,但比一个个敲,功率提高了许多。

上面两种办法尽管最原始,做起来很麻烦,简略犯错,但程序运转功率是最高的,现在仍有不少公司要求这么做,一是这样运转功率高,二是不需要引入其它的组件,防止呈现其它问题。

但关于咱们来说,这种操作要是多了,开发功率和代码可维护性都会受到影响,这种赋值特点代码很长,看起来很不舒畅,所以有了下面几种办法。

bean copier

apache 的 BeanUtils,内部运用了反射,功率很低,在《阿里java开发标准中》明令禁止运用,这儿就不过多评论。

是时候丢掉 BeanUtils 了!

spring的BeanUtils,对 apache BeanUtils 做了优化,运转功率较高,能够运用。

BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties(source, target, "id", "createTime"); //不复制指定的字段

cglib 的 BeanCopier,运用动态技能代替反射,在运转时生成一个子类,只需在第一次动态生成类时慢,后面基本就本接近原始的set,所以呀运转功率比上面两种要高许多。

BeanCopier beanCopier = BeanCopier.create(SourceData.class, TargetData.class, false);
beanCopier.copy(source, target, null);

咱们运用的是Spring BeanUtils,至少呈现过两次问题:

  • 一次是复制一方的目标类型变了,由int变成long,source.id int 复制到 target.id long 结果是空,由于类型不匹配,BeanUtils 不会复制。由所以运用反射,所以当时修改类型时,只修改了编译报错的当地,忘掉这种办法,导致结果都是空,这也很难怪开发,这种办法太隐蔽了。相同假如特点重命名,也会得到一个空,而且只能在运转时发现。
  • 另一次复制的时候会把一切特点都拷过去,漏掉疏忽主键 id,结果在插入的时候报了唯一索引抵触。咱们的场景比较特别,id,createTime,updateTime 这三个字段是表必须有的,一般也是不能被复制的,假如每个当地都手写疏忽,代码比较麻烦也简略忘掉。

上面3种办法都十分简略,意味着功用十分有限,假如你有一些复杂场景的复制,它们就无法支撑,例如深复制,复制一个 List。
别的一个最重要的点是:它们都是运转时的,这意味着你无法在编译时得到任何协助,无法提早发现问题。
从标题能够看出咱们本篇要讲的是另一个 copier:MapStruct,接下来就看下它是如何解决咱们问题的。

MapStruct

MapStruct 是一个根据 Java 注解处理器,用于生成类型安全且高性能的映射器。总结一下它有以下长处:

  1. 高性能。运用普通办法赋值,而非反射,MapStruct 会在编译期间生成类,运用原生的 set 办法进行赋值,所以功率和手写 set 基本是相同的。
  2. 类型安全。MapStruct 是编译时的,所以一旦有类型、称号等不匹配问题,就能够提早编译报错。
  3. 功用丰厚。MapStruct 的功用十分丰厚,例如支撑深复制,指定各种复制行为。
  4. 运用简略。你所需要做的便是定义接口和复制的行为,MapStruct 会在编译期生成完成类。

示例

和学习其它组件相同,咱们先用起来,准备两个类,SourceData,TargetData 特点彻底相同,其间 TestData 是另一个类。

public class SourceData {
    private String id;
    private String name;
    private TestData data;
    private Long createTime;
    public String getId() {
            return id;
    }
    public void setId(String id) {
            this.id = id;
    }
    public String getName() {
            return name;
    }
    public void setName(String name) {
            this.name = name;
    }
    public TestData getData() {
            return data;
    }
    public void setData(TestData data) {
            this.data = data;
    }
    public Long getCreateTime() {
            return createTime;
    }
    public void setCreateTime(Long createTime) {
            this.createTime = createTime;
    }
}

导入包 pom

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</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>

定义接口

这儿的 Mapper 是 MapStruct 的,可不是 Mybatis 的。

@Mapper
public interface BeanMapper {
    BeanMapper INSTANCE = Mappers.getMapper(BeanMapper.class);
    TargetData map(SourceData source);
}

运用

SourceData source = new SourceData();
source.setId("123");
source.setName("abc");
source.setCreateTime(System.currentTimeMillis());
TestData testData = new TestData();
testData.setId("123");
TargetData target = BeanMapper.INSTANCE.map(source);
System.out.println(target.getId() + ":" + target.getName() + ":" + target.getCreateTime());
//true
System.out.println(source.getData() == target.getData());

能够看到运用十分简略,默许情况下 MapStruct 是浅复制,所以看到最终一个输出是 true。编译后咱们能够在 target 目录下找到帮咱们生成的一个接口完成类 BeanMapperImpl,如下:

是时候丢掉 BeanUtils 了!

深复制

能够看到它也是帮生成 set 代码,且默许是浅复制,所以上面最终一个输出是 true。假如想变成深复制,在 map 办法上符号一下 DeepClone 即可:

@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);

从头编译一下,看到生成的代码变成如下,这次是深复制了。

是时候丢掉 BeanUtils 了!

集合复制

支撑,新增一个接口办法即可。

List<TestData> map(List<TestData> source);

类型不一致

假如我将 TargetData 的 createTime 改成 int 类型,再编译一下,生成代码如下:

是时候丢掉 BeanUtils 了!

能够看到它会默许帮咱们转化,但这是个躲藏的问题,假如我希望它能在编译时就提示,那么能够在 Mapper 注解上指定一些类型转化的策略是报错,如下:

@Mapper(typeConversionPolicy = ReportingPolicy.ERROR)

从头编译会提示错误:

java: Can't map property "Long createTime". It has a possibly lossy conversion from Long to Integer.

禁止隐式转化

假如我将类型改成 String 呢,编译又正常了,生成代码如下:

是时候丢掉 BeanUtils 了!

关于 String 和其它根底类型的包装类,它会隐式帮咱们转化,这也是个躲藏问题,假如我希望它能在编译时就提示,能够定义一个注解,并在 Mapper 中指定它,如下:

@Retention(RetentionPolicy.CLASS)
@MappingControl(MappingControl.Use.DIRECT)
@MappingControl(MappingControl.Use.MAPPING_METHOD)
@MappingControl(MappingControl.Use.COMPLEX_MAPPING)
public @interface ConversationMapping {
}
@Mapper(typeConversionPolicy = ReportingPolicy.ERROR, mappingControl = ConversationMapping.class)

从头编译会提示报错:

java: Can't map property "Long createTime" to "String createTime". Consider to declare/implement a mapping method: "String map(Long value)".

这个能够拜见 issus 上的评论:issus1428 issus3186

疏忽指定字段

疏忽字段能够运用 Mapping 注解的 ignore 特点,如下:

@Mapping(target = "id", ignore = true)

假如我想疏忽某些字段,而且复用起来,就像咱们的场景使用,能够定义一个IgnoreFixedField注解,然后打在办法上

@Mapping(target = "id", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Target(METHOD)
@Retention(RUNTIME)
@Documented
@interface IgnoreFixedField {
}
@IgnoreFixedField
@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);

这样只需打上这个注解,这3个字段就不会复制了。

与 lombok 集成

假如你的项目运用了 lombok,上面的代码或许没法正常工作。需要在 maven 对 lombok 也做下配置,在上面的 annotationProcessorPaths 参加如下配置即可。

<path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</path>

上面仅仅结合自己的实际场景的一些例子,MapStruct 还有更多的功用,拜见官方文档。

总结

会用之后咱们能够学习一下它的原理了,这也是咱们平时学习一个新的东西的习气,别一下子就扎到原理,源码里头,这样会严重冲击学习热心,要先跑起来先,看到成果后你会更有热情学习下去。

其实 MapStruct 的原理和 lombok 是相同的,都是在编译期间生成代码,而不会影响运转时。例如咱们最常见的 @Data 注解,查看源文件你会发现 getter/setter 生成了,源文件的类不会有 @Data 注解。

java 代码编译和履行的整个进程包括三个主要机制:1.java源码编译机制 2.类加载机制 3.类履行机制。 其间 java 源码编译由3个进程组成:1.分析和输入到符号表 2.注解处理 3.语义分析和生成class文件。 如下:

是时候丢掉 BeanUtils 了!

其间 annotation processing 便是注解处理,jdk7 之前选用 APT技能,之后的版别运用了 JSR 269 API。
JSR 是什么?java Specification Requests,Java 标准提案,是指向 JCP(Java Community Process)提出新增一个标准化技能标准的正式请求。jsr 269 是什么?在这儿

注解咱们十分熟悉,其实java里的注解有两种,一种是运转时注解,如常用 @Resource, @Autowired,另一种是编译时注解,如 lombok 的 @Data。

编译时注解主要作用是在编译期间生成代码,这样就能够防止在运转时运用反射。编译时注解处理中心接口是 Processor,它有一个笼统完成类 AbstractProcessor 封装了许多功用,假如要完成继承它即可。

知道原理后,咱们彻底能够仿照 lombok 写一个简略的生成器,更多信息能够参考这篇文章:Java编译期注解处理器AbstractProcessor

关于性能,知道原理后其实你也知道根本不用忧虑mapstruct的性能问题了,能够参考这个:benchmark

假如要说它的缺点,便是得为了这个简略的复制功用导这个包,假如你的程序只需很少的复制,那手动写一下也未尝不可,假如有大量复制需求,那就引荐运用了。

来历:https://www.cnblogs.com/jtea/p/17592696.html\color{#E9EBEC}{来历:https://www.cnblogs.com/jtea/p/17592696.html}