news 2026/9/28 8:23:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Web老四样技术栈实战:Servlet+JSP+Bootstrap+Mysql学生成绩管理系统

简介:这是一套基于Java技术栈的学生成绩管理系统完整项目,采用Servlet+JSP+Bootstrap+MySQL四层协作,面向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老四样技术栈:现在还能打吗

用 Java+Servlet+JSP+Bootstrap+Mysql 这套组合去做学生成绩管理系统,放在今天看像考古,但课程设计和毕业设计里这种 Java Web 老四样项目一直是重灾区:老师要一个能跑、能讲、能改的完整案例,学生要一份自己能说清原理的源码。这个系统解决的就是典型增删改查场景——老师录入成绩、学生按学号查分、按班级统计平均分和不及格人数,技术栈恰好覆盖从数据库到浏览器的一条完整链路。适合 Java 基础刚入门、需要完整案例源码做课设或基于 JSP 的毕设选题的人,也适合想补 Java Web 基本功的初级开发。这套组合还有个附加价值:它涉及的问题基本也是 java 面试八股文里的高频考点。

2. 从数据库到页面的请求链路:Servlet+JSP 的核心分工

2.1 三层架构与包结构:DAO、Service、Servlet、JSP 各管什么

学生成绩管理系统规模不大,但我不建议把所有代码堆在一个 Servlet 里。见过太多课设源码是ScoreServlet里既拼 SQL 又输出 HTML,改一个字段要在三个页面来回找。常见做法是分 Controller(Servlet)、Service、DAO、View(JSP)四层,包结构固定下来,后面加功能只用对号入座。

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 里不出现 SQL,DAO 里不出现业务判断,JSP 里不写 Java 脚本片段。Service 这一层在成绩系统里主要管两件事,一是校验成绩数值在 0 到 100 之间,二是判断同学生同课程是否已经录过成绩。把这两段逻辑放在 DAO 里也能跑,但后面加业务规则时,Service 层是唯一的改动点,不会牵动页面。

2.2 一个成绩查询请求的完整流转

用户在前端输入关键字点查询,请求在系统里走这样一条链:

  1. 浏览器 GET 请求/score/list?keyword=张三,带上下文路径,比如http://localhost:8080/sms/score/list。
  2. Tomcat 根据@WebServlet("/score/*")的映射,把请求交给ScoreServlet。
  3. ScoreServlet.doGet解析keyword参数,调用ScoreService.findByKeyword(keyword)。
  4. ScoreService判断 keyword 是否为空、是否要拼模糊查询,再调用ScoreDao.findByKeyword(keyword)。
  5. ScoreDaoImpl用 PreparedStatement 执行带 LIKE 的 SQL,从 Mysql 查出结果,封装成List<Score>。
  6. Servlet 把列表放到 request 作用域,request.getRequestDispatcher("/score_list.jsp").forward(req, resp)转发到 JSP。
  7. JSP 用 JSTL 遍历列表,渲染成 Bootstrap 表格,响应回浏览器。

这里有两个关键点。转发和重定向的区别后面单独说,先记住查询结果用转发,因为 JSP 需要拿到 request 里的scoreList属性。另外 Servlet 在整个过程里只做参数接收和视图调度,它不该知道 SQL 长什么样。这个链路在面试里也很常被追问,能画清楚这条线,Servlet 生命周期和 MVC 两题基本就稳了。

2.3 核心 Servlet 代码:doGet/doPost 与路由设计

成绩模块的 Servlet 我习惯用一个类处理列表、新增、修改、删除四个操作,用 URL 路径区分动作,而不是用?action=xxx的查询参数。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"); List<Score> 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/list,path 就是/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;charset=UTF-8"); chain.doFilter(request, response); } }

Filter 把编码统一在入口处处理,Servlet 里的setCharacterEncoding就变成了兜底。注意response.setContentType如果每次请求都设,会把响应头刷成 text/html,如果某个 Servlet 要输出 JSON,Filter 里就别写这一行,只保留两个 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,一行一参数,参数含义要能讲清楚。

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/sms_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username=root password=你的密码 initialSize=5 maxActive=20 maxWait=30000 minIdle=5 validationQuery=SELECT 1 testWhileIdle=true timeBetweenEvictionRunsMillis=60000

连接串里这四个参数缺一不可。characterEncoding=utf8保证从数据库读出来的中文不乱码,它和 Filter 的 setCharacterEncoding 是两条线,缺哪个都白搭。serverTimezone=Asia/Shanghai是给 Mysql 8.0 用的,不设的话 JDBC 驱动会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,本质是驱动拿不到 JVM 默认时区对应的数据库时区,直接显式指定最省事。useSSL=false关闭 SSL 握手,本地开发没必要加密连接。allowPublicKeyRetrieval=true是 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(); } }

