java 数据库 time 映射什么格式 java实体映射_字段

对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。

第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。

第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的

1.MapStruct是用来做什么的?

MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类。

您要做的就是定义一个映射器接口,该接口声明任何必需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射,即没有反射或类似内容。

与手动编写映射代码相比,MapStruct通过生成繁琐且易于出错的代码来节省时间。遵循配置方法上的约定,MapStruct使用合理的默认值,但在配置或实现特殊行为时不加理会。
与动态映射框架相比,MapStruct具有以下优点:

  1. 通过使用普通方法调用(settter/getter)而不是反射来快速执行
  2. 编译时类型安全性:只能映射相互映射的对象和属性,不能将order实体意外映射到customer DTO等。
  3. 如果有如下问题,编译时会抛出异常
  • 3.1 映射不完整(并非所有目标属性都被映射)
  • 3.2 映射不正确(找不到正确的映射方法或类型转换)
  1. 可以通过freemarker定制化开发

MapStruct是基于JSR 269的Java注释处理器,因此可以在命令行构建(javac,Ant,Maven等)以及您的IDE中使用。

它包含以下工件:

1. org.mapstruct:mapstruct:包含必需的注释,例如@Mapping
2. org.mapstruct:mapstruct-processor:包含注释处理器,该注释处理器生成映射器实现

1.1 Maven

https://search.maven.org/artifact/org.mapstruct/mapstruct

对于基于Maven的项目,将以下内容添加到您的POM文件中以使用MapStruct:

mapstruct作用再编译期间,运行期间就不需要的,故使用:<scope>provided</scope>

<!--mapStruct依赖 高性能对象映射-->
            <!--mapstruct核心-->
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct</artifactId>
                <version>1.5.3.Final</version>
                <scope>provided</scope>
            </dependency>
            <!--mapstruct编译-->
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.3.Final</version>
                <scope>provided</scope>
            </dependency>

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

  • 当POM中包含Lombok且不包含时
<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.3.Final</version>
</dependency>

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

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

  • 当POM中包含Lombok且包含时
<properties>
    <org.mapstruct.version>1.5.3.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.3.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中下载 mapstruct support 插件,安装重启Idea:

java 数据库 time 映射什么格式 java实体映射_java_02

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

2. 使用场景

基础转换

字段完全一致
  • 待转换的类
@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);

输出:

Target(id=1, age=18, userNick=Nick)

对应的字段名不一致、类型不一致
@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);

输出:

Target(id=1, age=18, nick=Nick)

高级转换

一对多字段互转

在业务代码中,常出现需要将一个类中的一些字段转换为另一个类的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;
}
  • 多个字段转换为一个字段
@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());
    }
}

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

  1. 创建自定义转换方法(本例为convertToExtra()):

方法入参类型为被转换的类(本例为VO),出参为转换好的字段(本例为extra);

为方法加上@Named注解,并自定义该方法在mapStruct中的名字(本例中为convertToExtra)。

  1. 在转换方法上增加Mapping注解,其中:

source字段必须与转换方法入参名字相同(本例中均为vo);

target字段为目标字段(本例中为extra);

qualifiedByName字段为上述自定义的方法名字。

  • 将一个字段转换为多个字段
@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);

输出:

DTO(id=1, extra=18,Nick)
VO(id=1, age=18, userNick=18)

  • 为转换加缓存

在上述的两个方法(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(",");
    }
}
子类字段互转

常用于平铺类和嵌套类之间的转换,例如,前端需要将类中的所有字段打平,就可以参考以下示例代码。
首先来了解一下DTO,DTO简单的理解就是做数据传输对象的,类似于VO,但是VO用于传输到前端。

现在有这么个场景,从数据库查询出来了一个user对象(包含id,用户名,密码,手机号,邮箱,角色这些字段)和一个对应的角色对象role(包含id,角色名,角色描述这些字段),现在在controller需要用到user对象的id,用户名,和角色对象的角色名三个属性。一种方式是直接把两个对象传递到controller层,但是这样会多出很多没用的属性。更通用的方式是需要用到的属性封装成一个类(DTO),通过传输这个类的实例来完成数据传输。

