抛🧱引🧱
延伸上一篇,本节我们讨论下在单个项目设计时,怎么利用领域模型驱动思想,设计单个项目结构.
需求
需求做一个数据查询工具,实现如下功能:
(1). 执行查询sql,展示数据,支持查询数据文件下载
(2). sql语义解析,不规范sql检查,大sql执行预警
(3). 无limit限制sql适配流式读取内容
(4). 对于执行操作人的历史记录,后续预警
(5). 一些不规范sql执行过慢,需要对此类sql执行限制
初步分析
简单的模型分析,核心模型:
- 命令发起方:定时,操作人员写sql
- sql执行器
- 数据输出模型,目前三种:邮件发送、页面展示、文件下载
进一步分析,结构化:
将上一步的模型图进一步抽象,首先转化为对应三个子领域,如下:
三个领域为:
- 执行器领域,
- 数据链接池管理领域
- 数据输出层领域
而sql发起层做为应用层,用于提供对外提供接口.所以下面重点在三个子领域建设上.
首先我们回过头来,看看我们的需求列表,
- (1)、(3) 可以规划执行器模块中
- 而对于(5)中sql执行的限制,其行为和获取数据库链接类似,可以规划到数据库链池领域中
- 而(1)中的展示显然归属于输出层
- 对于(2)、(4)中的需求,介于各个领域外,又都有交集,进而延伸出拦截层,用于做一些扩展逻辑
因此我们在细化结构图:
添加细节
任何项目前期一般都会考虑设计整体框架,但其实在实施中有很多排期,如前期核心功能、二期监控预警、三期辅助功能等等,下面将项目中的一些辅助功能添加:
根据我们刚才的分析,主模块已定,此时,其他小需求,就可以通过拦截器模块来不断扩展.
代码结构定义
分析到此,工程主体结构已比较清楚,下面定义各领域层的接口定义
这样,基本结构已清晰
- executer:负责sql执行,jdbc的逻辑包装
- output:负责数据输出
- interceptor: 拦截层负责业务扩展
- dataSource+slowPool:数据库连接资源管理层
看下定义的包接口:
结构串联
要将定义好的各模块串联起来,可能比简单的业务逐级调用controller->biz->service->dao这种方式麻烦一点.像我们熟悉的很多框架,如spring、mybatis等都常使用动态代理模式进行串联(这种串联可以多看看大神们的源码,慢慢体会).
我这里采用装饰器对executer进行包装的方式,将executer、output和interceptor连接起来,内部通过execute()进行模版定义,实现顶层规范,如下:
public class SqlExecuterWrapper implements SqlExecuter {
/**
* 实际的sql执行类
*/
private SqlExecuter sqlExecuter;
private List<SqlInterceptor> sqlInterceptors;
private DataOutput dataOutput;
public SqlExecuterWrapper(SqlExecuter sqlExecuter, List<SqlInterceptor> sqlInterceptorList, DataOutput dataOutput) {
this.sqlExecuter = sqlExecuter;
this.sqlInterceptors = sqlInterceptorList;
this.dataOutput = dataOutput;
}
@Override
public ResultSet execute() {
try {
//1.前置拦截正序处理
.......
//2.执行实际执行器
ResultSet resultSet = sqlExecuter.execute();
//3. SQL后拦截拦截处理
......
dataOutput.output(resultSet);
//3. output后置拦截倒序处理
......
} finally {
//关闭执行器资源
......
}
return null;
}
.......
对于数据连接池层的控制,封装连接池工厂,并对原有底层连接池进行包装,满足我们的需求,并且对于原有链接池的常用方法我们要进行保留,由于方法较多,不再一一重写,此处采用动态代理方式实现:
- 链接池包装类
public class DataSourcePoolWrapper extends DruidDataSource implements SlowPools {
private DruidDataSource druidDataSource;
public DataSourcePoolWrapper(DruidDataSource druidDataSource) {
this.druidDataSource = druidDataSource;
}
public DruidPooledConnection getConnection(boolean isSlowSql) throws SQLException {
return getConnection(isSlowSql, 0);
}
public DruidPooledConnection getConnection(boolean isSlowSql, long maxWaitMillis) throws SQLException {
if (isSlowSql) {
if (tryOpenSlowQuery(0)) {
DruidPooledConnection connection = druidDataSource.getConnection(maxWaitMillis);
return new DataSourceConnectionWrapper(connection.getConnectionHolder(), connection, this);
} else {
throw new KaelException("慢sql获取资源失败,稍后重试");
}
} else {
DruidPooledConnection connection = druidDataSource.getConnection(maxWaitMillis);
return new DataSourceConnectionWrapper(connection.getConnectionHolder(), connection, null);
}
}
@Override
public boolean tryOpenSlowQuery(long waitTime) {
return false;
}
@Override
public void release() {
}
}
- 动态代理实现类:
public class KaelDataSourceProxy implements MethodInterceptor {
private final DataSourcePoolWrapper druidDataSource;
public KaelDataSourceProxy(DruidDataSource druidDataSource) {
this.druidDataSource = new DataSourcePoolWrapper().setDruidDataSource(druidDataSource);
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
try {
//Object类常用方法调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
//接口默认方法调用
return invokeDefaultMethod(proxy, method, args);
} else if (DataSourcePoolWrapper.class.equals(method.getDeclaringClass())) {
//DataSourcePoolWrappe定义特有属性方法调用
return methodProxy.invoke(druidDataSource, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//默认原有链接池方法调用
return methodProxy.invoke(druidDataSource.getDruidDataSource(), args);
}
.......
}
总结
这样,数据查询工具的主体结构我们已经定义完成了.基本满足目前我们大部分需求.虽然看起来比我们直接写一个个service堆出来的代码麻烦,但对于后期的扩展维护,将会很好处理.
有时间建议大家多看看各类大神们的开源项目框架源码,其中领域思想处理巧妙让人称奇(推荐spring、mybatis、tomcat、dubbo,cloud等等).
延伸下一部分
本节总结了下,单个项目的领域驱动设计分析,下面跳出单个项目,我们来看下在整个分布式系统中微服务定义中的领域驱动设计思想及相关常用知识.