maxWait=30000这个参数值得单独说。它是获取连接的最大等待毫秒数,默认 -1 表示无限等,数据库挂了时线程会全部卡死在getConnection(),Tomcat 直接假死。设成 30 秒,连不上会快速抛异常,错误日志里能看到具体 SQL。testWhileIdle=true配合validationQuery=SELECT 1,让空闲连接定期探活,Mysql 的 wait_timeout 默认 8 小时,超过这个时间空闲连接被服务端断开,连接池里的连接变成死连接,下次取出来用直接报 Communications link failure,探活能把这个坑提前排掉。

3.3 动态 SQL 拼接:按学号/姓名/课程查成绩

成绩列表的搜索框要支持按学号、姓名、课程三个字段模糊查询。DAO 层最常见的写法是动态拼接 SQL,这里最容易出安全问题,必须在 PreparedStatement 里用占位符。

public List<Score> 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 1=1"); List<Object> 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()) { // 封装成 List<Score>,返回给 Service } } catch (SQLException e) { throw new RuntimeException("查询成绩失败", e); } return Collections.emptyList(); }

这里WHERE 1=1常被说成性能问题,其实 Mysql 的优化器会把它直接忽略,它的价值是让后面拼AND时不需要判断前面有没有条件。另一种写法是先判断 keyword 非空再拼WHERE,代码里会出现两个分支,维护起来反而容易漏。AS studentName这个别名是给实体映射用的,JDBC 的getObject("studentName")按列标签取值,如果查询返回的是s.name,列标签就是 name,和实体属性对得上;但多表查询里name和course_name混在一起,起别名能规避掉后续封装时字段错位的麻烦。

3.4 连接 Mysql 的典型翻车点:报错 2002、SSL、时区

开发环境连接 Mysql 的报错,一半以上集中在三个地方,都属于看日志能定位、不看日志全靠猜的问题。

第一个是ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。现象是程序连不上,命令行 mysql 也连不上。原因分两种,Mysql 服务没启动,或者客户端找的 socket 路径不对。解决方法是先确认服务:Linux 上执行systemctl status mysqld或service mysql status,Windows 上在服务管理器里看 MySql 服务是否运行。确认服务在跑还报这个错,就在连接串里把 localhost 换成 127.0.0.1 强制走 TCP,绕开 socket 文件路径问题。

第二个是 SSL 连接错误。Mysql 8.0 默认开启 SSL 加密连接,客户端驱动版本和服务器协议不匹配时,会报 SSL handshake 失败。解决就是在 url 里加useSSL=false,本地开发或内网环境这么做没有安全隐患。

第三个是时区报错,信息里带一串乱码时区名,前面已经说过用serverTimezone=Asia/Shanghai解决。还有个小众问题,密码里有@或#特殊字符时,url 里的密码部分要 URL 编码,否则参数解析直接把密码截断了,这种问题看一眼异常信息里的 url 就能发现。

4. JSP+Bootstrap 做成绩管理界面:表格、模态框与分页

4.1 用 JSTL+EL 在 JSP 里渲染成绩列表

JSP 页面最忌讳写<% for (int i = 0; ...) %>这种脚本片段,既难看又难维护。常见做法是用 JSTL 核心标签库加 EL 表达式。页面顶部引入标签库,列表部分用c:forEach循环。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>成绩管理</title> <link href="${pageContext.request.contextPath}/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container mt-3"> <form class="row g-2 mb-3" method="get" action="${pageContext.request.contextPath}/score/list"> <div class="col-auto"> <input type="text" class="form-control" name="keyword" value="${keyword}" placeholder="学号/姓名/课程"> </div> <div class="col-auto"> <button type="submit" class="btn btn-primary">查询</button> </div> </form> <table class="table table-striped table-hover align-middle"> <thead> <tr> <th>学号</th><th>姓名</th><th>班级</th> <th>课程</th><th>成绩</th><th>考试日期</th><th>操作</th> </tr> </thead> <tbody> <c:forEach items="${scoreList}" var="s"> <tr> <td><c:out value="${s.studentNo}"/></td> <td><c:out value="${s.studentName}"/></td> <td><c:out value="${s.className}"/></td> <td><c:out value="${s.courseName}"/></td> <td>${s.score}</td> <td>${s.examDate}</td> <td> <a href="${pageContext.request.contextPath}/score/delete?id=${s.id}" class="btn btn-sm btn-danger" onclick="return confirm('确定删除这条成绩?');">删除</a> </td> </tr> </c:forEach> </tbody> </table> <c:if test="${empty scoreList}"> <p class="text-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 class="table">加少量 JS 就够,插件的搜索、排序、分页全是给后端大数据准备的,引入后还要学它的 API、列配置写法,答辩时被问到底层实现往往答不上来。Bootstrap 方法里对这个项目最有用的几样,一是表格样式table-striped和table-hover,二是栅格系统row和col-auto做查询表单布局,三是模态框组件做新增编辑弹窗,四是form-control和form-select统一表单控件外观。就这四个,覆盖 90% 的界面需求。

4.3 新增/编辑成绩的表单与数据回显

