阅读 517

业务开发必看-实体映射工具推荐

前言

声明:

1、DO(业务实体对象),DTO(数据传输对象)。

在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。

  • 在我们对外暴露的Dubbo接口,一般这样定义接口类
 /**
     * 获取营销信息
     *
     * @param hallId 场馆编号
     * @param modelCode 车型代码
     * @return
     */
    BrandMarketInfoDTO getBrandMarketInfo(String hallId, String modelCode);
复制代码
  • 在和数据持久层映射实现的接口中,一般我们会这么写
/**
 * 获取水牌营销信息
 *
 * @param hallId
 * @param modelCode
 * @return
 */
HallCarManageDO getBoardMarketInfo(@Param("hallId") String hallId, @Param("modelCode") String modelCode);
复制代码

但是我们HallCarManageDO和BrandMarketInfoDTO中的属性和属性类型不是相等的。这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。

常见工具类及实现

以下列举被广大开发工程师常用的Bean属性复制工具

  • Spring.BeanUtils
  • Cglib.BeanCopier
  • MapStruct

以下选取属性赋值的功能来对比每个工具类的不同

BeanUtils

import org.springframework.beans.BeanUtils;

/**
 * @author james mu
 * @date 2019/10/22
 */
public class PojoUtils {

    /**
     *
     * @param source 源对象
     * @param clazz 目标对象
     *
     * @return 复制属性后的对象
     */
    public static <T> T copyProperties(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T target;
        try {
            target = clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("通过反射创建对象失败");
        }
        BeanUtils.copyProperties(source, target);
        return target;
    }

}

复制代码

springframework的BeanUtils也是通过java内省机制获取getter/setter,然后通过反射调用从而实现属性复制。

BeanCopier

依赖引用

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>
复制代码

工具类实现

import net.sf.cglib.beans.BeanCopier;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author james mu
 * @date 2019/10/22
 */
public class BeanCopierUtils {

    /**
     * BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。
     * 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能
     */
    public static Map<String, BeanCopier> beanCopierMap = new ConcurrentHashMap<String, BeanCopier>();

    /**
     * cp 对象赋值
     *
     * @param source 源对象
     * @param target 目标对象
     */
    public static void copyProperties(Object source, Object target) {
        String beanKey = generateKey(source.getClass(), target.getClass());
        BeanCopier copier = null;
        if (!beanCopierMap.containsKey(beanKey)) {
            copier = BeanCopier.create(source.getClass(), target.getClass(), false);
            beanCopierMap.put(beanKey, copier);
        } else {
            copier = beanCopierMap.get(beanKey);
        }
        copier.copy(source, target, null);
    }

    private static String generateKey(Class<?> class1, Class<?> class2) {
        return class1.toString() + class2.toString();
    }
}

复制代码
  1. 使用动态代理,生成字节码类,再通过Java反射成Class,调用其copy方法。

  2. 大家可以看到这里用到了ConcurrentHashMap存取copier,因为BeanCopier.create使用了缓存,该过程也消耗资源,建议全局只初始化一次。

    自定义转换器

  3. 支持自定义转换器。

MapStruct

MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

抓一下重点:

  1. 注解处理器
  2. 可以生成 JavaBean 之间那的映射代码
  3. 类型安全, 高性能, 无依赖性

从字面的理解, 我们可以知道, 该工具可以帮我们实现 JavaBean 之间的转换, 通过注解的方式。

同时, 作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。

入门

依赖引用

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.3.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>1.3.0.Final</version>
            <scope>provided</scope>
        </dependency>
复制代码

案例演示

UserDO

import lombok.Data;
import lombok.ToString;

import java.util.Date;

/**
 * @author james mu
 * @date 2019/10/22 
 */
@Data
@ToString
public class UserDO {
    private String name;
    private String password;
    private Integer age;
    private Date birthday;
    private String sex;
}


复制代码

UserDTO

import lombok.Data;
import lombok.ToString;


/**
 * @author james mu
 * @date 2019/10/22
 */
@Data
@ToString
public class UserDTO {
    private String name;
    private String age;
    private String birthday;
    private String gender;
}
复制代码

UserConvertUtils

import com.sanshengshui.javabeanconvert.DO.UserDO;
import com.sanshengshui.javabeanconvert.DTO.UserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @author james mu
 * @date 2019/10/22 
 */
@Mapper
public interface UserConvertUtils {

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

    /**
     * 类型转换
     *
     * @param userDO UserDO数据持久层类
     * @return 数据传输类
     */
    @Mappings({
               @Mapping(target = "gender", source = "sex")
            })
    UserDTO doToDTO(UserDO userDO);
}


复制代码
  1. DTO与DO中属性名相同时候默认映射,(比如name),属性名相同属性类型不同也会映射,(比如birthday,一个Data,一个String)
  2. DTO与DO中属性名不同的,需要通过@Mapping明确关系来形成映射(如sex对应gender)
  3. 无映射关系属性被忽略(如UserEntity的password)

结果如下:

UserDO(name=snow, password=123, age=20, birthday=Tue Oct 22 17:10:19 CST 2019, sex=男)
+-+-+-+-+-+-+-+-+-+-+-
UserDTO(name=snow, age=20, birthday=19-10-22 下午5:10, gender=男)

复制代码

MapStruct分析

上面中, 我写了3个步骤来实现了从 UserDTOUserDO 的转换。

那么, 作为一个注解处理器, 通过MapStruct 生成的代码具有怎么样的优势呢?

高性能

Java反射原理和反射低的原因:juejin.im/post/5da33b…

这是相对反射来说的, 反射需要去读取字节码的内容, 花销会比较大。 而通过 MapStruct 来生成的代码, 其类似于人手写。 速度上可以得到保证。

前面例子中生成的代码可以在编译后看到。 在 target/generated-sources/annotations 里可以看到。

对应的代码

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-10-22T17:10:17+0800",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Azul Systems, Inc.)"
)
public class UserConvertUtilsImpl implements UserConvertUtils {

    @Override
    public UserDTO doToDTO(UserDO userDO) {
        if ( userDO == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setGender( userDO.getSex() );
        userDTO.setName( userDO.getName() );
        if ( userDO.getAge() != null ) {
            userDTO.setAge( String.valueOf( userDO.getAge() ) );
        }
        if ( userDO.getBirthday() != null ) {
            userDTO.setBirthday( new SimpleDateFormat().format( userDO.getBirthday() ) );
        }

        return userDTO;
    }
}

复制代码

可以看到其生成了一个实现类, 而代码也类似于我们手写, 通俗易懂。

性能比较

测试在两个简单的Bean之间转换的耗时,执行次数分别为10、100、1k、10k、100k,时间单位为ms。

总结

虽然反射效率低,但这个时间是很小很小的。根据不同工具的性能及功能维度,个人建议当对象转换操作较少或者应用对性能要求较高时,尽量不采用工具,而是手写getter/setter;在不考虑性能的情况下,普通的对象转换可以使用Cglib.BeanCopier,复杂的对象转换使用MapStruct。

关注下面的标签,发现更多相似文章
评论