ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Swing+MySQL个人记账系统:从零搭建CS结构桌面应用

Java Swing+MySQL个人记账系统:从零搭建CS结构桌面应用 简介该资源是基于Java Swing开发的个人财务收支管理系统采用C/S架构后端使用MySQL数据库适合学习Swing界面编程以及Java与MySQL联动开发的初学者或课程设计使用者。系统内置用户管理、类别管理、收入管理、支出管理、个人信息修改、密码修改等模块覆盖财务数据录入、查询与维护的完整流程并附带数据库脚本javacaiwu.sql便于快速导入部署。压缩包内含111个文件主要由22个Java源程序、80个编译后的class文件、5个依赖jar包以及数据库SQL脚本组成整体体积约2.91MB结构精简但功能闭环。当前已有207人学习下载。对于希望了解传统桌面端管理系统分层结构与Swing事件驱动编程的读者该源码可直接导入eclipse或Myeclipse中运行并支持管理员与普通用户两种角色操作适合作为毕业设计或Java实训项目的参考蓝本。1. 一个CS结构的个人记账系统为什么该用Java Swing而不是Web如果你搜java swing个人财务收支管理系统eclipse开发Mysql数据库CS结构java编程源码这条长尾大概率是三种情况要交Java课程设计、准备毕设、或者想在公司内网搭一个不用部署Web服务器的记账工具。这个组合在国内教学型项目里出现频率极高Java Swing负责桌面界面MySQL存账目数据CS结构让多台机器连同一个库Eclipse承担全部编码、界面拖拽和调试。它的价值不在技术新而在完整——从建表到连接数据库到Swing界面再到打包运行全部环节都能在单机上闭环适合拿来理解桌面应用和数据库的完整交互过程。这套方案的受众很明确Java基础过关但没碰过完整项目的学生或者想快速做一个局域网内可多人使用的内务管理工具的从业者。它避开Web前端那些框架选型和技术债把注意力集中在JDBC、事件驱动、增删改查这三件核心事上。后面所有章节围绕一个目标让你在Eclipse里从零把这套系统跑起来并且知道各环节为什么这样设计、出问题往哪里查。2. 选型与前置准备为什么这三件套能凑到一起以及建表前要想清楚的事2.1 为什么是C/S结构而不是浏览器B/SC/S结构在这里意味着程序分成客户端和服务端两半Swing界面程序是客户端MySQL是服务端。用户操作界面界面通过JDBC直接访问数据库所有账目数据存放在MySQL里。和B/S相比C/S最直接的收益是Swing作为桌面程序天然拥有独立窗口、快捷键、本地文件读写能力对账目导出、Excel生成这类需求比网页顺手得多。另一个现实原因是课程设计和教学场景。B/S结构需要Tomcat、Servlet容器、前端页面三样同时跑起来任何一个环节配置失败都会让学生卡住。C/S结构只需要客户端连上数据库故障面小调试时看得到Swing窗口和数据库客户端两条线理解起来直观。如果你做的是公司内部小工具C/S还有一个好处数据库服务器和客户端分离后可以通过MySQL的账号权限控制不同用户能看到的账目这个后面会在建表时体现。2.2 环境搭建的几个版本选择先避掉最大的坑JDK用8或11都行不要追新。Eclipse装的是Java开发版内置的WindowBuilder插件用来拖拽Swing界面——网上说的“swing 召回”在Eclipse里的对应操作就是WindowBuilder。MySQL推荐5.7或8.0两种版本在JDBC连接参数上有差异这会在第5章单独讲。你下载的MySQL安装教程如果照着做提示服务启动失败八成是3306端口被占用或my.ini没写好这一步别纠结用Docker拉一个MySQL镜像或者直接重装都行。建库建表是整个项目的定海神针。用固定账目系统举例四张表是最简方案-- 创建数据库指定utf8mb4防中文乱码 CREATE DATABASE IF NOT EXISTS finance DEFAULT CHARACTER SET utf8mb4; USE finance; -- 用户表记账系统先得有登录人 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(32) NOT NULL COMMENT 密码建议MD5存, real_name VARCHAR(20) DEFAULT COMMENT 真实姓名 ); -- 收支分类表收入/支出各有哪些类别 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, type ENUM(income,expense) NOT NULL COMMENT 收入/支出, name VARCHAR(20) NOT NULL ); -- 账目明细表所有流水都在这 CREATE TABLE t_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 金额, note VARCHAR(255) DEFAULT COMMENT 备注, record_date DATE NOT NULL COMMENT 日期, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id), FOREIGN KEY (category_id) REFERENCES t_category(id) ); -- 数据初始化给每个用户塞默认分类 INSERT INTO t_user (username, password, real_name) VALUES (admin, MD5(123456), 管理员); INSERT INTO t_category (type, name) VALUES (income, 工资), (income, 兼职), (income, 理财), (expense, 餐饮), (expense, 交通), (expense, 购物), (expense, 医疗);关于密码MD5存储这里用的是MySQL内置的MD5函数适合教学演示。生产环境该用加盐哈希但在一个课程设计里MD5能让你省掉密码加密的代码量把精力放在主流程上。2.3 Eclipse里创建项目的正确姿势在Eclipse里新建Java Project而不是Dynamic Web Project因为后者会带来一堆用不到的Web依赖。项目名称建议叫FinanceManager把勾选“Add module-info.java”的选项取消——这个Java 9引入的模块描述文件在实际做Swing项目时只会添乱不加它。建完项目后先建包结构我习惯拆成这样com.finance.view— 所有Swing界面类登录、主窗口、记账表单、列表窗口com.finance.dao— 数据访问对象一个表对应一个DAO类com.finance.model— 实体类对应数据库中各个表com.finance.util— 工具类比如JDBC连接、日期格式化com.finance.app— 程序入口这个包结构本身是CS结构客户端一侧的分层view层负责界面和交互dao层负责JDBC操作model层只是装数据的盒子。你后面写任何Swing管理系统都可以套这个五层结构。3. 从连接数据库到DAO层把JDBC这条链路打通项目才算有了地基3.1 数据库连接工具类直接解决掉80%的启动报错JDBC连接是CS结构项目里最容易翻车的位置我把一个能稳定运行的连接工具类完整给出里面的参数每一项都有讲究package com.finance.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * 数据库连接工具类 * 课程设计级别用单例连接即可没必要上连接池 */ public class DBUtil { // 驱动类名MySQL 8.x 必须用 com.mysql.cj.jdbc.Driver // MySQL 5.7 及以下请改回 com.mysql.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; // 连接地址serverTimezoneAsia/Shanghai 是MySQL 8的必填参数 private static final String URL jdbc:mysql://localhost:3306/finance ?useSSLfalseserverTimezoneAsia/Shanghai characterEncodingutf8useUnicodetrue; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); // 如果看到这个异常说明驱动jar没放进classpath } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一关闭资源先关结果集再关语句最后关连接 public static void close(AutoCloseable... resources) { for (AutoCloseable res : resources) { if (res ! null) { try { res.close(); } catch (Exception e) { e.printStackTrace(); } } } } }这段代码的关键在于URL里的三个参数。useSSLfalse是关闭SSL握手因为本地开发不需要加密传输开了反而会在MySQL 8上多一层警告serverTimezoneAsia/Shanghai解决MySQL 8驱动强制要求时区参数的问题不写就会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognizedcharacterEncodingutf8配合数据库的utf8mb4字符集保证中文备注不会变成问号。在项目里首次使用getConnection()之前先在Eclipse里单独写个main方法测试连接这会帮你把驱动jar缺失、URL拼写错误、MySQL服务没启动这几类问题全部隔离出来。第一步跑通了后面的DAO才是水到渠成。3.2 实体类与DAO层写增删改查前先把结构立稳实体类的字段必须和数据库字段一一对应。我把Record实体类和对应的DAO写出来你照着这个模式可以把User、Category的DAO也补全package com.finance.model; import java.math.BigDecimal; import java.util.Date; /** * 账目明细实体类 * 字段命名采用驼峰与数据库下划线字段通过ResultSet映射 */ public class Record { private Long id; private Integer userId; private Integer categoryId; private BigDecimal amount; // 金额用BigDecimaldouble会丢精度 private String categoryName; // 冗余字段显示列表时直接展示分类名 private String type; // income / expense方便界面区分收支 private String note; private Date recordDate; // 每个字段的getter/setter在此省略 // 你可以用Eclipse: Source - Generate Getters and Setters 自动生成 }package com.finance.dao; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; import java.util.List; import com.finance.model.Record; import com.finance.util.DBUtil; public class RecordDao { /** * 多表联查记录带条件版本 * 按用户、分类、日期范围筛选是记账系统最核心的查询 */ public ListRecord listByCondition(Integer userId, Integer categoryId, String type, String keyword, Date start, Date end) { ListRecord list new ArrayList(); // 动态拼接SQL注意每个条件前都留一个空格 StringBuilder sql new StringBuilder( SELECT r.id, r.user_id, r.category_id, r.amount, r.note, r.record_date, ) .append(c.name AS category_name, c.type AS type ) .append(FROM t_record r JOIN t_category c ON r.category_id c.id ) .append(WHERE r.user_id ? ); // 参数列表避免直接拼字符串到SQL里 ListObject params new ArrayList(); params.add(userId); if (categoryId ! null) { sql.append(AND r.category_id ? ); params.add(categoryId); } if (type ! null !type.isEmpty()) { sql.append(AND c.type ? ); params.add(type); } if (keyword ! null !keyword.isEmpty()) { sql.append(AND r.note LIKE ? ); params.add(% keyword %); } if (start ! null) { sql.append(AND r.record_date ? ); params.add(new java.sql.Date(start.getTime())); } if (end ! null) { sql.append(AND r.record_date ? ); params.add(new java.sql.Date(end.getTime())); } sql.append(ORDER BY r.record_date DESC, r.id DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); while (rs.next()) { Record r new Record(); r.setId(rs.getLong(id)); r.setUserId(rs.getInt(user_id)); r.setCategoryId(rs.getInt(category_id)); r.setAmount(rs.getBigDecimal(amount)); r.setNote(rs.getString(note)); r.setRecordDate(rs.getDate(record_date)); r.setCategoryName(rs.getString(category_name)); r.setType(rs.getString(type)); list.add(r); } return list; } catch (SQLException e) { e.printStackTrace(); return list; // 出错时返回空列表不让界面直接崩溃 } } /** * 统计某时间段的收支合计 * 这个SQL返回一行一列做首页汇总卡片用 */ public BigDecimal sumAmount(Integer userId, String type, Date start, Date end) { String sql SELECT COALESCE(SUM(amount),0) FROM t_record r JOIN t_category c ON r.category_id c.id WHERE r.user_id ? AND c.type ? AND r.record_date BETWEEN ? AND ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setString(2, type); ps.setDate(3, new java.sql.Date(start.getTime())); ps.setDate(4, new java.sql.Date(end.getTime())); ResultSet rs ps.executeQuery(); if (rs.next()) { return rs.getBigDecimal(1); } } catch (SQLException e) { e.printStackTrace(); } return BigDecimal.ZERO; } }这段代码的逻辑核心是PreparedStatement的参数化查询。之所以不用Statement拼接SQL除了防SQL注入更实际的原因是动态条件拼接时参数类型和引号的处理非常容易出错。比如日期类型直接拼字符串会写出AND record_date 2024-01-01这样依赖MySQL隐式转换的SQL一旦日期格式不匹配就会导致记录查不出来。参数化之后setDate告诉驱动这是一个java.sql.DateJDBC驱动负责正确转义这属于CS结构开发里血泪经验换来的习惯。3.3 添加、删除、更新的标准写法以及事务什么时候用// 新增记录 public boolean insert(Record r) { String sql INSERT INTO t_record (user_id, category_id, amount, note, record_date) VALUES (?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, r.getUserId()); ps.setInt(2, r.getCategoryId()); ps.setBigDecimal(3, r.getAmount()); ps.setString(4, r.getNote()); ps.setDate(5, new java.sql.Date(r.getRecordDate().getTime())); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }删除记录时要注意t_record表没有级联删除的需求因为分类表是公共数据删除分类时不应当影响历史账目。我的做法是删分类前先查一下该分类下有没有记账有账就先提示用户切走这一步在Swing界面里是弹个确认框的事。事务只有在两种业务场景才需要批量导入账目或者修改分类后需要同步修改所有关联记录的冗余字段。JDBC原生事务写起来有点反人类不推荐在Swing项目里引入Spring的事务管理。简单场景直接在DAO方法上加synchronized关键字并用conn.setAutoCommit(false)手动控制即可。4. Swing界面与业务联动登录窗口、主界面和账目表格怎么串起来4.1 用WindowBuilder拖出一个登录窗口以及手动调整的关键属性在Eclipse中右键com.finance.view包New - Other - WindowBuilder - Swing Designer - JFrame。拖一个JPanel到窗口上放两个JLabel、一个JTextField、一个JPasswordField、一个JButton。按课程设计习惯窗体尺寸设成360x240居中显示标题叫“个人财务收支管理系统——登录”。登录按钮的事件代码是整个窗口的核心逻辑分三层先空值校验再去数据库查用户最后存会话状态btnLogin.addActionListener(e - { String username txtUsername.getText().trim(); String password new String(txtPassword.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user null) { JOptionPane.showMessageDialog(this, 用户名或密码错误); return; } // 登录成功后打开主界面关闭登录窗 new MainFrame(user).setVisible(true); dispose(); // 释放登录窗口资源否则程序退出时会有残留进程 });注意new String(txtPassword.getPassword())这一步getPassword()返回的是char[]而不是String这是Swing密码框的安全设计。千万别用txtPassword.getText()去拿密码虽然能拿到但不符合JPasswordField的使用规范而且IDE会直接警告。查库的findByUsernameAndPassword方法内部会先对密码做MD5再比对对应建表时SQL里用过的MD5()函数。4.2 主界面拆成三个区工具条、统计面板、明细表格主窗口是记账系统的门面我习惯用BorderLayout布局管理NORTH放工具栏和统计标签CENTER放JTableSOUTH放分页信息或状态栏。可视化拖拽时注意WindowBuilder能帮你摆组件但布局管理器嵌套超过两层后拖出的效果经常和你预期不一致这种时候就要手改代码——这是Swing开发最现实的“拖不如写”。JTable展示数据不是直接把List放进去而是用DefaultTableModel做桥String[] columns {ID, 日期, 分类, 收支, 金额, 备注}; DefaultTableModel tableModel new DefaultTableModel(columns, 0); JTable table new JTable(tableModel) { // 禁止直接编辑表格里的单元格 Override public boolean isCellEditable(int row, int column) { return false; } };这里复写isCellEditable返回false是新手最容易忽略的点。默认情况下JTable的单元格是允许双击编辑的而这个编辑操作发生在本地的JTable模型里不会回写数据库——用户改了界面上的数字刷新后又变回去看起来就像“数据丢了”。先禁用编辑把入口统一收敛到“新增/修改”按钮弹窗里是财务工具必须做的交互约束。加载数据的代码写在MainFrame构造函数里private void loadRecordData() { // 清空旧数据避免刷新时重复 tableModel.setRowCount(0); RecordDao recordDao new RecordDao(); ListRecord records recordDao.listByCondition( currentUser.getId(), null, null, null, startDate, endDate); for (Record r : records) { tableModel.addRow(new Object[]{ r.getId(), new SimpleDateFormat(yyyy-MM-dd).format(r.getRecordDate()), r.getCategoryName(), 收入.equals(r.getType()) ? 收入 : 支出, r.getAmount(), r.getNote() }); } // 刷新统计卡片上的数字 refreshSummary(); }这段代码六行但包含了一个必须养成的习惯Swing的对数据库和耗时操作的访问应当在构造函数里先同步做一次然后所有刷新都显式调用loadRecordData()。如果数据量变大比如一个月几千条构造函数里的加载可能让窗口卡住几百毫秒这时你才需要引入SwingWorker。对这个项目来说课程设计量级的数据量同步加载挂在UI线程上问题不大。4.3 记账表单的联动逻辑分类下拉框怎么跟着收支类型变新增一条账目时弹一个对话框里面有收支类型单选收入/支出、分类下拉框、金额输入框、日期选择器、备注文本域。分类下拉框的内容要随收支类型变化选“支出”时只显示餐饮、交通、购物这些“选收入”时只显示工资、兼职。实现在Swing里是监听JRadioButton的ActionListener根据所选类型重新给JComboBox设置数据源rdIncome.addActionListener(e - loadCategoriesToCombo(income)); rdExpense.addActionListener(e - loadCategoriesToCombo(expense));private void loadCategoriesToCombo(String type) { DefaultComboBoxModelCategory model new DefaultComboBoxModel(); ListCategory categories new CategoryDao().listByType(type); for (Category c : categories) { model.addElement(c); } cbCategory.setModel(model); }这样一个交互细节直接对应数据库里t_category表的type字段设计。分类数据不写死在界面里而是放数据库意味着你可以随时新增分类而不用改代码重新编译。这是CS结构相对Web软件的维护优势客户端连数据库数据结构变动通过SQL完成发布新版本只换数据库。5. 常见问题排查连不上库、中文乱码、界面卡死这几关绕不过去5.1 MySQL 8驱动加载报错ClassNotFoundException现象运行程序第一行就抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因只有两种一是mysql-connector-java的jar包没有放到项目的classpath里二是驱动类名写错。解决右键项目 - Build Path - Add External Archives选择你的MySQL驱动jar文件。确认类名时注意MySQL 5.x版驱动类是com.mysql.jdbc.DriverMySQL 8.x是com.mysql.cj.jdbc.Driver如果你手里是8.x的jar却写了5.x的类名JVM直接找不到类。建议把jar放到项目根目录下建一个lib文件夹这样整个项目拷贝给别人时驱动也带上了。5.2 明明能连上库但所有中文都显示成问号现象登录成功后界面上数据库里的中文记录全是???新写入的中文备注也是问号。原因通常是三处字符集没有统一数据库本身的字符集、JDBC连接的characterEncoding参数、SQL文件导入时的编码。解决数据库字符集在建库时已用DEFAULT CHARACTER SET utf8mb4固定JDBC参数在DBUtil里已带characterEncodingutf8剩下的坑在Eclipse的文本文件编码。Eclipse默认工作区编码可能是GBK你的Java源文件里写了中文字符串注释存盘时按GBK编码运行时Java读文件按UTF-8解码字符串全乱。解决方式是Window - Preferences - General - Workspace - Text file encoding改为UTF-8。MySQL导入SQL文件的编码命令行下加--default-character-setutf8mb4参数。5.3 修改了数据库数据界面上怎么刷新都不变现象在MySQL客户端删了一条记录Swing界面点“刷新”按钮表格里那条记录还在。原因不是程序没查询而是你刷新时用了同一个tableModel且数据没真正触发重绘。解决确认loadRecordData()内部先将tableModel.setRowCount(0)清空再装载。只有清空旧行JTable才能感知数据变化。如果清空后依然不变检查你定义的columns数组和addRow时Object数组的字段顺序是否对应数据显示错位比不显示更隐蔽。5.4 双击系统托盘关闭按钮后程序没真正退出现象点窗口右上角X号后界面消失了但Eclipse控制台显示程序还在运行小红方块没亮。原因MainFrame里某些非守护线程还活着或者你在打开子窗口后没有dispose()父窗口。Swing默认关闭操作只是隐藏窗口进程不会自动退出。解决主窗口设置setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)子窗口设置JFrame.DISPOSE_ON_CLOSE。如果这样还退出不干净在退出前检查是否有Timer或自定义线程在运行把它们在windowClosing事件里stop()掉。这个坑属于Swing项目里的“玄学”问题——看不到报错但进程就是赖着不走。5.5 在别的电脑上运行连接数据库报Communications link failure现象程序在自己电脑上一切正常拷到同事电脑运行就报Communications link failure或Connection timed out。原因CS结构下客户端和数据库服务器分离你代码里的URL写的是localhost:3306而localhost在每台电脑上指的都是自己。同事的电脑上没有装MySQL连接自然失败。解决把URL里的localhost改成数据库服务器的局域网IP例如jdbc:mysql://192.168.1.100:3306/finance。同时确认MySQL的监听地址不是127.0.0.1需要修改my.ini里的bind-address0.0.0.0并重启MySQL服务。再检查Windows防火墙是否放行了3306端口——这是局域网CS结构项目最常见的“软件写对了网络不通”的例子。6. 进阶技巧把单机记账系统做成能交付的完整工具这个项目要想从课程设计升级成真正能长期使用的工具有几个性价比极高的优化点。首推给密码加密方式做一次升级把MD5(password)改成MD5(username salt password)。这个改动只需在UserDao的登录查询和注册插入处加两行拼接逻辑但作为工程师向面试官展示安全意识的证据效果远比多写一个CRUD界面好得多。其次是账目导出功能。你不需要引入POI这种重量级库直接用CSV格式就行// 导出当前表格数据到CSV文件 private void exportToCsv(File file) throws IOException { try (PrintWriter writer new PrintWriter(file, UTF-8)) { writer.println(ID,日期,分类,收支,金额,备注); for (int i 0; i tableModel.getRowCount(); i) { writer.println( tableModel.getValueAt(i, 0) , tableModel.getValueAt(i, 1) , tableModel.getValueAt(i, 2) , tableModel.getValueAt(i, 3) , tableModel.getValueAt(i, 4) , tableModel.getValueAt(i, 5) ); } } }CSV文件Excel能直接打开转账备注里的逗号会导致列错位所以导出前用双引号包裹备注列并做转义。导出功能做完后整个系统的数据闭环才算完整——录入、存储、统计、导出功能够用交付时不需要在客户机器上再装任何工具。最后是打包发布的配置。用Eclipse自带Export功能导出Runnable JAR File选择“Package required libraries into generated JAR”这样打出来的jar包自带MySQL驱动但是体积会多出几兆换来的是在没配过驱动的机器上直接java -jar FinanceManager.jar就能跑。如果你想双击启动再写一个start.bat脚本内容是javaw -jar FinanceManager.jar放到客户端桌面旁边。实际交付还要做一次数据备份演练用mysqldump -u root -p finance backup.sql定期备份数据库确保哪天误删了账面数据能恢复。种种这些做法做完你会发现Swing项目本身不难难的是把边界情况想清楚——驱动版本换了能不能跑、换台电脑能不能连、删数据能不能恢复。我踩过最深的一个坑就是帮同学部署时因为JDBC的时区参数没写他那边明明一模一样的代码就报时区错折腾了两个小时才发现驱动版本不同导致参数要求不同。希望这些经验帮到你让你在这个长达十余年的Java经典作业组合上少花冤枉时间。本文还有配套的精品资源点击获取
返回列表