Object mapping method comparison and selection

In the process of project development, it is often necessary to write conversions between models. The most common ones are:

  • Entity to DTO
  • DTO to entity
  • VO turn...

for example:

// 实体:User
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String email;
    private String username;
    private String password;
    private Integer gender;
    private Date birthday;
}

// DTO:UserRegisterReq
@Data
public class UserRegisterReq {
    private String username;
    private String password;
    private String confirmPassword;
    private String email;
}
复制代码

in:

  • UserRegisterReq is the request input parameter of the Controller layer when the user registers
  • User is the User entity

When performing registration, we need to convert UserRegisterReq into User object and store it in the database. At this point, we often write code like the following:

@PostMapping("/users/reg")
public void reg(@RequestBody UserRegisterReq userRegisterReq) {
  // 省略password 与 confirmPassword等值判断
  User user = new User();
  user.setEmail(userRegisterReq.getEmail());
  user.setPassword(userRegisterReq.getPassword());
  user.setUsername(userRegisterReq.getUsername());
  // 保存user...
}
复制代码

The above code is feasible, but if there are too many fields in the class, it will be more troublesome - we wrote a bunch of code, just to realize the conversion of objects.

Method 1, IDEA plug-in quick conversion

IDEA provides the GenerateAllSetter plugin to help us quickly generate the above code.

The demonstration is as follows:

screenshot-20220520-162947.png

Just install the plugin, then press Alt + Enter (Option + Enter on macOS), and the object conversion code is automatically generated.

Method 2, realize object conversion with the help of object mapping framework

Although method 1 is very convenient, if the object has a lot of fields, the code will be very verbose and not concise enough.

In fact, there are many object mapping frameworks in the Java ecosystem that specifically help us realize the conversion between objects. Here I list the options that are relatively commonly used in the industry:

product Dozer Orika MapStruct CGLib BeanCopier Spring BeanUtils Apache BeanUtils
GitHub dozer 1.9K stars orika 1.2K stars mapstruct 5K stars cglib 4.3K stars - commons-beanutils 0.2K stars
工作原理 大量反射,主要基于Field.set(obj, obj)为field赋值 基于javassist生成对象映射字节码,并加载生成的字节码文件 基于JSR269,在在编译期生成对象映射代码 基于ASM的MethodVisitor为field赋值 基于Spring反射工具类 基于反射
性能排名 5 2 1 4 3 6

虽然选项很多,但笔者目前只建议大家使用MapStruct

MapStruct优势:

  • 编译器生成Getter/Setter,无运行期性能损耗,性能强劲
  • 基于JSR269,配置灵活
  • 基于Getter/Setter,和自己手写Getter/Setter没有区别,搜索字段引用等较方便

缺点:

  • 由于配置灵活,所以上手成本比其他组件稍微高一点点

MapStruct上手

配置IDE

参考 mapstruct.org/documentati… ,配置你的IDE

整合

  • 在项目中添加如下内容:
<!-- ref: https://mapstruct.org/documentation/installation/ -->
<properties>
    <org.mapstruct.version>1.4.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>
复制代码
  • 如果你的项目使用了Lombok,或使用了spring-boot-configuration-processor,则使用类似如下的配置:

    <properties>
        <org.mapstruct.version>1.4.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>8</source>
                    <target>8</target>
                    <encoding>UTF-8</encoding>
                    <!-- https://mapstruct.org/documentation/installation/ -->
                    <!-- https://mapstruct.org/documentation/stable/reference/html/#lombok -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.16</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.1.0</version>
                        </path>
                        <path>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                            <version>2.4.1</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    复制代码

使用

  • 定义接口,代码类似如下:

    import com.itmuch.gogolive1.domain.User;
    import com.itmuch.gogolive1.domain.UserRegisterReq;
    import org.mapstruct.Mapper;
    import org.mapstruct.factory.Mappers;
    
    @Mapper
    public interface UserConverter {
        /**
         * 固定写法:Mappers.getMapper(接口名.class);
         */
        UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
    
        User toUser(UserRegisterReq req);
    }
    复制代码
  • 使用:

    @PostMapping("/users/reg")
    public void reg2(@RequestBody UserRegisterReq userRegisterReq) {
      // 省略password 与 confirmPassword等值判断
      User user = UserConverter.INSTANCE.toUser(userRegisterReq);
      // 保存user...
    }
    复制代码

    由代码可知,只需如下代码,即可将UserRegisterReq转换User。

    User user = UserConverter.INSTANCE.toUser(userRegisterReq);
    复制代码

原理

编译代码,并在前面的UserConverter接口上,按快捷键 Command + Option + B (或点击 Navigate - Implementation(s) ) ,查找UserConverter的实现类,可跳转到类似如下代码:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-03-22T00:29:49+0800",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 11.0.9.1 (Azul Systems, Inc.)"
)
public class UserConverterImpl implements UserConverter {

    @Override
    public User toUser(UserRegisterReq req) {
        if ( req == null ) {
            return null;
        }

        UserBuilder user = User.builder();

        user.email( req.getEmail() );
        user.username( req.getUsername() );
        user.password( req.getPassword() );

        return user.build();
    }
}
复制代码

由代码可知,MapStruct在编译期间,生成了UserConverterImpl,并在其中实现了对象之间的转换。

和Spring整合

MapStruct支持与Spring整合,只需按如下步骤操作即可:

  • 编写Mapper接口,并在其中添加 (componentModel = "spring") 属性:

    @Mapper(componentModel = "spring")
    public interface UserSpringConverter {
        User toUser(UserRegisterReq req);
    }
    复制代码
  • 当使用时,只需注入 UserSpringConverter 即可:

    @Autowired
    UserSpringConverter userSpringConverter;
    复制代码

    这是因为,使用 (componentModel = "spring") 后,生成的实现类会自动添加 @Component 注解

拓展

本文只是介绍了较为简单的例子,事实上,MapStruct支持非常灵活的配置,例如:

  • 枚举映射

    @Mapper
    public interface OrderMapper {
    
        OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );
    
        @ValueMappings({
            @ValueMapping(target = "SPECIAL", source = "EXTRA"),
            @ValueMapping(target = "DEFAULT", source = "STANDARD"),
            @ValueMapping(target = "DEFAULT", source = "NORMAL")
        })
        ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
    }
    复制代码
  • 自定义表达式映射:

    @Mapper
    public interface SourceTargetMapper {
        SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    
        @Mapping(target = "timeAndFormat",
             expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
        Target sourceToTarget(Source s);
    }
    复制代码
  • 姿势繁多,有数十种,限于篇幅,无法一一枚举。建议大家移步 mapstruct.org/documentati… 查阅。个人经验来说,当转换的对象之间差异较大时,MapStruct的配置也会变得非常复杂,此时代码可读性、复杂性等等都比较高一些。个人不太喜欢折腾MapStruct,所以当遇到复杂情况时,笔者就直接手动实现对象映射了。毕竟,引入工具是帮助我们提效的,如果MapStruct需要大量配置,那意义就不大了。

更多示例

加入我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

扫码发现职位&投递简历

官网投递

job.toutiao.com/s/FyL7DRg

Guess you like

Origin juejin.im/post/7099734198453272607