[MapStruct]基础映射篇

1.相同属性名之间的映射

 

当两个类(Car和CarDto)中的属性名相同时,可自动将属性的值进行映射。如下:

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

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
    private int price;
}
@Mapper
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", 5);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); // CarDto(name=Morris, price=5)
    }
}
如果在carToCarDto方法上添加了@BeanMapping(ignoreByDefault = true)后,就会禁用同名属性间的自动映射。也就是源对象和目标对象即便是属性名相同,也不会自动将值进行映射,此时就必须使用下面说的@Mapping注解进行映射才行

2.不同属性名之间的映射

当两个类(Car和CarDto)中的属性名部分相同,部分不相同时,默认只会将相同属性的值进行映射,那么如何对两个不不同属性进行值的映射呢?就需要使用@Mapping这个注解,如下:

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


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


@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    @Mapping(source = "priceInCar",target = "priceInCarDto") //[3]
    CarDto carToCarDto(Car car);
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "Morris", 5);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); 
        //打印内容:CarDto(name=Morris, priceInCarDto=5)
        //如果不通过[3]出映射结果如下CarDto(name=Morris, priceInCarDto=0)
    }
}

从代码可以看出[1][2]处的属性名不同,MapStruct默认的只会对相同部分的属性进行映射,所以不通部分就要单独使用[3]处的@Mapper来进行单独映射(多个属性就要用多个@Mapper)。

 @Mapper作用:MapStruct会将自动将这个注解所在的接口生成一个实现类,里面就是源对象(source)和目标对象(target)属性之间映射的逻辑。

3. 通过自定义一个注解,将所有的映射关系放到注解中。

假设现在两个类Car和CarDto中的属性名不相同,你又不想将所有的映射关系写到Mapper接口中,那么你可以将这些映射单独拿出来放到一个自定义的注解中,然后再Mapper接口中直接使用这个自定义的注解即可。如下:

@Data
@AllArgsConstructor
public class Car {
    private String nameInCar;
    private String colorInCar;
}

@Data
@AllArgsConstructor
public class CarDto {
    private String nameInCarDto;
    private String colorInCarDto;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    //写多个@Mapping的一种简单方法就是将这个多个Mapper写到一个自定义注解中。
    //通过自定义注解,将多个属性之间的映射体现写好,然后用一个注解代替多个@Mapping注解
    @Car2CarDtoMapping
    CarDto carToCarDto(Car car);
}

@Retention(RetentionPolicy.CLASS)
@Mapping(source = "nameInCar", target = "nameInCarDto")
@Mapping(source = "colorInCar", target = "colorInCarDto")
public @interface Car2CarDtoMapping {
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "FOCUS", "YELLOW");

        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto); //CarDto(nameInCarDto=FOCUS, colorInCarDto=YELLOW)
    }
}

通过Car2CarDtoMapping这个注解,将不同属性的映射关系写出来,然后再CarMapper中直接使用这个注解就行了。当然,使用了这个注解后,我们仍然还可以在Mapper接口中使用@Mapping设置,这两者不冲突,可以一同使用。

4.使用自定义方法进行某个属性的映射

某些特定情况下,我们需要手动的通过指定某个方法来完成属性从某个类型到另外一个MapStruct无法生成的类型,或者说我们想通过某个方法来完成某个属性到另外一个属性的映射,我们可以在这个方法里写一些特定的逻辑。

具体的实现方式为通过在Mapper接口中default方法来完成。然后MapStruct会根据这个方法中的参数和返回值推测处要通过这个方法给哪个属性设置过映射关系,先看代码,如下:

@Data
@AllArgsConstructor
@ToString
public class Car {
    private String name;
    private int priceInCar;
    private Person person; //[1] 这个属性就是通过default方法要进行映射的源属性
}

@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String name;
    private int priceInCarDto;
    private PersonDto personDto;//[2] 这个属性就是通过default方法要进行映射的目标属性
}

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


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


@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    @Mapping(source = "priceInCar",target = "priceInCarDto")
    @Mapping(source = "person",target = "personDto") // [3]因为两个属性名称不同,所以这里必须映射一下,这是[4]的前提条件
    CarDto carToCarDto(Car car);
    //[4]MapStruct会发现[3]中的source与这个方法的参数类型相同,target中属性的类型,与方法的返回值相同,然后在进行属性映射时,就会调用这个方法
    // 可查看具体的CarMapper的实现类。
    default PersonDto personToPersonDto(Person person) {
        // 特殊的逻辑需求,加hello
        return new PersonDto("hello" + person.getName());
    }
}

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

