参考文档:MapStruct 1.3.0.Final参考指南 (kailing.pub)
官方:https://mapstruct.org/
官方案例 github项目:https://github.com/mapstruct/mapstruct-examples
学习版本 :1.4.1.Final
各工具包-转换性能对比:图片来源知乎大佬
引入依赖
<!-- 用于生成类型安全 Bean 映射器的注释处理器 -->
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.1.Final</version>
</dependency>
<!-- 测试依赖 非必须 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
第一个案例
实体类
@Data
public class Cat {
private Integer id_cat;// 测试 【字段名称不同】 的处理
private Integer age; // age name 为 【基本数据类型】 字段
private String name;
private Date birthdate;// 【时间类型】
private Master master; // 【引用数据类型】 - 动物的主人
}
@Data
public class CatDto {
private Integer id;
private Integer age;
private String name;
private Date birthdate;
private MasterDto masterDto;
}
@Data
public class Master {
private String name;
private Integer age;
}
@Data
public class MasterDto {
private String name;
private Integer age;
}
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(uses = {MasterMapper.class}) // 引入嵌套的对象 - 【不引入会造成引用数据类型为浅拷贝】
public interface CatMapper {
// Mappers工厂 创建 实例
CatMapper INSTANCE = Mappers.getMapper(CatMapper.class);
CatDto catToCatDto(Cat cat);
}
@Mapper
public interface MasterMapper {
MasterMapper INSTANCE = Mappers.getMapper(MasterMapper.class);
MasterDto masterToMasterDto(Master master);
}
调用
public class Test_01 {
public static void main(String[] args) {
Cat cat = Test_01.newInstanceCat();
System.out.println(cat.toString());
System.out.println(cat.getMaster().hashCode());
CatDto catDto = CatMapper.INSTANCE.catToCatDto(cat);
System.out.println(catDto.toString());
System.out.println(catDto.getMasterDto().hashCode());
}
public static Master newInstanceMaster() {
Master master = new Master();
master.setName("小辉");
master.setAge(18);
return master;
}
public static Cat newInstanceCat() {
Cat cat = new Cat();
cat.setId_cat(1);
cat.setName("小花猫");
cat.setAge(3);
cat.setMaster(Test_01.newInstanceMaster());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
try {
Date birthdate = formatter.parse("2020-01-01");
cat.setBirthdate(birthdate);
} catch (ParseException e) {
e.printStackTrace();
}
return cat;
}
}
映射器使用方式
1. 映射器 + Mappers工厂 (推荐使用-简单易懂)
import org.mapstruct.Mapper;
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
// 调用
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
2. 基于容器 的 "cdi","spring" 模式
spring模式案例, cdi模式测试时java类缺失(个人感觉不会使用,没有深究)
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface CatMapper {
CatDto catToCatDto(Cat cat);
}
// 注入到spring容器中Bean名称“catMapperImpl”,这里为注入示例
@Autowired
CatMapper catMapper;
映射器编写
1. 基础映射器
- @Mapper 标记为映射器,
- @Mapping 标记对象中的字段:source 源对象字段 ,target 目标字段
@Mapper
public interface CarMapper {
@Mapping(source = "make", target = "manufacturer")
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
@Mapping(source = "name", target = "fullName")
PersonDto personToPersonDto(Person person);
}
2. 自定义方法 不常用
自定义可以直接使用表达式来制定
这里付一个官方案例,可忽略
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
@Mapping( target = "key", ignore = true )
@Mapping( target = "descriptionArticle1", source = "brush.description" )
@Mapping( target = "descriptionArticle2", source = "paste.description" )
CombinedOfferingEntity toEntity(Toothbrush brush, ToothPaste paste, @Context ArticleRepository repo);
@ObjectFactory
default <T extends Entity> T lookup(Toothbrush brush, ToothPaste paste, @Context ArticleRepository repo,
@TargetType Class<T> targetType ) {
ComposedKey key = new ComposedKey(brush.getName(), paste.getName() );
CombinedOfferingEntity entity = repo.lookup( key );
if ( entity == null ) {
entity = new CombinedOfferingEntity();
}
return (T) entity;
}
}
public class SourceTargetMapperTest {
@Test
public void testToTarget() {
ToothPaste paste = new ToothPaste();
paste.setName( "Paradontax" );
paste.setDescription( "with active enzimes" );
Toothbrush brush = new Toothbrush();
brush.setName( "Oral-B" );
brush.setDescription( "rotating head" );
ArticleRepository repo = new ArticleRepository();
CombinedOfferingEntity entity = SourceTargetMapper.MAPPER.toEntity( brush, paste, repo );
//results
assertThat( entity ).isNotNull();
assertThat( entity.getKey() ).isNotNull();
assertThat( entity.getKey().getName1() ).isEqualTo( "Oral-B" );
assertThat( entity.getKey().getName2() ).isEqualTo( "Paradontax" );
assertThat( entity.getDescriptionArticle1() ).isEqualTo( "rotating head" );
assertThat( entity.getDescriptionArticle2() ).isEqualTo( "with active enzimes" );
}
}
public class ArticleRepository {
public CombinedOfferingEntity lookup(ComposedKey key) {
// do some DB lookups here.
CombinedOfferingEntity entity = new CombinedOfferingEntity();
entity.setKey( key );
return entity;
}
}
public interface Entity {
ComposedKey getKey();
}
public class ComposedKey {
private final String name1;
private final String name2;
public ComposedKey(String name1, String name2) {
this.name1 = name1;
this.name2 = name2;
}
public String getName1() {
return name1;
}
public String getName2() {
return name2;
}
}
public class CombinedOfferingEntity implements Entity {
private ComposedKey key;
private String descriptionArticle1;
private String descriptionArticle2;
@Override
public ComposedKey getKey() {
return key;
}
public void setKey(ComposedKey key) {
this.key = key;
}
public String getDescriptionArticle1() {
return descriptionArticle1;
}
public void setDescriptionArticle1(String descriptionArticle1) {
this.descriptionArticle1 = descriptionArticle1;
}
public String getDescriptionArticle2() {
return descriptionArticle2;
}
public void setDescriptionArticle2(String descriptionArticle2) {
this.descriptionArticle2 = descriptionArticle2;
}
}
public class Toothbrush {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class ToothPaste {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
3. 多源映射(入参)
- 源对象出现相同字段时必须用@Mapping指定
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
// 特殊用法
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "hn", target = "houseNumber") // 这里可以直接注入到指定字段中
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
4. 传入目标对象
- MappingTarget 用了标记目标对象
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
5. 自定义逻辑
- 见下 官方案例 1
6. map对象 映射类
public interface SourceTargetMapper {
@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
7. 枚举映射
@Mapper
public interface SpecialOrderMapper {
SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );
@ValueMappings({
@ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ),
@ValueMapping( source = "STANDARD", target = MappingConstants.NULL ),
@ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" )
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
8. 高级映射
// 1.默认值和常量
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
Target sourceToTarget(Source s);
// 2. 表达式
// @Mapper( imports = TimeAndFormat.class ) // 可以结合 Mapper 引入类
// @Mapping(target = "timeAndFormat",
// expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
@Mapping(target = "timeAndFormat",
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
// 3. 默认表达式
// 当 sourceId 为null时,会使用默认表达式
@Mapping(target="id", source="sourceId", defaultExpression = "java( java.util.UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
官方案例
1. 自定义逻辑处理 Mapping#qualifiedBy
/**
* A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple
* mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found'
* error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method.
* <p>
* Note that {@link #defaultValue()} usage will also be converted using this qualifier.
*
* @return the qualifiers
* @see Qualifier
*/
Class<? extends Annotation>[] qualifiedBy() default { };
自定义逻辑处理(枚举)
// 用于标识
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface LastElement {}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface FirstElement {}
// 具体逻辑类
public class IterableNonInterableUtil {
@FirstElement
public <T> T first( List<T> in ) {
if ( in != null && !in.isEmpty() ) {
return in.get( 0 );
}
else {
return null;
}
}
@LastElement
public <T> T last( List<T> in ) {
if ( in != null && !in.isEmpty() ) {
return in.get( in.size() - 1 );
}
else {
return null;
}
}
}
public class Source {
private List<Integer> myIntegers;
private List<String> myStrings;
}
public class Target {
private Integer myInteger;
private String myString;
}
// uses = IterableNonInterableUtil.class 引入对象方式
@Mapper( uses = IterableNonInterableUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
// qualifiedBy = FirstElement.class 指定具体方法
@Mapping( source = "myIntegers", target = "myInteger", qualifiedBy = FirstElement.class )
@Mapping( source = "myStrings", target = "myString", qualifiedBy = LastElement.class )
Target toTarget( Source s );
}
public class Main {
public static void main( String[] args ) {
Source s = new Source();
s.setMyIntegers( Arrays.asList( 5, 3, 7 ) );
s.setMyStrings( Arrays.asList( "five", "three", "seven " ) );
Target t = SourceTargetMapper.MAPPER.toTarget( s );
System.out.println( t.getMyInteger() ); // 5
System.out.println( t.getMyString() ); // seven
}
}
2. 反向注入 @InheritInverseConfiguration
@InheritInverseConfiguration 会根据正向转换生成对应的反向Mapping字段映射。
实体类(省略 get、set)
public class Customer {
private Long id;
private String name;
private Collection<OrderItem> orderItems;
}
public class CustomerDto {
public Long id;
public String customerName;
public List<OrderItemDto> orders;
}
public class OrderItem {
private String name;
private Long quantity;
}
public class OrderItemDto {
public String name;
public Long quantity;
}
@Mapper(uses = { OrderItemMapper.class })
public interface CustomerMapper {
CustomerMapper MAPPER = Mappers.getMapper( CustomerMapper.class );
@Mapping(source = "orders", target = "orderItems")
@Mapping(source = "customerName", target = "name")
Customer toCustomer(CustomerDto customerDto);
@InheritInverseConfiguration // 这里相当于正向 toCustomer 方法的 Mapping,等价于注释的Mapping
// @Mapping(source = "orderItems", target = "orders")
// @Mapping(source = "name", target = "customerName")
CustomerDto fromCustomer(Customer customer);
}
@Mapper
public interface OrderItemMapper {
OrderItemMapper MAPPER = Mappers.getMapper(OrderItemMapper.class);
OrderItem toOrder(OrderItemDto orderItemDto);
@InheritInverseConfiguration // 当正向没有不同时,可以忽略不写
OrderItemDto fromOrder(OrderItem orderItem);
}
3. 克隆 clone ( 1.4 可用)
/**
* Allows detailed control over the mapping process.
*
* @return the mapping control
*
* @since 1.4
*
* @see org.mapstruct.control.DeepClone
* @see org.mapstruct.control.NoComplexMapping
* @see org.mapstruct.control.MappingControl
*/
Class<? extends Annotation> mappingControl() default MappingControl.class;
实体类(省略 get、set)
public class CustomerDto {
private Long id;
private String customerName;
private List<OrderItemDto> orders;
private Map<OrderItemKeyDto, OrderItemDto> stock;
}
public class OrderItemDto {
private String name;
private Long quantity;
}
public class OrderItemKeyDto {
private long stockNumber;
}
@Mapper(mappingControl = DeepClone.class)
public interface Cloner {
Cloner MAPPER = Mappers.getMapper( Cloner.class );
CustomerDto clone(CustomerDto customerDto);
}
调用测试
public class ClonerTest {
@Test
public void testMapDtoToEntity() {
CustomerDto customerDto = new CustomerDto();
customerDto.setId( 10L );
customerDto.setCustomerName("Jaques" );
OrderItemDto order1 = new OrderItemDto();
order1.setName ("Table" );
order1.setQuantity( 2L );
customerDto.setOrders( new ArrayList<>( Collections.singleton( order1 ) ) );
OrderItemKeyDto key = new OrderItemKeyDto();
key.setStockNumber( 5 );
Map stock = new HashMap( );
stock.put( key, order1 );
customerDto.setStock( stock );
CustomerDto customer = Cloner.MAPPER.clone( customerDto );
System.out.println(customer); // org.mapstruct.example.dto.CustomerDto@504bae78
System.out.println(customerDto); // org.mapstruct.example.dto.CustomerDto@3b764bce
// check if cloned
assertThat( customer.getId() ).isEqualTo( 10 );
assertThat( customer.getCustomerName() ).isEqualTo( "Jaques" );
assertThat( customer.getOrders() )
.extracting( "name", "quantity" )
.containsExactly( tuple( "Table", 2L ) );
assertThat( customer.getStock() ).isNotNull();
assertThat( customer.getStock() ).hasSize( 1 );
Map.Entry<OrderItemKeyDto, OrderItemDto> entry = customer.getStock().entrySet().iterator().next();
assertThat( entry.getKey().getStockNumber() ).isEqualTo( 5 );
assertThat( entry.getValue().getName() ).isEqualTo( "Table" );
assertThat( entry.getValue().getQuantity() ).isEqualTo( 2L );
// check mapper really created new objects
assertThat( customer ).isNotSameAs( customerDto );
assertThat( customer.getOrders().get( 0 ) ).isNotEqualTo( order1 );
assertThat( entry.getKey() ).isNotEqualTo( key );
assertThat( entry.getValue() ).isNotEqualTo( order1 );
assertThat( entry.getValue() ).isNotEqualTo( customer.getOrders().get( 0 ) );
}
}