ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaEE网上考试系统课程设计:Servlet+JSP+JDBC完整实现与二次开发指南

JavaEE网上考试系统课程设计:Servlet+JSP+JDBC完整实现与二次开发指南

简介:这份资源是面向软件工程、计算机相关专业学生的JavaEE课程大作业完整方案,聚焦网上考试系统的设计与实现,适合需要完成期末项目、课程设计或自学JavaEE开发的学习者参考。压缩包共74个文件,约6.75MB,以16个Java源文件、16个JSP页面和16个class文件为核心,配合CSS样式、GIF/JPG图片素材及jar依赖,构成可运行的Web工程;另附2份doc文档,含设计报告与使用说明,便于理解系统结构与部署流程。资源已有3476人学习下载,经过较多同学验证。代码注释详细、难度适中,导入后只需修改姓名、学号等个人信息即可直接使用,报告部分可帮助梳理需求分析、数据库设计与功能模块说明,使用说明则降低环境配置门槛,适合作为课程作业提交或JavaEE入门实践的参考模板。

1. 网上考试系统:一份能直接跑通的 JavaEE 课程设计资源

期末周前一周,隔壁宿舍的 A 同学抱着笔记本过来,说他选的 JavaEE 大作业是「网上考试系统」,代码能编译但一登录就白屏,报告也不知道从哪下笔。我打开压缩包翻了翻,发现这套资源其实挺完整:后端是标准的 JavaEE 三层结构,前端是 JSP 加少量 JS,数据库脚本、部署说明、设计报告一应俱全。它解决的不是「高并发在线考试平台」这种工业级需求,而是课程设计场景下「能演示、能答辩、能改」的刚需——学生登录答题、教师组卷阅卷、后台管理题库,三个角色闭环跑得通。适合谁?正在做 JavaEE 课程设计、需要一份结构清晰可二次开发的参考实现的人;也适合想复习 Servlet + JSP + JDBC 这套经典组合的初学者。下面按「它是什么 → 怎么跑起来 → 怎么改」的顺序拆。

2. 技术栈与目录结构:先看清这套 JavaEE 骨架长什么样

2.1 技术选型:为什么是 Servlet + JSP + JDBC 这套老组合

这套项目的技术栈没有花活,属于课程设计里最稳的一档:后端 Servlet 负责请求分发和业务控制,JSP 负责页面渲染,JDBC 直连数据库做持久化,前端用原生 JS 做表单校验和倒计时,构建工具是 Maven,容器是 Tomcat。没有 Spring 全家桶,也没有前后端分离,好处是依赖少、启动快、答辩时每一行代码都能讲清楚来龙去脉。

我一般会跟人说,选这套组合不是因为它先进,而是因为它「透明」。Spring Boot 一个注解背后藏了多少自动配置,课程设计答辩时老师一问就露怯;而 Servlet 的doGet/doPost、web.xml的映射、JDBC 的Connection/PreparedStatement,每一层都摆在明面上。对于要交报告、要讲设计思路的场景,这种透明性比技术新颖度值钱得多。

依赖清单大致是这几块:javax.servlet-api(Servlet 容器接口)、jsp-api(JSP 编译支持)、mysql-connector-java(数据库驱动)、jstl(JSP 标签库,用于页面循环和条件渲染)。版本上不用追新,Servlet 3.1 / 4.0 配 Tomcat 8 或 9 都能跑,MySQL 驱动用 5.1.x 或 8.0.x 要和数据库版本对齐。这里有个血泪经验:驱动版本和数据库版本不匹配,是新手最常见的翻车点,后面避坑章节会细说。

2.2 目录结构:一个标准 Maven Web 工程的分层

把压缩包解开,目录大致是这样(不同版本可能略有出入,但分层逻辑一致):

exam-system/ ├── pom.xml # Maven 依赖与打包配置 ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exam/ │ │ │ ├── controller/ # Servlet 控制层 │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── dao/ # 数据访问层 │ │ │ ├── entity/ # 实体类(User/Question/Paper/Score) │ │ │ └── util/ # 数据库连接、工具类 │ │ ├── resources/ │ │ │ └── db.properties # 数据库连接配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml # Servlet 映射、欢迎页 │ │ ├── jsp/ # 各角色页面 │ │ ├── css/ js/ # 静态资源 │ │ └── index.jsp # 登录入口 │ └── test/ # 测试代码(可能为空) ├── sql/ │ └── exam.sql # 建库建表 + 初始数据 └── 使用说明.md / 设计报告.docx

