你是不是觉得用 Java 写一个带界面的桌面应用尤其是像记账本这种需要数据持久化的工具是一件很复杂的事情需要学 Spring Boot、搞前后端分离、部署数据库服务器其实对于个人开发者、学生或者只是想快速做一个自用工具的人来说有一个被严重低估的“黄金组合”Java Swing SQLite。这个组合的强大之处在于它让你用最纯粹的 Java 技术栈就能构建出一个独立、轻量、无需安装、数据本地的完整桌面应用。Swing 负责提供用户交互的窗口、按钮和表格而 SQLite 则作为一个嵌入式的数据库以单个文件的形式存储所有数据随应用分发开箱即用。本文将带你从零开始手把手实现一个功能完整的可视化本地记账本。你将学到的不只是 Swing 和 SQLite 的 API 调用更是如何将两者有机结合构建一个具备增删改查CRUD、数据可视化图表和数据导出能力的实战项目。无论你是想巩固 Java 基础、完成课程设计还是为自己制作一个私密的财务小助手这篇文章都能提供一条清晰的路径。我们会先快速搭建起项目骨架然后逐个击破界面设计、数据库连接、业务逻辑和图表展示这些核心模块。过程中你会遇到并解决一些典型问题比如 Swing 的线程安全、SQLite 的并发写入、以及如何将数据库查询结果优雅地展示在界面上。最终你将获得一个可独立运行的.jar文件在任何装有 Java 环境的电脑上都能使用。1. 为什么选择 Swing SQLite 做桌面记账本在开始写代码之前我们需要明确这个技术选型的优势与边界。这决定了它是否适合你的场景。核心优势极致的轻量与便捷零外部依赖用户只需要安装 JREJava 运行环境你的应用就能运行。不需要额外安装数据库服务器如 MySQL、应用服务器如 Tomcat或复杂的运行时环境。单文件数据库SQLite 数据库就是一个.db或.sqlite文件。你可以把它放在应用目录下备份、迁移、分发都极其简单——直接复制文件即可。开发简单直接使用 JDBC 连接 SQLite 与连接其他数据库无异Swing 的界面编写虽然繁琐但逻辑清晰。整个技术栈都在 Java 标准库或常见扩展包内学习曲线平滑。适合谁Java 初学者想通过一个综合项目将 GUI 编程、事件处理、数据库操作串联起来。课程设计/毕业设计需要交付一个功能完整、可演示、有数据库的桌面应用。个人工具开发者希望做一个自用的小工具注重隐私数据本地存储、即开即用。原型验证快速验证一个桌面应用的创意后期再考虑用 JavaFX 或 Electron 重构。它的局限也是特点界面“复古”Swing 的界面风格是经典的跨平台样式不如现代 Web 界面或 JavaFX 炫酷。不适合高并发SQLite 在写入时会对整个数据库文件加锁因此不适合多用户同时高频写入的场景。但对于个人记账本完全够用。功能需手动实现分页、高级过滤、复杂的图表交互等都需要自己编码实现不像一些现代框架有现成组件。明确了这些我们就可以抛开顾虑专注于用这个经典组合解决实际问题。2. 项目结构与核心技术栈在动手之前我们先规划好项目的目录结构并引入必要的库。2.1 项目目录结构一个清晰的结构有助于管理代码。建议采用 Maven 或 Gradle 项目这里以 Maven 为例personal-finance-ledger/ ├── pom.xml (Maven 配置文件) ├── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── yourname/ │ │ └── ledger/ │ │ ├── Main.java (程序入口) │ │ ├── model/ │ │ │ ├── Transaction.java (交易实体类) │ │ │ └── Category.java (分类实体类) │ │ ├── dao/ │ │ │ ├── DatabaseConnection.java (数据库连接单例) │ │ │ ├── TransactionDao.java (交易数据访问对象) │ │ │ └── CategoryDao.java (分类数据访问对象) │ │ ├── ui/ │ │ │ ├── MainFrame.java (主窗口) │ │ │ ├── TransactionPanel.java (交易管理面板) │ │ │ ├── ChartPanel.java (图表展示面板) │ │ │ └── component/ │ │ │ └── TransactionTableModel.java (自定义表格模型) │ │ └── util/ │ │ └── DateUtil.java (日期工具类) │ └── resources/ │ ├── ledger.db (SQLite 数据库文件首次运行生成) │ └── (可存放图标等资源) └── target/ (编译输出目录)2.2 核心技术栈与依赖我们需要以下核心依赖Java SE 8Swing 是 Java 标准库的一部分。SQLite JDBC Driver用于连接和操作 SQLite 数据库。JFreeChart可选但推荐一个强大的开源图表库用于生成饼图、柱状图等。在 Maven 的pom.xml中添加依赖project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.yourname/groupId artifactIdpersonal-finance-ledger/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies !-- SQLite JDBC 驱动 -- dependency groupIdorg.xerial/groupId artifactIdsqlite-jdbc/artifactId version3.42.0.0/version !-- 请使用最新稳定版 -- /dependency !-- JFreeChart 用于数据可视化 -- dependency groupIdorg.jfree/groupId artifactIdjfreechart/artifactId version1.5.3/version /dependency /dependencies /project3. 数据库设计与初始化数据是应用的核心。我们先设计简单的表结构。3.1 实体类定义 (Model)首先定义两个核心的实体类对应数据库中的表。Transaction.java (交易记录)package com.yourname.ledger.model; import java.math.BigDecimal; import java.time.LocalDateTime; public class Transaction { private Integer id; // 主键 private LocalDateTime time; // 交易时间 private BigDecimal amount; // 金额使用BigDecimal避免精度问题 private String type; // 类型INCOME(收入)/EXPENSE(支出) private Integer categoryId; // 分类ID外键 private String description; // 描述 // 构造方法、Getter和Setter省略需自行补充 // 推荐使用Lombok注解 Data但为清晰起见这里展示完整结构 public Transaction() {} public Transaction(LocalDateTime time, BigDecimal amount, String type, Integer categoryId, String description) { this.time time; this.amount amount; this.type type; this.categoryId categoryId; this.description description; } // ... Getter and Setter methods }Category.java (分类)package com.yourname.ledger.model; public class Category { private Integer id; private String name; // 分类名称如“餐饮”、“交通”、“工资” private String type; // 所属大类型INCOME/EXPENSE // 构造方法、Getter和Setter省略 }3.2 数据库连接与初始化 (DAO)我们使用单例模式管理数据库连接确保全局只有一个连接对于SQLite桌面应用这通常是安全且高效的。DatabaseConnection.javapackage com.yourname.ledger.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class DatabaseConnection { private static final String DB_URL jdbc:sqlite:ledger.db; // 数据库文件位于项目根目录 private static Connection connection null; // 私有构造方法 private DatabaseConnection() {} public static synchronized Connection getConnection() { if (connection null) { try { // 加载驱动SQLite JDBC驱动会自动注册 connection DriverManager.getConnection(DB_URL); initializeDatabase(); // 首次连接时初始化表 System.out.println(数据库连接成功。); } catch (SQLException e) { System.err.println(连接数据库失败: e.getMessage()); e.printStackTrace(); } } return connection; } private static void initializeDatabase() { String createCategoryTable CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN (INCOME, EXPENSE)) ); ; String createTransactionTable CREATE TABLE IF NOT EXISTS transaction ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, amount DECIMAL(10, 2) NOT NULL, type TEXT NOT NULL CHECK (type IN (INCOME, EXPENSE)), category_id INTEGER, description TEXT, FOREIGN KEY (category_id) REFERENCES category (id) ); ; // 插入一些默认分类 String insertDefaultCategories INSERT OR IGNORE INTO category (name, type) VALUES (工资, INCOME), (兼职, INCOME), (投资, INCOME), (餐饮, EXPENSE), (交通, EXPENSE), (购物, EXPENSE), (娱乐, EXPENSE), (住房, EXPENSE); ; try (Statement stmt connection.createStatement()) { stmt.execute(createCategoryTable); stmt.execute(createTransactionTable); stmt.execute(insertDefaultCategories); System.out.println(数据库表初始化完成。); } catch (SQLException e) { System.err.println(初始化数据库失败: e.getMessage()); } } public static void closeConnection() { if (connection ! null) { try { connection.close(); connection null; } catch (SQLException e) { e.printStackTrace(); } } } }关键点解析jdbc:sqlite:ledger.db连接字符串。如果ledger.db文件不存在SQLite 会自动创建。CREATE TABLE IF NOT EXISTS确保表不存在时才创建避免重复初始化。CHECK约束在数据库层保证type字段只能是INCOME或EXPENSE。FOREIGN KEY建立了transaction表和category表的外键关系。INSERT OR IGNORE插入默认分类如果已存在则忽略防止重复。4. 数据访问层 (DAO) 实现DAO 模式将数据库操作封装起来业务逻辑层无需关心 SQL 细节。TransactionDao.javapackage com.yourname.ledger.dao; import com.yourname.ledger.model.Transaction; import java.math.BigDecimal; import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; public class TransactionDao { public boolean addTransaction(Transaction transaction) { String sql INSERT INTO transaction(time, amount, type, category_id, description) VALUES(?, ?, ?, ?, ?); try (Connection conn DatabaseConnection.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setTimestamp(1, Timestamp.valueOf(transaction.getTime())); pstmt.setBigDecimal(2, transaction.getAmount()); pstmt.setString(3, transaction.getType()); pstmt.setInt(4, transaction.getCategoryId()); pstmt.setString(5, transaction.getDescription()); return pstmt.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public ListTransaction getAllTransactions() { ListTransaction transactions new ArrayList(); String sql SELECT t.*, c.name as category_name FROM transaction t LEFT JOIN category c ON t.category_id c.id ORDER BY t.time DESC; try (Connection conn DatabaseConnection.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { Transaction t new Transaction(); t.setId(rs.getInt(id)); t.setTime(rs.getTimestamp(time).toLocalDateTime()); t.setAmount(rs.getBigDecimal(amount)); t.setType(rs.getString(type)); t.setCategoryId(rs.getInt(category_id)); t.setDescription(rs.getString(description)); // 可以额外设置分类名称用于界面显示 // t.setCategoryName(rs.getString(category_name)); transactions.add(t); } } catch (SQLException e) { e.printStackTrace(); } return transactions; } // 实现 updateTransaction, deleteTransaction, queryByDateRange 等方法 public boolean deleteTransaction(int id) { String sql DELETE FROM transaction WHERE id ?; try (Connection conn DatabaseConnection.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, id); return pstmt.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 统计某段时间内的总收入/总支出 public BigDecimal getSummary(String type, LocalDateTime start, LocalDateTime end) { String sql SELECT SUM(amount) as total FROM transaction WHERE type ? AND time BETWEEN ? AND ?; try (Connection conn DatabaseConnection.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, type); pstmt.setTimestamp(2, Timestamp.valueOf(start)); pstmt.setTimestamp(3, Timestamp.valueOf(end)); ResultSet rs pstmt.executeQuery(); if (rs.next()) { BigDecimal total rs.getBigDecimal(total); return total ! null ? total : BigDecimal.ZERO; } } catch (SQLException e) { e.printStackTrace(); } return BigDecimal.ZERO; } }CategoryDao.java的实现类似主要负责分类的增删改查这里不再赘述。5. 用户界面 (Swing) 设计与实现这是最直观的部分。我们将创建一个主窗口包含菜单栏、工具栏、一个用于显示交易列表的表格和一个用于显示图表的面板。5.1 主窗口框架 (MainFrame.java)package com.yourname.ledger.ui; import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { private TransactionPanel transactionPanel; private ChartPanel chartPanel; public MainFrame() { setTitle(个人记账本 - Java Swing SQLite); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1000, 700); setLocationRelativeTo(null); // 居中显示 initMenuBar(); initMainLayout(); loadData(); } private void initMenuBar() { JMenuBar menuBar new JMenuBar(); JMenu fileMenu new JMenu(文件); JMenuItem exportItem new JMenuItem(导出数据...); JMenuItem exitItem new JMenuItem(退出); exitItem.addActionListener(e - System.exit(0)); fileMenu.add(exportItem); fileMenu.addSeparator(); fileMenu.add(exitItem); menuBar.add(fileMenu); setJMenuBar(menuBar); } private void initMainLayout() { // 使用 BorderLayout 作为主布局 setLayout(new BorderLayout()); // 顶部工具栏快速操作 JToolBar toolBar new JToolBar(); JButton addButton new JButton(新增记录); JButton refreshButton new JButton(刷新); toolBar.add(addButton); toolBar.addSeparator(); toolBar.add(refreshButton); add(toolBar, BorderLayout.NORTH); // 中心区域左侧交易列表右侧图表 JSplitPane splitPane new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setDividerLocation(600); // 初始分割位置 transactionPanel new TransactionPanel(); chartPanel new ChartPanel(); splitPane.setLeftComponent(transactionPanel); splitPane.setRightComponent(chartPanel); add(splitPane, BorderLayout.CENTER); // 底部状态栏 JPanel statusPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); statusPanel.add(new JLabel(就绪)); add(statusPanel, BorderLayout.SOUTH); // 按钮事件绑定 addButton.addActionListener(e - transactionPanel.showAddTransactionDialog()); refreshButton.addActionListener(e - refreshAllData()); } private void loadData() { // 启动时加载数据 refreshAllData(); } public void refreshAllData() { transactionPanel.refreshTableData(); chartPanel.refreshChart(); } public static void main(String[] args) { // 确保界面风格与系统一致 try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { e.printStackTrace(); } // 在事件分发线程中启动GUI SwingUtilities.invokeLater(() - { MainFrame frame new MainFrame(); frame.setVisible(true); }); } }5.2 交易管理面板 (TransactionPanel.java)这个面板是核心包含一个JTable来展示数据以及相关的操作按钮。package com.yourname.ledger.ui; import com.yourname.ledger.dao.TransactionDao; import com.yourname.ledger.model.Transaction; import com.yourname.ledger.ui.component.TransactionTableModel; import javax.swing.*; import javax.swing.table.TableRowSorter; import java.awt.*; import java.math.BigDecimal; import java.time.LocalDateTime; public class TransactionPanel extends JPanel { private JTable transactionTable; private TransactionTableModel tableModel; private TransactionDao transactionDao new TransactionDao(); public TransactionPanel() { setLayout(new BorderLayout()); initTable(); initButtonPanel(); } private void initTable() { tableModel new TransactionTableModel(); transactionTable new JTable(tableModel); // 设置表格排序器 TableRowSorterTransactionTableModel sorter new TableRowSorter(tableModel); transactionTable.setRowSorter(sorter); // 设置列宽 transactionTable.getColumnModel().getColumn(0).setPreferredWidth(50); // ID transactionTable.getColumnModel().getColumn(1).setPreferredWidth(150); // 时间 transactionTable.getColumnModel().getColumn(2).setPreferredWidth(80); // 类型 transactionTable.getColumnModel().getColumn(3).setPreferredWidth(100); // 金额 transactionTable.getColumnModel().getColumn(4).setPreferredWidth(100); // 分类 transactionTable.getColumnModel().getColumn(5).setPreferredWidth(200); // 描述 JScrollPane scrollPane new JScrollPane(transactionTable); add(scrollPane, BorderLayout.CENTER); } private void initButtonPanel() { JPanel buttonPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton addButton new JButton(新增); JButton editButton new JButton(编辑); JButton deleteButton new JButton(删除); JButton filterButton new JButton(筛选); buttonPanel.add(addButton); buttonPanel.add(editButton); buttonPanel.add(deleteButton); buttonPanel.add(filterButton); add(buttonPanel, BorderLayout.SOUTH); // 事件监听 addButton.addActionListener(e - showAddTransactionDialog()); deleteButton.addActionListener(e - deleteSelectedTransaction()); // 编辑和筛选功能需自行实现 } public void showAddTransactionDialog() { // 使用 JDialog 或 JOptionPane 创建一个输入对话框 // 这里简化使用 JOptionPane 组合多个输入字段 JTextField amountField new JTextField(10); JComboBoxString typeCombo new JComboBox(new String[]{EXPENSE, INCOME}); JComboBoxString categoryCombo new JComboBox(new String[]{餐饮, 交通, 工资}); // 应从数据库加载 JTextField descField new JTextField(20); JPanel panel new JPanel(new GridLayout(0, 2)); panel.add(new JLabel(金额:)); panel.add(amountField); panel.add(new JLabel(类型:)); panel.add(typeCombo); panel.add(new JLabel(分类:)); panel.add(categoryCombo); panel.add(new JLabel(描述:)); panel.add(descField); int result JOptionPane.showConfirmDialog(this, panel, 新增交易记录, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result JOptionPane.OK_OPTION) { try { Transaction t new Transaction(); t.setTime(LocalDateTime.now()); t.setAmount(new BigDecimal(amountField.getText().trim())); t.setType((String) typeCombo.getSelectedItem()); // 需要根据分类名称获取分类ID这里简化处理 t.setCategoryId(1); t.setDescription(descField.getText().trim()); if (transactionDao.addTransaction(t)) { JOptionPane.showMessageDialog(this, 添加成功); refreshTableData(); } else { JOptionPane.showMessageDialog(this, 添加失败, 错误, JOptionPane.ERROR_MESSAGE); } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, 金额格式错误, 输入错误, JOptionPane.ERROR_MESSAGE); } } } private void deleteSelectedTransaction() { int selectedRow transactionTable.getSelectedRow(); if (selectedRow -1) { JOptionPane.showMessageDialog(this, 请先选择一条记录, 提示, JOptionPane.WARNING_MESSAGE); return; } // 转换视图行索引为模型行索引因为排序后视图索引会变 int modelRow transactionTable.convertRowIndexToModel(selectedRow); int id (int) tableModel.getValueAt(modelRow, 0); // 假设第一列是ID int confirm JOptionPane.showConfirmDialog(this, 确定要删除这条记录吗, 确认删除, JOptionPane.YES_NO_OPTION); if (confirm JOptionPane.YES_OPTION) { if (transactionDao.deleteTransaction(id)) { JOptionPane.showMessageDialog(this, 删除成功); refreshTableData(); } } } public void refreshTableData() { tableModel.setTransactions(transactionDao.getAllTransactions()); tableModel.fireTableDataChanged(); // 通知表格数据已更新 } }5.3 自定义表格模型 (TransactionTableModel.java)为了让JTable正确显示我们的Transaction对象列表需要自定义一个TableModel。package com.yourname.ledger.ui.component; import com.yourname.ledger.model.Transaction; import javax.swing.table.AbstractTableModel; import java.time.format.DateTimeFormatter; import java.util.List; public class TransactionTableModel extends AbstractTableModel { private ListTransaction transactions; private final String[] columnNames {ID, 时间, 类型, 金额, 分类, 描述}; private final DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm); public void setTransactions(ListTransaction transactions) { this.transactions transactions; } Override public int getRowCount() { return transactions null ? 0 : transactions.size(); } Override public int getColumnCount() { return columnNames.length; } Override public String getColumnName(int column) { return columnNames[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Transaction t transactions.get(rowIndex); switch (columnIndex) { case 0: return t.getId(); case 1: return t.getTime().format(formatter); case 2: return t.getType().equals(INCOME) ? 收入 : 支出; // 本地化显示 case 3: return t.getAmount(); case 4: return 分类名称; // 这里应通过categoryId查询得到为简化先写死 case 5: return t.getDescription(); default: return null; } } Override public Class? getColumnClass(int columnIndex) { // 帮助JTable正确渲染不同类型的数据如数字右对齐 switch (columnIndex) { case 0: return Integer.class; case 3: return java.math.BigDecimal.class; default: return String.class; } } }6. 数据可视化图表实现 (ChartPanel.java)我们将使用 JFreeChart 来生成支出分类的饼图和月度趋势柱状图。package com.yourname.ledger.ui; import com.yourname.ledger.dao.TransactionDao; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.data.general.DefaultPieDataset; import javax.swing.*; import java.awt.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class ChartPanel extends JPanel { private TransactionDao transactionDao new TransactionDao(); private JTabbedPane tabbedPane; public ChartPanel() { setLayout(new BorderLayout()); tabbedPane new JTabbedPane(); add(tabbedPane, BorderLayout.CENTER); refreshChart(); } public void refreshChart() { tabbedPane.removeAll(); // 添加支出分类饼图 tabbedPane.addTab(支出分类, createPieChartPanel()); // 添加月度趋势图 tabbedPane.addTab(月度趋势, createBarChartPanel()); } private JPanel createPieChartPanel() { // 这里需要从数据库查询各分类的支出总额 // 假设我们有一个方法 getExpenseSummaryByCategory MapString, BigDecimal categoryExpense getExpenseSummaryByCategory(); DefaultPieDataset dataset new DefaultPieDataset(); categoryExpense.forEach((category, amount) - { if (amount.compareTo(BigDecimal.ZERO) 0) { dataset.setValue(category, amount); } }); JFreeChart chart ChartFactory.createPieChart( 支出分类占比, // 图表标题 dataset, // 数据集 true, // 是否显示图例 true, // 是否生成工具提示 false // 是否生成URL链接 ); return new ChartPanel(chart); } private JPanel createBarChartPanel() { // 查询最近6个月的收入支出趋势 DefaultCategoryDataset dataset new DefaultCategoryDataset(); LocalDateTime now LocalDateTime.now(); for (int i 5; i 0; i--) { YearMonth yearMonth YearMonth.from(now.minusMonths(i)); LocalDateTime start yearMonth.atDay(1).atStartOfDay(); LocalDateTime end yearMonth.atEndOfMonth().atTime(23, 59, 59); BigDecimal income transactionDao.getSummary(INCOME, start, end); BigDecimal expense transactionDao.getSummary(EXPENSE, start, end); String month yearMonth.toString(); // 格式如 2024-03 dataset.addValue(income, 收入, month); dataset.addValue(expense, 支出, month); } JFreeChart chart ChartFactory.createBarChart( 月度收支趋势, // 图表标题 月份, // 横轴标签 金额, // 纵轴标签 dataset // 数据集 ); return new ChartPanel(chart); } // 模拟方法按分类统计支出 private MapString, BigDecimal getExpenseSummaryByCategory() { // 实际应执行SQL: SELECT c.name, SUM(t.amount) FROM transaction t JOIN category c ON t.category_id c.id WHERE t.typeEXPENSE GROUP BY c.name // 此处返回模拟数据 return Map.of( 餐饮, new BigDecimal(1500.50), 交通, new BigDecimal(800.00), 购物, new BigDecimal(2300.00), 娱乐, new BigDecimal(500.00) ); } }注意getExpenseSummaryByCategory方法需要你根据实际的数据库查询来实现。这里为了演示图表生成使用了模拟数据。7. 程序入口与运行最后我们创建一个简单的入口类来启动应用。Main.javapackage com.yourname.ledger; import com.yourname.ledger.ui.MainFrame; public class Main { public static void main(String[] args) { // 直接调用 MainFrame 的 main 方法或在此初始化一些全局配置 MainFrame.main(args); } }运行步骤确保你的项目配置了正确的 Maven 依赖。运行Main.java或MainFrame.java中的main方法。首次运行会在项目根目录生成ledger.db数据库文件并创建表结构及默认分类。通过界面进行新增、删除、刷新等操作。8. 常见问题与排查思路在开发和使用过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案程序启动时报ClassNotFoundException或No suitable driver foundSQLite JDBC 驱动未正确引入检查pom.xml依赖运行mvn clean compile确保依赖已下载IDE 的类路径配置正确。无法新增或删除数据但无错误提示SQL 执行失败但被try-catch静默处理在catch块中打印e.printStackTrace()查看控制台输出定位 SQL 语法错误或约束冲突。界面添加记录后表格不刷新TableModel的数据已更新但未通知视图检查是否调用了fireTableDataChanged()方法在更新transactions列表后务必调用tableModel.fireTableDataChanged()。图表不显示或显示为空白JFreeChart 依赖缺失或数据集为空检查 Maven 依赖在getExpenseSummaryByCategory方法中打印查询结果确保 JFreeChart 依赖已添加检查数据库查询逻辑确保返回有效数据。在多处操作数据库后出现“database is locked”错误SQLite 默认不支持多线程同时写入检查是否在多个线程中同时持有Connection并执行写操作对于桌面应用建议使用单例连接并且将数据库操作封装在同步方法中或使用连接池但SQLite轻量场景下单例通常足够。打包成 JAR 后运行找不到数据库文件数据库文件路径是相对的在 JAR 中无法写入检查DB_URL使用绝对路径或用户目录路径将DB_URL改为jdbc:sqlite: System.getProperty(user.home) /.ledger/ledger.db确保有写入权限。9. 项目扩展与最佳实践至此一个基础的可视化记账本已经完成。但要让其更健壮、更实用可以考虑以下扩展方向和实践建议数据验证与用户体验在新增/编辑对话框中对金额、日期进行严格校验。使用JTable的渲染器 (TableCellRenderer) 对“收入”和“支出”用不同颜色显示。添加“双击编辑”功能。更强大的查询与筛选实现按日期范围、分类、金额区间筛选交易记录。在TransactionDao中增加带参数查询的方法。数据导入导出导出将表格数据导出为 CSV 或 Excel 文件。可以使用 Apache Commons CSV 或 POI 库。导入提供模板允许用户批量导入数据。数据备份与恢复添加菜单项允许用户将当前的ledger.db文件备份到指定位置。提供从备份文件恢复的功能。生产环境注意事项连接管理虽然我们用了单例但在应用退出时 (MainFrame的WindowListener中)应调用DatabaseConnection.closeConnection()关闭连接。异常处理不要仅仅printStackTrace()应向用户反馈友好的错误信息。日志记录引入SLF4JLogback记录操作日志和错误日志便于排查问题。打包分发使用Maven Shade Plugin或Launch4j将项目及其所有依赖打包成一个可执行的fat JAR或.exe文件方便分发。安全性考虑这是一个本地单机应用数据库文件明文存储。如果涉及非常敏感的数据可以考虑对数据库文件进行加密SQLite 支持加密扩展如SQLCipher或对特定字段在应用层进行加密。通过这个项目你不仅实践了 Swing 和 SQLite更掌握了一个完整桌面应用的开发流程从需求分析、技术选型、数据库设计、分层架构到界面实现和打包部署。这个模式可以复用到许多其他类型的个人桌面工具开发中如本地笔记、客户管理、小型库存系统等。