作者| fredwang(王文武)
导读
别再为VO到DTO,DTO到PO而苦恼,乐信BeanUtil组件为你排忧!
在日常java项目开发的过程中,经常会遇到需要将某一个对象的内容拷贝到另一个对象的场景。比如VO到DTO,DTO到PO,这些拷贝大部分的字段名字相同的。对开发同学而言,最直接的办法一路set,像下面这样。
Person p = new Person();
Student s = new Student();
p.setId(s.getId());
p.setName(s.getName());
需求
然而这种做法在字段少,偶尔需要用到字段赋值的时候还行,如果遇到字段多又经常需要在不同类之间转换的时候,此类做法就会导致代码快速膨胀;而且如果某天我加个字段,还得在这里也得修改一遍,很容易遗漏掉,代码的可维护性也很差
当前市面上上已经有多种
apache的BeanUtils
Spring的BeanUtils
cglib的Beancopier
梳理各业务团队自有的共享库,也有一些类似的设计。
对于lexin_common而言,首先要做的是,确认大家对BeanUtil有什么需求。经过分析,有以下几点:
类之间的copy
Map和类之间的转换
而对于市面上几种BeanUtil而言,都能满足第一点诉求,对第二点都未能直接满足。其中apache的BeanUtils可以实现map2Bean,但是没有实现bean2Map。
另外对于apache的BeanUtil来说,他已经上了阿里巴巴Java代码规范的黑名单,原因是性能太差。
设计
既然没有满足的,那就自己来干了。除了以上需求外,设计一个BeanUtil有哪些需要注意的呢?
哪些字段需要copy
如何保证完整copy
如何确保所需字段都能copy
如何才能快速完成copy
lexin_common的一些小特性
哪些字段需要copy
对于一个class而言,最基本的字段就是该class本身定义的相关字段了。除此以外,有些class还会继承自某一个父类,那么这个时候父类的public和protect字段也是需要关注的(敲黑板:Java各作用域的可见性)
fields = superClazz.getDeclaredFields();
fields = fields == null? new Field[]{} : fields;
for (Field field : fields) {
int modifier = field.getModifiers();
if(Modifier.isPublic(modifier) || Modifier.isProtected(modifier)){
list.add(field);
}
}
i++;
superClazz = superClazz.getSuperclass();
如何保证完整copy
对于需要拷贝的字段,如何保证我们的值都能copy过去也是我们需要考量的。这里主要需区别是基础类型和引用类型。对于基础类型,我们可以直接调用fied.set就能解决;对于引用类型,还需要多走一些拷贝
//这里做了一些小优化,把String,Number等类型也直接处理了
if(ReflectUtil.isSimpleDataType(field.getType())){
ReflectUtil.setFieldValue(obj, field.getName(), source);
}else{
copyProperties(source, target);
}
如何确保所需字段都能copy
平常工作中,很多同学会使用Lombok这一利器来减少我们的代码。然后在带来代码简洁的同时也对我们的工作带来一些复杂度,对市面上的集中BeanUtil分析测试发现对Lombok支持有如下表现
BeanUtil方法 | 支持 @Data | 支持 @Accessors |
---|---|---|
apache | 否 | 否 |
spring | 是 | 是 |
cglib | 是 | 否 |
apache和cglib都不能支持的原因在于他们在为字段set值时,去找返回为空的set方法(带上@Accessors注解以后,字段的set方法会返回this,不是默认的void),找不到字段的set方法就没法给字段set值
lexin_common由于直接使用了fied的get/set方法,所以Lombok的这些注解都不会影响他们,从原理来说,就算没有这些getter/setter方法,lexin_common也能把所有字段拷贝过去
如何才能快速完成copy
在做拷贝的过程中,原理都是从Java字节码反射获取到相关class,而反射本身是非常耗性能的。在开发BeanUtil的过程中,性能问题被大家抛了出来。主要的解决方案是针对反射环节做缓存。在整个BeanUtil模块,针对了以下几个环节做缓存
针对构造方法的缓存,避免每次生成新对象找对应构造方法
针对字段的缓存,避免每次获取某个类的所有字段需要重新反射
另外由于担心某些场景缓存的class或者字段太多,引发进程OOM,所以所有的缓存都是用WeakHashMap来实现。
增加缓存相关能力以后,lexin_common比起apache和cglib的性能都有几倍的提升。在不加一些特殊feature场景下,和spring的性能差不多。
lexin_common的一些小特性
字段别名
在lexin_common的开发过程中,针对公司一些场景的需求,lexin_common的BeanUtil增加了名字alias的feature,当我们需要拷贝的源class A 的字段A1到目标class B的字段B1的时候,可能会由于某些原因,A1和B1不相同并且无法做变更,这个时候可以使用字段B1打上FieldAliasAnnotation({"A1"})注解,就能把A1拷贝到B1上面去了。另外别名注解在源或目标Bean上都可以生效,别名也可以有多个。欢迎大家体验一下哦。
Date类型格式化
有些源Bean日期类型Date字段A,需要拷贝到目标Bean的字符串字段A上,可以使用日期格式化注解@DateFormatFieldAnnotation("yyyy-MM-dd"),这样日期字段拷贝成目标格式字符串;
当下
BeanUtil已经发布到线上支持了下列三个方法
copyProperties
toBean
toMap
这些方法都是单个对象之间做转换的,也有部分团队有List的对象做转换的需求,不过按照现在命名规则,会写成toMapList,容易造成误解,暂时没有发布出来。研发同学对这里有建议或者诉求的,欢迎提出来哈
各方案对比
方案 | 性能(百万次,ms) | 支持map转换 | 别名 | 日期格式转换 |
---|---|---|---|---|
apache BeanUtil | 3610 | × | × | × |
spring BeanUtils | 390 | × | × | × |
cglib BeanCopier | 202 | × | × | × |
hutool | 569 | √ | √ | × |
lexin_common | 563 | √ | √ | √ |
end
在看点这里