mapstruct使用指南

1 简介

一个工程中,我们会定义各种DO,DTO,VO等pojo。不同的pojo转化有两种方式,一是写getter/setter方法,另外一种是用BeanUtils.copyProperties(),第一种太繁琐,第二种不好定位问题,同时反射损耗性能。mapstruct完美解决上述问题。

mapstruct是一个java注解处理器,用来生成各种类型安全的映射类。主要优势如下:

  • 执行速度快,因为使用普通的java方法取代反射;
  • 编译类型安全,只要对应的实体和属性才会映射,不会把订单实体映射到顾客DTO等等;
  • 编译时明晰的报错信息,
    • 映射不完整(不是所有目标属性被映射)
    • 映射不准确(找不到适合的映射方法或类型转化)

2 定义mapper

2.1 maven配置

...
<properties>
    <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<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>
...

如果你的工程使用到lombok,请用以下配置:

...
<properties>
    <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
    <lombok.version>1.18.12</lombok.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
      </dependency>
</dependencies>
...
<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>
                    <path>
		              <groupId>org.projectlombok</groupId>
		              <artifactId>lombok</artifactId>
		              <version>${lombok.version}</version>
		            </path>
		                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
...

2.2 定义一个mapper

直接上代码:

package com.freedom.code.mapper;

import com.freedom.code.dto.UserDTO;
import com.freedom.code.entity.UserDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface UserMapper {

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

  UserDTO doToDto(UserDO userDO);
}

执行编译后,可以在target文件找到如下类:

package com.freedom.code.mapper;

import com.freedom.code.dto.UserDTO;
import com.freedom.code.entity.UserDO;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-01-03T22:29:23+0800",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 1.8.0_271 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDTO doToDto(UserDO userDO) {
        if ( userDO == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setCreateBy( userDO.getCreateBy() );
        userDTO.setCreateByName( userDO.getCreateByName() );
        userDTO.setCreateDate( userDO.getCreateDate() );
        userDTO.setUpdateBy( userDO.getUpdateBy() );
        userDTO.setUpdateByName( userDO.getUpdateByName() );
        userDTO.setUpdateDate( userDO.getUpdateDate() );
        userDTO.setVersion( userDO.getVersion() );
        userDTO.setId( userDO.getId() );
        userDTO.setName( userDO.getName() );
        userDTO.setAge( userDO.getAge() );
        userDTO.setEmail( userDO.getEmail() );

        return userDTO;
    }
}

可以看到mapstruct自动帮你生成实现类,自动帮你映射字段。

使用:

package com.freedom.code;

import com.freedom.code.dto.UserDTO;
import com.freedom.code.entity.UserDO;
import com.freedom.code.mapper.UserMapper;
import com.freedom.code.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

// 这里只写SpringBootTest这个注解; 如果是junit4的话, 就要加上@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {

  @Autowired
  private UserService userService;

  @Test
  public void testMapper1() {
    UserDO userDO = userService.getById("1345368146033819649");
    UserDTO userDTO = UserMapper.INSTANCE.doToDto(userDO);
    System.out.println(userDTO);
  }

}

是不是很简单。

如果字段名不同,可以使用@Mapping。

  @Mapping(target = "userId",source = "id")
  @Mapping(target = "username",source = "name")
  UserVO doToVo(UserDO userDO);

映射结果如下。
在这里插入图片描述

2.3 组件映射(实验阶段)

MapStruct支持使用原生注解,@Mapping支持原生注解@Target的ElementType.METHOD, ElementType.ANNOTATION_TYPE两种类型。
例子如下:

@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }

使用:

@Mapper
public interface StorageMapper {

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

    @ToEntity
    @Mapping( target = "weightLimit", source = "maxWeight")
    ShelveEntity map(ShelveDto source);

    @ToEntity
    @Mapping( target = "label", source = "designation")
    BoxEntity map(BoxDto source);
}

使用条件:

  • ShelveEntity ,BoxEntity 都有属性"id", “creationDate” 和 “name”
  • ShelveDto,BoxDto 都有属性"groupName"。
    目前这项功能还属于实验阶段,注解@InheritConfiguration 也能实现同样的功能。

2.4 定制映射方法

有些时候mapstruct无法生成某些类型的转化方法,这个时候我们可以手动生成。

@Mapper
public interface UserMapper {

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

  @Mapping(target = "roleDTO", source = "roleDO")
  UserDTO doToDto(UserDO userDO);

