Java Swing船舶管理系统:从JDBC到FTP上报的存量系统解析与升级实践
简介国内航运企业船舶管理系统是一套面向中小航运企业船舶管理场景的完整Java项目源码适用于正在学习企业级管理系统开发的学生或初级程序员。资源包共69个文件包含20个Java源文件、38个已编译的class文件、必要jar依赖及Excel数据文件可直观对照源码与编译产物理解项目结构。整包约19.46MB下载部署便捷。目前已有305人学习具有较高参考价值。系统覆盖船舶信息维护、航次调度与统计报表等典型业务模块界面采用Swing构建数据层整合MySQL并涉及FTP工具类能够帮助开发者快速搭建类似的航运信息管理平台值得深入阅读与改造。1. 航运企业的船舶管理系统为什么十年后还在用 Swing 桌面端多数人以为航运企业的信息系统早该全面 Web 化但真实情况是国内相当一部分航运公司的核心船舶管理系统至今还是十年前用 Java Swing 写的桌面端。这轮渡口、船队调度、机务科的机器上每天照常跑着靠离泊计划、船员名册、备件库存。这套系统不依赖浏览器也不依赖外网数据库直接落在公司局域网里的 MySQL 上初始化船队数据靠的是一张data.xls往总部传航次报表用的是edtftpj.jar这个老牌 FTP 库。它技术栈老但胜在场景匹配。这篇文章就把这套资源的完整链路拆开讲适合需要接手这类存量系统的 Java 工程师也适合准备做信息化改造的航运企业技术人员。2. BootMS1 启动链路与 Company.Boot.Ship 三层分包结构拿到压缩包后先不要急着找数据库脚本这个项目没有复杂的 Maven 依赖树它是一个典型的 Eclipse 老工程。先把它的静态结构看清楚再谈启动和改造。2.1 从 .project、.classpath 到 bin 目录一次典型的 Eclipse 老工程解压后你会看到.project、.settings/org.eclipse.jdt.core.prefs、bin、src、lib这些目录。.project是 Eclipse 的工程描述文件.classpath记录了编译和运行时的类路径src/Company/Boot/Ship是唯一源码根bin/Company/Boot/Ship是对应的编译输出目录。也就是说IDE 里直接以“Existing Projects into Workspace”方式导入Eclipse 就能通过.project恢复工程配置编译后自动输出到bin。这类工程在当年的标准做法是把第三方 jar 统一放在lib目录再在.classpath里逐个引用而不是交给 Maven 去中央仓库拉取。目录树大致是这样国内航运企业船舶管理系统.zip ├── BootMS1.java ├── .project ├── .classpath ├── .settings/org.eclipse.jdt.core.prefs ├── MainFrame.jpg ├── icon.png ├── data.xls ├── lib/ │ ├── mysql-connector-java-5.1.22-bin.jar │ ├── edtftpj.jar │ └── rt.jar ├── src/Company/Boot/Ship/ ├── bin/Company/Boot/Ship/ └── MainFrame.jpglib目录里的三个 jar 各有各的身份mysql-connector-java-5.1.22-bin.jar负责连 MySQL 5.x 数据库edtftpj.jar是 Enterprisedt 出品的 Java FTP 客户端用来往总部服务器传航次报表rt.jar则是老项目里非常典型的误配置。rt.jar是 JDK 自带的运行时类库正常情况根本不需要手动加进工程加了反而可能和当前 JRE 的类产生冲突。常见原因是当年开发机装的是自带 JRE 的 Eclipse有人为了让编译期看到javax.swing包而把它引了进来实际上正确做法是在 Eclipse 的 Installed JRE 里指定 JDK 版本而不是引rt.jar。这块到第 5 章还会细讲怎么清理。2.2 BootMS1 入口与 MainFrame 主窗体的启动时序BootMS1.java放在工程根目录从命名习惯看就是整个系统的启动类。Swing 应用有个硬性规则界面组件必须在事件分发线程EDT上创建否则界面会出现随机性闪烁、布局错乱甚至死锁。老项目里最常见的写法就是在main方法里通过SwingUtilities.invokeLater把主窗体的创建动作丢到 EDT 队列里主窗体类名对应这里的MainFrameimport javax.swing.SwingUtilities; public class BootMS1 { public static void main(String[] args) { // Swing 组件必须在 EDT 上创建invokeLater 负责把任务交给事件线程 SwingUtilities.invokeLater(new Runnable() { Override public void run() { // MainFrame 构造函数里会加载 MainFrame.jpg 背景图 MainFrame frame new MainFrame(); frame.setVisible(true); } }); } }这段代码是典型的老 Java 6 / Java 7 风格匿名内部类而不是 Lambda。逻辑上一共做了三件事把任务包装成RunnableinvokeLater将它异步提交到 EDT在 EDT 里实例化MainFrame并显示。frame.setVisible(true)放在最后因为 Swing 窗体在可见状态下才触发绘制事件在此之前构造函数里注册的监听器、加载的背景图、初始化的表格模型都已经准备完毕。如果你拿到源码后想改成 Java 8 的 Lambda 风格把内部类换成() - { ... }即可行为完全一致但要注意工程的 Java 编译级别要同步调高。2.3 Company.Boot.Ship 分包老项目的命名空间哲学Company是公司域名的反写或公司名缩写Boot对应启动引导层Ship是核心业务包。这种包结构在当年的桌面项目里很流行公司名保证唯一性Boot 层放入口类Ship 层放船舶档案、航次、报表等业务类。它没有像现在这样严格拆controller/service/dao而是按业务对象分包。bin和src目录结构完全一致说明构建方式就是 Eclipse 自动编译没有经过 Ant 或 Maven。接手这类工程时不要一上来就想改成 Spring Boot先顺着原有分包习惯跑通启动流程再逐步抽取公共逻辑否则一次改动过大容易把业务状态搞乱。3. 基于 MySQL 5.1.22 驱动的船舶数据访问层实现要点数据库访问层是这套系统的地基层。所有船舶档案、航次记录、备件台账都落在 MySQL 上而工程里那个mysql-connector-java-5.1.22-bin.jar直接决定了 JDBC 代码的写法。3.1 从驱动 jar 推断数据库访问风格这个 jar 是 2012 年前后的 MySQL JDBC 驱动。这个版本的驱动同时支持 JDBC 3.0 的Class.forName显式加载和 JDBC 4.0 的 SPI 自动注册但老项目普遍保留Class.forName因为在当时的开发环境里显式加载更可控也更容易排查“驱动缺失”的问题。连接工具类常见写法是这样package Company.Boot.Ship.db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbHelper { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://192.168.1.10:3306/shipping ?useUnicodetruecharacterEncodingutf8 autoReconnecttrue; private static Connection conn; public static Connection getConnection() throws SQLException { try { // 老驱动需显式加载新驱动即使省掉也会通过 SPI 自动注册 Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new SQLException(lib 目录缺少 mysql-connector-java-5.1.22-bin.jar, e); } if (conn null || conn.isClosed()) { conn DriverManager.getConnection(URL, ship_user, ship_pwd); } return conn; } }核心逻辑在getConnection方法里做了两层判断conn null处理首次调用conn.isClosed()处理 MySQL 端在wait_timeout超时后主动断开连接的情况。这里用了单连接缓存而不是每次新建连接是因为桌面客户端并发量低一个客户端同时只有一个主界面在操作单连接既省资源又能避免连接泄漏。192.168.1.10:3306是内网数据库地址shipping是库名ship_user和ship_pwd按实际环境替换。如果你接手后打算把它改成连接池需要特别注意isClosed()在这种写法里只能判断“本地 socket 是否关闭”遇到网络异常时它可能返回 false真正的探活要靠conn.isValid(3)配合SELECT 1完成。3.2 连接串里的关键参数与老驱动边界驱动 jar 版本老连接串反而简单但没配置好一样会踩坑。把这几个参数记清楚就够日常排查了。参数作用老驱动下的行为与坑useUnicodetrue启用 Unicode 编码传输不加必乱码是中文项目标配characterEncodingutf8指定客户端字符集必须与数据库端character_set_server一致autoReconnecttrue断线后自动重连只对“下次查询前的重连”有效正在执行的事务不会自动恢复useSSLfalse关闭 SSL 加密通道5.1.22 默认不强制加密但显式关闭后日志更干净也避免和总部局域网内网设备的 SSL 中间层冲突characterEncoding是这里面最容易出问题的。当年很多库在初始化时用utf8但表的排序规则是gbk_chinese_ci导致查询结果偶尔出现“问号”。排查时先用SHOW VARIABLES LIKE character%看服务端设置再和连接串对比两端统一成utf8最省心。autoReconnect不是万能药我一般会把它加上但同时要求业务层在捕获SQLException后重试一次查询而不是依赖驱动内部的自动重连逻辑。3.3 没有连接池的年代单连接的取舍这套系统里没有连接池框架这是当年的硬件和并发环境决定的。桌面端几十个用户同时在线每个客户端一个长连接对 MySQL 5.1 来说是完全可以承受的。但如果同一台机器上开了多个MainFrame实例或者你加入了多线程批量上报单连接就会成为瓶颈。遇到这种情况建议先给DbHelper加一个synchronized关键字锁住连接创建过程再用一个LinkedListConnection做简易空闲队列而不是立刻引入几十 MB 的 HikariCP。老系统稳定优先改动越少越容易回归验证。真正需要动连接层时再做升级也不迟。4. MainFrame 主窗体与船舶航次、FTP 上报模块的交互主窗体是整个系统的门面。MainFrame.jpg是主界面背景图icon.png是程序图标这两个图片文件直接参与了 Swing 界面构建。4.1 MainFrame 如何承载海事业务主界面用 Swing 做航运管理系统界面核心套路就是JFrame加BorderLayout中央放JTable北边放功能按钮和查询条件南边放状态栏。资源包里的MainFrame.jpg一般是整张界面背景图常见做法是重写JPanel.paintComponent把图片铺满再在上面叠放半透明的功能面板。icon.png则通过frame.setIconImage(Toolkit.getDefaultToolkit().getImage(icon.png))设置让 Windows 任务栏和标题栏显示企业标识。业务模块和 Swing 控件的对应关系大致如下业务模块主要控件数据表船舶档案管理JTable 搜索框t_ship航次记录JTable 选项卡t_voyage船员名册JTableJTextArea详情t_crewFTP 航次上报JButton 状态列t_report_log主窗体中央一般是一个JTabbedPane各业务页签共享同一个DbHelper连接。这里有个细节老系统界面上显示船舶状态在航、锚泊、修理往往是直接在表格单元格里写中文没有单独的状态字典表后期做统计报表时会很不方便。接手后如果要加“在航船舶数”这种统计优先考虑加一张字典映射而不是去解析中文字符串。4.2 edtftpj.jar 在航次上报中的用法edtftpj.jar是这套系统里负责“对外通信”的组件。航运企业的船队分布在沿海各港口总部需要每天汇总各船的航次动态、装载量、到港时间。由于早期卫星网络带宽有限用 FTP 传纯文本报表是最务实的选择。edtftpj 库的 API 很直接import com.enterprisedt.net.ftp.FTPClient; public class FtpUploader { public void uploadTripFile(String host, String user, String pwd, String localFile, String remoteFile) throws Exception { FTPClient ftp new FTPClient(); try { ftp.connect(host, 21); // 连接 FTP 控制端口 ftp.login(user, pwd); // 认证 ftp.type(FTPClient.TYPE_BINARY); // 二进制模式防止中文乱码 ftp.put(localFile, remoteFile); // 上传本地文件到远端目录 } finally { ftp.quit(); // 主动断开避免服务端挂起会话 } } }connect(host, 21)里的 21 是 FTP 默认控制端口type(FTPClient.TYPE_BINARY)是关键很多新手漏掉这行用默认 ASCII 模式传中文文本文件就会在服务器上看到“锟斤拷”。逻辑上这段代码做了五件事建立控制连接、登录、切二进制模式、上传、断开。值得注意的坑是ftp.put(localFile, remoteFile)的第二个参数是服务器上的相对路径它相对于用户登录后的根目录。如果总部服务器要求传到/report/2025/子目录这里要写成/report/2025/xxx.csv客户端本身没有目录概念。4.3 从 JTable 到 FTP航次数据上报链路实际的航次上报不是单独点一个按钮而是“选中航次 - 生成文件 - 上传 - 回写状态”的闭环。界面逻辑常见是这样// 主窗体中“上报选中航次”按钮的动作监听 private void doReportTrip(java.awt.event.ActionEvent evt) { int row tblTrip.getSelectedRow(); // 当前选中的表格行 if (row 0) { return; // 未选中直接返回 } String tripNo (String) tblTrip.getValueAt(row, 0); // 取航次号 File csv TripReportGenerator.exportByTripNo(tripNo); try { new FtpUploader().uploadTripFile( 202.98.12.34, report, ******, csv.getAbsolutePath(), /report/ tripNo .csv); markTripReported(tripNo); // 成功后把本地状态改为“已上报” } catch (Exception e) { showMessage(上报失败 e.getMessage()); } }这段代码先取选中行的航次号再调用TripReportGenerator把数据库里的航次记录导出成 CSV然后走 4.2 的 FTP 上传最后更新本地上报状态。第四个参数是本地临时文件路径第五个参数是远端路径。如果上传失败不能只弹窗提示最好在markTripReported失败时用红色标注状态列方便第二天跟踪。老系统一个常见的隐性问题是临时 CSV 文件没有清理机制时间久了会占用磁盘建议在finally块里检查文件是否存在并删除。5. data.xls 初始数据准备与中文编码、时区、rt.jar 的坑data.xls在资源包里容易被忽略但它恰恰是部署这套系统的第一道门槛。它里面通常是船队基础资料包括船名、IMO 号、总吨位、建造年份、船舶状态格式是 Excel 表格。5.1 data.xls 是给 DBA 还是给程序用先看事实工程lib目录里没有 Apache POI这就基本排除了“程序启动时读取 xls 初始化数据库”这种方案。没有 POIJava 侧很难直接解析 Excel 二进制格式。所以data.xls的真实定位是给 DBA 的“数据交付文档”部署时由 DBA 在 MySQL 里手工执行导入或另存为 CSV 后用 SQL 导入。最省事的做法是让 Excel 另存为 CSV再通过LOAD DATA LOCAL INFILE批量灌入-- 先将 data.xls 在 Excel 中另存为 data.csv字符集选 UTF-8 LOAD DATA LOCAL INFILE C:/shipping/data.csv INTO TABLE t_ship CHARACTER SET utf8 FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY IGNORE 1 LINES (imo_no, ship_name, owner_name, dwt, build_year, status);LOCAL INFILE表示文件在客户端本地路径FIELDS TERMINATED BY ,指定逗号作为字段分隔符OPTIONALLY ENCLOSED BY 处理带逗号的船名字段IGNORE 1 LINES跳过 CSV 第一行的列名。列清单最后的imo_no到status必须和 CSV 列顺序一致顺序反了会把船名灌进吨位字段。执行前先跑一条SELECT COUNT(*) FROM t_ship导入后数字对上了再覆盖正式库。DATA.xls文件名大小写在 Linux 环境敏感建议统一改成小写data.csv避免部署脚本找不到文件。5.2 中文乱码的三个检查点data.xls是中国船东的船船名里“长航”“中海”这类中文是常态乱码一出现基本绕不开三个位置。检查点排查手段期望结果MySQL 服务端字符集SHOW VARIABLES LIKE character%character_set_server为utf8JDBC 连接串检查DbHelper.URL含characterEncodingutf8导入文件编码Linux 执行file -bi data.csvcharsetutf-8若是iso-8859-1或gb2312需转换有一个高频坑Windows 上 Excel 另存为 CSV 时默认用 ANSI 编码中文 Windows 就是 GBK。拿这份 GBK 的 CSV 直接执行上面的CHARACTER SET utf8导入中文一定变乱码。我一般会先在 Linux 上跑file -bi data.csv确认编码是 GBK 就转一道iconv -f GBK -t UTF-8 data.csv data_utf8.csv mv data_utf8.csv data.csviconv的-f指定源编码-t指定目标编码。转换后打开文件看前几行确认“大连”“上海”这类中文正常了再执行 SQL。顺带说一句MySQL 5.1.22 时代的老库utf8并不是真正的四字节 UTF-8如果你的船名里有生僻字和 emoji老驱动是存不进去的这是utf8mb4出现的原因真遇到生僻字再考虑升级字符集船名场景里基本碰不到。5.3 lib 目录下 rt.jar 的隐患与正确清理方式rt.jar出现在lib目录是老 Eclipse 工程的经典“污点”。当年的开发者在编译时遇到javax.swing找不到直接把 JDK 里的rt.jar拷进了工程这么做短期内编译确实通过了但埋了雷JDK 升级后同一份类在扩展类加载器和应用类加载器各加载一次会报java.lang.SecurityException: Prohibited package name尤其在 Windows 多用户环境里切换 JDK 版本时特别容易出现。清理方式很简单从lib目录删除rt.jar打开 Eclipse 的Project Properties - Java Build Path - Libraries移除引用然后在Java Compiler里把编译级别设置成你实际安装的 JDK 版本比如 1.7 或 1.8。删除后重新编译只要代码里没有直接依赖某个 JDK 内部类立刻就能通过。6. 老系统不重构也能做的自检与迁移验证面对这套系统最忌讳的就是上来就重构。正确的顺序是先跑通最小闭环再做依赖管理改造最后才是驱动升级。6.1 先跑通“最小闭环”再谈后续改造最小闭环指不用完整界面写一个几行代码的入口验证最核心的三件事数据库连得上、主窗体打得开、FTP 能上传。FTP 这块可以用极简代码验证网络通不通import com.enterprisedt.net.ftp.FTPClient; public class FtpPing { public static void main(String[] args) throws Exception { FTPClient ftp new FTPClient(); ftp.connect(192.168.1.20); // 总部 FTP 内网地址 ftp.login(report, password); // 如果这里不抛异常说明控制连接、认证都正常 ftp.quit(); } }这段代码不做任何文件传输只验证地址可达和账号有效。运行前先ping 192.168.1.20再telnet 192.168.1.20 21确认端口通。如果 FTP 直连失败检查是否开启了被动模式ftp.pasv(true)很多内网防火墙只放行被动模式的数据端口这一步是排查重点。6.2 从 lib 管理到 Maven 的关键一步不重构但可以把依赖管理理清。老工程直接在.classpath写死 jar 路径换机器就报ClassNotFoundException。想逐步迁移到 Maven 但又不想改代码可以使用mvn install-file命令把本地 jar 安装进私有仓库保持现有import不变mvn install:install-file \ -Dfilelib/mysql-connector-java-5.1.22-bin.jar \ -DgroupIdmysql -DartifactIdmysql-connector-java \ -Dversion5.1.22 -Dpackagingjar mvn install:install-file \ -Dfilelib/edtftpj.jar \ -DgroupIdcom.enterprisedt -DartifactIdedtftpj \ -Dversion2.4.0 -Dpackagingjar-Dfile指向本地 jar 路径-DgroupId和-DartifactId是给依赖起一个坐标-Dversion必须和 jar 实际版本一致。装好后的依赖坐标就变成mysql:mysql-connector-java:5.1.22。这样做的价值在于把隐藏的lib依赖显性化后续升级驱动时只要改pom.xml里的版本号不必再手工替换 jar 文件。6.3 升级连接驱动的参数对比如果决定从 5.1.22 升级到 8.x 驱动连接方式和参数都变了建议先在测试环境做一个对比验证项目5.1.228.0.33Class.forName驱动类com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver时区配置不需要必须serverTimezoneAsia/ShanghaiSSL默认关闭默认开启需显式useSSLfalse字符编码characterEncodingutf8同样适用新驱动的连接串要写成这样String url jdbc:mysql://192.168.1.10:3306/shipping ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue;serverTimezone是 8.x 的强制项不配会直接抛Server time zone异常allowPublicKeyRetrievaltrue只有在useSSLfalse且使用caching_sha2_password插件时才需要。按 6.1 的最小闭环把新旧两套连接串各跑一遍对比同一张表的查询结果确认中文、日期、浮点字段都一致后再让主窗体切换新驱动。切换时保留旧 jar 不放行观察一个完整航次的上报流程出错就.classpath改回去这是老系统迁移的安全兜底。本文还有配套的精品资源点击获取