DateTime这个类不知道大家熟悉不,但是看到它的全限定名后估计你就知道它是谁了:cn.hutool.core.date.DateTime ,没错,它就存在于我们常用的hutool工具包里。
hutool包里有个处理日期时间的工具类:cn.hutool.core.date.DateUtil ,它提供了丰富的日期处理方法,但是它处理后的日期类型不是java.util.Date ,而是它内部封装的一个继承了java.util.Date的子类 cn.hutool.core.date.DateTime。
本次文章研讨的问题就是:为什么 DateTime对象 作为Dubbo远程调用的参数时,反序列化时不能正常解析出正确的日期?
Dubbo版本:2.7.15
举个例子来说明下
首先定义一个API接口
public interface IUserService {
// 根据注册日期范围筛选用户
List
}
服务端的实现类如下
@DubboService
public class UserServiceImpl implements IUserService {
@Override
public List
return findUserByRegisterTime(startTime, endTime);
}
private List
List
// …… 省略逻辑,从数据库获取用户列表
return users;
}
}
有一点要额外关注,我这里的 getUsersByRegisterTime 方法,参数类型的定义为 java.util.Date,意味着这个方法的提供者只想要日期类型的参数。
客户端的话,通过一个测试类来实现远程调用
@SpringBootTest
public class UserTest {
@DubboReference
IUserService userService;
@Test
public void rpcDateTest() {
// cn.hutool.core.date.DateTime
DateTime startTime = DateUtil.parse("2024-04-03 00:00:00");
DateTime endTime = DateUtil.parse("2024-04-04 23:59:59");
// java.util.Date
// Date startTime = new Date(2024, Calendar.APRIL,3,0,0,0);
// Date endTime = new Date(2024, Calendar.APRIL,4,23,59,59);
List
System.out.println(users);
}
}
启动服务端,启动测试类,看看服务提供者收到的对象
2024-04-03 18:01:44 ,执行这段代码时,我电脑刚好也是这个时间。所以结论就是 DateTime远程传到服务端,经过反序列的步骤后,转成了Date对象,时间却变成了当前时间。
为了找到出现这个异常的原因,我翻看了一遍Dubbo的源码,最终锁定在com.alibaba.com.caucho.hessian.io.Hessian2Input 这个类上,这是Dubbo包里提供的一个类,由于源码跳来跳去的眼晕,所以我写了个main方法来复刻一个事故现场
public static void main(String[] args) throws IOException {
DateTime startTime = DateUtil.parse("2024-04-03 00:00:00");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(bos);
out.writeObject(startTime);
out.close();
byte[] byteArray = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
Hessian2Input in = new Hessian2Input(bis);
Object obj = in.readObject();
System.out.println(obj.getClass());
System.out.println(obj);
}
输出结果
class cn.hutool.core.date.DateTime
2024-04-03 18:10:40
看,完美的复现了事故现场,究其原因,其实是Dubbo包下的Hessian2Input类没有对hutool包下的DateTime类提供合适的序列化支持,导致反序列化时先创建了一个DateTime对象(此时对象的时间为当前时间),但是却没有将时间相关的属性值(fastTime)给赋进去。
Debug详细过程如下
首先看序列化过程
断点打在writeOject方法
跟进去,由于dubbo包的 Hessian 中没有内置DateTime的专属系列化器,所以使用了默认的JavaSerializer(可见DateTime是个编外人员)
然后看writeObject的过程,即序列化的过程,进入到JavaSerializer的writeObject方法,下图就是序列化DateTime的几个步骤
首先 writeDefinition20(out) 方法将DateTime的四个属性值序列化到输出流中
然后 writeObjectBegin 方法将class的类定义写入输出流
最后 writeInstance 将对象的属性值也写入输出流
通过序列化的过程可以发现,Hessian2Output 仅仅将DateTime的四个属性值进行了序列化,并没有把从java.util.Date继承的 fastTime 属性进行序列化。
然后看反序列化的过程
首先将断点打在readObject方法上
跟进去,走到了Hessian2Input的readObject方法
通过readObjectDefinition 得到了要反序列化对象的全限定名与属性名称集合
然后进入Hessian2Input类的readObject方法,拿到上一步得到的类定义,调用 readObjectInstance 方法
readObjectInstance方法中,通过SerializerFactory拿到 JavaDeserializer 反序列化器,对 DateTime对象进行反序列化,注意看,这里反序列化时采用的DateTime构造器为无参构造器DateTime()
DateTime的构造函数如下
JavaDeserializer 的 readObject 方法中,先是通过构造器实例化了一个要反序列化的对象,这时 DateTime对象被创造出来,并且时间默认为当前时间(文章的前半部分是4/3号写的,由于鸽了几天,后半部分是4/9号写的,文章中的当前时间都是我写文章时的当前时间)
然后readObject(in, obj, fieldNames) 方法将DateTime的四个属性值给赋了进去,由于序列化的内容里没有fastTime相关内容,所以fastTime仍然是当前时间。
反序列化至此就结束了。
结论
DateTime在Dubbo中的序列化仅仅序列化了 mutable、firstDayOfWeek、timeZone、minimalDaysInFirstWeek 四个属性值,缺少了对当前时间的序列化内容,导致反序列化后的DateTime对象一直取的是当前时间。
java.util.Date对象的序列化
那么java.util.Date对象是怎么进行序列化与反序列化的呢,要知道Date类中对fastTime的属性定义可是加上了 transient 关键字的,意味着这个属性不会参与到序列化过程中
Dubbo的内置Hessian序列化是支持了Date对象的,序列化时采用 BasicSerializer 序列化器
BasicSerializer#writeObject中对Date对象的序列化支持如下
Date#getTime()方法中返回了fastTime的值
然后BasicSerializer就将这个fastTime的值序列化到了输出流中。
反序列化时,Hessian2Input#readObject 方法中对Date对象的支持如下
parseInt() * 60000L 读到的就是fastTime的值。
OK,到此为止,在Dubbo默认的序列化方式下,为什么java.util.Date对象能够正常被序列化与反序列化,而 cn.hutool.core.date.DateTime 却不能,这两个问题已经剖析明白了,如果你也在使用Dubbo的话,请多多注意这个问题,避免线上踩坑。
精彩内容
发表评论