對象拷貝 - 優雅的解決方案 Mapstruct

  • MapStruct GitHub 訪問地址 : https://github.com/mapstruct/mapstruct/前端

  • 使用例子 : https://github.com/mapstruct/mapstruct-examplesjava

  • MapStrcut與其它工具對比以及使用說明! http://www.tuicool.com/articles/uiIRjaigit

  • 是否一直在使用BeanUtils.copyProperties 用於對象屬性拷貝。 出現種種小問題。
    • 會將同名屬性拷貝到另一個對象中,操做方便可是存在一個缺陷 (速度慢)
    • 有些同名字段卻沒法進行特殊化處理,將會致使不想修改的字段被覆蓋。也不能自定義屬性映射
  • 在 mvc層 咱們常常會DTO對象返回給前端 進行字段渲染。咱們不喜歡將全部字段都顯示給前端,或者咱們須要修改字段返回給前端,例如 數據中存儲的上架下架是0,1  可是前端須要的字段是true 和 false。 咱們都得進行手動判斷處理而後編輯成DTO返回給前端github

 MapStruct是一種類型安全的bean映射類生成java註釋處理器
咱們要作的就是定義一個映射器接口,聲明任何須需的映射方法。在編譯的過程當中,MapStruct會生成此接口的實現。該實現使用純java方法調用的源和目標對象之間的映射,MapStruct節省了時間,經過生成代碼完成繁瑣和容易出錯的代碼邏輯。。spring

  • MapStruct 擁有的優勢:
    • 使用普通方法調用而不是反射來快速執行,他會在編譯器生成相應的 Impl 方法調用時直接經過簡單的 getter/setter調用而不是反射或相似的方式將值從源複製到目標
    • 編譯時類型安全性 : 只能映射彼此的對象和屬性,不能將商品實體意外映射到用戶 DTO等
    • 在構建時清除錯誤報告,如 映射不完整 (並不是全部目標屬性都被映射) 或 映射不正確(沒法找到適當的映射方法或類型轉換)
  • MapStruct 提供的重要註解 :
    • @Mapper : 標記這個接口做爲一個映射接口,而且是編譯時 MapStruct 處理器的入口
    • @Mapping : 解決源對象和目標對象中,屬性名字不一樣的狀況
    • Mappers.getMapper 自動生成的接口的實現能夠經過 Mapper 的 class對象獲取,從而讓客戶端能夠訪問 Mapper接口的實現
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">

   
    <properties>

       // ...

        <org.mapstruct.version>1.2.0.Final</org.mapstruct.version>

    </properties>

 

    <dependencies>

        ...

        <!-- MapStruct START -->

        <dependency>

            <groupId>org.mapstruct</groupId>

            <artifactId>mapstruct-jdk8</artifactId>

            <version>${org.mapstruct.version}</version>

        </dependency>

        <dependency>

            <groupId>org.mapstruct</groupId>

            <artifactId>mapstruct-processor</artifactId>

            <version>${org.mapstruct.version}</version>

        </dependency>

        <!-- MapStruct END -->

    </dependencies>

 

    <build>

        <plugins>

            <plugin>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

            </plugin>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-compiler-plugin</artifactId>

                <version>3.5.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>

                    <compilerArgs>

                        <compilerArg>-Amapstruct.defaultComponentModel=spring</compilerArg>

                        <compilerArg>-Amapstruct.suppressGeneratorTimestamp=true</compilerArg>

                        <compilerArg>-Amapstruct.suppressGeneratorVersionInfoComment=true</compilerArg>

                    </compilerArgs>

                </configuration>

            </plugin>

        </plugins>

    </build>

</project>
  • BasicObjectMapper包含了4個基本方法,單個和集合以及反轉的單個和集合。開發中如須要對象轉換操做可直接新建 interface 並繼承 BasicObjectMapper,並在新建的接口上加上 @Mapper(componentModel = "spring"),若是是屬性中包含其它類以及該類已經存在 Mapper 則註解中加上 users = {類名.class}。componentModel = "spring" 該配置表示生成的實現類默認加上 spring @Component 註解,使用時可直接經過 @Autowire 進行注入
public interface BasicObjectMapper<SOURCE, TARGET> {

 
    @Mappings({})

    @InheritConfiguration

    TARGET to(SOURCE var1);

 
    @InheritConfiguration

    List<TARGET> to(List<SOURCE> var1);

 
    @InheritInverseConfiguration

    SOURCE from(TARGET var1);

 
    @InheritInverseConfiguration