这个结构的关键在于controller → service → dao → entity四层分明。控制层只做参数接收和页面跳转,业务层处理组卷、判分这类逻辑,DAO 层封装所有 SQL,实体类对应数据库表。改功能时按层定位,不会一锅粥。我特别建议先看entity包,四五个实体类看完,整个系统的数据模型就清楚了:用户、题目、试卷、成绩,外加一个题目类型枚举。

2.3 数据库设计:五张核心表撑起整个业务

sql/exam.sql里通常包含建库语句和初始数据。核心表大致是这几张:

表名作用关键字段
user用户(学生/教师/管理员)id, username, password, role
question题库id, type, content, options, answer, score
paper试卷id, title, total_score, duration, create_by
paper_question试卷与题目关联paper_id, question_id, order_no
score成绩记录id, user_id, paper_id, score, submit_time

question表的type字段区分单选、多选、判断,options存选项(常见做法是用 JSON 或分隔符拼接),answer存标准答案。paper_question这张关联表是组卷的核心,一张试卷有哪些题、什么顺序,全靠它。score表记录谁在什么时候交了什么卷、得了多少分。这五张表的关系理清了,后面看代码就是顺藤摸瓜。

提示:导入exam.sql前先确认字符集。老脚本常用utf8,如果数据库默认是utf8mb4,中文题干可能出现乱码,建库时统一成utf8mb4更稳妥。

3. 跑起来:从导库到登录的完整启动链路

3.1 环境准备与数据库导入

启动这套系统的前置条件不多:JDK 8(或 11)、Maven 3.6+、Tomcat 8.5/9、MySQL 5.7/8.0。装好之后第一步是导库。用命令行或图形工具都行,命令行更直观:

# 登录 MySQL(按提示输入密码) mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 退出后导入脚本(路径按实际调整) mysql -u root -p exam_system < sql/exam.sql

导入完成后执行SHOW TABLES;确认五张表都在,再SELECT * FROM user;看看初始账号。通常脚本里会预置一个管理员账号和几个测试学生账号,密码可能是明文也可能是 MD5,看脚本里的数据。这一步别跳过,账号信息是后面登录的钥匙。

3.2 改配置:数据库连接那几行必须对上

src/main/resources/db.properties是数据库连接的唯一入口,内容大致长这样:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/exam_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码

四个参数逐个说清楚:jdbc.driver在 MySQL 8 驱动下是com.mysql.cj.jdbc.Driver,MySQL 5 驱动则是com.mysql.jdbc.Driver,写错直接ClassNotFoundException;jdbc.url里的serverTimezone不加,MySQL 8 会报时区错误;useSSL=false是本地开发省掉证书握手;用户名密码换成你自己的。改完这四行,连接层基本就通了。

对应的工具类DBUtil一般长这样,用静态块加载驱动、提供获取和关闭连接的方法:

public class DBUtil { private static String url; private static String username; private static String password; // 静态块在类加载时执行一次,读取配置并注册驱动 static { try { Properties props = new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序:ResultSet → Statement → Connection,逐个判空 try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }

逻辑说明:静态块保证驱动只注册一次,配置只读一次;getConnection每次新建连接,课程设计规模下够用,不用连接池。参数说明:url/username/password是静态字段,被所有 DAO 共享。这里有个常见误用——把close写成只关 Connection 不关 ResultSet,短时间看不出问题,压测或反复操作后连接泄漏,Tomcat 日志里会冒Too many connections。

3.3 部署到 Tomcat 并验证登录

配置改好后,用 Maven 打包再丢进 Tomcat:

# 在项目根目录执行,生成 target/exam-system.war mvn clean package # 把 war 复制到 Tomcat 的 webapps 目录(路径按实际调整) cp target/exam-system.war $TOMCAT_HOME/webapps/ # 启动 Tomcat $TOMCAT_HOME/bin/startup.sh # Windows 用 startup.bat

启动后访问http://localhost:8080/exam-system/,应该看到登录页。用脚本里的管理员账号登录,进后台看题库列表有没有数据;再用学生账号登录,看能不能进考试列表。两个角色都通了,说明数据库、Servlet 映射、JSP 渲染整条链路没问题。如果白屏或 404,先看 Tomcat 的logs/catalina.out,再看浏览器控制台,八成是web.xml映射路径和访问路径对不上,或者 war 包名和 context path 不一致。

注意:IDEA 里直接配 Tomcat 跑也可以,但要确认 Artifact 的 context path 和web.xml里的 servlet 映射一致,否则页面里的相对路径跳转会 404。

4. 核心模块拆解:登录、组卷、答题判分三条主链路

4.1 登录与权限拦截:一个 Filter 管住所有角色

登录流程是「表单提交 → Servlet 校验 → 写 Session → 跳角色首页」。校验逻辑在LoginServlet里,核心是查用户、比对密码、把用户对象塞进 Session:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user == null) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/jsp/login.jsp").forward(req, resp); return; } // 登录成功,用户信息写入 Session,后续页面据此判断角色 req.getSession().setAttribute("currentUser", user); // 按角色分流到不同首页 String target = "STUDENT".equals(user.getRole()) ? "/jsp/student/home.jsp" : "/jsp/admin/home.jsp"; resp.sendRedirect(req.getContextPath() + target); }

