目錄:
這是MapStruct系列的第四篇了,我還是挺佩服自己,竟然寫了這么多了。好吧,自戀完了,我們繼續。
一、隱式類型轉化
之前我們已經重點講了@Mapper注解是什么,以及怎么用。但是你有沒有想過,@Mapper到底什么條件下可以不用,什么條件下必須用。即MapStruct會自動進行類型轉化——隱式類型轉化。以下幾種情況,沒必要使用@Mapper自定義映射。
1)java中所有的基本數據類型與對應的包裝類型之間,比如int與Integer、boolean與Boolean之間。當包裝類轉化為基本類型時,將執行null非空檢查;
2)Java基本數字類型和包裝器類型之間,如int和long、byte和Integer。但是高精度轉低精度時可能會損失精度。
3)在所有java基本數據類型(包括包裝類型)和String類型之間的轉化。比如int和String、Boolean和String。當然可以使用java.text.DecimalFormat定制,這個之前提到過。
4)枚舉和String之間。
5)在大數字類型(java.math.BigInteger、java.math.BigDecimal)和java基本數據類型以及String之間。
6)java.sql.Date和java.util.Date之間的轉化;
7)java.sql.Time和java.util.Date之間的轉化;
8)java.sql.Timestamp和java.util.Date之間的轉化。
二、生成映射方法的流程
1)如果source和target屬性具有相同的類型,則將值簡單地從源復制到目標。如果屬性是一個集合(例如List),則該集合的副本將被設置到目標屬性中。
2)如果源屬性和目標屬性類型不同,則檢查是否存在另一種映射方法,該映射方法將源屬性的類型作為參數類型,將目標屬性的類型作為返回類型。如果存在這樣的方法,它將在生成的映射實現中調用。
3)如果不存在這樣的方法,MapStruct將查看是否存在屬性的源類型和目標類型的內置轉換。在這種情況下,生成的映射代碼將應用此轉換。
4)如果找不到這樣的方法,則MapStruct將嘗試生成自動子映射方法,該方法將在源屬性和目標屬性之間進行映射。
5)如果MapStruct無法創建基於名稱的映射方法,則會在構建時引發錯誤,指示不可映射的屬性及其路徑。
三、基於限定詞的映射方法選擇
限定詞的映射方法的目的:唯一的確定一個映射方法。
mapStruct的限定詞的映射方法有兩種,姑且可分為 @Named——在@Mapper中使用qualifiedByName引入、@Qualifier——在@Mapper中使用qualifiedBy引入。
我們先回憶一下@Named的形式:比如說有這樣兩個屬性:private A aa;private B bb。我們需要將A類型的aa轉化為B類型的bb(假如jdk版本是java8及以上)。那mapStruct是怎么做的呢?首先,mapStruct會在映射器里面找有沒有存在一個方法,該方法的參數類型是A,方法的返回值類型是B,如果有,則采用,否則繼續尋找。然后查看映射器有沒有use值,有的話,在這些自定義映射器里面找有沒有存在一個方法,該方法的參數類型是A,方法的返回值類型是B,有則使用,沒有則一般情況下會報錯。問題是,如果這樣的方法有且只有一個,那么我們可以不用@mapper的qualifiedByName指定,mapStruct會采用該方法。但是如果這樣的方法有多個(方法的內部邏輯不通),mapStruct不知道用哪個,此時必須用qualifiedByName指定,然后再映射器的方法、類上用@Named注明,不然報錯。
@Qualifier方式與@Named類似,以下是一個例子。需要注意的有兩點:1)注意放在類上和方法上注解的@Target屬性;2)Qualifier方式需要qualifiedBy引入。
//定義注解 @Qualifier @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface CarNameOperator { } /** * 奔馳 */ @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface CarName_Benz { } /** * 寶馬 */ @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface CarName_BMW { }
@CarNameOperator public class CarName { @CarName_Benz public String getBenzName(String name) { return "奔馳牌" + name; } @CarName_BMW public String getBMWName(String name) { return "寶馬牌" + name; } }
@Mapper(uses = {CarName.class}) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); /** * 奔馳車 */ @Mapping(source = "name2", target = "name1", qualifiedBy = {CarNameOperator.class, CarName_Benz.class}) Car1 toBenzCar(Car2 car2); /** * 寶馬車 */ @Mapping(source = "name2", target = "name1", qualifiedBy = {CarNameOperator.class, CarName_BMW.class}) Car1 toBMWCar(Car2 car2); }
@Test public void test1(){ Car2 car2 = new Car2(); car2.setId(1000); car2.setName2("小汽車"); System.out.println(car2); //Car1(id=1000, name1=奔馳牌小汽車) Car1 benzCar = CarMapper.INSTANCE.toBenzCar(car2); System.out.println(benzCar); //Car1(id=1000, name1=寶馬牌小汽車) Car1 bmwCar = CarMapper.INSTANCE.toBMWCar(car2); System.out.println(bmwCar); }
四、映射集合
這個比較簡單,我就直接引用文檔上的內容了。不過在這之前,我先用自己的話總結一下:MapStruct生成集合類型映射的原理是,遍歷集合中的每一個元素,然后每一個元素的轉化調用對應的轉化方法。這么說吧,如果你的映射器中已經有非集合類型的轉化,那么相應的集合類型的轉化你不用操心。請看下面這個例子:
@Mapper(uses = {StringSwitchDateMapper.class}) public interface OrderTrackMapper{ OrderTrackMapper INSTANCE = Mappers.getMapper(OrderTrackMapper.class); @Mappings({ @Mapping(source = "orderType.code", target = "orderType"), @Mapping(source = "operationType.code", target = "operationType"), @Mapping(source = "before", target = "oldData"), @Mapping(source = "after", target = "newData") }) OrderTrackDO toDO(OrderTrackDTO orderTrackDTO); List<OrderTrackDO> toDOs(List<OrderTrackDTO> carDTOList); }
以下來自官方文檔:
集合類型的映射(List
,Set
等等)以相同的方式映射bean類型,即通過定義與在映射器接口所需的源和目標類型的映射方法進行。生成的代碼將包含一個循環,該循環遍歷源集合,轉換每個元素並將其放入目標集合。如果在給定的映射器或其使用的映射器中找到用於集合元素類型的映射方法,則將調用此方法以執行元素轉換。或者,如果存在針對源元素類型和目標元素類型的隱式轉換,則將調用此轉換例程。下面顯示了一個示例:
@Mapper public interface CarMapper { Set<String> integerSetToStringSet(Set<Integer> integers); List<CarDto> carsToCarDtos(List<Car> cars); CarDto carToCarDto(Car car); }
生成的實現對每個元素integerSetToStringSet
執行從Integer
到的轉換String
,而生成carsToCarDtos()
的carToCarDto()
方法為每個包含的元素調用方法,如下所示:
//GENERATED CODE @Override public Set<String> integerSetToStringSet(Set<Integer> integers) { if ( integers == null ) { return null; } Set<String> set = new HashSet<String>(); 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<CarDto>(); for ( Car car : cars ) { list.add( carToCarDto( car ) ); } return list; }
請注意,當映射bean的集合類型屬性時,例如從Car#passengers
(類型List<Person>
)到CarDto#passengers
(類型List<PersonDto>
),MapStruct將尋找具有匹配參數和返回類型的集合映射方法。
//GENERATED CODE carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) ); ...
當然還有一個集合方面的映射@IterableMapping。這個就是沒有基本元素的轉化時需要自定義映射,原理和用法同@Mapping,我在這里就不涉及了。
五、Map映射
Map的映射有專門的注解@MapMapping。其屬性有keyDateFormat、valueDateFormat、keyNumberFormat、valueNumberFormat、keyQualifiedBy、valueQualifiedBy、keyQualifiedByName、valueQualifiedByName等,和@Mapping差不多,這里就看個簡單的小例子就行,其他的參考@Mapping。
@Mapper public interface MapMapper { MapMapper INSTANCE = Mappers.getMapper(MapMapper.class); @MapMapping(keyDateFormat = "yyyy-MM-dd HH:mm:ss:SSS", valueDateFormat = "yyyy-MM-dd HH:mm") Map<String, String> DateDateToStringString(Map<Date, Date> sourceMap) throws Exception; @InheritInverseConfiguration(name = "DateDateToStringString") Map<Date, Date> StringStringToDateDate(Map<String, String> sourceMap); /** * =============================================================== */ @MapMapping(valueNumberFormat = "0.00") Map<String, String> StringDoubleToStringString(Map<String, Double> sourceMap); Map<String, Double> StringStringToStringDouble(Map<String, String> sourceMap); }
public class MapTest { @Test public void Date_String() { Map<Date, Date> sourceMap = new HashMap<>(8); sourceMap.put(new Date(System.currentTimeMillis() + 15400), new Date(System.currentTimeMillis() + 20360)); sourceMap.put(new Date(System.currentTimeMillis() + 22300), new Date(System.currentTimeMillis() + 48100)); sourceMap.put(new Date(System.currentTimeMillis() + 30056), new Date(System.currentTimeMillis() + 62900)); //sourceMap.put(new Date(System.currentTimeMillis() + 50000), null); Map<String, String> targetMap = null; try { targetMap = MapMapper.INSTANCE.DateDateToStringString(sourceMap); } catch (Exception e) { e.printStackTrace(); } /* * key 2020-07-28 10:17:25:888, value 2020-07-28 10:17 * key 2020-07-28 10:17:40:544, value 2020-07-28 10:18 * key 2020-07-28 10:17:32:788, value 2020-07-28 10:17 */ Iterator<Map.Entry<String, String>> entryIterator = targetMap.entrySet().iterator(); Map.Entry<String, String> entry = null; while (entryIterator.hasNext()) { entry = entryIterator.next(); System.out.println("key " + entry.getKey() + ", value " + entry.getValue()); } } @Test public void String_Date() { Map<String, String> sourceMap = new HashMap<>(8); sourceMap.put("2020-07-28 10:17:25:888", "2020-07-28 10:17"); sourceMap.put("2020-07-28 10:17:40:544", "2020-07-28 10:18"); sourceMap.put("2020-07-28 10:17:32:788", "2020-07-28 10:17"); Map<Date, Date> targetMap = MapMapper.INSTANCE.StringStringToDateDate(sourceMap); /** * key Tue Jul 28 10:17:32 CST 2020 , value Tue Jul 28 10:17:00 CST 2020 * key Tue Jul 28 10:17:40 CST 2020 , value Tue Jul 28 10:18:00 CST 2020 * key Tue Jul 28 10:17:25 CST 2020 , value Tue Jul 28 10:17:00 CST 2020 */ for (Map.Entry<Date, Date> entry : targetMap.entrySet()) { System.out.println("key " + entry.getKey() + " , value " + entry.getValue()); } } /** * ====================================================== */ @Test public void Double_String() { Map<String, Double> sourceMap = new HashMap<>(4); sourceMap.put("A", 1.239023); sourceMap.put("B", 3.2341009); sourceMap.put("C", 10.0); /** * key A, valuel 1.24 * key B, valuel 3.23 * key C, valuel 10 */ Map<String, String> targetMap = MapMapper.INSTANCE.StringDoubleToStringString(sourceMap); for (Map.Entry<String, String> entry : targetMap.entrySet()) { System.out.println("key " + entry.getKey() + ", valuel " + entry.getValue()); } } @Test public void String_Double() { Map<String, String> sourceMap = new HashMap<>(4); sourceMap.put("A", "1.239023"); sourceMap.put("B", "3.2341009"); sourceMap.put("C", "10.0"); sourceMap.put("D", "0.09"); /** * key A, valuel 1.239023 * key B, valuel 3.2341009 * key C, valuel 10.0 * key D, valuel 0.09 */ Map<String, Double> targetMap = MapMapper.INSTANCE.StringStringToStringDouble(sourceMap); for (Map.Entry<String, Double> entry : targetMap.entrySet()) { System.out.println("key " + entry.getKey() + ", valuel " + entry.getValue()); } } }
六、枚舉映射
MapStruct支持將一種Java枚舉類型映射到另一種Java枚舉類型的方法。默認情況下,源枚舉中的每個常量都映射到目標枚舉類型中具有相同名稱的常量。如果需要,可以用注解@ValueMapping
。將源枚舉中的常量映射到具有其他名稱的常量。來自源枚舉的多個常量可以映射到目標類型中的相同常量。PS:@InheritInverseConfiguration
,@InheritConfiguration
可以與結合使用@ValueMappings
。
@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); } // GENERATED CODE public class OrderMapperImpl implements OrderMapper { @Override public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) { if ( orderType == null ) { return null; } ExternalOrderType externalOrderType_; switch ( orderType ) { case EXTRA: externalOrderType_ = ExternalOrderType.SPECIAL; break; case STANDARD: externalOrderType_ = ExternalOrderType.DEFAULT; break; case NORMAL: externalOrderType_ = ExternalOrderType.DEFAULT; break; case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL; break; case B2B: externalOrderType_ = ExternalOrderType.B2B; break; default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType ); } return externalOrderType_; } }
默認情況下,如果源枚舉類型的常量在目標類型中沒有名稱相同的對應常量,並且也未通過映射到另一個常量,則MapStruct將引發錯誤。這樣可以確保以安全且可預測的方式映射所有常量。如果由於某種原因發生了無法識別的源值,則生成的映射方法將拋出IllegalStateException。
<ANY_REMAINING>
和
<ANY_UNMAPPED>。這兩個在官方文檔里面解釋的比較模糊,我是一時沒讀懂,然后我用自己寫的例子理解了,先看看這個例子吧。
public enum OrderEnum1 { ORDER_A("A", "序號A"), ORDER_B("B", "序號B"), ORDER_C("C", "序號C"), ORDER_OTH("D", "其他序號"); @Setter @Getter private String code; @Setter @Getter private String desc; OrderEnum1(String code, String desc) { this.code = code; this.desc = desc; } } public enum OrderEnum2 { ORDER_1("1)", "序號1"), ORDER_2("2)", "序號2"), ORDER_B("B", "序號B"), ORDER_C("C", "序號C"), ORDER_5("5)", "序號5"), ORDER_6("6)", "其他序號"); @Setter @Getter private String code; @Setter @Getter private String desc; OrderEnum2(String code, String desc) { this.code = code; this.desc = desc; } }
@ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "ORDER_B"), @ValueMapping(source = "ORDER_1", target = "ORDER_A"), @ValueMapping(source = "ORDER_2", target = "ORDER_A"), @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "ORDER_OTH") }) OrderEnum1 toOrderEnum1(OrderEnum2 orderEnum2); //Generator public class OrderEnumMapperImpl implements OrderEnumMapper { @Override public OrderEnum1 toOrderEnum1(OrderEnum2 orderEnum2) { if ( orderEnum2 == null ) { return OrderEnum1.ORDER_B; } OrderEnum1 orderEnum1; switch ( orderEnum2 ) { case ORDER_1: orderEnum1 = OrderEnum1.ORDER_A; break; case ORDER_2: orderEnum1 = OrderEnum1.ORDER_A; break; case ORDER_B: orderEnum1 = OrderEnum1.ORDER_B; break; case ORDER_C: orderEnum1 = OrderEnum1.ORDER_C; break; default: orderEnum1 = OrderEnum1.ORDER_OTH; } return orderEnum1; } }
@ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "ORDER_B"), @ValueMapping(source = "ORDER_1", target = "ORDER_A"), @ValueMapping(source = "ORDER_2", target = "ORDER_A"), @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "ORDER_OTH") }) OrderEnum1 toOrderEnum1(OrderEnum2 orderEnum2); //Generator public class OrderEnumMapperImpl implements OrderEnumMapper { @Override public OrderEnum1 toOrderEnum1(OrderEnum2 orderEnum2) { if ( orderEnum2 == null ) { return OrderEnum1.ORDER_B; } OrderEnum1 orderEnum1; switch ( orderEnum2 ) { case ORDER_1: orderEnum1 = OrderEnum1.ORDER_A; break; case ORDER_2: orderEnum1 = OrderEnum1.ORDER_A; break; default: orderEnum1 = OrderEnum1.ORDER_OTH; } return orderEnum1; } }
MappingConstants.ANY_UNMAPPED與MappingConstants.ANY_REMAINING的區別是:
- MappingConstants.ANY_UNMAPPED:不管源枚舉類型與目標枚舉類型有沒有相同的名稱。只要在@ValueMapping中沒有涉及到的枚舉屬性值,都統一返回設定的值;
- MappingConstants.ANY_REMAINING:排除@ValueMapping中涉及到的枚舉值,也排除源枚舉類型與目標枚舉類型中相同的名稱外,其他的都返回統一值。
- 就是說ANY_UNMAPPED不管同名的屬性,而ANY_REMAINING管同名的屬性。