说到做毕业设计或者课程项目,很多同学在“用什么技术”这个问题上纠结半天。最后往往还是回归到最朴实、最稳妥的选择——JSP + Servlet + JDBC。别笑,这不是因为大家没追求,而是因为这些技术虽然“老”,但它们的逻辑清晰得就像透明玻璃。你想知道数据怎么从页面跑到数据库,再跑回来显示,JSP 是最好的老师。

今天咱们不聊那些虚头巴脑的理论,就手把手带你搭一个涵盖学生选课成绩管理两大核心功能的校园管理平台。我会把你当成一个刚接触后端开发的大学生,咱们一步步把代码敲出来,确保你不仅能跑起来,还能真正理解每一行在干什么。

为什么是 JSP?它过时了吗?

首先得回答这个扎心的问题。在现代互联网大厂,前后端分离已经是标配,Vue、React 满天飞。但是,对于中小型的内部管理后台、教学演示系统,或者你的毕业设计,JSP 依然有其独特的优势:

  1. 所见即所得的调试体验:HTML、CSS、Java 代码混写在 .jsp 文件里,改完刷新就能看效果,对初学者极其友好。
  2. 原生 Java 生态:你不需要学习新的框架语法,只要懂 Java,就能写 Web 应用。
  3. 部署简单:Tomcat 一跑,WAR 包一丢,完事。

当然,我也得实话实说,JSP 不适合处理高并发和复杂的前端交互逻辑。但对于“管理学生信息”、“录入成绩”这种典型的 CRUD(增删改查)场景,它轻快、高效,足够用到天荒地老。

第一步:地基打得牢,楼才盖得高

在写第一行代码之前,我们先要确定数据库结构。很多人跳过这步直接写代码,结果写到一半发现“哎?学生表怎么没有班级ID?”这种问题,返工最痛苦。

我们需要三张核心表:学生表课程表选课关系表,以及最后要提的成绩表

-- 创建数据库
CREATE DATABASE campus_manage_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE campus_manage_db;

-- 1. 学生表
CREATE TABLE students (
    student_id VARCHAR(20) PRIMARY KEY COMMENT '学号',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    password VARCHAR(50) NOT NULL COMMENT '登录密码',
    major VARCHAR(50) COMMENT '专业',
    class_name VARCHAR(50) COMMENT '班级'
);

-- 2. 课程表
CREATE TABLE courses (
    course_id VARCHAR(20) PRIMARY KEY COMMENT '课程编号',
    course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
    credits INT NOT NULL COMMENT '学分',
    teacher VARCHAR(50) COMMENT '授课教师',
    capacity INT NOT NULL DEFAULT 50 COMMENT '选课容量'
);

-- 3. 选课记录表(连接学生和课程)
CREATE TABLE selections (
    id INT AUTO_INCREMENT PRIMARY KEY,
    student_id VARCHAR(20) NOT NULL,
    course_id VARCHAR(20) NOT NULL,
    selection_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (student_id) REFERENCES students(student_id),
    FOREIGN KEY (course_id) REFERENCES courses(course_id),
    UNIQUE KEY unique_selection (student_id, course_id)
);

-- 4. 成绩表(可选,如果选课即有成绩,可合并;这里分开更清晰)
CREATE TABLE scores (
    id INT AUTO_INCREMENT PRIMARY KEY,
    student_id VARCHAR(20) NOT NULL,
    course_id VARCHAR(20) NOT NULL,
    score DECIMAL(5,2) COMMENT '分数',
    grade CHAR(2) COMMENT '等级',
    FOREIGN KEY (student_id) REFERENCES students(student_id),
    FOREIGN KEY (course_id) REFERENCES courses(course_id),
    UNIQUE KEY unique_score (student_id, course_id)
);

你看,这个结构是不是特别清晰?selections 表就是典型的“多对多”关系解析表。一个学生可以选多门课,一门课可以有多个学生选,靠这个中间表就搞定了一切。

第二步:连接数据库的“桥梁”

在 JSP 时代,我们通常不会在每一个页面里都写一遍 DriverManager.getConnection()。那样太乱,而且改个数据库密码要改几十个文件。我们需要一个 DBUtil 工具类。

这是一个非常经典的实现,建议直接保存到你的项目工具包中:

package com.campus.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtil {
    // 配置信息,建议后续移入 properties 文件
    private static final String URL = "jdbc:mysql://localhost:3306/campus_manage_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
    private static final String USER = "root";
    private static final String PASSWORD = "123456"; // 换成你自己的数据库密码
    private static final String DRIVER = "com.mysql.cj.jdbc.Driver";

    static {
        try {
            Class.forName(DRIVER);
        } catch (ClassNotFoundException e) {
            System.err.println("数据库驱动加载失败:" + e.getMessage());
        }
    }

    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();
            }
        }
    }
}

