简介面向高校计算机专业课程设计与毕业设计场景的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. JSPSQL 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 ServerMySQLJDBC 驱动类com.microsoft.sqlserver.jdbc.SQLServerDrivercom.mysql.cj.jdbc.Driver连接串前缀jdbc:sqlserver://jdbc:mysql://默认端口14333306自增语法IDENTITY(1,1)AUTO_INCREMENT分页写法OFFSET...FETCH或ROW_NUMBER()LIMIT字符串类型VARCHAR 排序规则VARCHAR按字符集认证模式Windows 认证 / 混合认证仅账号密码SQL Server 里最容易踩的第一个坑是“实例名”。本机安装默认实例时连接串写localhost:1433没问题但如果你装的是命名实例比如localhost\SQLEXPRESS连接串就得写成jdbc:sqlserver://localhost:1433;instanceNameSQLEXPRESS;databaseNamestudent_db漏了instanceName会报“找不到服务器实例”。另外 SQL Server 默认是 Windows 认证Java 程序用sa账号登录前必须先把服务器切成混合认证模式这一步留在避坑章细说。2.3 学生信息管理系统的功能边界先用一张模块表圈住需求课设最常见的翻车不是代码写不出来而是需求越做越大最后什么都缺角。我建议动手前先把功能边界画清楚登录管理、学生信息 CRUD、班级字典管理、按专业统计人数这四件事是一个“学生信息管理系统”的最小可交付集合。课程表和成绩表可以并入字段设计时预留即可不要一开始就做考勤、选课、宿舍分配这些外延功能。模块页面后端动作要点登录login.jspLoginServlet 校验账号密码Session 保存登录态学生管理student_list.jspStudentServlet 分页查询SQL Server 分页语法新增 / 编辑student_edit.jsp表单提交到 Insert / Update ServletPreparedStatement 参数绑定删除列表页勾选BatchDelete ServletIN 子句动态占位符统计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_ASNVARCHAR存中文没问题但排序、查询时可能表现诡异。建库语句里显式指定排序规则是最省心的做法。第四个是“逻辑删除 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, wxmexample.com, 2024-09-01), (2024020101, 刘芳, 女, 2006-02-23, 2, 13800000002, liufangexample.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 xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 filter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-nameLoginServlet/servlet-name servlet-classcom.example.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping welcome-file-list welcome-filelogin.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;databaseNamestudent_db; encryptfalse;trustServerCertificatetrue;loginTimeout5; 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) { } } } }连接串参数逐个说明encryptfalse表示不启用 TLS 加密早期 mssql-jdbc 驱动默认不加密新版驱动默认却变成了 true导致老项目换个驱动就报 SSL 错误建议显式写出来trustServerCertificatetrue配合自签名证书场景课设环境必须加loginTimeout5让连接失败时 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 指向/loginmethod 用 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 ListStudent 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); // 执行并封装 ListStudent略 } 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 子句的参数绑定别用字符串拼接批量删除的高频翻车点是有人图省事把多个学号直接拼进 SQLDELETE FROM t_student WHERE student_no IN (2024010101,2024010102)。如果学号来自前端的 checkbox黑客提交一个2024010101); DROP TABLE t_score;--就能让系统崩盘。参数绑定的正确做法是动态生成 N 个占位符public int batchDelete(ListString 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 个让 JSPSQL 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 pageEncodingUTF-8 %缺失时Tomcat 按默认 ISO-8859-1 解码。第二道是 request 没有设置为 UTF-8POST 表单提交的中文到 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.servletweb.xml 的命名空间也要同步更新要么升级到兼容依赖。对课设系统来说我强烈建议直接用 Tomcat 9 老代码的组合因为网上的参考资料、别人的项目包大多基于javax专为跑通项目的前提下稳定压倒一切。这个坑很容易被忽略因为它纯粹是环境差异跟代码质量无关。7. 让项目从“能跑”到“能交付”配置外置与 JSTL 瘦身7.1 把数据库连接信息外置到 db.properties项目能跑了之后第一个要做的工程化改造是消灭 DBUtil 里的硬编码账号密码。你永远不知道答辩现场机器上的sa密码是不是跟你本机一样把连接信息抽到配置文件里换环境只改文件不动代码。在src根目录新建db.propertiesjdbc.drivercom.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.urljdbc:sqlserver://localhost:1433;databaseNamestudent_db;encryptfalse;trustServerCertificatetrue jdbc.usernamesa jdbc.passwordyour_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 的印象完全不同。举个列表展示的例子改造前% ListStudent list (ListStudent) request.getAttribute(list); for (Student s : list) { % tr td% s.getStudentNo() %/td td% s.getStudentName() %/td /tr % } %改造后在页面顶部引入 JSTL 标签库Java 循环全部消失% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${list} vars tr td${s.studentNo}/td td${s.studentName}/td /tr /c:forEachEL 表达式${s.studentNo}会自动调用getStudentNo()方法输出结果由 JSP 引擎负责转义比% %多了一层 XSS 防护。如果还需要格式化日期加一行% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %用${s.birthDate}配合fmt:formatDate即可。回看我经手的这类项目无论课设还是老系统迁移最大的分水岭就在配置外置和 scriptlet 清理这两件事上它们不改变业务功能却决定了系统能不能被下一个人接手。我自己的习惯是哪怕再小的课设数据库连接信息也绝不裸写在类里这个习惯在后来维护老系统时救过我很多次。希望帮到你。本文还有配套的精品资源点击获取