新增和编辑共用一个模态框,是减少重复代码最直接的办法。新增时表单里是空值,编辑时用 JS 把对应行的数据塞进表单。注意这里有个选择:编辑数据回显走不走第二次请求。不走,因为列表页已经有关键数据,直接传给 JS 函数即可。

<button type="button" class="btn btn-success" onclick="openAddModal()">新增成绩</button> <div class="modal fade" id="scoreModal" tabindex="-1"> <div class="modal-dialog"> <form id="scoreForm" class="modal-content" method="post" action="${pageContext.request.contextPath}/score/add"> <div class="modal-header"> <h5 class="modal-title" id="modalTitle">新增成绩</h5> <button type="button" class="btn-close" ><servlet> <servlet-name>InitServlet</servlet-name> <servlet-class>com.sms.controller.InitServlet</servlet-class> <load-on-startup>1</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后缀。这里有三个容易踩的地方:

  1. 上下文路径不对导致 404。IDEA 里运行时的路径是项目名_war_exploded,部署到 Tomcat 后变成 war 文件名,页面里的路径必须全部用${pageContext.request.contextPath}拼接才不会断。
  2. 数据库配置在druid.properties里,打包后文件进了classes/druid.properties,上线要改密码时直接改解压目录下的这个文件再重启即可,不用重新打包。
  3. 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 Application,IDEA 自动生成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.lang=ALL-UNNAMED。

5.4 避坑清单:开发到部署的五条血泪经验

以下是这个项目里最常复现的五类问题,每一条都是现象先行,原因跟上,解决收尾。

1. 现象:JSP 页面中文全部乱码。
原因:JSP 文件本身是 UTF-8 编码,但容器用默认的 ISO-8859-1 解码,页面声明和实际编码不一致。
解决:JSP 第一行page指令写全contentType="text/html;charset=UTF-8" pageEncoding="UTF-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_exploded,Tomcat 部署后是 war 文件名。
解决:所有跳转路径用req.getContextPath()或${pageContext.request.contextPath}拼接,不要在代码里写死/sms/score/list。

5. 现象:Control 台报ClassNotFoundException: javax.servlet.http.HttpServlet。
原因:编译期依赖了 servlet-api,但运行时没有提供,或者打进了 war 和 Tomcat 自带的冲突。
解决:确认 Tomcat 的 lib 目录存在 servlet-api.jar,Maven 依赖里 scope 写 provided,IDEA 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% 的问题都出在参数没对上和编码没统一这两件事上。希望这篇笔记能帮你少走这段弯路,把精力留在真正能学到东西的地方。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 8:22:57

SVR回归预测小样本仿真数据:训练、调参与模型保存实战

简介&#xff1a;面向机器学习与数据预测场景的SVR回归实战资源包&#xff0c;聚焦支持向量回归模型的构建、训练、保存与加载预测全流程&#xff0c;适合需要快速上手SVR或完成回归任务的中初级开发者&#xff0c;也可作为课设或小型项目的参考。资源包含完整Python脚本与预处…

作者头像 李华
网站建设 2026/9/28 8:22:21

AI Agent生产级落地能力体检:Hermes、Claude Code与Codex实战解析

1. 项目概述&#xff1a;这不是一份榜单&#xff0c;而是一份AI Agent落地能力的体检报告 “9月AI Agent排行&#xff1a;Hermes第一&#xff0c;Claude Code、Codex进前十”——看到这个标题&#xff0c;我第一反应不是点开看名次&#xff0c;而是下意识翻出自己上个月在客户…

作者头像 李华
网站建设 2026/9/28 8:22:07

STM32 LIN总线实战:从CubeMX配置到波形调试的完整指南

1. 为什么LIN总线在车身电子里一直没被CAN取代如果你拆过车门模块、雨量传感器、座椅调节开关或者车内氛围灯&#xff0c;大概率会看到两根绞在一起的细线&#xff0c;一根叫LIN&#xff0c;一根搭铁。很多刚入行的朋友第一反应是"这不就是低速CAN吗"&#xff0c;其实…

作者头像 李华
网站建设 2026/9/28 8:21:30

CentOS 7安装LibreOffice与中文字体乱码处理实战

1. 先搞清楚&#xff1a;CentOS 7上装LibreOffice&#xff0c;到底难在哪CentOS 7服务器上装LibreOffice这件事&#xff0c;不少人第一反应是&#xff1a;不就是一个 yum install 的事吗&#xff1f;真这么简单就好了。我见过太多人在内网服务器上装完LibreOffice&#xff0c;开…

作者头像 李华
网站建设 2026/9/28 8:20:16

基于Python爬虫技术实现歌曲评论数据分析与可视化设计大数据专业毕业设计大数据分析项目案例

本文组织内容1. 绪论&#xff1a;本文首先介绍了音乐评论分析的背景和意义&#xff0c;阐述了通过分析服装领域评论来了解顾客体验和满意度的的重要性。同时&#xff0c;本文还概述了国内外对音乐评论领域的研究现状&#xff0c;并提出了本文的研究目的和研究问题。2. 技术介绍…

作者头像 李华