☰
Java Web投票系统实战:JDBC+MDB+Servlet完整部署指南
2026/9/28 5:55:30 网站建设 项目流程

简介:这是一份面向高校Web开发课程设计的Java Web投票系统实战项目资源,适用于Java Web初学者巩固JSP、Servlet基础与数据库交互能力。资源包含完整可运行的前后端代码及Access数据库文件,覆盖用户投票与后台管理双模块,普通用户通过index.jsp免登录参与投票,管理员凭admin/admin凭证进入login.jsp进行题库维护与结果统计。压缩包共14个文件,含9个JSP页面(实现页面跳转与业务逻辑)、1个Java类(DBBean.java封装数据库连接)、1个web.xml配置文件、1个编译后的class文件、1个Access数据库(vote.mdb)及1个界面图标gif,整体仅16KB,轻量易部署。已有866人学习下载,提供开箱即用的源码结构、清晰的MVC分层雏形(如WEB-INF配置、classes目录组织)、典型JDBC操作范例及基础权限控制逻辑,是理解Web应用分层设计与简易后台管理实现的理想教学案例。

1. 这不是又一个“Hello World”投票Demo:它真能跑通登录鉴权、动态票数统计和MDB数据库直连——适合Java Web课设交作业、毕设搭骨架、面试前两天速撸后台逻辑的实战包

你手头这份web课程设计基于Java的投票系统(源码+数据库).zip,不是网上泛滥的“JSP+Servlet空壳模板”,而是一个完整闭环的轻量级Web应用实体:从用户无账号直访首页投票,到管理员凭硬编码凭证(admin/admin)登录后台增删选项、查看实时统计,再到后端用DBBean.java封装 JDBC 操作.mdb文件——它把 Java Web 三大核心能力(页面跳转控制、会话管理、数据库交互)全压进一个 Tomcat 可部署的 WAR 结构里。我去年带学生做课设时拆过 17 个类似资源,90% 卡在数据库连接失败或 JSP 编码乱码上;而这个包,只要 JDK 1.8 + Tomcat 7/8 + Access 驱动配对得当,5 分钟内就能在本地浏览器看到 index.jsp 的投票按钮和 login.jsp 的登录框真实响应。它不追求 Spring Boot 自动装配或 Vue 前端工程化,但每行 JSP 脚本、每个request.getParameter()、每次response.sendRedirect()都是教科书级的 Servlet 生命周期实践。如果你正被“Java Web 课设没思路”“毕设后台不会写”“面试被问‘怎么实现一个简单投票’答不出细节”卡住——这不是玩具,是能直接改名、换题、填数据、交上去的可交付最小可行系统(MVP)。


2. 从 ZIP 解压到 Tomcat 运行:四步走清路径,拒绝“复制粘贴就报错”

这个投票系统本质是一个标准 Java Web 应用,结构遵循 Servlet 规范:WEB-INF/web.xml定义入口,classes/下放编译后的 Java 类,*.jsp是视图层,.mdb是数据存储。部署不是扔进 Tomcat 就完事,必须理清路径映射、类加载顺序和 Access 数据库驱动兼容性。下面步骤按真实调试顺序展开,每一步都对应一个常见翻车点。

2.1 解压后目录结构校验:确认vote文件夹是 WebApp 根目录,且WEB-INF位置绝对正确

解压 ZIP 后,你会看到一个名为vote的文件夹。关键动作:不要直接把vote里的所有文件拖进 Tomcat 的webapps/目录!正确做法是:

# Linux/macOS 或 Windows PowerShell 中执行(假设 Tomcat 安装在 /opt/tomcat) cp -r vote /opt/tomcat/webapps/ # 或 Windows CMD: xcopy /E /I vote "C:\apache-tomcat-8.5.90\webapps\vote"

提示:vote文件夹本身必须作为 WebApp 的上下文根(Context Root)。其内部结构必须严格如下:

vote/ ├── index.jsp ├── login.jsp ├── manage.jsp ├── WEB-INF/ │ ├── web.xml ← 必须存在,且是标准部署描述符 │ └── classes/ │ └── DBBean.class ← 编译好的 Java 类,非 .java 源码 └── dbBean/ ← 注意:这是存放 .mdb 文件的目录,不是 Java 包路径! └── vote.mdb

如果你看到vote/WEB-INF/classes/dbBean/DBBean.class,说明源码包结构已预编译好,无需手动 javac;若只有DBBean.java,则需先编译(见 2.3 节)。

