乐趣区

关于后端:对象转换工具-MapStruct-介绍

前言

在咱们日常开发的分层构造的应用程序中,为了各层之间相互解耦,个别都会定义不同的对象用来在不同层之间传递数据,因而,就有了各种 XXXDTOXXXVOXXXBO 等基于数据库对象派生进去的对象,当在不同层之间传输数据时,不可避免地常常须要将这些对象进行互相转换。

此时个别解决两种解决形式:① 间接应用 SetterGetter 办法转换、② 应用一些工具类进行转换(e.g. BeanUtil.copyProperties)。第一种形式如果对象属性比拟多时,须要写很多的 Getter/Setter 代码。第二种形式看起来尽管比第一种形式要简略很多,然而因为其应用了反射,性能不太好,而且在应用中也有很多陷阱。而明天要介绍的配角 MapStruct 在不影响性能的状况下,同时解决了这两种形式存在的毛病。

MapStruct 是什么

MapStruct 是一个代码生成器,它基于 约定优于配置 办法极大地简化了 Java bean 类型之间映射的实现。主动生成的映射转换代码只应用简略的办法调用,因而速度快、类型平安而且易于了解浏览,源码仓库 Github 地址 MapStruct。总的来说,有如下三个特点:

  1. 基于注解
  2. 在编译期主动生成映射转换代码
  3. 类型平安、高性能、无依赖性

MapStruct 应用步骤

MapStruct 的应用比较简单,只需如下三步即可。

① 引入依赖(这里以 Gradle 形式为例)

dependencies {
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

② 创立相干转换对象

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class DoctorDTO {

  private Integer id;

  private String name;
}

③ 创立转换器类(Mapper)

须要留神的是,转换器不肯定都要应用 Mapper 作为结尾,只是官网示例举荐以 XXXMapper 格局命名转换器名称,这里举例的是最简略的映射状况(字段名称和类型都齐全匹配),只须要在转换器类上增加 @Mapper 注解即可,转换器代码如下所示:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

  DoctorDTO toDTO(Doctor doctor);
}

通过上面这个简略的测试来校验转换后果是否正确,测试代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
public class DoctorTest {

  @Test
  public void testToDTO() {
    Integer doctorId = 9527;
    String doctorName = "mghio";

    Doctor doctor = new Doctor();
    doctor.setId(doctorId);
    doctor.setName(doctorName);

    DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor);

    assertEquals(doctorId, doctorDTO.getId());
    assertEquals(doctorName, doctorDTO.getName());
  }

}

测试后果失常通过,阐明应用 DoctorMapper 转换器达到咱们的预期后果。

MapStruct 实现浅析

在以上示例中,应用 MapStruct 通过简略的三步就实现了 DoctorDoctorDTO 的转换,那么,MapStruct 是如何做到的呢?其实通过咱们定义的转换器能够发现,转换器是接口类型的,而咱们晓得在 Java 中,接口是无奈提供性能的,只是定义标准,具体干活的还是它的实现类。

因而咱们能够大胆猜测,MapStruct 必定给咱们定义的转换器接口(DoctorMapper)生成了实现类,而通过 Mappers.getMapper(DoctorMapper.class) 获取到的转换器实际上是获取到了转化器接口的实现类。上面通过在测试类中 debug 来验证一下:

通过 debug 能够看出,DoctorMapper.INSTANCE 获取到的是接口的实现类 DoctorMapperImpl。这个转换器接口实现类是在 编译期 主动生成的,Gradle 我的项目是在 build/generated/sources/anotationProcessor/Java 下(Maven 我的项目在 target/generated-sources/annotations 目录下),生成以上示例转换器接口的实现类源码如下:

能够发现,主动生成的代码和咱们平时手写的差不多,简略易懂,代码齐全在编译期间生成,没有运行时依赖。和应用反射的实现形式相比还有一个有点就是,出错时很容易去 debug 实现源码来定位,而反射相对来说定位问题就要艰难得多了。

常见应用场景介绍

① 对象属性名称和类型完全相同

从上文的示例能够看出,当属性名称和类型完全一致时,咱们只须要定义一个转换器接口并增加 @Mapper 注解即可,而后 MapStruct 会主动生成实现类实现转换。示例代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  Target toTarget(Source source);
}

② 对象属性类型雷同然而名称不同

当对象属性类型雷同然而属性名称不一样时,通过 @Mapping 注解来手动指定转换。示例代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String sourceName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String targetName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);
}

③ 在 Mapper 中应用自定义转换方法

