JavaWeb在线考试系统手写教程:Servlet+JSP+MySQL全链路详解

发布时间:2026/9/24 22:45:28
JavaWeb在线考试系统手写教程:Servlet+JSP+MySQL全链路详解 简介这是一套基于Java EE技术栈的在线考试系统完整源码包整合JSP、MySQL与Tomcat面向正在做毕业设计的计算机专业学生以及需要项目实战的Java学习者。系统包含普通用户和管理员两类角色普通用户可在线考试、查询成绩、修改个人密码管理员则可对考生信息、考试成绩、试卷和题目进行统一管理功能覆盖从考试发起、作答到成绩统计的完整业务闭环。压缩包内共148个文件大小约13.69MB包含27个jar依赖库、23个java源文件与23个class编译文件、21个jsp页面以及js与css前端样式、xml配置文件和sql数据库脚本等类型。源码按照标准Java Web工程结构组织可导入Eclipse开发环境直接运行前后端模块划分清晰。目前已有1384人学习下载随包附带的数据库脚本与项目配置能帮助学习者快速搭建运行环境理解在线考试系统的数据表设计、权限管理和试题管理流程也可作为进一步二次开发的实用脚手架。1. 为什么javaWeb在线考试系统值得手写一遍搜索这个词的人大概率不是想买一套商业系统而是实训课或者毕业设计里要做这么个完整案例。一个典型的javaWeb在线考试系统从登录注册到题库管理、在线组卷、限时答题、自动评分链路很长刚好把Servlet、JSP、JDBC、MySQL这些JavaWeb核心知识点全部串起来。别以为这套东西过时了它反而是把“前端发请求、后端处理业务、数据库读写”讲得最直白的一块试验田。下面按一套课程设计里最标准的实现方案展开从技术选型、环境搭建、核心功能、库表设计到最后的验证方式一步一步给到能落到代码上的做法。尤其适合第一次独立完成JavaWeb项目、准备答辩或者面试讲项目的同学。你不需要在自己电脑上装一套复杂的框架体系只要把Servlet、JSP、MySQL这几样吃透这个系统就能跑起来而且在跑的过程中你能清楚说出每一个请求从点击到返回都经过了哪些代码。2. 技术选型ServletJSPMySQL在学生考试系统场景里的边界和取舍2.1 这套方案适合谁如果你手里的在线考试系统是实训作业、毕业设计或者只是想自己在简历里加一个拿得出手的完整项目ServletJSPMySQL就是最稳的答案。它跟你前两年学的东西完全对得上答辩时老师能看清楚你在请求处理、会话管理、数据库读写这些基础能力上的真实水平。反过来如果目标是给真实学校做一套全校考试的运营系统几百人同时在线、老师要实时监控作弊行为那这个组合就撑不住了该上Spring Boot加微服务那是另一个话题。我之所以这么选是因为在线考试系统的核心链路其实不复杂登录、抽题、答题、交卷、自动评分。这个业务规模下Servlet处理请求JSP渲染页面JDBC直接读写MySQL每一层都很轻逻辑清晰出了问题也好定位。不需要为了一个课设去背Spring Boot的一大堆自动配置那些配置反而把“请求怎么进来的”“数据怎么存的”盖在一个黑匣子里出了问题都不知道去哪一行打断点。2.2 组件清单每个部件在系统里到底干什么组件职责本系统里的具体落点Servlet接收HTTP请求、控制跳转、写业务逻辑LoginServlet处理登录ExamServlet处理交卷和评分JSP页面模板负责展示和表单提交login.jsp、exam.jsp、score.jsp三张主页面JDBCJava操作数据库的标准接口封装一个DbUtil类所有表的增删改查都走它MySQL数据存储user、question、paper、record这几张核心表TomcatServlet容器承接请求并调度Servlet在IDEA里以本地方式配置运行这套组合在教学上的价值在于每一层边界都很清楚。浏览器把表单POST给ServletServlet调JDBC查库查完把结果塞进request或session再forward到JSP渲染。数据在哪一步出了问题断点一打就能看到不会像Spring Boot那样层层代理新手经常找不到业务代码在哪。2.3 为什么不是Spring BootSpring Boot当然能做在线考试系统而且做得更快。一个RestController加一个MyBatis-Plus半天接口就齐了。但如果你做的是javaWeb教学案例直接上Spring Boot就等于把Servlet、JSP、会话管理这些最基础的手艺全跳过了。面试官问“登录状态保存在哪”你答“用的Session”可如果你没写过裸Servlet很可能连Session从哪来、什么时候失效都说不清楚。反过来如果读者基础已经够好只是急着交一个项目那我一般会建议直接上Spring Boot没有毛病。这篇文章锁定的还是那个能把JavaWeb基础链路完整跑通的方案它的适用边界就是单机、小规模、以教学和演示为主要目的。下面从IDEA 2026开始讲环境因为环境配不通后面所有代码都只是躺在编辑器里的摆设。3. 用IDEA 2026创建JavaWeb项目版本配合、目录结构与最小联通测试3.1 JDK、Tomcat、Maven的版本组合写JavaWeb项目版本配合是第一道坎很多新手在这一步就翻车。我常用的组合有两套组合A老机器稳妥型JDK 8 Tomcat 9 Maven 3.6Servlet包名是javax.servlet。组合B新机器推荐型JDK 17 Tomcat 10.1 Maven 3.8Servlet包名是jakarta.servlet。如果你的电脑是新买的JDK默认装了17甚至21那就别硬装JDK 8了。JDK 17和Tomcat 10.1配合很成熟IDEA 2026新建项目默认也是jakarta命名空间。下面所有代码都按组合B写第6章会专门讲Tomcat 9老项目迁移到Tomcat 10时那个让人头疼的包名变更。在IDEA 2026里新建项目的入口在File - New - Project。因为版本迭代不同小版本的向导界面略有差异但逻辑一致选择左侧的Jakarta EE分类Server选Tomcat 10.1Language选JavaBuild tool选MavenProject SDK选17。如果没有直接看到Jakarta EE分类就在New Project窗口顶部的搜索框输入“web”或者“servlet”IDEA会过滤出Web Application模板效果一样。这套配置的关键点在于Maven负责拉依赖Tomcat负责跑ServletIDEA只是把两者粘起来。这三个角色的版本必须互相兼容JDK 21配Tomcat 9经常会在启动时抛ClassNotFoundException原因就是Tomcat 9官方支持的JDK版本上限不够。3.2 最小联通测试一个Servlet加一个JSP环境配完别急着写业务先做最小联通测试。新建一个Servlet类包名用com.example.exam代码如下package com.example.exam; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(h2JavaWeb环境正常/h2); } }这段代码里WebServlet(/hello)是注解方式的请求映射替代了老项目里web.xml中一大段servlet和servlet-mapping配置。浏览器访问http://localhost:8080/项目名/hello时Tomcat会找到这个Servlet并调用doGet方法。resp.setContentType(text/html;charsetUTF-8)是在响应头里告诉浏览器返回的是HTML并且用UTF-8解析少这一行中文就会出现乱码。在webapp目录下放一个index.jsp内容很简单% page contentTypetext/html;charsetUTF-8 languagejava % html body h2考试系统入口/h2 a href${pageContext.request.contextPath}/hello测试Servlet/a /body /htmlJSP里的${pageContext.request.contextPath}会解析成当前项目的部署路径比如/exam_war_exploded。这里直接教新手用绝对路径写法是为后面CSS、JS加载问题提前打预防针。启动Tomcat后能看到页面并点击跳转显示“JavaWeb环境正常”说明整条链路通了可以进入正式功能开发。3.3 项目目录结构与依赖管理IDEA 2026创建的JavaWeb Maven项目目录结构和Eclipse时代完全不同新手最容易在这迷路。src/main/java放Java源码src/main/resources放配置文件和propertiessrc/main/webapp代替了原来的WebContent目录里面放JSP、CSS、JS和WEB-INF。WEB-INF目录下的文件不能被浏览器直接访问所以index.jsp要放在webapp根目录下而WEB-INF里只放web.xml以及后面要用的class文件。Maven管理的项目依赖统一写在pom.xml里不需要手动往WEB-INF/lib里拷贝jar包。Servlet API的依赖声明如下dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version6.0.0/version scopeprovided/scope /dependencyscope设为provided很关键意思是这个jar只在编译期使用打包和运行时由Tomcat提供。如果你写成默认的compile最终war包里会带一份servlet-api部署时可能出现类冲突表现是启动报错或者方法找不到。MySQL驱动和Druid连接池后面也要加进pom.xml但动态加载也是可以的第5章会给出连接池的标准配置。4. 核心功能实现从登录到自动评分在线考试功能模块怎么串起来4.1 登录与密码存储别再往数据库里存明文了在线考试系统的第一个入口就是登录。学生要登录才能考试老师要登录才能出题。user表里的密码字段我见过太多课设项目直接存明文答辩时被老师一句“你数据库被脱了怎么办”问得哑口无言。哪怕只是课程设计密码也至少要做一个MD5加盐代码如下import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class PasswordUtil { public static String md5WithSalt(String raw, String salt) { String src raw {exam} salt; try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(src.getBytes()); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }加盐的意思是原始密码拼接一段随机字符串后再做MD5salt字段单独存在user表里。这样做可以防止彩虹表直接反查同一密码在不同用户身上产生的密文也不同。代码里{exam}是固定混淆串salt是用户表里存的随机值两者结合让明文到密文的关系变得很难猜测。登录Servlet的写法也有一套固定套路WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userDao.findByUsername(username); if (user ! null user.getPassword() .equals(PasswordUtil.md5WithSalt(password, user.getSalt()))) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(exam.jsp); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }这里有两个跳转动作值得说明。sendRedirect是重定向浏览器地址栏会变成exam.jsp第二次请求跟第一次登录请求已经无关forward是服务器内部跳转地址栏还是login.jsp所以能在页面上显示req.setAttribute设置的msg错误提示。很多新手把这两个搞混结果登录成功后刷新页面又提交一次表单或者登录失败时看不到任何提示。session在这里存了loginUser后续每次请求通过session.getAttribute(loginUser)判断是否登录这是整套系统会话管理的核心。4.2 题目表与组卷逻辑ORDER BY RAND()到底能不能用出题这个功能逻辑其实很简单从题库里随机抽若干道题组成一张试卷。question表是题库的载体字段包括id、paper_id、qtype、content、option_a到option_d、answer、score。qtype用来区分单选、多选和判断题answer字段统一存标准答案。组卷的SQL最常见的写法是SELECT * FROM question WHERE paper_id ? ORDER BY RAND() LIMIT 10;ORDER BY RAND()的写法在数据量小的时候非常好用SQL本身读起来也直观把符合条件的数据随机排序后取前10条。但要知道它的代价MySQL会对全表所有符合条件的记录做一个随机排序题目量到几十万行时性能会明显下降。课程设计规模哪怕题库里有两万道题这个查询也能在几十毫秒内完成不需要过度设计。如果将来真要做海量题库再考虑用“按ID随机偏移”或者“每科目维护一个随机种子”的方案现在把功能跑通比优化更重要。组卷完成后试卷的题量和总分要写进paper表这样考试页面打开时才知道要渲染多少道题交卷后也知道满分是多少。4.3 答题过程Session里存答案最后一次性写库答题这个环节最稳的做法是把用户的答案先存在Session里等交卷时一次性写库。每答一道题就写一次数据库会在中途网络波动时留下大量脏数据而且代码要处理并发更新课设项目完全没必要。核心逻辑如下MapString, String answers (MapString, String) session.getAttribute(answers); if (answers null) { answers new HashMap(); session.setAttribute(answers, answers); } answers.put(questionId, req.getParameter(answer));这个代码段的含义是每道题答完提交一次请求Servlet把题号和答案的映射存进Session里的一个Map。questionId从请求参数里拿answer是用户在页面上选的选项值。整个考试过程中Session一直活着交卷时ExamServlet从这个Map里读出所有答案统一调用评分逻辑。倒计时功能用的是前端JavaScript加后端校验的双保险var seconds 600; var timer setInterval(function () { seconds--; document.getElementById(timer).textContent 剩余 seconds 秒; if (seconds 0) { clearInterval(timer); document.getElementById(examForm).submit(); } }, 1000);前端倒计时只管用户体验到了时间自动提交表单。但必须说明的是前端时间可以被篡改所以后端在考试开始时就把开始时间存进Session交卷时用当前时间减去开始时间超过试卷设置的时长就拒绝接收答案这是防止超时作弊的后端底线。4.4 自动评分多选题判分和判断题存储格式是最容易翻车的地方自动评分看着简单写起来全是细节。判断题如果存“正确”“错误”两个汉字字符串比较时只要有一丁点编码不一致就会判错多选题如果答案存成“ABD”评分时用contains判断又会把“ABC”这种错选给放过去。我给出一套可以复用的判分方法public static int judgeAnswer(String userAnswer, String rightAnswer, int score) { if (userAnswer null || rightAnswer null) { return 0; } userAnswer userAnswer.trim().toUpperCase(); rightAnswer rightAnswer.trim().toUpperCase(); if (rightAnswer.contains(,)) { SetString rightSet new HashSet(Arrays.asList(rightAnswer.split(,))); SetString userSet new HashSet(Arrays.asList(userAnswer.split(,))); return rightSet.equals(userSet) ? score : 0; } return userAnswer.equals(rightAnswer) ? score : 0; }这段代码先做两个预处理trim去掉首尾空格toUpperCase把字母统一成大写。这样做是为了处理用户提交答案时意外带入的换行或小写字母血泪经验表明textarea提交过来的数据经常自带一个换行符不trim直接比较必错。多选题的答案在数据库里用逗号分隔存比如A,B,D判分时把两边的字符串切成集合再比对保证只要选项组合不完全一致就不得分不会出现D选项漏选还被判对的漏洞。判断题的answer字段我建议统一存T或F因为判断题就两种状态用单个字母存储和比较都比存汉字稳妥数据库里也不会出现“真”“false”之类的怪数据。自动评分的调用时机在交卷Servlet里遍历Session里的答案Map逐题调judgeAnswer累加分数最后把总分和答题时间写入record表。5. 数据库与MySQL落库五张表结构和Druid连接池配置5.1 五张核心表的结构与关联在线考试系统跑起来的根基是数据库表设计不合理的项目后期改起来极其痛苦。我给出课程设计里最经典的五张表user用户表、subject课程表、paper试卷表、question题目表、record考试记录表。CREATE DATABASE online_exam DEFAULT CHARACTER SET utf8mb4; USE online_exam; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5加盐后的密文, salt VARCHAR(32) NOT NULL COMMENT 盐值, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 1 COMMENT 1学生2教师3管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE subject ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 课程名JavaWeb、数据结构 ); CREATE TABLE paper ( id INT PRIMARY KEY AUTO_INCREMENT, subject_id INT NOT NULL, name VARCHAR(100) NOT NULL, total_score INT NOT NULL DEFAULT 100, duration_minutes INT NOT NULL DEFAULT 60, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL COMMENT 属于哪张试卷, qtype TINYINT NOT NULL COMMENT 1单选 2多选 3判断, content VARCHAR(1000) NOT NULL, option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255), answer VARCHAR(50) NOT NULL COMMENT 单选A多选A,B,D判断T或F, score INT NOT NULL DEFAULT 5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, paper_id INT NOT NULL, score INT NOT NULL DEFAULT 0, start_time DATETIME, submit_time DATETIME );表与表之间的关系很清晰subject和paper是一对多paper和question是一对多user和record是一对多。物理外键我一般不加逻辑关联就够了。加了物理外键后删除题目或试卷时会受约束限制课设里经常要改数据重建表没有外键反而省事。question表直接用paper_id字段挂在某张试卷下面意味着同一道题不能同时出现在多份试卷里这对课程设计够用了。如果将来要支持题库复用再加一张试卷和题目的多对多关联表就行现在是完整案例优先考虑易读易改。5.2 初始化数据管理账号和一套可演示的试卷建完表第一件事是插入初始化数据保证项目跑起来就有内容可看。账号的密码一定是从PasswordUtil跑出来的密文不要手写一个看起来像MD5的字符串否则登录永远失败。INSERT INTO user (username, password, salt, real_name, role) VALUES (admin, e3743e2b7c6d7f0a9c5b4d6e8f0a1b2c, s1, 管理员, 3), (student, a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6, s2, 张三, 1); INSERT INTO subject (name) VALUES (JavaWeb); INSERT INTO paper (subject_id, name, total_score, duration_minutes) VALUES (1, JavaWeb期中测试, 100, 60); INSERT INTO question (paper_id, qtype, content, option_a, option_b, option_c, option_d, answer, score) VALUES (1, 1, Servlet的包名在Tomcat 10中变更为, javax.servlet, jakarta.servlet, java.servlet, org.servlet, B, 5), (1, 1, JSP最终会被容器编译为, Java类, HTML文件, JavaScript文件, XML文件, A, 5), (1, 2, 下列哪些属于MySQL索引类型, B树, 哈希, 链表, 跳表, A,B, 10), (1, 3, 判断题Session数据默认存储在服务器端。, T, NULL, NULL, NULL, T, 5);注意多选的answer存的是A,B这种带逗号的格式正好对应4.4节判分方法里rightAnswer.contains(,)的分支。判断题的四个选项字段传NULL因为页面渲染判断题时根本不需要选项。初始数据里故意混入一道关于Tomcat包名的题跟第6章的避坑内容呼应做演示时还能当考点。5.3 Druid连接池配置从DriverManager到连接池的必经一步一个教学级系统最容易被忽略的生产力部件是数据库连接池。如果每次都DriverManager.getConnection每次请求都要经历“建立TCP连接、MySQL鉴权、分配连接、执行SQL、断开连接”的全过程Tomcat一启动测试页面勉强能跑但考试页面几十个并发请求一起进来数据库连不上或者响应缓慢是必然的。连接池的意义在于复用连接Druid是JavaWeb教程里最常见的方案。在src/main/resources下放一个druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/online_exam?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password你的数据库密码 initialSize5 maxActive20 maxWait3000URL参数必须拆开讲清楚characterEncodingutf8解决中文写入数据库乱码serverTimezoneAsia/Shanghai解决MySQL 8时区报错useSSLfalse避免本地开发时SSL握手警告。这三个参数少一个项目跑起来就会遇到奇怪的报错或乱码第6章还会再提。对应的工具类import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DbUtil { private static DataSource dataSource; static { try (InputStream in DbUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConn() throws SQLException { return dataSource.getConnection(); } }静态代码块在类加载时执行一次创建DataSource后整个应用共享。getConn从连接池里取连接用完调用close归还。initialSize5是启动时预创建的连接数maxActive20是连接池最大连接数maxWait3000是拿不到连接时的等待毫秒数这三个参数是Druid调优的基础。并发量上来后优先调maxActive但课设场景20已经完全够用。6. 在线考试系统的避坑手册Tomcat 10改名、编码和路径问题一次讲清6.1 现象导入老项目后大量报错找不到javax.servlet从网上下载的javaWeb教学案例十有八九是Tomcat 8或Tomcat 9时代的项目代码里全是import javax.servlet。用IDEA 2026默认的Tomcat 10.1一跑编译直接报“程序包javax.servlet不存在”。原因是Tomcat 10把Java EE的命名空间从javax迁移到了jakarta这是Oracle把Java EE捐给Eclipse基金会后的一次大改名Tomcat 9及其之前的版本用javaxTomcat 10之后必须用jakarta。解决方法是全局替换把代码里所有import javax.servlet改成import jakarta.servletweb.xml头部的命名空间也要同步换成jakarta.servlet版本。IDEA里可以用CtrlShiftR全局替换替换完重新构建。如果不想改代码就把Tomcat版本降回9.0.xIDEA的Run Configuration里Server设置换成Tomcat 9即可两条路都行。我自己处理老项目时优先选升级代码因为Tomcat 9对JDK 17以下才友好新机器跑老版本Tomcat还会冒出一堆其他不兼容。6.2 现象IDEA 2026新建项目时找不到JavaWeb模板新版IDEA把创建项目的向导改了很多用户点New Project后没有直接叫“JavaWeb”的分类只有Spring Initializr、Jakarta EE之类的选项新手一下就懵了。这不是IDEA砍掉了功能而是把Web项目整合进了Jakarta EE模板里。选中Jakarta EE项目类型后在Additional Libraries里勾选WebIDEA就会生成webapp目录和web.xml。如果这一步也没找到还有一个可靠路径先创建一个普通Maven项目然后在项目结构里手动添加Web组件。右键项目选择Add Framework Support勾选Web ApplicationIDEA会自动补出webapp目录。这个方法虽然多几步但在任何IDEA版本里都能走通。有一个坑Add Framework Support生成的web.xml版本可能比较老需要手动把web-app头里的版本改成5.0或6.0否则Tomcat 10启动时会提示web.xml版本不兼容。6.3 现象JSP页面和数据库全是中文乱码中文乱码是JavaWeb里最经典的玄学问题而且经常是三个地方同时出问题。页面显示问号先在浏览器开发者工具里看响应头里的charset再检查JSP的contentType是否写了UTF-8。数据写入数据库后查询乱码检查JDBC URL有没有带characterEncodingutf8。数据库表本身编码是latin1的话前两处改好也没用建表时指定DEFAULT CHARACTER SET utf8mb4才能根治。一次完整的乱码排查顺序是先改JSP的contentType和pageEncoding再改JDBC URL加characterEncodingutf8最后确认数据库连接和表字段的排序规则是utf8mb4_general_ci。MySQL 8默认字符集已经是utf8mb4但如果是从旧库导入的数据表可能还是latin1这时需要用ALTER TABLE语句转换。这套流程走完乱码基本没跑。记住一个原则Servlet里不要随便new String(request.getParameter(“xx”).getBytes(“ISO-8859-1”),“UTF-8”)这种按老教程写的编码转换代码在新版Tomcat里反而会把本来正常的字符再转乱一次。6.4 现象页面HTML出来了但CSS和JS全部加载失败JSP里写了 hrefcss/style.css relstylesheet/ 浏览器地址是localhost:8080/exam/login.jsp时样式正常登录后跳转到exam.jsp就发现样式全丢了。原因是相对路径是相对于当前URL目录解析的页面路径变了CSS的相对路径就跟着变指向了一个不存在的地址。解决办法是写绝对路径用JSP内置的EL表达式取上下文路径link relstylesheet href${pageContext.request.contextPath}/css/style.css凡是JSP页面里的外链资源、表单action、超链接href都建议加上${pageContext.request.contextPath}前缀它能根据部署环境自动生成正确的根路径。这个习惯在项目上线时也会省很多事不用因为应用名变了就去改一堆链接。7. 验证与扩展从冒烟测试到倒计时自动交卷把系统验收提上日程7.1 一份五分钟跑完的冒烟测试清单项目开发完别急着答辩先按下面这张表把主流程走一遍。冒烟测试的目的是快速暴露“链路断没断”不需要覆盖所有边界但每个核心功能都必须点到序号测试场景操作预期结果1登录成功用student账号登录跳转到考试页面Session有值2登录失败输入错误密码停留在登录页并显示错误提示3展示试卷打开考试页面题目数量和题型与题库一致4作答并交卷做完全部题交卷自动评分后显示成绩写入record表5空答案交卷不做任何题直接交卷得0分不报空指针异常6超时交卷修改前端倒计时后交卷后端拒绝或标记超时按已有答案评分第5条很关键很多课设项目在用户一道题不答直接交卷时Servlet里遍历null的answerMap直接空指针页面一片白。评分方法judgeAnswer第一行就判断了userAnswer和rightAnswer是否为null返回0分这个设计就是为这种边界情况兜底的。7.2 值得扩展的两个方向倒计时后端校验与成绩分布如果做完系统还想更进一步推荐先做倒计时后端校验。前端倒计时只能拦普通用户懂一点前端知识的同学改一下JS变量就能无限续时间。正确做法是考试开始请求时把start_time写进Session交卷时计算时间差超过duration_minutes就不接受答案把超时状态和已答题目一次性记录到系统里。这个功能不用改任何表结构只加一个时间比较的逻辑却能让答辩时“防作弊设计”这一问很有底气。第二个值得做的是成绩分布页面。拿出record表里所有成绩用JDBC做一次简单统计算平均分、及格率、最高分和最低分在JSP里用表格展示。这个页面能体现对SQL聚合函数的掌握而且数据都是真实考试产生的演示效果好。想要更直观可以用ECharts画柱状图但课设阶段一个清晰的数据表格已经足够。最后说个教训我当年做课设时也图省事把密码明文存库结果答辩老师直接在浏览器地址栏输入后台地址看到了所有学生的密码表那种尴尬一辈子都记得。从那以后我做的每个Demo都至少保证密码加盐和登录校验这个习惯帮你省掉的不只是分数还有将来工作后被安全评审问住的风险。这一路踩过来希望帮到你。本文还有配套的精品资源点击获取