最近在开发一个系统,里面经常在一个请求里处理一系列的任务,其中一些任务是不需要阻塞请求结束的应答的,因此最简单的办法就是异步去执行这些任务。然而如果程序关闭导致任务被丢失,那是最不可接受的情况,因此这种情况下要把任务提交到 MQ 里,利用 MQ 的失败重试来保证非程序性任务失败(毕竟程序性问题最好解决,而非程序性丢失通常不容易恢复)。

但是一般来说在系统里做这么一个事情是非常复杂而又繁琐的,要初始化 MQ 的生产者和消费者,然后定义传输的 DTO,然后在异步任务处写一个破坏阅读的任务提交,来把任务提交给 MQ。怎么才能简化这个过程呢?

我想出并实现了一个模块来解决这个问题。如果原来的代码是

1
dbmapper.create(po);

那么在我的模块写起来就是

1
dbmapper.task().create(po);

是不是看起来很简单,又一目了然?

其实原理很简单,task()函数一定是得到了一个代理,通过这个代理拦截了 create 函数,将其函数名称、函数所在的接口名称和函数实参,通过 mq 传递给消费者;而消费者注册了相同的 mapper,因此通过反射获得了上述参数,从而在消费者实现了实际的数据库操作。怎么能让不同的 db 接口上都有 task 函数?当然就是让他们实现 task 接口啦,Java8 提供了default 函数,正好可以拿来提供 task 函数的默认实现,每个 dbmapper 直接声明 implements 接口,但是不需要再写 task 函数。在 task 函数里,每个 dbmapper 的代理可以在获取的时候懒创建。

原理是很简单的,写起来的方法也有很多种,由于是工作需要,代码和具体设计并不能提供出来,但是有了这个原理,相信大家都能自己做到。

对于我来说,这个模块编写过程中有一个容易出错的地方,原因是我在代理中获取到的 method 去获取其所在的 interface 的名称时,一开始用了getDeclaringClass 后又对其获取 interfaces,但是其实getDeclaringClass这个函数本来就是返回这个 method 所在的 interface 的,继续获取 interface 只能是把函数本来所在的接口跳过,最终消费者最后拿到的接口名称和函数是不对应的。只要 getDeclaringClass 的 name 就是 interface 的名称了。

此外还有一个地方需要留意,就是实参发给消费者的时候要对每个参数分别序列化后得到一个 String 数组,消费者先按照接口名称和函数名称得到形参的类型,然后才能对 String 数组按顺序反序列化成实参,这点其实无需多言,诸位实践的时候自然会想得到。(直接反序列化成 Object[] 可行吗?我没有试过,大家可以自己试一下,哈哈)