[MapStruct]数据类型转换

本章节对应官网5. Data type conversions

我们知道在对象属性之间进行映射,也就是将实例A的属性的值给实例B的属性时,我们不能保证两个属性一定是相同的类型。例如:

  • 实例A的属性age为int,实例B的属性age有可能为Long
  • Car实例中的dirver是String,CartDto实例中的属性driver是Person类型。

这章节我们就来讲解这种不同类型的属性应该如何在映射时进行转换。

1.隐式转换

隐式转换就是在某些特定类型的情况下,MatStruct会自动帮我们转换。

下面的几种情况MapStruct都会自动转换:

  • 基本数据类型与他们的包装类,例如int和Integer
  • 所有基本数值类型与他们的包装类,例如int和Integer
  • 所有基本数据类型与String类型。例如int和String
  • 枚举类型和String类型

还有一些可以隐式自动转换的,可以去参考官网。这里就不全写了。

2.引用类型的转换[5.2. Mapping object references]

引用类型转换就是对象中的某个属性他是非基本数据类型,而是一个对象。例如实例A中的属性driver是一个Person类的实例,实例B中的driver属性是PersonDto的一个实例。

下面通过代码看一下,这种情况下这种引用类型的属性怎么进行映射实现赋值。

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
    private Person driver;//[1]
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
    private PersonDto driver;//[2]
}

@Data
@AllArgsConstructor
@ToString
public class Person {
    private String name;
}

@Data
@AllArgsConstructor
@ToString
public class PersonDto {
    private String name;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    CarDto carToCarDto(Car car);
    PersonDto personToPersonDto(Person person);//[3]
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "Morris",new Person("kitty"));
        final CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); // CarDto(name=Morris, driver=PersonDto(name=kitty))
    }
}

注意看代码中[1]和[2]处,这就是我们所说的2个属性是两个不同类的实例 ,要将Car对象中的driver赋值给CarDto中的driver。具体做法就是对这两个是对象的属性driver再写一个对应的方法,也就[3]处所示。原理很简单,就是当MapStruct发现你要映射的实例中的属性是对象时,他会在Mapper中找有没有对应的方法,何为对应的方法呢?就是方法的参数为要进行取值的对象的类型,如这里了的Person,方法的返回值是要进行赋值的对象的类型,如这里的PersonDto。只要满足条件,MapStruct就会在其自动生成的实现类中使用这个方法作为转换的方法,如下图:

[MapStruct]数据类型转换_第1张图片

 所以只要记住,只要是要转的实例中的属性是引用类型,那么就位这个属性在单独写一个转换的方法就行。

注意,上面代码中我们Person和PersonDto中的属性是相同的,所以直接写一个简单的映射方法[3]就行了,但是如果属性不通又怎么办呢?下面来看一下官网说的具体做法:

  • 如果source(要取之的对象)和target(要进行赋值的对象)的属性是同一个类,那么会直接从source中取值赋给target中。例如都是Car和CarDto中的driver都是Person类型,就会直接转换,不需要写[3]处的方法了。
  • 如果soruce和target中的属性的类型不同,则会查找是否有专门方法来转化这个属性,如上例中Car的driver属性是Person类的实例,CarDto的driver的PersonDto类的实例。那么我们就需要单独写一个[3]处的方法。
  • 其余的省略,因为基本用不到,可参考官网。

3.控制将那个属性映射给谁

当需要我们手动指定将那个属性的值給哪个属性时,就可以通过这个部分讲的知识点。其实之前就讲过,就是通过@Mapping来指定。只不过这里主要用来将嵌入在属性中的属性的值給某个属性。看例子代码:

@Data
@AllArgsConstructor
@ToString
public class FishTank { // FishTank:鱼缸的意思
    private Fish fish;
}

@Data
@AllArgsConstructor
@ToString
public class Fish {
    private String type; //[1]
}

@Data
@AllArgsConstructor
@ToString
public class FishTankDto {
    private FishDto fish;
}

@Data
@AllArgsConstructor
@ToString
public class FishDto {
    private String kind; //[2]
}

@Mapper
public interface FishTankMapper {
    FishTankMapper INSTANCE = Mappers.getMapper( FishTankMapper.class );
     @Mapping(source = "fish.type", target = "fish.kind")//将[1]的值给[2]
     FishTankDto map(FishTank source);
}

public class Test {
    public static void main(String[] args) {
        FishTank fishTank = new FishTank(new Fish("liyu"));
        final FishTankDto fishTankDto = FishTankMapper.INSTANCE.map(fishTank);
        System.out.println(fishTankDto); //FishTankDto(fish=FishDto(kind=liyu))
    }
}

例子中就是将通过@Mapping指定将一个字符串属性type的值映射给对象属性中的字符串kind。

4.调用自定义的映射方法

我们知道在之前讲属性映射时,当某个属性的值映射给另外一个属性时,这个值是原封不动的赋值过去,如果你想对这个只进行一些特殊处理那么之前的方式是做不到的,那就需要用到这个小节的知识。通过调用自定义的一个方法,来对属性进行赋值。先上例子,东西比较多,耐心看完:

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
    private int priceInCar; //[1]
    private Person person; //[2]
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
    private int priceInCarDto; //[1]
    private PersonDto personDto; //[2]
}