这里有个小细节要注意:useSSL=false 是为了避免 MySQL 8.0+ 版本默认的 SSL 连接警告,serverTimezone=UTC 则是解决时区问题的标准做法。如果你发现存入数据库的时间少了8个小时,通常就是时区没配对。

第三步:选课功能——最核心的业务逻辑

选课是学生端最高频的操作。我们来模拟一个场景:学生登录系统,查看所有可选课程,点击“选课”,系统检查容量,插入记录,返回结果。

首先,我们需要一个 Course 实体类:

package com.campus.model;

public class Course {
    private String courseId;
    private String courseName;
    private int credits;
    private String teacher;
    private int capacity;
    private int currentCount; // 当前已选人数,前端显示用

    // 省略 getter/setter,实际开发中记得补全
    public String getCourseId() { return courseId; }
    public void setCourseId(String courseId) { this.courseId = courseId; }
    public String getCourseName() { return courseName; }
    public void setCourseName(String courseName) { this.courseName = courseName; }
    public int getCredits() { return credits; }
    public void setCredits(int credits) { this.credits = credits; }
    public String getTeacher() { return teacher; }
    public void setTeacher(String teacher) { this.teacher = teacher; }
    public int getCapacity() { return capacity; }
    public void setCapacity(int capacity) { this.capacity = capacity; }
    public int getCurrentCount() { return currentCount; }
    public void setCurrentCount(int currentCount) { this.currentCount = currentCount; }
}

接下来是关键。很多初学者写 JSP 时会直接把 SQL 语句写在 .jsp 页面里,千万别这么干。我们要用 Servlet 做控制器,JSP 只负责显示。

假设我们有一个 CourseServlet,处理查询课程列表的逻辑:

@WebServlet("/courses")
public class CourseServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        List<Course> courseList = new ArrayList<>();
        Connection conn = null;
        
        try {
            conn = DBUtil.getConnection();
            // 这里用了一个关联查询,顺便查出每门课已选人数
            String sql = "SELECT c.*, COUNT(s.id) as cnt FROM courses c " +
                         "LEFT JOIN selections s ON c.course_id = s.course_id " +
                         "GROUP BY c.course_id";
            
            PreparedStatement ps = conn.prepareStatement(sql);
            ResultSet rs = ps.executeQuery();
            
            while(rs.next()) {
                Course course = new Course();
                course.setCourseId(rs.getString("course_id"));
                course.setCourseName(rs.getString("course_name"));
                course.setCredits(rs.getInt("credits"));
                course.setTeacher(rs.getString("teacher"));
                course.setCapacity(rs.getInt("capacity"));
                course.setCurrentCount(rs.getInt("cnt"));
                courseList.add(course);
            }
            
            // 将数据存入请求域,转发给 JSP 页面
            request.setAttribute("courseList", courseList);
            request.getRequestDispatcher("/pages/student/courses.jsp").forward(request, response);
            
        } catch (SQLException e) {
            e.printStackTrace();
            response.sendRedirect("/error.jsp");
        } finally {
            DBUtil.close(conn);
        }
    }
}

