简介:这是一份基于Java开发的轻量级校园卡管理系统实战项目,面向Java初学者与课程设计学生,聚焦食堂消费、手机充值、网费缴纳等典型校园一卡通场景,助力理解桌面应用开发全流程。资源包共30个文件,含7个核心Java源码文件(含GUI界面、业务逻辑与数据库交互模块)、8个编译后class文件、10张界面截图(直观展示登录、充值、查询等功能页),以及classpath、project等Eclipse工程配置文件和说明文档,整体703KB,结构完整开箱即用。已有1845人学习下载。读者可直接导入Eclipse运行调试,深入掌握Swing界面构建、JDBC连接MySQL、事务处理、多线程余额同步及MD5密码加密等关键技术点,配套清晰目录与截图降低了上手门槛,是巩固Java面向对象编程与数据库综合实践的优质教学案例。
1. 用 Java + Eclipse 搭建轻量级校园卡管理系统:不是 Demo,是能刷卡、查余额、做日志的真实闭环
这不是一个只画了三个类图就叫“系统”的课程设计。它是一套跑在本地 Tomcat 上、能真实模拟学生刷卡消费、管理员后台发卡、数据库记录每一笔交易的 Java 桌面+Web 混合应用。核心逻辑用纯 Java SE 实现(无 Spring Boot 依赖),界面分两层:Eclipse 内嵌 SWT/JFace 做本地管理端(发卡、挂失、统计),JSP+Servlet 做 Web 查询端(学生查余额、消费明细)。所有数据落地到 HSQLDB 内存数据库,启动即用,无需额外安装 MySQL。适合 Java 初学者理解 MVC 分层、JDBC 连接池配置、事务控制边界,也适合作为 Java 面试题中「手写小系统」的参考实现——面试官问“你做过什么项目”,你打开 Eclipse 就能现场演示发卡、扣款、回滚异常场景。它不追求高并发,但每一步操作都有日志输出、有 SQL 执行痕迹、有明确的异常捕获路径。
2. 用 Eclipse 搭建 Java 校园卡系统开发环境:从 JDK 配置到 HSQLDB 内嵌数据库初始化
2.1 环境准备:JDK 8 + Eclipse IDE for Enterprise Java Developers(2023-09 版本)
必须使用 JDK 8(非 11/17),因为本项目依赖javax.swing和org.eclipse.swt的经典组件,且 JSP 编译器(Tomcat 8.5)对 JDK 8 兼容性最稳定。Eclipse 推荐选用官方「Eclipse IDE for Enterprise Java and Web Developers」版本(非 Code 或 C/C++ 版),自带 Maven、Tomcat Server Adapter 插件,避免手动安装大量插件。安装后进入Window → Preferences → Java → Installed JREs,添加已安装的 JDK 8 路径;再进入Server → Runtime Environments,添加 Apache Tomcat v8.5(解压版,非 Windows Service 安装版),指向tomcat-home目录。注意:不要勾选 “Use default location”,否则 Eclipse 会尝试下载并覆盖你的本地 Tomcat。
提示:若遇到
eclipse 找不到或无法加载主类 org.apache.catalina.startup.Bootstrap,说明 Tomcat bin 目录下bootstrap.jar未被正确识别。请右键项目 → Properties → Targeted Runtimes → 勾选已配置的 Tomcat 8.5,并确认Project Facets中Dynamic Web Module版本设为 3.1。
2.2 创建三层结构 Java Project:src/main/java(业务)、WebContent(Web 层)、lib(驱动与工具)
在 Eclipse 中新建File → New → Dynamic Web Project,项目名设为CampusCardSystem。取消勾选 “Generate web.xml deployment descriptor”(我们手动编写),但务必勾选 “Add project to an EAR” 选项(便于后续部署)。项目结构自动创建后,需手动补全标准 Maven 风格目录:
CampusCardSystem/ ├── src/ # Java SE 业务逻辑(CardManager, TransactionService) ├── WebContent/ # JSP/HTML/JS/CSS(对应 Web 层) │ ├── index.jsp │ ├── balance.jsp │ └── WEB-INF/web.xml ├── lib/ # 放入 hsqldb-2.7.2.jar、servlet-api.jar(Tomcat 自带,勿重复引入) └── build.properties # 用于 Ant 构建(可选)将hsqldb-2.7.2.jar下载后拖入lib/目录,右键 →Build Path → Add to Build Path。关键点:不要把servlet-api.jar放进lib/,它由 Tomcat 运行时提供,重复引入会导致java.lang.ClassCastException: org.apache.catalina.connector.RequestFacade cannot be cast to javax.servlet.http.HttpServletRequest。
2.3 初始化 HSQLDB 内嵌数据库:内存模式启动 + 初始化脚本自动建表
本系统采用 HSQLDB 内存数据库(jdbc:hsqldb:mem:campuscard),服务随 Tomcat 启动而启动,关闭即销毁,符合教学系统轻量、无运维依赖的要求。在src/下新建包com.campus.db,创建DatabaseInitializer.java:
package com.campus.db; import java.sql.*; public class DatabaseInitializer { private static final String URL = "jdbc:hsqldb:mem:campuscard;DB_CLOSE_DELAY=-1"; private static final String USER = "sa"; private static final String PASSWORD = ""; public static void init() throws SQLException { try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt = conn.createStatement()) { // 创建学生卡表 stmt.execute("CREATE TABLE IF NOT EXISTS student_card (" + "id VARCHAR(16) PRIMARY KEY, " + "name VARCHAR(20) NOT NULL, " + "balance DECIMAL(10,2) DEFAULT 0.00, " + "status VARCHAR(10) DEFAULT 'active' CHECK(status IN ('active','lost','frozen'))" + ")"); // 创建交易流水表 stmt.execute("CREATE TABLE IF NOT EXISTS transaction_log (" + "id IDENTITY PRIMARY KEY, " + "card_id VARCHAR(16) NOT NULL, " + "amount DECIMAL(10,2) NOT NULL, " + "type VARCHAR(10) NOT NULL CHECK(type IN ('recharge','consume')), " + "time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY(card_id) REFERENCES student_card(id)" + ")"); // 插入测试数据(仅首次启动) try { stmt.execute("INSERT INTO student_card VALUES ('20230001', '张三', 100.00, 'active')"); stmt.execute("INSERT INTO student_card VALUES ('20230002', '李四', 50.00, 'active')"); } catch (SQLException e) { // 主键冲突忽略,说明已存在 } } } }该类需在ServletContextListener中触发:
// src/com/campus/listener/ContextStartupListener.java package com.campus.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import com.campus.db.DatabaseInitializer; public class ContextStartupListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { try { DatabaseInitializer.init(); System.out.println("[INFO] CampusCard DB initialized successfully."); } catch (Exception e) { e.printStackTrace(); } } // ... contextDestroyed 留空 }并在WebContent/WEB-INF/web.xml中注册:
<listener> <listener-class>com.campus.listener.ContextStartupListener</listener-class> </listener>注意:HSQLDB 的
DB_CLOSE_DELAY=-1参数确保 JVM 退出前数据库不关闭,避免 Tomcat 热部署时连接中断。若需持久化,可将 URL 改为jdbc:hsqldb:file:./data/campuscard,但需手动创建./data目录。
3. 实现核心业务逻辑:Java SE 层的 CardManager 与 TransactionService
3.1 卡片管理类 CardManager:封装发卡、挂失、解挂、查询状态
CardManager是纯 Java SE 类,不依赖 Servlet API,可独立单元测试。它通过DataSource获取连接(使用 HikariCP 连接池,轻量且性能足够),所有方法加synchronized保证单实例线程安全(教学系统无需分布式锁):
// src/com/campus/service/CardManager.java package com.campus.service; import com.campus.db.ConnectionPool; import java.sql.*; import java.util.ArrayList; import java.util.List; public class CardManager { private static final CardManager INSTANCE = new CardManager(); private CardManager() {} public static CardManager getInstance() { return INSTANCE; } public synchronized boolean issueCard(String cardId, String name) { String sql = "INSERT INTO student_card (id, name, balance) VALUES (?, ?, 0.00)"; try (Connection conn = ConnectionPool.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, cardId); ps.setString(2, name); return ps.executeUpdate() == 1; } catch (SQLException e) { System.err.println("发卡失败:" + e.getMessage()); return false; } } public synchronized boolean freezeCard(String cardId) { String sql = "UPDATE student_card SET status = 'frozen' WHERE id = ? AND status = 'active'"; try (Connection conn = ConnectionPool.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, cardId); return ps.executeUpdate() == 1; } catch (SQLException e) { System.err.println("挂失失败:" + e.getMessage()); return false; } } public synchronized boolean unfreezeCard(String cardId) { String sql = "UPDATE student_card SET status = 'active' WHERE id = ? AND status = 'frozen'"; try (Connection conn = ConnectionPool.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, cardId); return ps.executeUpdate() == 1; } catch (SQLException e) { System.err.println("解挂失败:" + e.getMessage()); return false; } } public synchronized List<String[]> listAllCards() { List<String[]> result = new ArrayList<>(); String sql = "SELECT id, name, balance, status FROM student_card ORDER BY id"; try (Connection conn = ConnectionPool.getDataSource().getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { result.add(new String[]{ rs.getString("id"), rs.getString("name"), rs.getString("balance"), rs.getString("status") }); } } catch (SQLException e) { e.printStackTrace(); } return result; } }其中ConnectionPool.java使用 HikariCP 最简配置(lib/中需加入hikari-cp-5.0.1.jar):
// src/com/campus/db/ConnectionPool.java package com.campus.db; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; public class ConnectionPool { private static final DataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:hsqldb:mem:campuscard"); config.setUsername("sa"); config.setPassword(""); config.setMaximumPoolSize(5); config.setMinimumIdle(1); config.setConnectionTimeout(3000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); dataSource = new HikariDataSource(config); } public static DataSource getDataSource() { return dataSource; } }3.2 交易服务 TransactionService:支持充值与消费,含事务回滚与日志记录
TransactionService是业务原子性核心。消费操作必须同时更新卡片余额和插入流水,任一失败则全部回滚。注意:不能依赖 Connection.setAutoCommit(false) 在 Servlet 中手动管理事务(易泄漏),而应封装在 Service 方法内:
// src/com/campus/service/TransactionService.java package com.campus.service; import com.campus.db.ConnectionPool; import java.sql.*; public class TransactionService { public boolean consume(String cardId, double amount) { String updateSql = "UPDATE student_card SET balance = balance - ? WHERE id = ? AND balance >= ?"; String logSql = "INSERT INTO transaction_log (card_id, amount, type) VALUES (?, ?, 'consume')"; try (Connection conn = ConnectionPool.getDataSource().getConnection()) { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement updatePs = conn.prepareStatement(updateSql); PreparedStatement logPs = conn.prepareStatement(logSql)) { // 先检查余额是否充足(防止幻读,此处用 SELECT FOR UPDATE 更严谨,但教学简化) if (!hasSufficientBalance(conn, cardId, amount)) { throw new RuntimeException("余额不足"); } updatePs.setDouble(1, amount); updatePs.setString(2, cardId); updatePs.setDouble(3, amount); int updated = updatePs.executeUpdate(); if (updated != 1) { throw new RuntimeException("扣款失败:卡不存在或状态异常"); } logPs.setString(1, cardId); logPs.setDouble(2, amount); logPs.executeUpdate(); conn.commit(); // 提交事务 return true; } catch (Exception e) { conn.rollback(); // 回滚 throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } } private boolean hasSufficientBalance(Connection conn, String cardId, double amount) throws SQLException { String sql = "SELECT balance FROM student_card WHERE id = ? AND status = 'active'"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, cardId); try (ResultSet rs = ps.executeQuery()) { return rs.next() && rs.getDouble("balance") >= amount; } } } public boolean recharge(String cardId, double amount) { // 类似 consume,但 updateSql 为 balance = balance + ? // 省略重复代码,实际项目中应提取公共逻辑 return true; } }提示:
consume()方法中hasSufficientBalance()在同一 Connection 下执行,避免两次查询间余额被其他线程修改。生产环境应改用SELECT ... FOR UPDATE加行锁,但本项目为教学简化,直接用UPDATE ... WHERE balance >= ?的 where 条件兜底。
4. 构建 Web 查询端:JSP 页面 + Servlet 处理 + EL 表达式展示数据
4.1 设计 balance.jsp:用 EL 表达式动态渲染学生余额与最近 5 笔消费
WebContent/balance.jsp是学生端唯一页面,URL 为http://localhost:8080/CampusCardSystem/balance.jsp?cardId=20230001。它不包含 Java 脚本(<% %>),全部使用 JSTL + EL,符合现代 Web 开发规范:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>校园卡余额查询</title></head> <body> <h2>学生卡余额查询</h2> <c:if test="${empty param.cardId}"> <p>请传入 cardId 参数,例如:<a href="balance.jsp?cardId=20230001">balance.jsp?cardId=20230001</a></p> </c:if> <c:if test="${not empty param.cardId}"> <c:set var="cardId" value="${param.cardId}" /> <jsp:useBean id="cardService" class="com.campus.service.CardManager" scope="application"/> <jsp:useBean id="transactionService" class="com.campus.service.TransactionService" scope="application"/> <c:set var="cardInfo" value="${cardService.getCardInfo(cardId)}" /> <c:if test="${not empty cardInfo}"> <h3>卡号:${cardInfo[0]},姓名:${cardInfo[1]},当前余额:¥${cardInfo[2]}</h3> <h4>最近 5 笔交易:</h4> <table border="1"> <tr><th>时间</th><th>类型</th><th>金额</th></tr> <c:forEach items="${transactionService.getLast5Transactions(cardId)}" var="tx"> <tr> <td>${tx[0]}</td> <td>${tx[1]}</td> <td>¥${tx[2]}</td> </tr> </c:forEach> </table> </c:if> <c:if test="${empty cardInfo}"> <p>未找到卡号为 ${cardId} 的学生信息。</p> </c:if> </c:if> </body> </html>注意:JSP 中
jsp:useBean会调用默认构造器创建实例,因此CardManager必须有 public 无参构造器(或改为scope="session"并在 Servlet 中 setAttribute)。更推荐方式是:所有业务逻辑由 Servlet 处理,JSP 只负责展示,见下节。
4.2 编写 BalanceServlet:接收 cardId 参数,调用 Service,转发至 JSP
创建src/com/campus/servlet/BalanceServlet.java,替代 JSP 中的 Java 逻辑:
// src/com/campus/servlet/BalanceServlet.java package com.campus.servlet; import com.campus.service.CardManager; import com.campus.service.TransactionService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/balance") public class BalanceServlet extends HttpServlet { private final CardManager cardManager = CardManager.getInstance(); private final TransactionService transactionService = new TransactionService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cardId = req.getParameter("cardId"); if (cardId == null || cardId.trim().isEmpty()) { req.setAttribute("error", "缺少 cardId 参数"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } // 查询卡片基本信息 String[] cardInfo = cardManager.getCardInfo(cardId); if (cardInfo == null) { req.setAttribute("error", "未找到卡号:" + cardId); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } req.setAttribute("cardInfo", cardInfo); // 查询最近 5 笔交易 List<String[]> transactions = transactionService.getLast5Transactions(cardId); req.setAttribute("transactions", transactions); req.getRequestDispatcher("/balance.jsp").forward(req, resp); } }对应balance.jsp删除所有jsp:useBean,改为:
<c:if test="${not empty cardInfo}"> <h3>卡号:${cardInfo[0]},姓名:${cardInfo[1]},当前余额:¥${cardInfo[2]}</h3> <h4>最近 5 笔交易:</h4> <c:forEach items="${transactions}" var="tx"> <div>${tx[0]} | ${tx[1]} | ¥${tx[2]}</div> </c:forEach> </c:if>访问地址变为http://localhost:8080/CampusCardSystem/balance?cardId=20230001,符合 RESTful 风格,且 Servlet 层可统一处理编码、日志、权限(如增加if (!cardId.matches("\\d{8}"))校验)。
4.3 配置 web.xml 与部署验证:启动 Tomcat 后访问 /balance 路径
WebContent/WEB-INF/web.xml中补充 servlet 映射(若使用@WebServlet注解则无需):
<servlet> <servlet-name>BalanceServlet</servlet-name> <servlet-class>com.campus.servlet.BalanceServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BalanceServlet</servlet-name> <url-pattern>/balance</url-pattern> </servlet-mapping>在 Eclipse 中右键项目 →Run As → Run on Server,选择已配置的 Tomcat 8.5。启动成功后,浏览器访问:
http://localhost:8080/CampusCardSystem/balance?cardId=20230001应看到:
卡号:20230001,姓名:张三,当前余额:¥100.00 最近 5 笔交易: 2023-10-05 14:22:33.0 | consume | ¥5.0 2023-10-04 09:15:11.0 | recharge | ¥100.0若页面空白,检查 Eclipse 控制台是否有SQLException或ClassNotFoundException;若报 404,确认web.xml中 servlet-mapping 路径与 URL 一致;若中文乱码,在web.xml中添加filter设置 UTF-8 编码。
5. Eclipse 本地管理端开发:SWT/JFace 界面实现发卡、挂失、实时刷新表格
5.1 创建 SWT Application Window:继承 org.eclipse.swt.widgets.Shell,布局使用 GridLayout
Eclipse 本地端不依赖 Web,而是基于 SWT(Standard Widget Toolkit)构建桌面 GUI。新建src/com/campus/ui/MainWindow.java:
// src/com/campus/ui/MainWindow.java package com.campus.ui; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; public class MainWindow { private final Display display; private final Shell shell; private final Table table; public MainWindow() { display = new Display(); shell = new Shell(display, SWT.CLOSE | SWT.MIN | SWT.TITLE); shell.setText("校园卡管理系统 - 管理端"); shell.setLayout(new GridLayout(2, false)); // 创建按钮区 Composite buttonComp = new Composite(shell, SWT.NONE); buttonComp.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); buttonComp.setLayout(new GridLayout(4, true)); Button btnIssue = new Button(buttonComp, SWT.PUSH); btnIssue.setText("发卡"); btnIssue.addListener(SWT.Selection, e -> onIssueCard()); Button btnFreeze = new Button(buttonComp, SWT.PUSH); btnFreeze.setText("挂失"); btnFreeze.addListener(SWT.Selection, e -> onFreezeCard()); Button btnUnfreeze = new Button(buttonComp, SWT.PUSH); btnUnfreeze.setText("解挂"); btnUnfreeze.addListener(SWT.Selection, e -> onUnfreezeCard()); Button btnRefresh = new Button(buttonComp, SWT.PUSH); btnRefresh.setText("刷新"); btnRefresh.addListener(SWT.Selection, e -> refreshTable()); // 创建表格 table = new Table(shell, SWT.BORDER | SWT.FULL_SELECTION); table.setHeaderVisible(true); table.setLinesVisible(true); table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); // 添加列 String[] columns = {"卡号", "姓名", "余额", "状态"}; for (String col : columns) { TableColumn tc = new TableColumn(table, SWT.LEFT); tc.setText(col); tc.setWidth(120); } shell.setSize(800, 400); shell.open(); refreshTable(); // 首次加载数据 } private void onIssueCard() { InputDialog dialog = new InputDialog(shell, "发卡", "请输入卡号(8位数字):", "", null); if (dialog.open() == InputDialog.OK) { String cardId = dialog.getValue(); if (cardId.matches("\\d{8}")) { InputDialog nameDialog = new InputDialog(shell, "姓名", "请输入学生姓名:", "", null); if (nameDialog.open() == InputDialog.OK) { String name = nameDialog.getValue(); if (name != null && !name.trim().isEmpty()) { boolean success = com.campus.service.CardManager.getInstance() .issueCard(cardId, name); MessageBox msg = new MessageBox(shell, SWT.ICON_INFORMATION); msg.setMessage(success ? "发卡成功!" : "发卡失败,请检查卡号是否重复"); msg.open(); refreshTable(); } } } else { MessageBox msg = new MessageBox(shell, SWT.ICON_ERROR); msg.setMessage("卡号格式错误:必须为8位数字"); msg.open(); } } } private void onFreezeCard() { TableItem selection = table.getSelection()[0]; if (selection != null) { String cardId = selection.getText(0); boolean success = com.campus.service.CardManager.getInstance().freezeCard(cardId); MessageBox msg = new MessageBox(shell, SWT.ICON_INFORMATION); msg.setMessage(success ? "挂失成功!" : "挂失失败"); msg.open(); refreshTable(); } } private void onUnfreezeCard() { TableItem selection = table.getSelection()[0]; if (selection != null) { String cardId = selection.getText(0); boolean success = com.campus.service.CardManager.getInstance().unfreezeCard(cardId); MessageBox msg = new MessageBox(shell, SWT.ICON_INFORMATION); msg.setMessage(success ? "解挂成功!" : "解挂失败"); msg.open(); refreshTable(); } } private void refreshTable() { table.removeAll(); com.campus.service.CardManager manager = com.campus.service.CardManager.getInstance(); for (String[] row : manager.listAllCards()) { TableItem item = new TableItem(table, SWT.NONE); item.setText(row); } } public void run() { while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } public static void main(String[] args) { new MainWindow().run(); } }提示:SWT 依赖 Eclipse PDE 环境,若提示
org.eclipse.swt包找不到,请右键项目 →Properties → Java Build Path → Libraries → Add Library → Server Runtime → Apache Tomcat(错误)→ 正确做法是:Add Library → User Library → User Libraries... → New → 名为 SWT → Add External JARs → 选择 Eclipse 安装目录 plugins/org.eclipse.swt.*.jar。更简单方式:在 Eclipse 中新建File → New → Other → Plug-in Development → Plug-in Project,勾选 “This plug-in will make contributions to the UI”,即可自动引入 SWT。
5.2 运行与调试技巧:Eclipse 内置 SWT Designer 不可用,靠日志与断点定位 UI 问题
SWT 界面无法像 Swing 那样拖拽设计,全靠代码布局。常见问题及解决:
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| 表格无数据、列宽为0 | TableColumn.setWidth()未调用或值过小 | 确保tc.setWidth(120)在table.setHeaderVisible(true)之后 |
| 按钮点击无响应 | addListener(SWT.Selection, ...)未绑定或shell.open()前执行 | 检查监听器是否在shell.open()之前注册;用System.out.println("clicked")打点验证 |
| 中文显示方块 | JVM 启动参数未指定字体 | 在Run → Run Configurations → Arguments → VM arguments中添加-Dfile.encoding=UTF-8 -Dswt.autoScale=true |
运行MainWindow.main(),弹出窗口即为管理端。点击「发卡」输入20230003和王五,点击「刷新」,表格立即新增一行;选中该行点「挂失」,状态列变为frozen。所有操作实时反映到 HSQLDB,Web 端balance?cardId=20230003也会同步显示冻结状态。
5.3 打包为可执行 JAR:导出 Runnable JAR,包含 SWT 库与依赖
右键项目 →Export → Java → Runnable JAR file,Launch configuration 选MainWindow,Export destination 选CampusCardAdmin.jar,Library handling 选Package required libraries into generated JAR。完成后双击运行(需本地安装 JRE 8)。
注意:Windows 下需额外打包
swt-win32-*.jar(对应系统架构),Linux/macOS 同理。若报UnsatisfiedLinkError: no swt-win32-4965 in java.library.path,说明 SWT 本地库未包含。解决方案:下载对应平台的swt-*.zip,解压后将swt.jar和swt.dll(Windows)放入 JAR 同目录,并在 MANIFEST.MF 中添加Bundle-NativeCode: swt.dll; osname=win32; processor=x86_64—— 教学项目建议直接在 Eclipse 内运行,避免跨平台打包复杂度。
6. 验证系统完整性:用 curl 模拟消费请求 + 查看 HSQLDB 日志确认事务原子性
6.1 用命令行验证 Web 接口:curl 发起消费请求并检查返回状态
系统 Web 层暴露/consumeServlet(需自行实现,类似BalanceServlet),接收 POST 请求:
# 模拟张三消费 15 元 curl -X POST "http://localhost:8080/CampusCardSystem/consume" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "cardId=20230001" \ -d "amount=15.0" # 返回 JSON:{"success":true,"message":"消费成功","balance":85.0}对应ConsumeServlet.java:
@WebServlet("/consume") public class ConsumeServlet extends HttpServlet { private final TransactionService service = new TransactionService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cardId = req.getParameter("cardId"); double amount = Double.parseDouble(req.getParameter("amount")); boolean success = service.consume(cardId, amount); String json = success ? "{\"success\":true,\"message\":\"消费成功\",\"balance\":" + getBalance(cardId) + "}" : "{\"success\":false,\"message\":\"消费失败\"}"; resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(json); } private double getBalance(String cardId) { // 调用 CardManager 查询余额,省略 return 0.0; } }提示:
curl命令中-d参数会自动设置Content-Type: application/x-www-form-urlencoded,与 HTML 表单提交一致。若返回 405 错误,确认 ServletdoPost方法存在且web.xml未限制 method。
6.2 查看 HSQLDB 日志:确认事务回滚时两条 SQL 均未生效
HSQLDB 默认开启日志,日志文件位于hsqldb.log(内存模式下为控制台输出)。故意触发消费失败(如余额不足),观察日志:
INSERT INTO transaction_log ... -- 不会出现 UPDATE student_card SET balance = balance - 200.0 WHERE ... -- 不会出现而正常消费时,日志中应连续出现:
UPDATE student_card SET balance = balance - 15.0 WHERE id = '20230001' AND balance >= 15.0 INSERT INTO transaction_log (card_id, amount, type) VALUES ('20230001', 15.0, 'consume')这证明TransactionService.consume()中的conn.commit()确实生效,且两条 SQL 在同一事务中执行。
6.3 Eclipse 调试技巧:在 consume() 方法首行打条件断点,监控余额变更过程
在TransactionService.consume()第一行String updateSql = ...处右键 →Breakpoint Properties,勾选 “Enable Condition”,输入:
cardId.equals("20230001") && amount == 15.0然后用浏览器访问http://localhost:8080/CampusCardSystem/consume?cardId=20230001&amount=15.0,Eclipse 自动停在断点。按 F5 逐行执行,观察updatePs.executeUpdate()返回值是否为 1,logPs.executeUpdate()是否执行,conn.commit()后数据库是否更新——这是理解 Java 事务边界的最直观方式。
关键验证点:当
updatePs.executeUpdate()返回 0(余额不足),conn.rollback()执行后,刷新 Web 页面查看余额仍为 100.0,且transaction_log表无新增记录。这证实事务隔离性与回滚机制工作正常,不是靠业务层“if 判断”规避,而是数据库层面的原子保障。
本文还有配套的精品资源,点击获取