重构小记

半年前入职新公司以来,一直参与一个维护型的大型JAVA项目。项目是一个交易系统,基于一个非常老的框架搭建的(大概98年的一个框架),框架本身的设计思想不错,没有应用任何开源框架,简单明了。但是公司平时在设计和代码质量把控方面做得不够到位,导致现在代码的质量可以说是高偶合、低类聚的典型,随便改点什么都无法简单评估影响范围,大部分需求变更都不得不把业务和开发人员集中起来,开个大半天的会议讨论。经过无数次的吐槽与沟通,终于说服了领导,来次翻天覆地的重构。


写这篇博客的目的并不是关于重构的,而是在重构完成后遇到的技术问题,在此记录下来与大家分享。


首先,不得不先简单介绍下项目的情况,项目是一个交易系统,技术上很简单,就是一个servlet,对外提供接口。外部调用方封装报文参数以http方式请求,系统内部解析报文,然后交由一个个的step依次处理,最后返回结果。就这么简单,大致流程如下图所示:

                  

调用方的一次请求我们称之为一次交易,每个交易根据报文参数来决定使用哪些Step来完成不同的业务。Parser负责解析外部调用方传入的参数;每个Step相当于一个独立的、可复用的业务模块。比如创建一个用户,Step1负责创建用户的基本信息,Step2负责创建用户的安全信息,其它Step可以负责创建会员的资产信息等或者做一些权限验证、日志记录等,这样的设计初衷是为了复用Step;Wrapper把内部处理封装成结果返回给调用方。整个过程很容易用一段简单代码来表示:

public void process() {
	long start = System.currentTimeMillis();
	Connection conn = null;
	try {
		conn = ds.getConnection();
		
		processStep1(conn);
		processStep2(conn);
		...
		processStepN(conn);
		
		long end = System.currentTimeMillis();
		if (end - start > limit) {
			conn.rollback();
			return;
		}
		
		conn.commit();
		
	} catch (SQLException e) {
		if (null != conn) 
			conn.rollback();
	} finally {
		if (null != conn) conn.close();
	}
}

从代码上看,框架确实很简单,一开始从数据源拿到一个DB Connection,然后把Connection传入一个个的Step(Step内部直接使用这个Connection对象操作数据库)顺序处理,最后判断是否超时来决定是否提交事务。既然这么简单了,为什么还要重构呢?那是因为业务都在这些Step里面,有大量的Step,而且里面的代码...这里省略1000字。代码质量是重构一原因之一,最主要的是目前项目的代码风格是完全面向过程式的,没有一丁点的面向对象的感觉,维护的成本实在太高,所以重构是采用的是Domain-Driven Design这种纯面向对象的设计方式进行的,关于DDD大家可以自行搜索一下相关的文章,网上不少。


因为是一个大型项目,所以不可能一下子完全重构,而是一个模块一个模块、循序渐进地重构,还需要考虑新旧代码的兼容性,所以第一轮重构后的代码大致如下:

try {
	conn = ds.getConnection();
	
	processStep1(conn);
	processStep2(conn);
	...
	//重构后某个Step变成了
	Factory factory = ContextLoader
		.getCurrentWebApplicationContext().getBean("factory", Factory.class);
    DomainObject doObj = factory.create..();
    doObj.doSomething();
    
	processStepN(conn);
	
	long end = System.currentTimeMillis();
	if (end - start > limit) {
		conn.rollback();
		return;
	}
	
	conn.commit();
	
}

上面只是重构了某个Step,但完成整个交易的功能,必须兼容原来的代码。从代码可以看出,这里我们使用了spring(持久层使用了spring jdbc),从Context中取得工厂,通过工厂创建一个领域对象doObj,之后就可以使用领域对象去实现具体的业务了。等全部的Step都重构完成后,就可以完全丢掉现有的框架设计了。


不知道大家有没有看出问题,我们有个超时的判断,超过响应时间是要回滚的。但是使用spring jdbc我们一般只是声明一个DataSource,它自己通过data source去获取connection,这样spring jdbc使用的connection与我们代码中使用的connection肯定不是同一个对象,这样代码中的connection就无法控制doObj对数据库的提交与回滚了,起码超时回滚这个业务就肯定会出问题了。


摆在面前的只有两条路:

  1. 把某个交易中所涉及的全部Step都重构了,这个交易中完全去掉step,不再代码中维护connection,而是把事务的控制交给spring。这个做法可行,而且也是重构的最终目标,但是时间有限,在短时间内重构完一个交易涉及的全部Step,工作量实在不小,而且测试风险也很大。
  2. 想办法解决

各方面的压力,使得我没有选择,只能想办法解决。代码中的connection相当于new出来的一个对象,而spring中的DataSource又是一个singleton对象,这还不是一个Ioc的问题,因为spring jdbc需要的是一个DataSource,而我们只有Connection,所以我们必须实现一个新的DataSource,并且覆盖它的getConnection方法使它返回我们的代码中new出来的Connection对象。DataSource与我们的框架代码之间好像没有任何关系,怎么能才让它返回我们new出来的Connection呢?感谢我的同事老朱,讨论中他提到了ThreadLocal类,是啊,一次交易过程就是一个线程的一次执行过程,我们可以把Connection保存在ThreadLocal对象中去。


