• 推荐一款Java实体映射工具---mapstruct


    背景

    由于代码分层原因,导致代码中会有多种形如XXXVO、XXXDTO、XXXDO的类,并且经常发生各种VO/DTO/DO之后转换。从而产生很多 vo.setXXX(dto.getXXX()) 的代码。当字段多了之后不仅容易出错,而且有些浪费时间。也会有人使用 BeanUtils.copyProperties() 进行转换,这样虽然节省了代码。但是依旧存在一些问题。

    1. 使用反射性能不好
    2. 不同名称直接无法映射。

    本文将介绍一款Java实体对象映射框架---MapStruct。

    介绍

    官方文档:https://mapstruct.org/documentation/dev/reference/html/
    首页:https://mapstruct.org/

    MapStruct是一种基于 Java JSR 269 注释处理器,用于生成类型安全,高性能和无依赖的Bean映射代码。

    1. 通过getter/setter 进行字段拷贝,而不是反射
    2. 字段名称相同直接转换,名称不同使用 @Mapping 注解标识

    与动态映射框架相比,MapStruct的优势:

    1. 使用普通的getter/setter方法而非反射,执行更快
    2. 编译时类型安全
    3. 清晰的错误提示信息

    使用

    maven配置

    ...
    <properties>
        <org.mapstruct.version>1.4.0.Beta3</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>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    ...
    

    可配置项

    选项 描述 默认值
    mapstruct. suppressGeneratorTimestamp 设置成true 时,在生成的代码中不生成创建时间戳 false
    mapstruct.verbose false
    mapstruct.defaultComponentModel 组件模型,取决于如何获取mapper对象。
    支持:default、cdi、spring、jsr30
    可通过注解配置 @Mapper#componentModel()
    default
    mapstruct. suppressGeneratorVersionInfoComment 控制在生成的代码中生成版本信息comments false
    mapstruct.defaultInjectionStrategy 注入类型,仅适用于cdi、spring、jsr30
    支持:field、constructor
    可通过注解配置 @Mapper#injectionStrategy()
    field
    mapstruct.unmappedTargetPolicy 目标属性没有原属性填充时的提示策略
    支持:error、warn、ignore
    可通过注解配置 @Mapper#unmappedTargetPolicy()
    warn

    具体使用

    基本映射

    第一步:定义类,已省略 getter/setter 方法

    public class Student {
        private String stuName;
        private String stuNumber;
        private int gender;
    }
    
    public class StudentVO {
        private String stuName;                 // 姓名
        private String displayStuNumber;        // 展示学号
        private String gender;                  // 男 女
    }
    

    第二步:创建映射器。只需定义Java接口,并使用注解 @Mapper ,代码如下所示

    @Mapper
    public interface MapStruct101 {
       @Mappings({})
       StudentVO toStudentVO(Student student);
    }
    

    代码编译之后,生成MapStruct101的实现类。生成代码如下:

    @Override
    public StudentVO toStudentVO(Student student) {
      if ( student == null ) {
        return null;
      }
    	
      StudentVO studentVO = new StudentVO();
    
      studentVO.setStuName( student.getStuName() );
      studentVO.setGender( String.valueOf( student.getGender() ) );
    	
      return studentVO;
    }
    

    通过上面代码得出以下几个结论:

    1. 同名称的自动转换,如果类型不同也会进行隐式转换。题外音:类型不一致,字段名称一致的情况可能出错,需要注意。
    2. 字段之间的拷贝是通过 getter/setter 方法,而不是通过反射。题外音:类必需有 getter/setter 方法
    3. 名称不同的未进行转换(displayStuNumber未转换)

    字段名称不同的处理

    上面在映射接口我们直接使用了 @Mappings({}),未进行特殊处理,所以只对同名的进行了转换。现在我们增加注解,从而实现名称不同的字段之间的转换。
    接口类代码修改如下:

    @Mapper
    public interface MapStruct101 {
       @Mappings({
    	     @Mapping(source = "stuNumber", target = "displayStuNumber")
    	 })
       StudentVO toStudentVO(Student student);
    }
    

    再次编译之后生成代码如下:

    @Override
    public StudentVO toStudentVO(Student student) {
    		if ( student == null ) {
    				return null;
    		}
    
    		StudentVO studentVO = new StudentVO();
    
    		studentVO.setDisplayStuNumber( student.getStuNumber() );
    		studentVO.setStuName( student.getStuName() );
    		studentVO.setGender( String.valueOf( student.getGender() ) );
    
    		return studentVO;
    }
    

    我们通过使用 @Mapping 注解的 sourcetarget 进行不同名字段的映射。其中 source 代表源字段,target 表示 source 字段映射到的字段。

    字段转换时,需要简单处理

    上面我们发现 Student 类的 genderint 类型(0表示女,1表示男),StudentVOgenderString(男或女)。此时并不是直接的字段转换,而是需要映射。 此时我们再次修改映射接口代码如下:

    @Mapper
    public interface MapStruct101 {
      @Mappings({
    	     @Mapping(source = "stuNumber", target = "displayStuNumber"),
    	     @Mapping(target = "gender", expression = "java(student.getGender() == 1 ? "男" : "女")")
    	 })
       StudentVO toStudentVO(Student student);
    }
    

    编译之后生成代码如下:

    @Override
    public StudentVO toStudentVO(Student student) {
      if ( student == null ) {
        return null;
      }
    
      StudentVO studentVO = new StudentVO();
    
      studentVO.setStuName( student.getStuName() );
    
      studentVO.setGender( student.getGender() == 1 ? "男" : "女" );
      studentVO.setDisplayStuNumber( student.getStuNumber());
    
      return studentVO;
    }
    

    这样gender字段就变成了 男、女了。我们发现可以使用 @Mapping 注解的 expression 进行字段转换时的简单处理。

    字段转换时,需要复杂处理

    开发中有时候字段需要进行复杂逻辑处理,多行代码如果写在expression字段显然不合理。我们可以这样处理,修改映射接口如下:(此处还是以性别映射举例)

    @Mapper
    public interface MapStruct101 {
      @Mappings({
    	     @Mapping(source = "stuNumber", target = "displayStuNumber"),
    	     @Mapping(target = "gender", source = "gender", qualifiedByName = "transferGender")
    	 })
       StudentVO toStudentVO(Student student);
    	 
    	@Named("transferGender")
    	default String transferGender(int gender) {
    			return gender == 1 ? "男" : "女";
    	}
    }
    

    编译之后代码如下所示:

    @Override
    public StudentVO toStudentVO(Student student) {
      if ( student == null ) {
        return null;
      }
    
      StudentVO studentVO = new StudentVO();
    
      studentVO.setStuName( student.getStuName() );
    
      studentVO.setGender( transferGender(student.getGender()));
      studentVO.setDisplayStuNumber( student.getStuNumber());
    
      return studentVO;
    }
    
    default String transferGender(int gender) {
        return gender == 1 ? "男" : "女";
    }
    

    我们可以使用一个defaut方法进行复杂逻辑的处理,并使用@Named注解进行标注,并在 @Mapping 注解中使用 qualifiedByName 表明使用哪个方法进行处理转换。 这样生成代码之后就会调用指定方法进行转换。

    类中包含其他类的列表

    此处可以自己写demo验证看看哦。例如学生类中有List<Project>,则只需写出 ProjectProjecgVO 的映射即可。代码如下:
    类定义如下:

    public class Student {
        private String stuName;
        private String stuNumber;
        private int gender;
        private List<Project> projects;
    }
    
    public class Project {
        private String projectName;
        private double projectScore;
        private String teacherName;
    }
    
    public class StudentVO {
        private String stuName;                 
        private String displayStuNumber;        
        private String gender;                  
        private List<ProjectVO> projectVOList;
    }
    
    public class ProjectVO {
        private String projectName;
        private double projectScore;
        private String teacherName;
    }
    

    映射接口代码:

    @Mapper
    public interface MapStruct101 {
        @Mappings({
    			@Mapping(target = "gender", expression = "java(student.getGender() == 1 ? "男" : "女")")
    			@Mapping(target = "displayStuNumber", source = "stuNumber")
    			@Mapping(target = "projectVOList", source = "projects")
    		})
        StudentVO toStudentVO(Student student);
    }
    

    编译之后生成代码如下:

    @Override
    public StudentVO toStudentVOWithListObject(Student student) {
      if ( student == null ) {
        return null;
      }
    
      StudentVO studentVO = new StudentVO();
    
      studentVO.setProjectVOList( projectListToProjectVOList( student.getProjects() ) );
      studentVO.setStuName( student.getStuName() );
    
      studentVO.setGender( student.getGender() == 1 ? "男" : "女" );
      studentVO.setDisplayStuNumber( student.getStuNumber());
    
      return studentVO;
    }
    

    其实会自动生成包含类的映射关系,很是方便。

    对Builder的支持

    现在我们都是用grpc,生成对象都是通过Builder生成的,并没有直接的 setter 方法,这种情况mapstrcut也是支持的,具体生成代码是,会先生成对应的Builder对象,然后在调用 setter 方法。大家可以自行试一下,此处不再举例说明。

    引用

    映射接口写好了,我们应该如何使用呢?

    普通使用,可以通过如下代码:Mappers.getMapper(MapStruct101.class)

    @Test
    public void test() {
      MapStruct101 mapper = Mappers.getMapper(MapStruct101.class);
      Teacher teacher = Teacher.builder()
        .teacherName("张老师")
        .address("西二旗")
        .mobilePhone("123445")
        .build();
      TeacherVO teacherVO = mapper.toTeacherVO(teacher);
      System.out.println(teacherVO);
    }
    

    spring使用,需要修改组件模型为 spring,可以通过pom.xml的参数修改,也可以通过注解修改。修改之后会把实现类添加 @Component 从而成为一个bean。 此处我们通过修改注解,使用 @Mapper(commentModel = "spring")

    @Mapper(componentModel = "spring")
    public interface MapStruct102 {
        @Mapping(source = "teacherName", target = "name")
        @Mapping(source = "mobilePhone", target = "phone")
        TeacherVO toTeacherVO(Teacher teacher);
    }
    
    // 就可以使用bean注入 
    @Autowired
    private MapStruct102 mapStruct102;
    
    @Test
    public void test() {
      Teacher teacher = Teacher.builder()
        .teacherName("张老师")
        .address("西二旗")
        .mobilePhone("123445")
        .build();
      TeacherVO teacherVO = mapStruct102.toTeacherVO(teacher);
      System.out.println(teacherVO);
    }
    

    与BeanUtils对比

    public class Client3 {
        public static void main(String[] args) {
            MapStruct101 mapper = Mappers.getMapper(MapStruct101.class);
            School school = new School();
            school.setSchoolAge(120);
            school.setAddress("北京");
            school.setManager("校长");
            school.setSchoolName("北大");
            school.setTotalStudentCnt(10000);
            school.setTotalTeacherCnt(1000);
    
            long start = System.currentTimeMillis();
            mapper.toSchoolVO(school);
            long cost = System.currentTimeMillis() - start;
            System.out.println("cost:" + cost);			// 耗时:0
    
            SchoolVO schoolVO = new SchoolVO();
    
            start = System.currentTimeMillis();
            BeanUtils.copyProperties(school, schoolVO);
            cost = System.currentTimeMillis() - start;
            System.out.println("cost:" + cost); 	// 耗时:100+
        }
    }
    
    1. mapstruct编译时生成代码更快
    2. mapstruct可以对名称或类型不同的字段,进行处理
    3. 常见的Bean映射工具性能对比,结论:MapStruct最优
    4. 基本原理:
      4.1 BeanUtils.copyProperties 允许时反射机制
      4.2 mapStruct 编译期间,生成代码。可以参考文章:Java-JSR-269-插入式注解处理器框架原理 和我们使用的lombok原理一样
  • 相关阅读:
    秒转 时间格式 JavaScript seconds to time with format hh:mm:ss
    jQuery ajax表单提交实现局部刷新 ajaxSubmit
    jquery mobile header title左对齐 button右对齐
    Java数据库ResultSet转json实现
    jsp 局部刷新
    ajax提交url 与ajax提交表单的比较
    jquery + json + springMVC集成在controller中实现Ajax功能
    js获取url中指定参数值
    jquery ajax 局部刷新
    jquery ajax jsonp callback java 解决方案2
  • 原文地址:https://www.cnblogs.com/lvmengtian/p/14594185.html
Copyright © 2020-2023  润新知