<导航

Easy-mapper教程——模型转换工具

一、背景

  做Java开发都避免不了和各种Bean打交道,包括POJO、BO、VO、PO、DTO等,而Java的应用非常讲究分层的架构,因此就会存在对象在各个层次之间作为参数或者输出传递的过程,这里转换的工作往往非常繁琐。

  这里举个例子,做过Java的都会深有体会,下面代码的set/get看起来不那么优雅 。

ElementConf ef = new ElementConf();
ef.setTplConfId(tplConfModel.getTplConfIdKey());
ef.setTemplateId(tplConfModel.getTemplateId());
ef.setBlockNo(input.getBlockNo());
ef.setElementNo(input.getElementNo());
ef.setElementName(input.getElementName());
ef.setElementType(input.getElementType());
ef.setValue(input.getValue());
ef.setUseType(input.getUseType());
ef.setUserId(tplConfModel.getUserId());

为此业界有很多开源的解决方案,列出一些常见的如下:

Apache PropertyUtils

Apache BeanUtils

Cglib BeanCopier

Spring BeanUtils

Dozer

这些框架在使用中或多或少都会存在一些问题:

1、扩展性不高,例如自定义的属性转换往往不太方便。

2、属性名相同、类型不匹配或者类型匹配、属性名不同,不能很好的支持。

3、不支持Java8的lambda表达式。

4、一些框架性能不佳,例如Apache的两个和Dozer(BeanCopier使用ASM字节码生成技术,性能会非常好)。

5、对象的clone拷贝往往并不是使用者需要的,一般场景引用拷贝即可满足要求。

那么,为了解决或者优化这些问题,类库easy-mapper就应运而生。

附:

PO(persistant object) 持久对象

  在 o/r 映射的时候出现的概念,如果没有 o/r 映射,没有这个概念存在了。通常对应数据模型 ( 数据库 ), 本身还有部分业务逻辑的处理。可以看成是与数据库中的表相映射的 java 对象。最简单的 PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。

  一般情况下,一个表,对应一个PO。是直接与操作数据库的crud相关。不过也有文章说:PO 中应该不包含任何对数据库的操作。PO 仅仅用于表示数据,没有任何数据操作。不过也不用纠结这点,我接触到的项目没有用到PO设计,一般都是用的Entity来处理。

DO(Domain Object)领域对象

就是从现实世界中抽象出来的有形或无形的业务实体。一般和数据中的表结构对应。

DTO(Data Transfer Object)数据传输对象

数据传输对象。主要用于远程调用等需要大量传输对象的地方。

比如我们一张表有 100 个字段,那么对应的 PO 就有 100 个属性。 但是我们界面上只要显示 10 个字段, 客户端用 WEB service 来获取数据,没有必要把整个 PO 对象传递到客户端,

这时我们就可以用只有这 10 个属性的 DTO 来传递结果到客户端,这样也不会暴露服务端表结构 . 到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为 VO。

VO(view object) 值对象

  又名:表现层对象,即view object。通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。对于页面上要展示的对象,可以封装一个VO对象,将所需数据封装进去。

  DTO 与 VO 概念相似,并且通常情况下字段也基本一致。但 DTO 与 VO 又有一些不同,这个不同主要是设计理念上的,比如 API 服务需要使用的 DTO 就可能与 VO 存在差异。通常遵守 Java Bean 的规范,拥有 getter/setter 方法。

POJO(plain ordinary java object) 简单无规则 java 对象

  纯的传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 Java Bean ,只有属性字段及 setter 和 getter 方法。

BO(business object) 业务对象

  业务对象。封装业务逻辑的 java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。 一个BO对象可以包括多个PO对象。如常见的工作简历例子为例,简历可以理解为一个BO,简历又包括工作经历,学习经历等,这些可以理解为一个个的PO,由多个PO组成BO。

  比如投保人是一个PO,被保险人是一个PO,险种信息是一个PO等等,他们组合起来是第一张保单的BO。

DAO(data access object) 数据访问对象

  是一个 sun 的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作

Entity:实体,和PO的功能类似,和数据表一一对应,一个实体一张表。

Query:数据查询对象

各层接收上层的查询请求。 注意超过2个参数的查询封装,禁止使用Map类来传输。

二、Easy-mapper特点

1. 扩展性强。基于SPI技术,对于各种类型之间的转换提供默认的策略,使用者可自行添加。

2. 性能高。使用Javassist字节码增强技术,在运行时动态生成mapping过程的源代码,并且使用缓存技术,一次生成后续直接使用。默认策略为基于引用拷贝,因此在Java分层的架构中可以避免对象拷贝的代价,当然这有违背于函数式编程的不可变特性,easy-mapper赞同不可变,这里只不过提供了一种选择而已,请开放兼并。