有时候,对于某些类型(比方:一个类的属性是自定义的类),无奈以主动生成代码的模式进行解决。此时咱们须要自定义类型转换的办法,在 JDK 7 之前的版本,就须要应用抽象类来定义转换 Mapper 了,在 JDK 8 以上的版本能够应用接口的 默认办法 来自定义类型转换的办法。示例代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String sourceName;

  private InnerSource innerSource;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerSource {

  private Integer deleted;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String targetName;

  private InnerTarget innerTarget;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerTarget {

  private Boolean isDeleted;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);

  default InnerTarget innerTarget2InnerSource(InnerSource innerSource) {if (innerSource == null) {return null;}
    InnerTarget innerTarget = new InnerTarget();
    innerTarget.setIsDeleted(innerSource.getDeleted() == 1);
    innerTarget.setName(innerSource.getName());
    return innerTarget;
  }
}

④ 多个对象转换成一个对象返回

在一些理论业务编码的过程中,不可避免地须要将多个对象转化为一个对象的场景,MapStruct 也能很好的反对,对于这种最终返回信息来源于多个类,咱们能够通过配置来实现多对一的转换。示例代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Data
public class Address {

  private String street;

  private Integer zipCode;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Mapper
public interface AddressMapper {AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

  @Mapping(source = "doctor.id", target = "personId")
  @Mapping(source = "address.street", target = "streetDesc")
  DeliveryAddressDTO doctorAndAddress2DeliveryAddressDTO(Doctor doctor, Address address);
}

从这个示例中的转换器(AddressMapper)能够看出,当属性名称和类型齐全匹配时同样能够主动转换,然而当起源对象有多个属性名称及类型齐全和指标对象雷同时,还是须要手动配置指定的,因为此时 MapStruct 也无奈精确判断应该应用哪个属性转换。

获取转换器(Mapper)的几种形式

获取转换器的形式依据 @Mapper 注解的 componentModel 属性不同而不同,反对以下四种不同的取值:

  1. default 默认形式,默认形式,应用工厂形式(Mappers.getMapper(Class) )来获取
  2. cdi 此时生成的映射器是一个应用程序范畴的 CDI bean,应用 @Inject 注解来获取
  3. spring Spring 的形式,能够通过 @Autowired 注解来获取,在 Spring 框架中举荐应用此形式
  4. jsr330 生成的映射器用 @javax.inject.Named@Singleton 注解,通过 @Inject 来获取

① 通过工厂形式获取

上文的示例中都是通过工厂形式获取的,也就是应用 MapStruct 提供的 Mappers.getMapper(Class<T> clazz) 办法来获取指定类型的 Mapper。而后在调用的时候就不须要重复创建对象了,办法的最终实现是通过咱们定义接口的类加载器加载 MapStruct 生成的实现类(类名称规定为:接口名称 + Impl),而后调用该类的无参结构器创建对象。外围源码如下所示:

② 应用依赖注入形式获取

对于依赖注入(dependency injection),应用 Spring 框架开发的敌人们应该很相熟了,工作中常常应用。MapStruct 也反对依赖注入的应用形式,并且官网也举荐应用依赖注入的形式获取。应用 Spring 依赖注入的形式只须要指定 @Mapper 注解的 componentModel = "spring" 即可,示例代码如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper(componentModel = "spring")
public interface SourceMapper {SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);

}  

咱们能够应用 @Autowired 获取的起因是 SourceMapper 接口的实现类曾经被注册为容器中一个 Bean 了,通过如下生成的接口实现类的代码也能够看到,在类上主动加上了 @Component 注解。

最初还有两个注意事项:① 当两个转换对象的属性不统一时(比方 DoctorDTO 中不存在 Doctor 对象中的某个字段),编译时会呈现正告提醒。能够在@Mapping 注解中配置 ignore = true,或者当不统一字段比拟多时,能够间接设置 @Mapper 注解的 unmappedTargetPolicy 属性或unmappedSourcePolicy 属性设置为 ReportingPolicy.IGNORE。② 如果你我的项目中也应用了 Lombok,须要留神一下 Lombok 的版本至多是 1.18.10 或者以上才行,否则会呈现编译失败的状况。刚开始用的时候我也踩到这个坑了。。。

总结

本文介绍了对象转换工具 Mapstruct 库,以平安优雅的形式来缩小咱们的转换代码。从文中的示例中能够看出,Mapstruct 提供了大量的性能和配置,使咱们可能以简略快捷的形式创立从简略到简单的映射器。文中所介绍到的只是 Mapstruct 库的冰山一角,还有很多弱小的性能文中没有提到,感兴趣的敌人能够自行查看 官网使用指南。

退出移动版