简介:这是一份面向高校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,可确认其包含以下关键表:
| 表名 | 字段(含类型) | 用途说明 |
|---|---|---|
users | id(AutoNumber),username(Text),password(Text),role(Text) | 存储管理员账号(username='admin',password='admin')及潜在用户,role区分 'admin' / 'user' |
topics | id(AutoNumber),title(Text),description(Memo),status(Yes/No) | 投票主题主表,status=Yes表示启用中 |
options | id(AutoNumber),topic_id(Number),content(Text),votes(Number) | 每个主题下的选项,votes记录得票数,注意:此处无乐观锁或版本号字段,高并发场景需自行加锁 |
votes_log | id(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 块中关闭rs和stmt,否则连接池耗尽;close()方法缺陷:仅关闭Connection,未关闭Statement和ResultSet,实际部署时需重写为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>中的dbPath在DBBean.java中并未读取,属于课程设计常见冗余项。真实项目中应通过ServletContext.getInitParameter("dbPath")获取并动态构造 URL,而非硬编码 DSN。
3. 用户与管理员双入口:index.jsp与login.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()被显式调用,但DBBean的stmt未关闭,多次刷新将导致SQLException: Too many connections; - 无 CSRF 防护:投票提交为简单表单 POST,无 token 校验,若开放公网需增加 hidden input token 并服务端比对。
3.2 管理员流程:login.jsp→checkadmin.jsp→manage.jsp的身份校验链
管理员入口分为三步,构成典型的 Servlet/JSP 身份验证链:
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>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(); —— 此处同样缺失! %>manage.jsp:会话保护页,顶部强制校验<% if (!"true".equals(session.getAttribute("admin_logged_in"))) { response.sendRedirect("login.jsp"); return; // 终止后续执行 } %> <!-- 管理员操作界面 -->
安全缺陷与修复建议:
- SQL 注入漏洞:
checkadmin.jsp中直接拼接username和password,输入' OR '1'='1即可绕过认证。修复方式:改用 PreparedStatementString 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提交的optionId和topicId,完成计票与日志记录:
<% 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:
下载 UCanAccess 5.0.1(支持 JDK 11+):
官网地址:https://ucanaccess.sourceforge.net/site.html
解压后获取ucanaccess-5.0.1.jar、commons-lang3-3.8.1.jar、commons-logging-1.2.jar、hsqldb-2.5.0.jar、jackcess-3.0.1.jar修改
DBBean.java的驱动加载与 URL:// 替换原 Class.forName(...) 和 getConnection(...) Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); conn = DriverManager.getConnection("jdbc:ucanaccess://D:/path/to/vote.mdb");将上述 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.xml中version="2.4"),而 Tomcat 8.5 默认支持 Servlet 3.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">或保持原声明,但确保 Tomcat 8.5 的
conf/web.xml中servlet-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.JdbcOdbcDriver | JDK 版本过高 | java -version查看 JDK 版本;若 ≥ 8,改用 UCanAccess |
java.sql.SQLException: [Microsoft][ODBC Driver Manager] Data source name not found | ODBC 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且日志含NullPointerException | DBBean初始化失败(如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 面试中高频考察的防御式编程范例。
本文还有配套的精品资源,点击获取