@Data
@AllArgsConstructor
@ToString
public class Person {
    private String name;
}

@Data
@AllArgsConstructor
@ToString
public class PersonDto {
    private String name;
}

@Mapper
public abstract class CarMapperAbstract {
    static CarMapperAbstract INSTANCE = Mappers.getMapper( CarMapperAbstract.class );
    @Mapping(source = "priceInCar",target = "priceInCarDto")//[3]
    @Mapping(source = "person",target = "personDto")
    abstract CarDto carToCarDto(Car car);
    // 对[2]的两个属性映射时会走这个方法
    public PersonDto personToPersonDto(Person person) {
        return new PersonDto("hello" + person.getName());
    }
    // 对[1]的两个属性映射时会走这个方法
    public int priceInCarToPriceInCarDto(int price) {
        return price + 1;
    }
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "Morris", 5,new Person("kitty"));
        final CarDto carDto = CarMapperAbstract.INSTANCE.carToCarDto(car);
        System.out.println(carDto); //结果CarDto(name=Morris, priceInCarDto=6, personDto=PersonDto(name=hellokitty))
    }
}

这个例子中完成的业务是当进行属性间映射赋值时,当对[1]的属性赋值时要求在其前面加上字符串hello,当对[2]的属性进行映射赋值时,要将其值进行加1操作。这些业务在之前的方式中是无法做到的。所以这里用到了自定义映射方法来解决。 通过打印结果可以看出priceInCarDto的值由5变成了6,kitty也变成了hellokitty。

那么为何会知道对着2个属性进行赋值时将去调用这两个对应的自定义方法呢?其实很简答,只要按照MapStruct的规则来写就行了。当在MapStruct根据@Mapper生成实现类时,他会检查有没有符合自定映射的方法,规则就是:先通过默认方式进行映射,如[3]所示,然后当进行取值赋值时,会从Mapper中查找有没有符合自定义方法,如果当方法的参数的类型与要从中取值的属性类型相同时,并且方法的返回值的类型与要进行赋值的属性类型相同时,那么就说明有符合自定义的方法,然后就会原始的值传进来,执行操作后在讲解过返回去作为要赋值给某个属性的值。如上面例子中priceInCarToPriceInCarDto方法。

简单来说及时先通过@Mapping找到默认的映射关系,拿到原始的映射属性的值,然后再找有没有自定义的方法,找到的话就把原始值当参数扔进去进行处理,处理后的结果当成返回值赋值给最后的属性。

5.调用指定Mapper中的方法

前面讲的东西都是在自定的Mapper中进行映射或者使用自定义方法。这个章节讲解,如果使用其他Mapper中的内容进行映射。先看例子:
首先,我要完成的目的是将Car这个对象中的属性映射到CarDto中。所以这两个类是必须的,如下:

@Data
@AllArgsConstructor
public class Car {
    private String nameInCar;
    private String colorInCar;
    private Date dateInCar; // [1]
}

@Data
@AllArgsConstructor
public class CarDto {
    private String nameInCarDto;
    private String colorInCarDto;
    private String dateInCarDto; //[2]
}

// 注意属性中的[1]和[2],后面将会通过指定的类中的某个方法完成Date到String的类型转换

然后,下面是我们的MapStruct中要使用的Mapper接口

@Mapper(uses=DateMapper.class)//[3]
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    // 使用@MappingTarget作为要更新的类,通过第一个参数中的内容,更新到@MappingTarget的类中
    @Mapping(source = "nameInCar", target = "nameInCarDto")
    @Mapping(source = "colorInCar", target = "colorInCarDto")
    @Mapping(source = "dateInCar", target = "dateInCarDto") //[4]
    public CarDto car2CarDto(Car car);
}
// 注意[3]和[4]处,要将[4]处不通类型的属性进行映射时,会需要一个类型转换(Date转String),这里指定从[3]处指定来的类中去查找类型转换要用到的方法。具体使用那个方法,是有一个查找规则的。例如要将Date转String,那么这个方法的入参肯定要是Date类型,返回值要是String类型。就是依照这种入参和返回值类型的原则来进行查找。

因为正如标题说的要调用另外一个mapper,所以下面是[3]处的DateMapper类

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

最后是我们的测试代码:

public static void main(String[] args) {
        Car car = new Car( "Morris", "5",new Date());

        CarDto carDto = CarMapper.INSTANCE.car2CarDto(car);
        System.out.println(carDto);
}

原理解析

