说实话,刚接触 JSP(JavaServer Pages)的时候,很多人都会被那一堆嵌在 HTML 里的 Java 代码搞得晕头转向。但当你真正动手写出第一个能跑通的用户登录、成绩查询页面时,那种成就感是很实在的。今天我们就不聊枯燥的理论,直接上手做一个简易但完整的教育管理系统——包含用户注册、安全登录、教师录入成绩、学生查询成绩这几个核心功能。我会把每一个关键步骤的代码都摊开来讲,顺便把你可能遇到的那些“坑”和报错怎么解决,一次性说清楚。
项目结构:先把地基打牢
在写任何代码之前,你得有个清晰的项目结构。一个标准的 JSP Web 项目,通常会有这样的目录树。你不需要用复杂的框架,就用最基础的 Servlet + JSP + JDBC 组合,这样你才能真正理解数据是怎么流转的。
SchoolManagementSystem/
├── WebContent/
│ ├── WEB-INF/
│ │ └── web.xml
│ ├── css/
│ │ └── style.css
│ ├── login.jsp
│ ├── register.jsp
│ ├── dashboard.jsp
│ ├── teacher/
│ │ └── enterGrade.jsp
│ └── student/
│ └── viewGrade.jsp
├── src/
│ └── com/example/
│ ├── model/
│ │ ├── User.java
│ │ └── Grade.java
│ ├── dao/
│ │ ├── DBUtil.java
│ │ ├── UserDAO.java
│ │ └── GradeDAO.java
│ ├── servlet/
│ │ ├── RegisterServlet.java
│ │ ├── LoginServlet.java
│ │ ├── EnterGradeServlet.java
│ │ └── ViewGradeServlet.java
│ └── filter/
│ └── AuthFilter.java
└── pom.xml (如果用 Maven,不用也没关系)
你看,这个结构很清晰:model 放数据对象,dao 放数据库操作,servlet 放控制逻辑,filter 放权限拦截,jsp 放页面。记住,JSP 负责展示,Servlet 负责处理业务逻辑,别把两者混在一起,否则后期维护会让你想哭。
数据库设计:一切数据的源头
没有数据库,一切都是空谈。我们用最简单的 MySQL,建两张表:users 表存用户信息,grades 表存成绩。
CREATE DATABASE school_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE school_db;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('teacher', 'student') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE grades (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT NOT NULL,
course VARCHAR(100) NOT NULL,
score DECIMAL(5, 2) NOT NULL,
teacher_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (student_id) REFERENCES users(id),
FOREIGN KEY (teacher_id) REFERENCES users(id)
);
注意几个细节:第一,密码千万别存明文,后面我们会用 BCrypt 加密;第二,role 字段用来区分老师和学生,这是整个系统权限控制的基础;第三,外键约束保证数据的完整性,别嫌麻烦,这是好习惯。
模型层:让数据有形状
先定义 User 和 Grade 两个实体类。别小看这两个类,它们是后面所有逻辑的基石。
package com.example.model;
public class User {
private int id;
private String username;
private String password;
private String role;
// 构造器
public User() {}
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
// getter 和 setter
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
package com.example.model;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class Grade {
private int id;
private int studentId;
private String course;
private BigDecimal score;
private int teacherId;
private LocalDateTime createdAt;
// getter 和 setter 省略,自己补全,保持代码简洁
}
数据库工具类:连接池是关键
很多初学者直接用 DriverManager 每次获取连接,这在生产环境是大忌。我们用简单的连接池思想,或者至少用一个单例的 DBUtil。
package com.example.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/school_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASSWORD = "your_password"; // 换成你的密码
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException("数据库驱动加载失败", e);
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static void close(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
注意,Class.forName 这一步在 JDBC 4.0 之后其实可以省略,但写上更保险。密码千万别硬编码在代码里提交到 GitHub,后面我们会讲怎么避免这个问题。
用户注册:第一步就走对
注册页面的逻辑是:接收表单数据 → 验证输入 → 加密密码 → 插入数据库 → 跳转登录。
先看 register.jsp 页面,保持简洁:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>用户注册</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">
</head>
<body>
<h2>注册新账号</h2>
<!-- 注意:action 指向 Servlet,method 是 POST -->
<form action="${pageContext.request.contextPath}/register" method="POST">
<label>用户名:</label>
<input type="text" name="username" required><br>
<label>密码:</label>
<input type="password" name="password" required><br>
<label>确认密码:</label>
<input type="password" name="confirmPassword" required><br>
<label>角色:</label>
<select name="role">
<option value="student">学生</option>
<option value="teacher">教师</option>
</select><br>
<button type="submit">注册</button>
</form>
<p>已有账号?<a href="${pageContext.request.contextPath}/login.jsp">去登录</a></p>
</body>
</html>
对应的 RegisterServlet.java:
package com.example.servlet;
import com.example.dao.UserDAO;
import com.example.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
private UserDAO userDAO = new UserDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8"); // 防止中文乱码,这步很重要!
String username = request.getParameter("username");
String password = request.getParameter("password");
String confirmPassword = request.getParameter("confirmPassword");
String role = request.getParameter("role");
// 基本验证
if (username == null || username.trim().isEmpty() ||
password == null || password.trim().isEmpty()) {
request.setAttribute("error", "用户名和密码不能为空");
request.getRequestDispatcher("/register.jsp").forward(request, response);
return;
}
if (!password.equals(confirmPassword)) {
request.setAttribute("error", "两次密码不一致");
request.getRequestDispatcher("/register.jsp").forward(request, response);
return;
}
// 创建用户对象,密码在这里加密
User user = new User(username, password, role);
try {
boolean success = userDAO.register(user);
if (success) {
// 注册成功,重定向到登录页,避免表单重复提交
response.sendRedirect(request.getContextPath() + "/login.jsp");
} else {
request.setAttribute("error", "用户名已存在,请更换用户名");
request.getRequestDispatcher("/register.jsp").forward(request, response);
}
} catch (Exception e) {
e.printStackTrace();
request.setAttribute("error", "注册失败:" + e.getMessage());
request.getRequestDispatcher("/register.jsp").forward(request, response);
}
}
}
这里有个关键点:request.setCharacterEncoding("UTF-8")。如果你不做这一步,中文用户名或者报错信息在控制台或者页面上显示出来就是一堆乱码,这是新手最常踩的坑之一。
用户登录:安全第一
登录逻辑比注册稍微复杂一点,因为要验证密码。我们使用 BCrypt 来加密存储密码,这是目前 Java 生态中最推荐的方案。
先引入依赖,如果你用 Maven,在 pom.xml 加上:
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
然后在 UserDAO.java 中实现注册和登录方法:
package com.example.dao;
import com.example.model.User;
import org.mindrot.jbcrypt.BCrypt;
import java.sql.*;
public class UserDAO {
// 注册:密码加密后存入数据库
public boolean register(User user) {
String sql = "INSERT INTO users (username, password, role) VALUES (?, ?, ?)";
String hashedPassword = BCrypt.hashpw(user.getPassword(), BCrypt.gensalt());
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, user.getUsername());
ps.setString(2, hashedPassword);
ps.setString(3, user.getRole());
return ps.executeUpdate() > 0;
} catch (SQLException e) {
// 如果是重复键错误,返回 false
if (e.getErrorCode() == 1062) { // MySQL 重复键错误码
return false;
}
throw new RuntimeException("注册失败", e);
}
}
// 登录:验证密码
public User login(String username, String password) {
String sql = "SELECT id, username, password, role FROM users WHERE username = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String hashedPassword = rs.getString("password");
// 验证密码是否匹配
if (BCrypt.checkpw(password, hashedPassword)) {
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setRole(rs.getString("role"));
return user;
}
}
} catch (SQLException e) {
throw new RuntimeException("登录查询失败", e);
}
return null;
}
}
对应的 LoginServlet.java:
package com.example.servlet;
import com.example.dao.UserDAO;
import com.example.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private UserDAO userDAO = new UserDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userDAO.login(username, password);
if (user != null) {
// 登录成功,把用户信息存入 Session
HttpSession session = request.getSession();
session.setAttribute("user", user);
session.setAttribute("isLoggedIn", true);
// 根据角色跳转不同页面
if ("teacher".equals(user.getRole())) {
response.sendRedirect(request.getContextPath() + "/teacher/enterGrade.jsp");
} else {
response.sendRedirect(request.getContextPath() + "/student/viewGrade.jsp");
}
} else {
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
权限拦截器:防止未登录用户访问
这才是系统的精髓所在。如果没有这个过滤器,任何知道 URL 的人都能直接访问成绩页面。我们用 Servlet Filter 来实现。
package com.example.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@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 uri = req.getRequestURI();
// 放行静态资源和登录、注册相关请求
if (uri.endsWith(".jsp") || uri.contains("/css") ||
uri.equals(req.getContextPath() + "/login") ||
uri.equals(req.getContextPath() + "/register") ||
uri.equals(req.getContextPath() + "/login.jsp") ||
uri.equals(req.getContextPath() + "/register.jsp")) {
chain.doFilter(request, response);
return;
}
HttpSession session = req.getSession(false);
boolean isLoggedIn = session != null && session.getAttribute("isLoggedIn") != null;
if (isLoggedIn) {
chain.doFilter(request, response);
} else {
// 未登录,重定向到登录页
resp.sendRedirect(req.getContextPath() + "/login.jsp");
}
}
}
这个过滤器会拦截所有请求,如果不是登录或注册页面,就检查 Session 中是否有 isLoggedIn 标记。没有的话,直接踢回登录页。这就是为什么你直接访问 /teacher/enterGrade.jsp 会被重定向到登录页的原因。
成绩录入:教师端的体验
教师登录后,应该能看到一个简洁的成绩录入页面。这个页面需要展示所有学生,然后教师可以选择学生、输入课程和成绩。
teacher/enterGrade.jsp:
”`jsp <%@ page contentType=“text/html;charset=UTF-8” language=“java” %> <%@ taglib prefix=“c” uri=”http://java.sun.com/jsp/jstl/core” %> <%@ page import=“com.example.model.User” %> <!DOCTYPE html>
<title>录入成绩 - 教师端</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">
<%
User currentUser = (User) session.getAttribute("user");
if (currentUser == null || !"teacher".equals(currentUser.getRole())) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
%>
<h2>欢迎,<%= currentUser.getUsername() %> 老师</h2>
<a href="${pageContext.request.contextPath}/logout">退出登录</a>
<h3>录入成绩</h3>
<c:if test="${not empty error}">
<p style="color: red;">${error}</p>
</c:if>
<form action="${pageContext.request.contextPath}/enterGrade" method="POST">
<label>选择学生:</label>
<select name="studentId" required>
<option value="">-- 请选择 --</option>
<!-- 这里假设我们有一个隐藏的层或者 AJAX 加载学生列表 -->
<!-- 为了简化,我们在 Servlet 中预先查好学生列表放进 request -->
<c:forEach var="student" items="${students}">
<option value="${student.id}">${student.username}</option>
</c:forEach>
</select><br>
<label>课程名称:</label>
<input type="text" name="course" required placeholder="例如:高等数学"><br>
<label>成绩(0-100):</label>
<input type="number" name="score" min="0" max="100" step