关于ThreadLocal类,简单说就是通过ThreadLocal,可以在同一程线内(不同的方法、代码段等任何地方)共享信息。

首先,创建一个类,通过ThreadLocal用来存取Connection对象。

public class ConnThreadLocal {
	private static final ThreadLocal<Connection> local
		= new ThreadLocal<Connection>();
	
	public static void addConn(Connection conn) {
		local.set(conn);
	}
	
	public static Connection getConn() {
		return local.get();
	}
}

接着,修改框架代码,在获得Connection对象后,保存到ThreadLocal中去。

try {
	conn = ds.getConnection();
	ConnThreadLocal.addConn(conn); //把Connection对象保存到ThreadLocal中
	
	processStep1(conn);
	processStep2(conn);
	
	Factory factory = ContextLoader
		.getCurrentWebApplicationContext().getBean("factory", Factory.class);
    DomainObject doObj = factory.create..();
    doObj.doSomething();
    
	processStepN(conn);
	
	long end = System.currentTimeMillis();
	if (end - start > limit) {
		conn.rollback();
		return;
	}
	
	conn.commit();
}

最后,再实现一个新的DataSource,并且让spring jdbc使用这个新的DataSource我们的问题就解决啦!!!

public class CustomDataSource extends AbstractDataSource {

	@Override
	public Connection getConnection() throws SQLException {
		return ConnThreadLocal.getConn();
	}

	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		return this.getConnection();
	}

}

至此,一切就绪,上环境,测试。Duang.....出错啦,Connection is closed....哭哭哭,这个错误出现在doObj.doSomething方法中第二次数据库访问,怎么会这样呢?网上查阅了一翻,原来spring jdbc在每次数据库访问之后,都会调用Connection的close方法把Connection还给连接池,之后可以再通过连接池获取可用的连接。设计上非常合理,但是苦了我哎哭,只能想办法让CustomDataSource返回的链接不能被spring jdbc关闭,为此,需要一个新的Connection类:

public class CustomConnection implements java.sql.Connection {
	
	private java.sql.Connection conn;
	
	public CustomConnection(java.sql.Connection conn) {
		this.conn = conn;
	}
	
	@Override
	public Statement createStatement() throws SQLException {
		return conn.createStatement();
	}
	
	@Override
	public void close() throws SQLException {
		//覆盖close方法,不让spring关闭
	}
	
}

CustomConnection类内部维护一个真正可用的Connection对象,除了close方法,所有其它方法都委托这个对象去做。再修改CustomDataSource,使之返回CustomConnection对象。

public class CustomDataSource extends AbstractDataSource {

	@Override
	public Connection getConnection() throws SQLException {
		return new CustomConnection(ConnThreadLocal.getConn());
	}

	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		return this.getConnection();
	}

}

这样一来,spring jdbc获取的Connection对象就是我们框架代码里面的那个Connection对象了,并且spring jdbc关闭不了,至此,一切问题解决。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
健身国际俱乐部系统是一种专为健身俱乐部设计的管理软件,它通过集成多种功能来提高俱乐部的运营效率和服务质量。这类系统通常包含以下几个核心模块: 1. **会员管理**:系统能够记录会员的基本信息、会籍状态、健身历史和偏好,以及会员卡的使用情况。通过会员管理,俱乐部可以更好地了解会员需求,提供个性化服务,并提高会员满意度和忠诚度。 2. **课程预约**:会员可以通过系统预约健身课程,系统会提供课程时间、教练、地点等详细信息,并允许会员根据个人时间表进行预约。这有助于俱乐部合理安排课程,避免资源浪费。 3. **教练管理**:系统可以管理教练的个人信息、课程安排、会员反馈等,帮助俱乐部评估教练表现,优化教练团队。 4. **财务管理**:包括会员卡销售、课程费用、私教费用等财务活动的记录和管理,确保俱乐部的财务透明度和准确性。 5. **库存管理**:对于俱乐部内的商品销售,如健身装备、营养补充品等,系统能够进行库存管理,包括进货、销售、库存盘点等。 6. **数据分析**:系统能够收集和分析会员活动数据,为俱乐部提供业务洞察,帮助俱乐部制定更有效的营销策略和业务决策。 7. **在线互动**:一些系统还提供在线平台,让会员可以查看课程、预约私教、参与社区讨论等,增强会员之间的互动和俱乐部的社区感。 8. **移动应用**:随着移动设备的普及,一些健身俱乐部系统还提供移动应用,方便会员随时随地管理自己的健身计划。 9. **安全性**:系统会确保所有会员信息的安全,采取适当的数据加密和安全措施,保护会员隐私。 10. **可扩展性**:随着俱乐部业务的扩展,系统应该能够轻松添加新的功能和服务,以适应不断变化的市场需求。 健身国际俱乐部系统的选择和实施,需要考虑俱乐部的具体需求、预算和技术能力,以确保系统能够有效地支持俱乐部的运营和发展。通过这些系统的实施,健身俱乐部能够提供更加专业和高效的服务,吸引和保留更多的会员,从而在竞争激烈的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值