拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Servlet+JSP+MySQL学生成绩管理系统开发实战教程

Servlet+JSP+MySQL学生成绩管理系统开发实战教程

简介:基于Servlet+JSP+MySQL构建的JavaWeb学生成绩管理系统,是原项目升级版,重点新增ECharts统计图功能,适用于正在学习JavaWeb的学生,以及进行课程设计、毕业设计的开发者。系统围绕学生成绩管理核心场景,涉及教师、学生、课程、成绩等业务对象,提供成绩录入、查询、修改、删除及可视化统计展示能力,可帮助理解Servlet与JSP协作方式、数据库连接与操作、前端交互和图表渲染等关键知识点。资源包共包含344个文件,压缩包仅3.59MB,其中以86个Java源文件及对应class编译文件、37个JSP页面、62个JS脚本、28个CSS样式、6个Jar依赖包为主,另含SQL建表脚本与XML配置;Java用于业务逻辑,JSP负责页面生成,JS/CSS完善前端交互与样式,SQL脚本可直接导入MySQL。已有6723人学习/下载。除源码外,资源还展示了ECharts统计图在成绩分析中的具体用法,可将学生成绩转换为柱状图、饼图、折线图等,便于直观查看班级或课程分布;Java部分按Dao、Servlet等分层组织,导入数据库后即可运行并二次修改,是期末大作业或毕业设计的实用参考。

1. 如果接到“一周内上线学生成绩管理系统”的需求,我不会先上 Spring Boot

如果接到“一周内上线一个学生成绩管理系统”的需求,我不会第一反应就搬出 Spring Boot,而是先评估 Servlet + JSP + MySQL 的传统 JavaWeb 组合,统计图表用 ECharts 直接嵌进页面里。这套项目就是这么搭的:登录、权限拦截、学生信息维护、成绩增删改查、按课程统计平均分与及格率,JSP 负责页面展示,Servlet 负责接收请求并调用 DAO,数据全部落在 MySQL。对课程设计、毕业设计来说,这个体量刚好能把 MVC 分层讲清楚;对刚学完 JavaWeb、想找一个完整链路练手的人,它也是一个能跑、能改、能断点跟进去的起点。代码在自己手里,每一层都看得见,出了问题不像在 Spring 容器里那样摸不着头脑。

2. 架构与技术选型:Servlet+JSP+MySQL 的组合到底好在哪里

这块项目不需要微服务、不需要注册中心,甚至不需要 MyBatis 这类 ORM 框架。Servlet 负责接收请求,JSP 负责输出动态页面,MySQL 负责持久化,ECharts 负责图表渲染,四个组件拼起来就是一套完整的成绩管理闭环。尤其适合放大学生管理系统这类角色单一、接口数量在二十个以内的系统。

在真正动手改代码之前,我建议先把工程里每一个目录的作用搞清楚,否则会出现“知道怎么写,但不知道放哪”的问题。这一章就按项目的真实结构拆一遍 Servlet 生命周期和包划分,相当于先看地图再进门。

2.1 三层职责与 Servlet 生命周期:请求从 URL 到业务方法的完整路径

先把整个请求链路线性走一遍:浏览器发请求 → Tomcat 读取web.xml或@WebServlet注解上的 URL 匹配 → 找到对应的 Servlet → 首次访问时执行init()初始化 → 之后每次请求都会调用service()→ 按 GET/POST 自动派发到doGet()或doPost()→ 方法里调用 DAO 查数据库 → 把结果放进 request 或 session → 转发到 JSP → JSP 渲染成 HTML 返回浏览器。Servlet 生命周期说透了就是这三步:init只做一次,适合建立连接和读取配置;service每次请求都会触发,Tomcat 内部会根据请求方法自动分派;destroy在容器卸载时执行,适合释放连接资源。

