Java Web词汇量测试项目拆解:从Maven到Servlet的完整实践
2026/9/11 10:29:13 网站建设 项目流程

简介:这是一套基于Java、JavaScript和HTML构建的英语词汇量在线测试系统源码,面向英语学习者、Java Web初学者及测试系统开发人员,可用于搭建词汇量评估平台,也能用于学习前后端联调、会话管理和基础工程构建。压缩包共60个文件,其中包含12个Java源文件、8个JavaScript文件、6个HTML文件,以及PNG/JPG图片、XML配置、JSON配置和Maven相关文件等,整体约36.56MB。Java源文件用于处理用户验证、成绩记录及数据交互等后端逻辑,JavaScript实现答题校验、实时计分和页面动态效果,HTML负责组织题目与表单结构,图片素材则为测试界面和题目配图提供视觉支持。项目目录清晰,附有readme.txt和pom.xml,便于理解项目结构和快速启动。通过阅读这套源码,能够掌握Java、JavaScript、HTML三种语言在Web项目中的协作方式,了解在线测试系统的设计与开发流程,也方便在此基础上扩展题库、增加用户管理或统计分析功能。目前已有358人学习下载。

1. 词汇量测试这个 Java Web 项目,拆开能学到什么

词汇量测试这个需求,乍看就是出题、作答、给分,好像半天就能写完。但当题库要随机、成绩要落库、用户要登录时,项目很快变成一场前后端状态管理的拉锯战。这个基于 Java 服务器端、JavaScript 交互层和 HTML 结构层的英语词汇量测试源码,恰好把这条链路完整跑了一遍:Maven 管依赖和构建,12 个 Java 类处理后端逻辑,8 个 JavaScript 文件负责答题交互,6 个 HTML 页面承载测试界面,12 张 PNG 配图补充题目上下文。适合刚学完 Java Web 想练手完整流程的人,也适合在开发背单词工具但还在纠结答题流程怎么设计的工程师。下面从工程结构、出题流程、成绩落库和本地调试四个方向拆,把接口设计和参数边界说清楚。

2. 工程结构拆解:从 pom.xml 到 resources 下的静态文件

拿到源码包后,很多人习惯先点开src/main/java,但真正决定这个项目能不能跑起来的,是根目录里的pom.xml。一个 Java Web 项目在 IDEA 里看起来再光鲜,最后都要被 Maven 的坐标、依赖和打包方式拉回现实。

2.1 Maven 工程怎么认出这是个 Web 项目

pom.xml 里的第一个重点是<packaging>标签。如果是 jar,说明这是一个可独立运行的纯 Java 应用;如果是 war,说明它需要部署到 Tomcat、Jetty 这类 Servlet 容器里。从项目包含 HTML 和 JavaScript 静态资源这一点看,war 是更常见的形态。第二重点是依赖范围:Servlet API 通常在编译期需要,运行期由容器提供,所以 scope 是 provided;数据库驱动则往往声明为 runtime,避免在代码里出现硬编码。

下面这段是常见的 Maven Web 项目骨架,具体版本号以仓库里的实际 pom.xml 为准:

<project> <modelVersion>4.0.0</modelVersion> <groupId>com.vocabulary</groupId> <artifactId>vocabulary-test</artifactId> <version>1.0.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> <scope>runtime</scope> </dependency> </dependencies> </project>

这里给出的版本号只是示例。在实际项目里要以 pom 中锁定的版本为准,尤其是 MySQL 驱动的大版本要跟本地数据库版本兼容,比如 MySQL 5.7 用 8.0 驱动虽然能连,但时区参数不配置会直接报错。packaging 选择 war 还有一个好处:前端 HTML 和 JavaScript 可以直接放在 webapp 或 resources/static 目录下,由容器统一对外提供,不用像前后端分离项目那样单独搭 Node 服务。

2.2 目录结构与 59 个文件的分布

从文件数量看,这属于一个轻量级项目:12 个 Java、8 个 JavaScript、6 个 HTML、12 个 PNG,共 59 个文件。这样的体量最适合用来理解一个 Web 应用的最小完整形态。我一般会把这类项目整理成下面这个结构,源码包里的实际布局可能略有差异,但职责分层基本一致:

vocabulary-test ├── pom.xml └── src └── main ├── java │ ├── controller # 处理请求的 Servlet │ ├── service # 测试逻辑与成绩计算 │ └── dao # 数据库访问 ├── resources │ ├── static │ │ ├── html # 登录页、测试页、结果页 │ │ ├── js # 题目加载与交互脚本 │ │ └── images # PNG 配图 │ └── db.properties # 数据库连接配置 └── webapp └── WEB-INF └── web.xml