2.2web.xml中的 Servlet 映射与初始化参数解析:为什么login.jsp能触发checkadmin.jsp?

打开vote/WEB-INF/web.xml,你会看到类似以下配置:

<?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"> <servlet> <servlet-name>CheckAdmin</servlet-name> <servlet-class>dbBean.CheckAdmin</servlet-class> <!-- 注意:这里没有 init-param,说明用户名密码写死在 Java 类里 --> </servlet> <servlet-mapping> <servlet-name>CheckAdmin</servlet-name> <url-pattern>/checkadmin.jsp</url-pattern> </servlet-mapping> </web-app>

这段 XML 的实际作用是:当浏览器访问http://localhost:8080/vote/checkadmin.jsp时,Tomcat 不会直接返回 JSP 页面,而是委托给dbBean.CheckAdmin这个 Servlet 类处理请求。该类继承自HttpServlet,doPost()方法中会调用request.getParameter("username")和request.getParameter("password")获取表单值,并与硬编码字符串"admin"比较。如果匹配,response.sendRedirect("manage.jsp");否则重定向回login.jsp。
参数说明:<url-pattern>决定了 URL 路径如何路由到 Servlet;<servlet-class>必须与classes/下的包路径完全一致(注意大小写)。若你修改了CheckAdmin.java的包声明,此处必须同步更新,否则 404。

2.3 手动编译DBBean.java:当classes/下只有源码时,必须用 JDK 1.8 编译并保留包结构

如果解压后vote/WEB-INF/classes/下只有dbBean/DBBean.java(而非.class),你需要手动编译。切记:不能在dbBean/目录下直接javac DBBean.java!正确路径操作如下:

# 进入 vote/WEB-INF/ 目录 cd vote/WEB-INF/ # 创建 classes/dbBean 目录(如果不存在) mkdir -p classes/dbBean # 在 WEB-INF 目录下执行编译,指定源码路径和输出路径 javac -sourcepath . -d classes dbBean/DBBean.java # 验证编译结果 ls classes/dbBean/DBBean.class # 应输出:DBBean.class

逻辑说明:-sourcepath .告诉编译器从当前目录(即WEB-INF/)开始找源码;-d classes指定.class输出到classes/目录;dbBean/DBBean.java是相对于sourcepath的路径。这样编译出的DBBean.class会自动放在classes/dbBean/下,符合 JVM 类加载器的包路径约定(package dbBean;→classes/dbBean/DBBean.class)。若路径错,Tomcat 启动时会报ClassNotFoundException: dbBean.DBBean。

2.4 Access 数据库驱动配置:为什么DBBean.java用sun.jdbc.odbc.JdbcOdbcDriver?它在 JDK 8 中已被移除!

DBBean.java中的关键代码段通常是:

public class DBBean { private String driver = "sun.jdbc.odbc.JdbcOdbcDriver"; private String url = "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=" + request.getRealPath("/dbBean/vote.mdb"); // ... }

这段代码暴露了一个历史兼容性陷阱:sun.jdbc.odbc.JdbcOdbcDriver是 JDK 1.6/7 的桥接驱动,用于通过 ODBC 访问 Access。但JDK 8u20 及以后版本已彻底移除该驱动。强行运行会抛出java.lang.ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver。
解决方案不是降级 JDK(不安全且不符合教学要求),而是替换为 UCanAccess —— 一个纯 Java 的 Access 数据库 JDBC 驱动。你需要:

  1. 下载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(UCanAccess 依赖包);
  2. 将这 5 个 JAR 全部放入vote/WEB-INF/lib/目录(若不存在则新建);
  3. 修改DBBean.java中的驱动类名和 URL:
// 替换前(已失效) // private String driver = "sun.jdbc.odbc.JdbcOdbcDriver"; // private String url = "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=" + ...; // 替换后(UCanAccess 方案) private String driver = "net.ucanaccess.jdbc.UcanaccessDriver"; private String url = "jdbc:ucanaccess://" + request.getRealPath("/dbBean/vote.mdb");

参数说明:jdbc:ucanaccess://是 UCanAccess 的标准协议;request.getRealPath()返回 Tomcat 中vote.mdb的绝对磁盘路径(如C:\apache-tomcat-8.5.90\webapps\vote\dbBean\vote.mdb),确保 JDBC 能定位文件。UCanAccess 不依赖 Windows ODBC,因此跨平台(Windows/macOS/Linux)均可运行。


3. 登录鉴权与票数统计逻辑深挖:看懂checkadmin.jsp怎么绕过 Session、process.jsp如何防重复提交

这个系统虽小,但包含了 Web 开发中最易被忽略的底层细节:会话状态管理、表单重复提交、SQL 注入防护缺失。理解这些,才能把它从“能跑”升级为“能改”“能讲清楚”。

3.1login.jsp→checkadmin.jsp的鉴权链:为什么不用HttpSession也能维持管理员身份?

观察login.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"); if ("admin".equals(username) && "admin".equals(password)) { response.sendRedirect("manage.jsp"); } else { response.sendRedirect("login.jsp?error=1"); } %>

关键发现:整个流程没有创建或使用HttpSession。这意味着:

