在Java开发中,我们会看到各种各样的对象(实体)类,包括:
**Resp
对象,;**Req
对象,如OrderDetailReq用于查询订单详情信息的请求体可能有很多人会有一个疑问,为啥要搞这么多对象?
事实上,我自己也会有这个顾虑,这不没事找事么。一个对象从前端传输过来,使用的是QO,即查询对象;然后我在业务层来处理、转换这个对象,并用BO来承载加以封装;然后处理逻辑来到领域层,又需要转换为DO;随后,来到数据库交互层,进行CRUD,即增查改删操作,需要转换为PO;如果需要把数据再返回给前端,上述4个对象,很可能还需要反过来再封装一次,从PO到DO,到BO,再到VO。
这些对象基本上没有太大的差别,字段几乎都是一样的。PO一般比VO多一个逻辑删除字段,毕竟前端才不在乎你数据库的删除概念,前端能看到的数据,就是还没进行逻辑删除的数据。
可能还有人会问,能不能不要搞这么多对象,QO、VO、和PO用一个对象不香么?能少写很多类,少写很多类转换方法(即所谓的getter then setter)。
(个人观点)只能说,可行可不行,看公司编码规范。前端用不上的字段,后端也给前端返回,一来会给前端造成困扰(这个字段啥意思,用于展示或渲染什么数据),二来多余的字段参与网络传输,会降低性能(其实影响真的微乎其微)
之所以会产生这么多对象,一般都是因为随着业务逐渐发展,项目越来越庞大,前后端分离,业务分层势在必行,然后每一层都会定义很多POJO。
但是对于新项目,不应该过度设计,应该根据项目发展的具体情况来适当分层重构。
前面提到,业务开发中,可能会碰到各种各样的对象。事实上,哪怕没有这么多对象,几十张表也够烦的,需要定义getter,setter,构造器方法,重写equals和hashcode方法等。
对于JDK 14之前的版本,可考虑使用Lombok提高生产力,参考Java开发工具–Lombok深入实战。
JDK 14版本后,除Lombok,可考虑使用JDK Record类型,功能等价于Lombok。
对于对象之间的属性拷贝及转换,可考虑使用MapStruct,功能很强大,参考Java对象拷贝MapStruct。
对象拷贝(Object Copy),将一个对象的属性拷贝到另一个有着相同类类型的对象中去。主要有浅拷贝与深拷贝。Shallow Copy,可翻译为浅拷贝,浅复制,浅克隆。Deep Copy,可翻译为深拷贝,深复制,深克隆。
另外还有延迟拷贝(Lazy Copy)。
关于浅拷贝:
关于深拷贝:
无论是深拷贝还是浅拷贝,都需要实现Cloneable接口并且重写clone方法。
深拷贝相比于浅拷贝速度较慢并且花销较大。
两者主要区别在于是否支持引用类型的属性拷贝。
java.lang.Object
的clone()方法
clone方法将对象复制一份并返回给调用者。一般而言clone()
方法满足:
x.clone() !=x; // 克隆对象与原对象不是同一个对象
x.clone().getClass() == x.getClass(); // 克隆对象与原对象的类型一样
x.clone().equals(x)
成立Java中对于基本型变量采用的是值传递;而对于对象传递采用的是引用传递,即地址传递,实际上是对对象作浅拷贝。
方法调用(call by),根据参数传递的情况又分为值调用(call by value)和引用调用(call by reference)。传递值的是值调用,传递地址的是引用调用。Java的方法对象参数传递仍然是值调用。
实现深拷贝的方式:
clone()
方法里面重写克隆逻辑,对克隆对象内部的引用变量再进行一次克隆序列化的限制和问题:
两种的组合,实际上很少会使用。当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
延迟拷贝看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且在某些情况下,循环引用会导致一些问题。
如何选择
如果对象的属性全是基本类型的,可以使用浅拷贝,但是如果对象有引用属性,那就要基于具体的需求来选择。如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行。如果对象引用经常改变,就要使用深拷贝。
继承自java.lang.Object类的clone()方法是浅复制,除非加入
上面提到深拷贝,需要拷贝所有依赖的引用对象。而对象引用关系往往非常复杂,形成引用链(或叫对象图)
使用org.apache.commons.beanutils.BeanUtils
进行对象深入复制时,主要通过向BeanUtils框架注入新的类型转换器,BeanUtils对复杂对象的复制默认是引用。
org.apache.commons.beanutils.PropertyUtils.copyProperties()
方法几乎与BeanUtils.copyProperties()
相同,主要区别在于后者提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换,PropertyUtils不支持这个功能,所以说BeanUtils使用更普遍一点,犯错的风险更低一点。而且它仍然属于浅拷贝。
Apache提供SerializationUtils.clone(T)
,T对象需要实现Serializable接口,属于深克隆。
Spring中的BeanUtils,对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。
成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在的,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同。
Apache提供的BeanUtils和Spring的BeanUtils中拷贝方法的原理都是先用JDK中java.beans.Introspector
类的getBeanInfo()
方法获取对象的属性信息及属性get/set方法,接着使用反射(Method. invoke(Objectobj, Object...args)
方法)进行赋值。Apache支持名称相同但类型不同的属性的转换,Spring支持忽略某些属性不进行映射,都设置缓存保存已解析过的BeanInfo信息。
CGLib的BeanCopier原理:不是利用反射对属性进行赋值,而是直接使用ASM的MethodVisitor直接编写各属性的get/set方法(具体过程可见BeanCopier类的generateClass(ClassVisitor)方法)生成class文件,然后进行执行。由于是直接生成字节码执行,所以BeanCopier的性能较采用反射的BeanUtils有较大提高。
Dozer基于反射来实现对象深拷贝,反射调用set/get或直接对成员变量赋值。该方式通过invoke执行赋值,实现时一般会采用beanutil,Javassist等开源库。
Dozer支持简单属性映射、复杂类型映射、双向映射、隐式映射以及递归映射,支持定制化的属性字段映射,可使用xml或注解进行映射的配置,支持自动类型转换。
深拷贝,不用担心原始类和克隆类指向同一个对象的问题。Orika底层采用javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,因此在速度上比使用反射进行赋值会快很多。
对象拷贝可使用序列化来实现,真实业务开发中,有很大一部分时间是和前端打交道,而不仅仅是提供一个微服务应用(或SOA服务),提供给其他的微服务(SOA)调用(API Call,or Service Call)。现在前后端几乎都是使用JSON来传输数据,因此后端经常需要将JSON Object(POJO对象)转换成JSON String或从JSON String反序列化得到JSON Object。
此时可以使用的工具类就不要太多:FastJson,Jackson,Gson等
很多公司都有自研工具的习惯(传统),自研有不少好处,如稳定性和性能。因为会根据公司或团队的具体项目的业务需求,不用考虑一套大而全的脚手架,只实现简单的转换模板。
TODO
工具这么多,怎么选?
主要考虑两点:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。