User.java

@AllArgsConstructor
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String phoneNum;
    private String email;
    private Role role;
}

Role.java

@AllArgsConstructor
@Data
public class Role {
    private Long id;
    private String roleName;
    private String description;
}

UserRoleDto.java,这个类就是封装的类

@Data
public class UserRoleDto {
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 用户名
     */
    private String name;
    /**
     * 角色名
     */
    private String roleName;
}

测试类,模拟将user对象转换成UserRoleDto对象

public class MainTest {
    User user = null;

    /**
     * 模拟从数据库中查出user对象
     */
    @Before
    public void before() {
       Role role  = new Role(2L, "administrator", "超级管理员");
       user  = new User(1L, "zhangsan", "12345", "17677778888", "123@qq.com", role);
    }

    /**
     * 模拟把user对象转换成UserRoleDto对象
     */
    @Test
    public void test1() {
        UserRoleDto userRoleDto = new UserRoleDto();
        userRoleDto.setUserId(user.getId());
        userRoleDto.setName(user.getUsername());
        userRoleDto.setRoleName(user.getRole().getRoleName());
        System.out.println(userRoleDto);
    }
}

从上面代码可以看出,通过getter、setter的方式把一个对象属性值复制到另一个对象中去还是很麻烦的,尤其是当属性过多的时候。而MapStruct就是用于解决这种问题的。

2.使用MapStruct解决上述问题

这里我们沿用User.java、Role.java、UserRoleDto.java。

新建一个UserRoleMapper.java,这个来用来定义User.java、Role.java和UserRoleDto.java之间属性对应规则:

UserRoleMapper.java

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserRoleMapper {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    UserRoleDto toUserRoleDto(User user);

}

在测试类中测试:

通过上面的例子可以看出,使用MapStruct方便许多。

3.添加默认方法

添加默认方法是为了这个类(接口)不只是为了做数据转换用的,也可以做一些其他的事。

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserRoleMapper {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    UserRoleDto toUserRoleDto(User user);

    /**
     * 提供默认方法,方法自己定义,这个方法是我随便写的,不是要按照这个格式来的
     * @return
     */
    default UserRoleDto defaultConvert() {
        UserRoleDto userRoleDto = new UserRoleDto();
        userRoleDto.setUserId(0L);
        userRoleDto.setName("None");
        userRoleDto.setRoleName("None");
        return userRoleDto;
    }

}

测试代码:

@Test
public void test3() {
    UserRoleMapper userRoleMapperInstances = UserRoleMapper.INSTANCES;
    UserRoleDto userRoleDto = userRoleMapperInstances.defaultConvert();
    System.out.println(userRoleDto);
}

4. 可以使用abstract class来代替接口

mapper可以用接口来实现,也可以完全由抽象来完全代替

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public abstract class UserRoleMapper {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    public static final UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    public abstract UserRoleDto toUserRoleDto(User user);

    /**
     * 提供默认方法,方法自己定义,这个方法是我随便写的,不是要按照这个格式来的
     * @return
     */
    UserRoleDto defaultConvert() {
        UserRoleDto userRoleDto = new UserRoleDto();
        userRoleDto.setUserId(0L);
        userRoleDto.setName("None");
        userRoleDto.setRoleName("None");
        return userRoleDto;
    }
}

5.可以使用多个参数

可以绑定多个对象的属性值到目标对象中:

package com.mapstruct.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserRoleMapper {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    UserRoleDto toUserRoleDto(User user);

    /**
     * 多个参数中的值绑定 
     * @param user 源1
     * @param role 源2
     * @return 从源1、2中提取出的结果
     */
    @Mappings({
            @Mapping(source = "user.id", target = "userId"), // 把user中的id绑定到目标对象的userId属性中
            @Mapping(source = "user.username", target = "name"), // 把user中的username绑定到目标对象的name属性中
            @Mapping(source = "role.roleName", target = "roleName") // 把role对象的roleName属性值绑定到目标对象的roleName中
    })
    UserRoleDto toUserRoleDto(User user, Role role);
}