代码中,我们需要将[1],[2]俩个属性通过default方法来手动完成属性间映射的逻辑(添加一个hello字符串),这个逻辑是MapStruct自动无法完成的。在[3]处,我们先指定好属性间的映射关系,这个@Mapping主要是为了告诉MapStruct哪两个属性之间有映射关系,也就是把谁的属性值给谁。因为我们需求是在赋值的之前还需要添加一个hello字符串,这个是无法自动完成的,所以我们就自定义了一个方法,让这个方法来负责映射的逻辑,也就是将谁的属性给谁。

那么MapStruct怎么知道是用这个方法来讲person属性的值给personDto呢?MapStruct会看@Mapping的source对应的属性名的类型是否与这个方法的参数类型相同,target对应的属性名的类型是否与这个方法返回值相同,如果都匹配上,就会用这个方法来处理。说白了就是找那个方法的参数类型与source的属性名类型匹配,并且返回值的类型与target的属性名的类型相同的方法,只要匹配上了,就是用这个方法来处理映射的逻辑。

这里用default方法是因为java8以后默认可以在接口中直接写一个default方法。所以我们直接在Mapper中用了default方法。

如果要定义多个方法来进行多个属性的映射处理,那么我们可以通过抽象类来当做Mapper(MapStruct官网说过,抽象类和接口都可以用来写Mapper)

 下面写一个通过抽象类,来写一个通过多个方法来处理多个属性映射的例子:

// 省略与上面接口中用到的相同类,只写不同的类,其他的用上面的代码就行

@Mapper
public abstract class CarMapperAbstract {
    static CarMapperAbstract INSTANCE = Mappers.getMapper( CarMapperAbstract.class );
    @Mapping(source = "priceInCar",target = "priceInCarDto")//[1]
    @Mapping(source = "person",target = "personDto") //[2]
    abstract CarDto carToCarDto(Car car);
    //[3]:在[2]中指定的person的值给personDto属性时,会执行这个方法来完成
    public PersonDto personToPersonDto(Person person) {
        return new PersonDto("hello" + person.getName());
    }
    //[4]:在[1]中指定的priceInCar的值给PriceInCarDto属性时,会执行这个方法来完成
    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))
    }
}

从例子中可以看出Mapper通过抽象类的方式替换了接口的方式,同时priceInCar和priceInCarDto两个属性也是用了priceInCarToPriceInCarDto方法来完成映射逻辑,所以结果中priceInCarDto的值变成了6。

这样在一个类中就可以同时实现通过多个自定义方法来完成多个属性之间的映射逻辑了。而如果使用接口来编写Mapper,只能在接口中写一个方法。

5. 将多个对象中的属性映射到一个对象中(3.4. Mapping methods with several source parameters)

假如我们现在的需求如下图

 我们需要将两个对象Car和Person中的某些属性值映射到CarDto中的属性中,那么我们该怎么做,其实很简单就是将两个对象Car和Person当做参数传入即可(之前都是传一个参数),然后通过参数.属性进行映射即可。代码如下:

@Data
@AllArgsConstructor
public class Car {
    private String nameInCar;
    private String colorInCar;
}

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


@Data
@AllArgsConstructor
@ToString
public class CarDto {
    private String nameInCarDto;
    private String colorInCarDto;
    private String driverNameInCarDto;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    //将多个参数中属性映射到一个类中
    // source对应的就是参数名,如果你的参数是对象就用对象.属性;如果参数是基本类型,那么就直接用参数名
    // target对应的是返回对象中的属性。
    @Mapping(source = "person.name", target = "driverNameInCarDto")
    @Mapping(source = "car.nameInCar", target = "nameInCarDto")
    @Mapping(source = "car.colorInCar", target = "colorInCarDto")
    public CarDto carToCarDto(Car car,Person person);//两个参数的内容映射到CarDto中
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "FOCUS", "YELLOW");

        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car,new Person("john",24));
        System.out.println(carDto); //CarDto(nameInCarDto=FOCUS, colorInCarDto=YELLOW, driverNameInCarDto=john)
    }
}

