java注解处理器之MapStruct

介绍

MapStruct 是一个可以生成类型安全的,高性能的且无依赖的 JavaBean 映射代码的注解处理器,可以在编译期生成对应的 mapping,既没有 BeanUtils 等工具使用反射的性能问题,又免去了自己写映射代码的繁琐。

使用

简单转换

maven 依赖

复制<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct-jdk8</artifactId>
   <version>1.3.0.Final</version>
</dependency>
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct-processor</artifactId>
   <version>1.3.0.Final</version>
</dependency>

先定义两个 entity

@Data
public class Source {

private String id;

private Integer num;

private Integer count;
}

复制@Data
public class Target {

  private String id;

  private Integer num;

  private Integer count;
}

Source 为转换类,Target 为待转换类,接下来定义转换器

@Mapper
public interface SourceMapper {

SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

Target source2target(Source source);

}

定义一个 INSTANCE 是为了方便调用,方法名没有限制,mapstruct 会帮我们生成一个接口的实现类,

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-08-01T19:56:53+0800",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)

public class SourceMapperImpl implements SourceMapper {

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> Target <span class="hljs-title function_">source2target</span><span class="hljs-params">(Source source)</span> {
    <span class="hljs-keyword">if</span> ( source == <span class="hljs-literal">null</span> ) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-type">Target</span> <span class="hljs-variable">target</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Target</span>();

    target.setId( source.getId() );
    target.setNum( source.getNum() );
    target.setCount( source.getCount() );

    <span class="hljs-keyword">return</span> target;
}

}

调用转换器

public class Client {
public static void main(String[] args) {
Source source = new Source();
source.setId("1");
source.setNum(2);
source.setCount(3);

<span class="hljs-type">Target</span> <span class="hljs-variable">target</span> <span class="hljs-operator">=</span> SourceMapper.INSTANCE.source2target(source);
System.out.println(source);
System.out.println(target);

}
}

输出结果为

复制Source(id=1, num=2, count=3)
Target(id=1, num=2, count=3)

属性名不同的转换

如果属性名不同的话,可以通过 Mapping 注解来转换

@Data
public class Source {

private String sourceId;

private Integer sourceNum;

private Integer sourceCount;
}

复制@Data
public class Target {

  private String targetId;

  private Integer targetNum;

  private Integer targetCount;
}
复制@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceId", target = "targetId")
  @Mapping(source = "sourceNum", target = "targetNum")
  @Mapping(source = "sourceCount", target = "targetCount")
  Target source2target(Source source);

}

Mapping 注解是一个可重复注解,通过 Mapping 注解指定源属性名和目标属性名就可以了。

public class Client {
public static void main(String[] args) {

<span class="hljs-type">Source</span> <span class="hljs-variable">source</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Source</span>();
source.setSourceId(<span class="hljs-string">"1"</span>);
source.setSourceNum(<span class="hljs-number">2</span>);
source.setSourceCount(<span class="hljs-number">3</span>);

<span class="hljs-type">Target</span> <span class="hljs-variable">target</span> <span class="hljs-operator">=</span> SourceMapper.INSTANCE.source2target(source);
System.out.println(source);
System.out.println(target);

}
}

结果符合预期。

自定义转换

有时候,某些类型的转换不能通过 mapstruct 来实现,我们可以定义自己的转换逻辑。

@Data
public class Source {

private String sourceId;

private Integer sourceNum;

private Integer sourceCount;

private SubSource subSource;

}

复制@Data
public class SubSource {

  private String deleted;

}
复制@Data
public class Target {

  private String targetId;

  private Integer targetNum;

  private Integer targetCount;

  private SubTarget subTarget;

}
复制@Data
public class SubTarget {

  private Boolean deleted;

}

定义 SubSource 转换器

@Mapper
public class SubSourceMapper {

SubTarget subSource2subTarget(SubSource subSource) {
if (subSource == null) {
return null;
}
SubTarget subTarget = new SubTarget();
// 特殊的转换逻辑
subTarget.setDeleted(subSource.getDeleted().equals("T"));
return subTarget;
}
}

让 SourceMapper 使用自定义的转换器