对比两个方法~

5.直接使用参数作为属性值

package com.mapstruct.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserRoleMapper {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class);

    /**
     * 直接使用参数作为值
     * @param user
     * @param myRoleName
     * @return
     */
    @Mappings({
            @Mapping(source = "user.id", target = "userId"), // 把user中的id绑定到目标对象的userId属性中
            @Mapping(source = "user.username", target = "name"), // 把user中的username绑定到目标对象的name属性中
            @Mapping(source = "myRoleName", target = "roleName") // 把role对象的roleName属性值绑定到目标对象的roleName中
    })
    UserRoleDto useParameter(User user, String myRoleName);
}

测试类:

public class Test1 {
    Role role = null;
    User user = null;

    @Before
    public void before() {
        role = new Role(2L, "administrator", "超级管理员");
        user = new User(1L, "zhangsan", "12345", "17677778888", "123@qq.com", role);
    }
    @Test
    public void test1() {
        UserRoleMapper instances = UserRoleMapper.INSTANCES;
        UserRoleDto userRoleDto = instances.useParameter(user, "myUserRole");
        System.out.println(userRoleDto);
    }
}

6.更新对象属性

在之前的例子中UserRoleDto useParameter(User user, String myRoleName);都是通过类似上面的方法来生成一个对象。而MapStruct提供了另外一种方式来更新一个对象中的属性。@MappingTarget

public interface UserRoleMapper1 {

    UserRoleMapper1 INSTANCES = Mappers.getMapper(UserRoleMapper1.class);

    @Mappings({
            @Mapping(source = "userId", target = "id"),
            @Mapping(source = "name", target = "username"),
            @Mapping(source = "roleName", target = "role.roleName")
    })
    void updateDto(UserRoleDto userRoleDto, @MappingTarget User user);


    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    void update(User user, @MappingTarget UserRoleDto userRoleDto);
}

通过@MappingTarget来指定目标类是谁(谁的属性需要被更新)。@Mapping还是用来定义属性对应规则。

以此为例说明:

@Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "role.roleName", target = "roleName")
    })
    void update(User user, @MappingTarget UserRoleDto userRoleDto);

@MappingTarget标注的类UserRoleDto 为目标类,user类为源类,调用此方法,会把源类中的属性更新到目标类中。更新规则还是由@Mapping指定。

7.没有getter/setter也能赋值

对于没有getter/setter的属性也能实现赋值操作

public class Customer {

    private Long id;
    private String name;

    //getters and setter omitted for brevity
}

public class CustomerDto {

    public Long id;
    public String customerName;
}

@Mapper
public interface CustomerMapper {

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

    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto customerDto);

    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);
}

@Mapping(source = “customerName”, target = “name”)不是用来指定属性映射的,如果两个对象的属性名相同是可以省略@Mapping的。

MapStruct生成的实现类:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-02-14T15:41:21+0800",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public Customer toCustomer(CustomerDto customerDto) {
        if ( customerDto == null ) {
            return null;
        }

        Customer customer = new Customer();

        customer.setName( customerDto.customerName );
        customer.setId( customerDto.id );

        return customer;
    }

    @Override
    public CustomerDto toCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }

        CustomerDto customerDto = new CustomerDto();

        customerDto.customerName = customer.getName();
        customerDto.id = customer.getId();

        return customerDto;
    }
}

@InheritInverseConfiguration在这里的作用就是实现customerDto.customerName = customer.getName();功能的。如果没有这个注解,toCustomerDto这个方法则不会有customerName 和name两个属性的对应关系的。

8.使用Spring依赖注入

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
    private Long id;
    private String name;
}