看结果会发现,carDto中的属性值就是Car和Person对象中的。
因为有了两个参数,所以就必须告诉source使用哪个参数中的属性,所以就必须用变量名.属性名的形式,如source = "person.name"。

如果参数不是对象而是进本数据类型,那么就直接使用参数名就行了。如官网的例子

6. 将对象中内嵌bean中的属性赋值给目标对象的属性

先来理解一下标题的意思,看下图:

 Car中的dirver就是所谓的内嵌的bean,它是Car的一个属性,现在就要将这个driver中的属性(age和sex)的值直接映射给CarDto中的age和sex。这就是标题的意思。

那么具体怎么来实现呢?看一下下面的例子,在解释一下就明白了:

@Data
@AllArgsConstructor
public class Car {
    private String brand;
    private Person driver;
}

@Data
@AllArgsConstructor
public class Person {
    private int age;
    private String sex;
}

@Data
@AllArgsConstructor
public class CarDto {
    private String brand;
    private int age;
    private String sex;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    @Mapping(source = "driver", target = ".")//[1]
    CarDto carToCarDto(Car car);
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "FOCUS",new Person(24,"man"));
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto);//CarDto(brand=FOCUS, age=24, sex=man)
    }
}

注意[1]处,可以将driver(person类)中所有的属性值直接给CarDto中同名的属性就是通过target="."来完成的。这个点就表示this的意思,翻译过来就是说把driver中的属性映射到this所指向的类也就是carDto中的同名的属性中。这也就是这小节的知识点。前提是属性名必须都相同才行。

 7.更新一个已存在的对象中的属性

有时候我们不是要新建一个对象,而是要更新一个已存在的对象,这个MapStruct也可以帮我们完成。具体方法就是在想要更新的对象前用@MappingTarget标注一下就行。具体的看下面例子:

@Data
@AllArgsConstructor
public class Car {
    private String nameInCar;
    private String colorInCar;
}

@Data
@AllArgsConstructor
public class CarDto {
    private String nameInCarDto;
    private String colorInCarDto;
}

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    //@Mapping(source = "nameInCar", target = "nameInCarDto")上下两种写法都行
    @Mapping(source = "car.nameInCar", target = "nameInCarDto")
    @Mapping(source = "car.colorInCar", target = "colorInCarDto")
    public void updateCarDto(Car car, @MappingTarget CarDto carDto); //[1]
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car( "FOCUS", "YELLOW");
        CarDto carDto = new CarDto("FOCUS", "blule");

        CarMapper.INSTANCE.updateCarDto(car,carDto);//将car中的属性值更新给carDto中对应的属性
        System.out.println(carDto); // CarDto(nameInCarDto=FOCUS, colorInCarDto=YELLOW)
    }
}

看[1]处,通过@MappingTarget注解,MapStruct就知道了要去更新carDto对象,然后用前面的参数中的值更新到carDto中。

要注意的是如果两个对象中的属性不相同一定要用@Mapping来映射一下。映射后才能MapStruct才会知道将来从哪个属性中取值放到那个属性中,看一下下面MapStruct生成的实现类就明白了,它就是根据上面[1]处的两个@Mapping生成的。
        

 这就是这小节的内容了,对应官网3.6. Updating existing bean instances

8.映射没有get/set的属性[3.7. Mappings with direct field access]

当我们的实例中的属性没有get/set方法时,官网中提到MapStruct会使用一个所谓的read/write accessor来进行属性值的设置。那么我们就先来搞清楚什么是read/write accessor:

比如你要将一个实例A中的属性的值给实例B的属性,就必须先读取实例A中的属性才行,如果能从实例A中读出来这个属性就是read accessor。同理如果要把值写到实例B中的属性,实例B中的属性要能赋值进去那么他就是write accessor。

那么具体什么时候属性才算是read accessor呢?官方说了:你的属性是public或者public final修饰的就是read accessor,才可以从这个属性中取值。如果是static修饰的就不是read accessor(private肯定不是,因为他是私有的,实例外面根本访问不到)。

那么什么时候属性才算是write accessor呢?官方说了:你的属性只有是public修饰的时候才是write accessor,才可以给这属性赋值。如果是static或者是final修饰就不是write accessor了。

下面通过例子来解释下,内容较多请耐心开完:

例子1:

 红框中两个属性都是public所以满足上面我说的read/write accessor,也就是MapStrct就可以从car中读取属性,然后把值赋值给carDto中的属性。