很多 JSP 入门教程讲完这几个方法就停了,但这个项目把整条链路完整地串了起来——你在 IDE 里给 Servlet 打断点,可以看到请求怎么一步步走到数据库,再原路返回页面,中间任何一个环节出错都能在调用栈里定位。

传统项目里有两种注册 Servlet 的方式。一种是在类上直接写注解:

@WebServlet("/login") public class LoginServlet extends HttpServlet { // doGet / doPost 处理逻辑... }

另一种是在web.xml里声明映射:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.student.controller.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> </web-app>

建议新手两种方式都认识:注解方式写起来快,web.xml方式在旧项目和某些部署环境里仍然会遇到。这个项目中welcome-file设成了login.jsp,意思是访问项目根路径时自动打开登录页;url-pattern写/login,浏览器请求http://localhost:8080/项目名/login就会进入 LoginServlet。注意两处路径必须完全一致,否则 Tomcat 启动时可能直接报 mapper 冲突。

2.2 包结构划分:controller、dao、entity、filter 各管一摊

我拿到一个 JSP 项目,第一件事就是看包结构。这个项目的目录划分比较规整,是典型的三层分包方式:

src/main/java/com/student ├── controller/ # Servlet,只做参数接收、调用和跳转 │ ├── LoginServlet.java │ ├── ScoreServlet.java │ └── StatsServlet.java ├── dao/ # 数据访问层,负责 JDBC 和 SQL │ ├── UserDao.java │ ├── ScoreDao.java │ └── BaseDao.java ├── entity/ # 实体类,与表结构对应 │ ├── User.java │ └── Score.java ├── filter/ # 过滤器,统一做登录拦截 │ └── AuthFilter.java src/main/webapp ├── login.jsp ├── index.jsp ├── score/ │ └── list.jsp ├── stats/ │ └── chart.jsp └── static/ ├── css/ └── js/ └── echarts.min.js

这套分层的约定并不复杂:controller 层不写 SQL,只负责从 request 里取参数、调 DAO、往 request 里放数据、决定跳转到哪个 JSP;dao 层不关心页面长什么样,只负责拿连接、执行 SQL、把 ResultSet 转成实体对象;entity 层就是纯属性加 getter/setter,字段尽量与数据库列一一对应;filter 层统一拦截请求,判断用户有没有登录。静态资源单独放static目录,避免和 JSP 页面混在一起。

数据流向同样很直观。用户提交表单到/login,LoginServlet 用req.getParameter("username")拿到用户名,调到 UserDao 执行 SELECT,比对密码;成功之后把 User 对象放进 session,页面跳转到index.jsp。成绩列表页也是同样套路:ScoreServlet 接收pageNum和keyword,ScoreDao 拼分页 SQL,查询得到List<ScoreVO>,调用setAttribute("pageBean", ...)后转发到score/list.jsp,JSP 里用 JSTL 的<c:forEach>循环渲染成表格。

有一点需要提前记住:在 IDEA 里部署这种传统项目时,项目的 Web Facet 必须把src/main/webapp配置为 Web Resource Directory,否则 Tomcat 找不到 JSP,启动后访问全是 404。这个问题在后面避坑章节还会展开。

3. 数据库设计与初始化:三张表加一个连接池,先把数据底座打好

成绩管理系统的业务逻辑不复杂,但数据库设计如果偷懒,后面写 DAO 时会非常难受。常见做法是拆成四张表:用户表、学生表、课程表、成绩表,成绩表作为关联核心,把学生和课程连起来。这一章给出表结构设计、初始化 SQL,以及 JDBC 连接池的关键参数。

3.1 表结构设计:登录用户、学生、课程与成绩怎么落表

先看整体关系。sys_user表存登录账号,student表存学生基础信息,course表存课程信息,score表存成绩记录,一行为“某个学生在某门课上的某次成绩”。四张表的关系如下:

表名关键字段说明
sys_userid, username, password, role登录与权限判断,role 可区分管理员/学生
studentid, stu_no, name, class_name学号唯一,class_name 用于按班级统计
courseid, course_name, teacher课程维度,挂授课教师
scoreid, student_id, course_id, score, exam_time学生与课程多对多关系的落地点

成绩表里的student_id和course_id建议加上外键约束,从数据库层面保证不会插入一段不存在的学生成绩。class_name字段在校内系统里查询频率很高,可以单独建索引,按班级筛选成绩时就不会全表扫描。score字段用DECIMAL(5,1),能存 0.0 到 999.9,对学生成绩足够用,还保留一位小数。

3.2 初始化 SQL 脚本:建库、建表、插入测试数据

建库和建表脚本建议一次执行完,字符集统一用utf8mb4,避免后面中文乱码问题:

CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(16) DEFAULT 'admin' ) ENGINE=InnoDB; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(32) NOT NULL UNIQUE, name VARCHAR(64) NOT NULL, class_name VARCHAR(128), INDEX idx_class_name (class_name) ) ENGINE=InnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(128) NOT NULL, teacher VARCHAR(64) ) ENGINE=InnoDB; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1), exam_time DATE, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB; INSERT INTO sys_user (username, password, role) VALUES ('admin', '123456', 'admin'); INSERT INTO student (stu_no, name, class_name) VALUES ('2023001', '张伟', '计科2201'), ('2023002', '李娜', '计科2201'); INSERT INTO course (course_name, teacher) VALUES ('JavaWeb', '陈老师'), ('数据结构', '王老师'); INSERT INTO score (student_id, course_id, score, exam_time) VALUES (1, 1, 85.5, '2024-06-20'), (2, 1, 92.0, '2024-06-20');

