MapStruct介绍和基本功能示例

作用

在Java中用来做不同对象之间的转换(DTO,DO,BO,VO...),使用方式简单,只需要按规则写一个相关接口,甚至不需要实现(较类似于jpa的JpaRepository接口的使用方式),就能直接完成对象间的转换。
以下是官方介绍(摘要):官方文档链接 MapStruct官网

MapStruct 是一个 Java 注释处理器,用于生成类型安全的 bean 映射类。
您所要做的就是定义一个映射器接口,该接口可以声明任何您所需的映射方法。在编译期间,MapStruct 将生成此接口的实现。这个实现使用普通的 Java 方法调用(get,set方法)来映射源对象和目标对象,而不是使用反射

优点:

  1. 与手动编写映射代码相比,MapStruct 通过生成编写繁琐且容易出错的代码来节省时间。
  2. 编译时类型安全:只能映射相互映射的对象和属性,不能将订单实体意外映射到客户 DTO 等。
  3. 当出现以下情况,程序在编译期间就会给出提示。
           映射不完整(并非所有目标属性都被映射)
           映射不正确(找不到合适的映射方法或类型转换)

使用

下载插件(不是必须的,但是挺好用)

idea中下载 mapstruct support 插件,安装重启Idea:

image.png


插件功能示例:

  1. 在参数上,按 ctrl + 鼠标左键 ,能够自动进入参数所在类文件

go-to-declaration-from-target.gif

  1. 在要写映射关系的 target和source中 按 代码提示键(我的是alt + /),能自动给出代码提示,如果出不来,可能需要设置一下手动唤出代码提示功能,参考:手动唤出代码提示 设置方法

        source-auto-complete.gif

  1. Find Usages,对着参数或参数的get方法使用Find Usages 时,会给出 mapping中的提示(似乎没啥用)

        find-usages-from-source-method.png

引入依赖(maven)

这里需要注意:idea版本不同,引入的依赖和配置也有一些小差别,这里我的idea版本是 2019.3,其他版本idea可以先按着这个配置写,有问题的话,参考文末"常见问题"一节,若无法解决,可以在百度或google以下具体原因.
MapStruct主要依赖:其他依赖管理工具(Ant,Gradle)的导入,可见 官方文档链接

<!--最好版本号和这个一致,不然可能存在包不兼容等问题,详见文末“常见问题”一节-->
<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
  	<lombok.version>1.18.12</lombok.version>
</properties>

<dependencies>
    <dependency>
        <groupid>org.mapstruct</groupid>
        <artifactid>mapstruct</artifactid>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupid>org.apache.maven.plugins</groupid>
            <artifactid>maven-compiler-plugin</artifactid>
            <version>3.8.1</version>
            <configuration>
                <source>1.8
                <target>1.8</target>
                <annotationprocessorpaths>
                    <path>
                        <groupid>org.mapstruct</groupid>
                        <artifactid>mapstruct-processor</artifactid>
                        <version>${org.mapstruct.version}</version>
                    </path>
                  	<!--下面这个 项目中不使用 Lombok的话 不用加-->
                    <path>
                          <groupid>org.projectlombok</groupid>
                          <artifactid>lombok</artifactid>
                          <version>${lombok.version}</version>
                      </path>
                </annotationprocessorpaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Lombok依赖:(版本最好在1.16.16以上,否则会出现问题)通常是和lombok一起使用的

        <dependency>
            <groupid>org.projectlombok</groupid>
            <artifactid>lombok</artifactid>
            <version>${lombok.version}</version>
          	// 版本号 1.18.12
        </dependency>

调用方式

  1. spring注入方式:在Mapper中加入 componentModel="spring"

@Mapper(componentModel="spring")
写上之后,可以通过 @autowire 注解来注入这个工具类

  1. 默认方式

在类中写上 &nbsp;CarConverter INSTANCE = Mappers.getMapper(CarConverter.class);
其中 CarConverter 是类名

简单例子及原理

