MapStruct的介绍及入门使用

一、痛点

  代码中存在很多Java Bean之间的转换,编写映射转化代码是一个繁琐重复还易出错的工作。使用BeanUtils工具时,对于字段名不一致和嵌套类型不一致时,需要手动编写。并且基于反射,对性能有一定开销。Spring提供的BeanUtils针对apache的BeanUtils做了很多优化,整体性能提升了不少,不过还是使用反射实现,针对复杂场景支持能力不足。

二、MapStruct 机制

MapStruct是编译期动态生成getter/setter,在运行期直接调用框架编译好的class类实现实体映射。因此安全性高,编译通过之后,运行期间就不会报错。其次速度快,运行期间直接调用实现类,不会在运行期间使用发射进行转换。

三、环境搭建

Maven依赖导入:mapstruct依赖会导入MapStruct的核心注解。由于MapStruct在编译时工作,因此需要在<build>标签中添加插件maven-compiler-plugin,并在其配置中添加annotationProcessorPaths,该插件会在构建时生成对应的代码。

<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>
    <dependency>
        <groupid>org.projectlombok</groupid>
        <artifactid>lombok</artifactid>
        <version>${lombok.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>
                <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>

四、使用

单一对象转化

创建映射:如下两个类进行对象之间的转换

public class Student {
    private int id;
    private String name;
    // 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
    private String book;  
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    // 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
    private String letter; 
    // getters and setters or builder
}

两者之间进行映射,需要创建一个StudentMapper接口并使用@Mapper注解,MapStruct就知道这是两个类之间的映射器。

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

    // 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
    @Mapping(source = "student.book", target = "letter")
    StudentDto toDto(Student student);
}

当我们需要将Student属性映射到StudentDto

StudentDto studentDto = StudentMapper.INSTANCE.toDto(student);

当我们构建/编译应用程序时,MapStruct注解处理器插件会识别出StudentMapper接口并生成StudentMapperImpl实体类:如果类型中包含Builder, MapStruct会尝试使用它来构建实例,如果没有MapStruct将通过new关键字进行实例化。

public class StudentMapperImpl implements StudentMapper {
    @Override
    public StudentDto toDto(Student student) {
        if ( student == null ) {
            return null;
        }
        StudentDtoBuilder studentDto = StudentDto.builder();

        studentDto.id(student.getId());
        studentDto.name(student.getName());
        // ....
        return studentDto.build();
    }
}

多个对象转换为一个对象

public class Student {
    private int id;
    private String name;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private int classId;
    private String name;
    // getters and setters or builder
}

public class ClassInfo {
    private int id;
    private int classId;
    private String className;
    // getters and setters or builder
}

StudentMapper接口更新如下:如果两个属性中包含相同的字段时,需要通过sourcetarget指定具体使用哪个类的属性。

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

    @Mapping(source = "student.id", target = "id")    
    StudentDto toDto(Student student, ClassInfo classInfo);
}

子对象映射

多数情况下,POJO中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个Student类中包含ClassInfo类:

public class Student {
    private int id;
    private String name;
    private ClassInfo classInfo;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    private ClassInfoDto classInfoDto;
    // getters and setters or builder
}

public class ClassInfo {
    private int classId;
    private String className;
    // getters and setters or builder
}

public class ClassInfoDto {
    private int classId;
    private String className;
    // getters and setters or builder
}

在修改StudentMapper之前,我们先创建一个ClassInfoMapper转换器:

@Mapper
public interface ClassInfoMapper {
    ClassInfoMapper INSTANCE = Mappers.getMapper(ClassInfoMapper.class);
    ClassInfoDto dto(ClassInfo classInfo);
}

创建完ClassInfoMapper之后,我们再修改StudentMapper:添加uses标识,这样StudentMapper就能够使用ClassInfoMapper映射器

@Mapper(uses = {ClassInfoMapper.class})
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    @Mapping(source="student.classInfo", target="classInfoDto")
    StudentDto toDto(Student student, ClassInfo classInfo);
}

我们看先编译后的代码:新增了一个新的映射方法classInfoDtoToclassInfo。这个方法如果没有显示定义的情况下生成,因为我们将ClassInfoMapper对象添加到了StudenMapper中。

public class StudentMapperImpl implements StudentMapper {
    private final ClassInfoMapper classInfoMapper = Mappers.getMapper( ClassInfoMapper.class );