脚本里NOT NULL和UNIQUE是最值得关注的约束。学号stu_no加上唯一约束,从数据库层面杜绝重复学号;用户名username加唯一约束,登录冲突问题直接消失。外键约束保证成绩记录引用的学生和课程必须真实存在,但注意后续删除学生时会被外键挡住,所以业务里删除学生前需要先处理成绩记录。

连接数据库的方式,传统 JSP 项目普遍用连接池。这里用 c3p0 配置做示例,c3p0-config.xml放在src/main/resources下:

<c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/student_db?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</property> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <property name="user">root</property> <property name="password">123456</property> <property name="initialPoolSize">3</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">15</property> </default-config> </c3p0-config>

这份配置里最容易被忽略的是 URL 参数组合。serverTimezone=Asia/Shanghai解决 MySQL 8 的时区报错;useSSL=false关掉 SSL 握手,本地开发能明显减少连接耗时;allowPublicKeyRetrieval=true是为了兼容 MySQL 8 默认的caching_sha2_password认证插件,不加的话可能报Public Key Retrieval is not allowed。驱动类必须写com.mysql.cj.jdbc.Driver,而不是 MySQL 5 时代的com.mysql.jdbc.Driver,否则直接 ClassNotFoundException。

连接池数值按课设项目体量来即可:初始 3 个连接、最大 15 个连接已经足够。不要把maxPoolSize调得很大,本机 MySQL 默认连接数有限,学生机房这种共享数据库环境里,连接池过大反而会拖垮服务。

4. 核心功能与可视化:登录、成绩 CRUD 与 ECharts 统计图

这一章进入代码主干。前半部分讲登录和 Session 控制,中间讲成绩查询分页与增删改查,最后落地 ECharts 统计图。这三块是整个项目最核心的可复用代码,能直接抄进你自己的课设里。

4.1 Session 登录控制:从 LoginServlet 到 AuthFilter 的两层保护

登录逻辑不复杂,核心是“查库比对 + 写入 Session”。以 LoginServlet 为例:

