MapStruct 的基础使用

550人浏览 / 0人评论

参考

http://tools.jbritian.com/link/v

介绍

MapStruct 类似于我们熟悉的 BeanUtils, 是一个 Bean 的转换框架。

它与 BeanUtils 最大的不同之处在于,其并不是在程序运行过程中通过反射进行字段复制的,而是在编译期生成用于字段复制的代码(类似于 Lombok 生成 get() 和 set() 方法),这种特性使得该框架在运行时相比于 BeanUtils 有很大的性能提升。

引入

Maven

由于 MapStruct 和 Lombok 都会在编译期生成代码,如果配置不当,则会产生冲突,因此在工程中同时使用这两个包时,应该按照以下方案导入:

1、当 POM 中不包含 Lombok 时:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

2、当 POM 中包含 Lombok 且不包含 <annotationProcessorPaths> 时:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

注意:引入时,mapstruct-processor 必须 lombok 后面。

3、当 POM 中包含 Lombok 且包含 <annotationProcessorPaths> 时:

<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</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</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <properties>
                        <org.mapstruct.version>1.5.2.Final</org.mapstruct.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</source> <!-- depending on your project -->
                                    <target>1.8</target> <!-- depending on your project -->
                                    <annotationProcessorPaths>
                                        <path>
                                            <groupId>org.projectlombok</groupId>
                                            <artifactId>lombok</artifactId>
                                            <version>1.18.24</version>
                                        </path>
                                        <path>
                                            <groupId>org.mapstruct</groupId>
                                            <artifactId>mapstruct-processor</artifactId>
                                            <version>${org.mapstruct.version}</version>
                                        </path>
                                        <!-- other annotation processors -->
                                    </annotationProcessorPaths>
                                </configuration>
                            </plugin>
                        </plugins>
                    </build>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Idea Plugin

搜索 MapStruct Support 安装即可,可以在使用 MapStruct 时获得更加丰富代码提示。

使用

1、字段完全一致

待转换的类:

@Data
@Builder
public class Source {
    private Long id;
    private Long age;
    private String userNick;
}

转换目标类:

@Data
public class Target {
    private Long id;
    private Long age;
    private String userNick;
}

转换器:

// 注意:Mapper是Mapstruct的注解。
@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);

    public abstract Target convert(Source source);
}

使用示例:

final Source source = Source.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);

2、对应的字段名不一致、类型不一致

待转换的类:

@Data
@Builder
public class Source {
    private Long id;
    private Long age;
    private String userNick;
}

转换目标类:

@Data
public class Target {
    private Long id;
    private Integer age;
    private String nick;
}

转换目标类修改了 age 字段的类型,和 userNick 字段的名字,这两个类的字段仍然是一一对应的。

转换器:


@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);

    // 字段类型映射修改
    @Mapping(source = "age", target = "age", resultType = Integer.class)
    // 字段名映射修改
    @Mapping(source = "userNick", target = "nick")
    public abstract Target convert(Source source);
}

使用示例:

final Source source = Source.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);

3、一对多字段互转

在业务代码中,常出现需要将一个类中的一些字段转换为另一个类的 JSON 字段的情况,以下是一个简单的例子:

互相转换的类:

  • VO:前端渲染内容。
@Data
@Builder
public class VO {
    private Long id;
    private Long age;
    private String userNick;
}
  • DTO:传输内容,其中仅包含 id,其余字段均存放在 extra 字段中。
@Data
public class DTO {
    private Long id;
    private String extra;
}

1)多个字段转换为一个字段

  • 常用于将多个字段转为 JSON 字段,在以下示例中,为了避免引入第三方包(如 FastJson),仅使用字符串拼接两个字段,Json 方式同理。
@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    
    @Mapping(target = "extra", source = "vo", qualifiedByName = "convertToExtra")
    public abstract DTO convert(VO vo);
        
    @Named("convertToExtra")
    public String convertToExtra(VO vo) {
        return String.format("%s,%s", vo.getAge(), vo.getUserNick());
    }
}

将多个字段转换为一个字段,需要以下几个步骤:

  • 创建自定义转换方法(本例为 convertToExtra()):
    • 方法入参类型为被转换的类(本例为 VO),出参为转换好的字段(本例为 extra);
    • 为方法加上@Named 注解,并自定义该方法在 mapStruct 中的名字(本例中为 convertToExtra)。
  • 在转换方法上增加 Mapping 注解,其中:
    • source 字段必须与转换方法入参名字相同(本例中均为 vo);
    • target 字段为目标字段(本例中为 extra);
    • qualifiedByName 字段为上述自定义的方法名字。

