简介:在软件开发领域,CS(客户端-服务器)架构是构建桌面应用的基础模式,它将用户界面与业务逻辑、数据存储分离,提升了应用的可维护性和可扩展性。其核心原理在于客户端负责交互展示,服务器端(如数据库)处理数据持久化,两者通过网络或本地连接通信。这种架构的技术价值在于结构清晰、数据集中管理,非常适合需要复杂业务逻辑和可靠数据存储的桌面工具。在应用场景上,从企业ERP到个人工具软件广泛采用。本文聚焦于使用经典的Java技术栈实现一个CS架构的个人财务管理系统,其中通过JDBC进行数据库操作,并严格遵循MVC模式进行代码组织,确保事务一致性,为Java桌面开发提供了一个完整的工程实践范例。
1. 项目缘起与核心价值
最近在整理个人财务时,发现市面上的记账软件要么功能过于复杂,要么数据隐私让人不放心。作为一个有十多年Java开发经验的老兵,我决定自己动手,用最经典的Java Swing技术栈,从零搭建一个CS架构的个人财务收支管理系统。这个项目不仅解决了我的实际需求,更是一次对Java桌面应用开发核心技术的完整回顾与实践。如果你正在学习Java,或者想了解如何将Swing、JDBC、MVC这些教科书上的概念串联成一个可运行、可维护的实战项目,那么这篇记录将为你提供一个清晰的路线图。整个项目基于Eclipse IDE开发,使用Mysql作为数据存储,最终形成一个结构清晰、功能完整的CS结构桌面应用。接下来,我将从技术选型、环境搭建、核心模块实现到打包部署,为你完整拆解这个项目的每一个细节。
2. 技术栈选型与项目环境搭建
2.1 为什么选择Java Swing + MySQL + CS架构?
在开始敲代码之前,明确技术选型的理由至关重要。我选择这套组合,是基于以下几个核心考量:
首先,Java Swing作为Java SE的一部分,无需额外依赖任何第三方框架,就能构建出跨平台的图形界面。对于个人财务管理这种对界面复杂度要求不高,但需要稳定、可移植的应用来说,Swing是绝佳的选择。它内置了丰富的组件(JFrame, JTable, JButton等),事件驱动模型成熟,学习曲线相对平缓。虽然其界面风格略显“复古”,但通过合理的布局管理(如BorderLayout, GridBagLayout)和少量自定义渲染,完全可以做出清爽、实用的界面。
其次,数据存储选择了MySQL。个人财务数据虽然量不大,但结构规整,涉及账户、分类、流水等多张表的关系。MySQL作为最流行的开源关系型数据库之一,安装简单,社区资源丰富,JDBC驱动稳定。对于桌面应用,通常使用Connector/J驱动进行连接,其PreparedStatement能有效防止SQL注入,确保数据操作的安全。
最后,采用经典的CS(Client/Server)结构。这里的“Server”指的是数据库服务器,应用本身是客户端。这种结构将数据集中存储,便于备份和管理。虽然也有考虑过使用嵌入式数据库(如H2、SQLite)来简化部署,但考虑到未来数据可能需要在多台设备间同步(尽管本项目未实现),以及保持与常见Web技术栈(数据服务化)的潜在兼容性,独立的MySQL服务是更稳妥的选择。
2.2 开发环境详细配置指南
一个顺畅的开发环境是高效编码的基础。以下是每一步的详细操作和避坑点。
1. Java开发环境配置:确保你的系统已安装JDK 8或更高版本(推荐JDK 11 LTS)。打开命令行,输入java -version和javac -version验证。如果未安装,需从Oracle官网或AdoptOpenJDK等渠道下载安装。
注意:安装后务必配置
JAVA_HOME环境变量,并将其bin目录添加到PATH中。这是很多初学者容易忽略的一步,不配置会导致Eclipse或后续Maven等工具找不到正确的JDK。
2. Eclipse IDE安装与优化:从Eclipse官网下载“Eclipse IDE for Enterprise Java and Developers”版本,它包含了Java SE和Web开发所需的插件。解压即可运行。 安装后,建议进行以下设置以提升Swing开发体验:
- 调整编码:进入
Window -> Preferences -> General -> Workspace,将“Text file encoding”设置为UTF-8,避免中文乱码。 - 安装WindowBuilder插件(可选但强烈推荐):这是一个可视化的Swing/AWT设计器。通过
Help -> Eclipse Marketplace,搜索“WindowBuilder”安装。安装后,新建JFrame类时,可以切换到“Design”视图进行拖拽式布局,极大提高界面开发效率,尤其适合初学者理解布局管理器。 - 配置JRE:在
Window -> Preferences -> Java -> Installed JREs中,确保指向你安装的JDK目录,而不是JRE。
3. MySQL数据库安装与初始化:从MySQL官网下载社区版安装包。安装过程中,记住你设置的root用户密码。 安装完成后,使用命令行或MySQL Workbench图形工具创建本项目的数据库和用户。
-- 创建数据库 CREATE DATABASE personal_finance_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户(避免使用root) CREATE USER 'finance_user'@'localhost' IDENTIFIED BY 'YourSecurePassword123'; -- 授予权限 GRANT ALL PRIVILEGES ON personal_finance_db.* TO 'finance_user'@'localhost'; FLUSH PRIVILEGES;注意:生产环境务必使用强密码,且权限应根据最小权限原则分配。这里为了演示简化了权限。
4. 项目创建与依赖管理:在Eclipse中新建一个“Java Project”。我建议不使用Maven等构建工具来管理这个纯Swing项目,以保持极简。只需将MySQL的JDBC驱动JAR包(mysql-connector-java-8.0.xx.jar)添加到项目的构建路径(Build Path)中即可。右键项目 ->Build Path -> Configure Build Path -> Libraries -> Add External JARs...,选择下载好的驱动JAR包。
3. 数据库设计与核心表结构
数据库设计是应用的基石,良好的设计能保证数据一致性并简化业务逻辑。根据个人财务管理的核心需求,我设计了以下几张表。
3.1 实体关系分析与表结构定义
账户表 (account):记录资金存放的载体,如现金、支付宝、微信、银行卡等。
CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账户ID', name VARCHAR(50) NOT NULL COMMENT '账户名称(如:招商银行储蓄卡)', type VARCHAR(20) COMMENT '账户类型(现金、银行卡、电子支付等)', balance DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '当前余额', remarks TEXT COMMENT '备注', created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';设计思考:balance(余额)字段的设计存在争议。一种做法是不存储,每次通过统计流水计算得出;另一种是存储,并在每次发生流水时更新。我选择了存储,因为实时计算所有流水在数据量大时可能影响性能。但这就要求在事务中同时更新流水表和账户表,确保数据一致性,这是实现上的一个关键点。
分类表 (category):对收支进行归类,如餐饮、交通、工资、投资等。
CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', name VARCHAR(50) NOT NULL UNIQUE COMMENT '分类名称', type TINYINT NOT NULL COMMENT '类型:1-收入,0-支出', parent_id INT DEFAULT NULL COMMENT '父级分类ID,用于多级分类', icon VARCHAR(100) COMMENT '图标标识(前端可用)', FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支分类表';设计思考:使用type字段区分收入与支出分类。parent_id实现了简单的树形结构,支持二级分类(如“支出-餐饮-早餐”)。UNIQUE约束确保分类名不重复。
收支流水表 (transaction):核心表,记录每一笔具体的财务活动。
CREATE TABLE transaction ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '流水ID', amount DECIMAL(12, 2) NOT NULL COMMENT '金额(正数为收入,负数为支出)', account_id INT NOT NULL COMMENT '关联账户ID', category_id INT NOT NULL COMMENT '关联分类ID', transaction_time DATETIME NOT NULL COMMENT '交易时间', description VARCHAR(200) COMMENT '描述/备注', attachment_path VARCHAR(500) COMMENT '附件(如票据照片)存储路径', created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE RESTRICT, FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支流水表';设计思考:
- 金额正负:用正负值直接区分收支,简化了业务逻辑。查询时,
SUM(amount)即可得到净收入。 - 时间字段:
transaction_time是业务发生时间,created_time是系统记录时间,两者区分开更合理。 - 外键约束:使用
FOREIGN KEY和ON DELETE RESTRICT确保数据完整性。例如,如果某账户下还有流水,则不允许删除该账户。 - 附件存储:
attachment_path只存储文件在服务器或本地的路径,而不是文件本身(BLOB)。这是更通用的做法,避免数据库膨胀。
3.2 初始化数据与索引优化
建表后,需要插入一些常用的初始数据,并考虑查询性能。
-- 插入初始分类 INSERT INTO category (name, type) VALUES ('工资收入', 1), ('兼职收入', 1), ('投资理财', 1); INSERT INTO category (name, type) VALUES ('餐饮美食', 0), ('交通出行', 0), ('服饰美容', 0), ('生活日用', 0); -- 为高频查询字段添加索引 CREATE INDEX idx_transaction_time ON transaction(transaction_time); CREATE INDEX idx_transaction_account ON transaction(account_id); CREATE INDEX idx_transaction_category ON transaction(category_id);索引能大幅提升按时间、账户或分类查询流水明细的速度。
4. 系统架构与MVC模式实现
对于桌面应用,清晰的代码结构是长期可维护的关键。我采用了经典的MVC(Model-View-Controller)模式进行架构。
4.1 项目包结构规划
在Eclipse的src目录下,我建立了如下包结构,这能让代码职责分明:
src/ ├── com.finance.app │ ├── Main.java // 程序入口 │ ├── controller/ // 控制器层 │ │ ├── MainFrameController.java │ │ ├── TransactionController.java │ │ └── ... │ ├── view/ // 视图层(Swing界面) │ │ ├── component/ // 自定义组件(如分类选择器) │ │ ├── dialog/ // 各种对话框 │ │ └── frame/ // 主窗口、列表窗口 │ ├── model/ // 模型层 │ │ ├── entity/ // 实体类(与表对应) │ │ │ ├── Account.java │ │ │ ├── Category.java │ │ │ └── Transaction.java │ │ ├── dao/ // 数据访问对象 │ │ │ ├── BaseDAO.java // 封装通用CRUD │ │ │ ├── AccountDAO.java │ │ │ └── ... │ │ └── service/ // 业务逻辑层 │ │ ├── AccountService.java │ │ └── TransactionService.java │ └── util/ // 工具类 │ ├── DBUtil.java // 数据库连接与关闭 │ ├── DateUtil.java // 日期处理 │ └── ... └── resources/ // 资源文件(图片、配置文件) └── db.properties // 数据库连接配置4.2 核心模型层(Model)实现
实体类(Entity):使用纯Java Bean,属性与数据库字段一一对应,并提供getter/setter方法。
// com.finance.app.model.entity.Transaction.java public class Transaction { private Integer id; private BigDecimal amount; // 使用BigDecimal处理金额,避免精度丢失 private Integer accountId; private Integer categoryId; private LocalDateTime transactionTime; // 使用Java 8的日期时间API private String description; // ... 省略其他属性和方法 }注意:金额一定要用
BigDecimal,切忌用double或float,否则在财务计算中会出现精度误差。
数据库工具类(DBUtil):封装连接获取与释放,使用Properties读取配置文件,实现连接池(这里简化版使用DriverManager)。
// com.finance.app.util.DBUtil.java public class DBUtil { private static String url; private static String user; private static String password; static { // 从resources/db.properties加载配置 // ... } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 重载的close方法,用于关闭Connection, Statement, ResultSet public static void close(Connection conn, Statement stmt, ResultSet rs) { // 注意关闭顺序:rs -> stmt -> conn // 并在每个close调用处进行try-catch,避免一个关闭异常影响后续关闭 } }数据访问对象(DAO):每个实体类对应一个DAO,封装所有数据库操作。这里以TransactionDAO为例展示核心的增删改查。
// com.finance.app.model.dao.TransactionDAO.java public class TransactionDAO extends BaseDAO<Transaction> { // 插入一笔流水,并更新对应账户余额(需在Service层用事务控制) public int insert(Transaction trans) throws SQLException { String sql = "INSERT INTO transaction(amount, account_id, category_id, transaction_time, description) VALUES(?,?,?,?,?)"; // 使用PreparedStatement防止SQL注入 return executeUpdate(sql, trans.getAmount(), trans.getAccountId(), ...); } // 复合条件查询流水(按时间范围、账户、分类) public List<Transaction> queryByCondition(LocalDate start, LocalDate end, Integer accountId, Integer categoryId) { StringBuilder sql = new StringBuilder("SELECT * FROM transaction WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (start != null) { sql.append(" AND transaction_time >= ?"); params.add(start.atStartOfDay()); } if (end != null) { sql.append(" AND transaction_time < ?"); // 注意使用<,避免包含结束日期的下一天 params.add(end.plusDays(1).atStartOfDay()); } // ... 拼接其他条件 return executeQuery(sql.toString(), params.toArray()); } }BaseDAO利用泛型和反射,封装了executeQuery和executeUpdate等通用方法,避免了大量重复的JDBC模板代码。
业务逻辑层(Service):DAO只负责数据存取,复杂的业务规则在Service层实现。这是MVC中“Model”的核心。
// com.finance.app.model.service.TransactionService.java public class TransactionService { private TransactionDAO transDao = new TransactionDAO(); private AccountDAO accountDao = new AccountDAO(); public boolean addTransaction(Transaction trans) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入流水记录 transDao.insert(conn, trans); // DAO方法需支持传入Connection以在同一个事务中 // 2. 更新账户余额 Account account = accountDao.getById(conn, trans.getAccountId()); BigDecimal newBalance = account.getBalance().add(trans.getAmount()); // 金额正负已体现收支 account.setBalance(newBalance); accountDao.updateBalance(conn, account); conn.commit(); // 提交事务 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } } }这是本项目最关键的代码之一。**务必使用事务(Transaction)**来保证“记录流水”和“更新余额”这两个操作要么同时成功,要么同时失败。否则,可能出现流水记了但余额没变,或者反之,导致数据不一致。
4.3 视图层(View)与控制器层(Controller)联动
视图层主要由Swing的JFrame、JDialog和各种组件构成。以“新增流水”对话框为例。
视图(View):AddTransactionDialog.java负责界面布局和组件初始化。
public class AddTransactionDialog extends JDialog { private JTextField amountField; private JComboBox<Account> accountCombo; private JComboBox<Category> categoryCombo; private JSpinner dateSpinner; private JTextArea descArea; private JButton submitBtn; private JButton cancelBtn; // 初始化界面,加载账户和分类数据到下拉框 public AddTransactionDialog(Frame owner) { super(owner, "新增收支", true); // ... 界面布局代码(可使用WindowBuilder辅助设计) loadAccountData(); // 从AccountService获取数据 loadCategoryData(); // 从CategoryService获取数据 initListeners(); } private void initListeners() { submitBtn.addActionListener(e -> onSubmit()); cancelBtn.addActionListener(e -> setVisible(false)); } private void onSubmit() { // 收集界面数据,组装成Transaction对象 Transaction trans = new Transaction(); try { trans.setAmount(new BigDecimal(amountField.getText().trim())); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(this, "金额格式错误!", "错误", JOptionPane.ERROR_MESSAGE); return; } trans.setAccountId(((Account)accountCombo.getSelectedItem()).getId()); // ... 设置其他字段 // 关键:调用控制器处理业务逻辑 boolean success = TransactionController.addTransaction(trans); if (success) { JOptionPane.showMessageDialog(this, "添加成功!"); setVisible(false); // 通知主界面刷新列表 MainFrameController.getInstance().refreshTransactionTable(); } else { JOptionPane.showMessageDialog(this, "添加失败,请检查数据!", "错误", JOptionPane.ERROR_MESSAGE); } } }控制器(Controller):TransactionController.java作为视图和模型的中介。
// com.finance.app.controller.TransactionController.java public class TransactionController { private static TransactionService transService = new TransactionService(); public static boolean addTransaction(Transaction trans) { // 进行数据校验(如金额非空、账户存在等) if (trans.getAmount() == null || trans.getAccountId() == null) { return false; } // 调用Service层执行业务逻辑 return transService.addTransaction(trans); } // 其他控制器方法,如查询、删除、修改 public static List<Transaction> queryTransactions(LocalDate start, LocalDate end, Integer accountId) { return transService.queryByCondition(start, end, accountId, null); } }通过这种模式,视图只负责展示和接收用户输入,控制器负责调度和简单校验,模型负责核心业务和数据。代码耦合度低,易于测试和维护。
5. 核心功能模块实现详解
5.1 主界面设计与数据展示
主界面采用经典的JFrame,包含菜单栏、工具栏、账户概览面板、流水列表和统计图表区域。
账户概览面板:使用JPanel配合GridLayout或GridBagLayout,动态加载所有账户,显示账户名和实时余额。余额数据通过AccountService.getAllAccounts()获取,并监听账户或流水的增删改事件,实现实时刷新。
流水列表:这是核心组件,使用JTable来展示。直接使用DefaultTableModel虽然简单,但不利于与实体对象绑定。更优的做法是自定义一个AbstractTableModel。
public class TransactionTableModel extends AbstractTableModel { private List<Transaction> data; private String[] columnNames = {"ID", "时间", "账户", "分类", "金额", "备注"}; @Override public int getRowCount() { return data == null ? 0 : data.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public Object getValueAt(int row, int col) { Transaction t = data.get(row); switch (col) { case 0: return t.getId(); case 1: return DateUtil.format(t.getTransactionTime()); case 2: return AccountService.getAccountName(t.getAccountId()); // 关联查询账户名 case 3: return CategoryService.getCategoryName(t.getCategoryId()); // 关联查询分类名 case 4: return t.getAmount(); case 5: return t.getDescription(); default: return null; } } @Override public String getColumnName(int column) { return columnNames[column]; } // 自定义金额列渲染器,让支出显示为红色,收入显示为绿色 @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 4) return BigDecimal.class; return Object.class; } }然后,在主界面中:
transactionTable = new JTable(); transactionTable.setModel(new TransactionTableModel()); // 设置自定义渲染器 transactionTable.getColumnModel().getColumn(4).setCellRenderer(new AmountCellRenderer());AmountCellRenderer继承DefaultTableCellRenderer,在getTableCellRendererComponent方法中根据数值正负设置前景色。
条件查询与刷新:在主界面顶部放置查询条件组件(如JDatePicker用于选择时间范围,JComboBox选择账户),点击“查询”按钮后,调用TransactionController.queryTransactions(...)获取数据,再更新TransactionTableModel的data并调用fireTableDataChanged()刷新表格。
5.2 收支录入与数据校验
“新增流水”对话框(AddTransactionDialog)的实现有几个细节需要注意:
- 金额输入校验:在
amountField上可以添加DocumentListener或焦点失去事件,实时校验输入是否为合法数字。更严格的做法是使用JFormattedTextField。 - 分类下拉框优化:收支分类应分开显示。可以创建两个
JComboBox,一个放收入分类,一个放支出分类,再通过一个JRadioButton组(“收入”/“支出”)来切换显示哪个下拉框。数据从CategoryService.getCategoriesByType(type)获取。 - 日期时间选择:使用
JSpinner绑定一个SpinnerDateModel是简单的方法。更友好的方式是使用第三方库如JDatePicker。 - 数据提交的健壮性:在
onSubmit方法中,除了格式校验,还应进行业务校验,例如支出时金额不能大于账户余额(这需要查询当前余额)。这部分逻辑可以放在TransactionService.addTransaction中。
5.3 统计报表与图表生成
统计功能是财务系统的灵魂。我实现了按日、月、年以及按分类的统计。
数据统计服务:在TransactionService中增加统计方法。
public Map<String, BigDecimal> getStatByCategory(LocalDate start, LocalDate end, int type) { // type: 0-支出,1-收入 String sql = "SELECT c.name, SUM(t.amount) as total FROM transaction t " + "JOIN category c ON t.category_id = c.id " + "WHERE t.transaction_time >= ? AND t.transaction_time < ? AND c.type = ? " + "GROUP BY c.id, c.name"; // 执行查询,返回Map<分类名,总额> } public Map<String, BigDecimal> getDailyStat(LocalDate start, LocalDate end) { // 按天统计,使用DATE()函数 String sql = "SELECT DATE(transaction_time) as day, SUM(amount) as total FROM transaction " + "WHERE transaction_time >= ? AND transaction_time < ? " + "GROUP BY DATE(transaction_time) ORDER BY day"; }图表展示:Java Swing本身没有高级图表库。我选择了轻量级的JFreeChart库。将其JAR包加入项目后,可以轻松生成饼图、柱状图等。
// 生成支出分类饼图 private JPanel createCategoryPieChart(Map<String, BigDecimal> data) { DefaultPieDataset dataset = new DefaultPieDataset(); for (Map.Entry<String, BigDecimal> entry : data.entrySet()) { dataset.setValue(entry.getKey(), entry.getValue()); } JFreeChart chart = ChartFactory.createPieChart( "支出分类占比", dataset, true, // 显示图例 true, // 显示提示 false ); ChartPanel chartPanel = new ChartPanel(chart); chartPanel.setPreferredSize(new Dimension(400, 300)); return chartPanel; }将ChartPanel嵌入主界面的一个JTabbedPane中,即可实现图表选项卡。
5.4 数据备份与恢复功能
桌面应用的数据安全很重要。我实现了简单的数据库备份与恢复功能。
备份:本质上是调用MySQL的mysqldump命令。
public boolean backupDatabase(String backupPath) { String command = String.format( "mysqldump -u%s -p%s --databases %s > %s", DB_USER, DB_PASSWORD, DB_NAME, backupPath ); try { Process process = Runtime.getRuntime().exec(command); int exitCode = process.waitFor(); return exitCode == 0; } catch (IOException | InterruptedException e) { e.printStackTrace(); return false; } }注意:将数据库密码写在命令行中存在安全风险。一种改进方法是使用配置文件,并在执行时从标准输入传递密码(
-p后不接密码),但实现稍复杂。对于个人应用,这是一个权衡。
恢复:调用mysql命令执行备份的SQL文件。
public boolean restoreDatabase(String backupFilePath) { // 恢复前,可以弹窗二次确认,因为这会覆盖现有数据。 String command = String.format( "mysql -u%s -p%s %s < %s", DB_USER, DB_PASSWORD, DB_NAME, backupFilePath ); // ... 执行逻辑与备份类似 }在界面上,添加“系统设置”菜单,下设“备份数据库”和“恢复数据库”选项,通过JFileChooser让用户选择备份文件路径。
6. 项目打包、部署与进阶优化
6.1 将项目打包为可执行JAR
开发完成后,需要将项目打包成便于分发的形式。
- 导出可运行JAR:在Eclipse中,右键项目 ->
Export -> Java -> Runnable JAR file。 - 选择启动配置:
Launch configuration选择包含Main类的那个配置。 - 库处理方式:选择“Extract required libraries into generated JAR”。这会将所有依赖(如MySQL驱动、JFreeChart的JAR)解压并合并到最终的JAR文件中,生成一个“胖JAR”(Fat JAR),用户只需这一个文件即可运行。
- 处理外部资源:
resources/目录下的配置文件(如db.properties)需要被打包进JAR。确保在代码中通过ClassLoader.getResourceAsStream()来读取它们,而不是使用new File()。InputStream is = getClass().getClassLoader().getResourceAsStream("db.properties"); Properties props = new Properties(); props.load(is);
常见打包问题:
- 找不到主类:确保
META-INF/MANIFEST.MF文件中正确指定了Main-Class。Eclipse导出时会自动生成。 - 数据库连接失败:检查打包后
db.properties文件是否在JAR根目录,以及其中的连接参数(尤其是密码)是否正确。 - 缺少本地库(Native Library):某些图表库或驱动可能需要本地库。JFreeChart是纯Java的,没问题。但如果你用了其他需要本地库的组件,需要额外处理。
6.2 安装程序制作与部署
对于更专业的发布,可以使用Launch4j将JAR包装成Windows的.exe文件,或者使用JPackage(JDK 14+)生成真正的原生安装包(如MSI、DMG)。这可以包含JRE,让用户无需预先安装Java。
6.3 性能与体验优化建议
- 数据库连接池:当前每次操作都新建连接,频繁操作下性能有损。可以引入如HikariCP这样的轻量级连接池。
- 列表分页:当流水记录上万条时,一次性加载到
JTable会卡顿。需要实现分页查询,并在表格底部添加分页导航条。 - 数据缓存:账户列表、分类列表这些不常变的数据,可以在应用启动时加载到内存中缓存起来,避免频繁查询数据库。
- 异步操作:备份/恢复数据库、生成复杂报表等耗时操作,应该在单独的线程(如
SwingWorker)中执行,避免阻塞事件调度线程(EDT),导致界面卡死。 - 界面美化:可以使用
LookAndFeel更换Swing默认皮肤,如FlatLaf库能提供现代化的扁平化界面。
6.4 遇到的典型问题与解决方案
问题一:Swing界面在数据加载时卡顿。原因:在事件调度线程(EDT)中执行了耗时的数据库操作。解决:使用SwingWorker。
SwingWorker<List<Transaction>, Void> worker = new SwingWorker<>() { @Override protected List<Transaction> doInBackground() throws Exception { // 在后台线程执行耗时查询 return TransactionController.queryTransactions(...); } @Override protected void done() { try { List<Transaction> result = get(); // 在EDT中更新UI updateTableModel(result); } catch (Exception e) { e.printStackTrace(); } } }; worker.execute();问题二:金额计算出现几分钱的误差。原因:使用了double类型进行财务计算。解决:全线改用BigDecimal,并且定义统一的精度和舍入模式(如RoundingMode.HALF_UP)。
问题三:窗口关闭后,数据库连接未释放,导致连接数耗尽。原因:程序退出时,连接池或未关闭的连接没有正确释放。解决:为主窗口添加WindowListener,在windowClosing事件中,调用一个统一的资源清理方法,关闭所有数据库连接池的连接。
这个个人财务管理系统项目,虽然技术栈传统,但涵盖了CS桌面应用开发的完整闭环:从需求分析、技术选型、数据库设计、分层架构、模块实现、到打包部署和问题排查。它不仅是Swing和JDBC的练习场,更是理解MVC、事务、异常处理、UI线程等核心概念的绝佳案例。
本文还有配套的精品资源,点击获取