參考文檔: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 ) );
}
}