3. 映射灵活。源类型和目标类型属性名可以指定,支持Java8 lambda表达式的转换函数,支持排除属性,支持全局的自定义mapping。

4. 代码可读高。基于Fluent式API,链式风格。惰性求值的方式,可随意注册映射关系,最后再统一做映射。

三、获取Easy-mapper

项目托管在github上,地址点此 https://github.com/neoremind/easy-mapper 。使用Apache2 License开源。

最新发布的Jar包可以在maven中央仓库找到,地址 点此 。

Maven:

<dependency>
    <groupId>com.baidu.unbiz</groupId>
    <artifactId>easy-mapper</artifactId>
    <version>1.0.4</version>
</dependency>

简单实现Demo

POJO如下:

public class Person {
    private String firstName;
    private String lastName;
    private List<String> jobTitles;
    private long salary;
    // getter and setter...
}

DTO(Data Transfer Object)如下:

public class PersonDto {
    private String firstName;
    private String lastName;
    private List<String> jobTitles;
    private long salary;
    // getter and setter...
}

从POJO到DTO的映射如下:

Person p = new Person();
p.setFirstName("NEO");
p.setLastName("jason");
p.setJobTitles(Lists.newArrayList("abc", "dfegg", "iii"));
p.setSalary(1000L);
PersonDto dto = MapperFactory.getCopyByRefMapper()
                .mapClass(Person.class, PersonDto.class)
                .registerAndMap(p, PersonDto.class);
System.out.println(dto);

四、深入实践

1、注册和映射分开

helloworld中使用了registerAndMap(..)方法,其实可以分开使用,register只是让easy-mapper去解析属性并生成代码,一旦生成即缓存,然后随时map。 

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
                .map(p, PersonDto.class);

先注册,拿到mapper,再映射

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
Mapper mapper = MapperFactory.getCopyByRefMapper();
PersonDto dto = mapper.map(p, PersonDto.class);

先注册,拿到mapper直接映射

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
Mapper mapper = MapperFactory.getCopyByRefMapper().map(p, PersonDto.class);

2、指定属性名称

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .field("salary", "salary") .register() .map(p, PersonDto.class);

3、忽略某个属性

从源类型中排查某个属性,不做映射

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .exclude("lastName") .register() .map(p, PersonDto.class);

4、自定义属性转换

使用Transformer接口

PersonDto6 dto = new PersonDto6();
MapperFactory.getCopyByRefMapper().mapClass(Person6.class, PersonDto6.class)
        .field("jobTitles", "jobTitles", new Transformer<List<String>, List<Integer>>() {
            @Override
            public List<Integer> transform(List<String> source) {
                return Lists.newArrayList(1, 2, 3, 4);
            }
        })
        .register()
        .map(p, dto);

Java8的lambda表达式使用方式如下

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .field("firstName", "firstName", (String s) -> s.toLowerCase()) .register() .map(p, PersonDto.class);

Java8的stream方式如下

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .field("jobTitles", "jobTitleLetterCounts", (List<String> s) -> s.stream().map(String::length).toArray(Integer[]::new)) .register() .map(p, PersonDto.class);

如果指定了属性了类型,那么lambda表达式则不用写类型,Java编译器可以推测

MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .field("firstName", "firstName", String.class, String.class, s -> s.toLowerCase()) .register() .map(p, PersonDto.class);

5、自定义额外的全局转换

AtoBMapping接口做源对象到目标对象的转换

PersonDto6 dto = new PersonDto6(); 
MapperFactory.getCopyByRefMapper().mapClass(Person6.class, PersonDto6.class) .customMapping((a, b) -> b.setLastName(a.getLastName().toUpperCase())) .register() .map(p, dto);

6、映射已经新建的对象

registerAndMap和map方法的第二个参数支持Class,同时也支持已经新建好的对象。如果传入Class,则使用反射新建一个对象再赋值,目标对象可以没有默认构造方法,框架会努力寻找一个最合适的构造方法构造。

PersonDto dto = new PersonDto();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).registerAndMap(p, dto);

7、源属性为空是否映射

如果源属性为空,那么默认则不映射到目标属性,可以强制赋空。 

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .mapOnNull(true) .register() .map(p, PersonDto.class);

8、级联映射

如果Person类型中有Address,而PersonDto类型中有Address2,那么需要首先映射下,如下所示

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register(); 
Person p = getPerson();
p.setAddress(new Address("beverly hill", 10086)); 
PersonDto dto = MapperFactory.getCopyByRefMapper() .mapClass(Person.class, PersonDto.class) .register() .map(p, PersonDto.class);

