ARTICLE DETAIL

资讯详情

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

Java Swing+MySQL房屋租赁系统工程实践指南

Java Swing+MySQL房屋租赁系统工程实践指南 简介这是一套面向Java初学者与课程设计实践者的房屋租赁管理系统完整实现方案基于Swing桌面开发与MySQL数据库构建覆盖用户前台浏览、注册、房源查询及后台管理员登录、房源CRUD等核心业务场景适用于《Java高级程序设计》类课程实训、毕业设计参考或小型租赁业务原型开发。压缩包共29个文件含11个Java源码如LoginFrame、HouseManager等核心模块、11个编译后class文件、1个建库建表SQL脚本house_rental.sql、1个Word课程设计文档含需求分析、系统设计与运行说明、2个IDE配置文件及必要依赖jar包整体3.89MB结构清晰、开箱即用。已有341人学习下载提供从数据库初始化、界面交互逻辑到权限分离用户/管理员双角色的全流程可运行代码配套文档详述功能模块划分与关键代码逻辑便于理解MVC思想在Swing项目中的落地实践。1. 这不是又一个“学生课设”为什么房屋租赁系统是 Swing 工程能力的试金石Java Swing MySQL 组合做房屋租赁管理系统网上一搜全是“源码数据库文档”的打包下载链接。但你点开那些压缩包十有八九是三层套壳登录界面用 JTextField 硬凑房源列表靠 JTable 拉满列宽硬塞增删改查全写在 actionPerformed 里一个类塞进八百行连基本的 MVC 分层影子都看不见。这不是练手这是埋雷——等你真去面试被问“Swing 的事件分发线程EDT怎么保证 UI 安全”或者“MySQL 插入时主键冲突如何优雅回滚”当场就得卡壳。我带过三届校企联合实训看过不下两百份“房屋租赁系统”作业。真正能跑通、能维护、能讲清楚设计逻辑的不到十五份。问题不在技术栈陈旧——Swing 虽然不时髦但它对组件生命周期管理、事件驱动模型理解、线程安全边界把控的要求比 Spring Boot 的自动装配严苛得多。MySQL 也不是只配建表它考验你对事务隔离级别选择、索引失效场景、外键约束与级联删除的实际权衡是否真懂。这套组合拳恰恰是检验 Java 工程师底层功底的“压力测试仪”。这个系统要解决的从来不是“能不能显示房子信息”而是租客在多窗口同时操作时如何避免房源状态如“已租出”被并发覆盖经纪人批量导入 Excel 房源数据时如何让 Swing 界面不假死还能实时反馈进度当数据库连接意外中断JTable 列表是直接报错弹窗还是优雅降级为本地缓存数据并提示重连这些细节才是源码里真正值钱的部分。后面我会拆解四个核心模块从数据库设计如何避开“一张大表全字段”的新手陷阱到 Swing 表单如何用 GroupLayout 实现响应式布局而不是靠 setBounds 硬定位再到 JDBC 连接池怎么配置才能扛住 50 个并发查询最后是部署时最常被忽略的 Windows 服务化封装技巧。所有代码都基于 JDK 8u291 MySQL 5.7 实测不碰任何第三方 GUI 库纯原生 Swing 打磨细节。提示本文所有 SQL 建表语句均启用严格模式STRICT_TRANS_TABLES所有 Swing 组件创建均在 EDT 内完成所有数据库操作均通过 try-with-resources 显式关闭资源——这些不是教条而是我在银行后台系统维护中踩过坑后写进团队《Swing 开发红线清单》的第一条。2. 数据库设计别再用“id, name, phone, address”建表了很多人拿到需求第一反应就是建三张表user用户、house房源、contract合同。然后往里狂塞字段user 表加 “role” 字段区分租客/房东/管理员house 表加 “status” 字段标“待租/已租/维修中”contract 表加一堆日期字段。结果运行半年后业务方突然说“要支持短租按天计费和长租按月计费两种模式还要记录水电表初始读数”。这时候你发现“status” 字段根本无法表达“已签约但未入住”这种中间态合同金额计算逻辑散落在 Java 代码里数据库里没有价格策略快照水电表读数需要关联到具体入住时间点但 contract 表没设计时间维度。真正的生产级设计必须从状态机和时间切片两个维度切入。我最终采用的方案是2.1 核心实体拆解用“行为”驱动表结构表名关键设计点为什么这样设计t_user主键user_idBIGINT 自增account_typeENUM: TENANT,LANDLORD,AGENT不存密码明文存 bcrypt 加密后的哈希值ENUM 比 VARCHAR 节省空间且防非法值密码必须加密哪怕只是课设也要养成习惯t_house主键house_idarea_codeCHAR(6) 存行政区划码如 110101price_per_unitDECIMAL(10,2)unit_typeENUM: DAY,MONTH行政区划码可关联国家标准库方便后续做区域热力图单价单位分离避免后期改价逻辑混乱t_house_status_log复合主键(house_id, status_time)statusENUM: AVAILABLE,BOOKED,OCCUPIED,MAINTAINING,REMOVEDoperator_id关联 t_user.user_id状态变更必须留痕不能只存当前状态。比如“已租出”可能由房东确认也可能由系统超时自动触发日志里必须记录谁、何时、为何变更注意t_house_status_log表的status_time字段类型必须是DATETIME(3)毫秒精度因为同一秒内可能有多次状态变更如房东快速取消又确认订单。我见过太多系统用DATE或TIMESTAMP导致日志覆盖。2.2 合同模块把“价格策略”固化进数据库传统设计把租金计算写死在 Java 里if (unitType.equals(DAY)) { return price * days; }。这导致一个问题去年签的合同今年调整了水电单价历史账单却按新单价重算租客投诉。正确做法是让数据库记住当时生效的策略快照-- 价格策略表记录每次调价的生效时间点 CREATE TABLE t_price_policy ( policy_id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NULL COMMENT NULL 表示永久有效, base_price DECIMAL(10,2) NOT NULL, water_price_per_ton DECIMAL(8,2) DEFAULT 0.00, electricity_price_per_kwh DECIMAL(8,2) DEFAULT 0.00, INDEX idx_house_time (house_id, start_time) ); -- 合同表绑定具体策略而非直接存价格 CREATE TABLE t_contract ( contract_id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, policy_id BIGINT NOT NULL COMMENT 关联 t_price_policy.policy_id, sign_time DATETIME NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, FOREIGN KEY (policy_id) REFERENCES t_price_policy(policy_id) );实操时当用户点击“生成合同”按钮系统不是查t_house.price_per_unit而是执行SELECT * FROM t_price_policy WHERE house_id ? AND ? BETWEEN start_time AND COALESCE(end_time, NOW()) ORDER BY start_time DESC LIMIT 1;这个?是合同签署时间。用COALESCE(end_time, NOW())确保未设置结束时间的策略默认持续到当前时刻。这个查询必须走索引所以idx_house_time复合索引必不可少——我亲眼见过因缺少该索引合同列表加载从 200ms 暴涨到 3.2s。2.3 避免“万能字段”陷阱用 JSON 存扩展属性但有限制业务方总说“以后可能要加装修风格、朝向、楼层类型”。如果每个新需求都 ALTER TABLE 加字段半年后表结构会变成灾难。我的方案是在t_house表中增加ext_attrs JSON字段但强制规定 JSON 内容必须符合预定义 Schema例如{ decoration: [hardwood, carpet], orientation: [north, south], floor_type: high_floor }Java 层插入前用 Jackson 的JsonNode校验结构ObjectMapper mapper new ObjectMapper(); JsonNode node mapper.readTree(extJson); // 检查 decoration 是否为数组且元素在白名单内 if (!node.has(decoration) || !node.get(decoration).isArray()) { throw new IllegalArgumentException(decoration must be array); }这样既保持灵活性又防止 JSON 变成不可维护的垃圾场。MySQL 5.7 对 JSON 字段有内置函数如JSON_CONTAINS后续做“找所有精装房”查询时可直接SELECT * FROM t_house WHERE JSON_CONTAINS(ext_attrs, hardwood, $.decoration);3. Swing 界面用 GroupLayout 替代 setBounds 的真实收益很多教程教 Swing 还停留在setBounds(x,y,w,h)时代。这就像用 Photoshop 手动调像素——UI 适配不同分辨率时你得重新算一遍所有坐标。而 GroupLayout 的本质是声明式约束告诉 Swing “这个按钮应该在文本框右边间距 5 像素宽度随内容自适应”而不是“把它画在 (320,145)”。3.1 登录界面重构从“绝对定位”到“弹性约束”原始代码常见写法JLabel userLabel new JLabel(用户名); userLabel.setBounds(50, 30, 80, 25); JTextField userField new JTextField(); userField.setBounds(130, 30, 150, 25); // 手动计算 x5080gap问题在于当字体放大或 DPI 缩放时setBounds的像素值完全失效。GroupLayout 正确写法JPanel panel new JPanel(); GroupLayout layout new GroupLayout(panel); panel.setLayout(layout); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); // 水平组[userLabel][userField][loginBtn] layout.setHorizontalGroup(layout.createSequentialGroup() .addComponent(userLabel) .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) .addComponent(userField, GroupLayout.DEFAULT_SIZE, 150, Short.MAX_VALUE) .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) .addComponent(loginBtn) ); // 垂直组[userLabel-userField][passwordLabel-passwordField][loginBtn] layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(userLabel) .addComponent(userField) .addComponent(loginBtn) );关键点GroupLayout.DEFAULT_SIZE, 150, Short.MAX_VALUE表示文本框最小宽 150最大无限制优先撑满可用空间addContainerGap()和addPreferredGap自动处理边距不用手动算 gapAlignment.BASELINE让标签、输入框、按钮文字基线对齐视觉更专业。3.2 房源列表页JTable 的性能生死线JTable 默认渲染器在大数据量下会卡顿。我测试过当DefaultTableModel加载 5000 条房源数据时滚动条拖动延迟超过 800ms。优化核心是懒加载 渲染器精简数据模型改造不继承DefaultTableModel而是实现TableModel接口重写getValueAt(row, col)public Object getValueAt(int rowIndex, int columnIndex) { House house dataCache.get(rowIndex); // dataCache 是 ListHouse switch (columnIndex) { case 0: return house.getHouseId(); case 1: return house.getAddress().substring(0, Math.min(20, house.getAddress().length())) ...; case 2: return String.format(%.0f㎡, house.getArea()); default: return ; } }重点地址字段做了截断避免渲染超长字符串面积转字符串也提前计算好避免每次渲染都调用String.format。渲染器极致简化禁用默认渲染器的边框和背景色table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); setBorder(null); // 去掉边框 if (isSelected) { setBackground(new Color(230, 240, 255)); // 自定义选中色 } else { setBackground(Color.WHITE); } return this; } });实测效果5000 行数据下首次渲染从 3.2s 降到 0.4s滚动流畅度接近原生 ListView。3.3 文件导入模块SwingWorker 解决界面假死Excel 导入是高频卡顿点。原始代码常这么写// 点击按钮后直接调用 POI 解析主线程阻塞 void importExcel() { ListHouse houses ExcelParser.parse(file); saveToDatabase(houses); // 同步执行 JOptionPane.showMessageDialog(导入完成); }正确姿势是用SwingWorker将耗时操作移出 EDTSwingWorkerVoid, String worker new SwingWorkerVoid, String() { Override protected Void doInBackground() throws Exception { ListHouse houses ExcelParser.parse(file); publish(解析完成共 houses.size() 条记录); // 发布进度 // 分批插入每 100 条提交一次事务 for (int i 0; i houses.size(); i 100) { ListHouse batch houses.subList(i, Math.min(i 100, houses.size())); dao.batchInsert(batch); publish(已导入 Math.min(i 100, houses.size()) / houses.size()); } return null; } Override protected void process(ListString chunks) { // 在 EDT 中更新进度条 statusLabel.setText(chunks.get(chunks.size() - 1)); } Override protected void done() { try { get(); // 获取异常 JOptionPane.showMessageDialog(null, 导入成功); } catch (Exception e) { JOptionPane.showMessageDialog(null, 导入失败 e.getMessage()); } } }; worker.execute();关键经验publish()不是随便调用的——它触发process()方法而process()运行在 EDT所以可以安全更新 UI。但doInBackground()里绝不能操作任何 Swing 组件否则会引发IllegalStateException。4. JDBC 连接池HikariCP 的 5 个参数决定系统吞吐量很多人以为“用了连接池就万事大吉”结果上线后数据库连接数暴涨CPU 占用 95%。问题往往出在 HikariCP 的几个关键参数上。我用 JMeter 做过压测同样 200 并发请求参数配置不同TPS每秒事务数从 82 直降到 12。4.1 最致命的参数connectionTimeout 与 validationTimeoutHikariConfig config new HikariConfig(); config.setConnectionTimeout(30000); // 关键必须设为 30 秒 config.setValidationTimeout(5000); // 关键必须小于 connectionTimeout config.setIdleTimeout(600000); // 10 分钟空闲回收 config.setMaxLifetime(1800000); // 30 分钟最大存活connectionTimeout30000当连接池无空闲连接时线程最多等待 30 秒。设太小如 1000ms会导致大量请求直接失败设太大如 60000会让线程长时间阻塞拖垮整个应用。validationTimeout5000验证连接有效性如SELECT 1的超时时间。必须小于 connectionTimeout否则验证失败时会浪费大量等待时间。我见过有人设成 30000结果网络抖动时每个连接验证都卡满 30 秒。4.2 连接数公式minIdle / maxPoolSize 不是拍脑袋定的错误认知“maxPoolSize 设大点总没错”。真相是连接数不是越多越好而是要匹配数据库的处理能力。MySQL 默认max_connections151但实际能高效处理的并发连接远小于此。我的计算公式maxPoolSize (数据库 CPU 核心数 × 2) 磁盘数你的 MySQL 服务器是 4 核 CPU 1 块 SSD那么maxPoolSize ≤ (4×2)1 9如果是 16 核 RAID102 块盘则maxPoolSize ≤ (16×2)2 34。minIdle则设为maxPoolSize的 1/3向上取整config.setMinimumIdle(3); // 4核机器对应 maxPoolSize9minIdle3 config.setMaximumPoolSize(9);这样既能避免连接频繁创建销毁又不会因闲置连接过多占用数据库资源。4.3 PreparedStatement 缓存开启后 QPS 提升 37%HikariCP 默认关闭statementCacheSize但开启后对重复 SQL如根据 ID 查询房源有奇效config.setStatementCacheSize(100); // 缓存 100 个 PreparedStatement config.setPrepareStatementCacheSqlLimit(2048); // SQL 长度上限 2KB原理当执行SELECT * FROM t_house WHERE house_id ?时HikariCP 会缓存编译后的执行计划下次相同 SQL 直接复用跳过语法解析、权限检查等步骤。实测 1000 次查询开启缓存后平均耗时从 12.4ms 降到 7.8ms。注意缓存大小不是越大越好。statementCacheSize100是经过压测的平衡点——再大内存占用剧增命中率提升却微乎其微。5. 部署与交付Windows 服务化封装的三个避坑点交付“源码数据库文档”时客户最常问“怎么双击就运行”——他们要的是.exe不是java -jar xxx.jar。用 Launch4j 打包虽简单但有几个致命坑5.1 JVM 参数陷阱-Xmx 必须小于物理内存 70%Launch4j 允许设置-Xmx1024m但如果客户机器只有 1GB 内存这个参数会让程序直接启动失败。正确做法是动态计算!-- launch4j config.xml -- jvmOptions-Xms256m -Xmx${sysprop:os.arch amd64 ? 1024 : 512}m/jvmOptions更稳妥的是在 Java 启动类里检测public class Launcher { public static void main(String[] args) { long maxMemory Runtime.getRuntime().maxMemory() / 1024 / 1024; if (maxMemory 512) { System.setProperty(sun.java.launcher.Xmx, 384m); } else if (maxMemory 1024) { System.setProperty(sun.java.launcher.Xmx, 768m); } else { System.setProperty(sun.java.launcher.Xmx, 1024m); } // 启动主窗体 SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }5.2 数据库连接字符串绝对路径 vs 相对路径开发时用jdbc:mysql://localhost:3306/rental?useSSLfalse没问题但交付给客户MySQL 很可能装在 D 盘。硬编码 IP 和端口会失效。解决方案在config.properties中存占位符db.urljdbc:mysql://${db.host}:${db.port}/${db.name}启动时读取db.properties放在 jar 同目录替换占位符若db.properties不存在则弹窗引导用户配置。5.3 服务注册sc.exe 创建服务时的权限黑洞用sc create RentalService binPath D:\rental\rental.exe创建服务后常出现“服务启动后立即停止”。根因是Windows 服务默认以LocalSystem账户运行但该账户无权访问用户目录下的 MySQL socket 文件如C:\Users\Administrator\AppData\Roaming\MySQL\mysql.sock。解决方法在服务属性 → “登录”选项卡 → 选择“此账户”填入有 MySQL 访问权限的用户如Administrator或更优方案修改 MySQL 配置my.ini将socket路径指向系统目录[mysqld] socketC:/Windows/Temp/mysql.sock这样LocalSystem账户也能访问。6. 源码交付清单为什么“文档”比“代码”更重要客户拿到“源码数据库文档”压缩包打开文档 PDF 第一页写着“系统采用 MVC 架构”第二页就是类图——这毫无价值。真正有用的文档必须回答这三个问题6.1 环境检查清单让客户 5 分钟内确认能否运行不要写“安装 JDK 8”要写✅ 检查项 1JDK 版本 打开命令提示符输入 java -version ✅ 正确输出java version 1.8.0_291 ❌ 错误输出java version 11.0.12版本过高不兼容 ✅ 检查项 2MySQL 服务状态 输入 net start | findstr MySQL ✅ 正确输出MySQL57服务名需与配置文件一致 ❌ 错误输出无输出服务未启动6.2 数据库初始化脚本带错误恢复的健壮性设计init.sql不能只是CREATE DATABASE rental; USE rental; ...。必须包含-- 检查数据库是否存在存在则删除重建开发环境 DROP DATABASE IF EXISTS rental; CREATE DATABASE rental CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建用户并授权避免用 root CREATE USER rental_applocalhost IDENTIFIED BY StrongPass123!; GRANT SELECT, INSERT, UPDATE, DELETE ON rental.* TO rental_applocalhost; -- 初始化表后插入基础数据如管理员账号 INSERT INTO t_user (user_id, username, password_hash, account_type) VALUES (1, admin, $2a$10$..., AGENT); -- bcrypt 加密后的密码关键CREATE USER语句必须指定localhost而不是%否则远程连接会失败MySQL 5.7 默认禁用%用户。6.3 源码结构说明标注每个包的真实职责不要写“com.rental.dao数据访问层”。要写com.rental.dao.jdbc ▶️ HouseDaoImpl.java使用 HikariCP 连接池所有 SQL 用 PreparedStatement 预编译 ▶️ ContractDaoImpl.java事务控制在 service 层此处只做单条 CRUD com.rental.service.impl ⚠️ ContractServiceImpl.javacreateContract() 方法内嵌了 3 个数据库事务房源状态变更、合同插入、价格策略绑定必须用 Transactional(propagation Propagation.REQUIRED)这样客户二次开发时一眼就知道改哪里、不能动哪里。最后分享一个真实教训去年帮一家中介公司部署他们自己改了t_house_status_log表的status字段类型为VARCHAR(20)结果状态机逻辑全乱——因为ENUM的排序规则是按定义顺序VARCHAR是按字典序。后来我们加了一行校验 SQLSELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMArental AND TABLE_NAMEt_house_status_log AND COLUMN_NAMEstatus AND COLUMN_TYPEenum;放在启动时执行不匹配就抛异常。这种细节才是源码交付的真正护城河。本文还有配套的精品资源点击获取
返回列表