我是蝉沐风,一个让你沉迷于技术的讲述者,公众号「蝉沐风」,欢迎大家关注交流
时间都去哪儿了
「跑码场」在陀螺的经营下,猫粮生意一直很红火。
这一天,陀螺找到程序喵招财,说道:“年关将至,最近订单有点多,我查看了一下系统监控,发现RT有点长,你排查一下原因,别影响顾客下单。”
“RT是个啥?”招财挠了挠头问道。
“RT就是系统响应时间啊,在你进行系统升级之后,系统响应时间比原来变长了。”
“......直接说系统变卡了不就得了,还说得这么花里胡哨”,招财小声嘀咕,却也不敢直接回怼自己的师傅。
陀螺看着招财,“你这家伙在嘀咕什么呢?”。
“啊,没有没有,”招财连忙解释道,“我在想,时间都去哪儿了呢?前段时间系统做了下升级,对接了一种新的支付方式,问题很有可能出在第三方的支付接口上。”
public interface Payable {
/**
* 支付接口
*/
void pay();
}
/**
* @author 蝉沐风
* @description 「四十大盗」金融公司
* @date 2022/1/5
*/
public class SiShiDaDao implements Payable {
@Override
public void pay() {
try {
// ...
System.out.println("「四十大盗」支付接口调用中......");
//模拟方法调用延时
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 6000));
// ...
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
“你这锅倒是甩得挺快,那你说说怎么确定这个支付接口的执行时间呢?”陀螺问道。
“这还不简单,我在调用pay()
方法的位置前后各自添加一个记录时间戳的语句就行了,就像这样。”
public class Client {
public static void main(String[] args) {
Payable payable = new SiShiDaDao();
System.out.println("方法计时开始");
long startTime = System.currentTimeMillis();
payable.pay();
long endTime = System.currentTimeMillis();
System.out.println("方法运行时长为:" + (endTime - startTime) + "毫秒");
}
}
"师傅,你看呐!这个方法居然执行了3秒多,问题果然出在了这个接口身上!"招财脸上挂着抑制不住的兴奋。
“你的直觉和运气都很好,这么快就被你定位到了问题。”陀螺的表情未见波动,“先不着急修改,对于单纯的排查测试而言,你的代码并没有问题。但是如果现在恰好有一个在pay()
方法执行前后添加记录时间戳的业务需求,你会怎么实现?”
“我并不觉得上面写的测试代码用在实际业务场景下会有什么问题。”招财自信地说道。
“如果还需要你在上述需求的逻辑前后再添加日志记录呢?”陀螺追问。
“那就继续在前后添加日志逻辑呗。”
“如果还需要你继续添加支付权限审核逻辑以及积分变动逻辑呢?”陀螺继续追问。
“这......虽然可以继续在调用的位置前后追加各种逻辑,但是如此一来方法未免太臃肿了,但是真的会有这么变态的需求吗?”招财已然没有了之前的兴奋,眼神里透着迷惘。
“需求这种东西,唯一不变的就是变化本身。”
静态代理的诞生
“那有没有什么好的解决办法呢?”招财问道。
“我知道你现在住的房子是通过房产中介找到的,其实房产中介就是一个解决这个问题的思路。中介在你和真正的房屋出租者之间充当了媒介,中介对你提供的租房服务并不是中介本身有房子出租(排除中介自己买房子出租的情况),本质上是利用了房屋出租者提供的房屋出租功能,只不过中介加入了更强的宣传推介。”陀螺解释道。
“我还是没懂,房产中介和这个第三方接口有什么联系?”
陀螺继续解释道:“中介就是一个代理,代理本身使用了被代理对象提供的功能,但是又在功能的基础上做了增强。再以支付接口为例,金融公司提供的支付接口就是被代理对象(相当于真正的房屋出租者),处于某种考虑(保护被代理对象,或者被代理对象已经逻辑完备,例如无法要求房屋出租者有中介那么强有力的推销渠道),我们不会要求这个接口给我们提供更多的逻辑功能(因为是第三方jar包,我们无法修改源码),我们需要创造一个类似于房产中介的一个支付代理对象,在实现支付功能的基础上加上我们需要的业务逻辑。”
“我明白了,这是一种设计模式吗?”
“是的,这就是静态代理模式。以记录运行时间为例,尝试着实现一下静态代理模式吧。”陀螺鼓励招财。
臃肿的继承
招财思考了一会儿,写出了如下代码
/**
* @author 蝉沐风
* @description 「四十大盗」金融公司计时功能的代理
* @date 2022/1/5
*/
public class SiShiDaDaoTimeProxy extends SiShiDaDao {
@Override
public void pay() {
System.out.println("方法计时开始");
long startTime = System.currentTimeMillis();
super.pay();
long endTime = System.currentTimeMillis();
System.out.println("方法运行时长为:" + (endTime - startTime) + "毫秒");
}
}
“我创建了一个继承自SiShiDaDao
的代理对象SiShiDaDaoTimeProxy
,并重写了pay()
方法,在调用父类pay()
方法的基础上,前后添加了计时的逻辑,这就是你说的功能增强吧。如此一来,客户端调用支付接口的时候表面上使用的是我写的代理对象,但是本质上用的还是金融公司的接口。”
说罢,招财又写出了客户端调用的代码。
/**
* @author chanmufeng
* @description 调用客户端
* @date 2022/1/5
*/
public class Client {
public static void main(String[] args) {
SiShiDaDaoTimeProxy proxy = new SiShiDaDaoTimeProxy();
proxy.pay();
}
}
陀螺欣慰地点点头,"很好,你已经理解了静态代理的本质,如果我现在要你在开始计时之前打印一条日志,在计时结束之后再打印一条日志,对你来说也不是什么难事儿了。"
“简单!看我的!”招财很快便写出了代码。
/**
* @author 蝉沐风
* @description 「四十大盗」金融公司日志计时代理
* @date 2022/1/5
*/
public class SiShiDaDaoLogTimeProxy extends SiShiDaDaoTimeProxy {
@Override
public void pay() {
System.out.println("打印日志1");
super.pay();
System.out.println("打印日志2");
}
}
客户端代码和运行结果如下
public class Client {
public static void main(String[] args) {
SiShiDaDaoLogTimeProxy proxy = new SiShiDaDaoLogTimeProxy();
proxy.pay();
}
}
陀螺盯着招财,极力憋住笑声,继续问道,“我现在后悔了,想先计时,然后再打印日志,你该怎么办?”
招财慌了,好家伙,刚教育我的唯一不变的就是变化这个真理这么快就让我付诸实践了。
招财想,需求虽然只是变化了一下逻辑顺序,但是对于我实现而言简直就是翻天覆地的变化,为了应对需求,我必须先创建一个继承自SiShiDaDao
的代理对象SiShiDaDaoLogProxy
,然后再创建一个SiShiDaDaoTimeLogProxy
继承SiShiDaDaoLogProxy
,这还只是两层逻辑,万一逻辑更多,需要修改的代价就太大了!
招财明白了,这是陀螺故意考验自己。
“师傅,您就别玩儿我了,我意识到了我目前的实现方式不足以灵活地应付您说的需求,可是问题究竟出在哪儿呢?”招财求饶道。
陀螺笑着说:“看来你终于发现问题了,你使用继承实现了静态代理,可以达到目的,但是不够灵活,看一下使用继承时的UML类图。”
面向接口编程
招财看了一下,果然发现了问题,使用继承得到的UML是一条笔直的逻辑链,毫无复用性可言,无法通过组合的方式来满足不同的逻辑调用顺序。
哎?组合?招财想到了什么,“我好像知道如何走出这个困境了,看我代码”。
/**
* @author 蝉沐风
* @description 「四十大盗」金融公司计时代理
* @date 2022/1/6
*/
public class SiShiDaDaoTimeProxy implements Payable {
//被代理对象
private Payable payable;
public SiShiDaDaoTimeProxy(Payable payable) {
this.payable = payable;
}
@Override
public void pay() {
System.out.println("方法计时开始");
long startTime = System.currentTimeMillis();
payable.pay();
long endTime = System.currentTimeMillis();
System.out.println("方法运行时长为:" + (endTime - startTime) + "毫秒");
}
}
/**
* @author 蝉沐风
* @description 「四十大盗」金融公司日志代理
* @date 2022/1/6
*/
public class SiShiDaDaoLogProxy implements Payable {
//被代理对象
private Payable payable;
public SiShiDaDaoLogProxy(Payable payable) {
this.payable = payable;
}
@Override
public void pay() {
System.out.println("打印日志1");
payable.pay();
System.out.println("打印日志2");
}
}
"我使用组合的方式来代替继承,计时代理和日志代理都实现了Payable
接口,在创建代理的同时需要传入被代理对象,然后在代理中调用传入的被代理对象的方法,在方法前后就可以做一些增强的操作了。"招财解释道。
“接着说说,用这种实现方式是怎么解决我刚才的问题的?”陀螺继续问道。
招财不慌不忙,“如果现在的需求是先打印日志,再计算时间,客户端只需要这么调用。”
public class Client {
public static void main(String[] args) {
//先打印日志,再计算时间
Payable proxy = new SiShiDaDaoLogProxy(new SiShiDaDaoTimeProxy(new SiShiDaDao()));
proxy.pay();
}
}
"同样,如果想先计算时间,再打印日志,只需要修改一下代理生成的顺序就可以了,至于代理的内部实现一点也不需要变动。因为每个代理本身实现了Payable
,因此又可以作为被代理对象传入,继续被其他对象所代理。"
public class Client {
public static void main(String[] args) {
//先计算时间,再打印日志
Payable proxy = new SiShiDaDaoTimeProxy(new SiShiDaDaoLogProxy(new SiShiDaDao()));
proxy.pay();
}
}
“有点俄罗斯套娃的意思了”,陀螺听着招财这么解释,笑着说道。
“这个说法还真是形象,根据需求调整“套”的顺序就可以了,UML图我也给出来了。”
“非常好,目前为止你已经解决了时间都去哪儿了的问题了。如果现在我让你在订单系统的所有方法前后都添加计时功能和日志功能怎么办?而且我可能还想仅针对对某些类中的某些方法执行前后添加计时功能和日志功能,这该怎么办?我还想......”。
招财赶紧打断了陀螺,“......师傅,赶紧打住!您一旦这么问,就说明我目前的实现指定是满足不了您的需求了,等我先回去想想吧,目前的当务之急是赶紧跟金融公司提个pr,修复一下这个问题,别影响顾客下单。”
“我看你是怕影响你的年终奖”,陀螺嗔怪道。
“哈哈哈哈哈哈,不说了不说了,我pr去了”,招财赶紧一溜烟跑没了影。
很快,金融公司修复了这个问题,订单系统稳定如初。