news 2026/9/16 14:22:47

Java Web投票系统实战:JSP+Servlet+Access手写MVC教学基线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Web投票系统实战:JSP+Servlet+Access手写MVC教学基线

简介:这是一份面向高校Web开发课程设计实践的Java Web投票系统完整实现,适用于Java Web初学者巩固JSP、Servlet基础与数据库交互能力。资源包含前后端分离的双入口设计:普通用户通过index.jsp免登录参与投票,管理员凭admin/admin凭证登录login.jsp进行后台管理,涵盖添加、删除、统计等核心功能。压缩包共14个文件,以9个JSP页面(如vote.jsp、manage.jsp、process.jsp)构成交互逻辑,1个Java类(DBBean.java)封装数据库操作,1个XML配置文件(web.xml)定义应用结构,1个Access数据库(vote.mdb)存储投票数据,另含GIF图标与编译类文件,整体仅16KB,轻量易部署。已有866人学习下载,提供开箱即用的可运行源码、清晰的目录分层(vote/WEB-INF/classes/dbBean)及典型MVC雏形结构,便于理解Web应用请求流程、会话管理与简单权限控制机制。

1. 这不是又一个“Hello World”投票页面:它是一套可运行、可调试、可扩展的 Java Web 投票系统教学基线

你手头这份web课程设计基于Java的投票系统(源码+数据库).zip,表面看是学生期末作业压缩包,实则是一条被反复验证过的 Java Web 实战路径——从 JSP + Servlet 基础架构出发,绕过 Spring Boot 的封装黑箱,直击 HTTP 请求生命周期、会话管理、SQL 注入防护边界、JDBC 连接池雏形等核心机制。它不依赖 Maven 一键拉取,所有类(如DBBean.java)和配置(web.xml)都摊开在WEB-INF/classes下,你能清晰看到vote.mdb(Access 数据库)如何通过 JDBC-ODBC 桥接被dbBean封装,也能在process.jsp里逐行 debug 投票逻辑的事务控制粒度。适合两类人:一是刚学完 Servlet 生命周期但写不出完整流程的新手,能照着login.jsp → checkadmin.jsp → manage.jsp链路理解角色权限分层;二是准备 Java 面试题中「手写简易 MVC」或「JDBC 手动事务管理」的求职者,这里add.jsp提交后process.jsp中的conn.setAutoCommit(false)就是标准答案原型。它不追求高并发,但每一步跳转、每一次 SQL 拼接、每一个 request.setAttribute() 都暴露在眼皮底下。

2. 从 Access 数据库到 JSP 页面:解析vote.mdb结构与DBBean.java的 JDBC 封装逻辑

2.1vote.mdb表结构与字段语义映射

该系统使用 Microsoft Access 作为后端存储,文件位于根目录下。通过 Access 客户端或支持.mdb的工具(如 MDB Viewer Plus)打开vote.mdb,可确认其包含以下关键表:

表名字段(含类型)用途说明
usersid(AutoNumber),username(Text),password(Text),role(Text)存储管理员账号(username='admin',password='admin')及潜在用户,role区分 'admin' / 'user'
topicsid(AutoNumber),title(Text),description(Memo),status(Yes/No)投票主题主表,status=Yes表示启用中
optionsid(AutoNumber),topic_id(Number),content(Text),votes(Number)每个主题下的选项,votes记录得票数,注意:此处无乐观锁或版本号字段,高并发场景需自行加锁
votes_logid(AutoNumber),user_ip(Text),topic_id(Number),option_id(Number),create_time(Date/Time)投票日志表,用于防刷(但当前未在代码中强制校验 IP 重复)

提示:Access 的.mdb文件在 Java 中需通过 JDBC-ODBC 桥访问,这意味着部署时需在目标机器上配置 ODBC 数据源(DSN),且 JDK 8 及以上版本已移除sun.jdbc.odbc.JdbcOdbcDriver必须降级至 JDK 7 或改用 UCanAccess 驱动。这是该系统最易卡住的第一道门槛。

2.2DBBean.java的连接管理与 SQL 执行封装

DBBean.java位于WEB-INF/classes/dbBean/目录,是整个数据访问层的核心。其关键方法如下:

package dbBean; import java.sql.*; public class DBBean { private Connection conn = null; private Statement stmt = null; private ResultSet rs = null; // 构造函数中硬编码 DSN 名为 "vote" public DBBean() { try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // JDK 7 专用 conn = DriverManager.getConnection("jdbc:odbc:vote", "", ""); } catch (Exception e) { e.printStackTrace(); } } // 执行查询,返回 ResultSet(注意:未关闭资源,需调用方手动 close) public ResultSet executeQuery(String sql) { try { stmt = conn.createStatement(); rs = stmt.executeQuery(sql); } catch (SQLException e) { e.printStackTrace(); } return rs; } // 执行更新(INSERT/UPDATE/DELETE),返回影响行数 public int executeUpdate(String sql) { int result = 0; try { stmt = conn.createStatement(); result = stmt.executeUpdate(sql); } catch (SQLException e) { e.printStackTrace(); } return result; } // 关闭连接(但未处理 stmt/rs,存在资源泄漏风险) public void close() { try { if (conn != null && !conn.isClosed()) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
参数与逻辑说明:
  • "jdbc:odbc:vote":DSN 名称必须与操作系统 ODBC 数据源管理器中创建的名称完全一致,大小写敏感;
  • 空用户名密码("", ""):Access 数据库默认无认证,若设了密码需在此处传入;
  • executeQuery()返回ResultSet:调用方(如index.jsp)必须显式调用rs.next()并在 finally 块中关闭rsstmt,否则连接池耗尽;
  • close()方法缺陷:仅关闭Connection,未关闭StatementResultSet实际部署时需重写为try-with-resources或在每个业务方法末尾手动关闭

2.3web.xml中的 Servlet 映射与初始化参数

WEB-INF/web.xml定义了该 Web 应用的基础配置。关键片段如下:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 全局初始化参数:指定 Access 数据库路径(实际未被 DBBean 使用,属冗余配置) --> <context-param> <param-name>dbPath</param-name> <param-value>D:/vote.mdb</param-value> </context-param> <!-- 欢迎文件列表 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

注意<context-param>中的dbPathDBBean.java中并未读取,属于课程设计常见冗余项。真实项目中应通过ServletContext.getInitParameter("dbPath")获取并动态构造 URL,而非硬编码 DSN。

3. 用户与管理员双入口:index.jsplogin.jsp的请求流转与会话控制

3.1 普通用户流程:index.jsp如何加载主题与选项

index.jsp是系统首页,无需登录即可访问。其核心逻辑在于从topics表加载启用状态的主题,并关联options表展示选项:

<%@ page import="java.sql.*" %> <%@ page import="dbBean.DBBean" %> <% DBBean db = new DBBean(); ResultSet rsTopics = db.executeQuery("SELECT * FROM topics WHERE status = True"); %> <!-- HTML 表格渲染 --> <table> <tr><th>投票主题</th><th>操作</th></tr> <% while(rsTopics.next()) { %> <tr> <td><%= rsTopics.getString("title") %></td> <td> <a href="vote.jsp?topicId=<%= rsTopics.getInt("id") %>">参与投票</a> </td> </tr> <% } %> </table> <% // 必须手动关闭 ResultSet 和 Statement! if (rsTopics != null) rsTopics.close(); if (db != null) db.close(); // 但此 close 不关 rs/stmt,隐患在此 %>
关键点解析:
  • URL 参数传递vote.jsp?topicId=1将主题 ID 以 GET 方式传入,vote.jsp再根据该 ID 查询对应选项;
  • 资源未释放rsTopics.close()被显式调用,但DBBeanstmt未关闭,多次刷新将导致SQLException: Too many connections
  • 无 CSRF 防护:投票提交为简单表单 POST,无 token 校验,若开放公网需增加 hidden input token 并服务端比对

3.2 管理员流程:login.jspcheckadmin.jspmanage.jsp的身份校验链

管理员入口分为三步,构成典型的 Servlet/JSP 身份验证链:

  1. login.jsp:纯 HTML 表单,POST 到checkadmin.jsp

    <form action="checkadmin.jsp" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form>
  2. checkadmin.jsp:执行校验并设置会话属性

    <% String username = request.getParameter("username"); String password = request.getParameter("password"); DBBean db = new DBBean(); ResultSet rs = db.executeQuery( "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'" ); if (rs.next() && "admin".equals(rs.getString("role"))) { session.setAttribute("admin_logged_in", "true"); // 设置会话标识 response.sendRedirect("manage.jsp"); } else { out.println("登录失败,请检查用户名和密码"); } // rs.close(); db.close(); —— 此处同样缺失! %>
  3. manage.jsp:会话保护页,顶部强制校验

    <% if (!"true".equals(session.getAttribute("admin_logged_in"))) { response.sendRedirect("login.jsp"); return; // 终止后续执行 } %> <!-- 管理员操作界面 -->
安全缺陷与修复建议:
  • SQL 注入漏洞checkadmin.jsp中直接拼接usernamepassword,输入' OR '1'='1即可绕过认证。修复方式:改用 PreparedStatement
    String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery();
  • 会话固定风险:登录成功后未调用session.invalidate()request.getSession(true)生成新会话 ID;
  • 明文密码存储users表中password字段存的是明文'admin',生产环境必须使用 BCrypt 加密。

3.3process.jsp:投票动作的事务控制与防重提交

process.jsp是投票逻辑的执行中心,接收vote.jsp提交的optionIdtopicId,完成计票与日志记录:

<% String topicId = request.getParameter("topicId"); String optionId = request.getParameter("optionId"); DBBean db = new DBBean(); // 开启事务 db.conn.setAutoCommit(false); try { // 1. 更新选项票数 String updateSql = "UPDATE options SET votes = votes + 1 WHERE id = " + optionId; db.executeUpdate(updateSql); // 2. 记录投票日志(当前未校验 IP 是否已投) String logSql = "INSERT INTO votes_log (user_ip, topic_id, option_id, create_time) VALUES ('" + request.getRemoteAddr() + "', " + topicId + ", " + optionId + ", Now())"; db.executeUpdate(logSql); db.conn.commit(); // 提交事务 response.sendRedirect("index.jsp?msg=success"); } catch (Exception e) { db.conn.rollback(); // 回滚 response.sendRedirect("index.jsp?msg=error"); } finally { db.conn.setAutoCommit(true); // 恢复自动提交 db.close(); } %>
事务边界与并发问题:
  • setAutoCommit(false)作用域:仅对当前Connection生效,DBBean实例复用时需确保每次请求独占连接(当前无连接池,故可行);
  • IP 防重逻辑缺失votes_log表虽存在,但process.jsp未执行SELECT COUNT(*) FROM votes_log WHERE user_ip = ? AND topic_id = ?校验,需在try块开头添加此查询并判断rs.next()
  • Now()函数兼容性:Access 使用Now(),MySQL 用NOW(),Oracle 用SYSDATE,跨数据库迁移时需抽象为 DAO 方法。

4. 本地部署实战:JDK 7 + Tomcat 8 + UCanAccess 驱动的零配置启动方案

4.1 环境准备:规避 JDK 8+ 的 JDBC-ODBC 桥移除问题

由于DBBean.java强依赖sun.jdbc.odbc.JdbcOdbcDriver,而该驱动在 JDK 8u251 后被彻底移除,强行使用 JDK 7 是最简方案。但若必须用 JDK 11+,则需替换为 UCanAccess:

  1. 下载 UCanAccess 5.0.1(支持 JDK 11+):
    官网地址:https://ucanaccess.sourceforge.net/site.html
    解压后获取ucanaccess-5.0.1.jarcommons-lang3-3.8.1.jarcommons-logging-1.2.jarhsqldb-2.5.0.jarjackcess-3.0.1.jar

  2. 修改DBBean.java的驱动加载与 URL

    // 替换原 Class.forName(...) 和 getConnection(...) Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); conn = DriverManager.getConnection("jdbc:ucanaccess://D:/path/to/vote.mdb");
  3. 将上述 5 个 JAR 包复制到WEB-INF/lib/目录,覆盖原有依赖。

提示:UCanAccess 是纯 Java 实现,无需配置 ODBC DSN,getConnection()中的路径必须为绝对路径(Windows 下如D:/vote.mdb),相对路径会导致UCanAccessException: file not found

4.2 Tomcat 8.5 部署步骤与web.xml兼容性适配

该系统基于 Servlet 2.4 规范(web.xmlversion="2.4"),而 Tomcat 8.5 默认支持 Servlet 3.1。需做两处适配:

  1. web.xml头部声明升级(推荐):

    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
  2. 或保持原声明,但确保 Tomcat 8.5 的conf/web.xmlservlet-class未被覆盖(通常无需改动)。

部署命令流(Windows CMD):

# 1. 解压 vote.zip 到 Tomcat webapps 目录 unzip web课程设计基于Java的投票系统.zip -d C:\apache-tomcat-8.5.94\webapps\vote # 2. 复制 UCanAccess JAR(若使用 JDK 11+) copy ucanaccess-5.0.1.jar C:\apache-tomcat-8.5.94\webapps\vote\WEB-INF\lib\ copy commons-lang3-3.8.1.jar C:\apache-tomcat-8.5.94\webapps\vote\WEB-INF\lib\ # ... 复制其余 3 个 JAR # 3. 启动 Tomcat C:\apache-tomcat-8.5.94\bin\startup.bat # 4. 访问验证 # 普通用户:http://localhost:8080/vote/ # 管理员:http://localhost:8080/vote/login.jsp (账号 admin/admin)

4.3 常见启动报错与精准定位方法

报错信息根本原因定位命令/步骤
java.lang.ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriverJDK 版本过高java -version查看 JDK 版本;若 ≥ 8,改用 UCanAccess
java.sql.SQLException: [Microsoft][ODBC Driver Manager] Data source name not foundODBC DSN 未配置或名称错误控制面板 → 管理工具 → ODBC 数据源(64 位)→ 系统 DSN → 确认存在名为vote的 Access Database
HTTP Status 404 – Not Found应用未部署成功或上下文路径错误查看C:\apache-tomcat-8.5.94\logs\catalina.out,搜索deploy关键字;确认解压后目录为webapps/vote/(非webapps/vote/xxx/
HTTP Status 500 – Internal Server Error且日志含NullPointerExceptionDBBean初始化失败(如conn为 null)DBBean构造函数末尾添加System.out.println("DBBean init: " + (conn!=null));,重启后查catalina.out

5. 从课程设计到工程实践:三个可立即落地的增强技巧

5.1 为vote.jsp添加防刷校验:基于 Session 的单主题单次投票

当前系统允许同一 IP 对同一主题重复投票。利用HttpSession可实现轻量级防刷,无需修改数据库:

<!-- vote.jsp 开头添加 --> <% String topicId = request.getParameter("topicId"); String sessionKey = "voted_topic_" + topicId; if (session.getAttribute(sessionKey) != null) { out.println("<h3>您已对本主题投过票,不可重复提交</h3>"); out.println("<a href='index.jsp'>返回首页</a>"); return; // 终止页面渲染 } %> <!-- 表单提交后,在 process.jsp 成功分支中添加 --> <% if ("success".equals(request.getParameter("msg"))) { session.setAttribute("voted_topic_" + topicId, "true"); // 并设置 24 小时过期(可选) session.setMaxInactiveInterval(24 * 60 * 60); } %>
优势与局限:
  • 零数据库压力:校验在内存中完成,避免SELECT COUNT查询;
  • 会话级隔离:不同浏览器(不同 session)可分别投票,符合普通用户预期;
  • 不防代理/IP 池:仅对同一浏览器有效,高级防刷需结合 Redis + IP + User-Agent 多维指纹。

5.2manage.jsp中嵌入实时票数刷新:用 AJAX 替代整页 reload

管理员在manage.jsp查看票数时,当前需手动刷新页面。引入原生 JavaScript AJAX 可实现局部更新:

<!-- 在 manage.jsp 的票数表格中,为每个选项的 votes 单元格添加 id --> <td id="votes_<%= optionId %>"><%= optionVotes %></td> <!-- 页面底部添加 AJAX 刷新脚本 --> <script> function refreshVotes() { fetch('ajax_get_votes.jsp') .then(response => response.json()) .then(data => { Object.keys(data).forEach(topicId => { Object.keys(data[topicId]).forEach(optionId => { const el = document.getElementById('votes_' + optionId); if (el) el.textContent = data[topicId][optionId]; }); }); }); } // 每 5 秒刷新一次 setInterval(refreshVotes, 5000); </script>

配套ajax_get_votes.jsp(返回 JSON):

<%@ page contentType="application/json;charset=UTF-8" %> <%@ page import="java.sql.*" %> <%@ page import="dbBean.DBBean" %> <% DBBean db = new DBBean(); ResultSet rs = db.executeQuery("SELECT t.id as topic_id, o.id as option_id, o.votes FROM topics t, options o WHERE t.id = o.topic_id"); StringBuilder json = new StringBuilder("{"); while (rs.next()) { String topicId = rs.getString("topic_id"); String optionId = rs.getString("option_id"); String votes = rs.getString("votes"); if (json.length() > 1) json.append(","); json.append("\"").append(topicId).append("\": {\"").append(optionId).append("\": \"").append(votes).append("\"}"); } json.append("}"); out.print(json.toString()); rs.close(); db.close(); %>

5.3delete.jsp的安全删除确认:防止误操作的二次弹窗与 SQL 参数化

delete.jsp当前直接执行删除,无任何确认。增强方案如下:

<!-- delete.jsp 中,将删除链接改为按钮触发 JS --> <a href="#" onclick="confirmDelete(<%= topicId %>)">删除主题</a> <script> function confirmDelete(topicId) { if (confirm("确定要删除该投票主题及所有选项?此操作不可撤销!")) { window.location.href = "process_delete.jsp?topicId=" + topicId; } } </script>

配套process_delete.jsp(必须使用 PreparedStatement):

<% String topicId = request.getParameter("topicId"); DBBean db = new DBBean(); // 使用 PreparedStatement 防注入 String sql = "DELETE FROM options WHERE topic_id = ?"; PreparedStatement ps = db.conn.prepareStatement(sql); ps.setString(1, topicId); ps.executeUpdate(); sql = "DELETE FROM topics WHERE id = ?"; ps = db.conn.prepareStatement(sql); ps.setString(1, topicId); ps.executeUpdate(); response.sendRedirect("manage.jsp?msg=deleted"); %>

关键加固点process_delete.jsp中的PreparedStatement彻底杜绝了通过topicId=1 OR 1=1删除全部数据的风险,这是 Java 面试中高频考察的防御式编程范例。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 14:21:26

正弦条纹与格雷码协同相位解包裹原理与MATLAB实现

简介&#xff1a;本资源是一套面向计算机视觉与三维测量初学者及科研人员的MATLAB结构光编码图案生成工具包&#xff0c;聚焦于正弦条纹相位调制与格雷码空间编码两大核心技术&#xff0c;解决三维形貌重建中高精度条纹投影图案设计与批量生成的实际需求。压缩包共28个文件&…

作者头像 李华
网站建设 2026/9/16 14:19:05

Postman调试API签名与时间戳:从报错到自动生成签名脚本

最近在调一个开放平台的接口&#xff0c;我打开Postman&#xff0c;填好URL和Body&#xff0c;点Send&#xff0c;几秒钟后响应体里孤零零挂着一行字&#xff1a;{"msg": "请求缺失签名Sign"}我以为是漏了哪个参数&#xff0c;补上签名后又来一个&#xff…

作者头像 李华
网站建设 2026/9/16 14:18:53

SpringBoot旅游管理系统毕业设计:从源码到答辩实战拆解

简介&#xff1a;这套SpringBoot旅游管理系统毕业设计资源包&#xff0c;专为计算机专业学生打造&#xff0c;可直接用于毕业设计、课程设计或期末大作业&#xff0c;也可作为教学案例。系统覆盖旅游线路查询、酒店预订、景点信息展示、旅游攻略分享等典型业务&#xff0c;后端…

作者头像 李华
网站建设 2026/9/16 14:17:20

数据中心微网两阶段鲁棒规划方法解析

1. 项目背景与核心价值数据中心作为数字经济的核心基础设施&#xff0c;其能耗问题日益突出。据统计&#xff0c;全球数据中心年耗电量已超过2000亿千瓦时&#xff0c;相当于整个意大利的年用电量。在这种背景下&#xff0c;如何实现数据中心能源系统的经济高效运行成为行业痛点…

作者头像 李华
网站建设 2026/9/16 14:16:26

JSP音乐网站项目拆解:从环境搭建到部署排错全流程

简介&#xff1a;基于JSP的音乐网站完整毕业设计项目&#xff0c;面向计算机相关专业本科生&#xff0c;适用于课程设计或毕业论文答辩场景。项目采用SSH框架整合MVC分层思想&#xff0c;搭配MySQL数据库与Tomcat运行环境&#xff0c;覆盖从需求分析到界面实现的完整链路。压缩…

作者头像 李华