基于Java Swing与MySQL的房屋租赁管理系统开发实战
简介一套基于 Java Swing 和 MySQL 的房屋租赁管理系统项目面向 Java 课程设计、毕业设计及桌面应用初学者。系统包含前台用户注册登录、房屋分类浏览与后台管理员登录、房源信息录入修改删除查询等完整功能支持按不同条件检索房源管理员可维护房型、价格、房主等关键信息帮助读者掌握 Swing 图形界面与 MySQL 数据库的联动开发。压缩包共 29 个文件以 11 个 Java 源码、SQL 数据库脚本、课程设计文档和项目配置为主另含可运行 JAR 包整体大小仅 3.89MB。已有 342 人学习使用适合需要快速上手完整示例的读者尤其适合参考其界面布局与数据库表设计。通过源码与文档可了解系统分层、用户/管理员权限设计及房源管理流程数据库脚本便于直接导入运行是较为实用的练习与参考项目。1. 用 JavaSwingMySQL 做房屋租赁管理系统这条路还走得通吗用 JavaSwingMySQL 写一套房屋租赁管理系统在 Web 系统遍地走的今天听起来有点老派但它恰恰是很多 Java 基础学习者和应届生绕不开的一个项目形态。这个标题里的“源码数据库文档”意味着交付物是完整的前端界面是 Swing 桌面程序后端数据落在 MySQL配套的说明文档负责把表结构、运行步骤和功能清单交代清楚。它解决的是小房东或小中介最痛的几个问题——房源状态不透明、租客资料散落在 Excel、合同到期靠脑子记、收租记录对不上账。这套系统合适的场景很具体Java 课程设计或毕业设计要一个“能演示、能答辩、能跑通”的项目小规模房屋租赁门店想低成本上一个内部管理工具或者你想把 Java 基础、JDBC、Swing 组件、MySQL 这几个技能点串成一条线来练手。它不适合做成大型 SaaS但作为一个人月内能落地、交付物齐全的桌面管理系统性价比很高。下面按我从零搭这套系统的顺序把表设计、界面实现、JDBC 封装和打包交付中真正会踩的坑讲一遍。2. MySQL 表设计是底座房屋租赁系统该建哪几张表字段怎么定2.1 从房东视角倒推需求至少要有五张核心表做房屋租赁管理系统不能上来就写建表语句先站在使用者角度过一遍业务。房东或门店管理员每天要干的活无非几件登录系统看总览、登记新房源、录入租客身份信息、签合同收押金、每个月生成房租账单并登记收款、到期前查哪些合同要续签或退租。把这些动作拆开数据库表就清楚了。我这里的做法是分出五张表用户表t_user管登录账号房源表t_house记录房子本身的信息和当前状态租客表t_tenant存租客身份资料合同表t_contract关联房源和租客是业务核心账单表t_bill记录每期应收的房租、水电费及缴纳情况。表面上看合同表像是个“中间关联表”实际它比那复杂一点——合同带时间范围、租金金额、押金和支付方式这些字段直接决定后面账单怎么生成。表与表的关联关系是合同指向一条房源和一位租客账单挂在合同下面一个合同可以产生多期账单。另外两个容易被忽略的点房源和租客不能做成强关联因为同一套房退租后再租给下一个人是常态租客也要在换房后保留历史记录。所以业务状态只放在合同和房源上租客表保持纯粹。外键约束在 MySQL 里建议加上但删除策略要用好比如删除房源前先确认没有生效合同否则数据库会直接报错。2.2 建库建表一份可以直接执行的 SQL 脚本下面这段 SQL 是我常用的一套建表方案字符集统一utf8mb4存储引擎全部InnoDB。utf8mb4可以兼容手机号里出现特殊符号的情况避免后期插入数据时因为字符集不够宽而报错。CREATE DATABASE IF NOT EXISTS house_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE house_rental; DROP TABLE IF EXISTS t_bill; DROP TABLE IF EXISTS t_contract; DROP TABLE IF EXISTS t_tenant; DROP TABLE IF EXISTS t_house; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 密码演示项目可存明文生产必须加盐哈希, real_name VARCHAR(50) COMMENT 真实姓名, role VARCHAR(20) DEFAULT ADMIN COMMENT 角色ADMIN/MANAGER, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE t_house ( id INT PRIMARY KEY AUTO_INCREMENT, house_no VARCHAR(30) NOT NULL UNIQUE COMMENT 房号如B3-1402, community VARCHAR(50) NOT NULL COMMENT 小区名称, address VARCHAR(100) COMMENT 详细地址, area DECIMAL(6,2) COMMENT 面积平米, floor_no VARCHAR(10) COMMENT 所在楼层, orientation VARCHAR(10) COMMENT 朝向, rent_price DECIMAL(8,2) NOT NULL COMMENT 月租金, status TINYINT DEFAULT 0 COMMENT 0空置 1已租 2维修中, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表; CREATE TABLE t_tenant ( id INT PRIMARY KEY AUTO_INCREMENT, tenant_name VARCHAR(50) NOT NULL COMMENT 租客姓名, phone VARCHAR(20) NOT NULL COMMENT 联系电话, id_card VARCHAR(18) COMMENT 身份证号, job VARCHAR(50) COMMENT 职业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租客表; CREATE TABLE t_contract ( id INT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(32) NOT NULL UNIQUE COMMENT 合同编号, house_id INT NOT NULL COMMENT 房源ID, tenant_id INT NOT NULL COMMENT 租客ID, sign_date DATE COMMENT 签约日期, start_date DATE NOT NULL COMMENT 起租日期, end_date DATE NOT NULL COMMENT 到期日期, monthly_rent DECIMAL(8,2) NOT NULL COMMENT 月租金, deposit DECIMAL(8,2) DEFAULT 0 COMMENT 押金, pay_method VARCHAR(10) DEFAULT 月付 COMMENT 月付/季付/半年付/年付, status TINYINT DEFAULT 1 COMMENT 1生效 0已退租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_contract_house FOREIGN KEY (house_id) REFERENCES t_house(id), CONSTRAINT fk_contract_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租赁合同表; CREATE TABLE t_bill ( id INT PRIMARY KEY AUTO_INCREMENT, contract_id INT NOT NULL COMMENT 合同ID, bill_type VARCHAR(20) DEFAULT 租金 COMMENT 租金/水费/电费/物业费, bill_period VARCHAR(20) COMMENT 账期如2025-06, amount DECIMAL(10,2) NOT NULL COMMENT 应收金额, due_date DATE COMMENT 应缴日期, pay_status TINYINT DEFAULT 0 COMMENT 0未缴 1已缴, pay_time DATETIME NULL COMMENT 实际缴纳时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_bill_contract FOREIGN KEY (contract_id) REFERENCES t_contract(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账单表;这段脚本有几个参数值得细说。金额字段都用DECIMAL而不是FLOAT/DOUBLE因为浮点数存储租金会产生精度误差合同上写着 3500.00查询出来可能是 3499.9999做SUM统计时尤其明显。电话号码、身份证号、房号这些看起来像数字的字段一律用VARCHAR它们不需要参与算术运算却可能在前面出现0或者包含字母——比如房号B3-1402。日期字段用DATE类型只有记录创建时间才用DATETIME配DEFAULT CURRENT_TIMESTAMP这样插入时代码里少写一个字段。外键约束在课程设计里是加分项但要注意删除顺序。脚本里先把t_bill、t_contract这些子表 DROP 掉再 DROPt_house/t_tenant否则 MySQL 会因为你删父表时子表还引用着它而报错。这个顺序同样适用于后续手工清理测试数据。2.3 预置数据管理员账号和演示用基础数据空表跑起来的系统没法演示所以初始化脚本里要带两样东西管理员账号和几套有状态的演示数据。管理员账号用来完成登录演示演示数据则让登录后每个窗口都有内容可看。INSERT INTO t_user (username, password, real_name, role) VALUES (admin, admin123, 系统管理员, ADMIN); INSERT INTO t_house (house_no, community, address, area, floor_no, orientation, rent_price, status, remark) VALUES (B3-1402, 阳光花园, 3栋14层02号, 89.50, 14/18, 南, 3200.00, 0, 精装修带家电), (A1-0503, 翠湖天地, 1栋5层03号, 76.00, 5/11, 东, 2800.00, 1, 近地铁); INSERT INTO t_tenant (tenant_name, phone, id_card, job) VALUES (张明, 13812345678, 320102199001011234, 程序员), (李慧, 13987654321, 320102199505054321, 设计师); INSERT INTO t_contract (contract_no, house_id, tenant_id, sign_date, start_date, end_date, monthly_rent, deposit, pay_method, status) VALUES (HT2025001, 2, 1, 2025-01-01, 2025-01-10, 2026-01-09, 2800.00, 2800.00, 季付, 1); INSERT INTO t_bill (contract_id, bill_type, bill_period, amount, due_date, pay_status, pay_time) VALUES (1, 租金, 2025-01, 2800.00, 2025-01-05, 1, 2025-01-05 10:00:00), (1, 租金, 2025-02, 2800.00, 2025-02-05, 1, 2025-02-05 09:30:00), (1, 租金, 2025-03, 2800.00, 2025-03-05, 0, NULL);这里有一个很多人做错的点密码到底存不存明文。课程设计为了演示方便存明文能省事但文档里要明确写一句“生产环境必须改为 BCrypt 加盐哈希”。业务上另一个值得注意的设计是退款或者退租时不删除合同而是把状态改成 0让历史账单还能追查。这套预置数据故意只做了三笔账单、一笔合同保证系统跑起来后“到期账单查询”这些功能有内容可查又不会数据多到干扰截图和答辩演示。3. 用 Swing 把界面搭起来登录窗口、主界面和数据表格3.1 登录窗口与 JFrame 布局参数Swing 做登录窗口套路是固定的一个JFrame设定大小和关闭行为上面放两个输入框和一个按钮。但布局管理器选哪个、窗口怎么居中、回车键能不能触发登录这些细节决定了这个窗口像不像个正经软件。我一般用GridBagLayout它虽然写起来啰嗦但能精确控制组件位置和拉伸权重。public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; private JButton loginButton; public LoginFrame() { setTitle(房屋租赁管理系统 - 登录); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setSize(380, 220); setLocationRelativeTo(null); setResizable(false); JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(8, 8, 8, 8); gbc.fill GridBagConstraints.HORIZONTAL; gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名), gbc); gbc.gridx 1; gbc.weightx 1.0; usernameField new JTextField(16); panel.add(usernameField, gbc); gbc.gridx 0; gbc.gridy 1; gbc.weightx 0; panel.add(new JLabel(密 码), gbc); gbc.gridx 1; gbc.weightx 1.0; passwordField new JPasswordField(16); panel.add(passwordField, gbc); gbc.gridx 1; gbc.gridy 2; gbc.weightx 0; loginButton new JButton(登 录); panel.add(loginButton, gbc); add(panel); setVisible(true); } }GridBagConstraints里最常用到的参数是gridx/gridy组件所在的网格坐标、weightx/weighty布局变大时该组件占据的空间权重和insets组件之间的间距。setLocationRelativeTo(null)是让窗口在屏幕上居中这行容易漏漏掉之后窗口默认出现在左上角观感很差。setResizable(false)对登录窗口合适但对后面的主界面就不能用数据表格需要拉伸。登录按钮接入回车键时要给按钮调getRootPane().setDefaultButton(loginButton)这样按回车自动触发点击。这个小细节很多课程演示现场手忙脚乱地拿鼠标点按钮时就显得好用不少。3.2 用 JTabbedPane 做主界面功能区登录成功后的主窗口我更喜欢用JTabbedPane而不是铺一堆按钮的“功能菜单式”布局。原因很直接租赁管理每天高频用到的工作台、房源管理、租客管理、合同账单这几个区域做成 Tab 页切换最少点击业务员不用记菜单路径。Tab 页数量控制在四到五个标签文字用两个字或四个字比如“工作台”“房源”“租客”“合同”“账单”。public class MainFrame extends JFrame { public MainFrame(String loginUser) { setTitle(房屋租赁管理系统 - 欢迎 loginUser); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setSize(1024, 680); setLocationRelativeTo(null); JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.setTabPlacement(JTabbedPane.TOP); tabbedPane.addTab(工作台, new DashboardPanel()); tabbedPane.addTab(房源, new HousePanel()); tabbedPane.addTab(租客, new TenantPanel()); tabbedPane.addTab(合同, new ContractPanel()); tabbedPane.addTab(账单, new BillPanel()); add(tabbedPane, BorderLayout.CENTER); } }每个 Tab 页对应一个独立的自定义面板类这样做的好处是各自的表格、按钮和查询逻辑互不干扰主窗口代码非常干净。setTabPlacement默认就在顶部写出来是提醒自己团队里的新手别随便改成左边或底部那会让表格可用宽度变小。主窗口大小我习惯设为 1024×680这个尺寸在 20 寸以上显示器上能展示表格完整列又能容纳大多数笔记本屏幕。3.3 JTable 展示数据DefaultTableModel 的正确用法房源列表和账单列表是这套系统使用频率最高的两块界面实现核心是JTable加DefaultTableModel。JTable本身只管显示数据由TableModel提供。你不需要继承AbstractTableModel去手写那些抽象方法用一个DefaultTableModel实例通过setDataVector或循环addRow就能把数据库查出来的数据灌进表格。public class HousePanel extends JPanel { private JTable table; private DefaultTableModel model; public HousePanel() { setLayout(new BorderLayout(0, 10)); JButton refreshBtn new JButton(刷新); JPanel topBar new JPanel(new FlowLayout(FlowLayout.LEFT)); topBar.add(refreshBtn); add(topBar, BorderLayout.NORTH); String[] columns {ID, 房号, 小区, 面积, 租金, 状态}; model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; table new JTable(model); table.setRowHeight(28); table.getTableHeader().setReorderingAllowed(false); add(new JScrollPane(table), BorderLayout.CENTER); refreshBtn.addActionListener(e - loadData()); loadData(); } private void loadData() { model.setRowCount(0); // 这里调用 Service 层查询先留空第4章补实现 } }DefaultTableModel(columns, 0)的第一个参数是表头数组第二个是初始行数改成 0 表示空表格模型。重写isCellEditable返回false非常关键否则用户在界面上单击单元格就能改数值而且改完不落库容易产生“我明明改了怎么数据库没变”的困惑。setReorderingAllowed(false)禁止表头列拖拽换位避免业务员无意中把“租金”和“状态”两列调换后看错数据。JTable数据显示必须包一层JScrollPane不包的话表头不会固定在窗口顶部而且表格没有滚动条数据超过窗口高度时就翻车了。加载数据时先model.setRowCount(0)清空旧数据再循环添加新行比addRow前手动删除旧行少暴露数据错乱的概率。3.4 事件监听与查询逻辑分离界面上的“添加房源”“删除合同”“标记已缴”这些按钮事件监听里直接写 SQL 是代码腐化的开始。监听器里只做三件事取界面参数、调 Service 方法、刷新界面数据。比如点击“添加房源”按钮getSource()是按钮实例从输入框取出文本拼成实体对象传给 ServiceService 返回主键 ID界面再调用loadData()刷新表格。loginButton.addActionListener(e - { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(LoginFrame.this, 用户名和密码不能为空); return; } boolean ok new LoginService().checkLogin(username, password); if (ok) { dispose(); new MainFrame(username).setVisible(true); } else { JOptionPane.showMessageDialog(LoginFrame.this, 用户名或密码错误); } });new String(passwordField.getPassword())是JPasswordField的标准取值方式不要用getText()后者已被标记为废弃。登录失败时用JOptionPane.showMessageDialog弹窗同时保留当前窗口让用户改密码。判断用户名密码为空要在拼接参数之前做这里简单的逻辑可以提前拦截大多数误触回车导致的空查询。值得强调的是登录查询用了真实的 Service 方法而不是直接在监听器里写 JDBC这样后续把系统改成数据库连接池、把密码校验换成加密算法时界面代码不需要动。桌面程序的维护难度往往不在某个窗口写得多复杂而在业务规则散落在一堆按钮监听器里等到你找不到某条规则在哪触发时就会后悔当初没做这层分离。4. JDBC 连接与业务逻辑连接参数、工具类和 Service 分层4.1 连接参数MySQL 版本不同驱动类和 URL 不一样JDBC 连接 MySQL 的第一步是确认你用的 MySQL 版本这决定了驱动类名和 URL 参数。MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.x 之后改成com.mysql.cj.jdbc.Driver如果驱动类写错程序启动时直接抛ClassNotFoundException。另一个坑是时区参数8.x 版本不在 URL 里指定serverTimezone连接时会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized原因是 MySQL 服务器时区没写进 JDBC 的识别表。环境驱动类JDBC URLMySQL 5.7 mysql-connector-java 5.xcom.mysql.jdbc.Driverjdbc:mysql://localhost:3306/house_rental?useSSLfalseuseUnicodetruecharacterEncodingutf8MySQL 8.0 mysql-connector-java 8.xcom.mysql.cj.jdbc.Driverjdbc:mysql://localhost:3306/house_rental?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse在本地开发时必须显式声明否则 8.0 驱动默认尝试 SSL 握手日志里会多出好几行警告某些网络环境下还会拖慢连接建立。characterEncodingutf8负责数据库读写时的中文字符集转换漏掉这一项是中文乱码最常见的原因。参数里的house_rental要替换成你自己的数据库名账号密码默认 root 和安装时设置的密码不要带着默认密码直接交付。4.2 一个顺手够用的 DBHelper 工具类数据库连接不该在每个窗口类里通过DriverManager.getConnection获取那样同一时间点每个窗口各开各的连接资源浪费还难排查。我习惯先写一个静态工具类统一管理驱动加载、连接获取和资源释放。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBHelper { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/house_rental ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败检查 mysql-connector-java 是否在依赖中, e); } } private DBHelper() { } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }静态代码块里的Class.forName(DRIVER)保证类首次加载时就完成驱动注册如果项目里忘了放 MySQL 驱动 jar异常会在这里第一时间爆发错误信息“驱动加载失败”比连接时再报错更容易定位。工具类的构造函数设为private防止别人new DBHelper()制造出无意义实例。close方法按ResultSet、Statement、Connection的顺序倒序释放因为结果集依赖语句、语句依赖连接先关连接再关结果集会残留资源。这里加一个提醒密码硬编码在常量类里对项目演示够用但当你把这个项目放进简历或团队仓库时至少改成读取src/main/resources/db.properties配置文件。JDBC 连接配置和代码分离是进入开发现场的第一条规矩数据库密码换掉时不用重新编译整个程序。4.3 Service 层封装把 SQL 从界面里挖出来有了DBHelper接下来就要把各类业务查询封装到对应的 Service 类中。拿登录来说LoginService只负责校验账号密码拿房源查询来说HouseService只负责返回房源列表。界面层不接触Connection和PreparedStatement它只拿到业务结果。import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class HouseService { public ListString[] queryHouseList(String keyword) { ListString[] rows new ArrayList(); String sql SELECT id, house_no, community, area, rent_price, CASE status WHEN 0 THEN 空置 WHEN 1 THEN 已租 ELSE 维修中 END FROM t_house WHERE community LIKE ? OR house_no LIKE ? ORDER BY id DESC; try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String likePattern % keyword %; ps.setString(1, likePattern); ps.setString(2, likePattern); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { rows.add(new String[]{ String.valueOf(rs.getInt(id)), rs.getString(house_no), rs.getString(community), rs.getString(area), rs.getString(rent_price), rs.getString(status) }); } } } catch (SQLException e) { e.printStackTrace(); } return rows; } }PreparedStatement的值要占位符?不允许把参数直接拼接进 SQL 字符串。上面LIKE ?配合%keyword%能实现模糊搜索如果直接WHERE community LIKE % keyword %用户输入一个单引号就可能让整条 SQL 崩溃这就是老生常谈的 SQL 注入。Java 8 开始Connection、Statement、ResultSet都实现了AutoCloseable所以try-with-resources语法可以自动关闭连接不必手动调用DBHelper.close三件套。CASE status WHEN 0 THEN 空置这种写法是把数据库里的数字状态翻译成人类可读的中文在 SQL 层完成映射比在 Java 里写一堆if/else if更符合查询直觉。返回值用ListString[]是因为DefaultTableModel恰好接受String[]数组作为一行数据省去在界面层再转型的代码。等你做到后面可以给每张表配一个实体类比如House对象那是更规范的做法但小型桌面系统用String[]传递行列数据效率最高、也最好懂。4.4 登录校验的具体实现与参数传递登录逻辑在系统里只会写一次但它是理解“界面和业务分离”的最小例子。LoginService接收界面传来的用户名和密码去t_user表里查有没有匹配记录返回布尔值给界面弹窗或跳转。public class LoginService { public boolean checkLogin(String username, String password) { String sql SELECT COUNT(*) FROM t_user WHERE username ? AND password ?; try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1) 0; } } } catch (SQLException e) { e.printStackTrace(); } return false; } }这里查询条件把密码直接放进了 SQL是因为演示系统里t_user.password存的是明文。如果在第 2 章你按生产标准做了加盐哈希那登录逻辑就要改成查“用户名是否存在”再在内存里比对哈希。逻辑上说只按用户名查出用户记录然后比密码摘要更接近真实系统的做法。SELECT COUNT(*)返回的是第一列整数所以读取时用rs.getInt(1)即可。用户名和密码的setString顺序必须与?出现的顺序一致这个顺序错位是 JDBC 排错里最隐蔽的问题之一。5. 避坑指南中文乱码、连接失败、界面假死和打包问题排查5.1 中文乱码数据库读出 ????Swing 界面上显示问号现象程序启动后房源表里的小区名、租客姓名在 Swing 界面上显示成???或者通过程序插入的“阳光花园”在 MySQL 命令行里变成????。原因字符集不统一。常见的有三类数据库或表不是utf8mb4JDBC URL 没有带characterEncodingutf8Java 源码文件本身不是 UTF-8 编码IDE 默认 GBK 时中文字符串在编译期已经损坏。解决按三层逐一检查。第一层建库建表时明确DEFAULT CHARACTER SET utf8mb4对老库执行ALTER TABLE t_house CONVERT TO CHARACTER SET utf8mb4补救。第二层JDBC URL 加上characterEncodingutf8MySQL 8.0 还要确认serverTimezoneAsia/Shanghai。第三层IDEA 里把 File Encoding 全部设为 UTF-8并且在javac编译时指定-encoding UTF-8。这三层只要有一层不匹配乱码就会阴魂不散。5.2 数据库连接失败Communications link failure 到底是谁的锅现象启动程序点击登录控制台报Communications link failure或Access denied for user rootlocalhost程序直接崩溃。原因前三位的嫌疑犯分别是 MySQL 服务没启动、用户名密码错误、驱动版本和数据库版本不匹配。其中端口被占用和防火墙拦截在本地开发中也会出现但不常见。解决先用netstat -ano | findstr :3306确认 MySQL 端口在监听再用命令行mysql -u root -p验证账号密码能否手动登录最后核对 pom.xml 或 lib 目录里的驱动 jar 版本。如果你是跟着 mysql 安装配置教程装数据库时装偏了版本比如驱动用 8.x 连 5.7URL 还沿用老的com.mysql.jdbc.Driver就会看到ClassNotFoundException换成com.mysql.cj.jdbc.Driver后立刻恢复。另外localhost和127.0.0.1在某些 MySQL 用户授权表里是两个不同的账号连接地址要和授权 Host 一致。5.3 Swing 界面点击查询后假死窗口变成“黑匣子”现象点击“加载数据”按钮后整个窗口无法拖动、无法关闭标题栏显示“未响应”有时候过几秒恢复有时候永远卡死。原因这是 Swing 单线程模型的经典陷阱。按钮的ActionListener默认在事件分发线程EDTEvent Dispatch Thread执行你在监听器里执行 JDBC 查询、扫描大表、写文件EDT 被阻塞界面就无法处理重绘和点击事件。解决耗时操作丢到新线程查询完成后再通过SwingUtilities.invokeLater回到 EDT 更新表格。refreshBtn.addActionListener(e - { refreshBtn.setEnabled(false); new Thread(() - { ListString[] data new HouseService().queryHouseList(); SwingUtilities.invokeLater(() - { model.setRowCount(0); for (String[] row : data) { model.addRow(row); } refreshBtn.setEnabled(true); }); }).start(); });setEnabled(false)能防止用户在线程执行期间重复点击产生并发查询。invokeLater里更新表格能保证界面数据刷新和重绘发生在 EDT 上不会出现半个表格刷新的撕裂现象。如果你不想手动管理线程用SwingWorkerListString[], Void更强但新手阶段先用Thread invokeLater看清线程模型等理解透了再换装备。5.4 打包成 jar 后双击没反应缺驱动和清单文件现象在 IDEA 里点 Run 一切正常导出成可执行 jar 后双击图标没有任何反应或者命令行执行java -jar house.jar直接报NoClassDefFoundError: com/mysql/cj/jdbc/Driver。原因这是 Fat Jar带依赖的 jar没打全。IDEA 的默认 Build Artifact 只打包项目自己的 classMySQL 驱动在外部依赖里自然不会被包含。另一个可能是MANIFEST.MF里没有Main-Class声明jvm 不知道入口在哪。解决如果项目是 Maven在pom.xml里配置maven-assembly-plugin或maven-shade-plugin打 fat jar如果是纯 IDEA 工程Artifacts 配置时把mysql-connector-java的 “Output Layout” 拖进 jar 包。验证是否可用构建后执行jar tf target/house.jar | grep mysql看到com/mysql/cj/jdbc/Driver.class才算成功。打完包之后用java -jar从命令行启动保留控制台输出很多运行时错误能直接看到堆栈比双击黑匣子靠谱得多。5.5 JDBC 驱动找不到lib 目录里的 jar 形同虚设现象程序编译通过一运行时抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver把 jar 放在项目根目录或桌面都没用。原因classpath 没有包含 jar。IDE 里必须把 jar 添加为 Library 或者用 Maven 声明依赖文件存在但不在 classpath 上等于白放。解决IDEA 里选中mysql-connector-java-8.0.x.jar右键Add as Library或者在pom.xml中声明mysql依赖后执行mvn dependency:copy-dependencies把所有依赖打到 target 目录。这里有一条血泪经验不要在联网状态下一路点重试下到一半的 jar 文件常常几百字节放到 classpath 里会报奇怪的Bad magic number错误看见这个报错就删掉 jar 重新下载。6. 从“能跑”到“好用”事务边界、到期提醒和交付物检查清单系统界面和 CRUD 都跑通后真正让它从课程设计升级成“能交付的内部工具”靠的是三个进阶细节。第一是事务控制比如管理员把一笔账单标记为已缴时要同时更新账单状态和合同的租期记录这两个操作必须在一个事务里否则出现“钱收了但系统说没缴”就麻烦。用Connection手动控制事务时conn.setAutoCommit(false)后执行多条 SQL全部成功后conn.commit()任何一步失败就conn.rollback()注意这里不能把Connection放进 try-with-resources否则commit前连接就被自动关了。第二是合同到期提醒这是我非常推荐加的一个功能。查询今天到未来七天内的合同用 SQL 的DATEDIFF(end_date, CURDATE()) BETWEEN 0 AND 7在“工作台”Tab 页用一个醒目的红色表格展示。这个小功能对房东的实际价值最大但成本很低大约只需要一个查询、一个表格和一个刷新按钮。第三类细节是给程序写一个简单的启动日志文件比如在main方法里把启动时间和关键报错写入app.log员工用系统时遇到问题远程看一眼日志就能定位是数据库没启动还是代码里某个 NullPointer。最后是交付物检查清单。源码目录要保证不含本机绝对路径的配置文件数据库脚本house_rental.sql必须能在一台全新的 MySQL 上从头执行成功文档至少要写清三部分功能模块说明加每个窗口截图、表结构数据字典、从安装 MySQL 到启动 jar 的逐步流程。文档不用长让一个没接触过项目的人能按步骤跑起来就够了。我自己曾经交付项目时漏掉了 MySQL 8.0 的serverTimezone参数对方装完连不上远程排查了半小时后来把抢修过程补进了文档之后类似的求助电话就再没响过。希望这套从表设计到打包排查的经验帮到你——做 Swing 项目不复杂耐着性子把数据层和界面层拆干净后面每一步都走得稳。本文还有配套的精品资源点击获取