    List<SOURCE> from(List<TARGET> var1);

 
}
  • 直接使用進行對象數據轉換
@Data

public class ProductCategory {

    /** 類別編碼 */

    private String categoryCode;

    /** 類別名稱 */

    private String categoryName;

}

 
@Data

public class CategoryVo {

    private String code;

    private String name;

}

 


import org.mapstruct.Mapper;

import org.mapstruct.Mapping;

import org.mapstruct.Mappings;

import org.mapstruct.factory.Mappers;

@Mapper

public interface CategoryMapper extends BasicObjectMapper<CategoryVo, ProductCategory> {

    CategoryMapper MAPPER = Mappers.getMapper(CategoryMapper.class);

    @Mappings({

            @Mapping(source = "code", target = "categoryCode"),

            @Mapping(source = "name", target = "categoryName")

    })

    ProductCategory to(CategoryVo source);

}

public static void main(String[] args) {

    CategoryMapper categoryMapper = CategoryMapper.MAPPER;

 

    CategoryVo vo = new CategoryVo();

    vo.setCode("0000");

    vo.setName("屬性名稱");

 

    ProductCategory pc = categoryMapper.to(vo); // 經過 to方法獲得 ProductCategory

    System.out.println("1" + pc);

 

    CategoryVo vo1 = categoryMapper.from(pc); // 經過 from方法獲得 CategoryVo,既反轉 to方法

    System.out.println("2" + vo1);

 

    List<ProductCategory> pcList = categoryMapper.to(Arrays.asList(vo, vo1)); // 經過to方法從集合獲得轉換後的集合

    System.out.println("3" + pcList);

 

    List<CategoryVo> voList = categoryMapper.from(pcList); // 反轉集合

    System.out.println("4" + voList);

}
  • 自定義方法添加到映射器 : 在某些狀況下,須要手動實現 MapStruct 沒法生成的從一種類型到另外一種類型的特定映射,有以下兩種實現方法 :
  • 方法1> 在另外一個類上實現此類方法,而後由 MapStruct 生成的映射器使用該方法
  • 方法2> 在Java 8或更高版本時,能夠直接在映射器界面中實現自定義方法做爲默認方法。若是參數和返回類型匹配,生成的代碼將調用默認方法
@Mapper

public interface CarMapper {
        
    CarMapper MAPPER = Mappers.getMapper(CarMapper.class);
    
    @Mappings({...})

    CarDto carToCarDto(Car car);

 

    default PersonDto personToPersonDto(Person person) {

        // hand-written mapping logic

    }

}
  • 映射器也能夠定義爲抽象類的形式而不是接口,並直接在此映射器類中實現自定義方法。在這種狀況下,MapStruct將生成抽象類的擴展,並實現全部抽象方法。這種方法優於聲明默認方法的優勢是能夠在映射器類中聲明附加字段
@Mapper

public abstract class CarMapper {

    @Mappings(...)

    public abstract CarDto carToCarDto(Car car);

 

    public PersonDto personToPersonDto(Person person) {

        // hand-written mapping logic

    }

}
  • 多源參數映射方法 : MapStruct 支持多個源參數的映射方法,將幾個實體組合成一個數據傳輸對象
@Mapper

public interface AddressMapper {

    @Mappings({

        @Mapping(source = "person.description", target = "description"),

        @Mapping(source = "address.houseNo", target = "houseNumber")

    })

    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);

}
  • 若是多個源對象定義了一個具備相同名稱的屬性,則必須使用 @Mapping 註釋來指定從中檢索屬性的源參數,若是這種歧義未獲得解決,將會引起錯誤。對於在給定源對象中只存在一次的屬性,指定源參數的名稱是可選的,由於它能夠自動肯定
MapStruct 還提供直接引用源參數

@Mapper

public interface AddressMapper {

    @Mappings({

        @Mapping(source = "person.description", target = "description"),

        @Mapping(source = "hn", target = "houseNumber")

    })

    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);

}
  • 直接字段訪問映射 : MapStruct 支持 public 沒有 getter/setter 的字段的映射,若是 MapStruct 沒法爲屬性找到合適的 getter/setter方法,MapStruct 將使用這些字段做爲 讀/寫訪問器。若是它是 public,則字段被認爲是讀取存取器 public final。若是一個字段 static 不被視爲讀取存取器只有在字段被認爲是寫入訪問者的狀況下 public。若是一個字段 final 和/或 static 它不被認爲是寫入訪問者
public class Customer {

    private Long id;

    private String name;

    // getters and setter omitted for brevity

}

 

public class CustomerDto {

    public Long id;