@WebServlet("/login") public class LoginServlet extends HttpServlet { private final UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || username.isEmpty() || password == null || password.isEmpty()) { req.setAttribute("msg", "用户名或密码不能为空"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { // 登录成功,把用户对象放进 Session,后续页面靠它判断身份 req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

这里有几个细节值得注意。setCharacterEncoding("UTF-8")必须放在第一次读取参数之前,否则 POST 参数中文会乱码;req.getSession()如果 Session 不存在会自动创建,不需要先判断;登录成功用sendRedirect而不是forward,目的是让浏览器地址栏变成index.jsp,用户刷新页面不会重复提交登录表单;req.getContextPath()取到项目上下文路径,部署时如果项目名变化,代码不用跟着改。

光有登录还不能拦截未登录访问,需要一个过滤器统一把关:

@WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 放行登录页、登录接口、静态资源,避免死循环 String path = req.getRequestURI().substring(req.getContextPath().length()); if (path.equals("/login.jsp") || path.equals("/login") || path.startsWith("/static/") || path.equals("/favicon.ico")) { chain.doFilter(request, response); return; } Object user = req.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,强制回登录页 resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 已登录,放行到目标资源 chain.doFilter(request, response); } }

过滤器里最容易翻车的是静态资源拦截。如果/*把 CSS、JS、ECharts 文件全部拦截,未登录时访问登录页会连带把样式表拦截掉,页面丑陋不说,加载 ECharts 也会失败,图表渲染不出来,第一反应往往以为是 ECharts 本身的问题。所以路径判断里必须放行/static/。chain.doFilter(request, response)放行之后,后面的 Servlet 或 JSP 才会被执行。

4.2 成绩分页查询与增删改:PageBean 加 PreparedStatement 的常规写法

成绩列表不能一次性把几千行全查出来,一般用 PageBean 封装分页数据。分页查询的通用结构如下:

public class PageBean<T> { private int pageNum; // 当前页码 private int pageSize; // 每页显示条数 private long total; // 总记录数 private List<T> list; // 当前页的数据 public int getOffset() { // MySQL 分页从 0 开始,所以页码要减 1 return (pageNum - 1) * pageSize; } // getter / setter 省略 }

ScoreDao 里的分页查询方法,核心是“先查总数,再查当前页”:

public PageBean<ScoreVO> findPage(int pageNum, int pageSize, String keyword) { PageBean<ScoreVO> pageBean = new PageBean<>(); pageBean.setPageNum(pageNum); pageBean.setPageSize(pageSize); String countSql = "SELECT COUNT(*) FROM score"; String listSql = "SELECT s.stu_no, s.name, c.course_name, " + "sc.score, sc.exam_time " + "FROM score sc " + "JOIN student s ON sc.student_id = s.id " + "JOIN course c ON sc.course_id = c.id "; // 关键词不为空时,按学生姓名或学号模糊筛选 StringBuilder where = new StringBuilder(" WHERE 1=1 "); if (keyword != null && !keyword.trim().isEmpty()) { where.append(" AND (s.name LIKE ? OR s.stu_no LIKE ?) "); } // 第一步:查总数 long total = queryCount(countSql + where); pageBean.setTotal(total); // 第二步:查当前页数据,LIMIT 两个参数分别是偏移量和条数 listSql += where + " ORDER BY sc.exam_time DESC, sc.id DESC LIMIT ?, ?"; List<ScoreVO> list = queryList(listSql, keyword, pageBean.getOffset(), pageSize); pageBean.setList(list); return pageBean; }

我一般建议分页 SQL 加上排序字段,比如ORDER BY sc.exam_time DESC, sc.id DESC,否则数据相同时间时翻页顺序会乱。LIMIT ?, ?里第一个是偏移量,第二个是每页条数;查询总数和查询列表分两步执行,成绩系统数据量不大,不需要额外加事务,但如果做订单类系统,这两条 SQL 就应该放进同一个事务里保证一致性。

新增、修改、删除的方法结构基本一致,重点看这一组:

// 新增成绩 public int insertScore(Score score) { String sql = "INSERT INTO score (student_id, course_id, score, exam_time) " + "VALUES (?, ?, ?, ?)"; // 用 PreparedStatement 传参,防止 SQL 注入 return update(sql, score.getStudentId(), score.getCourseId(), score.getScore(), score.getExamTime()); } // 修改成绩 public int updateScore(Score score) { String sql = "UPDATE score SET score = ?, exam_time = ? WHERE id = ?"; return update(sql, score.getScore(), score.getExamTime(), score.getId()); } // 删除成绩 public int deleteById(int id) { String sql = "DELETE FROM score WHERE id = ?"; return update(sql, id); }

我一般会把update方法抽到 BaseDao 里统一处理,它内部创建 PreparedStatement、执行 executeUpdate、关闭资源。所有传参一律用?占位符,杜绝字符串拼接 SQL,这已经是传统 JSP 项目里最基本的底线。新增或修改完成后,Servlet 里再搞一次resp.sendRedirect("list?pageNum=1"),页面刷新后回到列表第一页,不会出现表单重复提交。

4.3 ECharts 统计图:后端返回 JSON,前端配置柱状图、饼图和折线图

成绩系统里最提气的功能是统计图,这也是这个项目新增的部分。后端不直接渲染图表,而是返回 JSON 数据,前端用 ECharts 配置项去画图,两边解耦,想改图表样式不用动 Java 代码。

后端统计接口的 Servlet 大致是这样:

@WebServlet("/stats/score") public class ScoreStatsServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 指定返回 JSON,并强制 UTF-8,否则前端拿到的中文会是乱码 resp.setContentType("application/json;charset=UTF-8"); // scoreDao.countByScoreRange() 返回: // [{range: '90-100', count: 5}, {range: '80-89', count: 12}, ...] List<Map<String, Object>> stats = scoreDao.countByScoreRange(); // 简单手动拼 JSON,字段少时够用 StringBuilder json = new StringBuilder(); json.append("{\"ranges\":["); for (int i = 0; i < stats.size(); i++) { if (i > 0) json.append(","); json.append("\"").append(stats.get(i).get("range")).append("\""); } json.append("],\"counts\":["); for (int i = 0; i < stats.size(); i++) { if (i > 0) json.append(","); json.append(stats.get(i).get("count")); } json.append("]}"); resp.getWriter().write(json.toString()); } }

统计页面chart.jsp里,先用<script>引入本地 ECharts 文件,再用fetch请求这个接口:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <! DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>成绩统计</title> <!-- 路径前必须拼 contextPath,部署后项目名变化也不会错 --> <script src="<%=request.getContextPath()%>/static/js/echarts.min.js"></script> </head> <body> <div id="scoreChart" style="width: 760px; height: 420px;"></div> <script> // 1. 请求后端统计接口 fetch('<%=request.getContextPath()%>/stats/score') .then(res => res.json()) .then(data => { // 2. 初始化图表 var chart = echarts.init(document.getElementById('scoreChart')); chart.setOption({ title: { text: '成绩分数段分布', left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.ranges // ['90-100','80-89','70-79','60-69','60以下'] }, yAxis: { type: 'value' }, series: [{ name: '人数', type: 'bar', data: data.counts, itemStyle: { // 按分数段设置不同颜色 color: function (params) { var colors = ['#1E90FF', '#91CC75', '#FAC858', '#EE6666', '#888']; return colors[params.dataIndex % colors.length]; } } }] }); }) .catch(err => console.error('统计图数据加载失败:', err)); </script> </body> </html>

这段逻辑说明三点。echarts.init一定要在 DOM 元素渲染完成后执行,所以脚本放在图表 div 后面比较稳;fetch返回的是 Promise,数据到达后再调用setOption,不能在外面直接用data.ranges,否则拿到的是 undefined。柱状图的itemStyle.color返回一个函数,ECharts 会为每个数据项调用它,从而让不同分数段显示不同颜色。

如果想把柱状图换成饼图或折线图,只需要改series这一节:

// 饼图:把 series.type 改成 'pie',并把 data 换成数组对象 series: [{ name: '成绩占比', type: 'pie', radius: ['40%', '70%'], // 这样得到环形图效果 data: data.pieData // [{name: '优秀', value: 12}] 结构 }] // 折线图:把 series.type 改成 'line',并加 smooth 平滑曲线 series: [{ name: '历次考试平均分', type: 'line', smooth: true, data: data.averageTrend // [78.5, 81.2, 84.0, 83.5] }]

饼图的radius数组前一个值是内半径,后一个是外半径,内半径不为 0 就变成环形;折线图的smooth控制曲线是否平滑。后端只需要按对应字段返回符合结构的 JSON,前端配置就能直接复用。ECharts 的官方示例很多,字段命名也稳定,拿到一个示例改成自己的数据字段即可,不需要背配置项。

5. 避坑实录:环境、驱动、乱码、路径四组卡点的排查记录

这套项目我在不同机器上反复部署过,踩过的坑基本集中在环境创建、数据库驱动、中文字符集、资源路径四块。下面按“现象 → 原因 → 解决”的方式整理,几乎可以当一份排错清单用。

5.1 环境卡点:新版本 IDEA 里建不出 JavaWeb 项目

现象:新版 IDEA 的 New Project 面板里找不到 Java Enterprise 选项,创建出来的普通 Java 项目没有src/main/webapp目录,手动补上后 Tomcat 启动找不到 JSP。

原因:新版 IDEA 默认不再把 Web 项目作为独立模板展示,只有项目被识别为 Web Facet 后,webapp目录才会被编译进部署包。还有一个常见原因是项目创建时选了普通 Java 而不是 Maven Web 骨架,导致连web.xml都没有。

解决:在项目上按Ctrl+Shift+Alt+S打开 Project Structure,选中 Modules,点加号添加 Web Facet,然后把 Web Resource Directory 指到src/main/webapp。另有一种更省事的方式:新建工程时选择 Maven Archetype 里的maven-archetype-webapp,它会自动生成web.xml和src/main/webapp。最后在 Run Configuration 里配置 Tomcat,Deployment 里添加war exploded而不是war,否则每次改 JSP 都要重新打包。

5.2 数据库卡点:MySQL 8 驱动的 ClassNotFoundException 与 SSL 报错

现象:项目启动后执行查询,控制台报ClassNotFoundException: com.mysql.jdbc.Driver;或者连接时抛The server time zone value异常;有时还夹杂Communications link failure。

原因:MySQL 5.x 时代驱动类是com.mysql.jdbc.Driver,MySQL 8.x 换成了com.mysql.cj.jdbc.Driver;同时 MySQL 8 默认时区不是本地东八区,连接 URL 不带serverTimezone就会报错。如果用的是本地 MySQL 8 且连接 URL 没加allowPublicKeyRetrieval=true,还会遇到公钥检索被拒。

解决:换用mysql-connector-java-8.x.jar,并把连接 URL 调整成下面这个标准写法:

jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

注意&在c3p0-config.xml里要写成&amp;,在 Java properties 或代码字符串里写&就行。我没有刻意去背参数,记住三类即可:编码类、时区类、SSL 类。MySQL 8 默认走 SSL 加密握手,本地开发用useSSL=false关掉,能省掉证书配置的麻烦。

5.3 乱码卡点:页面问号、参数乱码、入库乱码其实是三件事

现象:登录用户名输入中文后查询无结果;成绩列表页面看到“????”;MySQL 表里中文变成乱码。

原因:一个请求从浏览器到数据库要经过多个编码环节,任何一个环节不一致就出问题。最常见的是 JSP 页面pageEncoding没有指定 UTF-8;其次是 Servlet 读取 POST 参数前没有设置请求编码;第三是 MySQL 表建成默认的 latin1 字符集。

解决:按三个环节分别处理。第一,JSP 头部写pageEncoding="UTF-8",HTML 的<meta charset="UTF-8">也写上。第二,在 Filter 里统一设置request.setCharacterEncoding("UTF-8")和response.setCharacterEncoding("UTF-8"),这个 Filter 必须最先执行,因为参数一旦被读取,再设置编码就不生效了。第三,建库建表时统一DEFAULT CHARSET utf8mb4,注意utf8mb4和utf8的区别,前者才完整支持中文及 emoji。

5.4 路径卡点:JSP 页面样式丢失与 Servlet 404

现象:首页能打开但完全没有样式;echarts.min.js引入后浏览器控制台报 404;访问 Servlet 地址报 404 或 500。

原因:JSP 在子目录下引用了相对路径,比如list.jsp在score/下,里面写../static/css可能碰巧能用,但部署到不同项目上下文之后路径全断了。Servlet 404 的另一个常见原因是@WebServlet注解和web.xml里的映射同时存在,Tomcat 启动就报冲突。

解决:统一使用上下文路径拼接。JSP 里的写法是${pageContext.request.contextPath}:

<script src="${pageContext.request.contextPath}/static/js/echarts.min.js"></script> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/style.css">

Servlet 侧用req.getContextPath()。检查 404 时先看是不是过滤器拦截了路径,再看注解与 web.xml 是否冲突,最后确认浏览器地址栏大小写——Linux 服务器上ScoreServlet和scoreservlet是不同路径。这个检查顺序基本能覆盖 90% 的路径问题。

6. 部署与验证:打一个 WAR 包,把整套流程从开发态走到发布态

开发环境跑通只是第一步,真正能交出去的是打成 WAR 包放到 Tomcat 里还能正常跑的状态。传统 JSP 项目有两种打包方式:如果项目用 Maven 管理,直接执行:

mvn clean package -Dmaven.test.skip=true

执行完成后target/目录下会出现一个javaweb-student-score.war,这就是可部署文件。如果项目不用 Maven,可以在 IDEA 的 Project Structure → Artifacts 里新建 Web Application: Archive,构建后同样产出一个 WAR。把 WAR 复制到 Tomcat 的webapps/目录下,启动 Tomcat,控制台输出一行“Deployment of web application archive ... has finished”就说明部署成功。默认访问地址是:

http://localhost:8080/javaweb-student-score/login.jsp

这个路径里的javaweb-student-score就是 WAR 包文件名,如果文件名改了,路径第一段也要跟着变。到这里,部署环节全部完成,下面是我的固定验证顺序,照着过一遍就能判定这个系统能不能交付:

第一步,访问登录页,输入错误密码看是否提示错误,输入正确密码确认能进入首页。
第二步,不登录直接访问index.jsp,浏览器应被重定向回login.jsp,Session 拦截生效。
第三步,新增一条成绩、修改分数、删除一条记录,回列表页确认数据与数据库一致。
第四步,打开成绩统计页,在浏览器开发者工具 Network 面板里查看/stats/score请求是否返回 JSON,并确认柱状图、饼图与数据库查询结果对得上。

如果统计页一片空白,优先看 Network 接口而不是先调 ECharts 配置。图表空白绝大多数不是 ECharts 的问题,而是后端 JSON 没返回,或者请求被过滤器拦截了。

这套部署验证流程是从头到尾踩出来的。从那以后,我每次交付 JSP 项目都强制自己走一遍完整验证清单:先跑登录、再测增删改、再看图表接口,最后才打 WAR 包发给对方。在本地怎么改都能跑的环境里交付,到对方机器上就出事,多半是因为只验证了功能,没验证部署过程。把这个流程钉死,你交付的就不是一段能跑的代码,而是一个能落地的项目。希望帮到你。

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

返回列表