静态资源放在 resources/static 下是 Spring Boot 的习惯,而传统 Servlet 项目更常见的是放在 webapp 下。这个源码包既有 resources 又有 Java,说明它可能采用了其中一种混合方式。区分方法很简单:看 web.xml 里有没有配置 Servlet 映射,或者看 Java 代码里读取文件用的是ResourceUtils还是ServletContext.getResourceAsStream

下面的表格是这 59 个文件按职责的归类,能帮你快速定位入口:

文件类型数量实际职责
Java 源文件12登录验证、出题接口、成绩存储、会话管理
JavaScript8请求题库、渲染题目、答案校验、进度展示
HTML6用户登录、测试说明、答题页、结果页等
PNG 图片12题目配图与界面图标

2.3 .idea 与 .gitignore 里能挖出的信息

我看到文件清单里有.idea目录下的 uiDesigner.xml、sqldialects.xml、vcs.xml、misc.xml 和 dataSources.xml。这些文件不是源码,但信息密度很高。dataSources.xml记录了你在 IDEA 里配置过的数据库连接串和驱动,能直接看出项目用的是 MySQL 还是 SQLite;sqldialects.xml告诉你 SQL 方言,这是后来排查 SQL 语法报错的重要线索。.gitignore则在告诉你哪些文件不该进版本控制。常见问题是把 dataSources.xml 一起提交,导致别人拉下代码后多出本地不需要的数据源配置。正确的做法是只保留项目必须的源码和 pom.xml,把.idea整体忽略。

3. 出题与答题流程:后端题库接口与前端状态管理

题目从数据库到用户点击,要经过“查询题库 → JSON 序列化 → 前端渲染 → 选答案 → 判分”五个步骤。每一步都有容易出错的地方,比如答案字段泄漏到前端、随机函数不是真正的随机等。

3.1 题库表:一道题最少需要哪些字段

一道题从呈现到判分,最少需要题干、选项、正确答案和可选配图。如果将选项拆到单独的表,会引入额外的关联查询,对 59 个文件的轻量项目来说没有必要。常见做法是把四个选项并排放在同一条记录里,用 question 表的四个字段保存。这样查询时一次 IO 就能拿到整题,也方便做随机抽取。

表结构可以这样设计:

CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, word VARCHAR(64) NOT NULL, meaning VARCHAR(128) NOT NULL COMMENT '题干配图', option_a VARCHAR(128) NOT NULL, option_b VARCHAR(128) NOT NULL, option_c VARCHAR(128) NOT NULL, option_d VARCHAR(128) NOT NULL, answer TINYINT NOT NULL COMMENT '正确选项 1-4', image VARCHAR(128) DEFAULT NULL COMMENT '配图文件名' );

answer 字段用 1-4 表示 A-D,而不是直接存答案内容,这样前端在做答案比较时只需要判断selectedIndex === question.answer,不需要做字符串匹配,减少因空格和大小写导致的不一致。image 字段存相对文件名,真正访问时由后端拼出完整路径,前端只拿到相对路径可以减少暴露服务器目录结构的风险。

字段类型说明
wordvarchar(64)英语单词,题干主体
meaningvarchar(128)中文释义,用于出题时作为选项之一
option_a - dvarchar(128)四个候选释义
answertinyint正确选项,取值为 1 到 4
imagevarchar(128)配图文件名,如ambition.png

3.2 后端出题接口:Servlet 返回 JSON

题目表建好后,需要一个接口把题目数据提供给前端。传统 Servlet 项目里,通常是写一个QuestionServlet,映射到/api/questions。它负责从数据库读取全部题目,再用 Jackson 或 Gson 序列化成 JSON 返回。注意不要直接返回数据库里的原文,而是过滤掉 answer 字段,避免前端源码里泄露答案。我一般会创建一个只包含题目内容的视图对象(VO),把答案单独留在后端。

@WebServlet("/api/questions") public class QuestionServlet extends HttpServlet { private QuestionDao questionDao = new QuestionDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 可选参数 limit:控制本次测试题目数量,默认为 10 String limitParam = req.getParameter("limit"); int limit = 10; if (limitParam != null) { try { limit = Integer.parseInt(limitParam); } catch (NumberFormatException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "limit 必须是数字"); return; } } List<QuestionVO> questions = questionDao.findRandom(limit); resp.setContentType("application/json;charset=UTF-8"); new Gson().toJson(questions, resp.getWriter()); } }

