工作日记-文件柜驱动层开发总结
前言
上周一直在陆陆续续进行着文件柜驱动的开发,直到上周四终于开发完成,目前等待硬件部门提供硬件进行联调测试以及和上层业务系统进行联调测试,整体的开发不算特别顺利,经历了很多反复设计和思考的过程,这篇博客主要想总结下本次设计开发的得失,明得失才能有进步。
设计文档
用例图
用例图可以清楚的表达系统中的参与者和系统提供的功能概览。
文件柜驱动提供的功能比较简单,就是为上位机系统提供盘点文件柜的功能,所以用例图也很简洁明了。
用例文档
【用例名称】
盘点文件柜
【用例描述】
- 上位机系统告知驱动层哪个文件柜需要盘点
- 驱动层对需要统计的文件柜发送开启统计指令,驱动层最多等待LoadProperties.REQUEST_TIMEOUT毫秒
- 文件柜回复
- 如果回复成功,跳转到4步骤
- 如果回复失败,驱动层构造“保密柜开启盘点,返回响应失败”响应包给上位机系统,业务结束
- 如果超时未返回响应,驱动层构造“保密柜开启盘点超时,未返回响应”响应包给上位机系统,业务结束
- 文件柜内部启动盘点并等待N分钟进行内部处理,然后通过网络回传所有数据给驱动层
- 驱动最多等待5分钟,检查收到的数据,并构造响应给上位机系统
- 如果数据完整,把所有数据返回给上层业务,并构造“保密柜盘点成功”响应包给上位机系统,业务结束
- 如果数据不完整,驱动层构造 “保密柜盘点结果,盘点失败,失败原因,超时未返回协议指定条数的标签结果数据”响应包给上位机系统,业务结束
关键设计
异步模式
因为上层业务系统对文件柜的业务操作过程耗时较长,并且在业务层面上来看属于一个异步操作,所以在驱动层面上,就把和上层业务系统的交互接口设计为了异步模式。
笔者虽然在Netty框架中频繁的使用异步,但是还没有自己实际开发过异步框架,所以异步的设计对笔者来说是一个难点。但是在经过一番思考后,也成功的设计出了一个简单的异步框架。
最终设计UML如下:
上层业务系统使用CabinetService接口和驱动层交互,使用时
- 需要实现一个接口,间接传递了一个回调方法,也就是CabinetListener中的operationComplete方法,并且上层业务系统可以通过回调方法中的方法参数CabinetFuture来获取本次业务的结果
- 需要传递本地操作的基础数据参数params,比如文件柜的ip端口等等
日志设计
关于日志部分的设计,因为笔者之前读过 MyBatis技术内幕,书中介绍过MyBatis框架中的日志部分的实现原理,所以笔者在设计自己的框架时,也参考了MyBatis中的方法来进行设计。
简单来说,笔者的框架需要自己提供一套日志接口,框架内部源码如果需要打印日志,都通过自己的日志接口来进行操作。上层业务系统在使用本框架时,框架的日志工厂会自动尝试加载当前项目classpath下的日志框架类,如果成功,底层就使用该具体的日志框架。
为求简单,笔者只实现了Log4j2和不打印日志两种底层实现,后续可以根据实际使用需求再添加其他的日志框架实现即可。
核心代码如下:
/**
* 框架的统一日志接口
*/
public interface Log {
void error(String s, Throwable t);
void error(String s, Object... objects);
void debug(String s, Object... objects);
void info(String s, Object... objects);
void warn(String s, Object... objects);
}
package com.icomp.logging;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
Log4j2的实现,底层封装了Log4j2的Logger
*/
public class Log4j2Impl implements Log {
private Logger logger;
public Log4j2Impl(Class<?> aclass) {
this.logger = LogManager.getLogger(aclass);
}
@Override
public void error(String s, Throwable t) {
logger.error(s, t);
}
@Override
public void error(String s, Object... objects) {
logger.error(s, objects);
}
@Override
public void debug(String s, Object... objects) {
logger.debug(s, objects);
}
@Override
public void info(String s, Object... objects) {
logger.info(s, objects);
}
@Override
public void warn(String s, Object... objects) {
logger.warn(s, objects);
}
}
package com.icomp.logging;
import java.lang.reflect.Constructor;
/**
使用时,只需要这样写即可自动加载当前项目选择的日志框架
private static Log logger = LogFactory.getLog(ConnectServiceImpl.class);
*/
public class LogFactory {
private static Constructor<? extends Log> logContructor;
static {
//尝试加载log4j2
tryLoad(Log4j2Impl.class);
//尝试加载nologging(适用于调用者项目无日志框架)
tryLoad(NoLoggingImpl.class);
}
public static Log getLog(Class<?> aclass) {
try {
return logContructor.newInstance(aclass);
} catch (Throwable t) {
throw new RuntimeException("error create Logger" + aclass.getName() + ".Cause" + t, t);
}
}
private static void tryLoad(Class<? extends Log> logClass) {
if (logContructor == null) {
try {
Constructor<? extends Log> constructor = logClass.getDeclaredConstructor(Class.class);
Log log = constructor.newInstance(LogFactory.class);
log.info("LogFactory init success");
logContructor = constructor;
} catch (Throwable t) {
//如果出现异常,说明classpath中不存在该日志框架jar包,继续往下走
}
}
}
}
参数读取
我们平时开发使用Springboot框架时,可以方便的利用注解,把需要的参数配置到application.properties并使用注解注入到实例对象中。开发自己的框架时,我们也会涉及到参数暴露的需求。
一般来说,如果是不经常变化的底层参数,可以考虑放到配置文件或者数据库中,在项目启动时读取一次即可;如果是需要经常变化的参数,建议放到方法或者服务的参数中,每次调用服务时,由调用方传递进来。考虑到本次驱动的参数主要是不经常变化的参数,所以把参数提取到配置文件中,然后在项目启动后进行读取是一个方便的选择。
这里笔者选择了自定义一个配置文件,并使用Java的IO进行读取。核心代码如下:
connect.timeout=1000
idle.timeout=120
request.timeout=1000
thread.pool.size=5
package com.icomp;
import java.io.InputStream;
import java.util.Properties;
public class LoadProperties {
public static Integer CONNECT_TIMEOUT = 1000;
public static Integer IDLE_TIMEOUT = 120;
public static Integer REQUEST_TIMEOUT = 1000;
public static Integer THREAD_POOL_SIZE = 10;
/**
* 等待结果的遍历次数,总时间为REQUEST_TIMEOUT * RESULT_COUNT
*/
public static Integer RESULT_COUNT = 300;
static {
//如果配置文件不存在,直接使用默认设置,否则读取配置文件的信息,并设置到静态变量中
Properties properties = new Properties();
InputStream inputStream = Object.class.getResourceAsStream("/cabinet-api-config.properties");
try {
properties.load(inputStream);
CONNECT_TIMEOUT = Integer.parseInt((String) properties.get("connect.timeout"));
IDLE_TIMEOUT = Integer.parseInt((String) properties.get("idle.timeout"));
REQUEST_TIMEOUT = Integer.parseInt((String) properties.get("request.timeout"));
THREAD_POOL_SIZE = Integer.parseInt((String) properties.get("thread.pool.size"));
} catch (Throwable t) {
//ignore
}
}
// public static void main(String[] args) {
// System.out.println(LoadProperties.CONNECT_TIMEOUT);
// }
}