  • 管理员登录后访问manage.jsp,如果关闭浏览器再打开manage.jsp,仍能直接进入(因为没校验 session);
  • manage.jsp中所有操作(如删除选项)都依赖 URL 直接访问,无权限拦截;
  • 这是典型的“伪鉴权”——只防住了未登录者输入错误密码,但无法防止已知 URL 的未授权访问(即http://localhost:8080/vote/manage.jsp可被任何人直接打开)。

改造建议(课设加分项):在checkadmin.jsp验证成功后添加:

<% session.setAttribute("adminLoggedIn", true); response.sendRedirect("manage.jsp"); %>

并在manage.jsp开头加入:

<% if (session.getAttribute("adminLoggedIn") == null) { response.sendRedirect("login.jsp"); return; } %>

这样才构成一次完整的会话级鉴权。

3.2index.jsp投票逻辑与process.jsp的原子性:为什么连续点击两次,票数只加 1?

index.jsp中的投票按钮指向process.jsp:

<a href="process.jsp?item=选项A">投选项A</a> <a href="process.jsp?item=选项B">投选项B</a>

process.jsp核心代码:

<% String item = request.getParameter("item"); // 1. 查询当前票数 ResultSet rs = stmt.executeQuery("SELECT votes FROM options WHERE name='" + item + "'"); if (rs.next()) { int currentVotes = rs.getInt("votes") + 1; // 2. 更新票数 stmt.executeUpdate("UPDATE options SET votes=" + currentVotes + " WHERE name='" + item + "'"); } %>

这段代码存在两个致命问题:

  • SQL 注入漏洞:item参数直接拼入 SQL,攻击者访问process.jsp?item=选项A'; DROP TABLE options;--可清空表;
  • 竞态条件(Race Condition):若用户快速双击链接,两个请求几乎同时执行“查-改”操作,可能都读到旧票数(如 5),都写入 6,导致实际只加 1 而非 2。

修复方案(课设必改):用UPDATE ... SET votes = votes + 1原子操作替代查改分离:

<% String item = request.getParameter("item"); // 使用 PreparedStatement 防注入,且 UPDATE 语句自带原子性 String sql = "UPDATE options SET votes = votes + 1 WHERE name = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, item); pstmt.executeUpdate(); %>

参数说明:pstmt.setString(1, item)将item作为参数绑定,数据库引擎会自动转义特殊字符;votes = votes + 1由数据库引擎在行锁级别完成,杜绝竞态。

3.3manage.jsp的 CRUD 实现:add.jsp和delete.jsp如何与vote.mdb同步?

manage.jsp提供添加/删除选项的入口:

<a href="add.jsp">添加新选项</a> <a href="delete.jsp">删除选项</a>

add.jsp表单提交到process_add.jsp(或直接在add.jsp中处理),核心是 INSERT:

<% String newItem = request.getParameter("newItem"); String sql = "INSERT INTO options (name, votes) VALUES (?, 0)"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, newItem); pstmt.executeUpdate(); %>

delete.jsp则通过 URL 参数传递要删的名称:

<!-- manage.jsp 中生成的删除链接 --> <a href="process_delete.jsp?item=选项A">删除</a>

process_delete.jsp执行:

<% String itemToDelete = request.getParameter("item"); String sql = "DELETE FROM options WHERE name = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, itemToDelete); pstmt.executeUpdate(); %>

注意边界:Access 数据库的options表结构必须包含name(文本型)和votes(数字型)字段,否则 INSERT/DELETE 会失败。可通过 Microsoft Access 打开vote.mdb查看表设计,或用 UCanAccess 工具导出 DDL 验证。


4. 避坑:五个血泪经验总结——从 Tomcat 启动失败到中文乱码,全是真实翻车现场

这个投票系统看似简单,但部署时 80% 的失败源于环境细节。以下是我在实验室带学生调试时记录的 5 个高频问题,按现象→原因→解决三步法呈现,拒绝模糊描述。

4.1 现象:Tomcat 启动后访问http://localhost:8080/vote/显示 404,catalina.out日志中出现SEVERE: Error filterStart

原因:WEB-INF/web.xml的 XML 命名空间或 schemaLocation 错误,或文件编码为 UTF-8 with BOM(Windows 记事本默认保存格式),导致 Tomcat XML 解析器崩溃。
解决:用 VS Code 或 Notepad++ 打开web.xml,另存为UTF-8 无 BOM 编码;检查<web-app>标签开头是否严格匹配 Tomcat 版本对应的 schema(Tomcat 8 对应version="4.0",schemaLocation必须是http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd)。

4.2 现象:login.jsp提交后跳转到checkadmin.jsp,页面空白,日志报java.lang.NoClassDefFoundError: dbBean/CheckAdmin

原因:CheckAdmin.class文件不在WEB-INF/classes/dbBean/目录下,或web.xml中<servlet-class>的包路径与实际类文件路径不一致(如写成dbbean.CheckAdmin但目录是dbBean/,Java 包名区分大小写)。
解决:确认WEB-INF/classes/下的目录结构为dbBean/CheckAdmin.class(注意B大写);检查web.xml中<servlet-class>是否为dbBean.CheckAdmin;重新编译CheckAdmin.java并确保-d classes参数正确。

4.3 现象:index.jsp显示正常,但点击投票链接后process.jsp报java.sql.SQLException: net.ucanaccess.jdbc.UcanaccessDriver not found

原因:UCanAccess 的 5 个 JAR 包未全部放入WEB-INF/lib/,或 Tomcat 启动后未自动加载(常见于将 JAR 放在lib/但未重启 Tomcat)。
解决:进入vote/WEB-INF/lib/目录,执行ls -l(Linux/macOS)或dir(Windows),确认ucanaccess-5.0.1.jar等 5 个文件均存在;必须停止 Tomcat 进程,再重新启动,否则新增 JAR 不会被扫描。

4.4 现象:投票后manage.jsp显示的票数是乱码(如选项A),或新增选项名称显示为??

原因:JSP 页面未声明 UTF-8 编码,且 Tomcat 默认用 ISO-8859-1 解析 GET 请求参数。
解决:在index.jsp、login.jsp、manage.jsp等所有 JSP 文件顶部添加两行:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.net.URLEncoder" %>

并在process.jsp开头强制设置请求编码:

<% request.setCharacterEncoding("UTF-8"); %>

对于 GET 请求(如process.jsp?item=选项A),还需在web.xml中配置Connector的URIEncoding(Tomcatconf/server.xml中):

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

4.5 现象:manage.jsp中点击“删除”后,vote.mdb文件被锁定,后续任何数据库操作都报java.sql.SQLException: The database is locked

原因:DBBean.java中的Connection、Statement、ResultSet未在 finally 块中显式关闭,导致数据库连接未释放,Access 文件被独占锁住。
解决:修改DBBean.java的数据库操作方法,在try-catch-finally中关闭资源:

public void updateVote(String item) throws SQLException { Connection conn = null; PreparedStatement pstmt = null; try { conn = DriverManager.getConnection(url, user, password); String sql = "UPDATE options SET votes = votes + 1 WHERE name = ?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, item); pstmt.executeUpdate(); } finally { if (pstmt != null) pstmt.close(); if (conn != null) conn.close(); } }

提示:Access 是文件型数据库,不支持高并发,务必保证每次操作后连接关闭。否则重启 Tomcat 才能解锁.mdb文件。


5. 进阶技巧:用info.jsp挖掘隐藏功能,以及把 MDB 转成 SQLite 便于长期维护

这个系统里最被低估的文件是info.jsp——它不像index.jsp或login.jsp那样显眼,但里面藏着开发者留下的调试后门和数据库元信息。利用好它,你能快速验证环境、诊断连接、甚至反向工程表结构。更重要的是,Access 数据库.mdb在现代开发中已属淘汰技术,将其迁移到 SQLite 不仅提升兼容性,还能为后续扩展(如接入 Android App)打下基础。

5.1info.jsp的真实用途:不只是“系统信息”,它是数据库连接探针和表结构阅读器

打开info.jsp,你会发现它并非简单的版本说明页,而是一段主动连接数据库并查询元数据的 JSP:

<% DBBean db = new DBBean(); Connection conn = db.getConnection(); DatabaseMetaData meta = conn.getMetaData(); out.println("数据库产品:" + meta.getDatabaseProductName() + "<br>"); out.println("数据库版本:" + meta.getDatabaseProductVersion() + "<br>"); ResultSet tables = meta.getTables(null, null, "%", new String[]{"TABLE"}); while (tables.next()) { out.println("表名:" + tables.getString("TABLE_NAME") + "<br>"); } %>

实操价值:

  • 当process.jsp报数据库错误时,先访问http://localhost:8080/vote/info.jsp。如果它能正常输出数据库产品:UCanAccess和表名:options,说明 JDBC 连接成功,问题出在业务 SQL(如字段名写错);如果这里就报错,则是驱动或路径问题。
  • meta.getTables()返回的TABLE_NAME可直接用于构建动态管理界面(比如让manage.jsp自动生成所有表的列表)。
  • 你可以在此基础上扩展:添加meta.getColumns(null, null, "options", "%")查询options表的字段名、类型、是否主键,一键生成 CRUD 表单。

5.2 将vote.mdb迁移到 SQLite:三步完成,从此告别 Windows 专属依赖

Access 的.mdb文件有两大硬伤:一是必须依赖 Windows ODBC 或 UCanAccess(后者性能差);二是无法在 Linux/macOS 服务器上原生部署。迁移到 SQLite 后,数据库文件变成vote.db,可用任何语言(Python/Node.js/Java)直接读写,且 Tomcat 部署零额外依赖。

迁移步骤(使用免费工具 MDBTools + SQLite):

  1. 导出 MDB 表结构与数据为 SQL 脚本(Linux/macOS 终端):

    # 安装 mdbtools(Ubuntu/Debian) sudo apt-get install mdbtools # 导出 options 表结构 mdb-schema vote.mdb | sed 's/TEXT/text/g; s/INTEGER/integer/g' > vote_schema.sql # 导出 options 表数据(CSV 格式) mdb-export -D "%Y-%m-%d %H:%M:%S" vote.mdb options > options.csv
  2. 用 Python 脚本将 CSV 转为 SQLite INSERT 语句(避免手动处理引号和换行):

    # csv_to_sqlite.py import csv import sqlite3 conn = sqlite3.connect('vote.db') cursor = conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS options (name TEXT, votes INTEGER)") with open('options.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: cursor.execute("INSERT INTO options (name, votes) VALUES (?, ?)", (row['name'], int(row['votes']))) conn.commit() conn.close() print("SQLite 数据库 vote.db 创建成功")

    运行:python csv_to_sqlite.py

  3. 修改DBBean.java,切换 JDBC 驱动与 URL:

    // 替换前(UCanAccess) // private String driver = "net.ucanaccess.jdbc.UcanaccessDriver"; // private String url = "jdbc:ucanaccess://.../vote.mdb"; // 替换后(SQLite) private String driver = "org.sqlite.JDBC"; private String url = "jdbc:sqlite:" + request.getRealPath("/dbBean/vote.db");

    并将sqlite-jdbc-3.43.0.jar放入WEB-INF/lib/。

参数说明:jdbc:sqlite:是 SQLite JDBC 的标准协议;request.getRealPath()依然适用,因为vote.db也放在dbBean/目录下;SQLite 的INSERT语句与 Access 兼容度高,原有业务逻辑(如UPDATE options SET votes = votes + 1)无需修改。

5.3 从课设到生产:三个可立即落地的加固习惯

这个投票系统最大的价值,不是交差,而是让你亲手踩过 Web 开发的典型坑。从那以后我每次带新人,都会强制他们在这份源码上走三遍流程:
第一遍,只改index.jsp的 CSS,验证前端修改生效,建立“代码改了就能看见”的信心;
第二遍,在process.jsp中加一行System.out.println("投票项:" + item);,然后看catalina.out日志,理解请求如何从浏览器流到 Java 类;
第三遍,把admin/admin硬编码换成从WEB-INF/web.xml的<context-param>读取,再用getServletContext().getInitParameter("adminUser")获取,体会配置外置化的重要性。

这三步做完,你对 Java Web 的掌控感,就从“复制粘贴能跑”升级到了“知道哪行代码管什么、改哪里影响全局”。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询