doGet 方法里先解析 limit 参数,再做参数校验,最后把 VO 列表序列化为 JSON。这里将数据类型转换单独 catch,是因为用户传入limit=abc时不能直接让 500 错误打回页面,而是返回 400 提示请求参数有误。findRandom(limit)是 DAO 层方法,SQL 一般用ORDER BY RAND() LIMIT ?,但数据量大时RAND()全表扫描会有性能问题,更好的做法是随机取主键再回表查询,后面可以自己优化。

3.3 前端动态渲染:HTML 模板与 JavaScript 校验

HTML 页面里只需要一个容器节点和四个选项区域,剩下的交互由 JavaScript 控制。这里要注意的是,题目选项应该是按钮而不是文字,这样用户点击后能直接触发事件,不需要额外的表单提交。

<div id="question-area"> <h2 id="word"></h2> <img id="question-image" alt="题干配图" /> <div id="options"> <button class="option">let questionList = []; let current = 0; let score = 0; async function initQuiz() { const res = await fetch('/api/questions?limit=10'); questionList = await res.json(); renderQuestion(); } function renderQuestion() { const q = questionList[current]; document.getElementById('word').textContent = q.word; document.getElementById('question-image').src = '/images/' + (q.image || 'default.png'); const buttons = document.querySelectorAll('.option'); buttons.forEach((btn, index) => { btn.textContent = q['option_' + String.fromCharCode(97 + index)]; btn.dataset.index = index + 1; }); document.getElementById('progress').textContent = (current + 1) + ' / ' + questionList.length; } function handleOptionClick(btn) { const q = questionList[current]; if (parseInt(btn.dataset.index) === q.answer) score++; current++; if (current < questionList.length) { renderQuestion(); } else { showResult(score); } }

代码里String.fromCharCode(97 + index)是一种把索引映射成 a/b/c/d 选项字段名的技巧,这样不用写四行重复的 if。需要注意dataset.index是字符串,比较时必须用parseIntNumber强转,否则会出现"2" === 2不成立的隐蔽 bug。

3.4 计分规则:正确率与词汇量等级的映射

测试结束后的等级映射并没有统一标准,项目源码里的规则可以在 service 层看到。一般做法是按正确率分级,并用一个工具方法计算。

public class VocabularyGrader { public static String grade(int correct, int total) { double rate = (double) correct / total; if (rate >= 0.9) return "A"; if (rate >= 0.8) return "B"; if (rate >= 0.6) return "C"; return "D"; } }

这种映射简单直观,但实际产品中最好导出对照表给业务方确认。以下是几种常见的等级映射参考:

正确率区间等级词汇量参考
90% - 100%A熟练使用
80% - 89%B日常沟通无压力
60% - 79%C尚需巩固
0% - 59%D词汇储备不足

映射表在 Java 中可以用枚举实现,也可以用 Map 存放区间,避免出现魔法数字。

4. 用户登录与成绩记录:Session 和 JDBC 的配合

测试系统与官网静态页不同,用户提交成绩后要能查到自己历史测试记录,因此登录是必要环节。Java Web 里最轻量的方案就是 Session:登录成功后在 session 里写入 userId,需要鉴权的页面或接口通过 Filter 拦截。注意 Session 只适合单机部署,如果以后要横向扩展,需要换成 Redis 分布式会话,但这是后话。

4.1 Session 校验:哪些接口需要登录才能访问

