简介:基于JSP+Servlet构建的在线考试管理系统,整合jQuery、Bootstrap与JDBC技术,面向毕业设计学生与Java Web初学者,用于快速实现在线答题与管理后台,适合课程设计、毕业设计选题参考。学生端提供试题选择、在线答题、交卷后错题红色标记与正确答案绿色标注,并支持实时计时与答题进度提示;管理员端支持发布试卷与试题,覆盖题目、试卷、用户、角色四大模块,包括关键词搜索、试题锁定、权限分配等,功能完整。压缩包共104个文件,含43个Java源文件、22个JSP页面、12个jar依赖库,以及CSS/JS样式脚本、SQL数据库脚本等,整体大小6.14MB,基于IDEA 2021.3、MySQL 5.7.26、Tomcat 7.0.73与JDK 1.8运行。目前已有43人学习下载。资源附有可导入的项目目录与数据库初始化脚本,能直接部署演示,代码分层清晰,便于理解Servlet+JSP的请求处理与JDBC数据交互,对毕业设计、课程实践均有参考价值。
1. 基于jsp+servlet的在线考试管理系统是什么:一个典型JavaWeb课设项目的完整落地
如果你正在找java课程设计案例源码,大概率会在检索结果里看到“基于jsp+servlet的在线考试管理系统”这类项目。它不是什么新鲜架构,而是JSP做页面、Servlet做控制器、JDBC访问MySQL数据库的经典三层结构,用来支撑一套简单的在线考试流程:管理员维护题库和试卷,学生登录后在线答题,系统自动判分并返回成绩。这个方向之所以常被选做课设和毕设,是因为它恰好覆盖了JavaWeb底层的核心链路——请求响应、会话跟踪、数据库增删改查、表单处理、页面渲染。读这个标题背后的资料,你要拿到的不只是能跑的源码和数据库文件,而是把“浏览器发起请求到数据库返回结果”这条线彻底走通的能力。适合正在做JavaWeb课设、想补Servlet和JSP底层逻辑的初学者,也适合想快速复习传统JavaWeb项目打包部署的老手。
2. 用IDEA新建jsp项目并跑通Tomcat:骨架搭建与版本匹配的四个决策点
2.1 为什么JSP+Servlet这套老技术还值得搭一遍
网上大量免费python源码大全、springboot项目满天飞的今天,JSP+Servlet看起来像是上个时代的产物。但你真去面试或做课设答辩时,Servlet生命周期、请求转发与重定向、Session原理这些底层问题还是会被反复问。这套在线考试管理系统选JSP+Servlet,最大的价值在于没有框架帮你遮住细节:请求怎么进Servlet、Servlet怎么调DAO、结果怎么回写页面,每一步都看得见。理解了这些,后面再接触SpringMVC时,你一眼就能看出DispatcherServlet就是把原本手写的转发逻辑统一收口了。所以不要觉得这是“老技术没用”,它是最便宜的底层补课材料。
2.2 在IDEA里新建一个JSP项目的标准动作
用IDEA新建jsp项目,常见做法是直接建一个Java Enterprise工程,或者先建普通Java项目再手动添加Web支持。我一般选后者,步骤少且可控。具体操作是这样:
# 假设你已经用IDEA创建了一个普通Java项目,接下来在项目结构里加Web支持 # 1. 右键项目 -> Add Framework Support -> 勾选 Web Application # 2. 确认生成的 web/WEB-INF/web.xml 存在,这是JavaWeb应用的入口描述文件 # 3. 在 src 下新建包结构,例如 com.exam.servlet / com.exam.dao / com.exam.entity建完Web支持后,还需要配置Tomcat才能跑起来。打开Run/Debug Configurations,新增Tomcat Server -> Local,在Server标签页里把Application Server指向你本地的Tomcat安装目录。Deployment标签页里点加号,选择Artifact,一般会出现一个带“:war exploded”的选项,选它。这个“exploded”模式表示解压目录,开发时修改JSP或类文件不用重新打包,浏览器刷新就能生效。
<?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"> <display-name>OnlineExamSystem</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>这段web.xml声明了应用名称和欢迎页。实际项目里,Servlet的注册你可以选择在web.xml里用<servlet>和<servlet-mapping>配对写,也可以用@WebServlet("/login")注解直接标在Servlet类上。两种方式都常见,课设源码里往往混用,容易造成排查困难。我建议新写的代码统一用注解方式,少一层映射文件要维护。
2.3 Tomcat、JDK和Servlet API三者的版本匹配
这是新手第一个容易翻车的地方。Tomcat版本直接决定你能用哪个版本的Servlet API。Tomcat 9对应Servlet 4.0规范,需要JDK 8及以上;Tomcat 10对应Servlet 5.0,但包名从javax.servlet变成了jakarta.servlet。这意味着你从网上下载的大多数JSP+Servlet课设源码,很多是基于Tomcat 8或9写的,强行丢进Tomcat 10会直接编译报错,提示找不到javax.servlet包。解决办法很简单:本地老老实实用Tomcat 9.0.x配JDK 8或JDK 11。这个组合兼容性最好,能跑通绝大多数传统jsp项目。如果你已经装了Tomcat 10,参考下面的做法:
# 在pom.xml里显式引入Servlet API,编译期兜底 # 注意scope用provided,Tomcat运行时自己会提供实现,避免重复 <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency>如果你新建的是空Java项目没用Maven,那就不需要这个依赖,直接靠IDEA的Web Facet和Tomcat运行容器提供编译支持。
2.4 传统JSP项目打包war:构建输出与依赖目录的约定
开发时用war exploded跑得很开心,但最后交付课设或部署上线时,需要传统jsp项目打包war。这里有个关键约定:IDEA的Build Artifacts里,如果项目里有本地lib目录存放jar包,一定要在Output Layout里把lib目录加进去,并勾选“Include in project build”。否则打包出来的war里只有class文件和JSP,运行时会报ClassNotFoundException。正确配置后,war包结构是标准的:
exam.war ├── index.jsp ├── WEB-INF │ ├── web.xml │ ├── lib │ │ ├── mysql-connector-java-8.0.33.jar │ │ └── jstl-1.2.jar │ └── classes │ └── com/exam/servlet/LoginServlet.class打包时注意:Project Structure -> Artifacts里如果同时存在exam:war和exam:war exploded两个条目,点Build -> Build Artifacts时选择exam:war的Build动作。打出来的war包放在项目根目录的out/artifacts/exam_war/下,把这个war文件丢进Tomcat的webapps目录,启动Tomcat即可自动解压部署。这一步很多人栽在“代码能跑但打不出能部署的包”,本质就是Artifact里漏了lib目录,导致war里缺少数据库驱动。
3. 在线考试管理系统的数据库设计:建表脚本、初始化数据与MySQL连接池配置
3.1 标题里“70”的含义与系统表结构怎么拆
标题里的“70”如果是指工程编号或作业序号,那对应的源码包大概率包含完整的SQL脚本和初始化数据。如果把“70”理解为表数量,说明设计者把表拆得很细。实际做课设的在线考试系统,表规模通常在10到20张之间就能覆盖全部功能。核心表就六张:学生表、教师表或管理员表、试卷表、试题表、考试记录表、答题明细表。再加上扩展用的课程表、班级表、成绩汇总表,已经足够支撑“管理员出卷、学生答题、系统判分”的完整闭环。
3.2 试卷与试题的两种组织方式:固定试卷和随机抽题
这个设计决策直接影响建表脚本。固定试卷的意思是试卷表里直接存题目ID集合,学生打开试卷看到的题目顺序和内容都一样。随机抽题则需要在试卷配置里定义规则,比如“从章节一抽5道单选题,从章节二抽3道多选题”,学生每次进入试卷生成的题目不同。课设级别一般选固定试卷,逻辑简单,判分也直观。如果你想做得有区分度,可以在试卷表里加一个exam_type字段,0表示固定卷,1表示随机卷,随机卷的题目在进入考试时动态生成并写入考试记录表。这个字段一点,架构就从“死卷”变成了“活卷”,答辩时能多讲三分钟。
3.3 MySQL建表脚本与初始化数据:从ER图到可直接执行的SQL
下面给出最小可运行的核心表结构,覆盖用户、试卷、试题、考试记录四个主体。注意字符集统一用utf8mb4,否则存中文容易变问号。
-- 用户表:学生和管理员共用一张表,用role字段区分 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,课设可用MD5,生产要加盐', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT NOT NULL DEFAULT 0 COMMENT '0=学生 1=管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_role (role) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 试卷表 CREATE TABLE exam_paper ( id INT PRIMARY KEY AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL COMMENT '试卷名称', total_score INT NOT NULL DEFAULT 100 COMMENT '总分', duration_minutes INT NOT NULL DEFAULT 60 COMMENT '考试时长(分钟)', exam_type TINYINT NOT NULL DEFAULT 0 COMMENT '0=固定卷 1=随机卷', create_by INT COMMENT '创建人ID,关联sys_user', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试卷表'; -- 试题表 CREATE TABLE exam_question ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL COMMENT '所属试卷ID', question_type TINYINT NOT NULL COMMENT '1=单选 2=多选 3=判断', content TEXT NOT NULL COMMENT '题干', option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255), answer VARCHAR(10) NOT NULL COMMENT '正确答案,多选用逗号分隔', score INT NOT NULL DEFAULT 5 COMMENT '单题分值', FOREIGN KEY (paper_id) REFERENCES exam_paper(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试题表'; -- 考试记录表:学生每次答卷生成一条记录 CREATE TABLE exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '学生ID', paper_id INT NOT NULL, score DECIMAL(5,1) NOT NULL DEFAULT 0 COMMENT '判分后的成绩', status TINYINT NOT NULL DEFAULT 0 COMMENT '0=考试中 1=已交卷', start_time DATETIME, submit_time DATETIME, UNIQUE KEY uk_user_paper (user_id, paper_id) COMMENT '防止同一学生重复进同一张卷', FOREIGN KEY (user_id) REFERENCES sys_user(id), FOREIGN KEY (paper_id) REFERENCES exam_paper(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';这段SQL的要点在于uk_user_paper这个唯一键。它的作用是防止学生反复进入同一张固定试卷刷成绩,这也是在线考试系统区别于普通增删改查的关键点。初始化数据时先插入管理员账号admin/123456,再插入几道试题目,让项目一启动就能看到效果。
INSERT INTO sys_user (username, password, real_name, role) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', 1); -- 上面的密码是123456的MD5值,课设演示足够 INSERT INTO exam_paper (paper_name, total_score, duration_minutes, exam_type, create_by) VALUES ('Java基础测试卷', 100, 60, 0, 1); INSERT INTO exam_question (paper_id, question_type, content, option_a, option_b, option_c, option_d, answer, score) VALUES (1, 1, 'Servlet的生命周期方法中,以下哪个方法只被调用一次?', 'init()', 'service()', 'doGet()', 'destroy()', 'A', 10), (1, 3, 'JSP页面本质上是一个Servlet。', NULL, NULL, NULL, NULL, '对', 10);这段SQL就是典型的“可抄作业”内容。不管你从网上下载的在线考试管理系统源码里表结构是什么样,这套核心表都能作为你改造或手写建表脚本的底稿。字段注释写清楚,课设文档也能直接复用。
3.4 MySQL数据库连接池的选择:从DriverManager到HikariCP
课设级别的源码里,最常见的是每个DAO方法里写一遍Class.forName("com.mysql.cj.jdbc.Driver"),然后DriverManager.getConnection(url, user, password)。这种写法能跑,但性能和资源管理很差。mysql的数据库连接池主流方案在JavaWeb传统项目里是Druid或HikariCP。Druid在国内课设中使用率高,因为它自带监控页面,答辩时打开监控页展示SQL执行情况,效果很加分。配置如下:
// Druid连接池工具类,项目启动时初始化一次 public class DbUtil { private static DruidDataSource dataSource; static { // 注意MySQL 8的driverClass和8.0以下不一样 // com.mysql.jdbc.Driver 是MySQL 5.x用的,8.x必须用 com.mysql.cj.jdbc.Driver dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/exam_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("你的密码"); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这里三个参数值得说明。serverTimezone=Asia/Shanghai是因为MySQL 8的驱动要求显式指定时区,不写会报Server returns invalid timezone错误。useSSL=false是本地开发环境关掉SSL握手,减少连接耗时。initialSize和maxActive是连接池的初始连接数和最大活跃连接数,一个只有几十人同时在线的课设项目,配5/20完全够用。如果你下载的源码包没有lib目录里的Druid jar,记得在项目的WEB-INF/lib下补上druid-1.2.x.jar和mysql-connector-java-8.0.x.jar两个文件。否则运行时ClassNotFoundException是跑不掉的。
4. 从JSP到Servlet再到DAO:核心业务链路实现与关键参数约定
4.1 Servlet生命周期和请求分发:三个方法一个都不能搞混
Servlet生命周期是面试高频题,也是在线考试系统里控制器设计的理论基础。init()在Servlet首次被访问时执行一次,负责初始化资源。service()每次请求都会执行,根据HTTP方法分发到doGet或doPost。destroy()在应用卸载时执行一次,释放资源。写在线考试系统的登录功能时,我一般把表单提交用doPost处理,把页面跳转用doGet处理。这样做的原因是,doGet会把参数拼在URL上,不做登录校验时容易被浏览器历史记录和日志暴露内容。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 先统一设置请求编码,解决中文乱码 request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 调用DAO层验证账号密码 UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user == null) { // 失败回登录页,并携带一个错误提示参数 request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } else { // 成功把用户信息存入session HttpSession session = request.getSession(); session.setAttribute("loginUser", user); // 管理员跳后台,学生跳考试列表 if (user.getRole() == 1) { response.sendRedirect(request.getContextPath() + "/admin/paperList.jsp"); } else { response.sendRedirect(request.getContextPath() + "/student/examList.jsp"); } } } }这段代码里有个容易被忽略的细节:forward和sendRedirect的区别。登录失败用forward,因为要携带request域里的错误信息回登录页,信息还在同一个请求内。登录成功后用sendRedirect,因为要避免表单重复提交——如果成功也用forward,用户刷新页面就会再次提交登录请求,虽然不影响结果,但会在浏览器警告框里提示“确认重新提交表单”。这个细节在答辩时被问到的概率极高。
4.2 登录拦截与会话管理:Filter过滤器的标准写法
在线考试系统里,学生没登录直接访问考试页面是不允许的。课设源码里常见的做法是每个JSP页面顶部写一段Java代码判断session是否为空,这种写法虽然有用但代码重复。更规范的方案是用Filter统一拦截。注意Servlet的Filter在传统的javax.servlet包下,Tomcat 9之前都这样。
@WebFilter(urlPatterns = {"/student/*", "/admin/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpSession session = request.getSession(false); User user = (session == null) ? null : (User) session.getAttribute("loginUser"); if (user == null) { // 没有登录,丢回登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 管理员访问学生页面要用role做二次校验 if (request.getRequestURI().contains("/admin/") && user.getRole() != 1) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); return; } chain.doFilter(req, res); } }这里有两个参数值得注意。request.getSession(false)里的false表示“如果当前没有session就返回null,而不是新建一个session”。很多新手用getSession()不传参,会导致未登录用户也拿到一个空session,虽然不致命但浪费内存。另一个是sendRedirect里request.getContextPath()的作用,它会把项目部署名带上,比如/exam,避免重定向时丢失应用前缀。如果你不写这个前缀,部署名一旦从Root改成exam,所有重定向路径全部失效。
4.3 在线考试核心流程:加载试卷、提交答题、自动判分
这是系统里最核心的业务链路。学生点击“开始考试”后,Servlet加载试卷和题目,渲染到JSP页面;学生提交答案后,Servlet逐题比对答案计算总分。
@WebServlet("/exam/submit") public class ExamSubmitServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); HttpSession session = request.getSession(false); User user = (User) session.getAttribute("loginUser"); int paperId = Integer.parseInt(request.getParameter("paperId")); // 所有题目的ID和用户答案分别提交,用约定好的命名格式 String[] questionIds = request.getParameterValues("questionId"); String[] answers = request.getParameterValues("answer"); ExamDao examDao = new ExamDao(); // 查询正确答案列表 Map<Integer, String> correctMap = examDao.listCorrectAnswers(paperId); // 逐题判分 double totalScore = 0; int correctCount = 0; for (int i = 0; i < questionIds.length; i++) { int qid = Integer.parseInt(questionIds[i]); String userAnswer = answers[i].trim(); if (userAnswer.equalsIgnoreCase(correctMap.get(qid))) { // 答对,加上该题分数 totalScore += examDao.getQuestionScore(qid); correctCount++; } } // 写入考试记录,状态置为已交卷 examDao.saveExamRecord(user.getId(), paperId, totalScore); // 跳转到成绩页面,把成绩放在request域里 request.setAttribute("score", totalScore); request.setAttribute("correctCount", correctCount); request.getRequestDispatcher("/student/result.jsp").forward(request, response); } }这段代码里判分逻辑用的是全等比对,适合单选和判断。如果有多选题,答案顺序可能不同,比如正确答案是“A,C”,用户提交“C,A”,直接比对就会判错。处理方式是对字符串排序后再比对,或者把多选题答案按固定顺序存储。课设里为了省事,通常要求用户按选项顺序作答,这样判分逻辑就保持简单。另一个关键点是saveExamRecord里要处理唯一键冲突,同一学生重复提交同张试卷时,数据库会报Duplicate entry异常,所以提交前要先查询exam_record表里是否已有记录,有则做更新而不是插入。
4.4 JSP页面与Java代码分离:JSTL和EL表达式的配合
原始JSP页面里写满<% %>脚本片段的代码,能跑但极难维护。更合理的写法是用EL表达式取后端数据,用JSTL标签做循环和判断。在线考试系统的考试列表页就是一个典型场景。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h2>考试列表</h2> <table border="1"> <tr> <th>试卷名称</th> <th>总分</th> <th>时长(分钟)</th> <th>操作</th> </tr> <c:forEach items="${paperList}" var="paper"> <tr> <td>${paper.paperName}</td> <td>${paper.totalScore}</td> <td>${paper.durationMinutes}</td> <td><a href="${pageContext.request.contextPath}/exam/start?paperId=${paper.id}">开始考试</a></td> </tr> </c:forEach> </table> </body> </html>使用JSTL的前提是WEB-INF/lib下有jstl-1.2.jar,并且web.xml声明的是Servlet 2.4以上版本。这里最常见的坑是JSP页面报错“The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved”——原因是用了高版本的jakarta.servlet标签包,或者JSTL版本与Servlet版本不匹配。Tomcat 9 + JDK 8 + JSTL 1.2这个组合最稳定。另外注意${pageContext.request.contextPath}这个写法,它等效于前面Java代码里的request.getContextPath(),用于拼接带应用前缀的URL。如果你图省事写死路径/exam/start,一旦部署名变化,所有超链全部404。
5. 在线考试管理系统常见的翻车现场:避坑与排查
5.1 现象:Tomcat能启动,但访问Servlet报404
这个问题在JSP+Servlet课设里出现频率最高。明明Java类路径、注解都写了,浏览器访问http://localhost:8080/exam/login却404。原因之一是@WebServlet注解没被扫描到——IDEA的Web Artifact没有把编译后的class目录输出到WEB-INF/classes。打开Project Structure -> Artifacts,看Output Layout里有没有WEB-INF/classes这一项,没有就手动加,把Module Output添加进去。原因之二是注解路径写错,servlet映射不区分大小写路径匹配,但必须和浏览器地址保持一致的上下文。检查方式是启动项目后看Tomcat的控制台日志,日志里会打印“Initializing Servlet”之类的字样,没打印就是Servlet没加载。
5.2 现象:登录页输入中文用户名,保存到数据库变成“???”
这是JSP页面、Servlet请求、数据库表三级编码不一致导致的。解决要三处同时设。页面顶部必须有<%@ page contentType="text/html;charset=UTF-8" %>;Servlet里的doGet和doPost开头要写request.setCharacterEncoding("UTF-8");数据库连接URL里带characterEncoding=utf8参数。注意顺序:Tomcat 8及以上版本对POST请求的编码默认不是UTF-8,只靠页面声明不够。另外GET请求的参数编码不归request.setCharacterEncoding管,需要改Tomcat的server.xml里Connector加URIEncoding="UTF-8"。课设检查时优先看数据库表字符集,如果建表时用了DEFAULT CHARSET=latin1,JSP和Servlet全改对也没用,必须ALTER TABLE改成utf8mb4。
5.3 现象:数据库连接失败,控制台报ClassNotFoundException或Communications link failure
报ClassNotFoundException十有八九是MySQL驱动jar没放进WEB-INF/lib目录。注意不是放到项目模块的classpath里就算数,war打包时以WEB-INF/lib为准。报Communications link failure则分两种情况:MySQL服务没启动,或连接URL的端口不对。MySQL默认3306,如果你本地跑了多个实例改过端口,记得同步改URL。另外MySQL 8的驱动类名必须用com.mysql.cj.jdbc.Driver,用旧的com.mysql.jdbc.Driver会提示Loading class 'com.mysql.jdbc.Driver' is deprecated,虽然MySQL 5.7驱动还能兼容,但8.x版本里这个旧类已经移除。
5.4 现象:JSP页面能打开,但渲染时报500错误,提示EL表达式无法解析
这个坑多出现在JSTL标签库版本搭配上。常见报错是org.apache.jasper.el.JasperELException: The identifier [paper] is not a valid Java identifier。原因有两个可能:一是Servlet里setAttribute时把key写成了带特殊字符的字符串,比如request.setAttribute("paper list", list),EL表达式${paper list}无法解析,改成${paperList}就好;二是JSTL版本和Servlet版本冲突,Tomcat 10用jakarta.servlet作为包名后,老JSTL的uri不对,换成jakarta.tags.core标签前缀也不能用.jar里的旧标签库。解决方式就一个,Tomcat 9配jstl-1.2.jar,什么都别换,这是传统JSP项目里最稳的组合。
5.5 现象:用户刷新交卷页面,成绩被重复计算
这是典型的表单重复提交问题。在线考试系统里如果判分和保存是同一个请求完成的,刷新或浏览器回退就会触发二次判分,成绩记录出现两条或覆盖正确成绩。两层防线:第一层是前端,交卷按钮点击后置灰并禁用,配合JavaScript阻止重复提交;第二层是后端,在ExamSubmitServlet里先查询exam_record表是否已有该生该卷的记录,有则直接跳转成绩页不再执行判分。后端校验才是关键,前端禁用只是体验优化。用唯一键uk_user_paper(paper_id, user_id)配合数据库层兜底,这是最后一道防线,能扛住并发重复提交。
6. 部署、验证与平滑过渡:把JSP+Servlet项目用到位
6.1 war包部署到Tomcat的两种方式及场景选择
开发完成后,传统jsp项目打包war,部署方式就两种。第一种是把war包扔进Tomcat的webapps目录,启动Tomcat自动解压部署。这种方式适合交付课设、在局域网演示。注意Tomcat默认会解压war到同名目录,你改代码后重新打war包,要先在Tomcat里停掉应用并删除旧的解压目录,否则会出现“代码改了但页面没变化”的幻觉。第二种是在Tomcat的conf/server.xml里配置<Context docBase="/opt/exam/exam.war" path="/exam" />,这种适合war包放在Tomcat外部,解耦部署路径。第二种方式对于“源码+数据库”类项目资源尤其实用——数据库文件、配置外置、SQL脚本都放到独立目录,下一次重建环境时不用动Tomcat。
6.2 生产级部署:数据库配置外置与JNDI数据源切换
课设级项目把数据库账号密码写死在DbUtil.java里,可以理解。真正部署时,这等于把钥匙挂在门口。常见做法是把数据库配置挪到web.xml或外部properties文件。一个轻量做法是让DbUtil读取classpath下的db.properties文件,部署时只替换这个文件,不动代码。
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/exam_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=这里改成你的密码 jdbc.maxActive=20对应的DbUtil读取逻辑改成加载properties文件,替换硬编码。这样数据库密码、连接参数都集中在配置层,后续迁移服务器时不用改一行Java代码。Druid连接池本身支持用Properties对象初始化,直接配置文件驱动即可。
6.3 Nginx能不能配合JSP项目:静态资源和动态请求的分工
很多同学问nginx支持jsp吗,准确地说,Nginx不解析JSP,它只负责静态资源响应和反向代理。JSP的解析执行必须在Tomcat里完成。部署时常见的分工方式是:Nginx监听80端口,把.html/.css/.js/.png这类静态资源直接返回,把所有/exam/开头的动态请求转发给后面的Tomcat端口。这个架构的优势是静态响应快,且Nginx的代理能力可以为后续加HTTPS打基础。Nginx配置不需要写Java,但处理动态路径时要注意配置proxy_pass的路径拼接方式——如果proxy_pass http://127.0.0.1:8080;不带URI,请求/exam/login会原样转发到Tomcat;如果写成proxy_pass http://127.0.0.1:8080/;,路径可能被重写导致404。
6.4 验证这个方案值不值得做:部署检查清单与一次性能验证
部署完成后别急着收工,按清单过一遍:第一,用未登录浏览器直接访问/student/examList.jsp,确认被Filter踢回登录页;第二,用一个测试账号完成一次完整考试,确认成绩写入后再次点击开始考试,确认被唯一键拦下;第三,查看Druid监控页面或Tomcat日志,确认SQL执行没有全表扫描——如果题目表几百条数据量还出现慢SQL,检查查询条件是否用了索引。课程设计答辩时,评委最常问的两个问题就是“Session失效怎么处理”和“并发交卷会怎样”,这两个场景按上面的代码逻辑回答,基本能站住。
最后一件事值得做:用JMeter模拟20个用户同时交卷,看Tomcat默认线程池是否扛得住。如果响应时间超过3秒,先看数据库连接池是否配了Druid,再看代码里有没有在循环里反复getConnection()——把获取连接提到循环外能省掉大部分开销。我自己踩过的坑是DAO层每个方法都打开和关闭连接,20个并发就把连接池打满了,改成方法级复用和事务边界的正确划分后,同一份代码扛住了100并发。这种受挫—排查—优化—验证的循环,恰恰是JSP+Servlet这套老技术最值得你动手走一遍的理由,希望帮到你。
本文还有配套的精品资源,点击获取