逻辑说明:userService.login内部走 DAO 查库,返回 null 表示账号或密码不对;成功则把User对象放进 Session,键名currentUser是全系统约定。参数说明:username/password来自表单字段名,JSP 里<input name="username">必须和这里一致,改字段名要两边同步。

光有登录不够,还得防越权。项目里通常有一个AuthFilter,拦截/jsp/admin/*和/jsp/student/*,检查 Session 里有没有currentUser、角色对不对:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); User user = (User) req.getSession().getAttribute("currentUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); return; } // 管理员路径只允许 ADMIN 角色访问 if (uri.contains("/admin/") && !"ADMIN".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); }

这个 Filter 在web.xml里用<filter-mapping>配url-pattern为/jsp/*。改权限规则时只动这一个类,不用满项目找判断。我一般会把角色判断抽成常量或枚举,避免字符串硬编码写错大小写——"admin"和"ADMIN"不相等这种坑,排查起来很费时间。

4.2 组卷逻辑:教师端怎么把题目拼成一张卷

组卷是教师端的核心。流程是:教师勾选题目 → 提交题目 ID 列表 → 后端写paper和paper_question两张表。Servlet 接收参数后调 Service,Service 里开事务保证两张表要么都成功要么都回滚:

public boolean createPaper(Paper paper, List<Integer> questionIds) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入试卷主记录,返回自增主键 int paperId = paperDao.insert(conn, paper); // 2. 批量插入试卷-题目关联,order_no 决定答题顺序 for (int i = 0; i < questionIds.size(); i++) { paperQuestionDao.insert(conn, paperId, questionIds.get(i), i + 1); } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} return false; } finally { DBUtil.close(conn, null, null); } }

逻辑说明:setAutoCommit(false)关掉自动提交,两步插入都成功才commit,任何一步抛异常就rollback。参数说明:paper含标题、总分、时长;questionIds是前端传来的题目 ID 列表,顺序即答题顺序,order_no从 1 开始。这里的关键是 DAO 方法要接收外部传入的Connection,而不是自己新建——否则事务管不住两个 DAO 的连接。这是 JDBC 事务的经典写法,也是很多课程设计里容易写错的地方:每个 DAO 各开各的连接,事务形同虚设。

总分怎么算?常见做法是在插入前遍历题目,把每题score累加写进paper.total_score,而不是让前端传。前端传的总分可以被篡改,后端算才可靠。

4.3 答题与自动判分:客观题怎么对答案

学生提交答卷时,前端把「题目 ID → 所选答案」的映射传给后端。判分逻辑在ExamService.submit里:逐题取标准答案比对,累加得分,最后写score表。

public int gradeAndSave(int paperId, int userId, Map<Integer, String> answers) { List<Question> questions = questionDao.findByPaperId(paperId); int total = 0; for (Question q : questions) { String userAnswer = answers.get(q.getId()); if (userAnswer == null) continue; // 未作答不得分 // 多选题答案排序后比较,避免顺序不同误判 if (normalize(userAnswer).equals(normalize(q.getAnswer()))) { total += q.getScore(); } } scoreDao.insert(userId, paperId, total, new Date()); return total; } // 去空格、统一大写、多选答案按字符排序 private String normalize(String ans) { char[] cs = ans.replaceAll("\\s", "").toUpperCase().toCharArray(); Arrays.sort(cs); return new String(cs); }

逻辑说明:遍历试卷所有题目,取学生答案和标准答案,normalize做归一化后比较。参数说明:answers的 key 是题目 ID,value 是选项字母(如"A"或"ABD")。多选题最容易翻车——学生选"DBA"、标准答案是"ABD",直接字符串比较就判错,所以要先排序。这个细节报告里写进去,答辩时是个加分点。

判分只覆盖客观题。如果题库里有主观题(简答、论述),常见做法是标记为「待人工阅卷」,教师端单独一个页面打分,score表加个status字段区分已判/待判。这套资源里如果只有客观题,那判分链路就是全自动的,简单但完整。

5. 避坑与排查:启动和运行阶段最容易翻车的五件事

5.1 中文乱码:从请求到响应三处都要设

现象:题干、选项、用户名里的中文全变问号或方块。原因通常有三层:数据库字符集不是utf8mb4、JDBC url 没带characterEncoding=utf8、Servlet 里没设request.setCharacterEncoding("UTF-8")。解决:建库时指定字符集,url 补上编码参数,并在 Filter 里统一setCharacterEncoding,响应侧resp.setContentType("text/html;charset=UTF-8")。三处都对齐,乱码基本绝迹。

5.2 驱动版本不匹配:ClassNotFound 与时间戳异常

现象一:启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是 pom 里引的是 MySQL 5 驱动,配置里却写了 8 的类名(或反过来)。解决:驱动版本和类名对齐,5.x 用com.mysql.jdbc.Driver,8.x 用com.mysql.cj.jdbc.Driver。现象二:连库报时区错误The server time zone value ... is unrecognized。原因是 MySQL 8 驱动要求显式时区。解决:url 加serverTimezone=Asia/Shanghai。这两个是新手最高频的翻车点,改配置时一次到位。

5.3 404 与白屏:路径映射对不上

现象:访问首页 404,或登录后跳转白屏。原因多半是 context path 不一致——war 包名是exam-system,访问却用了/exam;或者web.xml里 servlet 的url-pattern和表单action不匹配。解决:先确认 Tomcatwebapps下的目录名,访问路径以它为准;再对照web.xml的<servlet-mapping>和 JSP 里的action,三者路径要能串起来。白屏但无报错,多半是 JSP 里 EL 表达式取不到 Session 属性,检查键名是否和setAttribute一致。

5.4 连接泄漏:Too many connections 的连锁反应

现象:反复登录、答题几次后,页面卡住,Tomcat 日志出现Too many connections。原因是 DAO 里Connection没关,或者close只关了部分资源。解决:统一走DBUtil.close,finally块里调用;检查每个 DAO 方法是否都有finally。课程设计规模下连接数不多,但泄漏是累积的,演示到一半崩掉很尴尬。我一般会在DBUtil里加个计数器打日志,开发阶段看连接开闭是否配对。

5.5 事务失效:每个 DAO 各开各的连接

现象:组卷时试卷插进去了,关联题目却没插进去,数据半截。原因是 Service 开了事务,但 DAO 内部自己getConnection,事务管的是另一个连接。解决:事务方法里把Connection作为参数传给 DAO,所有操作共用同一个连接,commit/rollback才有效。这个坑不看代码很难发现,因为单步执行都「成功」,只有异常回滚时才暴露。

6. 二次开发:从课程设计到能讲清楚的作品

拿到这套代码,最忌讳的是原样交上去。答辩老师看过的同类项目不少,改都没改一眼就能看出来。我的建议是从三个方向做增量,每个都不大但能体现你理解了架构。

第一个方向是加题型。现有题库如果是单选、多选、判断,可以加一个「填空」类型:question.type加枚举值,判分时对填空做去空格、忽略大小写的比较,前端 JSP 加一个输入框分支。改动点集中在Question实体、判分 Service 和一个 JSP 片段,四层结构的好处这时候就体现出来了——按层改,不牵一发动全身。

第二个方向是加考试时长控制。paper.duration字段如果只是展示没用上,可以在学生进入考试时把「开始时间 + 时长」写进 Session,前端 JS 做倒计时展示,后端在提交时校验是否超时,超时则按已答内容判分或直接作废。这里要注意前后端都要校验,只靠前端倒计时可以被绕过。

第三个方向是成绩统计。score表已经有数据,加一个教师端页面,按试卷维度算平均分、最高分、及格率,用一条 SQL 聚合就能出结果:

SELECT p.title, COUNT(s.id) AS submit_count, AVG(s.score) AS avg_score, MAX(s.score) AS max_score, SUM(s.score >= 60) / COUNT(s.id) AS pass_rate FROM score s JOIN paper p ON s.paper_id = p.id GROUP BY p.id, p.title;

这条 SQL 直接喂给 JSP 循环渲染就是一张统计表,报告里再配一段「为什么用聚合而非应用层计算」的说明,说服力就有了。参数说明:pass_rate依赖 MySQL 布尔转 0/1 的特性,换数据库要改写。

验证方法很简单:每改一个功能,先把三个角色的主流程各走一遍——管理员建题、教师组卷、学生答题看分。改完跑不通,八成是新代码破坏了原有的事务边界或 Session 约定。我吃过一次亏:加填空题时在 DAO 里新开了连接,结果组卷事务回滚不彻底,数据脏了半张表,排查了一晚上才发现是连接没共用。从那以后我每次动 DAO 层,都强制把「Connection 从哪来、到哪关」在纸上画一遍再写代码。希望这套拆解能帮你少走点弯路,把这份课程设计改成自己讲得明白的作品。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进