@Data
public class CustomerDto {
    private Long id;
    private String customerName;
}

// 这里主要是这个componentModel 属性,它的值就是当前要使用的依赖注入的环境
@Mapper(componentModel = "spring")
public interface CustomerMapper {

    @Mapping(source = "name", target = "customerName")
    CustomerDto toCustomerDto(Customer customer);
}

@Mapper(componentModel = “spring”),表示把当前Mapper类纳入spring容器。可以在其它类中直接注入了:

@SpringBootApplication
@RestController
public class DemoMapstructApplication {

	// 注入Mapper
    @Autowired
    private CustomerMapper mapper;

    public static void main(String[] args) {
        SpringApplication.run(DemoMapstructApplication.class, args);
    }

    @GetMapping("/test")
    public String test() {
        Customer customer = new Customer(1L, "zhangsan");
        CustomerDto customerDto = mapper.toCustomerDto(customer);
        return customerDto.toString();
    }
}

看一下由mapstruct自动生成的类文件,会发现标记了@Component注解。

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-02-14T15:54:17+0800",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
@Component
public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public CustomerDto toCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }

        CustomerDto customerDto = new CustomerDto();

        customerDto.setCustomerName( customer.getName() );
        customerDto.setId( customer.getId() );

        return customerDto;
    }
}

9.自定义类型转换

有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是Boolean类型,而目标对象类型是String类型,这种情况可以通过@Mapper的uses属性来实现:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
    private Long id;
    private String name;
    private Boolean isDisable;
}

@Data
public class CustomerDto {
    private Long id;
    private String customerName;
    private String disable;
}

定义转换规则的类:

public class BooleanStrFormat {
    public String toStr(Boolean isDisable) {
        if (isDisable) {
            return "Y";
        } else {
            return "N";
        }
    }
    public Boolean toBoolean(String str) {
        if (str.equals("Y")) {
            return true;
        } else {
            return false;
        }
    }
}

定义Mapper,@Mapper( uses = { BooleanStrFormat.class}),注意,这里的users属性用于引用之前定义的转换规则的类:

@Mapper( uses = { BooleanStrFormat.class})
public interface CustomerMapper {

    CustomerMapper INSTANCES = Mappers.getMapper(CustomerMapper.class);

    @Mappings({
            @Mapping(source = "name", target = "customerName"),
            @Mapping(source = "isDisable", target = "disable")
    })
    CustomerDto toCustomerDto(Customer customer);
}

这样子,Customer类中的isDisable属性的true就会转变成CustomerDto中的disable属性的yes。

MapStruct自动生成的类中的代码:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-02-14T16:49:18+0800",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

	// 引用 uses 中指定的类
    private final BooleanStrFormat booleanStrFormat = new BooleanStrFormat();

    @Override
    public CustomerDto toCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }

        CustomerDto customerDto = new CustomerDto();
		// 转换方式的使用
        customerDto.setDisable( booleanStrFormat.toStr( customer.getIsDisable() ) );
        customerDto.setCustomerName( customer.getName() );
        customerDto.setId( customer.getId() );

        return customerDto;
    }
}

要注意的是,如果使用了例如像spring这样的环境,Mapper引入uses类实例的方式将是自动注入,那么这个类也应该纳入Spring容器:

CustomerMapper.java指定使用spring

@Mapper(componentModel = "spring", uses = { BooleanStrFormat.class})
public interface CustomerMapper {

    CustomerMapper INSTANCES = Mappers.getMapper(CustomerMapper.class);

    @Mappings({
            @Mapping(source = "name", target = "customerName"),
            @Mapping(source = "isDisable", target = "disable")
    })
    CustomerDto toCustomerDto(Customer customer);
}

转换类要加入Spring容器:

@Component
public class BooleanStrFormat {
    public String toStr(Boolean isDisable) {
        if (isDisable) {
            return "Y";
        } else {
            return "N";
        }
    }
    public Boolean toBoolean(String str) {
        if (str.equals("Y")) {
            return true;
        } else {
            return false;
        }
    }
}

