学生成绩管理系统实战:Servlet+JSP+MySQL+JDBC开发全解析
2026/9/12 2:28:15 网站建设 项目流程

简介:基于Java技术栈的学生成绩管理系统项目,整合Servlet、JSP、Bootstrap与MySQL,适合Java Web初学者及毕业设计参考。系统覆盖学生信息维护、成绩录入、成绩查询、统计分析等典型功能,并体现MVC分层思想。资源打包为zip格式,共575个文件,大小9.17MB。其中85个Java源文件与170个class文件对应业务逻辑与编译结果;74个JSP页面负责动态展示;122个JS、56个CSS及Bootstrap组件完善前端交互;12个jar包提供依赖支持;另有SQL脚本可初始化数据库。包内还包含9个XML配置与3个properties文件,便于理解Web应用配置;TTF、WOFF、SVG、PNG等前端资源用于支撑页面图标与字体样式。已有3115人学习下载。项目代码结构清晰,包含DAO层、Servlet控制层与JSP视图层,可直接导入IDE运行,适合用于课程设计、期末项目或系统学习Java Web开发全流程。

1. 学生成绩管理系统还在用 Servlet 组合栈,说明什么

在网上搜“学生成绩管理系统”会得到一堆 Spring Boot + Vue 的现成架子,但真正被课程设计、本科毕业设计和企业内部老旧系统选中的,恰恰是 Java + Servlet + JSP + Bootstrap + MySQL 这套组合。原因不复杂:它把从前端页面到数据库表的整条链路全部摊开,没有 MyBatis 帮你拼 SQL,没有 Spring MVC 帮你隐式传参,每一个 HTTP 请求怎么进来、参数怎么取、数据库连接怎么开、结果怎么渲染,都直接写在代码里。作为学习者,能依靠这套系统理解 Web 应用最底层的请求—响应模型;作为维护者,改一个字段名不需要顺着三层注解追半天。本文要讲的就是这样一个系统的落地过程:从表结构设计、JDBC 封装、Servlet 路由,到 JSP 渲染和 Bootstrap 布局,再到上线前常见的连接池、中文乱码、404 路径问题,全部按可复现的方式走一遍。适合刚学完 Java SE 想做完整 Web 项目的人,也适合接手此类老旧系统时快速理清整改路径的从业者。

2. 环境搭建与 MySQL 表结构:先定数据模型再写代码

2.1 开发环境版本选择:JDK 8 和 Tomcat 8.5 为什么是稳妥起点

学生成绩管理系统这种三层结构的传统项目,对环境版本的敏感度比 Spring Boot 项目还高。JDK 8 是兼容性最好的基准,Tomcat 则建议选 8.5 或 9.0,这两个版本支持 Servlet 3.1/4.0 规范,恰好覆盖 JSP EL 表达式和 Servlet 注解开发的所有需求。如果你装了 JDK 17 再配上 Tomcat 10,会遇到javax.servlet包名整体变成jakarta.servlet的问题,教材里几乎所有示例代码都要改 import,对做课设的人来说得不偿失。Eclipse 用户创建 Dynamic Web Project 时,Target Runtime 一栏直接选 Tomcat 8.5,Dynamic web module version 保持 3.1,这样生成的 web.xml 头文件不会引入不兼容的 schema。这里常见的整条链路问题来自 IDE 内置浏览器缓存:JSP 改动后 Tomcat 自动重编译,但浏览器还在读旧 CSS,后面排错章专门讲这个问题。

2.2 MySQL 建库建表:成绩表怎么设计才能避免数据冗余

成绩管理系统的核心是学生、课程、成绩三张表,但很多初版设计会把学生姓名和课程名称直接冗余到成绩表里。表面上看查询方便,实际上只要学生改名(虽然少见)或课程改名,就要 UPDATE 成绩表里所有行。我一般这样设计:

CREATE DATABASE IF NOT EXISTS student_score DEFAULT CHARSET utf8mb4; USE student_score; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, class_name VARCHAR(50), gender ENUM('男', '女') DEFAULT '男', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 ) 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) NOT NULL, exam_date DATE, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB;

这段建表 SQL 里有三个值得注意的设计选择。第一,score表里不存学生姓名和课程名,只存两个外键 ID,查询时用 JOIN 把名字带出来;第二,UNIQUE KEY uk_stu_course保证同一个学生同一门课只有一条成绩记录,这是应用层防止重复录入的底线;第三,DECIMAL(5,1)比 FLOAT/DOUBLE 更适合存成绩,因为浮点数在比较和求和时会出现 89.9999 这类精度误差。ENGINE=InnoDB是硬性要求,外键约束需要事务和行级锁支持,MyISAM 在这套系统里没有使用场景。

2.3 MySQL 驱动与 JDBC 连接:别把密码写在每个 DAO 里

需要准备好 MySQL 的 JDBC 驱动 jar 包,通常使用mysql-connector-java,不同版本对应不同 MySQL Server。把驱动 jar 放进WEB-INF/lib目录后,写一个统一的连接工具类是这套项目的规范做法:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_score" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL Driver not found"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

这段代码的要点集中在 URL 参数上。serverTimezone=Asia/Shanghai解决 MySQL 8.0 的时区校验问题,不带这个参数会报The server time zone value错误;characterEncoding=utf8配合数据库的utf8mb4字符集,让中文姓名从 JDBC 到 Servlet 再到 JSP 的编码链路保持统一。密码硬编码在这里是课程设计的常态,但我建议把 DB 配置抽到db.properties里,通过Properties.load()读取,这样后续改库密码不必重新编译 Java 源码。关于连接管理,DriverManager.getConnection()每次调用都新建物理连接,Tomcat 层面更合理的方案是配置 JNDI DataSource,后面第 5 章展开说。

3. Servlet 路由与业务逻辑:把请求分发给对应的处理器

3.1 用 @WebServlet 注解代替 web.xml 映射的好处

传统 Servlet 项目里,每个 Servlet 都要在 web.xml 中注册,网页多了之后 web.xml 会变成一长串<servlet><servlet-mapping>的配对。Servlet 3.0 之后可以用@WebServlet("/student/list")直接挂在类上,Tomcat 启动时自动扫描注解完成注册。学生成绩管理系统建议按资源类型拆 Servlet:StudentServlet负责学生增删改查,CourseServlet负责课程管理,ScoreServlet负责成绩录入和统计。每个 Servlet 内部通过action参数区分具体操作:

@WebServlet("/student") public class StudentServlet extends HttpServlet { private StudentDAO studentDAO = new StudentDAO(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "edit": showEditPage(req, resp); break; case "delete": deleteStudent(req, resp); break; default: listStudents(req, resp); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("save".equals(action)) { saveStudent(req, resp); } } private void listStudents(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Student> list = studentDAO.findAll(); req.setAttribute("studentList", list); req.getRequestDispatcher("/jsp/student_list.jsp").forward(req, resp); } }

关于 GET 和 POST 的分工,这里有一个典型的设计取舍:查询、删除这类写操作放在 GET 里确实违背了 REST 语义,删除操作应该用 POST,但课程设计系统追求的是页面跳转简单,<a href="student?action=delete&id=3">直接就能触发,不需要写表单。实际生产环境必须把删除改成 POST + 确认框,否则搜索引擎爬虫或预加载插件会误删数据。req.getRequestDispatcher(...).forward()resp.sendRedirect()的区别在于:forward 是服务器内部跳转,地址栏不变,request 域里的 Attribute 还能继续读;重定向是二次请求,必须通过 URL 参数或 Session 传递数据。表单提交成功后我建议用重定向,避免刷新页面时重复提交表单。

3.2 DAO 层用 PreparedStatement 防 SQL 注入