    public String customerName;

}

 

@Mapper

public interface CustomerMapper {

    CustomerMapper MAPPER = Mappers.getMapper( CustomerMapper.class );
 
    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto customerDto);

 
    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);

}

// 生成的映射器以下

public class CustomerMapperImpl implements CustomerMapper {

    @Override

    public Customer toCustomer(CustomerDto customerDto) {

        // ...

        customer.setId( customerDto.id );

        customer.setName( customerDto.customerName );

        // ...

    }

 

    @Override

    public CustomerDto fromCustomer(Customer customer) {

        // ...

        customerDto.id = customer.getId();

        customerDto.customerName = customer.getName();

        // ...
    }
}
  • 檢索映射器 : Mapper實例 經過 org.mapstruct.factory.Mappers 的 getMapper() 方法來檢索。一般 映射器接口應該定義一個名爲的成員 INSTANCE ,它包含一個映射器類型的單個實例 :
@Mapper

public interface CarMapper {

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

 

    CarDto carToCarDto(Car car);

}

這種模式使客戶很是容易地使用映射器對象,而無需反覆實例化新的實例 :

Car car = ...;

CarDto dto = CarMapper.INSTANCE.carToCarDto( car );

 

使用依賴注入 : 經過 Spring 依賴注入能夠獲取映射器對象

@Mapper(componentModel = "spring")

public interface CarMapper {

    CarDto carToCarDto(Car car);

}


@Inject
private CarMapper mapper;
  • 數據類型轉換 : 源對象和目標對象中映射的屬性類型可能不一樣,MapStruct 提供自動處理類型轉換,提供以下自動轉換 :
    • 1> Java基本數據類型及其相應的包裝類型,如 int 和 Integer,boolean 和 Boolean 等生成的代碼是 null 轉換一個包裝型成相應的原始類型時一個感知,即 null 檢查將被執行
    • 2> Java基本號碼類型和包裝類型,例如之間 int 和 long 或 byte 和 Integer (大類類型數據轉換成小類可能出現精度損失)
    • 3> 全部Java基本類型之間 (包括其包裝) 和 String 之間,例如 int 和 String 或 Boolean 和 String,java.text.DecimalFormat 都可以指定格式字符串
    • int 到 String的轉換
int 到 String的轉換

@Mapper

public interface CarMapper {

    @Mapping(source = "price", numberFormat = "$#.00")

    CarDto carToCarDto(Car car);

 

    @IterableMapping(numberFormat = "$#.00")

    List<String> prices(List<Integer> prices);

}

BigDecimal 轉換爲 String

@Mapper

public interface CarMapper {

    @Mapping(source = "power", numberFormat = "#.##E0")

    CarDto carToCarDto(Car car);

}

從日期到字符串的轉換

@Mapper

public interface CarMapper {

    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")

    CarDto carToCarDto(Car car);

 

    @IterableMapping(dateFormat = "dd.MM.yyyy")

    List<String> stringListToDateList(List<Date> dates);

}

 

映射對象引用 : 對象中若是包含另外一個對象的引用,此時只需爲引用的對象類型定義映射方法便可

@Mapper

public interface CarMapper {

    CarDto carToCarDto(Car car);

 

    PersonDto personToPersonDto(Person person);

}

 

# 映射器控制嵌套的bean映射

@Mapper

public interface FishTankMapper {

    @Mappings({

    @Mapping(target = "fish.kind", source = "fish.type"),

    @Mapping(target = "fish.name", ignore = true),

    @Mapping(target = "plant", ignore = true ),

    @Mapping(target = "ornament", ignore = true ),

    @Mapping(target = "material", ignore = true),

    @Mapping(target = "ornament", source = "interior.ornament"),

    @Mapping(target = "material.materialType", source = "material"),

    @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")

    })

    FishTankDto map( FishTank source );

}
  • 調用其餘映射器 : MapStruct 中能夠調用在其餘類中定義的映射方法,不管是由MapStruct生成的映射器仍是手寫映射方法
# 手動實現的映射

public class DateMapper {

    public String asString(Date date) {

        return date != null ? new SimpleDateFormat("yyyy-MM-dd").format(date) : null;

    }

    public Date asDate(String date) {

        try {

            return date != null ? new SimpleDateFormat("yyyy-MM-dd").parse(date) : null;

        } catch (ParseException e) {

            throw new RuntimeException(e);

        }

    }

}

 

# 引用另外一個映射器類

@Mapper(uses = DateMapper.class)

public class CarMapper {