@Mapper(uses = SubSourceMapper.class)
public interface SourceMapper {

SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

@Mapping(source = "sourceCount", target = "targetCount")
@Mapping(source = "sourceNum", target = "targetNum")
@Mapping(source = "sourceId", target = "targetId")
@Mapping(source = "subSource", target = "subTarget")
Target source2target(Source source);

}

Mapper 注解的 uses 属性表示使用的其他转换器,既可以是我们自定义的,也可以是
mapstruct 生成的。java8 之后我们也可以通过默认方法的方式来实现自定义转换。

@Mapper
public interface SourceMapper {

SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

@Mapping(source = "sourceCount", target = "targetCount")
@Mapping(source = "sourceNum", target = "targetNum")
@Mapping(source = "sourceId", target = "targetId")
@Mapping(source = "subSource", target = "subTarget")
Target source2target(Source source);

default SubTarget subSource2subTarget(SubSource subSource) {
if (subSource == null) {
return null;
}
SubTarget subTarget = new SubTarget();
subTarget.setDeleted(subSource.getDeleted().equals("T"));
return subTarget;
}
}

多对一转换

将多个对象转换成一个

@Data
public class Person {

private String firstName;
private String lastName;
private int height;
private String description;

}

复制@Data
public class Address {

  private String street;
  private int zipCode;
  private int houseNo;
  private String description;

}
复制@Data
public class DeliveryAddress {

  private String firstName;
  private String lastName;
  private int height;
  private String street;
  private int zipCode;
  private int houseNumber;
  private String description;
}
复制@Mapper
public interface AddressMapper {

  AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

  @Mapping(source = "person.description", target = "description")
  @Mapping(source = "address.houseNo", target = "houseNumber")
  DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
}

两个输入源都有 description,必须指定一个输入源。

装饰器

装饰器可以让我们在转换前后添加一些额外的逻辑,以上一个程序为例,重新设置 DeliveryAddress 的 description。

public abstract class AddressMapperDecorate implements AddressMapper {

private final AddressMapper delegate;

protected AddressMapperDecorate(AddressMapper addressMapper) {
this.delegate = addressMapper;
}

// 装饰器逻辑 重新设置 description
@Override
public DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address) {
DeliveryAddress deliveryAddress = delegate.personAndAddress2DeliveryAddress(person, address);
deliveryAddress.setDescription(person.getDescription() + ":" + address.getDescription());
return deliveryAddress;
}
}

定义一个装饰器,必须实现转换接口并添加一个接口的构造器,定义为抽象类可以让我们只装饰指定的方法。

使用 DecoratedWith 注解来表明所使用的装饰器

@Mapper
@DecoratedWith(AddressMapperDecorate.class)
public interface AddressMapper {

AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
}

前置后置处理器

我们可以在转换方法调用前后做一些操作

@Mapper
public interface AddressMapper {

AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddress personAndAddress2DeliveryAddress(Person person,
Address address,
@Context Locale locale)
;

@BeforeMapping
default void beforeMapping(Person person,
Address address,
@MappingTarget DeliveryAddress deliveryAddress,
@TargetType Class<DeliveryAddress> deliveryAddressClass,
@Context Locale locale)
{
System.out.println("before mapping start...");
System.out.println(person);
System.out.println(address);
System.out.println(deliveryAddress);
System.out.println(deliveryAddressClass);
System.out.println(locale);
System.out.println("before mapping end...");
}

@AfterMapping
default void afterMapping(Person person,
Address address,
@MappingTarget DeliveryAddress deliveryAddress)
{
deliveryAddress.setDescription(person.getDescription() + "," + address.getDescription());
}
}

BeforeMapping 注解表示前置处理器,AfterMapping 注解表示后置处理器,MappingTarget 注解表示此参数为 target 实例,TargetType 注解表示参数为 target 类型,Context 注解表示参数为上下文参数,对应转换方法中的上下文,其余的参数为 source。

依赖注入

我们也可以将转换器定义为 spring 的 bean

@Mapper(componentModel = "spring")
public interface AddressMapper {

AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
}

接口实现类上会加上 Component 注解。