成绩管理系统的查询条件经常是动态拼接的,比如按学生姓名模糊查询、按班级筛选、按分数区间过滤。最容易写错的方式是:

String sql = "SELECT * FROM student WHERE name LIKE '%" + keyword + "%'";

这种字符串拼接会把单引号变成 SQL 语法的分界符,输入' OR '1'='1就能把整个表导出来。正确写法是使用带占位符的PreparedStatement

public List<Student> searchByName(String keyword) { List<Student> result = new ArrayList<>(); String sql = "SELECT s.id, s.student_no, s.name, s.class_name " + "FROM student s WHERE s.name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student stu = new Student(); stu.setId(rs.getInt("id")); stu.setStudentNo(rs.getString("student_no")); stu.setName(rs.getString("name")); stu.setClassName(rs.getString("class_name")); result.add(stu); } } } catch (SQLException e) { e.printStackTrace(); } return result; }

代码里用了 Java 7 的 try-with-resources 语法,ConnectionPreparedStatementResultSet声明在 try 括号内,方法结束后自动 close,不再需要手动在 finally 里判空关闭。LIKE ?配合ps.setString(1, "%" + keyword + "%"),通配符作为参数值传入而不是拼进 SQL 文本,这是防注入的关键。注意 MySQL 的LIKE对中文索引不友好,数据量过万后建议把搜索需求拆成前缀匹配或改用全文索引,这套课设系统的数据量一般碰不到这层瓶颈。

3.3 成绩统计的 SQL:一次查出平均分、最高分和及格率

成绩模块比学生、课程模块多一个统计需求:点击某个学生时,要展示其所有选课成绩,同时显示已修课程平均分;点击某门课时,要展示选课人数、最高分、最低分、平均分和不及格人数。逐条查出成绩再在 Java 里做统计是可行的,但一个GROUP BY就能完成的事不该写几十行循环:

SELECT c.course_name, COUNT(sc.id) AS total_students, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score, ROUND(AVG(sc.score), 1) AS avg_score, SUM(CASE WHEN sc.score < 60 THEN 1 ELSE 0 END) AS fail_count FROM course c LEFT JOIN score sc ON c.id = sc.course_id GROUP BY c.id, c.course_name;

这里用LEFT JOIN而不是INNER JOIN,是为了把还没人选修的课程也列出来,统计字段显示 NULL 或 0。SUM(CASE WHEN ...)是行转列的经典写法,比查出所有成绩后在 Java 里if (score < 60) failCount++干净得多。ROUND(AVG(sc.score), 1)保留一位小数,这个精度在成绩单上已经够看。对应这个查询的 DAO 方法返回值应该是一个Map<String, Object>列表,字段名直接对应 SQL 里的别名,JSP 页面用scoreStat.key取值时不会因为大小写问题找漏。

4. JSP 页面渲染与 Bootstrap 集成:后端拼好数据,前端负责分层

4.1 JSP 中的 EL 表达式和 JSTL,替代 Java 脚本片段

很多初学者写 JSP 喜欢在<%%>里嵌 Java 代码,比如循环输出学生表格。这种页面在 Tomcat 编译时会有性能损耗,且页面里 Java 源码和 HTML 混杂,后续调整样式时会看得头大。JSP 2.0 之后的标准做法是 EL 表达式取变量、JSTL 标签库控制循环和条件:

<%@ 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-CN"> <head> <meta charset="UTF-8"> <title>学生列表</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/bootstrap-3.4.1/css/bootstrap.min.css"> </head> <body> <div class="container"> <h2>学生信息管理</h2> <div class="row"> <div class="col-md-8"> <form class="form-inline" method="get" action="${pageContext.request.contextPath}/student"> <input type="hidden" name="action" value="search"> <div class="form-group"> <input type="text" class="form-control" name="keyword" placeholder="学生姓名"> </div> <button type="submit" class="btn btn-primary">搜索</button> </form> </div> </div> <table class="table table-striped table-bordered table-hover"> <thead> <tr> <th>学号</th><th>姓名</th><th>班级</th><th>操作</th> </tr> </thead> <tbody> <c:forEach items="${studentList}" var="stu"> <tr> <td>${stu.studentNo}</td> <td>${stu.name}</td> <td>${stu.className}</td> <td> <a href="${pageContext.request.contextPath}/student?action=edit&id=${stu.id}" class="btn btn-sm btn-warning">编辑</a> <a href="${pageContext.request.contextPath}/student?action=delete&id=${stu.id}" class="btn btn-sm btn-danger" onclick="return confirm('确认删除该学生?');">删除</a> </td> </tr> </c:forEach> </tbody> </table> </div> </body> </html>

页面要点有三个。第一,所有资源路径都用${pageContext.request.contextPath}作为前缀,这样项目部署到 Tomcat 的任意名字的 context 下,CSS、图标、链接都不会因为多一层路径而 404。第二,${stu.studentNo}这种写法遵循 JavaBean 命名规范自动调用getStudentNo()方法,如果实体类的属性是studentNo,那么 JSP 里的 EL 变量名不区分大小写直接对应属性名。第三,表单和链接的 action 都指向/student这个 Servlet 路径,而之前的/student/list这种映射在本项目里用不了,因为 Servlet 的@WebServlet只注册了一个同名路径。

4.2 表单回显和编辑页面:老数据怎么重新填进输入框

编辑学生信息是成绩管理系统里最典型的回显场景。点击列表中的“编辑”按钮后,Servlet 查出这条学生记录,setAttribute 放进 request,forward 到student_edit.jsp。页面上的输入框需要用 EL 表达式设置value属性:

<form method="post" action="${pageContext.request.contextPath}/student"> <input type="hidden" name="action" value="save"> <input type="hidden" name="id" value="${student.id}"> <div class="form-group"> <label>学号</label> <input type="text" class="form-control" name="studentNo" value="${student.studentNo}"> </div> <div class="form-group"> <label>姓名</label> <input type="text" class="form-control" name="name" value="${student.name}"> </div> <button type="submit" class="btn btn-success">保存</button> <a href="${pageContext.request.contextPath}/student" class="btn btn-default">返回列表</a> </form>

这个页面里的两个隐藏域是整个编辑功能的关键。name="id"的 hidden input 保证保存时知道更新的是哪一行;name="action"的 hidden input 让同一个 Servlet 的 doPost 知道这是新增还是修改。value="${student.id}"在新增页面会输出空字符串,因为新增时 request 里没有student这个属性,EL 对不存在的属性返回 null 然后输出空串。处理保存的 Servlet 代码里要判断id参数是否为空从而决定执行 INSERT 还是 UPDATE,这是很易错的分支逻辑。

4.3 Bootstrap 表格和弹窗:把确认框和模态框用对

如果只是把 Bootstrap 的 CSS 引进来,然后继续用浏览器原生confirm(),那 Bootstrap 只发挥了一半价值。成绩管理系统常见的使用场景是从成绩列表点击“录入成绩”按钮,弹出一个模态框,里面包含学生下拉框、课程下拉框、分数输入框。这个模态框需要 Bootstrap 的 JavaScript 组件驱动:

<!-- 模态框触发按钮 --> <button type="button" class="btn btn-primary">@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }

