相信很多猿仔都和我一样,学到知识总想着在实际工作中快速用到,这样就算暴露处理错误,也方便我们加深对知识的理解。 不才,年初想着使用策略+模板方法的设计模式去重构我们项目中臃肿的代码,上去就是一通撸。经过自测之后,满意地露出了淫荡的笑容...
然而,之后的工作时间中,总会有一些匪夷所思的数据出现,比如,短信接收者吐槽不是他的数据,为何他收到短信;再者,数据不一致等现象。
仔细检查代码,也没发现我这惊为天人的代码逻辑有何不妥之处,而且自己反复测试还是没有问题复现,故想着莫非用户他们自己误操作后,通过业务删除了数据?又或是运营同事误操作导致数据丢失?一阵阵臆想后,有些心烦意乱。可总不能把这些没凭证的想法作为问题的解决方案啊,所以我开始冷静下来,仔细查看历史日志的输出情况,对比寻找规律。
先谈谈我重构的业务,订单状态变更接口。
要知道,订单状态是有很多的,而且每一个状态都代表的一种业务逻辑,所以,如果我们把订单状态变更的接口写在一个方法中,这个方法长的有些不忍直视。有猿仔会想到每一个订单状态下的逻辑封装成一个方法,然后这样看上去就简洁很多。
这确实是种书写风格,也就是门面/外观的设计模式呗。但是要知道,订单的状态是很多的,对比不同状态做不同处理的话,使用if分支结构去做判断,还是会使代码不雅观。作为高逼格的程序猿,不会允许这种事情出现。
所以我想着使用策略解决多重if判断,使用模板方法抽取订单状态变更中重复的代码。这样组合,可以很好地满足开闭原则(对扩展开发,对修改关闭)。因为就算以后改代码,起码业务层代码不用动,只需要改对应的订单状态策略实现类即可。
思路是对的,实际却干了一件蠢事。我把所有订单策略实现类注入到IOC中,通过枚举列举订单状态对应的实现类,这样当我们请求携带订单状态访问接口时,策略上下文对象就会根据枚举找到对应的实现类的beanId,进而从IOC中拿到这个策略实现类,然后通过模版方法的方式执行业务。
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void statusChange(List<OrdStatusVo> ordStatusVos) throws Exception {
if (CollectionUtils.isEmpty(ordStatusVos)) {
return;
}
// 循环执行订单变更的业务
for (OrdStatusVo ordStatusVo : ordStatusVos) {
ordChangeStatusApiContext.doService(ordStatusVo);
}
}
/**
* 按照状态映射对应的实现,执行其业务逻辑
*
* @param ordStatusVo
* @throws Exception
*/
public void doService(OrdStatusVo ordStatusVo) throws Exception {
String beanId = OrdStatusBeanIdMappingApiEnum.getBeanIdByStatus(ordStatusVo.getStatus());
if (beanId == null) {
throw new Exception(“订单状态异常”);
}
// 获取订单状态变更策略实现类
OrdChangeStatusApiEvent event = springUtils.getBean(beanId, OrdChangeStatusApiEvent.class);
if(event == null){
throw new Exception(“订单状态异常”);
}
// 执行业务方法
event.doService(ordStatusVo);
}
此处,代码还算严谨,而且问题的根源也不在此。那么,我们来看看我的模板类是什么样的吧。
public abstract class OrdChangeStatusApiEvent {
/**
* 订单ID
*/
protected Long ordId;
/**
* 接口数据
*/
protected OrdStatusVo ordStatusVo;
public void doService(OrdStatusVo ordStatusVo) throws Exception {
// 初始化公共参数
initParams(ordStatusVo);
// 修改订单状态
changeStatus();
}
protected void changeStatus() throws Exception {
// 修改订单状态
}
protected void initParams(OrdStatusVo ordStatusVo) throws Exception {
// 初始化公共参数
this.ordStatusVo = ordStatusVo;
this.ordId = ordStatusVo.getOrderId();
}
}
也就是说,我在模版类中定义了一个属性订单ID,然后在策略实现类调用业务方法时,在模版中对其初始化。很多小伙伴可能认为这么做其实没什么问题,但我犯的错误在于,向IOC注入策略对象时,没有做特殊处理。
要知道,Spring默认帮我们创建的对象都是单例的,也就是说,每次从IOC获取的对象都是同一个。那么,我把策略实现类的属性订单ID赋值后,后面的请求过来会做覆盖。这就是为什么我自己测试没有问题的原因。我测试的数据都是单条的,也就是属于前一个请求处理完,后一个请求才会过来把之前的属性订单ID做修改。当有并发时,就会出现前一个请求尚未处理完,其属性订单ID已经发生变化,拿着这个已经变动的数据做处理,就会造成一系列的BUG。
分析至此,冷汗直流。赶快告知老大问题所在,然后在每个实现类上采用原型(scope=prototype)去解决这一问题。虽是一次事故,但也让我很好地理解了这三种设计模式以及SpringIOC在使用过程中需要注意的问题。最大的成就感,在于冷静分析之后的成果。
研发就是不断埋雷扫雷的过程。生活不会总是那么平淡,遇到问题,只要冷静,我们都是侦探!
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