jsp 在学校管理系统中的实际应用案例与常见问题解决方案
说到 JSP,很多初学者可能会皱眉头——”不是已经有那么多现代框架了吗,还学这老东西干嘛?”但你要知道,很多学校的实际系统,尤其是国内那些还在维护的老系统,底层都是 JSP 搭起来的。你去教务处问老师要用什么系统,他们很可能指着一个浏览器窗口说”就那个”,而你根本看不出它用了什么技术。今天咱就掰开揉碎了聊聊,JSP 在学校管理系统里到底是怎么干活的,以及那些让人头大的坑怎么填。
为什么学校偏爱 JSP
我见过太多学校信息化的场景了。一个三四百人的中学,教务处、政教处、教务处各有各的 Excel 表格,老师每天交作业要跑三个办公室。后来学校决定上一套系统,预算有限,开发人员也刚毕业,于是 JSP + Servlet + MySQL 就成了最常见的选择。
这不是因为 JSP 有多先进,而是因为它上手快、部署简单、跟 Java 生态无缝对接。学校用的服务器通常是 Tomcat,JSP 写出来直接扔 webapps 目录就能跑,连 Maven 都不用配置。对于一个每年预算就十几万、信息化人员就两三个的学校来说,这种”够用就行”的方案反而是最务实的。
我去年帮一所职业高中做的教务系统,核心就是 JSP。学生成绩查询、教师课表排布、宿舍管理、成绩录入——全部用 JSP 完成。系统跑了一年多,稳定得很。当然,中间也踩过不少坑,咱们一个一个说。
第一个实战案例:学生成绩管理系统
让我给你讲一个真实的项目。2023 年,我和几个同学接了一个县域高中的项目——“智学通”学生成绩管理系统。整个团队就三个人,我负责前端和 JSP 层,两个同学分别做数据库和后端接口。
系统的核心需求很朴素:
- 学生可以登录查询自己的成绩
- 教师可以录入和修改成绩
- 教务人员可以查看各班级的平均分、排名
- 支持成绩导出 Excel
1. 数据库设计
先说数据库。这个项目用了 MySQL 5.7,表结构设计如下:
-- 学生表
CREATE TABLE student (
student_id VARCHAR(20) PRIMARY KEY COMMENT '学号',
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender VARCHAR(10) COMMENT '性别',
class_id VARCHAR(20) COMMENT '班级ID',
password VARCHAR(64) NOT NULL COMMENT '密码(MD5加密)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 教师表
CREATE TABLE teacher (
teacher_id VARCHAR(20) PRIMARY KEY COMMENT '工号',
name VARCHAR(50) NOT NULL COMMENT '姓名',
subject VARCHAR(50) COMMENT '任教科目',
password VARCHAR(64) NOT NULL COMMENT '密码(MD5加密)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 课程表
CREATE TABLE course (
course_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '课程ID',
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
teacher_id VARCHAR(20) COMMENT '授课教师ID',
class_id VARCHAR(20) COMMENT '班级ID',
FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id)
);
-- 成绩表
CREATE TABLE score (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '成绩ID',
student_id VARCHAR(20) NOT NULL COMMENT '学生学号',
course_id INT NOT NULL COMMENT '课程ID',
exam_name VARCHAR(50) NOT NULL COMMENT '考试名称(如:期中考试)',
score DECIMAL(5,2) COMMENT '分数',
rank INT COMMENT '班级排名',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (student_id) REFERENCES student(student_id),
FOREIGN KEY (course_id) REFERENCES course(course_id)
);
-- 班级表
CREATE TABLE class_info (
class_id VARCHAR(20) PRIMARY KEY COMMENT '班级ID',
class_name VARCHAR(50) NOT NULL COMMENT '班级名称',
grade INT COMMENT '年级',
homeroom_teacher_id VARCHAR(20) COMMENT '班主任工号',
FOREIGN KEY (homeroom_teacher_id) REFERENCES teacher(teacher_id)
);
这个设计看着简单,但有个细节要注意:成绩表里没有直接存班级信息。为什么?因为一个班级可能会有多个班级同名不同年级的情况,而且班级可能会调整。通过 student_id 关联 student 表,再查到 class_id,这样更灵活。
2. JSP 页面实现:学生成绩查询
接下来是最核心的部分——JSP 页面。咱们从学生成绩查询说起。
首先,登录页面 login.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.*" %>
<%
// 简单的登录状态检查
String userType = (String) session.getAttribute("userType");
if (userType != null && !"guest".equals(userType)) {
response.sendRedirect("index.jsp");
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>智学通 - 登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
width: 400px;
}
.login-container h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.form-group input, .form-group select {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #667eea;
}
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.error-msg {
color: #e74c3c;
font-size: 14px;
margin-bottom: 15px;
text-align: center;
min-height: 20px;
}
</style>
</head>
<body>
<div class="login-container">
<h2>🎓 智学通登录</h2>
<div class="error-msg"><%= request.getParameter("error") != null ? request.getParameter("error") : "" %></div>
<form action="doLogin.jsp" method="post">
<div class="form-group">
<label>用户类型</label>
<select name="userType" id="userType" onchange="toggleIdInput()">
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="form-group">
<label id="idLabel">学号</label>
<input type="text" name="userId" id="userId" placeholder="请输入学号" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="btn-login">登 录</button>
</form>
</div>
<script>
function toggleIdInput() {
var userType = document.getElementById('userType').value;
var idLabel = document.getElementById('idLabel');
var idInput = document.getElementById('userId');
if (userType === 'student') {
idLabel.textContent = '学号';
idInput.placeholder = '请输入学号';
} else if (userType === 'teacher') {
idLabel.textContent = '工号';
idInput.placeholder = '请输入工号';
} else {
idLabel.textContent = '管理员账号';
idInput.placeholder = '请输入管理员账号';
}
}
</script>
</body>
</html>
这个登录页面看着挺清爽吧?关键点是动态切换输入框的提示文本,根据用户类型显示”学号”、”工号”或”管理员账号”。这个交互虽然简单,但对用户体验提升很大——老师不用看提示就能明白该填什么。
然后是 doLogin.jsp,处理登录逻辑:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.*" %>
<%@ page import="java.security.MessageDigest" %>
<%
request.setCharacterEncoding("UTF-8");
String userId = request.getParameter("userId");
String password = request.getParameter("password");
String userType = request.getParameter("userType");
// 简单的输入验证
if (userId == null || userId.trim().isEmpty() || password == null || password.trim().isEmpty()) {
response.sendRedirect("login.jsp?error=账号或密码不能为空");
return;
}
// MD5 密码加密(生产环境应该用更安全的算法)
String md5Password = md5(password);
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 连接数据库
String url = "jdbc:mysql://localhost:3306/school_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8";
conn = DriverManager.getConnection(url, "root", "your_password");
// 根据用户类型查询
String sql = "";
if ("student".equals(userType)) {
sql = "SELECT * FROM student WHERE student_id = ? AND password = ?";
} else if ("teacher".equals(userType)) {
sql = "SELECT * FROM teacher WHERE teacher_id = ? AND password = ?";
} else if ("admin".equals(userType)) {
sql = "SELECT * FROM admin WHERE admin_id = ? AND password = ?";
}
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userId);
pstmt.setString(2, md5Password);
rs = pstmt.executeQuery();
if (rs.next()) {
// 登录成功,设置 session
session.setAttribute("userId", userId);
session.setAttribute("userType", userType);
session.setAttribute("userName", rs.getString("name"));
session.setMaxInactiveInterval(3600); // 1小时超时
// 记录登录日志(实际项目应该有个 log 表)
response.sendRedirect("index.jsp");
} else {
response.sendRedirect("login.jsp?error=账号或密码错误");
}
} catch (Exception e) {
e.printStackTrace();
response.sendRedirect("login.jsp?error=系统繁忙,请稍后重试");
} finally {
// 关闭资源
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
// MD5 加密方法
public static String md5(String text) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(text.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
return text;
}
}
%>
这里有个重要的细节:资源关闭一定要放在 finally 块里。我见过太多新手写的代码,close() 放在 try 块末尾,一旦中间抛异常,连接就泄漏了。数据库连接池用多了还好,但这种小型学校系统一般不用连接池,每个请求都新建连接,泄漏一个就少一个,几天下来系统就崩了。
3. 成绩查询页面
学生登录后,看到的是成绩查询页面 student_grade.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.*" %>
<%
// 权限检查
String userType = (String) session.getAttribute("userType");
String userId = (String) session.getAttribute("userId");
if (userType == null || !"student".equals(userType)) {
response.sendRedirect("login.jsp?error=无权访问");
return;
}
// 获取考试名称参数
String examName = request.getParameter("examName");
if (examName == null || examName.trim().isEmpty()) {
examName = "期末考试";
}
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>成绩查询 - 智学通</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", sans-serif;
background: #f5f7fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 24px; }
.header .user-info { font-size: 14px; }
.main-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 30px;
margin-bottom: 20px;
}
.card h3 {
color: #333;
margin-bottom: 20px;
font-size: 18px;
border-left: 4px solid #667eea;
padding-left: 12px;
}
.filter-bar {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.filter-bar select, .filter-bar button {
padding: 10px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
background: white;
}
.filter-bar button {
background: #667eea;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s;
}
.filter-bar button:hover { background: #5a6fd6; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
color: #555;
font-weight: 600;
font-size: 14px;
}
td { color: #333; font-size: 14px; }
tr:hover { background: #f8f9fa; }
.score-excellent { color: #27ae60; font-weight: bold; }
.score-good { color: #3498db; font-weight: bold; }
.score-pass { color: #f39c12; font-weight: bold; }
.score-fail { color: #e74c3c; font-weight: bold; }
.empty-tip {
text-align: center;
color: #999;
padding: 40px;
font-size: 16px;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.pagination a, .pagination span {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 6px;
text-decoration: none;
color: #333;
font-size: 14px;
}
.pagination a:hover { background: #f0f0f0; }
.pagination .active {
background: #667eea;
color: white;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="header">
<h1>🎓 智学通 - 成绩查询</h1>
<div class="user-info">
欢迎,<%= session.getAttribute("userName") %> |
<a href="logout.jsp" style="color: white;">退出</a>
</div>
</div>
<div class="main-container">
<div class="card">
<h3>查询条件</h3>
<div class="filter-bar">
<select name="examName" id="examName">
<option value="期中考试" <%= "期中考试".equals(examName) ? "selected" : "" %>>期中考试</option>
<option value="期末考试" <%= "期末考试".equals(examName) ? "selected" : "" %>>期末考试</option>
<option value="月考" <%= "月考".equals(examName) ? "selected" : "" %>>月考</option>
</select>
<button onclick="searchGrade()">查询</button>
<a href="export_grade.jsp?examName=<%= examName %>" style="padding: 10px 20px; border: 2px solid #27ae60; border-radius: 8px; color: #27ae60; text-decoration: none; font-size: 14px;">📥 导出Excel</a>
</div>
</div>
<div class="card">
<h3>成绩明细</h3>
<table>
<thead>
<tr>
<th>课程名称</th>
<th>分数</th>
<th>班级排名</th>
<th>等级</th>
</tr>
</thead>
<tbody>
<%
try {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/school_system?useSSL=false&serverTimezone=Asia/Shanghai";
conn = DriverManager.getConnection(url, "root", "your_password");
String sql = "SELECT c.course_name, s.score, s.rank " +
"FROM score s " +
"JOIN course c ON s.course_id = c.course_id " +
"WHERE s.student_id = ? AND s.exam_name = ? " +
"ORDER BY c.course_name";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userId);
pstmt.setString(2, examName);
rs = pstmt.executeQuery();
boolean hasData = false;
while (rs.next()) {
hasData = true;
String courseName = rs.getString("course_name");
double score = rs.getDouble("score");
int rank = rs.getInt("rank");
// 根据分数判断等级和颜色
String level = "";
String scoreClass = "";
if (score >= 90) {
level = "优秀";
scoreClass = "score-excellent";
} else if (score >= 80) {
level = "良好";
scoreClass = "score-good";
} else if (score >= 60) {
level = "及格";
scoreClass = "score-pass";
} else {
level = "不及格";
scoreClass = "score-fail";
}
%>
<tr>
<td><%= courseName %></td>
<td class="<%= scoreClass %>"><%= score %></td>
<td>第 <%= rank %> 名</td>
<td><%= level %></td>
</tr>
<%
}
if (!hasData) {
%>
<tr>
<td colspan="4" class="empty-tip">暂无<%= examName %>成绩数据</td>
</tr>
<%
}
} catch (Exception e) {
e.printStackTrace();
%>
<tr>
<td colspan="4" class="empty-tip">查询失败,请稍后重试</td>
</tr>
<%
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
%>
</tbody>
</table>
</div>
</div>
<script>
function searchGrade() {
var examName = document.getElementById('examName').value;
window.location.href = 'student_grade.jsp?examName=' + encodeURIComponent(examName);
}
</script>
</body>
</html>
这个页面有个小 trick:根据分数动态设置颜色。优秀是绿色,良好是蓝色,及格是黄色,不及格是红色。这样学生一眼就能看到自己哪科需要加强。老师们反馈说这个设计很贴心,不用挨个看数字判断。
4. 教师成绩录入页面
教师录入成绩的页面稍微复杂一点,需要支持批量录入:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.*" %>
<%
String userType = (String) session.getAttribute("userType");
String userId = (String) session.getAttribute("userId");
String userName = (String) session.getAttribute("userName");
if (userType == null || !"teacher".equals(userType)) {
response.sendRedirect("login.jsp?error=无权访问");
return;
}
// 获取教师可录入的课程
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<Course> courses = new ArrayList<>();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/school_system?useSSL=false&serverTimezone=Asia/Shanghai";
conn = DriverManager.getConnection(url, "root", "your_password");
String sql = "SELECT c.course_id, c.course_name, cl.class_id, cl.class_name " +
"FROM course c " +
"JOIN class_info cl ON c.class_id = cl.class_id " +
"WHERE c.teacher_id = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userId);
rs = pstmt.executeQuery();
while (rs.next()) {
Course course = new Course();
course.courseId = rs.getInt("course_id");
course.courseName = rs.getString("course_name");
course.classId = rs.getString("class_id");
course.className = rs.getString("class_name");
courses.add(course);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>成绩录入 - 智学通</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", sans-serif;
background: #f5f7fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 24px; }
.main-container {
max-width: 1400px;
margin: 30px auto;
padding: 0 20px;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 30px;
margin-bottom: 20px;
}
.card h3 {
color: #333;
margin-bottom: 20px;
font-size: 18px;
border-left: 4px solid #667eea;
padding-left: 12px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.form-group {
flex: 1;
min-width: 200px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.form-group select, .form-group input {
width: 100%;
padding: 10px 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.form-group select:focus, .form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn-group {
display: flex;
gap: 15px;
margin-top: 20px;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover { background: #5a6fd6; }
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-secondary:hover { background: #e0e0e0; }
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover { background: #219a52; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: center;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
color: #555;
font-weight: 600;
}
td input {
width: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
td input:focus {
outline: none;
border-color: #667eea;
}
.tips {
background: #fff9e6;
border: 1px solid #ffe58f;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
color: #856404;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>📝 成绩录入</h1>
<div>欢迎,<%= userName %></div>
</div>
<div class="main-container">
<div class="card">
<h3>选择课程和考试</h3>
<div class="form-row">
<div class="form-group">
<label>选择班级</label>
<select id="classSelect" onchange="loadStudents()">
<option value="">-- 请选择班级 --</option>
<%
// 去重班级
Set<String> classIds = new HashSet<>();
for (Course c : courses) {
classIds.add(c.classId);
}
for (String classId : classIds) {
String className = "";
for (Course c : courses) {
if (c.classId.equals(classId)) {
className = c.className;
break;
}
}
%>
<option value="<%= classId %>"><%= className %></option>
<%
}
%>
</select>
</div>
<div class="form-group">
<label>选择考试</label>
<select id="examSelect">
<option value="期中考试">期中考试</option>
<option value="期末考试">期末考试</option>
<option value="月考">月考</option>
</select>
</div>
</div>
</div>
<div class="card" id="scoreCard" style="display: none;">
<h3>录入成绩</h3>
<div class="tips">
💡 提示:成绩范围 0-100 分,留空表示缺考。修改完成后点击"保存成绩"。
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>学号</th>
<th>姓名</th>
<th>成绩</th>
</tr>
</thead>
<tbody id="studentTableBody">
</tbody>
</table>
<div class="btn-group">
<button class="btn btn-success" onclick="saveScores()">💾 保存成绩</button>
<button class="btn btn-secondary" onclick="resetScores()">🔄 重置</button>
</div>
</div>
</div>
<script>
let currentClassId = '';
let students = [];
async function loadStudents() {
const classSelect = document.getElementById('classSelect');
currentClassId = classSelect.value;
if (!currentClassId) {
document.getElementById('scoreCard').style.display = 'none';
return;
}
// 加载学生列表
try {
const response = await fetch('api/get_students.jsp?classId=' + encodeURIComponent(currentClassId));
const data = await response.json();
students = data.students;
renderTable();
document.getElementById('scoreCard').style.display = 'block';
} catch (error) {
alert('加载学生列表失败');
}
}
function renderTable() {
const tbody = document.getElementById('studentTableBody');
tbody.innerHTML = '';
students.forEach((student, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>${student.studentId}</td>
<td>${student.name}</td>
<td><input type="number" min="0" max="100"
id="score_${student.studentId}"
placeholder="缺考"></td>
`;
tbody.appendChild(row);
});
}
async function saveScores() {
const examName = document.getElementById('examSelect').value;
const scores = [];
students.forEach(student => {
const input = document.getElementById(`score_${student.studentId}`);
const score = input ? input.value : '';
if (score !== '') {
scores.push({
studentId: student.studentId,
score: parseFloat(score)
});
}
});
if (scores.length === 0) {
alert('请至少录入一个成绩');
return;
}
try {
const response = await fetch('api/save_scores.jsp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
classId: currentClassId,
examName: examName,
scores: scores
})
});
const result = await response.json();
if (result.success) {
alert('成绩保存成功!');
location.reload();
} else {
alert('保存失败:' + result.message);
}
} catch (error) {
alert('网络错误,请稍后重试');
}
}
function resetScores() {
if (confirm('确定要清空所有成绩吗?')) {
const inputs = document.querySelectorAll('#studentTableBody input[type="number"]');
inputs.forEach(input => input.value = '');
}
}
</script>
</body>
</html>
这里用了一点现代技术——AJAX。虽然 JSP 是老技术,但它完全可以和 JavaScript 配合。我这个项目就是用 JSP 做后端接口,前端用原生 JavaScript 发 AJAX 请求。这样既保留了 JSP 的便捷(老师不用学新东西),又有了现代 Web 的流畅体验。
常见问题与解决方案
光讲案例不够,咱们得把那些让人头疼的问题也说清楚。这个系统上线后,老师们反馈最多的问题我都给你们整理出来了。
问题一:中文乱码
这是 JSP 项目最常见的问题,没有之一。你输入”张三”,数据库里变成了”张三”,查询出来还是”张三”。排查起来能让你掉一层皮。
原因分析:
中文乱码的本质是编码不统一。从浏览器提交表单,到 Servlet 接收参数,再到 JDBC 连接数据库,每一步的编码都可能不一样。
解决方案:
- JSP 页面统一设置编码
每个 JSP 文件头部都要加:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page pageEncoding="UTF-8" %>
- Servlet 中设置请求和响应编码
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置请求编码(处理表单提交)
request.setCharacterEncoding("UTF-8");
// 设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
// 你的业务逻辑...
}
- MySQL 连接字符串指定编码
String url = "jdbc:mysql://localhost:3306/school_system" +
"?useUnicode=true" +
"&characterEncoding=UTF-8" +
"&serverTimezone=Asia/Shanghai";
- MySQL 数据库和表设置 UTF-8
-- 创建数据库时指定编码
CREATE DATABASE school_system
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
-- 创建表时指定编码
CREATE TABLE student (
student_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 过滤器统一处理(推荐)
与其在每个 Servlet 里写 setCharacterEncoding,不如写一个过滤器:
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@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);
}
@Override
public void destroy() {
}
}
这个过滤器会拦截所有请求,统一设置编码。比每个 Servlet 都写一遍靠谱多了。
问题二:Session 过期导致用户掉线
学校系统的用户年龄跨度很大,从初中生到校长都有。很多老师反映”刚填了一半的成绩,页面突然跳回登录页”。这体验太差了。
原因分析:
Tomcat 默认的 Session 超时时间是 30 分钟。老师在录入成绩时,如果思考太久或者去接了杯水,Session 就过期了。
解决方案:
- 在 web.xml 中配置 Session 超时
<session-config>
<session-timeout>120</session-timeout>
</session-config>
这样 Session 就超时 120 分钟了。
- 在关键操作中刷新 Session
// 每次保存成绩时刷新 Session
session.setAttribute("lastActiveTime", System.currentTimeMillis());
session.invalidate(); // 强制失效
// 或者
session.setMaxInactiveInterval(7200); // 2小时
- 前端心跳检测
// 每5分钟发送一次心跳请求,保持Session活跃
setInterval(function() {
fetch('api/heartbeat.jsp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: Date.now() })
});
}, 300000);
问题三:SQL 注入
这个问题听起来很严重,但在学校管理系统中其实发生的概率很低——毕竟用户都是师生,没人会故意注入。但作为开发者,这个习惯必须养成。
不安全的写法(千万别这么干):
// ❌ 危险!直接拼接 SQL
String sql = "SELECT * FROM student WHERE student_id = '" + userId + "'";
PreparedStatement pstmt = conn.prepareStatement(sql);
安全的写法:
// ✅ 使用 PreparedStatement 参数化查询
String sql = "SELECT * FROM student WHERE student_id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userId);
ResultSet rs = pstmt.executeQuery();
PreparedStatement 的 ? 占位符会让数据库把参数当作纯数据处理,不会执行其中的 SQL 代码。这是防止 SQL 注入最有效的方法。
问题四:数据库连接泄漏
这个问题我前面提过,但值得再强调一遍。很多新手写的代码是这样的:
// ❌ 错误示范
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(url, user, password);
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
// 处理结果...
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 忘记关资源了!
}
一旦 executeQuery() 抛出异常,rs 和 pstmt 就不会被关闭,连接也不会被释放。连续几次之后,数据库连接池就满了,系统直接挂掉。
正确写法:
// ✅ 正确示范
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(url, user, password);
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
// 处理结果...
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 每个资源独立关闭,互不影响
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (pstmt != null) {
try { pstmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
如果项目比较大,建议使用连接池(如 HikariCP、Druid),这样可以复用连接,避免频繁创建和销毁连接的开销。
问题五:JSP 页面中的 Java 代码混乱
这是 JSP 最大的痛点——HTML 和 Java 代码混在一起,维护起来极其痛苦。你看看我之前写的 student_grade.jsp,里面嵌了一大堆 <% %>,读起来非常费劲。
解决方案:使用 JSTL 和 EL 表达式
JSTL(JSP Standard Tag Library)是 JSP 的标签库,可以大幅减少页面中的 Java 代码。
- 引入 JSTL 依赖
在 pom.xml 中:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
- 在 JSP 中使用 JSTL
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<c:forEach var="student" items="${studentList}">
<tr>
<td>${student.name}</td>
<td>${student.score}</td>
<td>
<c:choose>
<c:when test="${student.score >= 90}">优秀</c:when>
<c:when test="${student.score >= 80}">良好</c:when>
<c:when test="${student.score >= 60}">及格</c:when>
<c:otherwise>不及格</c:otherwise>
</c:choose>
</td>
</tr>
</c:forEach>
这样页面就干净多了。Java 逻辑放到 Servlet 或 Model 层,JSP 只负责展示。
更彻底的方案:MVC 架构
浏览器 → Servlet(控制器)→ Model(业务逻辑)→ JSP(视图)
Servlet 负责接收请求、调用业务逻辑、设置 request 属性,JSP 只负责用 EL 表达式展示数据。这样分工明确,后期维护方便很多。
问题六:文件上传中文乱码
成绩表有时候需要上传附件(比如成绩单扫描件),文件上传是个常见问题。
// 使用 commons-fileupload 处理文件上传
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置编码
request.setCharacterEncoding("UTF-8");
// 检查是否有文件上传
if (!ServletFileUpload.isMultipartContent(request)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "表单必须包含文件");
return;
}
// 创建工厂
FileItemFactory factory = new DiskFileItemFactory();
// 创建解析器
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析请求
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
// 普通表单字段
String fieldName = item.getFieldName();
String fieldValue = item.getString("UTF-8"); // 指定编码
System.out.println(fieldName + " = " + fieldValue);
} else {
// 文件上传
String fileName = item.getName();
// 处理文件名编码
fileName = new String(fileName.getBytes("ISO-8859-1"), "UTF-8");
// 保存到服务器
String uploadPath = getServletContext().getRealPath("https://www.ydtgop.cn/uploads");
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
File savedFile = new File(uploadPath + File.separator + fileName);
item.write(savedFile);
System.out.println("文件已保存: " + savedFile.getAbsolutePath());
}
}
response.sendRedirect("upload_success.jsp");
} catch (Exception e) {
e.printStackTrace();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "上传失败");
}
}
}
关键点:item.getString("UTF-8") 和 new String(fileName.getBytes("ISO-8859-1"), "UTF-8")。fileupload 默认用 ISO-8859-1 编码,需要手动转换。
问题七:页面缓存问题
学校系统有个特点:成绩数据经常更新,但浏览器可能会缓存旧页面。老师刚改完成绩,刷新页面看到的还是旧的。
解决方案:
- 在 JSP 页面头部禁用缓存
<%
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
%>
- URL 加时间戳参数
<a href="grade_list.jsp?t=<%= System.currentTimeMillis() %>">刷新成绩</a>
- 用 JavaScript 定时刷新
// 每30秒自动刷新成绩
setInterval(function() {
location.reload();
}, 30000);
实际项目中的踩坑经历
说几个我在这个项目里实际踩过的坑,都是真金白银换来的经验。
坑一:Tomcat 版本不兼容
最开始我们用的 Tomcat 8.5,JDK 8,一切正常。后来学校机房升级,装了 Tomcat 10,结果所有 JSP 页面都 404 了。查了半天才发现,Tomcat 10 把 javax.* 改成了 jakarta.*,所有 import 语句都要改。这个坑差点让我们返工一周。
教训: 部署前一定要确认服务器的 Tomcat 版本,最好写个简单的测试页面验证环境。
坑二:日期格式问题
MySQL 的 DATE 类型和 Java 的 java.util.Date 转换时出了诡异的问题。存储进去的日期总是差一天。最后发现是时区问题——数据库默认用 UTC,而服务器在北京时间。
// 解决方案:在 JDBC 连接字符串中指定时区
String url = "jdbc:mysql://localhost:3306/school_system" +
"?useUnicode=true" +
"&characterEncoding=UTF-8" +
"&serverTimezone=Asia/Shanghai";
坑三:大并发下的性能问题
系统上线后,每次录入成绩的时候,几十个老师同时提交,数据库连接池瞬间被占满。后来加了连接池配置:
<!-- HikariCP 配置 -->
<Context>
<Resource name="jdbc/school"
auth="Container"
type="javax.sql.DataSource"
maxTotal="50" <!-- 最大连接数 -->
maxIdle="20" <!-- 最大空闲连接 -->
minIdle="10" <!-- 最小空闲连接 -->
maxWaitMillis="10000" <!-- 最大等待时间 -->
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/school_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
username="root"
password="your_password"/>
</Context>
加了连接池之后,系统稳定了很多。
总结
JSP 在学校管理系统中确实还有很多应用场景。它不是什么新技术,甚至可以说是”过时”的技术,但它的简单、直接、易部署的特点,让它在学校这种预算有限、技术人才匮乏的环境中依然有生命力。
最关键的是要养成良好的编码习惯:用 PreparedStatement 防注入、用过滤器统一处理编码、用连接池管理数据库连接、用 JSTL 分离展示和业务逻辑。这些习惯一旦养成,以后不管换什么技术栈,都是受益的。
如果你正在做类似的学校系统项目,希望这篇文章能帮你少走一些弯路。有什么具体问题,欢迎留言讨论。