    CarDto carToCarDto(Car car);

}
  • 當爲該  carToCarDto() 方法的實現生成代碼時,MapStruct將查找將 Date 對象映射到String的方法,在 DateMapper 該類上找到它並生成 asString() 用於映射該 manufacturingDate 屬性的調用
  • 映射集合 : 集合類型(映射 List,Set 等等) 以相同的方式映射 bean類型,經過定義與在映射器接口所需的源和目標類型的映射方法。生成的代碼將包含一個遍歷源集合的循環,轉換每一個元素並將其放入目標集合中。若是在給定的映射器或其使用的映射器中找到了集合元素類型的映射方法,則會調用此方法以執行元素轉換。或者,若是存在源和目標元素類型的隱式轉換,則將調用此轉換例程
@Mapper

public interface CarMapper {

    Set<String> integerSetToStringSet(Set<Integer> integers);

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

    CarDto carToCarDto(Car car);

}

 

# 生成的集合映射方法

@Override

public Set<String> integerSetToStringSet(Set<Integer> integers) {

    if (integers == null) {

        return null;

    }

    Set<String> set = new HashSet<>();

    for (Integer integer : integers) {

        set.add(String.valueOf(integer));

    }

    return set;

}

 

@Override

public List<CarDto> carsToCarDtos(List<Car> cars) {

    if (cars == null) {

        return null;

    }

    List<CarDto> list = new ArrayList<>();

    for (Car car : cars) {

        list.add(carToCarDto(car));

    }

    return list;

}

 

映射Map :

public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")

    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);

}

 

映射流 :

@Mapper

public interface CarMapper {

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

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

    CarDto carToCarDto(Car car);

}
  • 映射枚舉 : 默認狀況下,源枚舉中的每一個常量映射到目標枚舉類型中具備相同名稱的常量。若是須要,可使用 @ValueMapping 註釋幫助將source enum中的常量映射爲具備其餘名稱的常量
@Mapper

public interface OrderMapper {

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

    @ValueMappings({

            @ValueMapping(source = "EXTRA", target = "SPECIAL"),

            @ValueMapping(source = "STANDARD", target = "DEFAULT"),

            @ValueMapping(source = "NORMAL", target = "DEFAULT")

    })

    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);

}

 

默認值和常量 : 

@Mapper(uses = StringListMapper.class)

public interface SourceTargetMapper {

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

 

    @Mappings({

            @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);

}

 

表達式 :

@Mapper

public interface SourceTargetMapper {

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

 

    @Mapping(target = "timeAndFormat", expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")

    Target sourceToTarget(Source s);

}
  • 肯定結果類型 : 當結果類型具備繼承關係時,選擇映射方法(@Mapping) 或工廠方法(@BeanMapping) 可能變得不明確。假設一個Apple和一個香蕉,這兩個都是 Fruit的專業
@Mapper(uses = FruitFactory.class)

public interface FruitMapper {

    @BeanMapping(resultType = Apple.class)

    Fruit map(FruitDto source);

}

 

public class FruitFactory {

    public Apple createApple() {

        return new Apple("Apple");

    }

    public Banana createBanana() {

        return new Banana("Banana");

    }

}
  • 控制 '空' 參數的映射結果 : 默認狀況下 null 會返回,經過指定 nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT 上 @BeanMapping,@IterableMapping,@MapMapping,或全局上 @Mapper 或 @MappingConfig,映射結果能夠被改變以返回空默認值
    • 1> Bean映射 : 將返回一個 '空' 目標bean,除常量和表達式外,它們將在存在時填充
    • 2> 基元 : 基元的默認值將被返回,例如 false for boolean 或 0 for int
    • 3> Iterables/Arrays : 一個空的迭代器將被返回
    • 4> 地圖 : 將返回空白地圖
  • 共享配置 : 經過指向中心接口來定義共享配置的可能性 @MapperConfig,要使映射器使用共享配置,須要在 @Mapper#config 屬性中定義配置界面。該 @MapperConfig 註釋具備相同的屬性 @Mapper 註釋。任何未經過的屬性 @Mapper 都將從共享配置繼承。指定 @Mapper 的屬性優先於經過引用的配置類指定的屬性
@MapperConfig(uses = CustomMapperViaMapperConfig.class, unmappedTargetPolicy = ReportingPolicy.ERROR)

public interface CentralConfig {}

 

@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )

public interface SourceTargetMapper {}

本文做者:雲楊四海
原文連接:對象拷貝 - 優雅的解決方案 Mapstruct
版權歸做者全部,轉載請註明出處express

相關文章
相關標籤/搜索