对应的 courses.jsp 页面,我们使用 JSTL 标签库来遍历列表,这样 HTML 看起来非常整洁:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>可选课程列表</title>
    <style>
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: center; }
        th { background-color: #4CAF50; color: white; }
        .enroll-btn { background-color: #2196F3; color: white; padding: 5px 10px; text-decoration: none; border-radius: 3px; }
        .full { color: red; font-weight: bold; }
    </style>
</head>
<body>
    <h2>本学期可选课程</h2>
    <table>
        <tr>
            <th>课程编号</th>
            <th>课程名称</th>
            <th>学分</th>
            <th>授课教师</th>
            <th>剩余名额</th>
            <th>操作</th>
        </tr>
        <c:forEach var="course" items="${courseList}">
            <tr>
                <td>${course.courseId}</td>
                <td>${course.courseName}</td>
                <td>${course.credits}</td>
                <td>${course.teacher}</td>
                <td>
                    <c:choose>
                        <c:when test="${course.capacity - course.currentCount > 0}">
                            ${course.capacity - course.currentCount}
                        </c:when>
                        <c:otherwise>
                            <span class="full">已满</span>
                        </c:otherwise>
                    </c:choose>
                </td>
                <td>
                    <c:if test="${course.capacity - course.currentCount > 0}">
                        <a href="${pageContext.request.contextPath}/selectCourse?id=${course.courseId}" class="enroll-btn">选课</a>
                    </c:if>
                </td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

第四步:成绩管理——老师的特权,学生的关注点

成绩管理涉及两个角色:老师录入,学生查询。我们先看老师端如何录入成绩。

这里需要用到一个 ScoreServlet,它负责接收表单提交的成绩数据,并更新数据库。

@WebServlet("/score")
public class ScoreServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        
        String studentId = request.getParameter("studentId");
        String courseId = request.getParameter("courseId");
        String scoreStr = request.getParameter("score");
        
        try {
            Connection conn = DBUtil.getConnection();
            
            // 先判断是否已有成绩记录,有则更新,无则插入
            String checkSql = "SELECT id FROM scores WHERE student_id = ? AND course_id = ?";
            PreparedStatement checkPs = conn.prepareStatement(checkSql);
            checkPs.setString(1, studentId);
            checkPs.setString(2, courseId);
            ResultSet rs = checkPs.executeQuery();
            
            String sql;
            PreparedStatement ps;
            
            if (rs.next()) {
                // 更新已有成绩
                sql = "UPDATE scores SET score = ?, grade = CASE WHEN ? >= 90 THEN 'A' WHEN ? >= 80 THEN 'B' WHEN ? >= 60 THEN 'C' ELSE 'D' END WHERE student_id = ? AND course_id = ?";
                ps = conn.prepareStatement(sql);
                double score = Double.parseDouble(scoreStr);
                ps.setDouble(1, score);
                ps.setDouble(2, score);
                ps.setDouble(3, score);
                ps.setDouble(4, score);
                ps.setString(5, studentId);
                ps.setString(6, courseId);
            } else {
                // 插入新成绩
                sql = "INSERT INTO scores (student_id, course_id, score, grade) VALUES (?, ?, ?, ?)";
                ps = conn.prepareStatement(sql);
                ps.setString(1, studentId);
                ps.setString(2, courseId);
                ps.setDouble(3, Double.parseDouble(scoreStr));
                ps.setString(4, calculateGrade(Double.parseDouble(scoreStr)));
            }
            
            int rows = ps.executeUpdate();
            if (rows > 0) {
                response.getWriter().write("{\"success\": true, \"msg\": \"成绩保存成功\"}");
            } else {
                response.getWriter().write("{\"success\": false, \"msg\": \"保存失败\"}");
            }
            
            DBUtil.close(conn);
            
        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("{\"success\": false, \"msg\": \"系统异常\"}");
        }
    }
    
    private String calculateGrade(double score) {
        if (score >= 90) return "A";
        if (score >= 80) return "B";
        if (score >= 60) return "C";
        return "D";
    }
}

你可能会问,为什么要用 JSON 格式返回?其实对于这个传统 JSP 项目,直接重定向回上一页也是可以的。但用 AJAX 异步提交成绩,体验会更流畅,页面不会闪烁。不过,考虑到“易上手”的原则,我们也可以直接在 Servlet 里处理完重定向:

response.sendRedirect(request.getContextPath() + "/teacher/scores.jsp?msg=success");

然后在页面上显示提示消息即可。

对于学生查询成绩,逻辑就简单多了。直接 SELECT * FROM scores JOIN courses ON ... WHERE student_id = ? 即可。这里我不再重复贴代码,你可以参考上面的模式,自己尝试写一个 ScoreServletdoGet 方法。

第五步:让系统“活”起来——登录与权限控制

一个完整的系统,必须要有登录。没有登录,谁都能进系统改成绩,那可就乱套了。

我们可以用 Session 来实现简单的身份验证。

LoginServlet 中:

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    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 role = request.getParameter("role"); // "student" or "teacher"

        try {
            Connection conn = DBUtil.getConnection();
            String sql = "SELECT * FROM students WHERE student_id = ? AND password = ?";
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, username);
            ps.setString(2, password);
            ResultSet rs = ps.executeQuery();
            
            if (rs.next()) {
                // 登录成功,存入 Session
                HttpSession session = request.getSession();
                session.setAttribute("user", rs.getString("student_id"));
                session.setAttribute("role", role);
                session.setAttribute("name", rs.getString("name"));
                
                if ("teacher".equals(role)) {
                    // 实际项目中老师表和学生表是分开的,这里简化演示
                    response.sendRedirect(request.getContextPath() + "/teacher/index.jsp");
                } else {
                    response.sendRedirect(request.getContextPath() + "/student/index.jsp");
                }
            } else {
                response.sendRedirect("login.jsp?error=1");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

更重要的是,我们需要一个 Filter(过滤器) 来拦截未登录的请求。这是防止学生直接输入 /teacher/index.jsp 就能访问老师后台的关键。

@WebFilter("/*")
public class AuthFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        
        String uri = req.getRequestURI();
        
        // 放行静态资源和登录页
        if (uri.equals("/login.jsp") || uri.contains("/static/") || uri.equals("/login")) {
            chain.doFilter(request, response);
            return;
        }
        
        HttpSession session = req.getSession();
        if (session.getAttribute("user") == null) {
            res.sendRedirect(req.getContextPath() + "/login.jsp");
            return;
        }
        
        chain.doFilter(request, response);
    }
}

有了这个 Filter,任何没有登录的用户尝试访问受保护页面,都会被“请回”登录页。这就像学校图书馆的大门,没带校园卡,保安(Filter)是不会让你