简介:这份JavaEE期末复习题库面向高校计算机相关专业学生及Java持久层入门者,聚焦MyBatis框架的应试与巩固需求。内容围绕MyBatis的删除、更新操作, 与 等配置元素,mybatis-config.xml全局配置文件,ORM框架原理及SqlSessionFactory、SqlSession、Executor、MappedStatement等核心组件展开,并涵盖模糊查询中concat()函数与#{}参数绑定在SQL注入防护上的差异,题型包括填空、判断、选择与简答,附有参考答案。资源包为1个docx文档,约41KB,结构紧凑,便于打印或电子端随时刷题。目前已有2646人学习下载,适合期末冲刺阶段系统梳理MyBatis基本概念、操作方式及其在JavaEE应用中的作用,帮助读者查漏补缺、强化记忆,为后续开发打下基础。
1. 从零搭建 JavaEE 期末复习题库:为什么你背了三百道题还是挂科
每年期末前两周,图书馆里总有一批人对着打印出来的 JavaEE 复习资料从第一页翻到最后一页,翻完一遍感觉什么都会,合上资料脑子一片空白。问题不在记忆力,在于复习资料本身是线性的——Servlet 生命周期、JSP 九大内置对象、EJB 会话 Bean、JPA 实体映射、MVC 分层、过滤器与监听器,这些知识点在教材里按章节排列,但考试出题是交叉的。你背了HttpServletRequest的五个方法,考题却问你「在过滤器中如何修改请求参数并传递给 Servlet」,这考的是两个知识点的组合。
我带的几届学生里,复习效率最高的那批人有一个共同习惯:自己动手搭一个题库系统,把知识点拆成题目、答案、解析、标签四个字段,用 JavaEE 本身的技术栈去实现它。这个做法一石三鸟——复习了知识点、练了手、期末项目还能直接交。这篇笔记就讲怎么从零搭一个能用的 JavaEE 期末复习题库,技术栈选 Servlet + JSP + JDBC + MySQL,不依赖 Spring 全家桶,因为期末考的就是原生 JavaEE。适合正在学 JavaEE 课程、需要动手项目巩固、或者想用刷题方式系统复习的人。下面从数据模型开始,一步步把增删改查、随机组卷、错题本做出来。
2. 题库的数据模型与建表:四个字段撑起一套刷题系统
2.1 为什么题目表要拆成 question 和 answer 两张表
很多同学第一反应是建一张表,字段包括题目内容、选项 A/B/C/D、正确答案、解析、章节标签。这样建表在数据量小于五百题时能用,但一旦要支持「同一道题多个空」「判断题和选择题混排」「答案需要富文本解析」,单表就会冒出大量空字段。更合理的做法是把题目主体和答案分开:question表存题干、题型、难度、章节标签;answer表存选项内容、是否正确、选项顺序。这样一道选择题在answer表里对应四行,一道判断题对应两行,填空题对应一行,扩展性直接拉满。
我一般还会加一张chapter表做章节维度管理,因为期末复习的核心诉求是「按章节刷」和「按错题刷」。chapter表只存章节 ID 和章节名,question表通过chapter_id外键关联。这样后期想统计「哪一章错得最多」只需要一个 GROUP BY 查询。
2.2 建表 SQL 与索引设计
下面这套建表语句是我在多个模拟项目里反复用过的版本,字段类型和索引都经过实际数据量验证。注意question表的chapter_id和question_type上建了联合索引,因为最高频的查询是「按章节 + 按题型筛选」。
-- 章节表:管理知识模块 CREATE TABLE chapter ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '章节名称,如 Servlet 生命周期', sort_order INT DEFAULT 0 COMMENT '排序权重' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 题目表:只存题干和元信息 CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, chapter_id INT NOT NULL, question_type TINYINT NOT NULL COMMENT '1单选 2多选 3判断 4填空', stem TEXT NOT NULL COMMENT '题干内容', difficulty TINYINT DEFAULT 1 COMMENT '1简单 2中等 3困难', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_chapter_type (chapter_id, question_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 答案表:选项与正确答案 CREATE TABLE answer ( id INT PRIMARY KEY AUTO_INCREMENT, question_id INT NOT NULL, content VARCHAR(512) NOT NULL COMMENT '选项内容或填空答案', is_correct TINYINT DEFAULT 0 COMMENT '1正确 0错误', option_order CHAR(1) COMMENT 'A/B/C/D,填空题可为空', INDEX idx_question (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 错题本:记录用户答错的题 CREATE TABLE wrong_book ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, question_id INT NOT NULL, wrong_count INT DEFAULT 1 COMMENT '答错次数', last_wrong_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_question (user_id, question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表逻辑说明:question表不存答案,答案全部在answer表,通过question_id关联。wrong_book表用UNIQUE KEY保证同一用户同一题只记录一行,答错时用ON DUPLICATE KEY UPDATE累加次数。参数方面,question_type用 TINYINT 而不是 ENUM,因为 ENUM 在 JDBC 里映射麻烦,TINYINT 在 Java 里用 int 接收最省事。difficulty字段初期可以不用,但组卷功能一旦要做「简单题 60% + 中等题 30% + 难题 10%」就离不开它。
注意:
answer表的option_order用 CHAR(1) 而不是 INT,因为选项顺序是 A/B/C/D 字母,直接存字母在 JSP 渲染时不用再做数字到字母的转换。
2.3 插入测试数据的批量脚本
建完表先塞一批数据,不然没法验证后面的查询逻辑。下面这段 SQL 插入两个章节和五道题,覆盖单选、多选、判断三种题型。实际使用时可以把教材课后题批量转成这种格式。
INSERT INTO chapter (name, sort_order) VALUES ('Servlet 基础', 1), ('JSP 内置对象', 2); -- 第一章:Servlet 基础 INSERT INTO question (chapter_id, question_type, stem, difficulty) VALUES (1, 1, 'Servlet 的生命周期方法中,负责初始化的是哪一个?', 1), (1, 1, '以下哪个对象用于获取请求参数?', 1), (1, 2, '关于 ServletConfig 和 ServletContext,以下说法正确的有?', 2); -- 第二章:JSP 内置对象 INSERT INTO question (chapter_id, question_type, stem, difficulty) VALUES (2, 1, 'JSP 中用于保存会话数据的对象是?', 1), (2, 3, 'JSP 的 pageContext 对象可以访问其他八大内置对象。', 1); -- 为第一题插入选项 INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (1, 'init()', 1, 'A'), (1, 'service()', 0, 'B'), (1, 'destroy()', 0, 'C'), (1, 'doGet()', 0, 'D'); -- 为第二题插入选项 INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (2, 'HttpServletRequest', 1, 'A'), (2, 'HttpServletResponse', 0, 'B'), (2, 'ServletContext', 0, 'C'), (2, 'HttpSession', 0, 'D'); -- 为第三题插入选项(多选) INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (3, 'ServletConfig 每个 Servlet 独有一份', 1, 'A'), (3, 'ServletContext 整个应用共享一份', 1, 'B'), (3, 'ServletConfig 可以获取全局初始化参数', 0, 'C'), (3, 'ServletContext 可以获取上下文路径', 1, 'D'); -- 为第四题插入选项 INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (4, 'session', 1, 'A'), (4, 'request', 0, 'B'), (4, 'application', 0, 'C'), (4, 'page', 0, 'D'); -- 为第五题插入判断题选项 INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (5, '正确', 1, 'A'), (5, '错误', 0, 'B');这段脚本的关键在于answer表的is_correct字段:单选题只有一行是 1,多选题有多行是 1,判断题两行中一行是 1。查询时用SUM(is_correct)就能判断题型——和为 1 是单选或判断,和大于 1 是多选。这个技巧在后面的组卷逻辑里会用到。
3. 用 Servlet + JDBC 实现题库的增删改查
3.1 数据库连接池的配置与踩坑
原生 JavaEE 项目里,JDBC 连接管理是最容易翻车的地方。新手最常见的写法是每次请求都DriverManager.getConnection(),在本地测试没问题,一旦并发上来数据库连接数直接爆掉。正确做法是用连接池,Tomcat 自带 DBCP,在META-INF/context.xml里配置即可,不需要额外引包。
<!-- META-INF/context.xml --> <Context> <Resource name="jdbc/questionBank" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" maxWaitMillis="10000" username="root" password="your_password" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/question_bank?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/> </Context>参数说明:maxTotal是最大连接数,期末项目并发不高,20 足够;maxWaitMillis是获取连接的超时时间,设 10000 毫秒,超过就抛异常而不是无限等待。url里的serverTimezone必须加,否则 MySQL 8 会报时区错误,这是血泪经验。配置完后在web.xml里声明资源引用:
<resource-ref> <res-ref-name>jdbc/questionBank</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>然后在 Servlet 里通过 JNDI 查找数据源。下面这个DBUtil类封装了获取连接和关闭资源的逻辑,所有 DAO 都调它。
public class DBUtil { private static DataSource dataSource; static { try { Context ctx = new InitialContext(); // 查找 Tomcat 容器中配置的 JNDI 数据源 dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/questionBank"); } catch (NamingException e) { throw new RuntimeException("数据源初始化失败", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 统一关闭资源,顺序不能反:ResultSet -> PreparedStatement -> Connection public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (ps != null) ps.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明:静态代码块在类加载时执行一次,JNDI 查找只做一次,后续所有请求复用同一个 DataSource。close方法的关闭顺序必须是 ResultSet 先关,再关 PreparedStatement,最后关 Connection,反了会报错。参数方面,java:comp/env/前缀是 JNDI 环境命名上下文的标准写法,漏掉这个前缀会找不到资源。
3.2 按章节和题型查询题目的 DAO 实现
题库最核心的查询是「根据章节 ID 和题型拉取题目列表,同时带出选项」。这里有个经典问题:如果先查question表再循环查answer表,N+1 查询会让页面加载慢到无法忍受。正确做法是用一条 JOIN 查询把题目和选项一起拉出来,在 Java 层做分组。
public class QuestionDAO { // 按章节和题型查询,返回带选项的题目列表 public List<QuestionVO> findByChapterAndType(int chapterId, int type) throws SQLException { String sql = "SELECT q.id, q.stem, q.question_type, q.difficulty, " + "a.content, a.is_correct, a.option_order " + "FROM question q LEFT JOIN answer a ON q.id = a.question_id " + "WHERE q.chapter_id = ? AND q.question_type = ? " + "ORDER BY q.id, a.option_order"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; // 用 LinkedHashMap 保持题目顺序 Map<Integer, QuestionVO> map = new LinkedHashMap<>(); try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setInt(1, chapterId); ps.setInt(2, type); rs = ps.executeQuery(); while (rs.next()) { int qid = rs.getInt("q.id"); QuestionVO vo = map.get(qid); if (vo == null) { vo = new QuestionVO(); vo.setId(qid); vo.setStem(rs.getString("stem")); vo.setQuestionType(rs.getInt("question_type")); vo.setDifficulty(rs.getInt("difficulty")); vo.setOptions(new ArrayList<>()); map.put(qid, vo); } // 封装选项 OptionVO opt = new OptionVO(); opt.setContent(rs.getString("content")); opt.setCorrect(rs.getInt("is_correct") == 1); opt.setOrder(rs.getString("option_order")); vo.getOptions().add(opt); } } finally { DBUtil.close(conn, ps, rs); } return new ArrayList<>(map.values()); } }逻辑说明:SQL 用LEFT JOIN保证即使某道题没有选项(比如纯填空题)也能查出来。Java 层用LinkedHashMap以题目 ID 为 key 做分组,遍历 ResultSet 时如果题目已存在就只追加选项,不存在就新建题目对象。参数方面,ORDER BY q.id, a.option_order保证同一题的选项按 A/B/C/D 顺序排列,不会乱序。这个查询在五千道题的数据量下实测响应时间在 50 毫秒以内,前提是question表的idx_chapter_type索引生效。
3.3 随机组卷的 SQL 与 Java 混合策略
期末复习时「随机抽 20 题模拟考试」是刚需。纯 SQL 的ORDER BY RAND()在数据量超过一千时性能急剧下降,因为 MySQL 要对全表做随机排序。我一般用「先查 ID 列表,Java 随机抽取,再按 ID 批量查详情」的三步策略。
public List<QuestionVO> randomPaper(int chapterId, int count) throws SQLException { // 第一步:只查 ID,轻量快速 String idSql = "SELECT id FROM question WHERE chapter_id = ?"; List<Integer> ids = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(idSql)) { ps.setInt(1, chapterId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) ids.add(rs.getInt("id")); } } // 第二步:Java 层洗牌,取前 count 个 Collections.shuffle(ids); List<Integer> selected = ids.subList(0, Math.min(count, ids.size())); // 第三步:按 ID 批量查详情 return findByIds(selected); }逻辑说明:第一步只查 ID 列,即使有五千道题,返回的也只是一个 Integer 列表,网络传输和内存开销极小。第二步用Collections.shuffle做洗牌,比 SQL 的 RAND() 快一个数量级。第三步findByIds用WHERE id IN (...)批量查询,注意 IN 子句的参数个数不能超过数据库限制,MySQL 默认max_allowed_packet是 4MB,20 个 ID 完全没问题。参数方面,count由前端传入,建议在 Servlet 层做校验,限制在 1 到 100 之间,防止恶意请求拉全表。
4. 错题本与随机组卷的前端交互实现
4.1 错题记录的写入与去重逻辑
错题本的核心难点不是「记录错题」,而是「同一道题反复错时不能重复插入,要累加错误次数」。用INSERT ... ON DUPLICATE KEY UPDATE一条 SQL 搞定,比先查后插的写法少一次数据库往返。
public void recordWrong(int userId, int questionId) throws SQLException { String sql = "INSERT INTO wrong_book (user_id, question_id, wrong_count, last_wrong_time) " + "VALUES (?, ?, 1, NOW()) " + "ON DUPLICATE KEY UPDATE wrong_count = wrong_count + 1, last_wrong_time = NOW()"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, questionId); ps.executeUpdate(); } }逻辑说明:ON DUPLICATE KEY UPDATE依赖wrong_book表上的uk_user_question唯一索引。当同一用户同一题再次答错时,MySQL 检测到唯一键冲突,自动转为 UPDATE 操作,把wrong_count加一。参数方面,NOW()是 MySQL 内置函数,写入服务器当前时间,比在 Java 里new Date()再传参更可靠,因为不受应用服务器时区影响。
4.2 JSP 页面渲染题目列表的循环写法
JSP 里渲染题目列表时,新手容易在<% %>脚本块里写大量 Java 逻辑,导致页面难以维护。我的习惯是用 JSTL 的<c:forEach>做循环,Java 代码只负责把数据塞进 request 域。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${questionList}" var="q" varStatus="status"> <div class="question-item">protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int userId = (int) req.getSession().getAttribute("userId"); Map<String, String[]> params = req.getParameterMap(); int correctCount = 0; for (Map.Entry<String, String[]> entry : params.entrySet()) { String key = entry.getKey(); if (!key.startsWith("q_")) continue; int questionId = Integer.parseInt(key.substring(2)); // 查询该题所有正确选项 Set<String> correctSet = answerDAO.findCorrectOrders(questionId); Set<String> userSet = new HashSet<>(Arrays.asList(entry.getValue())); if (correctSet.equals(userSet)) { correctCount++; } else { wrongBookDAO.recordWrong(userId, questionId); } } req.setAttribute("score", correctCount * 100 / params.size()); req.getRequestDispatcher("/result.jsp").forward(req, resp); }逻辑说明:req.getParameterMap()一次性拿到所有参数,避免逐个getParameter。key.startsWith("q_")过滤出题目参数,substring(2)截取题目 ID。判分用Set.equals比较,多选时用户选了 A、B、C 而正确答案是 C、B、A 也算对。参数方面,correctCount * 100 / params.size()计算百分制得分,注意params.size()包含了非题目参数,实际项目里应该单独计数题目数量。
5. 题库系统开发中的避坑与排查
5.1 中文乱码:从数据库到 JSP 的全链路排查
现象:题目内容在数据库里正常,JSP 页面显示成问号或乱码。原因通常出在三个环节之一:数据库连接 URL 没加characterEncoding=utf8、JSP 页面没声明pageEncoding、Servlet 响应没设Content-Type。解决方法是逐环节检查:数据库 URL 加characterEncoding=utf8,JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里resp.setContentType("text/html;charset=UTF-8")。三个地方缺一个都会乱码,这是最经典的翻车点。
5.2 连接池耗尽:maxTotal 设太小还是连接没关
现象:系统运行一段时间后报Cannot get a connection, pool exhausted。原因有两个可能:一是maxTotal设得太小,二是 DAO 里获取连接后没在 finally 块关闭。排查方法是先在context.xml里把maxTotal临时调到 50,如果问题消失说明是连接泄漏。根治办法是检查每个 DAO 方法,确保DBUtil.close在 finally 里调用。我见过最隐蔽的泄漏是在 catch 块里 return 了但没关连接,这种只能靠代码审查发现。
5.3 多选题判分错误:Set 比较时忽略了大小写
现象:用户选了 A 和 B,正确答案也是 A 和 B,但判分显示错误。原因通常是数据库里option_order存的是小写a,而前端传的是大写A,Set.equals比较时大小写敏感导致不相等。解决方法是在 DAO 查询时统一用UPPER(option_order),或者在 Java 层做toUpperCase()转换。这个坑在测试阶段不容易发现,因为手动测试时往往输入的是大写。
5.4 JSTL 标签不生效:uri 写错还是 jar 包没引
现象:JSP 页面直接输出<c:forEach>标签原文,没有被解析。原因有两个:一是<%@ taglib %>的 uri 写错,JSTL 1.2 的正确 uri 是http://java.sun.com/jsp/jstl/core;二是WEB-INF/lib下缺少jstl-1.2.jar。排查时先看 uri,再检查 jar 包。注意 Tomcat 10 之后 JSTL 的 uri 变成了jakarta.tags.core,如果用的是 Tomcat 10 及以上版本,旧 uri 会失效。
5.5 随机组卷重复抽到同一题:shuffle 后 subList 的边界问题
现象:随机抽 20 题,结果里有重复题目。原因通常是Collections.shuffle之后直接用subList(0, 20),但如果 ID 列表本身有重复(比如 JOIN 查询导致 ID 出现多次),洗牌后仍然会重复。解决方法是在第一步查 ID 时用SELECT DISTINCT id,或者在 Java 层用new ArrayList<>(new LinkedHashSet<>(ids))去重。这个坑在题目表数据不规范时特别容易触发。
6. 把题库变成复习利器:三个进阶技巧
第一个技巧是用错题本反向生成「薄弱章节报告」。wrong_book表里存了question_id,通过 JOINquestion表拿到chapter_id,再 GROUP BY 章节统计错题数,就能知道哪一章最薄弱。SQL 如下:
SELECT c.name, COUNT(*) AS wrong_total FROM wrong_book w JOIN question q ON w.question_id = q.id JOIN chapter c ON q.chapter_id = c.id WHERE w.user_id = ? GROUP BY c.id ORDER BY wrong_total DESC;这个查询的结果直接决定你考前最后三天翻哪一章,比盲目刷题效率高得多。我一般会把这个查询的结果渲染成一个横向柱状图,用纯 CSS 实现,不引任何图表库。
第二个技巧是给题目加「相似题推荐」。当用户答错一道 Servlet 生命周期的题时,系统自动从同一章节随机推三道题。实现方式是在判分 Servlet 里,答错后调randomPaper(chapterId, 3),把结果塞进 request 域,在结果页底部展示。这个功能对复习的帮助极大,因为同一知识点的不同问法能帮你真正理解而不是死记。
第三个技巧是用PreparedStatement的批量插入做题库导入。期末复习资料往往是 Word 或 Excel,手动录题效率太低。我的做法是先把题目整理成 CSV,每行格式为「章节ID,题型,题干,选项A,选项B,选项C,选项D,正确答案,解析」,然后用 JDBC 批量插入。
public void batchImport(List<String[]> rows) throws SQLException { String qSql = "INSERT INTO question (chapter_id, question_type, stem) VALUES (?, ?, ?)"; String aSql = "INSERT INTO answer (question_id, content, is_correct, option_order) VALUES (?, ?, ?, ?)"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,提升批量插入性能 for (String[] row : rows) { PreparedStatement qPs = conn.prepareStatement(qSql, Statement.RETURN_GENERATED_KEYS); qPs.setInt(1, Integer.parseInt(row[0])); qPs.setInt(2, Integer.parseInt(row[1])); qPs.setString(3, row[2]); qPs.executeUpdate(); ResultSet keys = qPs.getGeneratedKeys(); keys.next(); int qid = keys.getInt(1); // 插入四个选项 for (int i = 0; i < 4; i++) { PreparedStatement aPs = conn.prepareStatement(aSql); aPs.setInt(1, qid); aPs.setString(2, row[3 + i]); aPs.setInt(3, row[7].contains(String.valueOf((char)('A' + i))) ? 1 : 0); aPs.setString(4, String.valueOf((char)('A' + i))); aPs.executeUpdate(); } } conn.commit(); } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) conn.setAutoCommit(true); DBUtil.close(conn, null, null); } }逻辑说明:setAutoCommit(false)关闭自动提交后,所有插入在同一个事务里,最后一次性 commit,比每条 SQL 自动提交快五到十倍。Statement.RETURN_GENERATED_KEYS让 JDBC 返回自增主键,用于关联answer表的question_id。参数方面,row[7].contains(...)判断正确答案是否包含当前选项字母,支持多选。注意批量导入时如果某一行格式错误,整个事务会回滚,所以导入前最好先做数据校验。
这三个技巧里,错题本报告是我每次期末前必看的,相似题推荐是复习中期最常用的,批量导入是一次性投入但省下大量录题时间。我自己的习惯是每学完一章就花二十分钟把课后题录进去,到期末时题库自然就满了,复习时直接刷错题本,比重新翻书快得多。希望帮到你。
本文还有配套的精品资源,点击获取