简介:面向高校计算机专业课程设计与毕业设计场景的JSP学生信息管理系统项目包,采用BS架构,基于JSP与SQL Server实现,适合需要参考完整前后端交互、数据库设计及答辩展示的Java Web学习者。资源共55个文件,以42个JSP页面为核心,兼顾页面展示和业务处理;同时包含3个用于答辩或演示的PPT、1个DOC设计文档、SQL Server数据库文件(MDF/LDF)以及若干JPG界面图片,压缩包整体2.45MB,便于下载后快速部署。项目源码已经过测试校正,可百分百运行,导入IDE即可体验完整功能;数据库文件可快速还原系统数据,JPG图片可作为界面设计参考,文档与PPT则有助于梳理设计思路和答辩汇报。已有798人学习下载,适合作为课程设计、毕业设计或Java Web入门实践的直接参考。
1. JSP+SQL Server 做学生信息管理:不是新技术的存量组合,是课设必答的实战题
JSP + SQL Server 这个组合,在今天的招聘 JD 里几乎绝迹,但在课设、毕设和大量存量的老旧教务系统里,它依然是真实跑着的一整套技术栈。标题里这个学生信息管理系统,本质上是一次“浏览器发请求 → Tomcat 中的 Servlet 处理 → JDBC 读写 SQL Server → 页面回显”的全链路实战,适合正在做课设的学生、刚接手老系统的新人,以及想补 Java Web 基础链路的人。这里不引入 Spring Boot,只讨论这套最简架构怎么把系统做出来、跑起来、交得出去。下文按“原理 → 建库 → 搭骨架 → 实现 CRUD → 排坑 → 加分项”的顺序展开。
2. 先理解这套技术栈的运行时:JSP 的编译机制与 SQL Server 的 3 个选型差异
2.1 JSP 不是“写死的网页”,它等价于一个 Servlet
很多第一次写 JSP 的同学,以为 JSP 就是“HTML 里嵌几个 Java 片段”,跑起来页面长什么样由标签决定。这个理解不够:JSP 文件第一次被访问时,Tomcat 会把它翻译成一个.java源文件,再编译成.class,翻译结果是一个以_jspService()为核心方法的 Servlet 类。页面上所有 HTML 标签在翻译后都变成了out.write()输出语句,而<% ... %>里的 Java 代码会原样进入方法体。
这个机制解释了三个现象。第一,JSP 和 Servlet 本质是同一样东西,所以 JSP 能直接request、response、session内置对象,它不是模板引擎,而是 Servlet 的一种书写形式。第二,JSP 里定义的变量全是_jspService()方法的局部变量,跨请求不共享,想存登录状态得用 Session,不能靠 JSP 的实例变量。第三,也是最重要的——JSP 里什么 Java 代码都能写,正因为能写,才特别容易写烂,后面讲 JSTL 瘦身时我会专门回来说这一点。
2.2 为什么选 SQL Server 而不是 MySQL:驱动、端口、认证差异一览
如果题目没指定数据库,我一般建议用 MySQL,资料多、默认配置对新手友好。但标题明确写了 SQL Server,那就要知道 SQL Server 和 MySQL 在 Java Web 里的真实差异,否则按 MySQL 的惯性写 JDBC,第一行连接串就翻车。
| 对比项 | SQL Server | MySQL |
|---|---|---|
| JDBC 驱动类 | com.microsoft.sqlserver.jdbc.SQLServerDriver | com.mysql.cj.jdbc.Driver |
| 连接串前缀 | jdbc:sqlserver:// | jdbc:mysql:// |
| 默认端口 | 1433 | 3306 |
| 自增语法 | IDENTITY(1,1) | AUTO_INCREMENT |
| 分页写法 | OFFSET...FETCH或ROW_NUMBER() | LIMIT |
| 字符串类型 | VARCHAR+ 排序规则 | VARCHAR按字符集 |
| 认证模式 | Windows 认证 / 混合认证 | 仅账号密码 |
SQL Server 里最容易踩的第一个坑是“实例名”。本机安装默认实例时连接串写localhost:1433没问题,但如果你装的是命名实例,比如localhost\SQLEXPRESS,连接串就得写成jdbc:sqlserver://localhost:1433;instanceName=SQLEXPRESS;databaseName=student_db,漏了instanceName会报“找不到服务器实例”。另外 SQL Server 默认是 Windows 认证,Java 程序用sa账号登录前,必须先把服务器切成混合认证模式,这一步留在避坑章细说。
2.3 学生信息管理系统的功能边界:先用一张模块表圈住需求
课设最常见的翻车不是代码写不出来,而是需求越做越大,最后什么都缺角。我建议动手前先把功能边界画清楚:登录管理、学生信息 CRUD、班级字典管理、按专业统计人数,这四件事是一个“学生信息管理系统”的最小可交付集合。课程表和成绩表可以并入,字段设计时预留即可,不要一开始就做考勤、选课、宿舍分配这些外延功能。
| 模块 | 页面 | 后端动作 | 要点 |
|---|---|---|---|
| 登录 | login.jsp | LoginServlet 校验账号密码 | Session 保存登录态 |
| 学生管理 | student_list.jsp | StudentServlet 分页查询 | SQL Server 分页语法 |
| 新增 / 编辑 | student_edit.jsp | 表单提交到 Insert / Update Servlet | PreparedStatement 参数绑定 |
| 删除 | 列表页勾选 | BatchDelete Servlet | IN 子句动态占位符 |
| 统计 | statistics.jsp | 按班级 / 专业 GROUP BY | 结果集转 JSON 或直接表格 |
这张表同时是开发顺序:先做登录和数据库连接,再做学生列表分页,之后才是增删改和统计。数据库设计永远排在代码前面,接下来建表。
3. 数据库先行:5 张核心表的建表 SQL 与字段设计的关键选择
3.1 核心表结构与建表脚本:用户表、班级表、学生表、课程表、成绩表
数据库是整套系统的地基。表拆得合理,后面的 DAO 层写起来几乎是一路顺风。下面这套结构是这类系统最常见的传统设计:t_user管登录,t_class管班级字典,t_student存学生主信息,t_course和t_score组成成绩域。建表顺序有讲究——先建被引用的班级表和课程表,再建引用它们的学生表和成绩表,否则外键会报错。
-- 1. 用户表(登录用) CREATE TABLE t_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存哈希,不存明文 real_name NVARCHAR(50), role TINYINT DEFAULT 1, -- 1管理员 2普通用户 status TINYINT DEFAULT 1, -- 1启用 0禁用 create_time DATETIME DEFAULT GETDATE() ); -- 2. 班级表 CREATE TABLE t_class ( class_id INT IDENTITY(1,1) PRIMARY KEY, class_name NVARCHAR(50) NOT NULL, major_name NVARCHAR(50), -- 专业名称 grade VARCHAR(10), -- 年级,如 2024 teacher_name NVARCHAR(50) ); -- 3. 学生表(先建班级表再建它) CREATE TABLE t_student ( student_no VARCHAR(20) PRIMARY KEY, -- 学号,字符型 student_name NVARCHAR(50) NOT NULL, gender CHAR(1) CHECK (gender IN ('男','女')), birth_date DATE, class_id INT FOREIGN KEY REFERENCES t_class(class_id), phone VARCHAR(20), email VARCHAR(50), address NVARCHAR(100), enroll_date DATE, status TINYINT DEFAULT 1, -- 1在读 0离校 create_time DATETIME DEFAULT GETDATE() ); -- 4. 课程表 CREATE TABLE t_course ( course_id INT IDENTITY(1,1) PRIMARY KEY, course_name NVARCHAR(50) NOT NULL, credit DECIMAL(3,1), teacher NVARCHAR(50) ); -- 5. 成绩表 CREATE TABLE t_score ( score_id INT IDENTITY(1,1) PRIMARY KEY, student_no VARCHAR(20) FOREIGN KEY REFERENCES t_student(student_no), course_id INT FOREIGN KEY REFERENCES t_course(course_id), score DECIMAL(5,2), exam_date DATE, UNIQUE (student_no, course_id) );脚本里用了IDENTITY(1,1),这是 SQL Server 的自增语法,区别于 MySQL 的AUTO_INCREMENT。t_student.student_no设为主键,因为学号天然唯一;但如果你担心学号录入错误要改,也可以加一个自增的student_id作为主键,student_no只加唯一索引。两种方案都能用,我倾向于后者——业务主键和物理主键分离,后续做批量删除、关联成绩表时更干净。
3.2 表字段设计里 4 个不吐不快的细节
第一个是学号必须用VARCHAR而不是INT。学号有可能是“2024010101”这种带前导零的字符串,用 int 存会丢掉前导零,还可能在超长后溢出;更不用说有的学校学号里带字母。第二个是性别字段用CHAR(1)加 CHECK 约束,可读性比 0/1 好,查询结果直接显示男/女,省一层翻译。第三个是所有表都留create_time,用GETDATE()做默认值,录入数据时不花代码维护时间。
第三个细节是多音字和排序规则。SQL Server 建库时如果排序规则不是中文相关(比如Chinese_PRC_CI_AS),NVARCHAR存中文没问题,但排序、查询时可能表现诡异。建库语句里显式指定排序规则是最省心的做法。第四个是“逻辑删除 vs 物理删除”:课设阶段直接 DELETE 完全没问题,但如果你想让系统更像生产环境,给t_student加一个status字段,删除时 UPDATE 置 0,列表查询默认带WHERE status = 1——这也是很多老系统的真实做法。
3.3 初始化数据:管理员账号与测试学生
建表之后立刻插入少量基础数据,否则页面调通了也没内容可看。管理员账号密码这里直接用哈希值,下面脚本里password是“admin123”加盐后的 SHA-256 结果,具体生成代码放到第 5 章展示。
-- 管理员:admin / admin123 INSERT INTO t_user (username, password, real_name, role) VALUES ('admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', '系统管理员', 1); -- 两个班级 INSERT INTO t_class (class_name, major_name, grade, teacher_name) VALUES ('计科2401班', '计算机科学与技术', '2024', '张老师'), ('软工2401班', '软件工程', '2024', '李老师'); -- 学生举例 INSERT INTO t_student (student_no, student_name, gender, birth_date, class_id, phone, email, enroll_date) VALUES ('2024010101', '王小明', '男', '2005-06-12', 1, '13800000001', 'wxm@example.com', '2024-09-01'), ('2024020101', '刘芳', '女', '2006-02-23', 2, '13800000002', 'liufang@example.com', '2024-09-01');插入后要立刻做一件事:用 SQL Server 的查询工具跑一遍SELECT * FROM t_student,确认中文没有变成问号。如果这个环节乱码,后面代码层面再怎么调都很难彻底解决,这属于数据库排序规则的锅,不是 Java 的锅。
4. 打通 JDBC 连接层:从空项目到登录跑通的最小骨架
4.1 项目目录结构与 web.xml:先让 Tomcat 认识你的工程
很多同学拿到一个项目压缩包,第一步就卡在“这工程怎么导进 IDEA 都跑不起来”。其实这类 JSP 项目的目录结构是高度统一的:源码放src,页面放web,依赖 jar 放web/WEB-INF/lib。记住一个铁律:WEB-INF下的文件不能通过 URL 直接访问,所以 JSP 页面要放在web根目录下,Servlet 的编译产物和 web.xml 放WEB-INF里。
student-management/ ├── src/ │ └── com/example/ │ ├── filter/EncodingFilter.java │ ├── servlet/LoginServlet.java │ ├── dao/UserDao.java │ ├── entity/User.java │ └── util/DBUtil.java ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ │ │ └── mssql-jdbc-10.2.0.jre8.jar │ ├── login.jsp │ ├── student_list.jsp │ └── student_edit.jspweb.xml 是让 Tomcat 认识路由的关键。下面这个配置做了三件事:注册一个编码过滤器(解决 POST 中文乱码,位置必须放最前面)、注册 LoginServlet 并映射到/login路径、设置欢迎页为login.jsp。
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.example.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> </web-app>过滤器映射的<url-pattern>/*</url-pattern>会拦截所有请求,包括静态资源。对于这个项目足够用,但如果工程里引入图片和 CSS,需要在过滤器里对静态文件类型放行,否则会多一层无谓的字符转换开销。
4.2 连接 SQL Server 的 DBUtil:驱动加载、连接串参数、关闭顺序
DBUtil 是整套系统的生命线,数据库连不上,后面全白搭。我习惯先写一个最小版本,在main方法里跑通连接,再往上搭页面。下面这个类包含了驱动注册、获取连接、释放资源三件事:
package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; private static final String URL = "jdbc:sqlserver://localhost:1433;databaseName=student_db;" + "encrypt=false;trustServerCertificate=true;loginTimeout=5"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("SQL Server JDBC 驱动加载失败,检查 jar 是否在 WEB-INF/lib 下"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, "sa", "你的密码"); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) { } } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) { } } if (conn != null) { try { conn.close(); } catch (SQLException ignored) { } } } }连接串参数逐个说明:encrypt=false表示不启用 TLS 加密,早期 mssql-jdbc 驱动默认不加密,新版驱动默认却变成了 true,导致老项目换个驱动就报 SSL 错误,建议显式写出来;trustServerCertificate=true配合自签名证书场景,课设环境必须加;loginTimeout=5让连接失败时 5 秒内快速报错,而不是卡几十秒。注意 SQL Server 驱动里没有 MySQL 那种serverTimezone参数,日期时区问题基本不用处理。
4.3 最小业务闭环:LoginServlet + UserDao + login.jsp 的完整链路
登录是第一个要实现的功能,也是理解 MVC 分层的最好样例。请求路径是:浏览器提交表单 → Tomcat 按 web.xml 映射找到 LoginServlet → Servlet 调 UserDao 查库 → 查到了存 Session → 重定向到列表页;查不到回到登录页并提示错误。先看 UserDao,它只干一件事,按用户名和密码查用户:
package com.example.dao; import com.example.entity.User; import com.example.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsernameAndPassword(String username, String passwordHash) { String sql = "SELECT user_id, username, real_name, role FROM t_user " + "WHERE username = ? AND password = ? AND status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, passwordHash); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setUserId(rs.getInt("user_id")); u.setUsername(rs.getString("username")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getInt("role")); return u; } } return null; } catch (SQLException e) { throw new RuntimeException("查询用户失败", e); } } }这里用了PreparedStatement,即使登录查询也不能拼接字符串——username里带一个单引号就能让拼接式 SQL 面目全非。LoginServlet 的逻辑是:从 request 取参数 → 对密码做哈希 → 调 DAO → 成功则写 Session 并重定向,失败则带错误信息回登录页:
package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; import com.example.dao.UserDao; import com.example.entity.User; import com.example.util.PasswordUtil; public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.findByUsernameAndPassword( username, PasswordUtil.sha256(password, username)); if (user == null) { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect("student_list"); } }login.jsp只需要一个表单,action 指向/login,method 用 post。注意doPost里的req.setCharacterEncoding("UTF-8")与 web.xml 里的过滤器重复了,这是防御性写法,过滤器的位置如果配置不对,Servlet 里这行能兜底。密码哈希工具类PasswordUtil的实现放到第 5 章和第 7 章的代码里讲,这里先记住原则:密码绝不存明文。到此登录链路跑通,整个系统的最小骨架成立,后面都是 CRUD 的扩展。
5. 把 CRUD 做完整:分页列表、新增编辑、批量删除与登录会话
5.1 学生列表分页:OFFSET-FETCH 和 ROW_NUMBER() 两种 SQL Server 写法
列表分页是每个管理系统的刚需。SQL Server 的分页写法不像 MySQL 那样一句LIMIT走天下,它有两种主流姿势:SQL Server 2012 及以上版本用OFFSET-FETCH,更老的版本用ROW_NUMBER()子查询。先看新版写法:
SELECT * FROM t_student ORDER BY student_no OFFSET 10 ROWS -- 跳过前 10 行 FETCH NEXT 5 ROWS ONLY; -- 取接下来的 5 行配套的 DAO 方法要做的就是把OFFSET和FETCH NEXT后面的数字换成参数位置:
public List<Student> findPage(int page, int pageSize) { String sql = "SELECT * FROM t_student ORDER BY student_no " + "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"; // page 从 1 开始,offset = (page - 1) * pageSize try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); // 执行并封装 List<Student>,略 } catch (SQLException e) { throw new RuntimeException("分页查询失败", e); } }如果你负责维护的老数据库是 SQL Server 2008 R2,上面这句会直接报语法错误。这时要用兼容写法:先用ROW_NUMBER()给结果编号,外层再按编号范围过滤。第 6 章避坑部分有这条的具体现象描述。分页另一个容易忽略的配套动作是算总页数,需要单独一条SELECT COUNT(*) FROM t_student,将总记录数除以 pageSize 向上取整,列表页才画得出一二三页的页码条。
5.2 新增与编辑:用 PreparedStatement 挡住 SQL 注入
新增学生的 SQL 看似简单,却正是注入重灾区。反面教材长这样:"INSERT INTO t_student VALUES ('" + studentNo + "', '" + studentName + "')"——因为studentName这个变量的值完全由用户输入控制,一旦表单里输入'); DROP TABLE t_student;--,拼接出的 SQL 就变成了一段能删表的恶意语句。这不是理论风险,是有真实案例的。正确做法一律用占位符:
public int insertStudent(Student s) { String sql = "INSERT INTO t_student " + "(student_no, student_name, gender, birth_date, class_id, phone, email, address, enroll_date) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getStudentName()); ps.setString(3, s.getGender()); ps.setDate(4, s.getBirthDate() == null ? null : new java.sql.Date(s.getBirthDate().getTime())); ps.setInt(5, s.getClassId()); // ... 后面字段按序绑定 return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("新增学生失败", e); } }参数绑定这一步有四个注意点。第一,setDate要传java.sql.Date,不能直接把java.util.Date塞进去,类型不匹配会报错。第二,新增前要查重学号,SELECT 1 FROM t_student WHERE student_no = ?,存在则不许插入,否则会撞主键约束。第三,表单里所有文本框都要做空值判断,学号姓名可以为空吗?不能,这是业务底线。第四,编辑操作和新增操作建议共用同一个校验逻辑,用一个validate(Student s)方法同时被两个 Servlet 调用,避免两处校验不一致。
5.3 批量删除:IN 子句的参数绑定,别用字符串拼接
批量删除的高频翻车点,是有人图省事把多个学号直接拼进 SQL:DELETE FROM t_student WHERE student_no IN ('2024010101','2024010102')。如果学号来自前端的 checkbox,黑客提交一个'2024010101'); DROP TABLE t_score;--就能让系统崩盘。参数绑定的正确做法是动态生成 N 个占位符:
public int batchDelete(List<String> studentNos) { StringBuilder sql = new StringBuilder("DELETE FROM t_student WHERE student_no IN ("); for (int i = 0; i < studentNos.size(); i++) { sql.append(i == 0 ? "?" : ",?"); } sql.append(")"); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < studentNos.size(); i++) { ps.setString(i + 1, studentNos.get(i)); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("批量删除失败", e); } }有两个边界必须处理。一是studentNos为空时,生成的 SQL 是IN (),直接语法错误,调用方要先判空,或者前端限制必须勾选才能点删除按钮。二是批量删除的成绩表关联:如果有成绩表的学生被删了,外键约束会拦截 DELETE,常见做法是先删t_score里对应学生的成绩,再删学生,两步放在同一个事务里。用conn.setAutoCommit(false)包住两条 DELETE,最后统一 commit,任何一步失败就 rollback——这是给课设系统加分的经典操作。
5.4 登录状态与密码哈希:Session 里存什么、怎么存
登录状态的管理,核心原则就一句:Session 里只存必要信息,不存密码。前面 LoginServlet 里session.setAttribute("loginUser", user),user 对象只承载 user_id、username、real_name、role,不含 password 字段,这是刻意的——即便 Session 被别人拿到,也不会泄露密码明文。
密码存储和校验的完整逻辑用一小段工具类实现:
package com.example.util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class PasswordUtil { public static String sha256(String rawPassword, String salt) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update((salt + rawPassword).getBytes(StandardCharsets.UTF_8)); byte[] bytes = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }这里用的盐就是用户名,比不加盐强很多。原理是相同的原始密码加上不同的盐,哈希结果不同,避免两个账号密码相同导致哈希值也相同的“撞库”风险。更工程化的做法是给每个用户随机生成独立盐并单独存一列,但在课设项目里,用用户名做盐足够展示你的安全意识到位。
退出登录的做法是session.invalidate()销毁会话,然后在logout.jsp或一个 LogoutServlet 里重定向回登录页。还有一个细节:登录之后,访问student_list.jsp等受保护页面要在入口处做 Session 判断,最简单的方式是写一个AuthFilter,拦截除登录页和静态资源外的所有路径,检查session.getAttribute("loginUser")是否为 null,为 null 一律踢回 login.jsp。
5.5 统计查询:按专业统计学生人数的 GROUP BY 示例
管理系统加一个统计模块,观感会立刻上一个档次。最简单的统计是“各专业学生人数”,SQL 内连接班级表后按专业分组:
SELECT c.major_name, COUNT(s.student_no) AS student_count FROM t_class c LEFT JOIN t_student s ON c.class_id = s.class_id GROUP BY c.major_name ORDER BY student_count DESC;LEFT JOIN而不是INNER JOIN,是为了让那些还没招到学生的专业也能显示 0,而不是直接消失。DAO 层查询结果是一个只有两列的列表,Servlet 取出后转成一个 JSON 字符串,页面端用有 HTML 表格渲染出来即可,不需要引入前端图表库。如果想做得更精致,可以再加一个“按年级统计”或“男女生比例”,SQL 套路完全一样,只是 GROUP BY 的字段换成gender而已。
6. 部署与运行避坑:5 个让 JSP+SQL Server 项目翻车的常见问题
6.1 驱动加载失败:ClassNotFoundException 的 90% 原因
现象:Tomcat 启动正常,一访问页面就报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver,控制台堆栈直接指向 DBUtil 的Class.forName行。
原因 90% 是同一个:JDBC 驱动 jar 没有放进web/WEB-INF/lib目录。IDEA 里从 Maven 仓库拉到依赖不等于运行时能加载,Tomcat 只认自己 classpath 下的 jar。另一个隐蔽原因是驱动和 JDK 版本不匹配——很老的 sqljdbc 驱动在 JDK 8 上能跑,换到 JDK 11 就容易报错,而新版 mssql-jdbc 驱动对 JDK 版本有明确要求。
解决:把mssql-jdbc的 jar 直接复制到web/WEB-INF/lib下,重启 Tomcat。如果用了 Maven,检查 pom 里依赖的 scope 是否为compile,并确认package出的 war 里包含这个 jar。看驱动版本与 JDK 的匹配表是最后一招,但对绝大多数课设场景,jar 位置才是根因。
6.2 中文乱码:从页面到数据库的环节排查
现象:表单提交“张三”,列表页显示“??”,或者页面标题乱码成一堆菱形符号。
原因:中文乱码是三道关卡的问题叠加,不是单点故障。第一道是 JSP 页面本身没声明编码,<%@ page pageEncoding="UTF-8" %>缺失时,Tomcat 按默认 ISO-8859-1 解码。第二道是 request 没有设置为 UTF-8,POST 表单提交的中文到 Servlet 里已经乱了。第三道是 SQL Server 数据库或表的排序规则不是中文排序规则,数据在写入时被丢弃或转换出错。
解决:按顺序排查。先在 JSP 头部确认pageEncoding和contentType都带 UTF-8;给 web.xml 配置全局编码过滤器(第 4 章已写);最后在 SQL Server 里执行SELECT name, collation_name FROM sys.databases,看目标库的排序规则是否含Chinese_前缀,不对就重建库或在建库时指定COLLATE Chinese_PRC_CI_AS。这三步走完,乱码基本绝迹。开发阶段最有效的预防手段,是一开始建库就指定排序规则,而不是等数据乱了再补救。
6.3 连不上数据库:TCP/IP 协议没启用是最容易被忽视的一步
现象:SQL Server Management Studio 连库一切正常,但 Java 程序启动时报The TCP/IP connection to the host localhost, port 1433 has failed,或者Connection refused。
原因:SSMS 用的是 Shared Memory 或 Named Pipes 协议,而 JDBC 走的是 TCP/IP。SQL Server 默认安装时可能没启用 TCP/IP 协议,或者启用了但监听端口不是 1433。这个问题在课设里极其常见,因为很多同学的 SQL Server 是安装时一路默认,根本没有打开过网络配置。
解决:打开 SQL Server Configuration Manager(在开始菜单 SQL Server 配置工具里),找到“SQL Server 网络配置”,点开“协议”,把 TCP/IP 的状态改成“已启用”,右键选择“属性”,在“IP 地址”页签确认 TCP 端口写着 1433。改完必须重启 SQL Server 服务,配置才会生效。注意区分服务名:默认实例叫MSSQLSERVER,命名实例叫MSSQL$实例名,别重启错了。这条是这套系统最常见的血泪经验,我第一次搭的时候就卡在这里一个下午。
6.4 分页 SQL 报语法错误:OFFSET 是 SQL Server 2012 之后的语法
现象:在某个服务器上跑分页列表,Tomcat 抛com.microsoft.sqlserver.jdbc.SQLServerException: Incorrect syntax near 'OFFSET',整个列表页 500。
原因:数据库版本是 SQL Server 2008 R2 或更早,OFFSET...FETCH语法是 2012 版本才引入的。很多同学本机装的是 2019 或 2022,写的时候没问题,部署到实验室的老服务器上就炸了。判断版本可以在 SSMS 里执行SELECT @@VERSION。
解决:把 DAO 里分页 SQL 换成ROW_NUMBER()兼容写法:
SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY student_no) AS row_num, t_student.* FROM t_student ) AS t WHERE row_num BETWEEN ? AND ?;第一条参数传(page - 1) * pageSize + 1,第二条传page * pageSize。这种写法从 SQL Server 2005 一路兼容到 2022,是存量系统里最稳妥的分页方案。如果项目同时要兼容新旧两版,可以把分页 SQL 的生成逻辑封装成一个工具方法,按数据库版本来选语法。
6.5 Tomcat 版本与包名冲突:javax.servlet 还是 jakarta.servlet
现象:项目在 Tomcat 9 跑得好好的,换到 Tomcat 10 或 11,启动直接报NoClassDefFoundError: javax/servlet/http/HttpServlet,或者ClassNotFoundException。
原因:Tomcat 10 开始把 Servlet API 从javax.servlet迁移到jakarta.servlet,包名彻底换了。老项目里所有import javax.servlet.*的代码在 Tomcat 10 上就是找不到类,不是 jar 缺失,是包名不存在。
解决:三个选择。要么把 Tomcat 版本固定在 9.x,老项目最省事的方案;要么把代码里所有javax.servlet批量替换成jakarta.servlet,web.xml 的命名空间也要同步更新;要么升级到兼容依赖。对课设系统来说,我强烈建议直接用 Tomcat 9 + 老代码的组合,因为网上的参考资料、别人的项目包大多基于javax,专为跑通项目的前提下,稳定压倒一切。这个坑很容易被忽略,因为它纯粹是环境差异,跟代码质量无关。
7. 让项目从“能跑”到“能交付”:配置外置与 JSTL 瘦身
7.1 把数据库连接信息外置到 db.properties
项目能跑了之后,第一个要做的工程化改造是消灭 DBUtil 里的硬编码账号密码。你永远不知道答辩现场机器上的sa密码是不是跟你本机一样,把连接信息抽到配置文件里,换环境只改文件,不动代码。在src根目录新建db.properties:
jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.url=jdbc:sqlserver://localhost:1433;databaseName=student_db;encrypt=false;trustServerCertificate=true jdbc.username=sa jdbc.password=your_password再写一个简单的 PropertiesUtil 读取它,getResourceAsStream能自动从 classpath 根目录加载:
package com.example.util; import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class PropertiesUtil { private static final Properties PROPS = new Properties(); static { try (InputStream in = PropertiesUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { if (in == null) { throw new ExceptionInInitializerError("db.properties not found"); } PROPS.load(in); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } public static String get(String key) { return PROPS.getProperty(key); } }然后 DBUtil 里改用PropertiesUtil.get("jdbc.url")等取参。这个改造的收益立竿见影:换数据库服务器,只需要编辑db.properties后重启 Tomcat,一行代码都不用动。
7.2 用 JSTL 替代 JSP 里的 Java 片段,页面回到展示层
第二个改造是清理 JSP 里的<%脚本片段。评分老师翻代码时,看到满屏 scriptlet 和看到满屏 JSTL 的印象完全不同。举个列表展示的例子,改造前:
<% List<Student> list = (List<Student>) request.getAttribute("list"); for (Student s : list) { %> <tr> <td><%= s.getStudentNo() %></td> <td><%= s.getStudentName() %></td> </tr> <% } %>改造后,在页面顶部引入 JSTL 标签库,Java 循环全部消失:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${list}" var="s"> <tr> <td>${s.studentNo}</td> <td>${s.studentName}</td> </tr> </c:forEach>EL 表达式${s.studentNo}会自动调用getStudentNo()方法,输出结果由 JSP 引擎负责转义,比<%= %>多了一层 XSS 防护。如果还需要格式化日期,加一行<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>,用${s.birthDate}配合<fmt:formatDate>即可。
回看我经手的这类项目,无论课设还是老系统迁移,最大的分水岭就在配置外置和 scriptlet 清理这两件事上:它们不改变业务功能,却决定了系统能不能被下一个人接手。我自己的习惯是哪怕再小的课设,数据库连接信息也绝不裸写在类里,这个习惯在后来维护老系统时救过我很多次。希望帮到你。
本文还有配套的精品资源,点击获取