例子

有如下两个类,
image.pngimage.png
要从Car转成CarDto,只需要写一个如下接口就可以了:

@Mapper
public interface CarConverter {
	
    // 实例
    CarConverter INSTANCE = Mappers.getMapper(CarConverter.class);
	
    /**
    	属性名一样的,会自动映射并填充,属性名不一样的用下面这个 @Mapping 注解,定义映射关系
        source 为数据来源,target为要填充的目标属性
    */
    @Mapping(source = "wheel", target = "whe")
    CarDto carToDto(Car car);
}

测试使用:

    @Test
    void testt(){
        Car car = new Car("五菱宏光", "白", "4");
        CarDto carDto = CarConverter.INSTANCE.carToDto(car);
        System.out.println(carDto);
        // 输出为: CarDto(carName=五菱宏光, color=白, whe=4)
    }

示例car 和 cardto 代码

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {
    private String carName;
    private String color;
    private String wheel;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CarDto {

    private String carName;
    private String color;
    private String whe;
}

原理

在程序编译时,会自动生成我们所写的转换接口(@Mapper注解修饰的接口)的实现类,这个实现类在classes路径下,上面的例子生成的实现类如下:

// 这个类是自动生成,不需要自己写
public class CarConverterImpl implements CarConverter {
    public CarConverterImpl() {
    }

    public CarDto carToDto(Car car) {
        if (car == null) {
            return null;
        } else {
            CarDto carDto = new CarDto();
            carDto.setWhe(car.getWheel());
            carDto.setCarName(car.getCarName());
            carDto.setColor(car.getColor());
            return carDto;
        }
    }
}

可以看到,它就是调用的目标生成类的 setter方法 和 数据源类的 getter方法,来进行对象的创建的,后面会介绍复杂一点的数据结构的转换。

多个入参构造一个对象

如下例子:通过User和Role,构造一个UserRoleDTO
image.pngimage.pngimage.png

// 多个Mapping时,用 Mappings 包起来
@Mappings({
            @Mapping(source = "user.userId", target = "id"), // 把user中的userId绑定到目标对象的id属性中
            @Mapping(source = "user.name", target = "name"), // user和 role中都有name属性,所以需要指定一个来传给UserRoleDto
    })
    UserRoleDto toUserRoleDto(User user, Role role);

上面代码中需要注意的是,不管有多少入参,当目标类中的属性名和入参类型中的属性名一致时,都不用写Mapping,会自动映射,除非多个参数的类型中有同名的属性,这种情况需要指定一个source,通过类似 user.name的方式(user 是参数名)。
测试:

User user = new User(1L, "小王八蛋", "北京");
Role role = new Role("管理员", "2", "描述");
UserRoleDTO userRoleDTO = CarConverter.INSTANCE.toUserRoleDto(user, role);
System.out.println(userRoleDTO);	
// 输出UserRoleDTO(id=1, name=小王八蛋, address=北京, roleName=null, roleId=2, description=描述)

User ,Role, UserRoleDTO 代码

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;

    private String name;

    private String address;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDTO {
    private Long id;
    private String name;
    private String address;
    private String roleName;
    private String roleId;
    private String description;
}

数据类型转换(int -> String 等)

基本数据类型的转换例如:int、long、String等会自动完成,不需要写Mapping映射关系。
指定格式的数据类型转换:

@Mapper
public interface DataFormatMapper {

    DataFormatMapper INSTANCE = Mappers.getMapper(DataFormatMapper.class);

    /**
     * numberFormat 指定基本数据类型与String之间的转换
     * dateFormat 指定Date和String之间的转换
     */
    @Mappings({
        	// 这里 # 表示多个数字,  0 表示一个数字(0-9)
            @Mapping(source = "price", target = "price", numberFormat = "#.00元"),
            @Mapping(source = "stock", target = "stock", numberFormat = "#个"),
            @Mapping(target = "saleTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "validTime", dateFormat = "yyyy-MM-dd HH:mm")
    })
    ProductDTO toDto(Product product);;
	
}

类中包含其他类的映射

  1. 如果是类型相同的两个类,写映射关系后,是简单的浅拷贝(只拷贝引用),不会新建对象。

如:User--> UserRoleDTO(其中都包含Role类),映射时只拷贝role的引用,不会生成一个新的role对象。
image.pngimage.png
转换方法:

// role 的属性名一样,类型也一样,不用写Mapping,还有就是 role的赋值是浅拷贝 只拷贝引用,不生成新的对象
@Mapping(source = "userId", target = "id"), // 把user中的userId绑定到目标对象的id属性中
UserRoleDTO toUserRoleDto(User user);

User,UserRoleDTO代码:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private Role role;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDTO {
    private Long id;
    private String name;
    private String address;
    private Role role;
}
  1. 类型不同的话,如下

分别是:User ---UserRoleDTO --- Role --- RoleDTO
image.pngimage.pngimage.pngimage.png
要将User 转换成 UerRoleDTO:除了写一个User转UerRoleDTO的转换方法外,还要额外写一个 Role转成RoleDTO的转换方法,当执行User转UerRoleDTO时,会自动的调用这个Role转成RoleDTO的方法来生成对象(是通过查找 **参数类型和返回类型与我们所需转换的两个类相符合的方法 **来执行的),如下。

    // 这两个方法写在一起也行,分别写在两个接口里也行,执行时,都会自动调用
	@Mapping(source = "userId", target = "id") // 把user中的userId绑定到目标对象的id属性中
    UserRoleDTO toUserRoleDto(User user);

	// 当上面那个toUserRoleDto方法调用时,会自动调用这个方法来完成 User类中的Role 到 UserRoleDTO类中的RoleDTO  的转换
    @Mapping(source = "roleId", target = "id")
    RoleDTO toRoleDto(Role role);

自定义映射方式

// 自定义的映射方法:转换boolen为String时,做一些判断然后返回对应的值。
@Named("DoneFormater")
public class DoneFormater {

    @Named("DoneFormater")
    public String toStr(Boolean isDone) {
        if (isDone) {
            return "已完成";
        } else {
            return "未完成";
        }
    }

    @Named("DoneDetailFormater")
    public String toDetail(Boolean isDone) {
        if (isDone) {
            return "该产品已完成";
        } else {
            return "该产品未完成";
        }
    }

    public Boolean toBoolean(String str) {
        if (str.equals("已完成")) {
            return true;
        } else {
            return false;
        }
    }

}

// 通过uses 来导入上面我们写的 自定义映射方法
@Mapper( uses = {DoneFormater.class})
public interface ObjectQualiferMapper {

    ObjectQualiferMapper INSTANCE = Mappers.getMapper(ObjectQualiferMapper.class);
	
    // 当有多个方法 拥有一样的参数和返回类型时,需要指定使用其中的哪一个,使用qualifiedByName指定
    @Mapping(source = "isDone", target = "isDone", qualifiedByName = "DoneDetailFormater")
    ProductDTO toDto(Product product);

}

list映射

@Mapper(componentModel = "spring")
public interface UserMapping {

    /**
     * Student 转化为 User
     * @param Student
     * @return
     */
     User studentToUser(Student student);
     
    // 	当执行 下面这个List的转换时,会遍历list: students,
   	//  然后自动调用上面的Student转User的转换方法,来进行转换
     /**
     * Students 转化为 Users
     * @param Students
     * @return
     */
     List<user> studentsToUsers(List<student> students);
}

map映射

可以使用@MapMapping实现对keyvalue的分别映射

@Mapper
public interface MapMapper {

    MapMapper INSTANCE = Mappers.getMapper(MapMapper.class);

    @MapMapping(valueDateFormat = "yyyy-MM-dd HH:mm:ss")
    Map<string, string=""> toDTO(Map<long, date=""> map);

}

枚举映射

public enum E1 {

    E1_1,
    E1_2,
    E1_3

}

public enum E2 {

    E2_1,
    E2_2,
    E2_3

}

映射方法:使用@ValueMappings@ValueMapping ,可以理解成是用if判断枚举值,然后返回对应结果

@Mapper
public interface DataEnumMapper {

    DataEnumMapper INSTANCE = Mappers.getMapper(DataEnumMapper.class);

    @ValueMappings({
            @ValueMapping(target = "E1_1", source = "E2_1"),
            @ValueMapping(target = "E1_2", source = "E2_2"),
            @ValueMapping(target = MappingConstants.NULL, source = "E2_3") //转换成null
    })
    E1 toDTO(E2 e2);

}

使用表达式和默认值

// 使用外部的类,需要通过这种形式导入
@Mapper(imports = MathUtils.class)
public interface ObjectExpressionMapper {

    ObjectExpressionMapper INSTANCE = Mappers.getMapper(ObjectExpressionMapper.class);
	
    //expression中可以直接写Java代码
    @Mappings({
        
        	// 设置默认值
         	@Mapping(target = "productId", source = "productId", defaultValue = "123"), //当product的productId为null,设置为0
        	// 表达式
            @Mapping(target = "price", expression = "java(product.getPrice1() + product.getPrice2())"),//直接相加
            @Mapping(target = "price2", expression = "java(MathUtils.addAndReturn0(product.getPrice1(), product.getPrice2()))") //使用工具类处理
    		// 常量
        	@Mapping(target = "stock", constant = "100"), //固定设置为100, 常量
    })
    ProductDTO toDTO(Product product);

}

一个多级嵌套结构的例子

这是我们要生成的DTO,其中包含一个List<menusbean>,在MenusBean中又包含一个List<childrenbean>,这样的映射关系,参考下面的代码:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespLoginDTO {

    private String id;
    private String username;
    private String cname;
    private String sex;
    private String mobile;
    private String email;
    private String token;
    private String description;
    private String roleName;
    private String roleId;
    private List<menusbean> menus;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class MenusBean {
       
        private int id;
        private String title;
        private String icon;
        private List<childrenbean> children;


        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public static class ChildrenBean {
      
            private int id;
            private String title;
            private String path;


        }
    }
}


转换类:

@Mapper(componentModel = "spring")
public interface RespLoginDtoConverter {

    RespLoginDtoConverter INSTANCE = Mappers.getMapper(RespLoginDtoConverter.class);

    @Mappings({
            @Mapping(source = "user.userId", target = "id"),
            @Mapping(source = "userPerms", target = "menus")
    })
    RespLoginDTO createRespLoginDTO(User user, Role role, List<userperm> userPerms, String token);


    // 下面的几个方法都是为了辅组完成上面的那个createRespLoginDTO转换而写的
    List<resplogindto.menusbean> createMenusBeans(List<userperm> userPerms);

    @Mappings({
            @Mapping(source = "mainPerm.permId", target = "id"),
            @Mapping(source = "mainPerm.permTitle", target = "title"),
            @Mapping(source = "mainPerm.permIcon", target = "icon"),
            @Mapping(source = "childrenPrem", target = "children")
            //@Mapping(source = "userPerms.mainPerm", target = "menus"),
    })
    RespLoginDTO.MenusBean createMenusBean(UserPerm userPerm);

    List<resplogindto.menusbean.childrenbean> createMenusBeanChildrenBeans(List<perm> perms);

    @Mappings({
            @Mapping(source = "permId", target = "id"),
            @Mapping(source = "permTitle", target = "title"),
            @Mapping(source = "permPath", target = "path")
            //@Mapping(source = "userPerms.mainPerm", target = "menus"),
    })
    RespLoginDTO.MenusBean.ChildrenBean createChildrenBean(Perm perm);

}

其中User,Role 是常规的映射,不多解释,主要是要生成的DTO中的List<menusbean> menusList<userperm> userPerms 间的映射,下面是UserPerm类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserPerm {
    /**
     * 主权限
     */
    private Perm mainPerm;
    /**
     * 子权限
     */
    private List<perm> childrenPrem;
}
@Data
@EqualsAndHashCode(callSuper = false)
public class Perm implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long permId;
    private Long parentId;
    private String permCode;
    private String permName;
    private LocalDateTime createDate;
    private String permPath;
    private String permTitle;
    private String permIcon;
    private Boolean permType;
}

当执行List<userperm> userPerms 转换成List<menusbean> menus时,会自动找到 MenusBeanUserPerm的方法,然后在MenusBeanUserPerm的过程中,又需要找到List<perm>转成 List<childrenbean> 的方法,然后再List<perm>转成 List<childrenbean> 的过程中,又要找到PermChildrenBean的方法,如此循环无限套娃,就完成了最初的多个对象转换成一个RespLoginDTO对象的功能。

常见问题

写一下自己遇到的几个问题:

No property named “XXX“ exists in source parameter(s). Did you mean “null“?

根本原因:看一下项目的/classes里面,看对应的类是不是编译生成了,或者说类是有的,但是里面没有geter/setter等方法(主要是Lombok的版本原因)。
情况一、可能是idea版本的问题:idea 2018 之前的版本需要添加下面的配置,后期的版本就不需要了,我自己用的2019.3,不需要这个依赖也能跑 ,如果加上,一定要注意这个依赖的版本,需要和 org.mapstruct:mapstruct 包的版本完全一致,否则报错

		<dependency>
          <groupid>org.mapstruct</groupid>
          <artifactid>mapstruct-processor</artifactid>
          <version>${org.mapstruct.version}</version>
     			<!--这里我用的版本号 :1.4.2.Final -->
          <scope>provided</scope>
    </dependency>

情况二、可能是没写maven插件: 在biuld里面加上(注意这里maven-compiler-plugin的版本要在3.6以上),最好和下面的版本一致,否则可能仍然会报错

<build>
    <plugins>
        <plugin>
            <groupid>org.apache.maven.plugins</groupid>
            <artifactid>maven-compiler-plugin</artifactid>
            <version>3.8.1</version>
            <configuration>
                <source>1.8
                <target>1.8</target>
                <annotationprocessorpaths>
                    <path>
                        <groupid>org.mapstruct</groupid>
                        <artifactid>mapstruct-processor</artifactid>
                        <version>${org.mapstruct.version}</version>
                      	<!--这里我用的版本号 :1.4.2.Final -->
                    </path>
                  	<path>
                        <groupid>org.projectlombok</groupid>
                        <artifactid>lombok</artifactid>
                        <version>1.18.12</version>
                    </path>
                </annotationprocessorpaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Couldn’t retrieve @Mapper annotation

情况一、包冲突:项目中使用了swagger,swagger里面也包含mapstruct,排除掉就好

<dependency>
    <groupid>io.springfox</groupid>
    <artifactid>springfox-swagger2</artifactid>
    <version>${swagger2.version}</version>
    <scope>compile</scope>
    <exclusions>
        <exclusion>
          <groupid>org.mapstruct</groupid>
          <artifactid>mapstruct</artifactid>
        </exclusion>
    </exclusions>
</dependency>

情况二、导入mapstruct项目时,同时使用了 mapstruct-jdk8mapstruct-processor 但是版本不一致,将版本号改为一样即可。

<dependency>
    <groupid>org.mapstruct</groupid>
    <artifactid>mapstruct-jdk8</artifactid>
    <version>1.2.0.Final</version>
</dependency>
<dependency>
    <groupid>org.mapstruct</groupid>
    <artifactid>mapstruct-processor</artifactid>
    <version>1.2.0.Final</version>
</dependency>





















</resplogindto.menusbean.childrenbean></resplogindto.menusbean></long,></string,>

posted @ 2021-06-01 11:25  vb_ahh  阅读(11089)  评论(0编辑  收藏  举报