恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java个人财务管理系统源码实战解析
首页
资讯中心
/
Java个人财务管理系统源码实战解析
Java个人财务管理系统源码实战解析
发布时间:2026/9/16 8:22:27
简介这是一份面向Java初学者与Web开发入门者的个人财务管理系统实战源码基于Spring BootMyBatisThymeleaf技术栈构建聚焦MVC架构实践与RESTful接口设计帮助开发者掌握用户管理、收支记账、预算控制、报表统计等核心业务模块的完整实现逻辑。资源包共32个文件含4个关键Java业务类如User.java、Account.java、9个MyBatis映射XML、10个编译后class文件及README.md等说明文档总大小仅37KB轻量易读适合快速导入IDE分析分层结构与SQL映射关系。已有1296人学习下载源码结构清晰包含完整用户登录鉴权、收支增删改查、余额自动计算、预算预警提醒等可运行功能同时涵盖JUnit单元测试、Docker部署配置及数据库索引与密码哈希等安全实践细节是理解Java Web全栈开发流程的优质教学参考项目。1. 为什么一个“Java个人财务管理系统”源码包比你刷十道Java八股文更能暴露真实工程能力很多人下载Java个人财务管理系统源码.zip后直接解压双击run.bat看到登录界面就以为“跑通了”。但真正拉开差距的从来不是能不能启动而是——当你要把“张三上月工资5800元、还房贷3200元、买菜花去642元”准确记入系统并在月底自动生成「收支趋势折线图分类饼图现金余额预警」时代码里哪几处设计决定了你改3行还是改300行这个源码包本质是一份带业务语义的Java工程快照它用Swing或JavaFX搭界面用JDBC连H2/SQLite做本地存储用SimpleDateFormat处理日期边界用BigDecimal规避金额计算精度丢失甚至在导出Excel时要绕开Apache POI对空单元格的默认填充逻辑。它不考你“HashMap扩容机制”但会逼你面对“用户连续点击‘添加支出’按钮5次却只生成1条有效记录”的并发写入问题它不问“JVM内存模型”但会让你在导出3年账单CSV时遭遇OutOfMemoryError: Java heap space。适合刚学完Java基础、能写冒泡排序但没做过完整模块的开发者也适合准备Java面试、想用真实项目补全“项目经验”话术的中级工程师——因为这里的每一处try-catch、每一个DAO层接口命名、每一条SQL的WHERE条件写法都是面试官可能追问“为什么这么设计”的弹药。2. 从解压到可运行本地环境搭建与核心模块依赖解析2.1 环境检查与JDK版本锁定策略该类系统普遍基于Java 8构建非Java 11原因在于Swing组件在高版本JDK中存在UI渲染兼容性问题且多数配套的报表库如JFreeChart未完全适配模块化系统。执行以下命令验证java -version javac -version提示若输出为java version 17.0.1或更高需单独安装JDK 8并配置临时环境变量。Windows下在CMD中执行set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_333 set PATH%JAVA_HOME%\bin;%PATH%Linux/macOS使用export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64。切勿全局切换JDK版本避免影响其他项目。2.2 源码结构逆向识别快速定位主入口与数据层解压后观察目录结构典型布局如下src/ ├── main/ │ ├── java/ │ │ └── com/example/finance/ # 包名常含finance/account/budget等关键词 │ │ ├── ui/ # Swing/JavaFX界面类MainForm.java, LoginFrame.java │ │ ├── dao/ # 数据访问对象AccountDAO.java, TransactionDAO.java │ │ ├── model/ # 实体类Transaction.java含amount(BigDecimal), date(LocalDate) │ │ ├── service/ # 业务逻辑FinanceService.java含addIncome/addExpense方法 │ │ └── util/ # 工具类DateUtil.java, NumberUtil.java │ └── resources/ │ ├── db/ # 内置数据库文件finance.db 或 h2.mv.db │ └── config.properties # JDBC连接配置urljdbc:h2:./db/finance;DB_CLOSE_ON_EXITFALSE关键识别点主类通常命名为MainApp.java、FinanceSystem.java或Start.java含public static void main(String[] args)方法若存在pom.xml说明使用Maven需执行mvn compile mvn exec:java -Dexec.mainClasscom.example.finance.MainApp若无pom.xml但存在build.xmlAnt或build.gradleGradle按对应工具构建。2.3 数据库初始化H2/SQLite内嵌库的静默建表逻辑该系统多采用H2数据库轻量、纯Java、支持内存模式其建表语句通常藏于DAO类的静态块或initDatabase()方法中。以TransactionDAO.java为例public class TransactionDAO { private static final String CREATE_TABLE_SQL CREATE TABLE IF NOT EXISTS transactions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, amount DECIMAL(12,2) NOT NULL, // 关键DECIMAL而非DOUBLE防精度丢失 category VARCHAR(50) NOT NULL, date DATE NOT NULL, type CHAR(1) CHECK(type IN (I,E)), // IIncome, EExpense note TEXT ); public void initDatabase() { try (Connection conn DriverManager.getConnection( jdbc:h2:./db/finance;DB_CLOSE_ON_EXITFALSE, sa, )) { try (Statement stmt conn.createStatement()) { stmt.execute(CREATE_TABLE_SQL); // 表不存在则创建存在则跳过 } } catch (SQLException e) { throw new RuntimeException(数据库初始化失败, e); } } }注意DB_CLOSE_ON_EXITFALSE参数至关重要——若省略程序退出时H2会自动关闭数据库连接导致下次启动时无法读取历史数据。此参数必须显式声明不可依赖默认值。2.4 运行前必改的3个配置项打开src/main/resources/config.properties路径可能为resources/db.properties修正以下字段配置项常见错误值正确值说明jdbc.urljdbc:h2:~/financejdbc:h2:./db/finance~指向用户目录易因权限问题失败./表示项目根目录相对路径稳定可靠jdbc.usernamerootsaH2默认用户名为sa非rootMySQL惯性思维易错app.startup.date2023-01-012024-01-01部分系统用此值初始化首笔账单日期若设为过去太久会导致首页图表显示空白修改后保存执行编译运行命令# Windows javac -encoding UTF-8 -cp .;lib/* src/main/java/com/example/finance/MainApp.java java -cp .;lib/* com.example.finance.MainApp# Linux/macOS javac -encoding UTF-8 -cp .:lib/* src/main/java/com/example/finance/MainApp.java java -cp .:lib/* com.example.finance.MainApp提示-encoding UTF-8防止中文注释或界面文字乱码lib/*表示引用所有jar包如h2-2.2.224.jar、jfreechart-1.5.3.jar若报ClassNotFoundException检查lib目录是否存在且jar包完整。3. 核心功能落地收支录入、分类统计与图表生成的代码级实现3.1 金额输入的防御性校验BigDecimal的正确用法链用户在界面输入“5800.5”时若直接用Double.parseDouble()转换后续计算可能产生5800.499999999999。正确路径是// UI层获取文本 String input amountTextField.getText().trim(); if (input.isEmpty()) { JOptionPane.showMessageDialog(this, 金额不能为空); return; } // 转换为BigDecimal关键用String构造禁用double构造 BigDecimal amount; try { amount new BigDecimal(input); // ✅ 正确字符串直接构造 } catch (NumberFormatException e) { JOptionPane.showMessageDialog(this, 请输入有效数字如5800.5); return; } // 业务层存入数据库前确保精度为2位小数 amount amount.setScale(2, RoundingMode.HALF_UP); // ✅ 强制保留2位四舍五入 // DAO层插入SQLPreparedStatement预编译防注入 String sql INSERT INTO transactions(amount, category, date, type, note) VALUES(?, ?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, amount); // ✅ 使用setBigDecimal非setDouble ps.setString(2, categoryComboBox.getSelectedItem().toString()); ps.setDate(3, Date.valueOf(datePicker.getDate())); // LocalDate → SQL Date ps.setString(4, transactionType); // I or E ps.setString(5, noteTextArea.getText()); ps.executeUpdate(); }注意setScale(2, RoundingMode.HALF_UP)是财务系统硬性要求HALF_UP表示传统四舍五入如1.235 → 1.24区别于HALF_EVEN银行家舍入。若省略此步用户输入100.005可能存为100.00造成账目差异。3.2 分类统计SQLGROUP BY与聚合函数的业务语义映射要生成「本月各分类支出总额」报表DAO层需执行public ListCategorySummary getMonthlyExpenseByCategory(int year, int month) { String sql SELECT category, SUM(amount) as total_amount, COUNT(*) as count FROM transactions WHERE type E // 限定支出类型 AND YEAR(date) ? AND MONTH(date) ? // 按年月过滤H2语法 GROUP BY category ORDER BY total_amount DESC; // 按金额降序方便前端展示 ListCategorySummary result new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, year); ps.setInt(2, month); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { CategorySummary summary new CategorySummary(); summary.setCategory(rs.getString(category)); summary.setTotalAmount(rs.getBigDecimal(total_amount)); // ✅ 用getBigDecimal接SUM summary.setCount(rs.getInt(count)); result.add(summary); } } } catch (SQLException e) { throw new RuntimeException(查询分类统计失败, e); } return result; }提示H2的YEAR()/MONTH()函数可直接提取日期字段无需DATE_FORMATMySQL或EXTRACTPostgreSQL。若需兼容其他数据库应改用date ? AND date ?区间查询传入2024-01-01和2024-02-01。3.3 JFreeChart图表生成从数据集合到PNG文件的完整流程CategorySummary列表需转为饼图关键步骤public void generateExpensePieChart(ListCategorySummary summaries, String outputPath) { // 1. 构建DefaultPieDataset自动处理null/zero值 DefaultPieDataset dataset new DefaultPieDataset(); for (CategorySummary s : summaries) { if (s.getTotalAmount().compareTo(BigDecimal.ZERO) 0) { // 过滤零值 dataset.setValue(s.getCategory(), s.getTotalAmount().doubleValue()); } } // 2. 创建JFreeChart对象标题、图例、工具提示 JFreeChart chart ChartFactory.createPieChart( 本月支出分类占比, // 图表标题 dataset, // 数据集 true, // 是否显示图例 true, // 是否生成工具提示 false // 是否生成URL ); // 3. 自定义渲染器解决中文标签乱码 PiePlot plot (PiePlot) chart.getPlot(); plot.setLabelFont(new Font(SimSun, Font.PLAIN, 12)); // Windows用宋体Linux/macOS用Noto Sans CJK SC plot.setNoDataMessage(暂无支出数据); // 4. 导出为PNG抗锯齿提升清晰度 try { ChartUtils.saveChartAsPNG( new File(outputPath), chart, 800, 600, // 宽高像素 null, // 图像缩放null表示1:1 true, // 启用抗锯齿 0.8f // 文本渲染质量 ); } catch (IOException e) { throw new RuntimeException(生成图表失败, e); } }注意ChartUtils.saveChartAsPNG是JFreeChart 1.5版本的推荐方式替代已废弃的ChartUtilities.saveChartAsPNG。若导出图片中文为方框需确认JVM是否加载了中文字体——在main()方法开头添加GraphicsEnvironment.getLocalGraphicsEnvironment() .registerFont(Font.createFont(Font.TRUETYPE_FONT, new File(simsum.ttc)));4. 生产级加固并发安全、数据持久化与导出功能的健壮性改造4.1 多线程下的账单添加synchronized块的精准作用域当用户快速点击“保存”按钮可能触发多个addTransaction()调用。若DAO方法未加锁会出现同一笔交易被插入两次主键冲突或忽略余额计算错乱A线程读余额1000→B线程读余额1000→A存入500→B存入300→最终余额1800而非1300。正确做法是在Service层对账户余额操作加锁而非DAO层public class FinanceService { private final Object balanceLock new Object(); // 锁对象私有化避免外部干扰 public void addIncome(Long accountId, BigDecimal amount, String note) { synchronized (balanceLock) { // ✅ 锁定余额更新逻辑非整个方法 BigDecimal currentBalance accountDAO.getBalance(accountId); BigDecimal newBalance currentBalance.add(amount); accountDAO.updateBalance(accountId, newBalance); transactionDAO.insertIncome(accountId, amount, note); } } public void addExpense(Long accountId, BigDecimal amount, String note) { synchronized (balanceLock) { BigDecimal currentBalance accountDAO.getBalance(accountId); if (currentBalance.compareTo(amount) 0) { throw new InsufficientBalanceException(余额不足); } BigDecimal newBalance currentBalance.subtract(amount); accountDAO.updateBalance(accountId, newBalance); transactionDAO.insertExpense(accountId, amount, note); } } }提示synchronized作用于实例方法会锁住整个对象影响其他无关操作如查询统计。此处用私有锁对象balanceLock仅保护余额相关代码段粒度更细。4.2 CSV导出的内存优化流式写入避免OOM导出3年账单约10万条记录时若先ListTransaction再StringBuilder.append()极易触发OutOfMemoryError。应采用BufferedWriter流式写入public void exportTransactionsToCsv(ListLong transactionIds, String outputPath) { try (BufferedWriter writer Files.newBufferedWriter(Paths.get(outputPath), StandardCharsets.UTF_8)) { // 写入CSV头 writer.write(ID,金额,分类,日期,类型,备注); writer.newLine(); // 分批查询并写入避免一次性加载全部数据 int batchSize 1000; for (int i 0; i transactionIds.size(); i batchSize) { int end Math.min(i batchSize, transactionIds.size()); ListLong batchIds transactionIds.subList(i, end); ListTransaction batch transactionDAO.findByIds(batchIds); for (Transaction t : batch) { String line String.format(%d,%s,%s,%s,%s,%s, t.getId(), t.getAmount().toPlainString(), // ✅ toPlainString()避免科学计数法 escapeCsvField(t.getCategory()), t.getDate().toString(), // LocalDate.toString() → 2024-01-15 t.getType().equals(I) ? 收入 : 支出, escapeCsvField(t.getNote()) ); writer.write(line); writer.newLine(); } } } catch (IOException e) { throw new RuntimeException(导出CSV失败, e); } } private String escapeCsvField(String field) { if (field null) return ; // CSV规范含逗号、换行、双引号的字段需用双引号包裹内部双引号转义为两个双引号 if (field.contains(,) || field.contains(\n) || field.contains(\)) { return \ field.replace(\, \\) \; } return field; }注意toPlainString()确保BigDecimal输出为100.00而非1E2escapeCsvField()处理CSV特殊字符否则Excel打开时字段会错位。4.3 数据库备份与恢复H2内置工具的自动化脚本H2提供Script命令导出SQL但需通过JDBC执行public void backupDatabase(String backupPath) { String sql SCRIPT TO backupPath DROP; // DROP选项会包含DROP TABLE语句 try (Connection conn DriverManager.getConnection( jdbc:h2:./db/finance;DB_CLOSE_ON_EXITFALSE, sa, ); Statement stmt conn.createStatement()) { stmt.execute(sql); } catch (SQLException e) { throw new RuntimeException(数据库备份失败, e); } } public void restoreDatabase(String backupPath) { // 先删除旧表 String dropSql DROP ALL OBJECTS DELETE FILES; // 再执行备份SQL需逐行读取backup.sql并执行 try (Connection conn getConnection(); Statement stmt conn.createStatement(); BufferedReader reader Files.newBufferedReader(Paths.get(backupPath))) { stmt.execute(dropSql); String line; while ((line reader.readLine()) ! null) { if (!line.trim().isEmpty() !line.startsWith(--)) { stmt.execute(line); } } } catch (SQLException | IOException e) { throw new RuntimeException(数据库恢复失败, e); } }提示SCRIPT TO生成的SQL文件可直接用文本编辑器查看便于审计数据变更DROP ALL OBJECTS DELETE FILES会清空数据库并删除.h2.db文件确保干净恢复。5. 面试高频考点穿透从源码中提炼Java基础、JDBC与设计模式的实战印证5.1 Java基础真题映射源码里的“八股文”现场还原面试题源码位置关键代码片段考察点String为什么不可变如何保证安全性Transaction.java的getNote()方法public String getNote() { return note; }note为private final String不可变对象天然线程安全避免外部篡改note内容若改为public String note;则破坏封装性ArrayList和LinkedList增删查性能差异ReportService.java的月度报表缓存private ListTransaction cache new ArrayList();查询频繁遍历统计选ArrayList若需在中间频繁插入如实时流水应改用LinkedListtry-with-resources为何能自动关闭资源TransactionDAO.java的queryAll()方法try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ... }编译后自动插入finally块调用close()即使发生异常也保证释放Connection/Statement5.2 JDBC最佳实践PreparedStatement防注入与连接池的缺失警示源码中若存在拼接SQL危险示范// ❌ 危险SQL注入漏洞 String sql SELECT * FROM transactions WHERE category userInput ; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql);正确写法已在2.3节展示// ✅ PreparedStatement预编译userInput作为参数绑定 String sql SELECT * FROM transactions WHERE category ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, userInput); // userInput被当作纯数据非SQL代码 ResultSet rs ps.executeQuery();注意该源码包未集成HikariCP等连接池每次操作都新建Connection。面试时若被问“如何优化”应回答“引入HikariCP配置maximumPoolSize10在DataSourceUtil类中单例管理DAO层通过dataSource.getConnection()获取连接避免频繁创建销毁开销。”5.3 设计模式落地痕迹观察者模式驱动的余额实时更新当添加一笔收入主界面的“当前余额”标签需立即刷新。源码中常见实现// Subject被观察者 public class AccountBalanceSubject { private ListBalanceObserver observers new ArrayList(); private BigDecimal balance; public void addObserver(BalanceObserver observer) { observers.add(observer); } public void setBalance(BigDecimal newBalance) { this.balance newBalance; notifyObservers(); // 状态变更时通知所有观察者 } private void notifyObservers() { for (BalanceObserver obs : observers) { obs.onBalanceChanged(balance); // 推送新余额 } } } // Observer观察者 public class BalanceLabelObserver implements BalanceObserver { private JLabel balanceLabel; public BalanceLabelObserver(JLabel label) { this.balanceLabel label; } Override public void onBalanceChanged(BigDecimal balance) { SwingUtilities.invokeLater(() - // ✅ 在EDT线程更新UI balanceLabel.setText(余额 balance.toPlainString() 元) ); } } // 使用在MainForm.java中注册 AccountBalanceSubject subject new AccountBalanceSubject(); subject.addObserver(new BalanceLabelObserver(balanceLabel)); // 当service.addIncome()成功后调用subject.setBalance(newBalance)提示此模式解耦了业务逻辑Service与UI展示JLabel符合开闭原则——若新增“余额变化音效”只需添加新Observer无需修改Subject代码。5.4 一个必须掌握的调试技巧用H2 Console实时观测数据变更H2内置Web控制台启动后可直连数据库验证SQL执行结果在config.properties中添加h2.web.allowtrue h2.web.port8082启动程序后浏览器访问http://localhost:8082JDBC URL填jdbc:h2:./db/finance用户名sa密码留空执行SELECT * FROM transactions ORDER BY date DESC LIMIT 10确认最新记录已写入。这比反复重启程序看UI更高效——尤其当发现“添加成功但列表不显示”时先查数据库确认数据是否存在再排查Swing表格模型TableModel的fireTableRowsInserted()是否被调用。本文还有配套的精品资源点击获取