MapStruct自动生成的类:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-02-14T16:55:35+0800",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
@Component
public class CustomerMapperImpl implements CustomerMapper {

	// 使用自动注入的方式引入
    @Autowired
    private BooleanStrFormat booleanStrFormat;

    @Override
    public CustomerDto toCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }

        CustomerDto customerDto = new CustomerDto();

        customerDto.setDisable( booleanStrFormat.toStr( customer.getIsDisable() ) );
        customerDto.setCustomerName( customer.getName() );
        customerDto.setId( customer.getId() );

        return customerDto;
    }
}
  1. 探究引入顺序
    本文在第一章提到,引入MapStruct时,必须要注意Lombok包与MapStruct包的顺序,关于这一点,网上很少有相关文章提及。
  • 问题来源
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

实践发现,在一个空工程中,如果按照上述写法引入MapStruct,其并不能正常工作。

而当修改引入顺序为以下方案时,则MapStruct可以正常使用。

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

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

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

进一步测试发现,只有mapstruct-processor包在lombok包下面时,MapStruct才能够正常使用。

MapStruct基本原理

为了探究上述问题产生的原因,我们首先要理解MapStruct的基本原理。

MapStruct与其他Bean映射库最大的不同就是,其在编译期间生成转换代码,而不是在运行时通过反射生成代码。

为了更直观的理解这一点,可以从target中找到MapStruct自动生成的对应的ConveterImpl类,如下图所示:

java 数据库 time 映射什么格式 java实体映射_java_03

即MapStruct为我们编写的Convert抽象类自动生成了一个实现。

而Lombok也是在编译时自动生成代码,那么问题大概率就出现在这里了。

MapStruct是如何与Lombok共存的?

查阅MapStruct官方文档可以发现这样一段内容:

java 数据库 time 映射什么格式 java实体映射_java_04

其中提到,MapStruct的annotation processor必须在Lombok的annotation processor生成完代码之后,才可以正常运行。

所以,这应该就是在导入dependencies时,必须先导入Lombok包,再导入MapStruct-processor包才可以正常运行的原因了。不过还有个问题没有解决:

Maven到底在哪里规定了annotation processor的载入顺序?难道每次创建工程时,必须记住这些包导入顺序么?

MapStruct官方推荐的导入流程

在进一步查看MapStruct官网时发现,其并没有将MapStruct-processor放在dependencies中,而是放在了annotationProcessorPaths层级下:

https://mapstruct.org/documentation/installation/

...
<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.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

这又是为什么呢?

查阅Maven官方文档,对于有这样一段描述:

If specified, the compiler will detect annotation processors only in those classpath elements. If omitted, the default classpath is used to detect annotation processors. The detection itself depends on the configuration of annotationProcessors.

即如果有层级,则使用这个层级声明注解处理器的顺序执行,如果没有,则按照默认classpath的顺序来使用注解处理器。

地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths
地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths

我们接下来以下命令来获取当前Maven项目中的classpath:

mvn dependency:build-classpath -Dmdep.outputFile=classPath.txt

从导出内容可以看出,classPath中的Jar包顺序就是与dependencies中导入的顺序是相同的。

自此,关于MapStruct导入顺序的所有问题均已经被解决,总结如下:

  1. 在POM中没有annotationProcessorPaths时,Maven使用的classPath作为注解处理器执行的顺序,而classPath的顺序正是dependencies中导入的顺序。
  2. 当MapStruct依赖在Lombok依赖前面时,在执行注解处理器期间, 由于Lombok还未生成get、set代码,因此在MapStruct看来,这些类并没有公开的成员变量,也就无从生成用于转换的方法。
  3. 在使用annotationProcessorPaths后,其强制规定了注解处理器的顺序,dependencies中的顺序就被忽略了,Maven一定会先运行Lombok再运行MapStruct,代码即可正常运行。