简介:压缩包内包含一套面向电力行业监控领域的人机界面(HMI)系统源代码,适合电力后台与监控系统开发人员、SCADA初学者及需要二次集成的技术团队使用。该套代码以电力监控系统的数据采集、实时展示与远程控制为核心,涵盖了从设备连接到前端交互的完整链路,并针对多类监控曲线与后台数据管理提供了实现示例。文件共计51个,压缩后大小2.45MB,主要构成包括11个动态页面、9个编译后类文件、8个基础源码文件,以及若干依赖库、图标与项目配置。其中基础源码与类文件实现业务逻辑,动态页面负责曲线展示与操作界面,依赖库包含数据库连接池等组件,整体是可导入开发工具直接运行的电力监控前后端工程项目。目前已有563人参与学习下载。通过这份源代码可以了解电力监控人机界面的分层设计思路与前后端协作方式,尤其是数据库持久化、连接池管理以及基于网页的交互组件实际应用,方便在此基础上进行功能扩展,或迁移到具体生产环境作为原型参考。
1. 电力监控系统源码包没你想的那么神秘:SCADAHMI 到底能解决什么
电力监控系统这个名词听起来像电网公司专用的东西,但工厂配电、数据中心、园区能耗管理里到处都在用,许多项目只是把“设备状态”换成“可远传的测点数据”而已。SCADAHMI.rar 是一份用 Java 写的 SCADA/HMI 后台源码,代码里没有花哨的微服务框架,全是监控后台、监控连接系统真正绕不开的组件:一组曲线 JSP 页面、PostgreSQL 数据库、Proxool 连接池,以及一个负责浏览器端互动的 URLApplet。它适合三类人:刚接手旧电力项目的维护工程师,想搞懂实时采集链路的技术负责人,以及想在电力监控方向找一个能落地改造起点的开发者。下面按工程结构、运行配置、数据链路和常见翻车点逐层拆开,帮你判断这套源码值不值得下载、下完怎么让它先跑起来。
2. 导入环境与工程结构:让这套 Java Web 监控后台在本地跑起来
2.1 先认识 .mymetadata:这是 MyEclipse 工程的身份证
从压缩包解出来的内容里,.mymetadata、.classpath、.project三者同时出现,基本能断定这是个 Eclipse 系列 IDE 导出的 Java Web 工程。很多人一上来就用 IDEA 的File > Open打开根目录,结果页面打不开、依赖飘红,原因是 IDEA 不读.mymetadata,不知道 Web 根目录是WebRoot而不是WebContent。
| 文件/目录 | 在工程里的实际作用 |
|---|---|
| .project / .classpath | Eclipse 的工程描述和 Java 构建路径 |
| .mymetadata | MyEclipse 部署信息,记录 Web 根目录和上下文名 |
| src | Java 源码,通常放 Servlet、DAO、数据库工具类 |
| WebRoot/xxxCurve.jsp | 电力曲线展示页,如 gjtCurve、lcCurve、blzCurve 等 |
| WebRoot/META-INF | 应用元数据,老工程常在这里做数据源描述 |
| WebRoot/WEB-INF | 放 web.xml、lib 和页面模板 |
| .settings/org.hibernate.eclipse.console.prefs | IDE 里残留下来的 Hibernate 控制台配置 |
这个结构里WebRoot相当于现代工程的src/main/webapp。压缩包首页的多个*Curve.jsp并不是独立系统,而是不同监测量在不同设备或者不同画面下的曲线入口。换句话说,CurveJsp.jsp更像是所有曲线页面的公共入口模板,业务上通过 URL 参数决定读取哪个点位。
注意一个小陷阱:.settings目录里的 Hibernate 配置只说明开发者曾在 IDE 里试过 Hibernate 工具,不说明全项目都走了 ORM。很多老电力后台的实际查询是 JDBC 直连,曲线页面为了速度宁愿手动拼 SQL。看代码时不要被.settings带偏判断。
2.2 环境搭配:JDK、Tomcat、PostgreSQL 的版本别太新
这种工程大多诞生在 JDK 1.6/1.7 时代,用的是 JSP + Servlet + JDBC + PostgreSQL + Proxool。我的建议是先按老环境搭,不是不能用新版本,而是不值得花时间调兼容性。实际部署时常用搭配:
| 组件 | 建议版本 | 选型说明 |
|---|---|---|
| JDK | 1.7 | 老代码没有模块化,高版本 JDK 可能遇到非法反射访问 |
| Tomcat | 7.0.x | Servlet 3.0,JSP 编译兼容性最稳 |
| PostgreSQL | 9.2 或 9.4 | 和旧版 PostgreSQL 驱动行为一致 |
| IDE | Eclipse JEE + WTP | 识别 .mymetadata 最省事,IDEA 需手工配 Web 目录 |
| 数据库驱动 | postgresql-9.2/9.4 对应 jar | 要和数据库小版本匹配,太新的驱动有时不认老库配置 |
这套工程不是 Maven,依赖 jar 全在WebRoot/WEB-INF/lib。解压之后先检查这个目录,看看postgresql.jar和proxool.jar是否还在。曾经有不止一个项目因为 SVN 提交时把 lib 目录忽略了,导致代码拿到手后所有 JDBC 类全部报ClassNotFoundException,环境检查的第一步永远是确认 jar 存在。
2.3 数据库准备:建库、建用户、调权限
下面是 PostgreSQL 建库命令,按这个过程走能少踩很多权限坑:
sudo -u postgres psql -c "CREATE USER scada WITH PASSWORD 'scada';" sudo -u postgres psql -c "CREATE DATABASE power_scada OWNER scada ENCODING 'UTF8';"第一行创建和工程连接串同名的用户,第二行建库并把 owner 给这个用户。旧工程里连接串常写在proxool.xml或src下的配置文件里,用户名、库名、密码必须三者一致。PostgreSQL 默认认证常见为md5或scram-sha-256,遇到连接报错时优先看pg_hba.conf,确认本地连接用的是md5,否则密码校验会在底层失败,表现却是“连接超时”或“password authentication failed”。
建完库后用psql -U scada -h localhost -d power_scada -W试连一次,能进入命令行再往下走。这个检查只要几十秒,却能排除掉一半的“数据库连接池初始化失败”。如果项目里有初始化 SQL 脚本,记得一并执行,光有库没有表,后面页面能开但曲线是空的。
2.4 导入工程与部署启动:两种 IDE 的操作路径
Eclipse/MyEclipse 的常规流程:
File > Import > Existing Projects into Workspace,选中解压目录。- 如果项目不被识别,右键工程名,选择 MyEclipse 的
Convert to Web Project。 - 在
Window > Preferences > Server > Runtime Environments里添加 Tomcat 7。 - 右键工程
Run As > Run on Server,选择 Tomcat 7。
IDEA 的流程略有区别:
File > Open打开工程根目录。- 在
Project Structure > Modules里把src标记为 Sources,把WebRoot标记为 Web Resource Directory。 - 在 Web 模块里把 Web 根目录设为
WebRoot,并把 Deployment 的 Application context 设置为/SCADAHMI。 - 添加 Tomcat 配置后启动。
部署后看日志是默认动作:
tail -f /opt/tomcat/logs/catalina.out日志里出现Deploying web application archive和Server startup in xxx ms,说明工程已经启动。此时用curl -I快速验证:
curl -I "http://localhost:8080/SCADAHMI/CurveJsp.jsp"如果返回 200,说明 JSP 被编译并响应;如果 404,先查上下文名;如果 500,打开异常栈,多半是数据库连接串或连接池配置问题。第一次验证不要直接看曲线,先确认“Web 容器能编译 JSP”,这是最小闭环。别省略这一步,数据库配置错误密密麻麻的时候,先把“页面能跑”和“数据能取到”两件事切分开,排查会快很多。
3. Proxool 连接池到实时曲线:监控后台里最值得读的一段链路
3.1 SCADA 数据为什么要连接池:高并发写库不是靠多开连接解决
电力监控后台的压力模型很特殊:测点数量多、采集周期短,每分钟可能成百上千条记录要写库。如果用裸 JDBC 每次都新建Connection,PostgreSQL 需要建进程、做认证,压力一大,连接建立本身就成了瓶颈。连接池的做法是预先把若干物理连接准备好,业务线程反复借用和归还。Proxool 是早期 Java 生态里很简单的一种,配置在 XML 里,运行时给出jdbc:proxool.别名的 URL,易读、易排错。
Proxool 为什么适合这种旧工程:它不依赖 Spring 容器,可以脱离框架单独使用;配置项非常直接,适合没人长期维护的小团队。反过来讲,它的监控和 SQL 统计能力比 HikariCP 差不少,所以今天新写的系统不会再用它。但在 SCADAHMI 这个源码包里,Proxool 就是监控后台连接 PostgreSQL 的桥梁,理解它的配置方式,几乎等于理解了整个后台的数据通道。
3.2 Proxool 配置与 JDBC 代码:别名和连接串必须对上
在WEB-INF或者工程配置目录下,通常能找到proxool.xml。常见写法如下:
<proxool> <alias>power</alias> <driver-url>jdbc:postgresql://localhost:5432/power_scada</driver-url> <driver-class>org.postgresql.Driver</driver-class> <driver-properties> <property name="user" value="scada"/> <property name="password" value="scada"/> </driver-properties> <maximum-connection-count>20</maximum-connection-count> <minimum-connection-count>2</minimum-connection-count> <house-keeping-sleep-time>30000</house-keeping-sleep-time> <simultaneous-build-throttle>5</simultaneous-build-throttle> </proxool>几个参数值得说明:alias决定应用层连接 URL 中的名字;driver-url才是 PostgreSQL 的真实 JDBC 地址;maximum-connection-count是池的最大连接数,不设置默认可能过高;house-keeping-sleep-time是 Proxool 后台保洁线程的检查间隔,单位毫秒,设太短会让 CPU 频繁扫描池。simultaneous-build-throttle控制连接池同时建立连接的线程数,设太大,数据库刚起来时会突然涌出一堆连接。
对应 Java 代码里取连接的方式:
Class.forName("org.logicalcobwebs.proxool.ProxoolDriver"); Connection conn = DriverManager.getConnection("jdbc:proxool.power");这里jdbc:proxool.power中的power必须和配置里的<alias>power</alias>一致。很多人在这行卡住,把别名写成数据库名,启动时直接报No such alias。别问我是怎么知道的,这种错误藏得深,日志里只写“无法找到别名”,不告诉你是谁写错了。
3.3 曲线页面读库:一个可复用的 DAO 查询片段
曲线画面最底层的操作是把采样值按时间取出来。最近 100 条采样值并拼接文本的示例:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; public class CurveDao { public String getLatestSeries(int pointId) { String sql = "SELECT record_time, value FROM scada_sample " + "WHERE point_id = ? ORDER BY record_time DESC LIMIT 100"; StringBuilder sb = new StringBuilder(); Connection conn = null; try { Class.forName("org.logicalcobwebs.proxool.ProxoolDriver"); conn = DriverManager.getConnection("jdbc:proxool.power"); try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, pointId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { sb.append(rs.getTimestamp("record_time")) .append(",") .append(rs.getDouble("value")) .append("\n"); } } } } catch (Exception e) { throw new RuntimeException("查询监控曲线数据失败", e); } finally { if (conn != null) { try { conn.close(); } catch (Exception ignore) {} } } return sb.toString(); } }说明:LIMIT 100控制返回条数,ORDER BY record_time DESC保证取的是最新数据。拼接成逗号分隔文本是给页面绘图用的,老 JSP 页面通常直接用这个字符串初始化 Applet 或简单前端图表。conn.close()在 Proxool 里并非物理关闭,而是把连接归还到池,所以 finally 里的写法是安全的。
提示:这里的连接用户和密码只是示例值。真实部署时建议单独建一个只读账号给后台查询用,不要把 PostgreSQL 的超级用户写进 proxool.xml,否则一旦 Web 应用被上传了恶意 JSP,等于把数据库权限直接送出去。
3.4 JSP 侧的参数调整:点号、时间窗口和刷新周期
工程里的gjtCurve.jsp、lcCurve.jsp、wjjCurve.jsp、blzCurve.jsp等页面,虽然名字不同,内部结构几乎一致。JSP 从 URL 参数里拿pid,查库,再输出一段包含图表的 HTML。典型片段:
<% String pointId = request.getParameter("pid"); CurveDao dao = new CurveDao(); String series = dao.getLatestSeries(pointId == null ? 101 : Integer.parseInt(pointId)); %> <applet code="cn.curve.CurveApplet.class" width="900" height="400"> <param name="data" value="<%= series %>"> <param name="refreshInterval" value="2000"> </applet>这里几个参数在实际现场最常被改:
| 参数 | 作用 | 经验值 |
|---|---|---|
| pid | 点位 ID,决定画哪条曲线 | 按点位表配置 |
| limit | 采样条数,影响曲线长度 | 100 到 500 之间 |
| refreshInterval | 刷新毫秒数,影响实时性 | 2000 到 5000 |
| 数据格式 | 逗号分隔文本或 JSON | 和前端 Applet 解析逻辑必须一致 |
刷新太短会造成频繁查询,太长看不清实时;data直接塞进 HTML 的做法在数据量大时会让页面体积膨胀,一次传几千条点会明显卡顿。这个设计很脆弱,但确实是那个年代常见做法。
4. 监控连接系统与 HMI 通信:从现场 PLC 到浏览器曲线的完整通道
4.1 SCADA 如何与 PLC 连接:先分清“采集链路”和“展示链路”
监控连接系统这个关键词,在电力后台里指的是“后台服务怎么和设备层通信”。很多人以为 SCADA 系统直接用 JDBC 连 PLC,这是不对的。常见套路是三层:现场 PLC/RTU 提供 Modbus TCP 或 IEC 60870-5-104 接口;前置机或采集服务读规约,把通信号映射成点位值;点位值写入 PostgreSQL,Web 后台再从库里取数展示。厂里的中控 SCADA 系统基本都是这个链路,差别只是规约和点表。
所以“SCADA 如何与 PLC 连接”的答案不是一句“网线连通就行”,而是要先确定通信规约。Modbus TCP 最常见的操作是读保持寄存器,请求里要带事务标识、协议标识、长度、单元标识、功能码、起始地址和数据长度。这些术语看起来繁琐,却是真正“监控连接系统”的核心。看懂这个链路后再回头看 SCADAHMI,就会发现压缩包里那些类名和 JSP 页面,处于“采集完之后的数据处理与展示段”,不是直接跟 PLC 拽数据的那段。
4.2 URLApplet 的定位:网络化 HMI 的历史组件
压缩包里的URLApplet值得单独说。Applet 是嵌在网页里的小 Java 程序,URLApplet 的典型做法是去读取一个 URL,拿到服务端返回的实时数据,再在浏览器里画曲线或刷状态灯。在还没有 WebSocket 的年代,它解决了“浏览器页面轮询数据并且不整页刷新”的需求。
HTML 嵌入片段如下:
<applet code="cn.scada.urlapplet.URLApplet.class" archive="SCADAHMI.jar" width="800" height="300"> <param name="dataUrl" value="/SCADAHMI/servlet/RealDataServlet?pointId=101"> <param name="refresh" value="2000"> </applet>dataUrl就是要轮询的地址,refresh是轮询间隔。实际项目中dataUrl会指向一个 Servlet,Servlet 内部查 PostgreSQL 再返回文本或 XML。这样 Applet 不需要自己连数据库,安全边界是清晰的。你可以把这个 Applet 理解成“HTTP 长轮询前端”。
注意,现代浏览器不再支持 Java 插件,所以这套代码拿到今天更多是参考业务链路,而不是直接上线用。判断一个 SCADA 源码包是否值得学习,关键不在 Applet 还能不能用,而在于数据流是否完整:采集层、存储层、展示层是不是都给你留了修改入口。
4.3 点表和采样表:设备到画面的最小数据落点
为了让链路看得更清楚,通常需要一张点位表和一张采样表。
CREATE TABLE scada_point ( point_id INTEGER PRIMARY KEY, point_name VARCHAR(64) NOT NULL, unit VARCHAR(20) ); CREATE TABLE scada_sample ( id BIGSERIAL PRIMARY KEY, point_id INTEGER NOT NULL REFERENCES scada_point(point_id), record_time TIMESTAMP NOT NULL, value DOUBLE PRECISION NOT NULL ); CREATE INDEX idx_sample_time ON scada_sample(point_id, record_time DESC);点位表是静态台账,采样表是时序记录。idx_sample_time索引直接支撑“查最近 N 条曲线”的 SQL。采集服务拿到 PLC 数据后,先到点表里查这个点是否登记,再写采样表。JSP 页面读采样表,Applet 画曲线。这就是监控连接系统的完整闭环。理解了这个闭环再去看源码包里的文件,你会知道哪个文件负责数据访问、哪个负责页面展示、哪个只是历史遗留的过渡组件。
Modbus 和 IEC 104 的选型也影响后台设计。Modbus TCP 适合局部接线盒和单个设备,按寄存器地址读;IEC 104 适合变电站和调度侧,按遥测/遥信对象地址收发。许多电力后台会把两套规约同时部署在一个前置服务里,数据统一进 PostgreSQL,页面上并不关心数据来自哪种协议。这也是为什么 SCADAHMI 只关心数据库和展示,不关心底层 PLC 品牌的原因。
5. 避坑与排查:PostgreSQL、JSP 编码和连接池的五类翻车记录
5.1 现象:启动时直接报 ClassNotFoundException: org.postgresql.Driver
原因:WebRoot/WEB-INF/lib目录下的 PostgreSQL 驱动 jar 丢失。很多老工程通过 SVN 同步,.classpath里有 jar 名,但 jar 本身被忽略,换到新机器后依赖就断了。另外,不同 PostgreSQL 驱动的类名相同,但驱动版本和数据库版本不匹配时,会出现连接握手失败,报错却故意绕到 ClassNotFoundException。
解决:先把lib目录补全,确认存在postgresql-9.x.jar和proxool-x.x.jar。再检查 Tomcat 的lib目录有没有旧驱动残留,Tomcat 全局 lib 里的 PostgreSQL 驱动会和 Web 应用里的旧 jar 冲突,轻则告警,重则驱动类被重复加载。稳妥做法是只在WebRoot/WEB-INF/lib放一套驱动,Tomcat 全局目录不放数据库驱动。
5.2 现象:配置了 proxool.xml,连接池还是报初始化失败
原因分三类,很多人一上来就改最大连接数,结果问题不在这里。第一个原因是<alias>和代码里jdbc:proxool.xxx不一致;第二个原因是 PostgreSQL 的pg_hba.conf认证规则不认当前连接方式;第三个原因是连接串里的库名或端口写错,导致 Postgre 直接拒绝。
解决:按顺序排查。先执行psql -U scada -h localhost -d power_scada -W手动连一次,数据库层通了再看 Proxool。再把jdbc:proxool.power和<alias>power</alias>对齐。然后看启动日志里的原始异常,是连接池问题还是认证问题。很多“连不上数据库”的假象,其实只是密码里的特殊字符没有在 XML 中转义。
5.3 现象:JSP 页面能打开,但中文全部变成乱码
原因:老工程多数在 Eclipse 里以 GBK 编码编写 JSP,页面上却声明了 UTF-8,或者反过来。Tomcat 6/7 默认URIEncoding也不是 UTF-8,URL 里传中文参数时就会乱。PostgreSQL 数据库编码如果不是 UTF8,查询结果回到页面又会被再转一次,三方编码不一致,乱码就成了必然。
解决:统一编码。先确认每个 JSP 文件的物理编码,用file命令看:
file *.jsp如果是iso-8859或GBK开头,而页面pageEncoding写的是 UTF-8,就把文件转成 UTF-8:
iconv -f GBK -t UTF-8 gjtCurve.jsp > gjtCurve_utf8.jsp && mv gjtCurve_utf8.jsp gjtCurve.jsp同时把数据库编码固定为 UTF8,连接串里再加?characterEncoding=UTF-8。Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。这三处全部对齐,乱码问题才算根治。
5.4 现象:曲线页能打开,但点线不刷新,甚至一片空白
原因:最常见的是页面直接读缓存,没有请求后台。Applet 被浏览器拦截后,整个画布区域是空白的,看起来像“没有数据”。另一个原因是 JSP 的参数名不匹配,页面请求pid,后台代码读的却是pointId,返回默认点位,而默认点位在库里没有数据。
解决:先用浏览器开发者工具看网络面板,确认请求是否发出、状态码是多少。再打开日志看有没有 SQL 异常。然后把 JSP 里的request.getParameter("pid")和前端传入参数对齐。对于缓存问题,在 JSP 响应头里加:
<% response.setHeader("Cache-Control", "no-cache"); %>这行代码放到曲线页面顶部,能解决大部分“数据不刷新”的假象。
5.5 现象:Applet 在浏览器里被拦,曲线区域空白报 Java plug-in 不支持
原因:不是业务代码问题,而是运行环境问题。JDK 8 之后的版本不再提供浏览器 Java 插件,Chrome、Edge 和 Firefox 也彻底移除了 npapi 支持。这个源码包的 HMI 部分确实依赖 Applet 做前端绘制,拿现代浏览器直接开必然失败。
解决:临时验证用低版本 JDK 和 IE 模式,或者干脆把前端换成现代方案。最省事的替换是写一个 Servlet 返回 JSON,页面用 ECharts 或 uCharts 画曲线。业务链路不变,只把“Applet 拉数据”改成“JavaScript 拉数据”。这一步改完后,这套电力监控后台才能在新环境里继续用,而不是停在“能跑但只能老浏览器跑”的境地。
6. 一个验证技巧:用本地仿真数据源代替 PLC 快速打通全链路
接手这套源码时,我最怕的是现场 PLC 还没准备好,后台页面又是空白状态,根本分不清是前端问题、数据库问题还是连接池问题。从那以后,我每次都会在本地先起一个仿真采集器,用定时任务往scada_sample里写模拟数据,先把“采集到存储到展示”这条链路打通,再对接真实 PLC。
仿真采集器最简单的写法是用 Java 的Timer:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.util.Random; import java.util.Timer; import java.util.TimerTask; public class SimulatedCollector { public static void main(String[] args) throws Exception { Class.forName("org.logicalcobwebs.proxool.ProxoolDriver"); Timer timer = new Timer("SimulateCollector", true); timer.scheduleAtFixedRate(new TimerTask() { private final Random random = new Random(); private int seq = 0; @Override public void run() { String sql = "INSERT INTO scada_sample(point_id, record_time, value) " + "VALUES (101, now(), ?)"; try (Connection conn = DriverManager.getConnection("jdbc:proxool.power"); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setDouble(1, 220.0 + random.nextDouble() * 10); ps.executeUpdate(); if (++seq % 10 == 0) { System.out.println("已写入 " + seq + " 条仿真数据"); } } catch (Exception e) { e.printStackTrace(); } } }, 0, 2000); Thread.sleep(Long.MAX_VALUE); } }这段代码的作用是每两秒向scada_sample表插入一条电压仿真值,点位 ID 固定为 101。random.nextDouble() * 10让数据在 220 到 230 附近波动,接近真实电压读数。TimerTask里的try-with-resources自动关闭语句和连接,连接还回池,不会漏。
运行仿真采集器后,打开曲线页面,传参pid=101,观察曲线是否按两秒一次的节奏跳动。如果页面有数据但卡住,先看刷新参数;如果没有任何数据,先查scada_sample表:
psql -U scada -h localhost -d power_scada -c "SELECT count(*) FROM scada_sample;"表里有数据但页面空白,问题在 JSP 或 Applet;表里没有数据,问题在采集器或连接串。这一步把整个黑匣子拆成了两段,排查范围立刻缩小。这个习惯我一直保留着,现在接手任何带实时曲线的老系统,第一件事都是先塞一批仿真数据让页面动起来,再碰真实通信协议,少走了大量白屏焦虑的弯路。希望帮到你。
本文还有配套的精品资源,点击获取