    @Override
    public StudentDto toDto(Student student) {
        if ( student == null ) {
            return null;
        }
        StudentDtoBuilder studentDto = StudentDto.builder();

        studentDto.id(student.getId());
        studentDto.name(student.getName());
        studentDto.classInfo = (classInfoDtoToclassInfo(student.calssInfo))
        // ....
        return studentDto.build();
    }

    protected ClassInfoDto classInfoDtoToclassInfo(ClassInfo classInfo) {
        if ( classInfo == null ) {
            return null;
        }
        ClassInfoDto classInfoDto = classInfoMapper.toDto(classInfo);
        return classInfoDto;
    }
}

数据类型映射
自动类型转换适用于一下几种情况:
【1】基本类型及其包装类之间的转换:int和Integer,float与Float,long与Long,boolean与Boolean等。
【2】任意基本类型与任意包装类之间。如int和long,byte和Integer等。
【3】所有基本类型及包装类与String之间。如boolean和String,Integer和String等。
【4】枚举和String之间。
【5】Java大数类型java.math.BigInteger, java.math.BigDecimal和Java基本类型(包括其包装类)与String之间。

日期转换:指定格式

public class Student {
    private int id;
    private String name;
    private LocalDate birth;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    pprivate String birth;
    // getters and setters or builder
}

创建映射器

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

    // 也可以指定数字的格式
    // @Mapping(source = "price", target = "price", numberFormat = "$#.00")
    @Mapping(source = "birth", target = "birth", dataFormat = "dd/MM/yyyy")    
    StudentDto toDto(Student student, ClassInfo classInfo);
}

List映射

定义一个新的映射方法

@Mapper
public interface StudentMapper {
    List<StudentDto> map(List<Student> student);
}

自动生成的代码如下:

public class StudentMapperImpl implements StudentMapper {

    @Override
    public List<StudentDto> map(List<Student> student) {
        if ( student == null ) {
            return null;
        }

        List<StudentDto> list = new ArrayList<StudentDto>( student.size() );
        for ( Student student1 : student ) {
            list.add( studentToStudentDto( student1 ) );
        }

        return list;
    }

    protected StudentDto studentToStudentDto(Student student) {
        if ( student == null ) {
            return null;
        }

        StudentDto studentDto = new StudentDto();

        studentDto.setId( student.getId() );
        studentDto.setName( student.getName() );

        return studentDto;
    }
}

SetMap型数据的处理方式与List相似:

@Mapper
public interface StudentMapper {

    Set<StudentDto> setConvert(Set<Student> student);

    Map<String, StudentDto> mapConvert(Map<String, Student> student);
}

添加默认值
@Mapping注解有两个很实用的标志就是常量constant和默认值defaultValue。无论source如何取值,都将始终使用常量值,如果source取值为null,则会使用默认值。修改一下StudentMapper,添加一个constant和一个defaultValue:

@Mapper(componentModel = "spring")
public interface StudentMapper {
    @Mapping(target = "id", constant = "-1")
    @Mapping(source = "student.name", target = "name", defaultValue = "zzx")
    StudentDto toDto(Student student);
}

如果name不可用,我们会替换为zzx字符串,此外,我们将id硬编码为-1

@Component
public class StudentMapperImpl implements StudentMapper {

    @Override
    public StudentDto toDto(Student student) {
        if (student == null) {
            return null;
        }

        StudentDto studentDto = new StudentDto();

        if (student.getName() != null) {
            studentDto.setName(student.getName());
        }
        else {
            studentDto.setName("zzx");
        }
        studentDto.setId(-1);

        return studentDto;
    }
}

添加表达式

MapStruct甚至允许在@Mapping注解中输入Java表达式。你可以设置defaultExpression

@Mapper(componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface StudentMapper {

    @Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
    @Mapping(source = "student.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
    StudentDto toDtoWithExpression(Student student);
}

五、依赖注入

如果你使用的是Spring,只需要修改映射器配置,在Mapper注解中添加componentModel = "spring",告诉MapStruct在生成映射器实现类时,支持通过Spring的依赖注入来创建,就不需要在接口中添加INSTANCE字段了。这次生成的StudentMapperImpl会带有@Component注解,就可以在其它类中通过@Autowire注解来使用它。

@Mapper(componentModel = "spring")
public interface StudentMapper {}

如果你不使用SpringMapStruct也支持Java CDI

@Mapper(componentModel = "cdi")
public interface StudentMapper {}

转载自https://blog.csdn.net/zhengzhaoyang122/article/details/132657876

posted @ 2024-06-01 14:00  liftsail  阅读(229)  评论(0编辑  收藏  举报