news 2026/8/29 2:48:06

Java Swing实战:从零构建个人财务管理系统(MVC+MySQL)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing实战:从零构建个人财务管理系统(MVC+MySQL)

简介:在软件开发领域,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 -versionjavac -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='收支流水表';

设计思考

  1. 金额正负:用正负值直接区分收支,简化了业务逻辑。查询时,SUM(amount)即可得到净收入。
  2. 时间字段transaction_time是业务发生时间,created_time是系统记录时间,两者区分开更合理。
  3. 外键约束:使用FOREIGN KEYON DELETE RESTRICT确保数据完整性。例如,如果某账户下还有流水,则不允许删除该账户。
  4. 附件存储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,切忌用doublefloat,否则在财务计算中会出现精度误差。

数据库工具类(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利用泛型和反射,封装了executeQueryexecuteUpdate等通用方法,避免了大量重复的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的JFrameJDialog和各种组件构成。以“新增流水”对话框为例。

视图(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配合GridLayoutGridBagLayout,动态加载所有账户,显示账户名和实时余额。余额数据通过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(...)获取数据,再更新TransactionTableModeldata并调用fireTableDataChanged()刷新表格。

5.2 收支录入与数据校验

“新增流水”对话框(AddTransactionDialog)的实现有几个细节需要注意:

  1. 金额输入校验:在amountField上可以添加DocumentListener或焦点失去事件,实时校验输入是否为合法数字。更严格的做法是使用JFormattedTextField
  2. 分类下拉框优化:收支分类应分开显示。可以创建两个JComboBox,一个放收入分类,一个放支出分类,再通过一个JRadioButton组(“收入”/“支出”)来切换显示哪个下拉框。数据从CategoryService.getCategoriesByType(type)获取。
  3. 日期时间选择:使用JSpinner绑定一个SpinnerDateModel是简单的方法。更友好的方式是使用第三方库如JDatePicker
  4. 数据提交的健壮性:在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

开发完成后,需要将项目打包成便于分发的形式。

  1. 导出可运行JAR:在Eclipse中,右键项目 ->Export -> Java -> Runnable JAR file
  2. 选择启动配置Launch configuration选择包含Main类的那个配置。
  3. 库处理方式:选择“Extract required libraries into generated JAR”。这会将所有依赖(如MySQL驱动、JFreeChart的JAR)解压并合并到最终的JAR文件中,生成一个“胖JAR”(Fat JAR),用户只需这一个文件即可运行。
  4. 处理外部资源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 性能与体验优化建议

  1. 数据库连接池:当前每次操作都新建连接,频繁操作下性能有损。可以引入如HikariCP这样的轻量级连接池。
  2. 列表分页:当流水记录上万条时,一次性加载到JTable会卡顿。需要实现分页查询,并在表格底部添加分页导航条。
  3. 数据缓存:账户列表、分类列表这些不常变的数据,可以在应用启动时加载到内存中缓存起来,避免频繁查询数据库。
  4. 异步操作:备份/恢复数据库、生成复杂报表等耗时操作,应该在单独的线程(如SwingWorker)中执行,避免阻塞事件调度线程(EDT),导致界面卡死。
  5. 界面美化:可以使用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线程等核心概念的绝佳案例。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/29 2:43:31

COM-HPC高端变体发布:嵌入式模块电脑算力跃升至服务器级

做嵌入式的朋友应该都有这种感觉&#xff1a;前两年大家还在争COM Express能不能上PCIe Gen4&#xff0c;今年已经没人问这个问题了。边缘AI、机器视觉、实时数据融合这些负载&#xff0c;把模块电脑的算力要求一下子拉到了一个此前只有服务器才需要面对的水平。带宽不够、内存…

作者头像 李华
网站建设 2026/8/29 2:42:15

STM32通用定时器精准控制GPIO开关:从原理到实战

1. 项目概述&#xff1a;为什么用定时器控制开关&#xff1f;在嵌入式开发里&#xff0c;控制一个开关&#xff08;比如继电器、LED、MOS管&#xff09;听起来是最基础的操作&#xff0c;直接拉高拉低一个GPIO引脚不就行了&#xff1f;确实&#xff0c;对于简单的开关控制&…

作者头像 李华
网站建设 2026/8/29 2:41:06

Transformer与CKF融合的锂电池SOC高精度估计系统

简介&#xff1a;锂电池荷电状态&#xff08;SOC&#xff09;估算一直是电池管理系统&#xff08;BMS&#xff09;中的核心难题&#xff1a;电池本身是强非线性、时变且与温度强相关的电化学系统&#xff0c;传统安时积分法存在累积误差&#xff0c;扩展卡尔曼滤波在大非线性场…

作者头像 李华
网站建设 2026/8/29 2:38:55

本地SLM内存设计:从OOM崩溃到稳定运行的工程实践

在本地跑小型语言模型&#xff08;SLM&#xff09;&#xff0c;刚开始对话还算流畅。多开几个会话之后&#xff0c;系统日志开始出现 out of memory&#xff0c;随后进程退出。如果再碰上浏览器、编译器和调试工具同时开着&#xff0c;几乎会回到“什么都不敢多开”的状态。很多…

作者头像 李华
网站建设 2026/8/29 2:31:21

BFS算法与最小步数模型:从迷宫寻路到状态空间搜索

1. 从棋盘到迷宫&#xff1a;为什么“最小步数”是搜索算法的灵魂如果你刷过一些算法题&#xff0c;或者玩过像《华容道》、推箱子这类经典益智游戏&#xff0c;一定会对一个概念印象深刻&#xff1a;从起点到终点&#xff0c;最少需要多少步&#xff1f;这个看似简单的问题&am…

作者头像 李华
网站建设 2026/8/29 2:28:15

数学建模竞赛中BP与CNN的PyTorch实战:从模型选型到代码落地

1. 从数学建模到代码落地&#xff1a;一次真实的解题复盘2021年的研究生数学建模D题&#xff0c;题目本身已经记不太清了&#xff0c;但那种从拿到赛题、分析需求、到最终用代码实现模型的完整过程&#xff0c;至今记忆犹新。当时我们队伍的核心策略&#xff0c;就是围绕题目中…

作者头像 李华