属性映射⼯具——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。当然可以使⽤
4)枚举和String之间。
e200
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 {
}
/**
* 宝马标志307sw
*/
@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 = Mapper(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 = BenzCar(car2);
System.out.println(benzCar);
//Car1(id=1000, name1=宝马牌⼩汽车)
Car1 bmwCar = BMWCar(car2);
System.out.println(bmwCar);
}
四、映射集合
  这个⽐较简单,我就直接引⽤⽂档上的内容了。不过在这之前,我先⽤⾃⼰的话总结⼀下:MapStruct⽣成集合类型映射的原理是,遍历集合中的每⼀个元素,然后每⼀个元素的转化调⽤对应的转化⽅法。这么说吧,如果你的映射器中已经有⾮集合类型的转化,那么相应的集合类型的转化你不⽤操⼼。请看下⾯这个例⼦:
@Mapper(uses = {StringSwitchDateMapper.class})
public interface OrderTrackMapper{
OrderTrackMapper INSTANCE = Mapper(OrderTrackMapper.class);
@Mappings({
@Mapping(source = "de", target = "orderType"),
@Mapping(source = "de", 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( Passengers() ) );
...
  当然还有⼀个集合⽅⾯的映射@IterableMapping。这个就是没有基本元素的转化时需要⾃定义映射,原理和⽤法同@Mapping,我在这⾥就不涉及了。
马自达二手车五、Map映射
  Map的映射有专门的注解@MapMapping。其属性有keyDateFormat、valueDateFormat、keyNumberFormat、
valueNumberFormat、keyQualifiedBy、valueQualifiedBy、keyQualifiedByName、valueQualifiedByName等,和@Mapping差不多,这⾥就看个简单的⼩例⼦就⾏,其他的参考@Mapping。
@Mapper
public interface MapMapper {
MapMapper INSTANCE = Mapper(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 = Set().iterator();
Map.Entry<String, String> entry = null;
while (entryIterator.hasNext()) {
entry = ();
System.out.println("key " + Key() + ",    value " + Value());
}
}
保时捷堵宝马一年
@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 : Set()) {
System.out.println("key " + Key() + " ,  value " + Value());
}
}
/**
* ======================================================
*/
@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 : Set()) {
System.out.println("key " + Key() + ",  valuel " + Value());
}
}
@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 : Set()) {
System.out.println("key " + Key() + ",  valuel " + Value());
}
}
}
六、枚举映射
  MapStruct⽀持将⼀种Java枚举类型映射到另⼀种Java枚举类型的⽅法。默认情况下,源枚举中的每个常量都映射到⽬标枚举类型中具有相同名称的常量。如果需要,可以⽤注解@ValueMapping。将源枚举中的常量映射到具有其他名称的常量。来⾃源枚举的多个常量可以映射到⽬标类型中的相同常量。PS:@InheritInverseConfiguration,@InheritConfiguration可以与结合使⽤@ValueMappings。
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mapper( 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。
  MapStruct还具有⼀种将所有剩余(未指定)映射映射到默认映射的机制。在⼀组值映射中只能使⽤
⼀次。它有两种风
格:<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