	// 手动写的映射逻辑,假设mapstruct不能自动生成
  default RoleDTO doToDto(RoleDO roleDO) {
    if (roleDO == null) {
      return null;
    }
    RoleDTO roleDTO = new RoleDTO();
    roleDTO.setId(roleDO.getId());
    roleDTO.setName(roleDO.getName());
    return roleDTO;
  }
}

生成的代码如下

public class UserMapperImpl implements UserMapper {

    @Override
    public UserDTO doToDto(UserDO userDO) {
        if ( userDO == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();
		// mapstruct自动应用你所写的映射方法
        userDTO.setRoleDTO( doToDto( userDO.getRoleDO() ) );
        userDTO.setCreateBy( userDO.getCreateBy() );
        userDTO.setCreateByName( userDO.getCreateByName() );
        userDTO.setCreateDate( userDO.getCreateDate() );
        userDTO.setUpdateBy( userDO.getUpdateBy() );
        userDTO.setUpdateByName( userDO.getUpdateByName() );
        userDTO.setUpdateDate( userDO.getUpdateDate() );
        userDTO.setVersion( userDO.getVersion() );
        userDTO.setId( userDO.getId() );
        userDTO.setName( userDO.getName() );
        userDTO.setAge( userDO.getAge() );
        userDTO.setEmail( userDO.getEmail() );

        return userDTO;
    }

除了以上接口形式,也可以在抽象类中定义方法,如下:

@Mapper
public abstract class UserMapper {

  public static UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

  @Mapping(target = "roleDTO", source = "roleDO")
  public abstract UserDTO doToDto(UserDO userDO);

  public RoleDTO doToDto(RoleDO roleDO) {
    if (roleDO == null) {
      return null;
    }
    RoleDTO roleDTO = new RoleDTO();
    roleDTO.setId(roleDO.getId());
    roleDTO.setName(roleDO.getName());
    return roleDTO;
  }
}

生成代码如下:

public class UserMapperImpl extends UserMapper {

    @Override
    public UserDTO doToDto(UserDO userDO) {
        if ( userDO == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setRoleDTO( doToDto( userDO.getRoleDO() ) );
        userDTO.setCreateBy( userDO.getCreateBy() );
        userDTO.setCreateByName( userDO.getCreateByName() );
        userDTO.setCreateDate( userDO.getCreateDate() );
        userDTO.setUpdateBy( userDO.getUpdateBy() );
        userDTO.setUpdateByName( userDO.getUpdateByName() );
        userDTO.setUpdateDate( userDO.getUpdateDate() );
        userDTO.setVersion( userDO.getVersion() );
        userDTO.setId( userDO.getId() );
        userDTO.setName( userDO.getName() );
        userDTO.setAge( userDO.getAge() );
        userDTO.setEmail( userDO.getEmail() );

        return userDTO;
    }

2.5 多个参数的映射方法

例子:

  @Mapping(target = "username", source = "userDO.name")
  @Mapping(target = "roleName", source = "roleDO.name")
  @Mapping(target = "age", source = "myAge")
  UserRoleDTO doToUserRoleDto(UserDO userDO, RoleDO roleDO, Integer myAge);

生成代码如下:

public class UserMapperImpl implements UserMapper {

    @Override
    public UserRoleDTO doToUserRoleDto(UserDO userDO, RoleDO roleDO, Integer myAge) {
        if ( userDO == null && roleDO == null && myAge == null ) {
            return null;
        }

        UserRoleDTO userRoleDTO = new UserRoleDTO();

        if ( userDO != null ) {
            userRoleDTO.setUsername( userDO.getName() );
        }
        if ( roleDO != null ) {
            userRoleDTO.setRoleName( roleDO.getName() );
        }
        if ( myAge != null ) {
            userRoleDTO.setAge( myAge );
        }

        return userRoleDTO;
    }
 }

2.6 映射嵌套类的属性

例子:

@Mapper
public interface CustomerMapper {

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

  @Mapping( target = "name", source = "record.name" )
  @Mapping( target = ".", source = "record" )
  @Mapping( target = ".", source = "account" )
  CustomerDO customerDtoToCustomer(CustomerDTO customerDto);

}

生成代码如下:

public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public CustomerDO customerDtoToCustomer(CustomerDTO customerDto) {
        if ( customerDto == null ) {
            return null;
        }

        CustomerDO customerDO = new CustomerDO();

        customerDO.setName( customerDtoRecordName( customerDto ) );
        customerDO.setBuyDate( customerDtoRecordBuyDate( customerDto ) );
        customerDO.setBankName( customerDtoAccountBankName( customerDto ) );
        customerDO.setAmount( customerDtoAccountAmount( customerDto ) );
        customerDO.setId( customerDto.getId() );

        return customerDO;
    }

    private String customerDtoRecordName(CustomerDTO customerDTO) {
        if ( customerDTO == null ) {
            return null;
        }
        Record record = customerDTO.getRecord();
        if ( record == null ) {
            return null;
        }
        String name = record.getName();
        if ( name == null ) {
            return null;
        }
        return name;
    }

    private LocalDateTime customerDtoRecordBuyDate(CustomerDTO customerDTO) {
        if ( customerDTO == null ) {
            return null;
        }
        Record record = customerDTO.getRecord();
        if ( record == null ) {
            return null;
        }
        LocalDateTime buyDate = record.getBuyDate();
        if ( buyDate == null ) {
            return null;
        }
        return buyDate;
    }

    private String customerDtoAccountBankName(CustomerDTO customerDTO) {
        if ( customerDTO == null ) {
            return null;
        }
        Account account = customerDTO.getAccount();
        if ( account == null ) {
            return null;
        }
        String bankName = account.getBankName();
        if ( bankName == null ) {
            return null;
        }
        return bankName;
    }

    private BigDecimal customerDtoAccountAmount(CustomerDTO customerDTO) {
        if ( customerDTO == null ) {
            return null;
        }
        Account account = customerDTO.getAccount();
        if ( account == null ) {
            return null;
        }
        BigDecimal amount = account.getAmount();
        if ( amount == null ) {
            return null;
        }
        return amount;
    }
}

CustomerDTO.record和CustomerDTO.account中的每个属性都会映射到目标类的属性,如果有冲突,可以使用@Mapping( target = “name”, source = “record.name” )解决。record和account都有name属性,使用source = "record.name"解决冲突。

2.7 更新已经存在的bean实例

例子:

@Mapping(target = "roleDTO", source = "roleDO")
  void doToDto(UserDO userDO, @MappingTarget UserDTO userDTO);

生成的代码:

 @Override
    public void doToDto(UserDO userDO, UserDTO userDTO) {
        if ( userDO == null ) {
            return;
        }

        userDTO.setRoleDTO( doToDto( userDO.getRoleDO() ) );
        userDTO.setCreateBy( userDO.getCreateBy() );
        userDTO.setCreateByName( userDO.getCreateByName() );
        userDTO.setCreateDate( userDO.getCreateDate() );
        userDTO.setUpdateBy( userDO.getUpdateBy() );
        userDTO.setUpdateByName( userDO.getUpdateByName() );
        userDTO.setUpdateDate( userDO.getUpdateDate() );
        userDTO.setVersion( userDO.getVersion() );
        userDTO.setId( userDO.getId() );
        userDTO.setName( userDO.getName() );
        userDTO.setAge( userDO.getAge() );
        userDTO.setEmail( userDO.getEmail() );
    }

2.8 public字段的映射

如果字段是public修饰,如果也没setter和getter方法,mapstruct也可以支持。mapstruct把这种字段当做read/write accessor。

  • read accessor: 字段是public或public final修饰,如果有static修饰就不是。
  • write accessor:字段只有public修饰,如果有final或static修饰都不是。
    例子:
@Data
public class CarDO {

  private Long id;
  private String name;

}
public class CarDTO {
  public Long id;
  public String name;
}
@Mapper
public interface CarMapper {

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

  CarDTO dtoToDo(CarDO carDO);
}

生成的代码:

public class CarMapperImpl implements CarMapper {

    @Override
    public CarDTO dtoToDo(CarDO carDO) {
        if ( carDO == null ) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        carDTO.id = carDO.getId();
        carDTO.name = carDO.getName();

        return carDTO;
    }
}

2.9 使用builder映射 略

2.10 使用构造函数映射 略

3 获得mapper方式

主要有两种:

@Mapper
public interface CarMapper {

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

  CarDTO dtoToDo(CarDO carDO);
}

这种方式比较直接,方便使用。

另外一种方式:

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

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

  CarDTO dtoToDo(CarDO carDO);
}

生成的代码当中包含@Component注解,可以使用@Autowired方式注入使用。

@Component
public class CarMapperImpl implements CarMapper {

    @Override
    public CarDTO dtoToDo(CarDO carDO) {
        if ( carDO == null ) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        carDTO.id = carDO.getId();
        carDTO.name = carDO.getName();

        return carDTO;
    }
}

4 数据类型转换

mapstruct在很多情况下可以自动转换类型,比如int和String类型,可以自动调用String.valueOf(int)或Integer.parseInt(String)进行转换。
例子:

public class CarMapperImpl implements CarMapper {

    @Override
    public CarDTO dtoToDo(CarDO carDO) {
        if ( carDO == null ) {
            return null;
        }
        String name = null;
        name = carDO.getName();
        CarDTO carDTO = new CarDTO( name );
        if ( carDO.getId() != null ) {
            carDTO.setId( carDO.getId().intValue() );
        }
        // 自动转换类型
        if ( carDO.getPrice() != null ) {
            carDTO.setPrice( Integer.parseInt( carDO.getPrice() ) );
        }
        return carDTO;
    }
}

具体如下;

  • 所有java基本数据类型和它们对应的包装类型可以自动转换,比如int和Integer,boolean 和Boolean等等。当由包装类型转换为其基本类型时,会校验是否为null。
  • 所有java基本数据类型和其他数据类型,比如int和Long,byte和Integer,当由高数据类型转到低数据类型,数据精度可能丢失,注解@Mapper和@MapperConfig 有方法typeConversionPolicy 控制警告或报错,默认ReportingPolicy.IGNORE即不管。
  • 所有java基本数据类型及其包装类型和String之间的转换。
  • 等等不在列举,具体看官方文档。

5 collections的映射

例子:

 List<CarDTO> doToDtos(List<CarDO> carDOS);

生成代码

    @Override
    public List<CarDTO> doToDtos(List<CarDO> carDOS) {
        if ( carDOS == null ) {
            return null;
        }

        List<CarDTO> list = new ArrayList<CarDTO>( carDOS.size() );
        for ( CarDO carDO : carDOS ) {
            list.add( dtoToDo( carDO ) );
        }

        return list;
    }

5.1 map的映射

例子:

@Mapper
public interface SourceTargetMapper {

  @MapMapping(valueDateFormat = "dd.MM.yyyy")
  Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);

  @MapMapping(valueDateFormat = "dd.MM.yyyy")
  Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source);
}