如果没有提前注册,那么会抛出如下异常:

com.baidu.unbiz.easymapper.exception.MappingException: No class map found for (Address, Address2), make sure type or nested type is registered beforehand

9、输出生产的源代码

可指定log的level为debug,则会在console输出生成的源代码。

另外,可在环境变量中指定如下参数,输出源代码或者编译后的class文件到本地文件系统。 

-Dcom.baidu.unbiz.easymapper.enableWriteSourceFile=true -Dcom.baidu.unbiz.easymapper.writeSourceFileAbsolutePath="..." -Dcom.baidu.unbiz.easymapper.enableWriteClassFile=true -Dcom.baidu.unbiz.easymapper.writeClassFileAbsolutePath="..."

五、框架映射规则

默认使用SPI技术加载框架预置的属性处理器。

在META-INF/services/com.baidu.unbiz.easymapper.mapping.MappingHandler文件中,规则优先级由高到低如下:

1、指定了Transformer,则用自定义的transformer。

2、属性类型相同,则直接按引用拷贝赋值;primitive以及wrapper类型,直接使用“=”操作符赋值。

3、如果目标属性类型是String,那么尝试源对象直接调用toString()方法映射。

4、如果源属性是目标属性的子类,则直接引用拷贝。

5、如果是其他情况,则级联的调用mapper.map(..),注意框架未处理dead cycle的情况。

最后,如果5仍然不能完成映射,那么框架会抛出如下异常:

com.baidu.unbiz.easymapper.exception.MappingCodeGenerationException: No appropriate mapping strategy found for FieldMap[jobTitles(List<string>)-->jobTitles(List<integer>)] ... com.baidu.unbiz.easymapper.exception.MappingException: Generating mapping code failed for ClassMap([A]:Person6, [B]:PersonDto6), this should not happen, probably the framework could not handle mapping correctly based on your bean.

六、框架依赖类库

+- org.slf4j:slf4j-api:jar:1.7.7:compile
+- org.slf4j:slf4j-log4j12:jar:1.7.7:compile
|  \- log4j:log4j:jar:1.2.17:compile
+- org.javassist:javassist:jar:3.18.1-GA:compile

七、性能测试报告

以下测试基于Oracal Hotspot JVM,参数如下:

java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)
 
-Xmx512m -Xms512m -XX:MetaspaceSize=256m

首先充分预热,各个框架,各调用一次,然后再进行benchmark。

测试机器配置如下:

CPU: Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz

MEM: 8G

测试代码见链接 BenchmarkTest.java 。

-------------------------------------
| Create object number:   10000      |
-------------------------------------
|     Framework     |    time cost   |
-------------------------------------
|      Pure get/set |      11ms      |
|       Easy mapper |      44ms      |
|  Cglib beancopier |       7ms      |
|         BeanUtils |     248ms      |
|     PropertyUtils |     129ms      |
|  Spring BeanUtils |      95ms      |
|             Dozer |     772ms      |
-------------------------------------
-------------------------------------
| Create object number:  100000      |
-------------------------------------
|     Framework     |    time cost   |
-------------------------------------
|      Pure get/set |      56ms      |
|       Easy mapper |     165ms      |
|  Cglib beancopier |      30ms      |
|         BeanUtils |     921ms      |
|     PropertyUtils |     358ms      |
|  Spring BeanUtils |     152ms      |
|             Dozer |    1224ms      |
-------------------------------------
-------------------------------------
| Create object number: 1000000      |
-------------------------------------
|     Framework     |    time cost   |
-------------------------------------
|      Pure get/set |     189ms      |
|       Easy mapper |     554ms      |
|  Cglib beancopier |      48ms      |
|         BeanUtils |    4210ms      |
|     PropertyUtils |    4386ms      |
|  Spring BeanUtils |     367ms      |
|             Dozer |    6319ms      |
-------------------------------------

八、与高阶函数搭配使用

和 guava 一起使用做集合的转换

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register(); 
List<Person> personList = getPersonList(); 
Collection<PersonDto> personDtoList = Collections2.transform(personList, p -> MapperFactory.getCopyByRefMapper().map(p, PersonDto.class));
System.out.println(personDtoList);

和 functional java 一起使用做集合的转换

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register(); 
List<Person> personList = getPersonList(); 
fj.data.List<PersonDto> personDtoList = fj.data.List.fromIterator(personList.iterator()).map( person -> MapperFactory.getCopyByRefMapper().map(person, PersonDto.class));
personDtoList.forEach(e -> System.out.println(e));

和Java8的stream API的配合做map

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register(); 
List<Person> personList = getPersonList(); 
List<PersonDto> personDtoList = personList.stream().map(p -> MapperFactory.getCopyByRefMapper().map(p, PersonDto.class)).collect(Collectors.toList());

