RouterOS 高级调试工具:是一款基于javaFX开发的,更适合国人使用习惯的桌面工具,它通过操作界面实现了RouterOS 命令配置的可视化操作,将复杂的路由器命令融合进来,使得用户在使用RouterOS软路由时感到非常得直观、易用。
目录
1,RouterOS 简介
- RouterOS的简单介绍
2,RouterOS API
- 介绍了RouterOS API通信与协议原理
3,RouterOS API 客户端
- Mikrotik -java: RouterOS API 协议的Java客户端实现
4,RouterOS 高级调试工具
-
系统架构及简介
-
系统主要类关系图
-
系统主要类设计思路及实现
5,打包与发布
- EXE4J
- Inno Setup
6,系统功能
- 系统功能脑图展示
7,系统部分功能截图
- 系统部分功能截图
一:RouterOS 简介
RouterOS是一种路由操作系统,并通过该软件将标准的PC电脑变成专用的路由器,在软件的开发和应用上不断的更新和发展,软件经历了多次更新和改进,使其功能在不断增强和完善。特别在无线、认证、策略路由、带宽控制和防火墙过滤等功能上有着非常突出的功能,其极高的性价比,受到许多网络人士的青睐。
RouterOS 是 由拉脱维亚MikroTik 司开发的一 种基于Linux 内核的路由操作系统。RouterOS在具备现有路由系统的大部分功能,能针对网吧、企业、小型ISP接入商、社区等网络设备的接入,Mikrotik厂家提供带有RouterOS的硬件路由器,同时支持标准的x86构架PC。一台586PC机就可以实现路由功能,提高硬件性能同样也能提高网络的访问速度和吞吐量。完全是一套低成本,高性能的路由器系统。
MikroTik RouterOS 是将标准的PC电脑变成功能强大的路由器,添加标准的PC网络接口卡能增强路由器的功能。MikroTik RouterOS基于路由、PPPoE认证、Web认证、流量控制、Web-proxy、专业无线等于一身 ,可以根据需要增加或删除相应的功能,是许多路由器所无法实现的。同时MikroTik RouterBOARD专门为RouterOS设计的路由硬件,能稳定的应用在各种网络环境中。
二:RouterOS API
RouterOS API 允许用户创建自定义软件解决方案,与RouterOS通信,以收集信息、调整配置和管理路由器。API严格遵循命令行接口(CLI)的语法。它可以用来创建翻译或自定义的配置工具,帮助使用RouterOS轻松运行和管理路由器。
默认情况下,API使用端口#8728,并且服务已启用。
API协议
-
简述
与路由器的通信是通过向路由器发送句子并接收一个或多个句子来完成的。句子是以零长度的单词结尾的单词序列。单词是以某种方式编码的句子的一部分——编码长度和数据。通信是通过向路由器发送句子并接收对已发送句子的回复来进行的。使用API发送给路由器的每个句子都应该包含命令,第一个单词后面跟着没有特定顺序的单词,句子的结尾用零长度的单词标记。当路由器收到完整的句子(命令字,没有或多个属性字,零长度字)时,它被评估和执行,然后形成应答并返回。
-
API words
单词是句子的一部分。每个单词都必须以特定的方式编码——单词的长度后跟单词的内容。字的长度应以将要发送的字节数表示。
Value of length # of bytes Encoding 0 <= len <= 0x7F 1 len, lowest byte 0x80 <= len <= 0x3FFF 2 len | 0x8000, two lower bytes 0x4000 <= len <= 0x1FFFFF 3 len | 0xC00000, three lower bytes 0x200000 <= len <= 0xFFFFFFF 4 len | 0xE0000000 len >= 0x10000000 5 0xF0 and len as four bytes -
每个单词被编码为长度,后面跟着相同字节的内容;
-
单词被组合成句子。句子结束时以零长度词结尾;
-
方案允许长度为0x7FFFFFFFFF的编码,只支持四个字节的长度;
-
len的字节首先发送最重要的字节(网络顺序);
-
如果字的第一个字节是>= 0xF8,则它是一个保留控制字节。在接收到未知的控制字节后,API客户端不能继续,因为它不知道如何解释下面的字节;
-
一般来说,单词可以这样描述<<encoded word length><word content>>,
单词内容可以分为5个部分:命令词、属性词、API属性词、查询词和回复词
-
-
命令词 (Command word)
句子中的第一个单词必须是command,后跟属性词和零长词或终止词。命令字的名称应以“/”开头。命令名紧跟在CLI后面,空格替换为“/”。有一些特定于API的命令;
命令词结构有严格的顺序:
-
编码长度
-
内容前缀/
-
CLI转换命令
/login /user/active/listen /interface/vlan/remove /system/reboot
-
-
属性词(Attribute word)
根据内容,每个命令词都有自己的属性词列表。
Atribute单词结构由5部分组成,按顺序排列:
- 编码长度
- 内容前缀等于符号-=
- 属性名
- 等号分隔-=
- 属性的值(如果有),该属性可能没有值(由于单词的编码方式不同,Value可以在属性词的值中包含多个等号)
没有编码长度前缀的示例:
=address=10.0.0.1 =name=iu=c3Eeg =disable-running-check=yes
警告:属性词和API参数的顺序并不重要,不应依赖它们
-
API属性词(API attribute word)
API属性词结构的顺序非常严格:
- 编码长度
- 带点的内容前缀。
- 属性名
- 名称用等号=符号进行后缀
- 属性值
目前唯一这样的API属性是tag
注意:如果句子包含API属性单词标记,那么路由器返回给该标记句子的每个句子都将使用相同的标记进行标记。
-
查询词(Query word)
句子可以有额外的查询参数来限制其范围。
使用查询词属性的句子示例:
/interface/print ?type=ether ?type=vlan ?#|!
查询词以“?”开头。
目前只有print命令处理查询词。
注意:查询词是有顺序的
-
回复词(Reply word)
它只由路由器发送。它只在客户发送完整句子后发送。
- 回复的第一个词以“!”开头;
- 发送的每个句子至少会生成一个回复(如果连接没有终止);
- 每句话的最后一个回复是第一个单词的回复!完成;
- 错误和异常情况始于!陷阱
- 数据回复开始!重新
- 如果API连接关闭,RouterOS将发送!以原因作为回复,然后关闭连接;
API语句
API语句是使用API进行通信的主要对象。
- 空话被忽略。
- 在接收到零长单词后,对句子进行处理。
- 客户端在登录之前可以发送的句子数量和大小有限制。
- 属性词的顺序不应依赖。因为顺序和数量是可以改变的。proplist属性。
- 句子结构如下:
- 第一个单词应该包含命令词;
- 应该包含长度为零的单词来终止句子;
- 可以不包含或包含多个属性词。句子中的定语词没有特定的顺序,顺序对定语词并不重要;
- 不能包含任何或多个查询词。句子中疑问词的顺序很重要。
注意:零长单词结束句子。如果未提供,路由器将不会开始评估发送的单词,并将所有输入视为同一句子的一部分。
初始登录
- 首先,客户端发送/login命令。
- 回复包含=ret=challenge参数。
- 客户端发送第二个/login命令,其中包含=name=username和=response=response。
- 如果出现错误,reply包含=ret=错误消息。
- 如果成功登录,客户端可以开始发出命令。
标签
- 可以同时运行多个命令,而无需等待前一个命令完成。如果API客户端正在这样做,并且需要区分命令响应,那么它可以在命令语句中使用“tag”API参数。
- 如果在命令语句中包含具有非空值的“tag”参数,则具有完全相同值的“tag”参数将包含在此命令生成的所有响应中。
- 如果不包含“tag”参数或其值为空,则此命令的所有响应都不会包含“tag”参数。
命令
/cancel
- 可选参数:=tag=要取消的命令的标记,如果没有它,将取消所有正在运行的命令不会自动取消
- 所有被取消的命令都会被中断,通常情况下会生成'!陷阱'和'!“完成”的回答
- 请注意/cancel是单独的命令,可以有自己的“唯一”。tag'参数,该参数与此命令的“=tag”参数无关
/listen
- listen命令在控制台打印命令可用的地方可用,但它在任何地方都没有预期的效果(即可能不起作用)
- !当特定项目列表中的某些内容发生变化时,会生成复句
- 当项目被删除或以任何其他方式消失时,“!re‘句子包含值’=。死亡=是'
- 此命令不会终止。要终止它,请使用/cancel命令。
/getall
- 当控制台打印命令可用时,getall命令可用。由于版本3.21,getall是print的别名。
- 回复包含=。id=物料内部编号属性。
API打印命令与控制台对应命令的不同之处如下:
- 在不支持参数的情况下。可以使用查询词筛选项目(见下文)。
- .proplist参数是一个逗号分隔的属性名称列表,应该包含在返回的项中。
- 返回的项目可能具有其他属性。
- 未定义返回属性的顺序。
- 如果列表包含重复条目,则不定义对此类条目的处理。
- 如果有属性值存在。proplist,但不在该项中,则该项没有此属性值(?该项的名称将计算为false)。
- 如果proplist不存在,所有属性都会按照print命令的要求包括在内,即使是那些访问时间较慢的属性(例如文件内容和性能计数器)。因此使用。支持proplist。省略。如果设置了=detail=参数,proplist可能会有较高的性能损失。
查询
print命令接受限制返回语句集的查询词。此功能自RouterOS 3.21开始提供。
-
查询词以“?”开头。
-
查询词的顺序很重要。查询从第一个单词开始计算。
-
将为列表中的每个项计算查询。如果查询成功,则处理项;如果查询失败,则忽略项。
-
查询使用布尔值堆栈进行计算。最初,堆栈包含无限量的“真”值。在计算结束时,如果堆栈至少包含一个“false”值,则查询失败。
-
查询词的操作遵循以下规则:
查询语句 描述 ?name 如果项的属性值为name,则推送'true',否则推送'false'。 ?-name 如果项没有属性名称的值,则按“true”,否则按“false”。 ?name=x ?=name=x 如果属性name的值等于x,则推送'true',否则推送'false'。 ?<name=x 如果属性name的值小于x,则推送'true',否则推送'false'。 ?>name=x 如果属性name的值大于x,则推送'true',否则推送'false'。 ?#operations 对堆栈中的值应用操作。
操作字符串从左到右计算。
后跟任何其他字符或字尾的十进制数字序列被解释为堆栈索引。
Top值的索引为0。
后跟字符的索引在该索引处推送值的副本。后跟单词结尾的索引将用该索引处的值替换所有值。
!字符用相反的值替换最高的值。
&弹出两个值并推入逻辑'and'操作的结果。
|弹出两个值并推入逻辑'or'操作的结果。
.后索引什么都不做。
.在另一个字符推送顶部值的拷贝后。注意:API中不支持正则表达式,因此不要尝试发送带有~符号的查询
例如:获取所有以太网和VLAN接口:/interface/print ?type=ether ?type=vlan ?#|
获取所有带有非空注释的路由:
/ip/route/print ?>comment=
!trap
当由于某些原因API语句失败时,trap被返回,并伴随着message属性和某些情况下category参数
-
message
<<< /ip/address/add <<< =address=192.168.88.1 <<< =interface=asdf <<< >>> !trap >>> =category=1 >>> =message=input does not match any value of interface
-
category
如果是一般错误,则对其进行分类并返回错误类别。此属性的可能值为
- 缺少项目或命令
- 参数值失败
- 命令执行中断
- 脚本相关的失败
- 一般故障
- API相关故障
- TTY相关故障
- 通过:return命令生成的值
三:RouterOS API 客户端
Mikrotik RouterOS API的Java客户端库实现。
这个项目提供了一个Java客户机来使用远程API操作Mikrotik路由器。
开源地址:https://github.com/GideonLeGrange/mikrotik-java
maven依赖
<dependency>
<groupId>me.legrange</groupId>
<artifactId>mikrotik</artifactId>
<version>3.0.7</version>
</dependency>
如何使用API最好通过示例来说明。
这些例子应该说明如何使用这个库。请注意,假设用户精通Java并理解Mikrotik命令行语法。命令行语法告诉你可以传递哪些命令,但是这个库使用的RouterOS API并不支持所有的命令。
在调试API调用时需要考虑以下几点:
- RouterOS API不支持自动完成。您需要写出命令和参数名称。例如,你不能说/ip/hotspot/user/add name=john add=10.0.0.1,你需要写出地址。
- 您需要在值中加上空格。你不能说name=Joe Blogs,你需要使用name="Joe Blogs"
- 根因为ApiCommandException的异常是指从远端RouterOS设备接收到的错误信息。
-
建立连接
ApiConnection con = ApiConnection.connect("10.0.1.1"); // connect to router con.login("admin","password"); // log in to router con.execute("/system/reboot"); // execute a command con.close(); // disconnect from router
上面的示例展示了一种使用默认API端口和超时创建未加密连接的简单方法,这对于开发和测试非常有用。
TLS
对于生产环境,建议对API流量进行加密。要做到这一点,你需要通过传递一个SocketFactory实例来打开到路由器的TLS连接,SocketFactory实例是你希望用来构造TLS套接字的API
ApiConnection con = ApiConnection.connect(SSLSocketFactory.getDefault(), "10.0.1.1", ApiConnection.DEFAULT_TLS_PORT, ApiConnection.DEFAULT_CONNECTION_TIMEOUT);
上面的默认SSL套接字工厂实例被传递给API。只要路由器的证书已经添加到本地密钥存储中,就可以这样做。除了允许用户指定套接字工厂外,上面的方法还提供了对TCP端口和连接超时的完全控制。
RouterOS还支持匿名TLS。
连接超时
默认情况下,如果API不能连接到指定的路由器,它将生成一个异常。这可以立即发生(通常是在操作系统返回“连接拒绝”错误时),但如果路由器主机有防火墙或有其他网络问题,也可能需要长达60秒。这60秒是“默认连接超时”,可以通过将首选超时作为connect()调用的最后一个参数传递给APi来覆盖。例如:
ApiConnection con = ApiConnection.connect(SSLSocketFactory.getDefault(), "10.0.1.1", ApiConnection.DEFAULT_TLS_PORT, 2000); // connect to router on the default API port and fail in 2 seconds
常量
ApiConnection中提供了一些常量,让用户更容易使用默认端口和超时来构建连接:
常量 描述 值 DEFAULT_PORT 未加密连接的默认TCP“端口”值 8728 DEFAULT_TLS_PORT 加密连接的默认TCP“端口”值 8729 DEFAULT_CONNECTION_TIMEOUT 默认连接超时值(毫秒) 60000 -
读数据
一个返回结果的简单示例-打印所有接口:
List<Map<String, String>> rs = con.execute("/interface/print"); for (Map<String,String> r : rs) { System.out.println(r); }
结果作为String键/值对的映射列表返回。这样做的原因是,一个命令可以返回多个结果,这些结果有多个堆变量。例如,要打印上面命令中返回的所有接口的名称,执行:
for (Map<String, String> map : rs) { System.out.println(map.get("name")); }
过滤结果:
相同的查询,但过滤了结果:打印所有类型为“vlan”的接口。
List<Map<String, String>> rs = con.execute("/interface/print where type=vlan");
选择返回的字段:
打印所有类型为“vlan”的接口,并只返回它们的名称:
List<Map<String, String>> rs = con.execute("/interface/print where type=vlan return name");
-
写数据
本例展示如何新建GRE接口:
con.execute("/interface/gre/add remote-address=192.168.1.1 name=gre1 keepalive=10");
修改上面例子中创建的对象的IP地址:
con.execute("/interface/gre/set .id=gre1 remote-address=10.0.1.1");
现在移除对象:
con.execute("/interface/gre/remove .id=gre1");
取消对象上的变量设置
取消变量的设置略有不同,您需要使用一个名为value-name的参数。这并没有很好的记录。假设你有这样一个防火墙规则:
con.execute("/ip/firewall/filter/add action=accept chain=forward time=00:00:01-01,mon")
假设规则可以被访问为.id=*1,你可以使用value-name取消它的设置,如下所示:
con.execute("/ip/firewall/filter/unset .id=*1 value-name=time");
-
异步执行命令
我们可以异步运行一些命令来继续接收更新:
这个例子展示了如何运行'/interface wireless monitor',并将结果发送给监听器对象,由它打印出来:
String tag = con.execute("/interface/wireless/monitor .id=wlan1 return signal-to-noise", new ResultListener() { public void receive(Map<String, String> result) { System.out.println(result); } public void error(MikrotikApiException e) { System.out.println("An error occurred: " + e.getMessage()); } public void completed() { System.out.println("Asynchronous command has finished"); } } );
ResultListener接口有三个用户需要实现的方法:
- 调用receive()来接收由路由器从API产生的结果。
- 当基于从路由器接收到的“陷阱”或其他(通常是连接)问题引发异常时,将调用Error()。
- 当路由器指示命令已完成或已取消时,将调用Completed()。
上述命令将在结果可用时以异步方式运行并发送结果,直到它被取消。命令(由唯一返回的String标识)被这样取消:
con.cancel(tag);
-
命令超时设置
命令超时可用于确保同步命令在特定时间内返回或失败。命令超时与connect()中使用的连接超时是分开的,可以使用setTimeout()来设置。下面是一个例子:completed()在路由器指示命令已完成或已取消时被调用。
ApiConnection con = ApiConnection.connect("10.0.1.1"); // connect to router con.setTimeout(5000); // set command timeout to 5 seconds con.login("admin","password"); // log in to router con.execute("/system/reboot"); // execute a command
如果用户未设置命令超时时间,则命令超时时间默认为60秒。
四:RouterOS 高级调试工具
-
系统架构及简介
![](https://img.haomeiwen.com/i23568343/cfaae0c1fb0528ac.png)
-
展示层
基于javaFX+jfoenix(客户端组件框架)+DataFX(客户端Controller与fxml文件关联)。
-
业务功能
主要提供了设备发现,系统登录/退出,设备状态,接口列表,桥配置,无线,RADIUS,Mesh网络,网络,防火墙,系统,文件,工具等功能。
-
业务接口
定义了业务功能主要接口,并基于xKernel插件体系,开发了接口的默认实现,封装了完成业务功能的 RouterOS 命令,为后期适配RouterOS命令变化升级,提供了灵活的实现及机制。
-
RouterOS API Client
Mikrotik RouterOS API的Java客户端库实现。
-
远程设备
基于RouterOS 系统的设备。
-
系统主要类关系图
![](https://img.haomeiwen.com/i23568343/aea8e1b12beb9746.png)
-
系统主要类设计思路及实现
-
本地设备发现
-
设计思路
工具与设备需要在同一网段,工具按照设备发现协议通过发起多播数据套接字将长度为4的数据包以广播的方式发送出去,到多个设备客户端指定的端口(如:5678),设备指定端口(5678)收到长度为4的数据包时,设备识别为设备发现请求包,此时将自己的mac地址,IP地址,系统版本,系统平台等信息按照设备发现协议封装成数据包返回给工具指定端口(如:6789),完成设备发现与响应。
-
设备发现协议->请求协议
名称 类型 长度 设备发现请求标识 byte[] 4 -
设备发现协议->响应协议:由响应标识+响应内容组成
响应标识
名称 类型 长度 设备发现响应标识 byte[] 4 响应内容:由1个或者多个<类型字节数组[长度2]+内容长度字节数组[长度2]+内容字节数组[内容长度]>段组成
名称 类型 长度 类型 byte[] 2 内容长度 byte[] 2 内容 byte[] 内容长度
-
-
关键代码
@Slf4j public class DeviceDiscoverImpl implements DeviceDiscover { /** * 广播地址端口 */ public static final int BORADCAST_PORT = 6789; /** * 接收端口 */ public static final int RECEIVE_PORT = 5678; /** * 默认广播地址 */ public static final String DEFAULT_MULTICAST_ADDR = "255.255.255.255"; /** * socket超时时间 */ public static final int SO_TIMEOUT = 10000; /** * 处理超时时间 */ public static final long TIMEOUT_MILLIS = 10000L; public static final String GB_2312 = "gb2312"; /** * 设备发现 * <li></li> * * @author duanyong@jccfc.com * @date 2022/4/6 9:44 * @return: void */ @Override public void discover() { log.info("设备发现"); //启动接收者 startReceiver(); //广播默认地址 doBroadcast(DEFAULT_MULTICAST_ADDR); //广播本地局域网 doLocalBroadcast(); } /** * 广播本地局域网 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 11:06 * @return: void */ private void doLocalBroadcast() { try { Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); if(interfaces == null){ return; } while (interfaces.hasMoreElements()) { interfaces .nextElement() .getInterfaceAddresses() .stream() .filter(interfaceAddress -> interfaceAddress.getNetworkPrefixLength() >= 0) .filter(interfaceAddress -> interfaceAddress.getNetworkPrefixLength() <= 32) .filter(interfaceAddress -> !interfaceAddress.getAddress().getHostAddress().startsWith("127")) .forEach(interfaceAddress -> broadcast(interfaceAddress)); } } catch (Exception e) { log.error("广播本地局域网异常", e); } } /** * 启动接收者 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 10:06 * @return: void */ private void startReceiver() { Receiver receiver = new Receiver(); receiver.start(); } /** * 广播 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 9:43 * @param interfaceAddress: 网络接口地址 * @return: void */ private void broadcast(InterfaceAddress interfaceAddress) { int mask = interfaceAddress.getNetworkPrefixLength(); String multicastAddr = IpUtils.getNetBrdcstAddr(interfaceAddress.getAddress().getHostAddress(), IpUtils.getNetMask(mask)); doBroadcast(multicastAddr); } /** * 执行广播 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 9:53 * @param multicastAddr:地址 * @return: void */ private void doBroadcast(String multicastAddr) { try(MulticastSocket socket = new MulticastSocket(BORADCAST_PORT)){ InetAddress inetAddress = InetAddress.getByName(multicastAddr); DatagramPacket datagramPacket = new DatagramPacket(new byte[4], 0, 4, inetAddress, RECEIVE_PORT); socket.send(datagramPacket); }catch (Exception exception){ log.error("广播发生异常",exception); } } /** * 接收者 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 10:21 */ class Receiver extends Thread { @Override public void run() { try(MulticastSocket socket = new MulticastSocket(RECEIVE_PORT)) { socket.setSoTimeout(SO_TIMEOUT); long time = System.currentTimeMillis(); while (System.currentTimeMillis() - time <= TIMEOUT_MILLIS) { byte[] buff = new byte[2048]; DatagramPacket packet = new DatagramPacket(buff, 0, buff.length); try { socket.receive(packet); handlePacket(packet); } catch (IOException ioException) { log.error("接收并处理数据包异常",ioException); } } } catch (Exception exception) { log.error("接收者异常",exception); } } /** * 根据设备发现协议解析数据包 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 11:09 * @param packet:数据包 */ private void handlePacket(final DatagramPacket packet) { if (packet.getLength() == 0){ return; } //开启线程执行解析 new Thread(()->doHandlePacket(packet)).start(); } /** * 执行解析数据包 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/6 11:31 * @param packet:数据包 * @return: void */ private void doHandlePacket(DatagramPacket packet) { try { //定义设备信息对象 Dev dev = new Dev(); dev.setIp(packet.getAddress().getHostAddress()); //数据拷贝 byte[] data = new byte[packet.getLength()]; System.arraycopy(packet.getData(), 0, data, 0, packet.getLength()); //当前索引位置 int cursor = 4; while (cursor != data.length) { Protocol protocol = new Protocol(data, cursor).decode(); cursor = protocol.getCursor(); if (setData(dev, protocol)){ continue; } } dev.setBoard(Broads.getDBBroad(dev.getBoard())); dev.setVersion(Versions.toDBVersion(dev.getVersion())); //如果Mac为空 则表示解析失败 if (StringUtils.isEmpty(dev.getMac())) { return; } log.info("发现设备:{}",dev); //发送发现设备事件 EventBusUtil.getInstance().post(dev); } catch (Exception ex) { log.error("解析数据包异常", ex); } } private boolean setData(Dev dev, Protocol protocol) { int type = protocol.getType(); String content = protocol.getContent(); //类型1:Mac地址 if (type == 1) { dev.setMac(content); return true; } //类型5:id if (type == 5) { dev.setIdentity(content); return true; } //类型7:系统版本 if (type == 7) { dev.setVersion(content); return true; } //类型8:平台名称 if (type == 8) { if (content.indexOf(Constant.SYSTEM_PLATFORM_NAME) == -1){ return true; } dev.setPlatform(content); return true; } //类型10:运行时间 if (type == 10) { int sec = Integer.valueOf(content).intValue(); int day = sec / 86400; sec %= 86400; int hour = sec / 3600; sec %= 3600; int minute = sec / 60; sec %= 60; String upTime = day + "天" + hour + "小时" + minute + "分钟" + sec + "秒"; dev.setUpTime(upTime); return true; } //其他 if (type == 11 || type != 12) { return true; } //board信息 dev.setBoard(content); return false; } /** * 协议对象 * <li> * 协议编码规则:标识字节数组[长度4]+<类型字节数组[长度2]+内容长度字节数组[长度2]+内容字节数组[内容长度]>组成 * </li> * @author duanyong@jccfc.com * @date 2022/4/6 14:48 */ private class Protocol { /** * 数据 */ private byte[] data; /** * 当前位置 */ private int cursor; /** * 类型 */ private int type; /** * 内容 */ private String content; public Protocol(byte[] data, int cursor) { this.data = data; this.cursor = cursor; } public int getCursor() { return cursor; } public int getType() { return type; } public String getContent() { return content; } /** * 解码 * <li> * 类型字节数组[长度2]+内容长度字节数组[长度2]+内容字节数组[内容长度] * </li> * @author duanyong@jccfc.com * @date 2022/4/6 14:12 * @return: org.javacoo.javafx.service.device.DeviceDiscoverImpl.Receiver.Protocol */ public Protocol decode() throws UnsupportedEncodingException { //获取类型字节数组 byte[] typeBytes = ByteUtil.getSubBytes(data, cursor, 2); //获取类型 type = ByteUtil.getIntByBytes(typeBytes, 0, 2); //移动索引位置+2 cursor += 2; //获取长度字节数组 byte[] lengthBytes = ByteUtil.getSubBytes(data, cursor, 2); //获取长度 int length = ByteUtil.getIntByBytes(lengthBytes, 0, 2); //移动索引位置+2 cursor += 2; //获取内容字节数组 byte[] values = ByteUtil.getSubBytes(data, cursor, length); //移动索引位置+length cursor += length; //获取内容 //类型1:Mac地址 if (type == 1) { content = ByteUtil.from6BytesToMac(values).toLowerCase(); } else { //类型10:运行时间 if (type == 10){ content = String.valueOf(ByteUtil.getIntFromByteArrayBigEnd(values)); }else { content = new String(values, GB_2312); } } return this; } } } }
-
-
连接到设备
-
设计思路
采用多Session设计,支持连接多个客户端设备,通过SessionFactory管理。
Session接口定义了客户端设备的连接,断开,命令同步执行,命令异步执行等方法。
SessionFactory实现了SessionListener接口,并包含了一个设备id与Session映射的Map对象,通过SessionListener的连接断开与建立回调机制维护。
当Session建立后,通过SessionChecker与设备保持长链接。
SessionChecker继承Thread类,系统启动时便启动一个线程,每隔5秒遍历一次遍历SessionFactory中保存的Session对象,依次发起命令与设备通信,实现心跳机制,并通过HeartResultListener,检查心跳结果,如果正常返回,则设置当前Session对象的最后活动时间,否则进入异常处理,如果是定义的断开异常信息,则发布SessionCloseEvent事件。
SessionChecker包含一个内部类Guarder,Guarder继承Thread类,随系统启动时,启动一个线程,每隔5秒遍历一次SessionFactory中保存的Session对象,检查其最后活动时间与当前时间比较,是否大于3秒,如果大于则发布SessionTimeoutEvent事件。
-
关键代码
Session接口:
@Spi(Constant.SPI_DEFAUL_KEY) public interface Session { ... /** * 连接 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/3 15:47 * @param communicationParam: 连接参数 * @return: java.lang.String 设备ID */ String connect(CommunicationParam communicationParam); ... }
Session接口实现类:
public class SessionImpl implements Session { private static final Logger logger = LoggerFactory.getLogger(SessionImpl.class); private volatile ApiConnection con = null; private String deviceId = null; private CommunicationParam communicationParam; private long lastActiveTime = System.currentTimeMillis(); private final List<SessionListener> sessionListeners = new CopyOnWriteArrayList(); private String platform = ""; /** * 连接 * <li></li> * * @param communicationParam : 连接参数 * @author duanyong@jccfc.com * @date 2022/4/3 15:47 * @return: java.lang.String 设备ID */ @Override public String connect(CommunicationParam communicationParam) { try { if (this.con != null) { try { this.con.close(); } catch (Exception e) { logger.error(communicationParam.getIp(), e); } } if (communicationParam.getPort() == 8729) { this.con = ApiConnection.connect(AnonymousSocketFactory.getDefault(), communicationParam.getIp(), communicationParam.getPort(), 3000); } else { this.con = ApiConnection.connect(SocketFactory.getDefault(), communicationParam.getIp(), communicationParam.getPort(), 6000); } this.con.login(communicationParam.getUserName(), communicationParam.getPassword()); } catch (Exception e) { logger.error(communicationParam.getIp(), e); try { if (this.con != null) { this.con.close(); } } catch (Exception exception) { } throw new ApiOptException(e); } if (this.deviceId == null) { this.deviceId = UUID.randomUUID().toString(); } communicationParam.setDeviceId(this.deviceId); this.communicationParam = communicationParam; //回调 this.sessionListeners.forEach(sessionListener -> sessionListener.connectCallBack(this)); //如果不可管理 if (!this.canManage()) { this.disconnect(); throw new ApiOptException("不支持的设备"); } else { return this.deviceId; } } ... }
SessionFactory接口:
@Spi(Constant.SPI_DEFAUL_KEY) public interface SessionFactory { /** * 连接 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/3 15:20 * @param communicationParam: 连接参数 * @return: org.javacoo.javafx.api.session.Session session */ Session connect(CommunicationParam communicationParam); ... }
SessionFactory接口实现类:
public class SessionFactoryImpl implements SessionFactory,SessionListener{ /** * Session map */ private final Map<String, Session> sessions = new ConcurrentHashMap(); /** * 连接 * <li></li> * * @param communicationParam : 连接参数 * @author duanyong@jccfc.com * @date 2022/4/3 15:20 * @return: org.javacoo.javafx.api.session.Session session */ @Override public Session connect(CommunicationParam communicationParam) { Session session; if(communicationParam.getDeviceId() == null){ session = new SessionImpl(); }else { session = this.sessions.get(communicationParam.getDeviceId()); } session.connect(communicationParam); session.addSessionListener(this); Session ss = (Session)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] {Session.class}, (proxy, method, args) -> { if ((method.getName() .equals("getCommunicationParam")) || (method.getName() .equals("disconnect")) || (method.getName() .equals("connect"))) { Object result = method.invoke(session, args); return result; } if (!sessions.containsKey(session.getCommunicationParam().getDeviceId())) { throw new ApiOptException("设备失去连接"); } return method.invoke(session, args); }); this.sessions.put(communicationParam.getDeviceId(), ss); return ss; } ... }
SessionChecker:
public class SessionChecker extends Thread { private static final Logger logger = LoggerFactory.getLogger(SessionChecker.class); private static final String NAME = "SessionChecker"; private final Guarder guarder = new Guarder(); private SessionFactory sessionFactory; private volatile AtomicBoolean stop = new AtomicBoolean(true); public SessionChecker(){ this.guarder.start(); start(); } public void init(SessionFactory sessionFactory) { stop.compareAndSet(true,false); this.sessionFactory = sessionFactory; } public void stopThread(){ logger.info("挂起线程"); this.interrupt(); this.guarder.interrupt(); stop.compareAndSet(false,true); } private void processException(Session session, Exception ex) { String msg = ExceptionUtils.getMessage(ex); if (msg == null){ return; } logger.info(session.getCommunicationParam().getIp() + " 发生异常=" + msg); boolean closed = Stream.of(Constant.CLOSE_SESSION_EXCEPTION_MSG_KEYS.split(",")).anyMatch(errMsgKey->msg.contains(errMsgKey)); if (closed) { try { logger.info("关闭设备连接=" + session.getCommunicationParam().getIp()); session.disconnect(); } catch (Exception e) { logger.error("", e); } EventBusUtil.getInstance().post(SessionCloseEvent.builder().session(session).build()); } logger.error("", ex); } @Override public void run() { while (!isInterrupted()) { try { try { Thread.sleep(Constant.SLEEP_SILLIS); } catch (InterruptedException localInterruptedException) { logger.error("线程中断",localInterruptedException); } if(stop.get()){ continue; } List<Session> allSessions = this.sessionFactory.getAllSession(); if(allSessions == null || allSessions.isEmpty()){ continue; } allSessions.stream() .filter(session -> session != null) .filter(session -> SessionChecherExecutor.getTaskCount(session.getCommunicationParam().getIp(), NAME) < 1) .forEach(session -> SessionChecherExecutor.execute(session.getCommunicationParam().getIp(), new MRunnable() { @Override public void run() { Session currentSession = sessionFactory.getSession(session.getCommunicationParam().getDeviceId()); if (currentSession == null){ return; } String deviceId = currentSession.getCommunicationParam().getDeviceId(); try { currentSession.executeWithAsync(Constant.HEARTBET_CMD, new HeartResultListener(currentSession, deviceId)); } catch (Exception ex) { logger.error(currentSession.getCommunicationParam().getIp(), ex); processException(currentSession, ex); } } @Override public String getSrc() { return session.getCommunicationParam().getIp(); } @Override public String getTag() { return NAME; } })); } catch (Throwable th) { logger.error("", th); } } } private class Guarder extends Thread { private Guarder() { } @Override public void run() { while (!isInterrupted()) { try { Thread.sleep(Constant.SLEEP_SILLIS); } catch (InterruptedException localInterruptedException) { logger.error("线程中断",localInterruptedException); } if(stop.get()){ continue; } try { List<Session> allSessions = SessionChecker.this.sessionFactory.getAllSession(); if(allSessions == null || allSessions.isEmpty()){ continue; } allSessions.stream() .filter(session -> System.currentTimeMillis() - session.lastActiveTime() > Constant.SESSION_TIMEOUT_SILLIS) .forEach(session -> EventBusUtil.getInstance().post(SessionTimeoutEvent.builder().session(session).build())); } catch (Throwable th) { logger.error("", th); } } } } public class HeartResultListener implements ApiResultListener { private final String deviceId; private final Session session; public HeartResultListener(Session session, String deviceId) { this.deviceId = deviceId; this.session = session; } @Override public void receive(Map<String, String> data) { Session session = sessionFactory.getSession(this.deviceId); session.setActiveTime(System.currentTimeMillis()); } @Override public void error(Exception ex) { logger.error("连接异常,ip:{}",this.session.getCommunicationParam().getIp()); logger.error(this.session.getCommunicationParam().getIp(), ex); processException(this.session, ex); } @Override public void completed() { } } }
-
-
协议编解码
-
设计思路
直接使用Mikrotik-java客户端库,该库提供了Mikrotik API 的编解码方案的实现。
-
关键代码
Command类:这个内部类用于构建复杂的命令与参数,查询和属性列表
class Command { @Override public String toString() { return String.format("cmd[%s] = %s, params = %s, queries = %s, props=%s ", tag, cmd, params, queries, properties); } Command(String cmd) { if (!cmd.startsWith("/")) { cmd = "/" + cmd; } this.cmd = cmd; } String getCommand() { return cmd; } /** * Add a parameter to a command. */ void addParameter(String name, String value) { params.add(new Parameter(name, value)); } /** * Add a valueless parameter to the command */ void addParameter(Parameter param) { params.add(param); } /** * Add a property to include in a result */ void addProperty(String... names) { properties.addAll(Arrays.asList(names)); } void addQuery(String... queries) { this.queries.addAll(Arrays.asList(queries)); } void setTag(String tag) { this.tag = tag; } List<String> getQueries() { return queries; } String getTag() { return tag; } List<String> getProperties() { return properties; } List<Parameter> getParameters() { return params; } private final String cmd; private final List<Parameter> params = new LinkedList<>(); private final List<String> queries = new LinkedList<>(); private final List<String> properties = new LinkedList<>(); private String tag; }
Util类:提供Mikrotik API 的编解码方案的实现
final class Util { private static final String DEFAULT_CHARSET = "gb2312"; /** * write a command to the output stream */ static void write(Command cmd, OutputStream out) throws UnsupportedEncodingException, IOException { encode(cmd.getCommand(), out); for (Parameter param : cmd.getParameters()) { encode(String.format("=%s=%s", param.getName(), param.hasValue() ? param.getValue() : ""), out); } String tag = cmd.getTag(); if ((tag != null) && !tag.equals("")) { encode(String.format(".tag=%s", tag), out); } List<String> props = cmd.getProperties(); if (!props.isEmpty()) { StringBuilder buf = new StringBuilder("=.proplist="); for (int i = 0; i < props.size(); ++i) { if (i > 0) { buf.append(","); } buf.append(props.get(i)); } encode(buf.toString(), out); } for (String query : cmd.getQueries()) { encode(query, out); } out.write(0); } /** * decode bytes from an input stream of Mikrotik protocol sentences into * text */ static String decode(InputStream in) throws ApiDataException, ApiConnectionException { StringBuilder res = new StringBuilder(); decode(in, res); return res.toString(); } /** * decode bytes from an input stream into Mikrotik protocol sentences */ private static void decode(InputStream in, StringBuilder result) throws ApiDataException, ApiConnectionException { try { int len = readLen(in); if (len > 0) { byte buf[] = new byte[len]; for (int i = 0; i < len; ++i) { int c = in.read(); if (c < 0) { throw new ApiDataException("Truncated data. Expected to read more bytes"); } buf[i] = (byte) (c & 0xFF); } String res = new String(buf, Charset.forName(DEFAULT_CHARSET)); if (result.length() > 0) { result.append("\n"); } result.append(res); decode(in, result); } } catch (IOException ex) { throw new ApiConnectionException(ex.getMessage(), ex); } } /** * encode text using Mikrotik's encoding scheme and write it to an output * stream. */ private static void encode(String word, OutputStream out) throws UnsupportedEncodingException, IOException { byte bytes[] = word.getBytes(DEFAULT_CHARSET); int len = bytes.length; if (len < 0x80) {//128 out.write(len); } else if (len < 0x4000) {//16384 len = len | 0x8000;//32768 out.write(len >> 8); out.write(len); } else if (len < 0x20000) {//131072 len = len | 0xC00000;//12582912 out.write(len >> 16); out.write(len >> 8); out.write(len); } else if (len < 0x10000000) {//268435456 len = len | 0xE0000000;//3758096384 out.write(len >> 24); out.write(len >> 16); out.write(len >> 8); out.write(len); } else { out.write(0xF0); out.write(len >> 24); out.write(len >> 16); out.write(len >> 8); out.write(len); } out.write(bytes); } /** * read length bytes from stream and return length of coming word */ private static int readLen(InputStream in) throws IOException { int c = in.read(); if (c > 0) { if ((c & 0x80) == 0) { } else if ((c & 0xC0) == 0x80) { c = c & ~0xC0; c = (c << 8) | in.read(); } else if ((c & 0xE0) == 0xC0) { c = c & ~0xE0; c = (c << 8) | in.read(); c = (c << 8) | in.read(); } else if ((c & 0xF0) == 0xE0) { c = c & ~0xF0; c = (c << 8) | in.read(); c = (c << 8) | in.read(); c = (c << 8) | in.read(); } else if ((c & 0xF8) == 0xF0) { c = in.read(); c = (c << 8) | in.read(); c = (c << 8) | in.read(); c = (c << 8) | in.read(); c = (c << 8) | in.read(); } } return c; } }
-
-
应用主场景
-
设计思路
应用主场景由三个类组成:应用启动类,登录类,主界面类。
应用启动类:负责应用启动时初始化jfoenix与datafx,加载图片与全局CSS样式,以及加载并显示登录界面。
登录类:登录界面分为上下两部分,上面部分展示设备IP,用户名,密码,端口输入框。下面部分分为两个tab页,分别展示在线设备和登录历史记录,点击一条在线设备,可实现快速填充设备IP输入框。点击一条登录历史记录可实现快速登录。
主界面类:主界面是标准的桌面管理系统界面,分为顶部菜单栏,左侧导航栏,右侧主操作区域
-
关键代码
启动类:业务代码,无参考价值
登录类:LoginController,业务代码,无参考价值
主界面类:MainController,主要是操作区域实现了菜单添加右键菜单关闭相关功能值得参考
/** * 系统主界面 * <li></li> * @author duanyong@jccfc.com * @date 2022/3/27 14:45 */ @Slf4j @ViewController("/fxml/main/main.fxml") public class MainController { private <T> void addTab(String title, Node icon, Class<T> controllerClass, Object userData) { addTab(title, icon, new Flow(controllerClass), userData); } /** * 添加右键菜单 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/16 10:58 * @param tabPane: * @return: void */ private void addRightMenu(TabPane tabPane) { tabPane.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { //右键 if (event.getButton() == MouseButton.SECONDARY) { Node node = event.getPickResult().getIntersectedNode(); String styleClass = node.getParent().getStyleClass().toString(); if(!"tab-label".equals(styleClass)){ return; } //给node对象添加下来菜单; NavRightMenu.getInstance().show(node, javafx.geometry.Side.BOTTOM, 0, 0); } }); } private <T> void addTab(String title, Node icon, Flow flow, Object userData) { FlowHandler flowHandler = flow.createHandler(); WrapTab tab = tabsMap.get(title); if (tab == null) { tab = new WrapTab(title,flowHandler); tab.setUserData(userData); tab.setGraphic(icon); try { StackPane node = flowHandler.start(new AnimatedFlowContainer(Duration.millis(320), ContainerAnimations.SWIPE_LEFT)); node.getStyleClass().addAll("tab-content"); node.setPadding(new Insets(15, 20, 15, 20)); tab.setContent(node); } catch (FlowException e) { e.printStackTrace(); } tabPane.getTabs().add(tab); tabsMap.put(title, tab); tab.setOnClosed(event -> removeTabsMap(title)); } if (Constant.HOME_MENU_TITLE.equals(title)) { tab.setClosable(false); } tabPane.getSelectionModel().select(tab); } /** * 关闭当前tab * <li></li> * @author duanyong@jccfc.com * @date 2022/4/16 14:01 * @param text:标题 * @return: void */ private void removeTabsMap(String text){ WrapTab wrapTab = tabsMap.get(text); if(wrapTab == null){ return; } tabsMap.remove(text); try { wrapTab.getFlowHandler().getCurrentViewContext().destroy(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } /** * 删除当前table * <li></li> * @author duanyong@jccfc.com * @date 2022/4/16 14:21 * @param tab: * @return: void */ private void removeTab(Tab tab){ tabPane.getTabs().remove(tab); removeTabsMap(tab.getText()); } ... }
-
-
列表界面与新增,修改界面
-
设计思路
考虑到列表界面与新增,修改界面诸多共性,采用了模板方法模式,将其抽象封装到基类里面,特殊处理由具体子类实现,分别实现了列表Controller基类BaseListController,设置界面基类BaseSetController,以及模型基类BaseDataModel。规范了开发,提高 了开发效率。
-
关键代码
列表Controller基类:BaseListController
/** * 列表Controller基类 * <p>说明:</p> * <li></li> * * @author duanyong@jccfc.com * @date 2022/4/10 15:48 */ public abstract class BaseListController<T> extends BaseController{ @ActionHandler protected FlowActionHandler actionHandler; @FXML protected VBox root; @FXML protected TableView<T> tableView; @Inject protected BaseDataModel<T> dataModel; @FXML @ActionTrigger("add") protected ToggleButton addBtn; @FXML @ActionTrigger("modify") protected ToggleButton modifyBtn; @FXML @ActionTrigger("delete") protected ToggleButton delBtn; @FXML @ActionTrigger("disable") protected ToggleButton disableBtn; @FXML @ActionTrigger("enable") protected ToggleButton enableBtn; @FXML @ActionTrigger("refush") private ToggleButton refreshBtn; /** * 当前选中的值 */ protected T selectedData; @PostConstruct private void init() { EventBusUtil.getInstance().register(this); initCmp(); initData(); } @PreDestroy private void destroy() { onDestroy(); } @FXML @ActionMethod("refush") protected void refush(){ init(); } /** * 组件初始化 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 21:52 */ private void initCmp(){ initButton(); SortedList<T> tableItems = new SortedList<>(dataModel.getDatas(), getComparator()); tableItems.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(tableItems); tableView.setFixedCellSize(Constant.FIXED_CELL_SIZE); tableView.setOnMouseClicked(event -> { selectedData = tableView.getSelectionModel().getSelectedItem(); changeButtonState(); }); tableView.setRowFactory(tv -> { TableRow<T> row = new TableRow<>(); row.itemProperty().addListener((obs, previousAccessRule, currentAccessRule) -> changeRowStyle(row,currentAccessRule)); return row ; }); //行初始化 initColumn(); dataModel.selectedIndexProperty().bind(tableView.getSelectionModel().selectedIndexProperty()); } /** * 数据初始化 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 21:53 */ private void initData(){ dataModel.getDatas().clear(); ProcessChain.create() .addSupplierInExecutor(() -> loadData()) .addConsumerInPlatformThread(datas -> { if (!CollectionUtils.isEmpty(datas)) { dataModel.getDatas().addAll(datas); } }).onException(e -> { logger.error("数据初始化失败:",e); AlertUtil.show("数据初始化失败:"+ ExceptionUtils.getMessage(e)); }).run(); } /** * 初始化按钮 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 15:08 * @return: void */ private void initButton(){ selectedData = null; if(this.delBtn != null){ this.delBtn.setDisable(true); } if(this.modifyBtn != null){ this.modifyBtn.setDisable(true); } if(this.disableBtn != null){ this.disableBtn.setDisable(true); } if(this.enableBtn != null){ this.enableBtn.setDisable(true); } doInitButton(); } /** * 根据接口信息改变按钮状态 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 15:00 * @return: void */ private void changeButtonState() { if(selectedData == null){ if(this.delBtn != null){ this.delBtn.setDisable(true); } if(this.modifyBtn != null){ this.modifyBtn.setDisable(true); } if(this.disableBtn != null){ this.disableBtn.setDisable(true); } if(this.enableBtn != null){ this.enableBtn.setDisable(true); } } else { if(this.delBtn != null){ this.delBtn.setDisable(false); } if(this.modifyBtn != null){ this.modifyBtn.setDisable(false); } } doChangeButtonState(); } @FXML @ActionMethod("add") private void add(){ if(getAddController() == null){ return; } try { actionHandler.navigate(getAddController()); } catch (VetoException e) { e.printStackTrace(); } catch (FlowException e) { e.printStackTrace(); } } @FXML @ActionMethod("modify") private void modify(){ if(this.selectedData == null || getUpdateController() == null){ return; } Context.getInstance().setPageCacheValue(getUpdateController().getName(),this.selectedData); try { actionHandler.navigate(getUpdateController()); } catch (VetoException e) { e.printStackTrace(); } catch (FlowException e) { e.printStackTrace(); } } @FXML @ActionMethod("delete") private void delete(){ if(selectedData == null){ return; } String alertTitle = "确认删除吗?"; JFXAlert alert = new JFXAlert(root.getScene().getWindow()); alert.initModality(Modality.APPLICATION_MODAL); alert.setOverlayClose(false); JFXDialogLayout layout = new JFXDialogLayout(); layout.setHeading(new Label("消息提示")); layout.setBody(new Label(alertTitle)); JFXButton closeButton = new JFXButton("取消"); closeButton.setOnAction(event -> alert.hideWithAnimation()); JFXButton determineButton = new JFXButton("确定"); determineButton.setOnAction(event -> { alert.hideWithAnimation(); ProcessChain.create() .addSupplierInExecutor(() -> doDelete()) .addConsumerInPlatformThread(baseResp -> { if (baseResp.isSuccess()) { dataModel.getDatas().remove(dataModel.getSelectedIndex()); } }).onException(e -> { logger.error("操作失败:",e); AlertUtil.show("操作失败:"+ExceptionUtils.getMessage(e)); }).run(); }); layout.setActions(closeButton, determineButton); alert.setContent(layout); alert.show(); } @FXML @ActionMethod("disable") private void disable(){ if(selectedData == null){ return; } String alertTitle = "确认禁用吗?"; JFXAlert alert = new JFXAlert(root.getScene().getWindow()); alert.initModality(Modality.APPLICATION_MODAL); alert.setOverlayClose(false); JFXDialogLayout layout = new JFXDialogLayout(); layout.setHeading(new Label("消息提示")); layout.setBody(new Label(alertTitle)); JFXButton closeButton = new JFXButton("取消"); closeButton.setOnAction(event -> alert.hideWithAnimation()); JFXButton determineButton = new JFXButton("确定"); determineButton.setOnAction(event -> { alert.hideWithAnimation(); ProcessChain.create() .addSupplierInExecutor(() -> doDisable()) .addConsumerInPlatformThread(baseResp -> { if (baseResp.isSuccess()) { refush(); } }).onException(e -> { logger.error("操作失败:",e); AlertUtil.show("操作失败:"+ExceptionUtils.getMessage(e)); }).run(); }); layout.setActions(closeButton, determineButton); alert.setContent(layout); alert.show(); } @FXML @ActionMethod("enable") private void enable(){ if(selectedData == null){ return; } ProcessChain.create() .addSupplierInExecutor(() -> doEnable()) .addConsumerInPlatformThread(baseResp -> { if (baseResp.isSuccess()) { refush(); } }).onException(e -> { logger.error("操作失败:",e); AlertUtil.show("操作失败:"+ExceptionUtils.getMessage(e)); }).run(); } /** * 比较对象 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 16:38 * @return: java.util.Comparator<T> */ protected abstract Comparator<T> getComparator(); /** * 初始化列 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 16:42 * @return: void */ protected abstract void initColumn(); /** * 加载数据 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 16:51 * @return: java.util.List<T> */ protected abstract List<T> loadData(); /** * 初始化按钮 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/24 15:08 * @return: void */ protected void doInitButton(){} /** * 根据接口信息改变按钮状态 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 15:00 * @return: void */ protected void doChangeButtonState() { } /** * 改变行样式 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 16:46 * @param row: * @param t: * @return: void */ private void changeRowStyle(TableRow<T> row,T t){ if (t != null && getChangeRowStyleCondition(t)) { //方式1 row.styleProperty().setValue("-fx-background-color: grey ;"); //方式2 // row.pseudoClassStateChanged(old, currentPerson.getYears() > 60); }else{ row.styleProperty().setValue(""); } } /** * 获取改变行样式的条件 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/26 14:48 * @param t: * @return: boolean */ protected boolean getChangeRowStyleCondition(T t){return false;}; /** * 当页面销毁时触发 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 16:58 * @return: void */ protected void onDestroy(){} /** * 删除 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 17:24 * @return: org.javacoo.javafx.api.base.bean.BaseResp */ protected BaseResp doDelete(){ return BaseResp.ok(); } /** * 可用 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 17:25 * @return: org.javacoo.javafx.api.base.bean.BaseResp */ protected BaseResp doEnable(){ return BaseResp.ok(); } /** * 不可用 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 17:25 * @return: org.javacoo.javafx.api.base.bean.BaseResp */ protected BaseResp doDisable(){ return BaseResp.ok(); } /** * 获取新增控制类 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 20:11 */ protected Class getAddController(){return null;} /** * 获取修改制类 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 20:13 */ protected Class getUpdateController(){return null;} }
设置界面基类:BaseSetController
/** * 设置界面基类 * <li></li> * * @author duanyong@jccfc.com * @date 2022/4/9 15:10 */ public abstract class BaseSetController<T> extends BaseController{ @ActionHandler protected FlowActionHandler actionHandler; @FXML protected VBox root; @FXML protected Label title; @FXML @BackAction protected JFXButton cancelBut; @FXML @ActionTrigger("update") protected JFXButton updateBut; @FXML @ActionTrigger("add") protected JFXButton saveBut; protected T data; @PostConstruct private void init() { EventBusUtil.getInstance().register(this); Optional<T> dataOptional = Context.getInstance().getPageCacheValue(this.getClass().getName()); if(dataOptional.isPresent()){ this.data = dataOptional.get(); } initCmp(); initAction(); initData(); } @PreDestroy private void destroy() { doDestroy(); } /** * 组件初始化 * <li></li> * * @author duanyong@jccfc.com * @date 2022/4/9 9:57 * @return: void */ private void initCmp() { if(this.data != null || isSinglePage()){ if(saveBut != null){ title.setText("修改"+getTitle()); saveBut.managedProperty().bind(saveBut.visibleProperty()); saveBut.setVisible(false); }else{ title.setText(getTitle()); } }else{ title.setText("添加"+getTitle()); updateBut.managedProperty().bind(updateBut.visibleProperty()); updateBut.setVisible(false); } doInitCmp(); } /** * 回退 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:35 * @return: void */ protected void back() { try { actionHandler.navigateBack(); } catch (VetoException e) { e.printStackTrace(); } catch (FlowException e) { e.printStackTrace(); } } /** * 更新 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:38 * @return: void */ @ActionMethod("update") private void update() { ProcessChain.create() .addSupplierInExecutor(() -> doUpdate()) .addConsumerInPlatformThread(baseResp -> { if (baseResp.isSuccess()) { AlertUtil.show("操作成功"); if(!isSinglePage()){ back(); } }else{ AlertUtil.show("操作失败"); } }).onException(e -> { logger.error("操作失败",e); AlertUtil.show("操作失败:"+ ExceptionUtils.getMessage(e)); }).run(); } /** * 新增 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:38 * @return: void */ @ActionMethod("add") private void add() { ProcessChain.create() .addSupplierInExecutor(() -> doAdd()) .addConsumerInPlatformThread(baseResp -> { if (baseResp.isSuccess()) { AlertUtil.show("操作成功"); if(!isSinglePage()){ back(); } }else{ AlertUtil.show("操作失败"); } }).onException(e -> { logger.error("操作失败",e); AlertUtil.show("操作失败:"+ ExceptionUtils.getMessage(e)); }).run(); } /** * 页面标题 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:20 * @return: java.lang.String */ protected abstract String getTitle(); /** * 设置值 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:31 * @param t: * @return: void */ protected abstract void setData(T t); /** * 获取值 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:31 * @return: T */ protected abstract T getData(); /** * 执行组件初始化 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:22 * @return: void */ protected void doInitCmp(){} /** * 动作初始化 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:25 * @return: void */ protected void initAction(){} /** * 数据初始化 * <p>说明:</p> * <li></li> * * @author duanyong@jccfc.com * @date 2022/4/9 21:53 */ protected void initData(){} /** * 执行销毁 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:34 * @return: void */ protected void doDestroy(){} /** * 执行更新 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:37 * @return: org.javacoo.javafx.api.base.bean.BaseResp */ protected BaseResp doUpdate(){ return BaseResp.ok(); } /** * 执行新增 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 9:37 * @return: org.javacoo.javafx.api.base.bean.BaseResp */ protected BaseResp doAdd(){ return BaseResp.ok(); } /** * 是否是单页 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 14:47 */ protected boolean isSinglePage(){ return false; } }
模型基类:BaseDataModel
/** * 模型基类 * <li></li> * @author duanyong@jccfc.com * @date 2022/4/9 10:12 */ @ViewScoped public class BaseDataModel<T> { private int counter = 0; private IntegerProperty selectedIndex; private ListProperty<T> datas; public ListProperty<T> getDatas() { if (datas == null) { ObservableList<T> innerList = FXCollections.observableArrayList(); datas = new SimpleListProperty<>(innerList); } return datas; } public IntegerProperty selectedIndexProperty() { if (selectedIndex == null) { selectedIndex = new SimpleIntegerProperty(); } return selectedIndex; } public int getSelectedIndex() { return selectedIndexProperty().get(); } public void setSelectedIndex(int selectedIndex) { this.selectedIndex.set(selectedIndex); } }
-
-
插件体系
-
设计思路
基于xKernel实现自定义插件体系
<dependency> <groupId>com.javacoo</groupId> <artifactId>xKernel</artifactId> <version>1.0.0</version> </dependency>
-
关键代码
使用示例:
@Spi(Constant.SPI_DEFAUL_KEY) public interface SdkInterfaceService {} /** * 获取接口服务 * <p>说明:</p> * <li></li> * @author duanyong@jccfc.com * @date 2022/4/10 21:59 */ public SdkInterfaceService getSdkInterfaceService(){ return ExtensionLoader.getExtensionLoader(SdkInterfaceService.class).getDefaultExtension(); }
-
-
四:打包与发布
使用EXE4J+Inno Setup 将工具打包成windows桌面应用安装程序
五:系统功能
![](https://img.haomeiwen.com/i23568343/cfe130d569af1ae2.png)
六:系统部分功能截图
1:登录界面
![](https://img.haomeiwen.com/i23568343/eed7ba9aba0f8292.png)
2:首页界面
![](https://img.haomeiwen.com/i23568343/b04719a238a73665.png)
3:接口列表
![](https://img.haomeiwen.com/i23568343/74b0674bbc499b35.png)
4:桥配置
![](https://img.haomeiwen.com/i23568343/e0e8f192980d5dfa.png)
5:无线
![](https://img.haomeiwen.com/i23568343/53035f1494db9406.png)
6:网络
![](https://img.haomeiwen.com/i23568343/e3549c8048387d29.png)
7:防火墙
![](https://img.haomeiwen.com/i23568343/8254dcf70fdf5100.png)
8:系统
![](https://img.haomeiwen.com/i23568343/0bb2bd36e0f04ba3.png)
9:文件
![](https://img.haomeiwen.com/i23568343/b680fb7d19469daa.png)
10:工具
![](https://img.haomeiwen.com/i23568343/f623dc7cbb73ab4a.png)
一些信息
路漫漫其修远兮,吾将上下而求索
码云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
邮箱:xihuady@126.com
网友评论