生成代码:

public class SourceTargetMapperImpl implements SourceTargetMapper {

    @Override
    public Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source) {
        if ( source == null ) {
            return null;
        }

        Map<String, String> map = new HashMap<String, String>( Math.max( (int) ( source.size() / .75f ) + 1, 16 ) );

        for ( java.util.Map.Entry<Long, Date> entry : source.entrySet() ) {
            String key = new DecimalFormat( "" ).format( entry.getKey() );
            String value = new SimpleDateFormat( "dd.MM.yyyy" ).format( entry.getValue() );
            map.put( key, value );
        }

        return map;
    }

    @Override
    public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
        if ( source == null ) {
            return null;
        }

        Map<Long, Date> map = new HashMap<Long, Date>( Math.max( (int) ( source.size() / .75f ) + 1, 16 ) );

        for ( java.util.Map.Entry<String, String> entry : source.entrySet() ) {
            Long key;
            try {
                key = new DecimalFormat( "" ).parse( entry.getKey() ).longValue();
            }
            catch ( ParseException e ) {
                throw new RuntimeException( e );
            }
            Date value;
            try {
                value = new SimpleDateFormat( "dd.MM.yyyy" ).parse( entry.getValue() );
            }
            catch ( ParseException e ) {
                throw new RuntimeException( e );
            }
            map.put( key, value );
        }

        return map;
    }
}

6 Stream的映射

@Mapper
public interface CarMapper {

    Set<String> integerStreamToStringSet(Stream<Integer> integers);

    List<CarDto> carsToCarDtos(Stream<Car> cars);

    CarDto carToCarDto(Car car);
}
//GENERATED CODE
@Override
public Set<String> integerStreamToStringSet(Stream<Integer> integers) {
    if ( integers == null ) {
        return null;
    }

    return integers.map( integer -> String.valueOf( integer ) )
        .collect( Collectors.toCollection( HashSet<String>::new ) );
}

@Override
public List<CarDto> carsToCarDtos(Stream<Car> cars) {
    if ( cars == null ) {
        return null;
    }

    return cars.map( car -> carToCarDto( car ) )
        .collect( Collectors.toCollection( ArrayList<CarDto>::new ) );
}

7 映射values

7.1 enum to enum

7.2 enum-to-String or String-to-enum

参考文档:https://mapstruct.org/documentation/stable/reference/html/#expressions

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值