这段过滤器放在 Servlet 之前执行,对所有请求做编码统一处理。req.setCharacterEncoding("UTF-8")只对请求体的 POST 参数生效,URL 路径里带的 GET 参数编码由 Tomcat 的URIEncoding="UTF-8"属性控制,需要在server.xml里改 Connector 配置。第二处乱码如果发生在 AJAX 返回 JSON 时,需要多写一行resp.setContentType("application/json;charset=UTF-8"),否则浏览器按页面编码解析会读出鎴愮哗这类字符。第三处乱码根源通常是 JDBC URL 没加characterEncoding=utf8,前面 DBUtil 里已经处理。

5. 页面布局、分页查询和请求流程的调整细节

5.1 用 Bootstrap 导航条把所有功能入口串起来

成绩管理系统的功能模块如果直接堆成链接列表,用户在浏览器里看到的界面会比较混乱。常见做法是使用 Bootstrap 导航条作为整体框架,上面放学生管理、课程管理、成绩管理三个一级菜单,每个菜单通过下拉二级菜单展开具体操作。这个导航条放在header.jsp里,所有页面用<%@ include file="header.jsp" %>静态引入。

<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="${pageContext.request.contextPath}/index.jsp">学生成绩管理系统</a> </div> <ul class="nav navbar-nav"> <li class="active"><a href="${pageContext.request.contextPath}/student">学生管理</a></li> <li><a href="${pageContext.request.contextPath}/course">课程管理</a></li> <li><a href="${pageContext.request.contextPath}/score">成绩管理</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="${pageContext.request.contextPath}/logout.jsp">退出</a></li> </ul> </div> </nav>

