
简介这是一份基于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 做本地管理端发卡、挂失、统计JSPServlet 做 Web 查询端学生查余额、消费明细。所有数据落地到 HSQLDB 内存数据库启动即用无需额外安装 MySQL。适合 Java 初学者理解 MVC 分层、JDBC 连接池配置、事务控制边界也适合作为 Java 面试题中「手写小系统」的参考实现——面试官问“你做过什么项目”你打开 Eclipse 就能现场演示发卡、扣款、回滚异常场景。它不追求高并发但每一步操作都有日志输出、有 SQL 执行痕迹、有明确的异常捕获路径。2. 用 Eclipse 搭建 Java 校园卡系统开发环境从 JDK 配置到 HSQLDB 内嵌数据库初始化2.1 环境准备JDK 8 Eclipse IDE for Enterprise Java Developers2023-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 Projectsrc/main/java业务、WebContentWeb 层、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.jarTomcat 自带勿重复引入 └── 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.javapackage 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-classcom.campus.listener.ContextStartupListener/listener-class /listener注意HSQLDB 的DB_CLOSE_DELAY-1参数确保 JVM 退出前数据库不关闭避免 Tomcat 热部署时连接中断。若需持久化可将 URL 改为jdbc:hsqldb:file:./data/campuscard但需手动创建./data目录。3. 实现核心业务逻辑Java SE 层的 CardManager 与 TransactionService3.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 ListString[] listAllCards() { ListString[] 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?cardId20230001。它不包含 Java 脚本% %全部使用 JSTL EL符合现代 Web 开发规范% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle校园卡余额查询/title/head body h2学生卡余额查询/h2 c:if test${empty param.cardId} p请传入 cardId 参数例如a hrefbalance.jsp?cardId20230001balance.jsp?cardId20230001/a/p /c:if c:if test${not empty param.cardId} c:set varcardId value${param.cardId} / jsp:useBean idcardService classcom.campus.service.CardManager scopeapplication/ jsp:useBean idtransactionService classcom.campus.service.TransactionService scopeapplication/ c:set varcardInfo value${cardService.getCardInfo(cardId)} / c:if test${not empty cardInfo} h3卡号${cardInfo[0]}姓名${cardInfo[1]}当前余额¥${cardInfo[2]}/h3 h4最近 5 笔交易/h4 table border1 trth时间/thth类型/thth金额/th/tr c:forEach items${transactionService.getLast5Transactions(cardId)} vartx 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 无参构造器或改为scopesession并在 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 笔交易 ListString[] 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} vartx div${tx[0]} | ${tx[1]} | ¥${tx[2]}/div /c:forEach /c:if访问地址变为http://localhost:8080/CampusCardSystem/balance?cardId20230001符合 RESTful 风格且 Servlet 层可统一处理编码、日志、权限如增加if (!cardId.matches(\\d{8}))校验。4.3 配置 web.xml 与部署验证启动 Tomcat 后访问 /balance 路径WebContent/WEB-INF/web.xml中补充 servlet 映射若使用WebServlet注解则无需servlet servlet-nameBalanceServlet/servlet-name servlet-classcom.campus.servlet.BalanceServlet/servlet-class /servlet servlet-mapping servlet-nameBalanceServlet/servlet-name url-pattern/balance/url-pattern /servlet-mapping在 Eclipse 中右键项目 →Run As → Run on Server选择已配置的 Tomcat 8.5。启动成功后浏览器访问http://localhost:8080/CampusCardSystem/balance?cardId20230001应看到卡号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布局使用 GridLayoutEclipse 本地端不依赖 Web而是基于 SWTStandard 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 那样拖拽设计全靠代码布局。常见问题及解决问题现象原因解决方案表格无数据、列宽为0TableColumn.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.encodingUTF-8 -Dswt.autoScaletrue运行MainWindow.main()弹出窗口即为管理端。点击「发卡」输入20230003和王五点击「刷新」表格立即新增一行选中该行点「挂失」状态列变为frozen。所有操作实时反映到 HSQLDBWeb 端balance?cardId20230003也会同步显示冻结状态。5.3 打包为可执行 JAR导出 Runnable JAR包含 SWT 库与依赖右键项目 →Export → Java → Runnable JAR fileLaunch configuration 选MainWindowExport destination 选CampusCardAdmin.jarLibrary 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.dllWindows放入 JAR 同目录并在 MANIFEST.MF 中添加Bundle-NativeCode: swt.dll; osnamewin32; processorx86_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 cardId20230001 \ -d amount15.0 # 返回 JSON{success:true,message:消费成功,balance:85.0}对应ConsumeServlet.javaWebServlet(/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;charsetUTF-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?cardId20230001amount15.0Eclipse 自动停在断点。按 F5 逐行执行观察updatePs.executeUpdate()返回值是否为 1logPs.executeUpdate()是否执行conn.commit()后数据库是否更新——这是理解 Java 事务边界的最直观方式。关键验证点当updatePs.executeUpdate()返回 0余额不足conn.rollback()执行后刷新 Web 页面查看余额仍为 100.0且transaction_log表无新增记录。这证实事务隔离性与回滚机制工作正常不是靠业务层“if 判断”规避而是数据库层面的原子保障。本文还有配套的精品资源点击获取