属性映射工具——MapStruct(四)
目录:
这是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管同名的属性。