尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java Web期末题库:Servlet/JSP考点与Tomcat刷题站部署

Java Web期末题库:Servlet/JSP考点与Tomcat刷题站部署 简介这份 Java Web 复习题集1面向准备期末考试的在校学生与 Java Web 入门自学者以单项选择题形式覆盖 JSP 与 Servlet 的核心考点帮助读者在考前集中梳理知识盲区、快速定位易混淆概念。资源共 1 个 pdf 文件压缩包约 701KB篇幅紧凑、便于打印或移动端随时翻阅内容按题目顺序编排答案与解析可直接对照。题目涉及 JSP 动作指令标记与 include、taglib、page 指令的区别变量声明范围与四种作用域大小关系session、application、response、request 等隐含对象的创建时机与典型用法还包括 Cookie 获取、JavaBean 编写规范、jsp:setProperty 属性设置以及 Servlet 生命周期、过滤器、EL 表达式等高频考查点。已有 2846 人学习适合作为章节自测与考前冲刺的题库材料配合教材可逐题复盘错误原因、补齐薄弱环节。1. 期末前一周Java Web 题库真正的用法不是刷题班级群里流传的《Java Web 复习题集(1)期末考试题库含答案.pdf》通常有几百道题很多人从第一页顺着往下背背到第四章就乱了。问题不在记忆力而在于 Java Web 期末考的考点高度收敛Servlet 生命周期、JSP 九大内置对象与四种作用域、session 与 cookie、JDBC 事务、MVC 分层与标准目录结构、Filter 与 Listener这几块能吃掉八成以上的分值。一份题库的价值不是答案本身而是把几百道散题还原成一张「知识点—分值—题型」的映射表让每一道错题都能挂回一个可以集中攻克的块上。顺着这个思路题库要经过三步才能变成分数先给题目建立知识点骨架再把 PDF 解析成可检索、可去重、可统计错题率的本地数据最后用标准 Java Web 项目结构把它跑成一个能在线刷题的站并接上自动部署。这三步正好构成了一个后端工程师复习时最舒服的闭环也能顺手把 Servlet、JSP、Tomcat 目录约定这些考点在动手过程中过一遍。适合正在准备 Java Web 期末考试的人也适合想把老旧教材项目改造成内部培训题库的开发。2. Java Web 题库的知识点骨架从 Servlet 生命周期到 JSP 九大内置对象题库里的题目顺序是乱的先按题型过一遍意义不大。更有效的做法是先画一张知识点分布表再用这张表去反向标注每一道题的题号标注完你会发现某些章节的题号密度高得离谱那就是该优先复习的地方。知识点常见题型分值占比参考复习优先级Servlet 生命周期与线程安全简答、编程15%~20%高JSP 内置对象与作用域选择、填空10%~15%高session 与 cookie简答、编程10% 左右高JDBC 与事务编程、简答15% 左右高MVC 分层与目录结构综合大题10% 左右中Filter 与 Listener简答、编程10% 左右中EL 与 JSTL选择、填空5%~10%中这张表不需要精确它的作用是给复习排序。下面三节把优先级最高的三类考点拆开顺带给出题库里出现频率最高的那几种问法。2.1 Servlet 生命周期与线程安全题库里分值最高的一组生命周期这一块题库里的问法翻来覆去就三种说出三个方法与调用次数、写一个用 init 做初始化的 Servlet、判断「Servlet 是单例吗成员变量要不要加锁」。把下面这张表记牢选择题和简答题基本就稳了。阶段方法调用次数触发时机初始化init(ServletConfig)一次首次访问或容器启动服务service(req, resp)每次请求请求到达时由容器调用销毁destroy()一次应用卸载或容器关闭真正容易丢分的是线程安全。容器默认只创建一个 Servlet 实例多个请求线程共用它所以成员变量天生不安全。下面的写法是题库编程题的标准答案形态。// WebServlet 是 Servlet 3.0 注解等价于 web.xml 里的 servlet servlet-mapping WebServlet(name counterServlet, urlPatterns /counter, loadOnStartup 1) public class CounterServlet extends HttpServlet { // 共享状态必须同步或者干脆不要定义成成员变量 private int count 0; private ServletConfig config; Override public void init(ServletConfig config) throws ServletException { super.init(config); // 必须先调用父类否则 getServletContext() 拿到 null this.config config; log(init 只执行一次loadOnStartup1 表示容器启动时就加载); } Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { synchronized (this) { // 对临界区加锁避免自增丢失 count; } resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(访问次数 count 线程 Thread.currentThread().getName()); } Override public void destroy() { log(destroy 只执行一次用来释放 init 中申请的资源); count 0; } }代码里有几个参数值得单独拎出来讲。loadOnStartup 1决定容器启动时是否创建实例值越小越早加载负数表示首次访问才创建这正好对应题库里「Servlet 什么时候被实例化」那道题的答案。super.init(config)不能省省掉之后getServletContext()会返回 null这是简答题常见的扣分点。synchronized (this)的锁粒度是实例因为只有一个实例所以能起到作用但如果单例本身就不该持有状态更好的答法是直接用AtomicInteger或把计数放到ServletContext里。写代码题的时候顺手提一句「能用局部变量就不用成员变量」阅卷时是加分项。2.2 JSP 九大内置对象与四种作用域的配对题九大内置对象几乎是必考填空题难点不在记名字而在记住每个对象的类型和作用域。内置对象类型作用域典型用途requestHttpServletRequestrequest取表单参数、请求转发传值responseHttpServletResponsepage设置响应头、输出流sessionHttpSessionsession保存登录状态applicationServletContextapplication全局计数、上下文参数outJspWriterpage输出字符内容pageObjectthispage当前 JSP 实例pageContextPageContextpage存取其他作用域、拿其他八个对象configServletConfigpage读 JSP 初始化参数exceptionThrowablepage仅在 isErrorPagetrue 时可用四种作用域的范围顺序是 page request session applicationEL 表达式${name}会按 pageContext、request、session、application 的顺序逐个查找找到就停。题库里那道「四个作用域都存了同名变量EL 取到哪一个」的答案就出在这里。下面的 JSP 片段把取值顺序和 JSTL 迭代一起演示了。%-- 顶部声明页面编码与 JSTL 标签库缺 c 前缀会报 500 --% % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % pageContext.setAttribute(scope, page); request.setAttribute(scope, request); session.setAttribute(scope, session); application.setAttribute(scope, application); % %-- 输出 request因为 page 作用域没存值优先命中 request --% pEL 取到${scope}/p %-- 显式指定作用域避免考试时被「就近原则」坑到 --% psession 作用域${sessionScope.scope}/p c:forEach varq items${questionList} varStatusst li${st.count}. ${q.stem}/li /c:forEach${scope}走的是隐式查找${sessionScope.scope}是限定作用域查找这两个写法在题库里经常被拿来当判断题。c:forEach的varStatus提供count从 1 开始和index从 0 开始两个属性题库里「JSTL 循环从 1 计数」那道题考的就是count。另外pageEncoding和contentType里的 charset 要一致不一致时中文乱码会出现在out.write而不是 EL 输出上排查时容易看错方向。2.3 MVC 分层与 Java Web 项目标准目录结构综合大题的标准答法「画出 Java Web 项目的标准目录结构并说明各目录作用」是综合题里的常客。答题时按下面这棵树写再配一句分层职责说明基本能拿满。# 一个标准的、能被 Tomcat 直接识别的 Java Web 项目结构 javaweb-quiz/ ├── src/main/java/ # Java 源码按分层建包 │ └── com/example/quiz/ │ ├── controller/ # Servlet只做参数校验和转发 │ ├── service/ # 业务逻辑事务边界所在层 │ ├── dao/ # JDBC 数据访问最靠近数据库 │ └── model/ # 实体类与表结构对应 ├── src/main/webapp/ # Web 根目录打包后成为 WAR 的内容 │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符不在这个目录下容器读不到 │ │ ├── classes/ # 编译后的 .class构建工具自动放 │ │ └── lib/ # 第三方 jar如 mysql-connector-java │ ├── static/ # css、js、图片可被浏览器直接访问 │ └── index.jsp # 欢迎页默认由容器按 welcome-file 找 └── pom.xml # Maven 构建描述关键点有三个。WEB-INF目录下的资源浏览器无法直接访问web.xml和lib必须放在这里放错位置的表现是类加载不到或者配置完全不生效。classes目录对应源码编译产物Maven 会在打包时自动完成搬运。index.jsp这类欢迎页由web.xml里的welcome-file-list决定写了多个就按顺序找第一个存在就用它。分层的标准说法是controller 只负责接收参数、调用 service、选择跳转方式service 承担业务规则和事务开启关闭dao 只做 SQL 和结果集映射model 是纯数据载体。考试里写「Servlet 里直接写 JDBC」通常会被扣分理由就是越过了 service 层事务没法统一管理。目录职责放错会怎样WEB-INF/web.xml部署描述符容器找不到等同无配置WEB-INF/lib运行期依赖 jar运行时报 ClassNotFoundExceptionWEB-INF/classes编译产物同上且编译工具不会自动清理static/静态资源放进 WEB-INF 后浏览器 404src/main/java源码不会被打进 WAR3. 把 PDF 题库解析成可检索数据切题、配对与去重背题的效率瓶颈在于「检索」而 PDF 恰恰是最不适合检索的格式。把题库转成结构化 JSON才能做到按知识点抽查、按错题率排序、自动生成模拟卷。这一步的处理质量直接决定后面刷题站好不好用。3.1 先判断 PDF 是文本型还是扫描型拿到文件不要急着写脚本先用一条命令确认它有没有文字层。# pdfinfo 来自 poppler-utils用来快速看页数和元信息 pdfinfo Java Web 复习题集(1)期末考试题库含答案.pdf | head -20 # 抽取第 1 到 3 页的文字有输出说明存在文字层 pdftotext -f 1 -l 3 -layout Java Web 复习题集(1)期末考试题库含答案.pdf - | head -40-layout会尽量保留原始列位置对带选项的题目比较友好如果输出是空白或者只有零星几个字符说明是扫描件得走 OCR 路线此时直接在原图上做版面分析再识别比先 OCR 成纯文本再切题更靠谱。PDF 类型特征首选工具注意事项文本型pdftotext 有完整输出pdfplumber / pdftotext优先用 -layout扫描型无文字层页数即图片数OCR 版面分析识别错字需人工校对图文混排部分页有文字pdfplumber 按页判断逐页决定是否走 OCR加密型pdfinfo 提示已加密需要已知口令别用在线解密服务3.2 用 pdfplumber 切题与配对答案的最小脚本题干、选项、答案的识别全靠正则脚本本身不长难点在于容错。import re import json import pdfplumber Q_RE re.compile(r^(\d{1,3})[.、]\s*(.)$) # 1. 题干 / 12、题干 O_RE re.compile(r^([A-D])[.、]\s*(.)$) # A. 选项 A_RE re.compile(r^(?:参考答案|答案|答)[:]\s*(.)$) # 答案B def parse(path): items, cur [], None with pdfplumber.open(path) as pdf: for page in pdf.pages: # x_tolerance 控制字符间距判定值越小越不容易把两列粘一起 text page.extract_text(x_tolerance1.5) or for raw in text.splitlines(): line raw.strip() if not line: continue m Q_RE.match(line) if m: # 命中新题干收尾上一条 if cur: items.append(cur) cur {no: int(m.group(1)), stem: m.group(2), options: [], answer: , page: page.page_number} continue if cur is None: continue # 页眉页脚在题干之前直接丢 m O_RE.match(line) if m: cur[options].append(f{m.group(1)}. {m.group(2)}) continue m A_RE.match(line) if m: cur[answer] m.group(1).strip() continue cur[stem] line # 题干跨行拼回去 if cur: items.append(cur) return items if __name__ __main__: data parse(Java Web 复习题集(1)期末考试题库含答案.pdf) with open(questions.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(解析题数:, len(data), 有答案:, sum(1 for q in data if q[answer]))参数上要盯三个地方。x_tolerance是横向容差默认值在双栏排版下容易把左右两栏拼成一行调到 1.5 到 2 之间通常能分开。extract_text在某些页会返回None所以必须写or 否则splitlines直接抛异常。题目编号正则在\d{1,3}上做了限制避免把「2024 年真题」这类行号误判成题干。答案常常不在题目后面而是集中放在 PDF 末尾的附录里形如「1-5 BCDAA」。这种要单独解析一段再按题号回填。def fill_appendix(data, appendix_text): # 匹配 1-5 BCDAA 或 1. B 两种常见附录格式 for chunk in re.finditer(r(\d{1,3})\s*[-—]\s*(\d{1,3})\s*([A-D]{2,10}), appendix_text): start, end, letters int(chunk.group(1)), int(chunk.group(2)), chunk.group(3) for offset, letter in enumerate(letters): no start offset for q in data: if q[no] no and not q[answer]: q[answer] letter return data回填只填answer为空的题避免覆盖正文里已经抽到的答案。letters的位数和题号区间长度不一致时说明该页附录被截断了这种情况要人工看一眼原 PDF别让脚本硬猜。3.3 题干归一化与去重同一份题库里同一个知识点经常出现两道措辞几乎一样的题刷起来浪费时间。归一化之后按题干哈希去重就够了。import hashlib, re, json def norm(s): s re.sub(r\s, , s) # 去掉全部空白字符 s re.sub(r[(]\s*[)], , s) # 去掉空括号 s re.sub(r[。、], , s) # 去掉中文标点避免全半角差异 return s def key(q): return hashlib.md5(norm(q[stem]).encode(utf-8)).hexdigest()[:12] with open(questions.json, encodingutf-8) as f: qs json.load(f) seen, uniq {}, [] for q in qs: if not q[stem]: continue # 题干为空的直接丢通常是页眉误判 k key(q) if k in seen: # 重复题保留选项更多、答案更完整的那一条 if len(q[options]) len(q[answer]) \ len(seen[k][options]) len(seen[k][answer]): uniq[uniq.index(seen[k])] q seen[k] q continue seen[k] q uniq.append(q) print(原始:, len(qs), 去重后:, len(uniq))去重的核心是norm函数它把所有可能造成「看起来一样但哈希不同」的因素都消掉了。用 MD5 取前 12 位只是为了让 key 短一点碰撞概率在几百条数据规模下可以忽略。脏数据表现产生原因处理方式题干里混入页码页脚被当成正文正则过滤纯数字行选项缺 A 或 D双栏排版串行调小 x_tolerance 重新抽取答案行变成题干附录格式不统一单独解析附录并回填同一题重复出现题库合并自多份试卷题干归一化后哈希去重题干末尾被截断跨页断行按题号连续性人工核查4. 用标准目录结构把题库跑成 Java Web 项目有了questions.json下一步是让它可访问。搭一个最小的 Java Web 项目就够了顺便把标准目录结构、web.xml 和 JSP 编译产物这几个考点在真实环境里过一遍。4.1 web.xml 与 Servlet 3.0 注解怎么取舍两种方式都能注册 Servlet区别在于教材和考试更认 web.xml而实际开发更爱注解。刷题站可以两者都用注解写新的 Servletweb.xml 保留欢迎页、过滤器顺序和错误页配置这几样注解表达不了或者表达起来很别扭。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 display-namejavaweb-quiz/display-name !-- 欢迎页按顺序找第一个存在就用它 -- welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 编码过滤器声明在前保证所有请求先过它 -- filter filter-nameencodingFilter/filter-name filter-classcom.example.quiz.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 统一错误页配合 exception 内置对象使用 -- error-page error-code404/error-code location/WEB-INF/views/404.jsp/location /error-page error-page exception-typejava.lang.Throwable/exception-type location/WEB-INF/views/500.jsp/location /error-page /web-appfilter-mapping的书写顺序决定过滤器链的执行顺序这是考试里「多个过滤器谁先执行」的标准依据用WebFilter注解时顺序不确定所以涉及顺序的场景必须回到 web.xml。/WEB-INF/views/下的 JSP 不能被浏览器直接访问只能通过转发到达顺手就实现了「禁止绕过控制器直接看页面」这个效果。4.2 配置 Tomcat 后查看 JSP 编译后的 Java 类JSP 最终会被 Jasper 编译成 Servlet找到那份产物就能直观理解「JSP 本质是 Servlet」这句话。部署和查找路径的命令如下。export CATALINA_HOME/opt/tomcat # 把 WAR 丢进 webappsTomcat 会自动解压 cp target/javaweb-quiz.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.sh tail -f $CATALINA_HOME/logs/catalina.out # 看启动日志和异常栈 # 访问一次页面让 JSP 完成编译然后找产物 curl -s http://localhost:8080/javaweb-quiz/index.jsp -o /dev/null find $CATALINA_HOME/work/Catalina/localhost/javaweb-quiz -name *.class产物默认只有.class想看生成的.java源码得改 Jasper 的初始化参数。最简单的方式是在conf/web.xml里给 jsp servlet 加两个init-param。servlet servlet-namejsp/servlet-name servlet-classorg.apache.jasper.servlet.JspServlet/servlet-class init-param param-namedevelopment/param-name param-valuetrue/param-value !-- 开发模式改了 JSP 会重新编译 -- /init-param init-param param-namekeepgenerated/param-name param-valuetrue/param-value !-- 保留生成的 .java 源文件 -- /init-param load-on-startup3/load-on-startup /servlet改完重启work/Catalina/localhost/javaweb-quiz/org/apache/jsp/下面就会同时出现index_jsp.java和index_jsp.class。打开那份.java会看到_jspService方法九个内置对象都在里面被声明和赋值out是JspWriterpageContext是PageContext——这正是九大内置对象那道题的实物证据。参数默认值作用排查用途developmenttrue是否检测 JSP 变更并重编译线上设 false 提升性能keepgeneratedfalse是否保留生成的 .java排查 JSP 语法错误的行号forktrue是否用独立进程编译 JSP设为 false 才能看到编译日志genStringAsCharArrayfalse字符串是否以 char 数组输出大页面吞吐优化提示work目录可以随时整个删掉Tomcat 会重新编译但别在运行中删可能出现类加载冲突。4.3 编码、路径与 404/500 的排查顺序刷题站最常见的问题就那么几类按固定顺序排查比到处翻代码快得多。现象常见根因处理方式GET 请求中文参数乱码Connector 未配 URIEncoding在 server.xml 的 Connector 加 URIEncodingUTF-8POST 中文乱码未设 request 编码在过滤器里 setCharacterEncoding静态资源 404放进了 WEB-INF移到 webapp 根目录或 static 下所有请求 404context path 写错应用名即 context path检查 URL 前缀500 且栈里有 ClassNotFoundjar 没进 WEB-INF/lib检查依赖 scope 是否为 providedJSP 改完不生效developmentfalse 或缓存删除 work 目录下对应应用文件夹乱码问题的根子在于容器对 URI 和请求体的解码时机不同GET 参数在 Connector 层就解码了配URIEncoding才有用POST 参数要等request.getParameter第一次调用才解析所以必须在那之前设置编码放在过滤器里最稳妥。用useBodyEncodingForURItrue可以让 URI 也跟随请求体编码但显式配 UTF-8 更清楚。5. 参数类题目的对照答法session、JDBC 事务与过滤器顺序题库里丢分最集中的不是概念题而是那些「问参数含义和默认值」的题。这类题没有推理空间只能靠对照记忆把参数和它影响的行为绑在一起记。5.1 session 与 cookie 的参数题session 靠 JSESSIONID 这个 cookie 维持两者的参数经常被混着考。参数所属含义默认值setMaxAge(int)Cookie存活秒数0 立即删除负数为会话级-1setPath(String)Cookie生效路径当前路径setHttpOnly(boolean)Cookie禁止 JS 读取falsesetMaxInactiveInterval(int)HttpSession闲置超时秒数容器配置常为 1800invalidate()HttpSession立即失效—setAttribute / getAttributeHttpSession存取值—// 登录成功后写 session并把 JSESSIONID 的 cookie 属性收紧 HttpSession session request.getSession(); // 无则创建 session.setAttribute(user, username); session.setMaxInactiveInterval(30 * 60); // 单位是秒这里是 30 分钟 // 手动控制会话 cookie 的属性 Cookie jsid new Cookie(JSESSIONID, session.getId()); jsid.setHttpOnly(true); // 禁 JS 读取降低 XSS 影响 jsid.setPath(request.getContextPath()); // 只在当前应用下发送 response.addCookie(jsid); // 退出登录销毁会话 session.invalidate();getSession()等价于getSession(true)不存在就新建只想读取不想创建时用getSession(false)返回 null这一点在简答题里常被追问。setMaxInactiveInterval的单位是秒配web.xml里的session-configsession-timeout时单位是分钟两个地方单位不同是经典陷阱。invalidate()之后原对象不能再用再调getAttribute会抛IllegalStateException。5.2 JDBC 事务与连接池参数事务题的固定套路是关自动提交、执行多条、全部成功才 commit、异常时 rollback、finally 里恢复状态并关连接。隔离级别常量脏读不可重复读幻读读未提交TRANSACTION_READ_UNCOMMITTED可能可能可能读已提交TRANSACTION_READ_COMMITTED不会可能可能可重复读TRANSACTION_REPEATABLE_READ不会不会可能串行化TRANSACTION_SERIALIZABLE不会不会不会Connection conn null; PreparedStatement ps null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 开启事务 conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); ps conn.prepareStatement(UPDATE score SET point point ? WHERE qid ?); ps.setInt(1, 2); ps.setInt(2, 1001); ps.executeUpdate(); conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignore) { } } throw new RuntimeException(批改分数失败, e); } finally { if (ps ! null) try { ps.close(); } catch (SQLException ignore) { } if (conn ! null) { try { conn.setAutoCommit(true); // 归还连接前还原状态 conn.close(); } catch (SQLException ignore) { } } }setAutoCommit(false)之后的每一条语句都在同一个事务里直到 commit 或 rollback。finally里把autoCommit还原成 true 容易被忽略连接池复用连接时上一个事务的未提交状态会污染下一个使用者表现为「莫名其妙锁表」。隔离级别的默认值跟数据库有关MySQL 默认REPEATABLE_READOracle 默认READ_COMMITTED答题时如果题目写了具体数据库就按它的默认值答。5.3 过滤器与监听器的执行顺序这一块的考点集中在「谁先谁后」和「监听器能不能改请求」。顺序组件触发时机能否修改请求1ServletContextListener应用启动否2Filter.doFilter 前置代码请求进入能可包装3Servlet.service业务处理—4Filter.doFilter 后置代码响应返回能5ServletContextListener.contextDestroyed应用卸载否WebFilter(urlPatterns /*) public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig config) { String v config.getInitParameter(encoding); // 读 web.xml 的 init-param if (v ! null !v.isEmpty()) { this.encoding v; } } Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; request.setCharacterEncoding(encoding); // 必须早于 getParameter HttpServletResponse response (HttpServletResponse) resp; response.setContentType(text/html;charset encoding); long start System.currentTimeMillis(); chain.doFilter(req, resp); // 放行到下一个过滤器或 Servlet long cost System.currentTimeMillis() - start; System.out.println(request.getRequestURI() 耗时 cost ms); } Override public void destroy() { } }chain.doFilter之前是前置处理之后是后置处理计时、日志、权限校验都靠这一对位置实现。init里读init-param让编码可配比写死更符合考试对「可配置」的要求。多个过滤器的执行顺序由web.xml中filter-mapping的先后决定用注解时顺序不确定所以涉及顺序的题一律答 web.xml。6. 进阶用 Jenkins 自动部署刷题站并做答案回归验证题库数据会不断补充每次手动cpWAR 再重启太慢。用 Jenkins 拉代码、构建、部署再跑一遍接口级的题目数校验能把「数据解析回归」和「部署」合成一条流水线。pipeline { agent any environment { CATALINA_HOME /opt/tomcat APP_NAME javaweb-quiz } stages { stage(拉取题库数据) { steps { // 题库解析脚本放在仓库里Jenkins 上只需执行 sh python3 tools/parse_pdf.py --out src/main/webapp/WEB-INF/questions.json } } stage(构建) { steps { sh mvn -B clean package -DskipTests } } stage(部署) { steps { sh # 先停旧应用再替换 WAR最后重启 curl -sf -u deployer:secret \\ http://localhost:8080/manager/text/undeploy?path/${APP_NAME} || true cp target/${APP_NAME}.war ${CATALINA_HOME}/webapps/ ${CATALINA_HOME}/bin/shutdown.sh || true sleep 5 ${CATALINA_HOME}/bin/startup.sh } } stage(回归验证) { steps { sh # 等容器起来最多重试 20 次 for i in $(seq 1 20); do code$(curl -s -o /dev/null -w %{http_code} \\ http://localhost:8080/${APP_NAME}/api/count) [ $code 200 ] break sleep 3 done # 题目总数低于阈值说明解析脚本出问题了直接让流水线失败 total$(curl -s http://localhost:8080/${APP_NAME}/api/count) echo 题库题数: $total if [ $total -lt 300 ]; then echo 题数异常偏低疑似 PDF 解析失败 exit 1 fi } } } post { failure { echo 构建或验证失败保留现场排查 } } }几个参数值得调整。undeploy里的凭据对应的角色需要在conf/tomcat-users.xml中配置否则热部署接口会返回 401。重试循环的 20 次和 3 秒间隔是根据 Tomcat 冷启动时间估的题库应用体积小通常 10 秒内就绪如果加了大量索引构建可以适当放大。题数阈值300是个经验值它真正的价值在于把「PDF 换了版式导致正则失配」这类静默失败变成流水线红灯而不是靠人刷页面时才发现题库空了。验证还可以做得更细按知识点抽查接口是否存在对应题目用一条 SQL 直接统计分布。-- 按知识点统计题量与答案覆盖率答案覆盖低的章节要先补解析 SELECT kp.name AS 知识点, COUNT(q.id) AS 题量, SUM(CASE WHEN q.answer IS NOT NULL AND q.answer THEN 1 ELSE 0 END) AS 有答案, ROUND(SUM(CASE WHEN q.answer IS NOT NULL AND q.answer THEN 1 ELSE 0 END) / COUNT(q.id) * 100, 1) AS 覆盖率 FROM question q JOIN knowledge_point kp ON kp.id q.kp_id GROUP BY kp.name ORDER BY 覆盖率 ASC;覆盖率低于 90% 的知识点多半是答案集中在附录、解析脚本没回填成功回到 3.2 节那段fill_appendix里加一条格式分支就能解决。最后补一个刷题站的本地导出技巧把/api/questions?kpJDBC的返回直接落成 Markdown 错题本考前只看覆盖率最低的两个知识点比从第一页翻到最后一页稳得多。本文还有配套的精品资源点击获取
返回列表