ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Web老四样技术栈实战:Servlet+JSP+Bootstrap+Mysql学生成绩管理系统

Java Web老四样技术栈实战:Servlet+JSP+Bootstrap+Mysql学生成绩管理系统 简介这是一套基于Java技术栈的学生成绩管理系统完整项目采用ServletJSPBootstrapMySQL四层协作面向Java Web初学者、课程设计及毕业设计参考者可用来掌握动态网页开发、MVC分层与数据库交互。压缩包共575个文件约9.17MB内含85个Java源码、74个JSP页面、122个JS脚本、56个CSS样式及12个JAR依赖库并附带SQL数据库脚本覆盖从后端逻辑到前端展示的完整链路。已有3117人学习使用。项目中包含学生管理、成绩录入、查询与统计等核心模块实体类、DAO与Servlet分层清晰便于对照理解请求处理、页面渲染和CRUD操作Bootstrap组件与自定义CSS的结合也展示了如何在传统JSP项目中构建响应式界面。对想快速上手传统Web开发或准备相关实验报告的人这份资源兼具参考性和可运行价值。1. 学生成绩管理系统的Java老四样技术栈现在还能打吗用 JavaServletJSPBootstrapMysql 这套组合去做学生成绩管理系统放在今天看像考古但课程设计和毕业设计里这种 Java Web 老四样项目一直是重灾区老师要一个能跑、能讲、能改的完整案例学生要一份自己能说清原理的源码。这个系统解决的就是典型增删改查场景——老师录入成绩、学生按学号查分、按班级统计平均分和不及格人数技术栈恰好覆盖从数据库到浏览器的一条完整链路。适合 Java 基础刚入门、需要完整案例源码做课设或基于 JSP 的毕设选题的人也适合想补 Java Web 基本功的初级开发。这套组合还有个附加价值它涉及的问题基本也是 java 面试八股文里的高频考点。2. 从数据库到页面的请求链路ServletJSP 的核心分工2.1 三层架构与包结构DAO、Service、Servlet、JSP 各管什么学生成绩管理系统规模不大但我不建议把所有代码堆在一个 Servlet 里。见过太多课设源码是ScoreServlet里既拼 SQL 又输出 HTML改一个字段要在三个页面来回找。常见做法是分 ControllerServlet、Service、DAO、ViewJSP四层包结构固定下来后面加功能只用对号入座。com.sms ├── controller // Servlet接收请求、调度视图 │ ├── LoginServlet.java │ ├── StudentServlet.java │ └── ScoreServlet.java ├── service // 业务逻辑成绩校验、统计规则 │ ├── ScoreService.java │ └── impl │ └── ScoreServiceImpl.java ├── dao // 数据访问只负责 JDBC 和 SQL │ ├── ScoreDao.java │ └── impl │ └── ScoreDaoImpl.java ├── entity // 实体类对应数据库表 │ ├── Student.java │ ├── Course.java │ └── Score.java ├── util │ ├── DBUtil.java // 连接池初始化 │ └── ResultUtil.java // 统一返回结构处理 json 或消息提示 └── filter └── EncodingFilter.java分层原则很简单Servlet 里不出现 SQLDAO 里不出现业务判断JSP 里不写 Java 脚本片段。Service 这一层在成绩系统里主要管两件事一是校验成绩数值在 0 到 100 之间二是判断同学生同课程是否已经录过成绩。把这两段逻辑放在 DAO 里也能跑但后面加业务规则时Service 层是唯一的改动点不会牵动页面。2.2 一个成绩查询请求的完整流转用户在前端输入关键字点查询请求在系统里走这样一条链浏览器 GET 请求/score/list?keyword张三带上下文路径比如http://localhost:8080/sms/score/list。Tomcat 根据WebServlet(/score/*)的映射把请求交给ScoreServlet。ScoreServlet.doGet解析keyword参数调用ScoreService.findByKeyword(keyword)。ScoreService判断 keyword 是否为空、是否要拼模糊查询再调用ScoreDao.findByKeyword(keyword)。ScoreDaoImpl用 PreparedStatement 执行带 LIKE 的 SQL从 Mysql 查出结果封装成ListScore。Servlet 把列表放到 request 作用域request.getRequestDispatcher(/score_list.jsp).forward(req, resp)转发到 JSP。JSP 用 JSTL 遍历列表渲染成 Bootstrap 表格响应回浏览器。这里有两个关键点。转发和重定向的区别后面单独说先记住查询结果用转发因为 JSP 需要拿到 request 里的scoreList属性。另外 Servlet 在整个过程里只做参数接收和视图调度它不该知道 SQL 长什么样。这个链路在面试里也很常被追问能画清楚这条线Servlet 生命周期和 MVC 两题基本就稳了。2.3 核心 Servlet 代码doGet/doPost 与路由设计成绩模块的 Servlet 我习惯用一个类处理列表、新增、修改、删除四个操作用 URL 路径区分动作而不是用?actionxxx的查询参数。REST 风格在传统 JSP 项目里用WebServlet(/score/*)就能实现路径可读性更好。WebServlet(/score/*) public class ScoreServlet extends HttpServlet { private ScoreService scoreService new ScoreServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String path req.getPathInfo(); // 返回 /list /delete 这类子路径 if (/delete.equals(path)) { delete(req, resp); } else { // /list 和未知路径都走列表避免空指针 list(req, resp); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String path req.getPathInfo(); if (/add.equals(path)) { add(req, resp); } else if (/update.equals(path)) { update(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword req.getParameter(keyword); ListScore list scoreService.findByKeyword(keyword); req.setAttribute(scoreList, list); req.setAttribute(keyword, keyword null ? : keyword); req.getRequestDispatcher(/score_list.jsp).forward(req, resp); } private void delete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String id req.getParameter(id); scoreService.deleteById(Integer.parseInt(id)); // 删除完重定向防止刷新页面重复提交 resp.sendRedirect(req.getContextPath() /score/list); } }说明几个参数和设计选择。WebServlet(/score/*)里的*匹配/score后面的所有子路径getPathInfo()拿到的就是*匹配到的部分比如请求/score/listpath 就是/list。doGet 管查询和删除doPost 管新增和修改符合 HTTP 方法语义也方便前端表单区分 method。req.setCharacterEncoding(UTF-8)放在每个方法开头是双保险虽然后面还会加 Filter但 Servlet 里再设一次能防止某些容器环境下 Filter 顺序异常导致乱码。删除操作用 GET 是课设里常见写法链接点击即删除生产系统必须改成 POST 加确认这个第 5 章还会提。2.4 编码过滤器、重定向与转发两个必设参数整个项目里最容易翻车的不是业务代码是中文乱码和提交后重复刷新。乱码的根子有三个地方请求参数解码、响应编码、数据库连接串字符集。请求和响应层面的问题用一个 Filter 一次解决。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }Filter 把编码统一在入口处处理Servlet 里的setCharacterEncoding就变成了兜底。注意response.setContentType如果每次请求都设会把响应头刷成 text/html如果某个 Servlet 要输出 JSONFilter 里就别写这一行只保留两个 setCharacterEncoding 更稳妥。新增成绩成功后的处理我坚持用重定向而不是转发。看下面这段private void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String studentNo req.getParameter(studentNo); String courseId req.getParameter(courseId); String score req.getParameter(score); int rows scoreService.addScore(studentNo, Integer.parseInt(courseId), Double.parseDouble(score)); // 保存成功重定向到列表刷新页面不会重复提交 resp.sendRedirect(req.getContextPath() /score/list); }重定向是浏览器重新发起一次 GET 请求地址栏变成/score/list此时再按 F5 刷新重新执行的是查询而不是新增。转发是服务器内部跳转浏览器地址栏不变刷新会再次提交表单成绩就会插两条。所以规则是写操作增删改成功后一律重定向读操作查询用转发。req.getContextPath()返回部署上下文名比如/sms拼 URL 时带上它部署时改 war 名不会导致链接全部失效。3. Mysql 表设计与 JDBC 连接池成绩数据怎么存、怎么取3.1 学生成绩系统的四张核心表结构成绩管理系统最少需要四张表学生表、课程表、成绩表、用户表。这里踩过的坑是直接用score和course做表名它们在 MySql 里不是保留字但在某些工具和动态 SQL 拼接时容易和字段名混淆。我习惯给业务表加前缀比如t_score或者统一用反引号包裹。下面这套建表 SQL 是可直接执行的。CREATE DATABASE IF NOT EXISTS sms_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE sms_db; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL COMMENT 课程名, credit DECIMAL(3, 1) DEFAULT 2.0 COMMENT 学分 ); CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, course_id INT NOT NULL COMMENT 课程id, score DECIMAL(5, 1) COMMENT 成绩保留一位小数, exam_date DATE DEFAULT NULL COMMENT 考试日期, UNIQUE KEY uk_student_course (student_no, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_no) REFERENCES t_student (student_no), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course (id) ); CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role TINYINT DEFAULT 1 COMMENT 1管理员 2学生 );几个设计决定背后的原因。成绩字段用DECIMAL(5,1)而不是 DOUBLE浮点数算平均分会有误差DECIMAL 是定点数存 99.5 就是 99.5。uk_student_course唯一键保证同一学生同一课程只能有一条成绩这是业务上最硬的需求就算漏写在 Service 里的判断数据库也能兜住。外键在这个系统里保留没问题数据量小不用考虑删除性能。t_user表单独加t_前缀是因为user在某些 Mysql 版本里是信息模式相关词裸用容易在 Navicat 里报语法高亮问题。3.2 Druid 连接池配置与参数连接池用 Druid 还是 HikariCP 在这个项目里差别不大课设里 Druid 更常见监控页面还能当答辩亮点。配置集中在src/main/resources/druid.properties一行一参数参数含义要能讲清楚。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/sms_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernameroot password你的密码 initialSize5 maxActive20 maxWait30000 minIdle5 validationQuerySELECT 1 testWhileIdletrue timeBetweenEvictionRunsMillis60000连接串里这四个参数缺一不可。characterEncodingutf8保证从数据库读出来的中文不乱码它和 Filter 的 setCharacterEncoding 是两条线缺哪个都白搭。serverTimezoneAsia/Shanghai是给 Mysql 8.0 用的不设的话 JDBC 驱动会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized本质是驱动拿不到 JVM 默认时区对应的数据库时区直接显式指定最省事。useSSLfalse关闭 SSL 握手本地开发没必要加密连接。allowPublicKeyRetrievaltrue是 Mysql 8.0 的 caching_sha2_password 插件要求的不设会报Public Key Retrieval is not allowed。连接池初始化代码固定成工具类整个项目只有这一个地方接触 DataSource。public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException(初始化连接池失败, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }maxWait30000这个参数值得单独说。它是获取连接的最大等待毫秒数默认 -1 表示无限等数据库挂了时线程会全部卡死在getConnection()Tomcat 直接假死。设成 30 秒连不上会快速抛异常错误日志里能看到具体 SQL。testWhileIdletrue配合validationQuerySELECT 1让空闲连接定期探活Mysql 的 wait_timeout 默认 8 小时超过这个时间空闲连接被服务端断开连接池里的连接变成死连接下次取出来用直接报 Communications link failure探活能把这个坑提前排掉。3.3 动态 SQL 拼接按学号/姓名/课程查成绩成绩列表的搜索框要支持按学号、姓名、课程三个字段模糊查询。DAO 层最常见的写法是动态拼接 SQL这里最容易出安全问题必须在 PreparedStatement 里用占位符。public ListScore findByKeyword(String keyword) { StringBuilder sql new StringBuilder( SELECT s.student_no, s.name AS studentName, s.class_name, c.course_name, sc.score, sc.exam_date FROM t_score sc JOIN t_student s ON sc.student_no s.student_no JOIN t_course c ON sc.course_id c.id WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND (s.name LIKE ? OR s.student_no LIKE ? OR c.course_name LIKE ?)); String like % keyword.trim() %; params.add(like); params.add(like); params.add(like); } sql.append( ORDER BY sc.exam_date DESC, s.student_no ASC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { // 封装成 ListScore返回给 Service } } catch (SQLException e) { throw new RuntimeException(查询成绩失败, e); } return Collections.emptyList(); }这里WHERE 11常被说成性能问题其实 Mysql 的优化器会把它直接忽略它的价值是让后面拼AND时不需要判断前面有没有条件。另一种写法是先判断 keyword 非空再拼WHERE代码里会出现两个分支维护起来反而容易漏。AS studentName这个别名是给实体映射用的JDBC 的getObject(studentName)按列标签取值如果查询返回的是s.name列标签就是 name和实体属性对得上但多表查询里name和course_name混在一起起别名能规避掉后续封装时字段错位的麻烦。3.4 连接 Mysql 的典型翻车点报错 2002、SSL、时区开发环境连接 Mysql 的报错一半以上集中在三个地方都属于看日志能定位、不看日志全靠猜的问题。第一个是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。现象是程序连不上命令行 mysql 也连不上。原因分两种Mysql 服务没启动或者客户端找的 socket 路径不对。解决方法是先确认服务Linux 上执行systemctl status mysqld或service mysql statusWindows 上在服务管理器里看 MySql 服务是否运行。确认服务在跑还报这个错就在连接串里把 localhost 换成 127.0.0.1 强制走 TCP绕开 socket 文件路径问题。第二个是 SSL 连接错误。Mysql 8.0 默认开启 SSL 加密连接客户端驱动版本和服务器协议不匹配时会报 SSL handshake 失败。解决就是在 url 里加useSSLfalse本地开发或内网环境这么做没有安全隐患。第三个是时区报错信息里带一串乱码时区名前面已经说过用serverTimezoneAsia/Shanghai解决。还有个小众问题密码里有或#特殊字符时url 里的密码部分要 URL 编码否则参数解析直接把密码截断了这种问题看一眼异常信息里的 url 就能发现。4. JSPBootstrap 做成绩管理界面表格、模态框与分页4.1 用 JSTLEL 在 JSP 里渲染成绩列表JSP 页面最忌讳写% for (int i 0; ...) %这种脚本片段既难看又难维护。常见做法是用 JSTL 核心标签库加 EL 表达式。页面顶部引入标签库列表部分用c:forEach循环。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 title成绩管理/title link href${pageContext.request.contextPath}/css/bootstrap.min.css relstylesheet /head body div classcontainer mt-3 form classrow g-2 mb-3 methodget action${pageContext.request.contextPath}/score/list div classcol-auto input typetext classform-control namekeyword value${keyword} placeholder学号/姓名/课程 /div div classcol-auto button typesubmit classbtn btn-primary查询/button /div /form table classtable table-striped table-hover align-middle thead tr th学号/thth姓名/thth班级/th th课程/thth成绩/thth考试日期/thth操作/th /tr /thead tbody c:forEach items${scoreList} vars tr tdc:out value${s.studentNo}//td tdc:out value${s.studentName}//td tdc:out value${s.className}//td tdc:out value${s.courseName}//td td${s.score}/td td${s.examDate}/td td a href${pageContext.request.contextPath}/score/delete?id${s.id} classbtn btn-sm btn-danger onclickreturn confirm(确定删除这条成绩?);删除/a /td /tr /c:forEach /tbody /table c:if test${empty scoreList} p classtext-muted没有符合条件的成绩记录/p /c:if /div script src${pageContext.request.contextPath}/js/bootstrap.bundle.min.js/script /body /html几个细节值得注意。${pageContext.request.contextPath}这个表达式会输出部署上下文名所有 CSS、JS、表单 action 都拼上它war 包改名为/sms或/sms_war_exploded页面里的路径不用跟着改。c:out转义输出学生姓名里如果含script标签显示出来是纯文本XSS 攻击被挡在渲染层。${keyword}直接放在 value 里回显搜索词用户按 F5 后关键字还在这个体验细节比什么都实用。4.2 Bootstrap 表格与模态框选型手写还是插件成绩管理页面要不要用 Bootstrap Table 插件我的结论是数据量没过万就别用。这个系统的数据量级决定了手写原生table classtable加少量 JS 就够插件的搜索、排序、分页全是给后端大数据准备的引入后还要学它的 API、列配置写法答辩时被问到底层实现往往答不上来。Bootstrap 方法里对这个项目最有用的几样一是表格样式table-striped和table-hover二是栅格系统row和col-auto做查询表单布局三是模态框组件做新增编辑弹窗四是form-control和form-select统一表单控件外观。就这四个覆盖 90% 的界面需求。4.3 新增/编辑成绩的表单与数据回显新增和编辑共用一个模态框是减少重复代码最直接的办法。新增时表单里是空值编辑时用 JS 把对应行的数据塞进表单。注意这里有个选择编辑数据回显走不走第二次请求。不走因为列表页已经有关键数据直接传给 JS 函数即可。button typebutton classbtn btn-success onclickopenAddModal()新增成绩/button div classmodal fade idscoreModal tabindex-1 div classmodal-dialog form idscoreForm classmodal-content methodpost action${pageContext.request.contextPath}/score/add div classmodal-header h5 classmodal-title idmodalTitle新增成绩/h5 button typebutton classbtn-close >servlet servlet-nameInitServlet/servlet-name servlet-classcom.sms.controller.InitServlet/servlet-class load-on-startup1/load-on-startup /servlet这个配置的数值越小优先级越高。用注解开发时对应WebServlet(value /init, loadOnStartup 1)。还有一个并发层面的坑必须知道Servlet 是单实例多线程所有请求共享同一个实例所以在 Servlet 里写实例字段存请求数据会出现 A 请求的数据被 B 请求覆盖的灵异问题。成绩模块里不要在 ScoreServlet 里写private String keyword;这种字段局部变量才是安全的。5.2 传统 JSP 项目打 war 包部署到 Tomcat传统 JSP 项目打包 war常见做法有两条线。项目没有用 Maven 时在 IDEA 里 File → Project Structure → Artifacts → 点加号选 Web Application Archive然后 Build → Build Artifacts 生成 war。项目用 Maven 时直接mvn clean package产物在 target 目录。我一般推荐第二条因为 Maven 还能统一管理 servlet-api、jstl 这些依赖的版本。war 的部署结构是固定的解压后必须是WEB-INF/classes、WEB-INF/lib、页面文件在根目录。把 war 放到tomcat/webapps目录重启 Tomcat 自动解压访问路径就是 war 文件名去掉.war后缀。这里有三个容易踩的地方上下文路径不对导致 404。IDEA 里运行时的路径是项目名_war_exploded部署到 Tomcat 后变成 war 文件名页面里的路径必须全部用${pageContext.request.contextPath}拼接才不会断。数据库配置在druid.properties里打包后文件进了classes/druid.properties上线要改密码时直接改解压目录下的这个文件再重启即可不用重新打包。servlet-api.jar 不能打进 war 的WEB-INF/lib否则和 Tomcat 自带版本冲突启动报java.lang.LinkageError或NoClassDefFoundError。Maven 里 servlet-api 和 jsp-api 的 scope 必须设置为 provided。5.3 开发环境配置vscode 写 servlet 与 idea 新建 jsp 项目被问得最多的是 vscode 编写 servlet 代码需要怎么配置环境。说实话vscode 写 Java Web 是能跑但不好调试的路线适合临时改代码不适合做完整项目。配置最少需要四步装 Red Hat Java 扩展、装 Tomcat for Java 插件、把 Tomcat 目录下的lib/servlet-api.jar和lib/jsp-api.jar加入项目的 classpath、用插件启动 Tomcat 部署当前目录。它能编译能启动但断点调试不如 IDEA 顺手遇到复杂变量排查会很痛苦。IDEA 新建 jsp 项目我更推荐路径是 New Project → Java Enterprise → 勾选 Web ApplicationIDEA 自动生成web/WEB-INF/web.xml和 index.jsp。如果是 Maven 项目用maven-archetype-webapp骨架创建IDEA 会补全目录结构。开发时注意 Tomcat 的版本要和本地 JDK 匹配Tomcat 9 配 Java 8/11 没问题配 Java 17 要小心类模块限制遇到java.lang.reflect.InaccessibleObjectException时在启动 VM 参数加--add-opens java.base/java.langALL-UNNAMED。5.4 避坑清单开发到部署的五条血泪经验以下是这个项目里最常复现的五类问题每一条都是现象先行原因跟上解决收尾。1. 现象JSP 页面中文全部乱码。原因JSP 文件本身是 UTF-8 编码但容器用默认的 ISO-8859-1 解码页面声明和实际编码不一致。解决JSP 第一行page指令写全contentTypetext/html;charsetUTF-8 pageEncodingUTF-8项目里所有 JSP 统一这两个属性加上第 2 章的 Filter 双重保障乱码问题基本绝迹。2. 现象部署后请求数据库报Communications link failure。原因Mysql 服务没启动或者连接池里的连接因为 wait_timeout 变成死连接。解决先命令行mysql -uroot -p验证服务再检查 Druid 的testWhileIdle是否开启。注意 Mysql 8.0 的默认 wait_timeout 和连接池探活周期要匹配探活间隔小于数据库断开时间就不会踩。3. 现象删除成绩记录后再新增同学生同课程报Duplicate entry。原因表上的唯一键在拦截但没有被友好处理页面显示 500。解决在 add 的 Service 里捕获SQLIntegrityConstraintViolationException把它转换成业务提示返回该学生此课程成绩已存在这是数据库约束对业务最好的保护。4. 现象war 包部署到 Tomcat 后访问全部 404但 IDEA 里运行正常。原因上下文路径变了。IDEA 运行时的路径包含_war_explodedTomcat 部署后是 war 文件名。解决所有跳转路径用req.getContextPath()或${pageContext.request.contextPath}拼接不要在代码里写死/sms/score/list。5. 现象Control 台报ClassNotFoundException: javax.servlet.http.HttpServlet。原因编译期依赖了 servlet-api但运行时没有提供或者打进了 war 和 Tomcat 自带的冲突。解决确认 Tomcat 的 lib 目录存在 servlet-api.jarMaven 依赖里 scope 写 providedIDEA Artifacts 打包时不把该 jar 放进 WEB-INF/lib。6. 把成绩管理系统往前推一步统计报表、导出 Excel 与手工验证成绩系统做到能增删改查只是及格想让它有说服力加两个小而实的功能班级成绩统计和 Excel 导出。统计接口的 SQL 是加分项里的核心按班级分组算平均分和不及格人数一次查询出结果。SELECT s.class_name, ROUND(AVG(sc.score), 1) AS avg_score, COUNT(*) AS total_cnt, SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) AS fail_cnt FROM t_score sc JOIN t_student s ON sc.student_no s.student_no GROUP BY s.class_name ORDER BY avg_score DESC;然后在这个 Servlet 里把查询结果渲染成一个小页面用 Bootstrap 的卡片和表格展示不用引图表库。如果答辩想炫一点可以花半小时接一个 ECharts 柱状图数据源就是上面这个 JSON 接口。更实用的是 Excel 导出用 Apache POI 的 XSSFWorkbook三四十行代码把成绩列表生成 xlsx设置response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)和Content-Disposition头浏览器直接下载。这个小功能每次答辩都会被老师当亮点追问成本却很低。做完功能一定要有一份手工测试清单别只验证正常路径。我每次验收这个系统会固定跑一遍新增成绩后列表总数加一总分正确学号或成绩留空时浏览器校验拦截绕过浏览器直接 POST 时服务端也拒绝删除同一条记录两次第二次列表没变化且不报错搜索张能返回所有姓张学生的全部课程成绩停止 Mysql 服务后访问列表页面显示友好错误而不是 Tomcat 白页成绩边界值 0 和 100 都能保存99.5 这种 0.5 步进值正常显示。这套清单跑完系统才算真正能交付。这个技术栈我做得越多越有一个体会老四样项目真正的价值不在技术新而在链路短、可变因素少一套代码能从建库一路讲到浏览器渲染中间每一步都是能亲眼看到结果的。我最早做课程设计时把所有 SQL 都堆在 Servlet 里改一个查询条件要翻三個方法后来规规矩矩分了层才发现 90% 的问题都出在参数没对上和编码没统一这两件事上。希望这篇笔记能帮你少走这段弯路把精力留在真正能学到东西的地方。本文还有配套的精品资源点击获取
返回列表