2)将一个字段转换为多个字段

  • 该方法常用于从 JSON 字段中取出数据。原理与上述方法类似,定义两个自定义转换方法,用于转换 extra 字段。
@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);

    @Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
    @Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
    public abstract VO convertToVO(DTO dto);
        
    @Named("extractAge")
    public Long extractAge(String extra) {
        // 从extra中提取第一个值
        return Long.valueOf(extra.split(",")[0]);
    }

    @Named("extractUserNick")
    public String extractUserNick(String extra) {
        // 从extra中提取第二个值
        return extra.split(",")[1];
    }
}

使用示例:

final VO vo = VO.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();

// 转为DTO
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);

// 转回VO
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);

4、为转换加缓存

在上述的两个方法(extractAge 和 extractUserNick)中,进行了重复的 String.split() 操作,如果该操作更加复杂(如从 JSON 串中提取内容),则会造成资源的浪费。

为此,可以给当前的 converter 加一个缓存字段 extraFieldBufferLocal,如下例所示。在例子中,每次解析 extra 字段前,先判断 buffer 是否存在,如果存在则使用缓存内容。

注:Mapstruct 中使用 xxx.INSTANT 获得的转换器是单例的,因此,如果要在多线程环境中转换时加入缓存,其缓存必须声明为 ThreadLocal 类型。

@Mapper
public abstract class Converter { 
    public static Converter INSTANT = Mappers.getMapper(Converter.class);

    /**
     * extra字段解析后的buffer,避免多次重复解析
     */
    private final ThreadLocal<String[]> extraFieldBufferLocal = new ThreadLocal<>();

    @Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
    @Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
    public abstract VO convertToVO(DTO dto);
    
    @Named("extractAge")
    public Long extractAge(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extractExtraField(extra));
        }

        return Long.valueOf(extraFieldBufferLocal.get()[0]);
    }

    @Named("extractUserNick")
    public String extractUserNick(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extractExtraField(extra));
        }

        return extraFieldBufferLocal.get()[1];
    }

    /**
     * 提取extra字段
     *
     * @param extra extra字段
     * @return extra字段的提取结果
     */
    public String[] extractExtraField(final String extra) {
        return extra.split(",");
    }
}

5、子类字段互转

常用于平铺类和嵌套类之间的转换,例如,前端需要将类中的所有字段打平,就可以参考以下示例代码。

互相转换的类:

VO:

@Data
@Builder
public class VO {
    private Long id;
    private Date gmtCreate;
    private Long age;
    private String userNick;
}

DTO:

@Data
public class DTO {
    private Long id;
    private Date gmtCreate;
    private Config config;

    @Data
    public static class Config{
        private String age;
        private String userNick;
    }
}

在 DTO 中,VO 的 age 和 userNick 字段被放到了子类 Config 中。此时也可以使用上一节展示的自定义转换函数法进行转换,不过 MapStruct 提供了一种更加直观简单的转换方法:

转换器:


@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);

    @Mapping(target = "config.age", source = "age")
    @Mapping(target = "config.userNick", source = "userNick")
    abstract DTO convertToDTO(VO source);

    @Mapping(target = "age", source = "config.age")
    @Mapping(target = "userNick", source = "config.userNick")
    abstract VO convertToVO(DTO dto);
}

使用示例:


final VO vo = VO.builder()
        .id(1L)
        .age(10L)
        .gmtCreate(new Date())
        .userNick("nick")
        .build();

final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);

final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);

6、利用 Spring 进行依赖注入

本文以上示例代码中,都是使用 Converter.INSTANT 来获得 Convert 实例,这在业务代码中可能显得有些突兀,而 MapStruct 提供了依赖注入的机制,让我们能够在 Spring 的环境下,更优雅的获得 Converter,以下是一个例子:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class Converter {
    public abstract Target convert(Source source);
}

使用示例:

@Controller
public class MainController {

    @Resource
    private Converter convert;

    @GetMapping("/")
    @ResponseBody
    public boolean test() {
        final Source source = Source.builder()
                .id(1L)
                .age(18L)
                .userNick("nick")
                .build();

        final Target result = convert.convert(source);
        System.out.println(result);

        return true;
    }
}

全部评论