所有页面复用同一个导航条可以保证菜单高亮逻辑一致,但注意这里的高亮是写死在class="active"里的,如果访问成绩页面,学生管理菜单仍然高亮。改进方案是在每个 JSP 顶部加pageContext.setAttribute("activeMenu", "score"),然后导航条中每个<li>用 EL 判断输出 active。静态 include 和动态 include 的选择值得提一句:<%@ include %>在编译期把文件内容嵌入当前 JSP,性能好但没法传参数;<jsp:include>在运行期动态执行目标 JSP,适合带参数引入片段,比如 header 里要显示当前登录用户的信息时——这必须依赖sessionScope.user动态决定显示什么。

5.2 分页的 SQL 和 JSP 交互:LIMIT 子句的边界用法

学生表数据超过 200 行时,列表页全量加载已经不舒服,超过了 1000 行会让浏览器出现明显的卡顿。分页是成绩系统的常见加分项。最简单可靠的分页方案仍然是 MySQL 的LIMIT子句配合两个参数offset, count。假设每页显示 10 条,当前第 3 页的 offset 是(3-1)*10 = 20

public List<Student> findPage(int page, int pageSize) { String sql = "SELECT id, student_no, name, class_name FROM student ORDER BY id LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); // 执行查询并封装对象 } } public int countAll() { String sql = "SELECT COUNT(*) FROM student"; // 执行查询,返回总数 }

LIMIT的第一个参数从 0 开始,所以第 1 页的 offset 是 0 而不是 10。JSP 分页条可以用一端判断上一页/下一页是否可用。如果查询条件带WHERE name LIKE ?,分页 SQL 必须复用相同的 WHERE 条件,COUNT(*)也要用相同条件查询才能算出正确总页数。很多初版代码把 search 和分页分开实现,搜索之后点击第 2 页搜条件丢失,这需要在请求里同时保留当前的关键词参数和页码参数,文件在 Servlet 里拼到跳转的 URL 上。

5.3 启动即报错 404 和 500 的处理方式

成绩管理系统部署之后最常遇到这两类错误。404 一般是请求路径和 Servlet/JSP 路径不匹配,F12 Network 里能看到哪个资源 404 了,重点检查两处:@WebServlet注解里的 URL 是否以/开头,JSP 中<form action="...">里的路径是否带contextPath。500 错误里 Servlet 异常的堆栈信息会直接输出到 Tomcat 的 catalina.out 文件里,一般不显示在浏览器页面中,这时候要养成查看日志的习惯。如果加载不了Class.forName("com.mysql.cj.jdbc.Driver"),多半是驱动 jar 包没进WEB-INF/lib,Eclipse 里Build Path引入的 jar 如果没勾选 Deploy Assembly,部署到 Tomcat 后是不存在于 WebContent/WEB-INF/lib 下的,这个导致运行时才报 ClassNotFound 的问题很隐蔽。

6. 连接池改造与成绩导出的组合收尾

6.1 把 JDBC 改成内嵌 HikariCP 连接池的几个改动

如果这套系统有机会写成期末报告,把DriverManager.getConnection()升级成 HikariCP 连接池是最能体现工程素养的改动。HikariCP 是当前生产环境使用率最高的连接池之一,改造只涉及 DBUtil:把依赖 jar 放进 lib,然后初始化一个HikariDataSource

private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/student_score" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }

maximumPoolSize在开发环境 10-20 即可,不需要盲目调大,每次连接占用 MySQL 侧一个线程,过大的连接数会导致数据库负载反而上升。改成连接池之后,conn.close()的语义从关闭物理连接变成归还到池里,try-with-resources仍然适用,DAO 代码不需要改动,这个特性让改造风险最小。minimumIdle预热几个空闲连接可以降低前几次请求的延迟。整个系统里的 DAO 方法通过getConnection()获取的都是池化连接,连接数满了之后新请求会在connectionTimeout时间内等待,超时抛 SQLException。这个阶段关注HikariCP日志里连接获取淡慢,更多场景来自单条 SQL 太慢把池占满。

6.2 检查 Score 统计查询是否用对了索引

大型系统上线前 DBA 会用EXPLAIN看执行计划,成绩管理系统虽然数据量小,但这个习惯值得学。给score表加上学生和课程两个外键上的索引之后,JOIN的查询路径可以从全表扫描变成索引查找:

EXPLAIN SELECT c.course_name, COUNT(sc.id) FROM course c LEFT JOIN score sc ON c.id = sc.course_id GROUP BY c.id, c.course_name;

EXPLAIN结果里如果 type 是ALL说明走的全表扫描,加上indexref就是走了索引。score表上的uk_stu_course联合索引已经覆盖了(student_id, course_id)的组合查询,按课程分组统计时会用到这个索引的 course 部分——最左前缀规则下,只有以 student_id 开头才能命中完整索引,所以涉及某门课的成绩统计时可以加一个INDEX(course_id)。实际优化加索引时要根据衡量查询模式去加,不能给每个列都建索引,索引写放大反而影响 INSERT/UPDATE 性能。

6.3 成绩导出 Excel 时 JSP 用 response 输出流的注意点

成绩列表导出 Excel 是这类系统的高频附加功能。常见方案是自己拼 CSV,因为 CSV 可以由 Excel 直接打开,不需要额外依赖。另一种是用 POI 生成真实.xlsx,代码量更大。

resp.setContentType("text/csv;charset=UTF-8"); resp.setHeader("Content-Disposition", "attachment; filename=scores.csv"); PrintWriter out = resp.getWriter(); out.write('\ufeff'); // BOM,防止 Excel 打开 CSV 时中文乱码 out.println("学号,姓名,课程,成绩"); for (ScoreVO vo : scoreList) { out.println(vo.getStudentNo() + "," + vo.getStudentName() + "," + vo.getCourseName() + "," + vo.getScore()); } out.flush();

这段代码里有两个工程细节。Content-Dispositionattachment告诉浏览器这是下载而不是页面渲染,filename后的后缀名决定文件类型。写 CSV 时加\ufeffBOM 头是处理 Excel 打开乱码的标准做法——Excel 通过 BOM 识别 UTF-8 编码,不加这个字符它默认按本地编码解析。如果改成 POI 导出,同样需要设置响应头的Content-Disposition。导出功能在 Servlet 里输出时要确保之前没有调用过getWriter()执行过任何 HTML 输出,否则响应流里已经叠加了页面内容,下载的文件会损坏。此时可以结合 Controller 里做输出前检查此请求来自表单提交的链接,而不会夹带已有的 JSP 片段。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询