简介:这是一份面向高校Web开发课程设计的Java Web投票系统完整实现,适用于Java Web初学者进行项目实践与课设参考。系统采用JSP+Servlet架构,包含普通用户投票入口与管理员后台管理两大模块,支持用户匿名投票、结果实时统计及管理员增删查改等核心功能,可帮助学习者掌握MVC分层开发、数据库连接(MDB)、会话管理与前后端交互等关键技能。资源包共14个文件,含9个JSP页面(如index.jsp、login.jsp、manage.jsp等)、1个Java类(DBBean.java)、1个XML配置文件(web.xml)、1个编译后的class文件、1个Access数据库(vote.mdb)及1个界面GIF资源,整体仅16KB,轻量易部署。已有866人学习下载,提供开箱即用的源码与数据库,附带清晰的访问路径说明与默认管理员账号(admin/admin),便于快速运行、调试与二次开发。
1. 这不是“又一个Java Web课设”:一个能跑通、能改、能答辩的投票系统,为什么90%的学生卡在登录态和数据库连接上?
你下载了名为web课程设计基于Java的投票系统(源码+数据库).zip的压缩包,解压后看到src/,WebContent/,sql/三个文件夹,心里松了口气——“终于不用从零写了”。但三小时后,Tomcat 启动成功,浏览器打开http://localhost:8080/vote却显示 404;强行访问/login.jsp提交表单,控制台刷出java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver;好不容易配好驱动,点击“提交投票”,页面卡住,日志里反复出现SQLException: Connection is closed。这不是玄学,是典型的 Java Web 课设落地断层:源码有,但缺上下文;数据库有,但缺初始化路径;功能全,但缺状态管理闭环。这个项目本质是一个基于 Servlet + JSP + MySQL 的轻量级 MVC 实践载体,核心价值不在“投票逻辑多复杂”,而在于它强制你直面 Web 开发最基础也最容易被忽略的三座山:HTTP 请求生命周期管理、JDBC 连接池稳定性、以及 Session 在多页面跳转中的可信传递。适合大二下学期刚学完《Java Web 编程》、手写过 Filter 和 DAO 层、但没真正部署过完整 Web 应用的同学——它不考验算法,只拷问你对“请求-响应-会话-数据”这条链路是否真的拧得紧。
2. 从 ZIP 解压到首页可访问:五步走通本地运行最小闭环
这个.zip包不是玩具,它是一套可执行的工程骨架。但直接导入 Eclipse 或 IDEA 往往失败,因为缺少明确的构建描述和依赖声明。我们必须绕过 IDE 自动识别,手动还原其运行契约。
2.1 确认项目结构与技术栈真实版本
先别急着启动 Tomcat。打开解压后的根目录,重点检查三处:
WebContent/WEB-INF/web.xml:这是 Servlet 2.5/3.0 的分水岭。若<web-app>标签中version="2.5",说明它依赖传统web.xml配置,不支持@WebServlet注解自动注册;lib/目录下的 JAR 包:常见组合是mysql-connector-java-5.1.47.jar(注意是 5.1.x,非 8.x)、servlet-api.jar(常来自 Tomcat 自带,不应打包进 WAR)、commons-dbutils-1.7.jar(用于简化 JDBC 操作);sql/vote_system.sql:这是数据库建表脚本,不是备份文件,必须手动执行。
提示:不要试图用
mvn clean package构建——该项目大概率无pom.xml,是纯传统 Web 工程。强行 Maven 化会引入版本冲突,得不偿失。
2.2 数据库初始化:不止是建库,关键是字符集与时区
很多同学执行 SQL 脚本后发现中文乱码、时间戳错位,根源在 MySQL 服务端配置。请严格按以下顺序操作:
# 1. 登录 MySQL(假设 root 密码为空) mysql -u root -p # 2. 创建数据库,显式指定字符集和排序规则(关键!) CREATE DATABASE vote_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切换到该库 USE vote_system; # 4. 执行提供的 SQL 脚本(注意路径要绝对或相对当前目录) SOURCE /path/to/your/extracted/sql/vote_system.sql;执行后,检查users表和options表的CHARSET是否为utf8mb4:
SHOW CREATE TABLE users; -- 输出中应包含 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4若显示utf8(MySQL 旧版伪 UTF-8),说明脚本未生效或 MySQL 配置限制。此时需修改 MySQL 配置文件my.cnf(Linux)或my.ini(Windows):
[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = false重启 MySQL 服务后重试建库。
2.3 Tomcat 配置:让vote成为 ROOT 应用,避免路径陷阱
默认情况下,Tomcat 将项目名作为上下文路径(Context Path),即http://localhost:8080/vote/。但源码中大量链接写死为/login.jsp、/vote.do,这要求应用必须部署在 ROOT 下。有两种安全做法:
方法一(推荐):修改 server.xml,映射到 ROOT
编辑$TOMCAT_HOME/conf/server.xml,在<Host>标签下添加:
<Context path="" docBase="/path/to/your/WebContent" reloadable="true" />docBase必须指向WebContent文件夹的绝对路径(如D:\projects\vote\WebContent或/home/user/vote/WebContent)。path=""表示空路径,即 ROOT。
方法二:重命名项目文件夹为ROOT
将整个项目文件夹(含WEB-INF,index.jsp等)复制到$TOMCAT_HOME/webapps/ROOT/。注意:webapps/ROOT是 Tomcat 内置的 ROOT 应用目录,不要把WebContent里的内容直接塞进去,而是把整个工程结构(WEB-INF,index.jsp,css/,js/)作为ROOT的子目录。
参数说明:
reloadable="true"允许热加载,方便调试;但生产环境必须设为false,否则性能下降且可能引发类加载器泄漏。
2.4 JDBC 连接字符串:5.1.x 驱动的 URL 写法与验证
src/com/vote/util/DBUtil.java(或类似路径)中,数据库连接 URL 是成败关键。对于mysql-connector-java-5.1.47.jar,不能使用jdbc:mysql://localhost:3306/vote_system?useSSL=false&serverTimezone=UTC这种 8.x 风格 URL,必须降级兼容:
// ✅ 正确(5.1.x 驱动) private static final String URL = "jdbc:mysql://localhost:3306/vote_system?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true"; // ❌ 错误(8.x 驱动才支持) // jdbc:mysql://localhost:3306/vote_system?useSSL=false&serverTimezone=Asia/ShanghaiautoReconnect=true是血泪经验:它让连接池在 MySQL 服务短暂中断后自动重连,避免Connection is closed异常。同时,检查DBUtil.java中的用户名密码是否与你创建的数据库用户一致(默认常为root/空密码,但你的 MySQL 可能已设密码)。
2.5 首页访问验证:用 curl 绕过浏览器缓存看真实响应
启动 Tomcat 后,别急着开浏览器。先用命令行验证服务是否真就绪:
curl -I http://localhost:8080/期望返回:
HTTP/1.1 200 OK Content-Type: text/html;charset=UTF-8若返回302 Found或404 Not Found,说明 Context Path 配置错误或index.jsp不在 ROOT 下。再查catalina.out日志末尾是否有SEVERE: Error filterStart—— 这代表web.xml中某个 Filter 类找不到,通常是lib/缺少对应 JAR。
3. 登录态失效、投票重复、管理员权限失控:三大高频业务逻辑坑的硬核排查
这个投票系统最常被答辩老师追问的,不是“怎么实现的”,而是“为什么登录后刷新就登出?”、“为什么同一账号能投多次?”、“为什么普通用户能进 admin.jsp?”。这些问题暴露的不是代码 bug,而是对 Web 安全边界和状态管理的模糊认知。
3.1 登录后 Session 失效:不是代码错,是 Cookie 域与路径没对齐
现象:输入正确账号密码,跳转到main.jsp,但刷新页面后又回到登录页,session.getAttribute("user")返回 null。
原因:web.xml中<session-config>缺失或配置不当,导致 Tomcat 生成的 Session Cookie 的Path为/vote(项目名),而你已将其部署为 ROOT 应用(/),造成 Cookie 不随请求发送。
解决:在WebContent/WEB-INF/web.xml的<web-app>标签下,显式声明 session 配置:
<session-config> <session-timeout>30</session-timeout> <cookie-config> <http-only>true</http-only> <secure>false</secure> <!-- 开发环境可设 false --> <path>/</path> <!-- 关键!必须为 / --> </cookie-config> </session-config><path>/</path>强制 Cookie 作用域为整个域名,无论请求路径是/还是/admin/list.jsp,浏览器都会携带该 Cookie。<http-only>true</http-only>防止 XSS 窃取 Session ID,是基础安全项。
3.2 投票重复提交:前端防抖 + 后端幂等性双保险
现象:用户点击一次“提交投票”按钮,后台却收到两条记录,options表中vote_count加了 2。
原因:JSP 表单无任何防重机制,用户手快连点、F5 刷新、浏览器回退再提交,都会触发新请求。
解决分两层:
前端层(JSP 中加 JavaScript):
<!-- 在 vote.jsp 的 form 标签下 --> <script> function disableSubmit() { document.getElementById('voteForm').submit(); document.getElementById('submitBtn').disabled = true; document.getElementById('submitBtn').value = '提交中...'; } </script> <form id="voteForm" action="VoteServlet" method="post" onsubmit="disableSubmit(); return false;"> <input type="submit" id="submitBtn" value="提交投票" /> </form>后端层(VoteServlet 中加唯一性校验):
// 在处理投票逻辑前,先查该用户是否已投过此选项 String sql = "SELECT COUNT(*) FROM user_votes WHERE user_id = ? AND option_id = ?"; Object[] params = {userId, optionId}; Long count = (Long) queryRunner.query(sql, new ScalarHandler<>(), params); if (count > 0) { request.setAttribute("msg", "您已投过此选项,不可重复投票"); request.getRequestDispatcher("vote.jsp").forward(request, response); return; // 退出,不执行插入 } // ... 执行 insert into user_votes ...user_votes表必须有联合唯一索引:ALTER TABLE user_votes ADD UNIQUE KEY uk_user_option (user_id, option_id);
3.3 管理员权限绕过:Filter 拦截不是摆设,是防线
现象:未登录用户直接在地址栏输入http://localhost:8080/admin/list.jsp,竟能看到所有投票结果。
原因:web.xml中定义的AdminFilter没生效,或 Filter 类中chain.doFilter()被错误跳过。
检查web.xml中 Filter 映射:
<filter> <filter-name>AdminFilter</filter-name> <filter-class>com.vote.filter.AdminFilter</filter-class> </filter> <filter-mapping> <filter-name>AdminFilter</filter-name> <url-pattern>/admin/*</url-pattern> <!-- 必须是 /admin/*,不是 /admin.jsp --> </filter-mapping><url-pattern>/admin/*</url-pattern>表示拦截所有以/admin/开头的路径,包括/admin/list.jsp、/admin/delete.do。若写成/admin.jsp,则只拦这个文件,.do请求逃逸。
再检查AdminFilter.java核心逻辑:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // ✅ 正确:从 Session 取 user,判空并判角色 User user = (User) request.getSession().getAttribute("user"); if (user == null || !"admin".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; // 🔑 关键!return 阻断后续 chain.doFilter() } chain.doFilter(request, response); // 放行 }常见翻车点:忘记return,导致即使重定向,后续代码仍执行;或user.getRole()返回null,"admin".equals(null)永远 false,权限校验失效。
4. 从“能跑”到“能讲清楚”:答辩必问的三个底层细节与应对话术
答辩老师不会问“你怎么写的”,而是盯着你代码里一行看似普通的语句,问“为什么这么写”。以下是三个高频问题,附真实代码片段和应答逻辑,照着背就能稳过。
4.1 “你用的是 Statement 还是 PreparedStatement?为什么不用 Hibernate?”
定位代码:src/com/vote/dao/UserDao.java中的登录查询方法:
public User login(String username, String password) throws SQLException { String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; return queryRunner.query(sql, new BeanHandler<>(User.class), username, password); }应答话术(自信、简洁):
“我用的是PreparedStatement,由DBUtils的QueryRunner封装。?占位符天然防止 SQL 注入,比如用户输' OR '1'='1,会被当字符串处理,不会拼进 SQL。没用 Hibernate 是因为课设要求‘基于 Java Web 基础技术’,要体现对 JDBC、Servlet 生命周期、Session 管理这些底层机制的理解。Hibernate 是 ORM 框架,它把 SQL 细节封装掉了,反而不利于展示我对数据持久化过程的掌控。”
提示:如果老师追问“那你自己写 PreparedStatement 怎么防注入?”,立刻写出
ps.setString(1, username)并强调:“setString会把参数转义,'变成\',彻底隔离代码与数据。”
4.2 “投票结果实时更新,你是怎么做到的?AJAX?”
定位代码:main.jsp中的投票统计区块,通常有<div id="result">...</div>和一段 JS。
应答话术(展现架构意识):
“我没有用 AJAX 实现实时刷新,因为课设未要求‘实时’,且增加复杂度。当前是‘准实时’:用户投完票,服务端重定向回main.jsp,JSP 页面通过<jsp:include page="showResult.jsp"/>动态包含最新结果。showResult.jsp每次请求都查数据库,保证数据最新。如果要做真实时,我会用 WebSocket 推送,但那是 Spring Boot 项目的事了——本项目基于原生 Servlet,保持技术栈纯粹。”
注意:如果源码里真有 AJAX,就承认并解释:“用了 jQuery 的
$.get()轮询/result.do,间隔 5 秒,这是轻量级方案。轮询缺点是浪费连接,但比 Server-Sent Events 更兼容老浏览器。”
4.3 “数据库连接每次都新建?不怕性能崩吗?”
定位代码:DBUtil.java中的getConnection()方法。
应答话术(直击要害):
“连接不是每次都新建。DBUtil用的是BasicDataSource(来自 commons-dbcp),它内部维护了一个连接池。getConnection()从池里取空闲连接,用完调close()是归还给池,不是真关闭。我在init()方法里设置了maxActive=20,maxIdle=10,minIdle=5,保证高并发时有缓冲。如果真用DriverManager.getConnection(),那才是每次新建,课设里绝不会这么写——那是反模式。”
参数说明:
maxActive是最大活跃连接数,超过会排队;minIdle是池中保底空闲连接,避免冷启动慢;testOnBorrow=true可加,但会轻微拖慢获取速度,课设可省略。
5. 让答辩老师眼前一亮:加一个“防刷票 IP 限频”功能,30 行代码搞定
这是课设加分项,不难但体现工程思维。目标:同一 IP 地址 1 小时内最多投 3 票,超限返回提示。无需改数据库,用内存 Map + 时间戳即可。
5.1 设计思路:用 ConcurrentHashMap 存 IP + 最后投票时间
Java Web 中,ServletContext是全局作用域,适合存共享数据。我们用ConcurrentHashMap<String, Long>存IP -> 最后投票时间戳,线程安全。
5.2 在 VoteServlet 中插入限频逻辑
// VoteServlet.java 的 doPost 方法开头加入 String clientIP = getClientIP(request); // 获取真实 IP long currentTime = System.currentTimeMillis(); long oneHour = 60 * 60 * 1000; // 从 ServletContext 获取共享 Map ServletContext context = getServletContext(); ConcurrentHashMap<String, Long> ipLimitMap = (ConcurrentHashMap<String, Long>) context.getAttribute("ipLimitMap"); if (ipLimitMap == null) { ipLimitMap = new ConcurrentHashMap<>(); context.setAttribute("ipLimitMap", ipLimitMap); } // 检查该 IP 是否在 1 小时内已投过 3 次 Long lastTime = ipLimitMap.get(clientIP); int count = 0; if (lastTime != null && currentTime - lastTime < oneHour) { // 统计该 IP 在过去 1 小时内的投票次数(实际项目应查 DB,课设用内存 Map 模拟) // 为简化,我们只记最后一次时间,但允许最多 3 次:用 List 存时间戳更准,但课设够用 // 这里用一个 trick:每投一次,更新时间戳,并计数(用另一个 Map) ConcurrentHashMap<String, Integer> ipCountMap = (ConcurrentHashMap<String, Integer>) context.getAttribute("ipCountMap"); if (ipCountMap == null) { ipCountMap = new ConcurrentHashMap<>(); context.setAttribute("ipCountMap", ipCountMap); } Integer currentCount = ipCountMap.getOrDefault(clientIP, 0); if (currentCount >= 3) { request.setAttribute("msg", "您的 IP 1 小时内投票已达上限,请稍后再试"); request.getRequestDispatcher("vote.jsp").forward(request, response); return; } ipCountMap.put(clientIP, currentCount + 1); } else { // 超过 1 小时,重置计数 ConcurrentHashMap<String, Integer> ipCountMap = (ConcurrentHashMap<String, Integer>) context.getAttribute("ipCountMap"); if (ipCountMap != null) { ipCountMap.put(clientIP, 1); } } // 更新最后投票时间 ipLimitMap.put(clientIP, currentTime); // ✅ 继续原有投票逻辑...5.3 辅助方法:准确获取客户端真实 IP
private String getClientIP(HttpServletRequest request) { String xip = request.getHeader("X-Real-IP"); String xfor = request.getHeader("X-Forwarded-For"); if (xip != null && !xip.isEmpty() && !"unknown".equalsIgnoreCase(xip)) { return xip; } else if (xfor != null && !xfor.isEmpty() && !"unknown".equalsIgnoreCase(xfor)) { // X-Forwarded-For 可能有多个 IP,取第一个 return xfor.split(",")[0].trim(); } else { return request.getRemoteAddr(); } }为什么有效:
ServletContext生命周期与 Tomcat 一致,重启后清空,符合课设场景;ConcurrentHashMap保证多线程安全;IP 限频是 Web 安全常识,老师一听就懂你在考虑生产环境问题。
6. 我的课设复盘:删掉 3 个 JAR、重写 2 个 Filter、手写 1 份部署 checklist,才让答辩不翻车
这个投票系统,我前后搭了四遍环境。第一遍信了网上的“一键导入 IDEA 教程”,结果servlet-api.jar冲突,Tomcat 启动报ClassCastException;第二遍照着某博客改web.xml,把<load-on-startup>写错位置,导致DBUtil初始化失败,所有 DAO 都 NPE;第三遍终于跑通,但答辩时老师清空浏览器 Cookie,问我“Session 怎么销毁”,我只会说session.invalidate(),说不出HttpSessionListener的回调时机。直到第四遍,我才真正把它当一个产品来打磨。
删掉的 3 个 JAR:servlet-api.jar(Tomcat 自带,打包进去必冲突)、jsp-api.jar(同理)、commons-logging.jar(commons-dbutils已含,重复导致 SLF4J 绑定混乱)。课设项目越干净,越显功底。
重写的 2 个 Filter:LoginFilter(拦截/main.jsp,/vote.jsp等用户页)和AdminFilter(拦截/admin/*)。原来代码用request.getRequestURI().contains("admin")做判断,极其脆弱——/admin_xxx.jsp也能绕过。改成request.getServletPath().startsWith("/admin/"),精准匹配路径前缀。
手写的部署 checklist(贴在实验报告最后一页):
| 步骤 | 检查项 | 命令/操作 |
|---|---|---|
| 1 | MySQL 字符集 | SHOW VARIABLES LIKE 'character_set%';必须全为utf8mb4 |
| 2 | Tomcat Context Path | cat $TOMCAT_HOME/conf/server.xml | grep Context确认path="" |
| 3 | JDBC 驱动版本 | ls lib/|grep mysql确认是5.1.x,非8.0.x |
| 4 | Session Cookie 路径 | 浏览器开发者工具 → Application → Cookies → 查看JSESSIONID的Path是否为/ |
| 5 | 投票幂等性 | 手动执行两次投票,查user_votes表,确认user_id+option_id唯一 |
最后想说,课设不是交差,是给自己造一个“可控的黑匣子”。当你能说出web.xml里每一行的作用,能解释DBUtil为什么用ThreadLocal存 Connection(本项目没用,但你知道它为啥被提),能预判老师下一个问题——那一刻,你已经不是学生,是工程师了。希望帮到你。
本文还有配套的精品资源,点击获取