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());
为此业界有很多开源的解决方案,列出一些常见的如下:
这些框架在使用中或多或少都会存在一些问题:
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))); } }
文章整理自: