Dubbo RPC调用时DateTime的反序列化问题

发布于:2024-04-16 ⋅ 阅读:(26) ⋅ 点赞:(0)

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<UserInfoDTO> getUsersByRegisterTime(Date StartTime, Date endTime);
}
服务端的实现类如下
@DubboService
public class UserServiceImpl implements IUserService {

    @Override
    public List<UserInfoDTO> getUsersByRegisterTime(Date startTime, Date endTime) {
        return findUserByRegisterTime(startTime, endTime);
    }

    private List<UserInfoDTO> findUserByRegisterTime(Date StartTime, Date endTime) {
        List<UserInfoDTO> users = new ArrayList<>();
        // …… 省略逻辑,从数据库获取用户列表
        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<UserInfoDTO> users = userService.getUsersByRegisterTime(startTime, endTime);
        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的话,请多多注意这个问题,避免线上踩坑。