常见做法是通过一个LoginFilter过滤/api/*下的受保护接口。宁可多拦截一个接口,也不要放过一个,因为测试记录属于用户隐私数据。

@WebFilter("/api/secure/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) req; HttpSession session = httpReq.getSession(false); if (session != null && session.getAttribute("userId") != null) { chain.doFilter(req, resp); } else { ((HttpServletResponse) resp).sendError(HttpServletResponse.SC_UNAUTHORIZED); } } }

getSession(false)的核心作用是:不会为无登录请求的客户端自动创建 session。如果这里不传 false,每个机器人请求都会在服务器上留下一个空 session,慢慢把内存耗尽。Filter 里只保留 session 属性判断,业务逻辑不要写在这里。

4.2 用户登录的 Servlet 实现

登录接口一般用 POST 提交用户名和密码。密码不能明文存储,至少要用 SHA-256 加盐。以下代码演示了登录校验的基本结构。

@WebServlet("/api/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || password == null || username.trim().isEmpty() || password.trim().isEmpty()) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码不能为空"); return; } User user = userDao.findByUsername(username); if (user != null && user.getPassword().equals(hash(password))) { req.getSession(true).setAttribute("userId", user.getId()); resp.setStatus(HttpServletResponse.SC_OK); } else { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误"); } } private String hash(String raw) { // 实际实现里要加盐并压缩多次,这里省略 return Integer.toHexString(raw.hashCode()); } }

登录成功时直接让容器创建 session,并把 userId 放进去。错误提示只返回 401 状态码,不区分“用户不存在”和“密码错误”,这是为了防止账号枚举。上面示例里的hash方法只是示意,真实项目中不能用String.hashCode(),因为它既不抗碰撞也不固定,正确做法是加盐 + 多次哈希,比如 PBKDF2。

4.3 成绩写入数据库:PreparedStatement 防注入

成绩记录表至少需要用户 ID、测试时间、答对数量和总题数。等级可以在展示时再算,但也可以在写入时算好存起来。

CREATE TABLE test_history ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, correct INT NOT NULL, total INT NOT NULL, grade VARCHAR(4) NOT NULL, test_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

写入逻辑用 PreparedStatement,不要用字符串拼接 SQL:

public void saveHistory(Connection conn, int userId, int correct, int total, String grade) throws SQLException { String sql = "INSERT INTO test_history (user_id, correct, total, grade) VALUES (?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, correct); ps.setInt(3, total); ps.setString(4, grade); ps.executeUpdate(); } }

参数占位符?的顺序必须和 SQL 中列的顺序一致,四个 setXxx 调用分别绑定用户、答对数、题数和等级。使用 PreparedStatement 不仅避免拼接,还能复用执行计划,在高频写入时有性能优势。注意try-with-resources会自动关闭 PreparedStatement,不需要手写 close。

4.4 连接泄漏与乱码:最常见的两个坑

很多人写完第一个版本后,测试几次就发现数据库连接耗尽。原因通常是conn没有在 finally 中关闭。前面的例子只关闭了 PreparedStatement,Connection 如果是自己通过DriverManager.getConnection拿到的,也必须关闭。建议统一用一个DbUtil工具类,提供getConnection()close(AutoCloseable...)方法,并在每个 DAO 方法里调用。

另一个高发问题是中文乱码。响应 JSON 时设置resp.setCharacterEncoding("UTF-8")还不够,要放在getWriter()之前。对于 POST 请求,Tomcat 默认按 ISO-8859-1 解析参数,所以还要加上req.setCharacterEncoding("UTF-8"),并且放在第一次读取参数之前。这两个顺序写反,测试时会随机出现时而正常时而乱码的现象。

问题表现检查位置
连接泄漏运行几分钟后请求变慢或超时DAO 中 Connection 是否关闭
中文乱码题目与选项显示为问号Servlet 的编码设置顺序
Session 失效刷新后需要重新登录Filter 中是否误调用了 getSession(true)

5. 本地验证:用 curl 与 IDEA 快速定位接口问题

项目部署到 Tomcat 后,很多人习惯直接打开浏览器点页面,但真正遇到问题时,浏览器帮不了太多。用 curl 验证接口,能快速把问题定位在后端还是前端。

5.1 打包并启动

在项目根目录执行以下命令,跳过测试可以加快打包速度:

mvn clean package -DskipTests

生成的 war 包在target/目录下。IDEA 里配置 Tomcat 时,把 Application context 设为/,这样前端接口路径/api/questions不用带项目名前缀,和本地联调保持一致。

5.2 用 curl 跑通登录和受保护接口

启动后先用 curl 发起登录请求,把 session 保存到本地文件:

curl -c cookies.txt \ -d "username=admin&password=123456" \ http://localhost:8080/api/login

-c cookies.txt表示将响应中的 Set-Cookie 写入文件。随后请求受保护接口时,用-b cookies.txt带上这个 cookie:

curl -b cookies.txt \ "http://localhost:8080/api/questions?limit=3"

如果返回 JSON 数组,说明登录、Session 和出题链路都正常。如果返回 401,问题在 Session 或 Filter。如果返回 400,说明 limit 参数解析异常。前端页面加载不出来时,先看浏览器开发者工具 Network 面板里接口的响应状态码,再决定是调 JS 还是调 Java。最后一个小技巧:在 IDEA 的 Run Configuration 里给 Tomcat 设置 VM 参数-Dfile.encoding=UTF-8,能避免控制台日志里中文乱码干扰排错判断。

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

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

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

立即咨询