尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JSP+Servlet+MySQL一卡通系统实战:从分层架构到Tomcat部署

JSP+Servlet+MySQL一卡通系统实战:从分层架构到Tomcat部署 简介基于 JSPJava ServletMySQL 技术栈的校园卡一卡通管理系统完整源码面向 Java Web 学习者、计算机专业毕业生及需要开发校园消费管理场景的开发者。系统实现身份识别、账户充值、消费记账、管理员审核等功能采用 MVC 分层设计覆盖 Controller、Service、Dao 完整调用链适合课程设计、毕业设计或学习传统 Java Web 工程结构。压缩包共 219 个文件、5.6MB包含 26 个 JSP 页面负责界面渲染、28 个 Java 类承担业务处理16 份 CSS 与 9 份 JS 完善前端样式和交互另有 45 个 GIF 动图与 24 张 JPG 截图用于演示流程一并提供 SQL 数据库初始化脚本和 jar 依赖库可直接导入 IDE 运行。已有 276 人在线学习该资源适合正在开展校园管理类 Web 项目的读者参考。通过阅读源码可快速理解 Servlet 控制层、业务层与 Dao 持久层的分层实现方式并结合项目截图核对界面与代码的对应关系数据库文件能帮助搭建测试环境是快速掌握 JSP 项目开发流程的实用素材。1. 从一次刷卡说起JSP 一卡通系统到底在做什么想象一下学生在食堂刷一次卡卡片机把学号、金额、时间发给后台后台先在账户表里扣一次余额再往消费记录表插一条账单最后在 JSP 页面上把余额刷新出来。这一串动作跨了前台页面、Servlet 控制器、Service 业务层、DAO 数据层和 MySQL 事务任何一个环节掉链子都会出现扣款了但查不到记录这类经典故障。这套 JSP JAVA MYSQL 的校园卡一卡通管理系统正是把这些步骤拆成了用户管理、账户管理、消费账单、充值申请、管理员审核几个模块代码包里的BillDaoImpl.class、AdminBillServlet.class、UserServiceImpl.class等类分别对应着数据持久化、请求分发和业务逻辑三层。适合刚学完 Java Web 基础、想找完整项目练手的人也适合已经在做毕设或课程设计、需要参考分层写法的同学。它能帮你理清一件事一个请求从 Tomcat 到 JSP 再回到浏览器的完整生命周期远比你背熟的 MVC 概念图具体得多。2. 数据库与实体设计从表结构看一卡通的功能边界2.1 五张核心表如何划分职责一卡通系统的核心是账户余额和交易流水这两块数据。常见做法是把持卡人信息、账户余额、消费记录、充值记录、申请审核拆成独立的表而不是全塞进一张大表里。表划分的合理性直接决定了后续统计报表和权限控制的复杂度。我一般会按下面的方式设计字段名以项目源码中的实际变量为准但结构基本是这几类表名核心字段作用usersuser_id, card_no, user_name, password, role, status存放持卡人基础信息和角色学生/教师/管理员accountsaccount_id, user_id, balance, is_active一张卡对应一条余额记录金额用 decimal 不用 floatbillsbill_id, user_id, amount, item_name, bill_time每笔消费的明细支撑后续的账单查询deposit_recordsdeposit_id, user_id, amount, deposit_time充值流水与 bills 分开记录applyapply_id, user_id, apply_type, apply_status, create_time补卡、挂失、开通权限等申请流程为什么余额要单独拆成 accounts 表而不是直接放在 users 里因为一卡通可能涉及多张卡、挂失换卡、临时卡等场景单独拆表后换卡只需要修改 accounts 表的外键关联持卡人的基本信息不受影响而且余额字段的读写频率远高于用户资料拆表后数据库的锁竞争范围更小。金额字段必须用DECIMAL(10,2)用 float 做累加和扣减会产生浮点误差这在财务类数据里是不能接受的。2.2 DAO 层实现BillDaoImpl 与 UserDaoImpl 的职责边界源码中的BillDaoImpl.class和UserDaoImpl.class是典型的 DAO 层实现。DAO 层只负责一件事把 Java 对象转换成 SQL 执行再结果集转换成 Java 对象。它不关心业务逻辑更不关心请求从哪个 Servlet 过来。以插入一条消费记录为例BillDaoImpl 里通常会有这么一段public int insertBill(Connection conn, Bill bill) throws SQLException { String sql INSERT INTO bills(user_id, amount, item_name, bill_time) VALUES(?, ?, ?, NOW()); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bill.getUserId()); ps.setBigDecimal(2, bill.getAmount()); ps.setString(3, bill.getItemName()); return ps.executeUpdate(); } }这里有几个细节值得注意。conn是从外部传入的而不是在 DAO 内部开启新连接这样做的目的是让 Service 层控制事务边界——一次扣款需要同时更新 accounts 表余额和插入 bills 表记录两个操作要么一起成功要么一起回滚。PreparedStatement的?占位符能避免拼接 SQL 字符串产生的注入风险比如用户传一个1 OR 11进来预编译会把它当成普通字符串处理。返回值executeUpdate()返回受影响的行数如果返回 0 说明插入失败Service 层可以根据这个值决定是否抛出异常。2.3 业务层的事务控制BillServiceImpl 如何保证扣款和记账一致BillServiceImpl.class对应 Service 层实现它的职责是编排 DAO 方法。只贴 DAO 代码看不出问题真正容易翻车的是这里扣余额和加账单是两个 SQL 操作如果第一条执行成功、第二条抛异常数据库里就会出现钱扣了但消费记录丢了的脏数据。下面这段是常见的事务控制写法public void consume(int userId, BigDecimal amount, String itemName) { Connection conn null; try { conn DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); // 关闭自动提交开启手动事务 AccountDaoImpl accountDao new AccountDaoImpl(); BillDaoImpl billDao new BillDaoImpl(); int rows accountDao.deductBalance(conn, userId, amount); if (rows 0) { throw new RuntimeException(余额不足或账户不存在); } billDao.insertBill(conn, buildBill(userId, amount, itemName)); conn.commit(); // 两条 SQL 都成功才提交 } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(消费失败事务已回滚, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码里的关键在于setAutoCommit(false)之后的commit()和rollback()配对。很多初学的人把事务写在 DAO 层导致每个 DAO 方法自己开事务、自己提交Service 层根本控制不了整体原子性。另外注意deductBalance的 SQL 一般写成UPDATE accounts SET balance balance - ? WHERE user_id ? AND balance ?把余额判断放进 SQL 条件里比先 SELECT 查出来再在 Java 里比较更安全——数据库的行级锁能避免并发扣款时超扣。3. 从 JSP 表单到 Servlet一次充值请求的完整生命周期3.1 用户点击充值后 Tomcat 内部发生了什么当用户在浏览器里点下充值按钮Tomcat 会按 URL 映射找到对应的 Servlet。假设表单提交到/adminBillweb.xml里配置了servlet-mapping那么容器会把这个请求交给AdminBillServlet.class。ServletRequest 对象携带着表单里的参数到达 Servlet比如actionrecharge、amount100、userId21。Tomcat 会为每次请求创建一个线程来处理这也是为什么 Servlet 是单实例多线程工作模式——AdminBillServlet的成员变量如果被多个请求同时修改会引发线程安全问题。我见过不止一个人把数据库连接直接写成 Servlet 成员变量高并发下连接被多个线程抢用最后报Connection is closed或者数据错乱就是这个原因。3.2 AdminBillServlet用 action 参数区分操作类型一个后台管理 Servlet 往往要处理列表查询、审核通过、审核拒绝、账户充值多个动作。项目里的AdminBillServlet通常用请求参数action来区分WebServlet(/adminBill) public class AdminBillServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (recharge.equals(action)) { doRecharge(request, response); } else if (list.equals(action)) { doList(request, response); } else if (audit.equals(action)) { doAudit(request, response); } else { response.sendError(HttpServletResponse.SC_BAD_REQUEST); } } }WebServlet(/adminBill)是 Servlet 3.0 以后的注解式配置省掉了 web.xml 里的servlet声明适合这个量级的项目。doPost里先统一设置字符编码因为 Tomcat 8 及以上版本默认按 UTF-8 解码 POST 请求但旧版本或者手动修改过server.xml的情况需要显式声明。把大段的业务逻辑写在 doGet/doPost 里是新手常见问题拆成doRecharge、doAudit这样的私有方法后每个方法的职责一目了然也方便后续加单元测试。充值操作落到 Service 层时完整的调用链是AdminBillServlet.doRecharge()→UserServiceImpl.updateBalance()→AccountDaoImpl.update()。表单里传的userId如果是普通 StringService 层必须做一次数值校验否则传abc进来会在Integer.parseInt抛 NumberFormatException。3.3 JSP 页面上的表单与数据展示对应到 JSP 源码充值页面大概是这样的结构% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h3账户充值/h3 form action${pageContext.request.contextPath}/adminBill methodpost input typehidden nameaction valuerecharge 用户IDinput typetext nameuserIdbr 金额input typetext nameamountbr input typesubmit value确认充值 /form /body /html${pageContext.request.contextPath}是 JSP EL 表达式输出当前项目的上下文路径。它是 jsp 个人信息展示页面里经常用到的基本功——如果直接写死/jsp-campus-card这种硬编码路径项目改名或者部署到别的 Tomcat 目录下所有链接都会 404。表单里那个hidden的action字段就是上一节 Servlet 里request.getParameter(action)的数据来源。顺带说一个 jsp 编译 class 文件保存在哪里的问题。Tomcat 首次访问 jsp 页面时会把 jsp 翻译成 .java 再编译成 .class默认存放在$TOMCAT_HOME/work/Catalina/localhost/项目名/org/apache/jsp/下。如果改了 JSP 页面但刷新没生效多半是 Tomcat 没有重新编译删除 work 目录重启即可这一步在mysql 安装配置教程和web项目配置tomcat后查看jsp编译后的java类这些需求里经常被一起问到。3.4 GET 与 POST 请求的乱码处理差别充值只改数据必须用 POST。但订单查询、消费明细这类只读操作往往用 GET 方便分享链接。GET 请求的参数放在 URL 里Tomcat 8 之前默认用 ISO-8859-1 解码 URL所以中文参数会出现乱码。Tomcat 8 之后默认改成 UTF-8但server.xml里的URIEncoding属性仍然可以直接配置。安全起见项目里可以加一个全局过滤器统一设置编码而不是在每个 Servlet 里重复写request.setCharacterEncodingWebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }这样不管 POST 参数还是转发到 JSP 后的输出字符编码都统一成了 UTF-8能省掉一多半中文乱码问题。filter 的chain.doFilter一定要放在设置编码之后否则后续 Servlet 拿到的还是未解码的参数。4. Tomcat 部署与 MySQL 联调跑通一卡通系统的标准步骤4.1 MySQL 数据库导入与账号配置拿到项目压缩包后数据库目录下通常有一个.sql文件包含了建表语句和初始数据。命令行导入是最直接的方式mysql -uroot -p school_card.sql如果你的 MySQL 是免安装版记得先确保bin目录加入了系统 PATH否则命令找不到。导入后用mysql -uroot -p登录执行SHOW DATABASES;确认数据库存在再执行USE 数据库名; SHOW TABLES;看看表是否齐全。生产环境一般不建议直接用 root 账号连应用。单独建一个账号只授予这个库的权限更安全CREATE USER card_applocalhost IDENTIFIED BY Card2024; GRANT SELECT, INSERT, UPDATE, DELETE ON school_card.* TO card_applocalhost; FLUSH PRIVILEGES;GRANT后面精确到库名school_card.*比给全局权限更克制。如果想用 Navicat for MySQL 连接新建连接时填主机localhost、端口3306、用户名card_app即可。MySQL 8.0 默认的caching_sha2_password加密方式在旧版本 JDBC 驱动下连不上如果报Public Key Retrieval is not allowed可以在连接串加allowPublicKeyRetrievaltrue或者把用户改成mysql_native_password。4.2 JDBC 连接参数修改源码里 jdbc 配置一般在src下面的db.properties或.java常量里。改成你自己的环境jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/school_card?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernamecard_app jdbc.passwordCard2024characterEncodingutf8解决中文写入乱码serverTimezoneAsia/Shanghai是 MySQL 8.0 之后的必填项否则驱动拿不到服务器时区直接报错useSSLfalse是本地开发禁用 SSL 加密减少握手开销。如果你的mysql connector是 5.x 版本驱动类名要写成com.mysql.jdbc.Driver6.0 之后才改成com.mysql.cj.jdbc.Driver。把这个写错Tomcat 一启动就会抛ClassNotFoundException。4.3 把项目部署进 Tomcat项目是普通动态 Web 工程一般直接导出成 WAR 包或者直接把整个项目文件夹拷贝到$TOMCAT_HOME/webapps/下。然后启动# Linux / macOS $TOMCAT_HOME/bin/startup.sh # Windows $TOMCAT_HOME\bin\startup.bat启动后访问http://localhost:8080/项目名/。注意 IDEIDEA/Eclipse里如果是 Artifact 部署配置里Application context的值就是访问路径中的项目名。如果 404先看 Tomcat 日志logs/catalina.out有没有报错而不是急着改前端代码。部署时 jsp 编译 class 文件保存在哪里的问题还会再碰到一次Tomcat 启动后会在work/Catalina下生成编译产物如果你更新了 JSP 文件但浏览器始终显示旧页面清空work目录后重启是最快的解决办法。4.4 常见报错与排查对照表下面是部署过程中出现频率最高的六个报错及对应处理方式现象原因处理ClassNotFoundException: com.mysql.jdbc.Driver驱动包缺失或驱动类名写错把 mysql-connector jar 放进WEB-INF/libMySQL 8 用com.mysql.cj.jdbc.DriverAccess denied for user card_applocalhost账号密码错误或授权未生效重跑 GRANT 语句并执行FLUSH PRIVILEGESUnknown database school_card数据库没导入成功执行SHOW DATABASES核对库名重新导入 sql 文件Cannot create PoolableConnectionFactory连接 URL 缺少 timezone 或密码错误核对serverTimezoneAsia/Shanghai和 db.properties 密码java.sql.SQLException: Lock wait timeout exceeded事务没提交导致行锁未释放检查 Service 层是否漏了conn.commit()或连接池是否耗尽页面中文全是?JSP 编码与数据库编码不一致JSP 加pageEncodingUTF-8数据库建表指定DEFAULT CHARSETutf8mb4关于 MySQL 8.0 与 5.7 的差异多说一句如果你按mysql 安装配置教程装的是最新版 MySQL密码默认用caching_sha2_password而项目老驱动是老版本mysql-connector-java:5.1.x那会有认证方式不兼容的问题。要么换 8.x 驱动要么把用户改成mysql_native_password否则 Tomcat 启动时数据源初始化会卡在握手鉴权。5. 进阶给一卡通系统补上安全验证与分页查询一卡通系统涉及现金充值、账户余额简单跑通只是第一步。如果想让这个项目从课程设计上升到一个能演示给面试官看的程度下面三个优化可以优先考虑全部基于现有代码小改。先看 SQL 注入问题。项目里如果用的是字符串拼接 SQL 而不是PreparedStatement比如String sql SELECT * FROM users WHERE user_name name 攻击者传 OR 11就能直接绕过密码查询。最直接的修复是把所有 DAO 层拼串改成?占位符 ps.setString。替换完成后还可以做一个快速验证在登录页面输入admin--如果返回登录失败而不是异常抛错说明拼接问题已处理干净。再看查询分页。以账单查询为例SQL 拼接分页的通用写法是SELECT * FROM bills WHERE user_id ? ORDER BY bill_time DESC LIMIT ?, ?; SELECT COUNT(*) FROM bills WHERE user_id ?;第一条的LIMIT ?, ?两参数分别为偏移量和每页条数比如每页 10 条查第 3 页就是LIMIT 20, 10。第二条COUNT(*)用于计算总页数然后封装成分页对象传给 JSP。注意OFFSET越大的时候查询会变慢数据量过万可以改用上一页最后一条记录的 bill_time 作为游标的方式但这对于毕设项目是过度设计能讲清楚 LIMIT 边界即可。另外可以在用户表加一个固定的status字段来支持账号锁定每个尝试登录的 IP 记录失败次数连续 5 次失败就暂时锁定 15 分钟。核心代码是用UPDATE users SET status 0 WHERE card_no ?判断 15 分钟时间窗用created_at BETWEEN NOW() - INTERVAL 15 MINUTE AND NOW()。最后是日志审计。一卡通扣费流水改动敏感用 Filter 记录每个请求的处理耗时和访问路径WebFilter(/adminBill) public class AccessLogFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; long start System.currentTimeMillis(); chain.doFilter(req, resp); long cost System.currentTimeMillis() - start; System.out.println(request.getRequestURI() 耗时 cost ms); } }System.currentTimeMillis()在耗时统计上精度足够如果以后要统计更多指标可以换System.nanoTime()但一卡通管理后台的访问频率没有必要用高精度计时。这个AccessLogFilter只拦/adminBill避免给静态资源也打印日志。真要发布上线把 System.out 替换成 Log4j2 或 SLF4J 就行。本文还有配套的精品资源点击获取
返回列表