在Scala中使用

object EasyMapperTest {
 
  def main(args: Array[String]) {
    MapperFactory.getCopyByRefMapper.mapClass(classOf[Person], classOf[PersonDto]).register
    val personList = List(
      new Person("neo1", 100), new Person("neo2", 200), new Person("neo3", 300) ) val personDtoList = personList.map(p => MapperFactory.getCopyByRefMapper.map(p, classOf[PersonDto])) personDtoList.foreach(println) } }

九、小结

  首先基于大量的反射技术的Apache的两个工具BeanUtils和PropertyUtils性能均不理想,Dozer的性能则更为不好。

其次,基于ASM字节码增强技术的Cglib库真是经久不衰,性能在各个场景下均表现非常突出,甚至好于纯手写的get/set。

最后,在调用10,000次时,easy-mapper好于Spring的BeanUtils,100,000次时持平,但是达到1,000,000次时,则落后。由于Spring BeanUtils非常的简单,采用了反射技术Method.invoke(..)做赋值处理,一般现代编译器都会对“热点”代码做优化,如R神的 《关于反射调用方法的一个log》 提到的,可以看出超过一定调用次数后,基于profiling信息,JIT同样可以对反射做自适应的代码优化,这里对Method.invoke(..)在调动超过一定次数时会转为代理类来做实现,而不是调用native方法,因此JIT就可以做很多dereflection的事情优化性能,因此Spring的BeanUtils性能也不差。

可以看出相比于老派的框架,easy-mapper性能非常优秀,虽然和Cglib BeanCopier有差距,这也可以看出使用Javassist的source level的API来做字节码操作性能肯定不会优于直接用ASM,但是easy-mapper的特点在于灵活、可扩展性、良好的编程体验方面,因此从这个tradeoff来看,easy-mapper非常适用于生产环境和工业界,而Cglib可用于一些对性能非常考究的框架内使用。

十、bug优化 

使用过程中发现了一个不足之处,网上也没有相关的解决方案,于是自己写了个工具类辅助解决。

先看问题:

上面我们说到easy-mapper是有缓存功能的,在调用register()时就生成了缓存。而缓存的key就是.mapClass()方法传入的两个class。

两个实体类:SysUserEntity 与 SysUserEntity1,属性相同:

@Setter
@Getter
//@Builder
public class SysUserEntity implements Serializable{

    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private Integer sex;
SysUserEntity1 dto = MapperFactory.getCopyByRefMapper()
                .mapClass(SysUserEntity.class, SysUserEntity1.class).exclude("name")
                .registerAndMap(entity, SysUserEntity1.class);
        System.out.println(dto);

执行上面的代码拷贝后,即使你第二次执行没有exclude("name"),结果name也被exclude,这就是缓存带来的问题。因为你相同的mapClass的key已经生成了拷贝规则,它会直接走缓存的规则进行拷贝。

可以使用clear()方法清除缓存,但是这样会把所有的缓存的key全部删除了。 

MapperFactory.getCopyByRefMapper().clear();

所以自己简单写个工具类指定删除需要清楚的key的缓存:

public static void clearKey() throws NoSuchFieldException, IllegalAccessException {
        Field field1 = MapperFactory.getCopyByRefMapper().getClass().getDeclaredField("classMapCache");
        field1.setAccessible(true);
        Memoizer<MapperKey, ClassMap<Object, Object>> classMapCache =
                (Memoizer<MapperKey, ClassMap<Object, Object>>) field1.get(MapperFactory.getCopyByRefMapper());
        Field field2 = MapperFactory.getCopyByRefMapper().getClass().getDeclaredField("mapperCache");
        field2.setAccessible(true);
        Memoizer<MapperKey, AtoBMapping<Object, Object>> mapperCache =
                (Memoizer<MapperKey, AtoBMapping<Object, Object>>) field2.get(MapperFactory.getCopyByRefMapper());
        if (classMapCache != null) {
            //classMapCache.clear();
            classMapCache.remove(new MapperKey(TypeFactory.valueOf(SysUserEntity.class), TypeFactory.valueOf(SysUserEntity1.class)));
        }
        if (mapperCache != null) {
            //mapperCache.clear();
            mapperCache.remove(new MapperKey(TypeFactory.valueOf(SysUserEntity.class), TypeFactory.valueOf(SysUserEntity1.class)));
        }

    }

 

文章整理自:

https://go.ctolib.com/article/wiki/8982

https://blog.csdn.net/hfismyangel/article/details/79709650

posted @ 2019-06-13 16:33  字节悦动  阅读(836)  评论(0编辑  收藏  举报