例子2:实例A中(Car)中的属性不是read accessor的情况

 当给car的属性brand设值static后,根据官网说明这个属性就不是read accessor了,也就是没法从这个属性中读取数据了,所以编译时右边new Car("focus")时就直接报错了。

例子3:实例B中(CarDto)中的属性不是write accessor的情况

 根据官网说明,我们将CarDto的属性设值了一个static,让其变为非write accessor,这样就无法再对这个属性进行赋值了。所以将car转为carDto时会发现并没有将FOCUS这个属性赋值给carDto的brand属性,如上图右侧。具体当CarDto中的brand加了static后,MapStruct具体做了什么,可以看一下CarMapper的实现类。这里只要知道如果加了static就没法对其进行赋值就行了。

9.使用builder的方式 [3.8. Using builders]

所谓builder的方式是指在你的类中通过builder来对类中属性赋值。而不是使用get/set方法。官网指出只要我们按照其要求的写法,MapStruct就可以自动识别出是Builder的方式,基本规则如下:

  • 当类中有一个无参数的public static方法(代码[1]处),这个方法返回一个builder时(代码[2]处)。MapStruct就可以识别出,如下:
  • builder类中必须有一个无参的方法,这个方法返回我们要赋值的类,对应[4]处
@Data
@AllArgsConstructor
@ToString
public class Car {
    private String brand;
}

public class CarDto {
    private final String brand;

    protected CarDto(CarDto.Builder builder) {
        this.brand = builder.brand;
    }

    public static CarDto.Builder builder() {//[1] 无参的公共的静态方法
        return new CarDto.Builder(); // [2]返回了一个builder
    }

    public static class Builder {
        private String brand;

        public Builder brand(String brand) {//[3]方法名必须与属性名相同
            this.brand = brand;
            return this;
        }

        public CarDto create() {//[4]无参方法,返回目标对象也就是CarDto
            return new CarDto( this );
        }
    }
}

@Mapper
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( "FOCUS");
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto);// CarDto(brand=FOCUS)
    }
}

关于这个builder的其他高级特性,有机会在补充。

10.通过构造方法来映射目标类[3.9. Using Constructors]

意思就是说通过构造方法就知道要把实例A中的属性值给哪个实例中属性。MapStruct当检查如果类中有上一小节的builder时,就使用builder,如果找不到就会通过这小节将的构造方法。

这里所谓的使用builder和构造方法,是指自动生活生成的Mapper实现类中用的哪种方式来创建对象,看下图,就是通过builder的方式创建对象,然后赋值。

 再讲builder方式之前的所有例子中都是用的构造方式的形式创建实例的。所以这里我就不放例子代码了。自己去看一下自动生成的那个Mapper的实现类就行了。

这里我重点要介绍的是当类中有多个构造方法时,怎么指定用哪个或者MapStruct怎么来选择呢?

  • 当构造方法上有@Default注解时,会优先使用这个构造方法来创建实例
  • 当只有一个public的构造方法时,会优先使用这个,其他的非public的构造方法会忽略
  • 如果有无参构造方法,会优先使用无参构造方法,其他构造方法会忽略
  • 如果有多个都满足条件的,那么就会编译报错了,这是就要使用@Default来指定一个

11.将map中的内容映射到目标实例中[3.10. Mapping Map to Bean] 

前面说的大多数都是如何将实例A中的属性的值映射到实例B中的属性,这一节说一下如何讲一个Map中的属性的值映射给实例B中的属性里。其实想当简单,用法相同,直接上代码:

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

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    // 使用@MappingTarget作为要更新的类,通过第一个参数中的内容,更新到@MappingTarget的类中
    @Mapping(source = "name", target = "nameInCar")
    @Mapping(source = "color", target = "colorInCar")
    public Car map2Car(Map<String, String> map);
}

public class Test {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("name","雪佛兰");
        map.put("color","red");
        Car car = CarMapper.INSTANCE.map2Car(map);
        System.out.println(car);
    }
}

代码中可以看出,直接讲Map当做参数放到Mapper中就行了,然后通过@Mapping映射即可。然后大家去看一下Mapper的实现类,看看MapStruct最终生成的代买是什么样子的就没明白了。

到此,这一部分对应官网第三章节的内容就结束了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值