MapStruct在编译时,会检查我们CarMapper中用于转换的对象中要进行映射的每个属性的类型关系,当发现Car中的Date类型的dateInCar要映射到CarDto中的dateInCarDto属性为String时,那么MapStruct首先要做的就是要指定再哪去解决这个类型转换(因为两个属性类型不同,不可能直接赋值)。
这时,因为我们在CarMapper上使用了@Mapper(uses=DateMapper.class),MapStruct就知道这个装换的方法将在DataMapper这个类中,到底使用里面的那个方法呢?规则就是去找那个参数与要转换的属性的类型相同的(dateInCar属性为Date),返回值的类型与最终要转换为的属性类型相同的那个方法便是。所以我们例子测试代码中最终将Date类型的dateInCar转换为String类型的dateInCarDto时,会使用DataMapper中的public String asString(Date date)这个方法完成类型转换。
所以,当我们去看MapStruct最终生成的CarMapperImpl这个类时,会发现在赋值时,用到了asString这个方法,如下图:
[MapStruct]数据类型转换_第2张图片

6. 通过@Qualifier指定要是用的映射方法[5.9. Mapping method selection based on qualifiers]

上面我们讲过在将实例A的属性赋值给实例B的属性时,可以自定一个方法,然后赋值时就会调用这个方法,在里面我们可以自定义一些逻辑和业务。我们也说过,关于如何MapStruct如何知道使用那个方法的规则---就是根据参数和返回值。

但是问题来了,如果有两个方法的参数和返回值都一样,就会出现问题,看下面例子:

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
}

// 要多些这个类,是因为Mapper无法写两个方法,只能写一个。所以通过这种方式
public class NameTranslation {
    public String translateNameEG(String name) {
        return name + "1";

    }

    public String translateNameGE(String name) {
        return name + "2";
    }
}

@Mapper(uses = NameTranslation.class)//指定映射时,在NameTranslation类中查找符合规则的方法
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    CarDto carToCarDto(Car car);
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "Morris");
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); // 运行代码会报异常
    }
}

我们运行上面代码,你会发现报如下错误:

这是因为,MapStruct不知道该使用那个方法了,虽然方法名不同,但是MapStruct是根据参数和返回值来决定使用那个方法的。这些就出现混淆了。所以就报错了。

此时,要解决这个问题,就用到了这个小节的知识点,使用@Qualifier来指定用哪个就行。

具体做法如下:

首先,通过@Qualifier定义一个自定义注解

然后,然后在通过@Qualifier给映射的方法自定义一个注解

具体来看一下实现的代码(代码很多,耐心看完):

第一步:定义要进行转换实例对应的类

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
}

第二步:定义这小节的知识点中讲到的@Qualifier注解。这些注解主要在Mapper中使用,用来在@Mapping指定使用那个对应的方法来进行映射。

// ---------------------------
@Qualifier
@Target(ElementType.TYPE)//[1]
@Retention(RetentionPolicy.CLASS)
public @interface Translator {
}

//对产生混淆的方法通过注解进行区分,每个方法一个注解,共2个注解,如下:
@Qualifier
@Target(ElementType.METHOD)//[2]
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}

@Qualifier
@Target(ElementType.METHOD)//[2]
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}

注意观察[1]和[2]的区别昂。

第三步:在Mapper指定使用上面自定义的注解,来指定映射时使用哪个方法,如下:

@Mapper(uses = NameTranslation.class)//[1]
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    @Mapping( target = "name", qualifiedBy = { Translator.class, EnglishToGerman.class } )//[2]
    CarDto carToCarDto(Car car);
}

使用时要[1][2]一起使用,意思就是告诉MapStruct到NameTranslation类中的 使用Translator注解下的EnglishToGerman注解对应的方法来对name属性进行映射。

第四步:运行来看结果

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "Morris");
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); //CarDto(name=MorrisEG)
    }
}

通过运行结果可以看出对象转换后,name属性的值多了EG,说明指定映射方法成功了。

东西有点多,大家耐心看一下。

7.通过qualifiers指定default方法进行属性映射[5.10. Combining qualifiers with defaults]

官网中的这个标题,乍一看有点晕,其实就是说在Mapper接口中写了多个default方法,然后通过qualifiedByName来指定使用那个方法来进行映射,其实就是比上面自定义@Qualifer更方便一点而已(需要单独写类和注解)。

下面先直接看代码:

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    @Mapping( target = "name", source = "name" ,qualifiedByName = "nameInCarToNameInCarDto1", defaultValue = "DEFAULT" )
    CarDto carToCarDto(Car car);

    @Named("nameInCarToNameInCarDto1")
    default String defaultValueForQualifier1(String name) {
        return name + "default1";
    }

    @Named("nameInCarToNameInCarDto2")
    default String defaultValueForQualifier2(String name) {
        return name + "default2";
    }
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( null);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); //CarDto(name=focusdefault1)
    }
}

代码中在Mapper接口中写了2个default方法,这两个方法专门用来进行属性映射,然后再@Mapping中通过qualifiedByName来指定当前属性映射时用哪个方法。所以从打印结果可以看出我们用了第一个方法。

如果在映射时,我们的Car实例中的name属性如果为null,name就会使用defaultValue指定的值作为默认值传到我们的映射方法中。大家可以试一下下面的代码:

 public static void main(String[] args) {
        Car car = new Car( null);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); //CarDto(name=DEFAULTdefault1)
    }

看到结果了吧,相信大家都明白了,我就不多做解释了。

到此,这部分告一段落,会在新的一篇中继续新的内容

你可能感兴趣的:(java,java)