简介这是一套基于JSP与SQL Server的学生信息管理系统设计与实现项目采用B/S架构适合Java Web课程设计、毕业设计及初阶开发者学习参考。资源内含项目全套源码与完整文档源码已经测试校正可正常运行配套说明文档与答辩PPT便于理解系统设计、数据库搭建与功能实现。压缩包共包含55个文件其中42个JSP页面构成系统主要界面4个JPG图片用于界面展示或素材3个PPT便于答辩汇报另含SQL Server数据库文件MDF/LDF、运行配置文件及可执行文件等整体仅2.45MB轻量易用。目前已有798人学习浏览。通过该资源可掌握学生信息管理系统的模块划分、数据库表设计、前后端交互逻辑同时获得可直接修改部署的完整项目适合课程设计、项目实训或有类似开发需求者快速上手。1. 一个课程设计题目为什么值得拆开讲打开压缩包的那一刻绝大多数人看到的是一堆.jsp、.java、.sql文件外加一份写满目录结构的文档。很多同学把这套东西当成“交差用的黑匣子”——数据库跑起来、Tomcat 能启动、页面能点两下就宣布完工。但真到了答辩或者二次开发的时候老师随便问一句“你怎么处理中文乱码”“为什么用PreparedStatement”场面就很容易翻车。这套基于 JSP SQL Server 的学生信息管理系统本质是 Java Web 里最典型的“三层业务”表现层是 JSP 页面控制层靠 Servlet数据层走 JDBC 连到 SQL Server。它在学校场景里出现频率极高但它不是只能用来应付检查——把它的结构和踩坑点真正吃透你能复用的东西远超一个课程设计本身。这篇文章就顺着一条实际落地的路径来讲怎么搭环境、怎么设计表、怎么写代码、怎么处理那些让人挠头的坑最后给几个能让评分老师眼前一亮、也让系统真正能用的进阶动作。新手按步骤能跑通熟手可以重点看第三章的表设计和第五章的排查思路。先说结论这类系统的难点从来不在增删改查而在环境匹配、中文处理和 JDBC 资源管理这三件事上。2. 先把地基打牢JSP SQL Server 的环境选型和版本匹配2.1 为什么这个组合还在被广泛使用以及它适合谁JSP 是 JavaEE 规范里的视图技术服务端把页面渲染好再发给浏览器SQL Server 是微软的关系型数据库在 Windows 环境下部署成本低图形化管理工具成熟。这两个东西组合在一起是很多高校实训和企业遗留系统里的常见搭配。它的核心逻辑是浏览器请求 JSP 页面JSP 里通过 Java 代码或者经过 Servlet 转发调用 JDBCJDBC 驱动把 SQL 语句送到 SQL Server 执行结果再拼回 HTML 返回。适合的人群很明确需要快速交付一个中小型管理系统的开发者尤其是学生信息这类数据量不大、表关系清晰的业务以及要维护早期遗留项目的工程师。它不像 Spring Boot MyBatis 那样有一大堆自动配置但正因为没有框架帮你兜底跑通它反而能让你把请求、响应、连接、事务这些 Web 基础看得清清楚楚。2.2 JDK、Tomcat、SQL Server 的版本匹配矩阵版本匹配是这个项目里最容易“开局即翻车”的地方。JSP 本身不是独立运行的它必须跑在 Servlet 容器里最常用的是 Apache Tomcat。SQL Server 需要对应版本的 JDBC 驱动驱动版本和 JDK 版本之间有严格的对应关系。下面这个表是我在实际部署中验证过比较稳的组合组件推荐版本关键说明JDK1.8 或 111.8 兼容性最好11 需要确认驱动支持Tomcat8.5 或 9.08.5 对应 Servlet 3.19.0 对应 Servlet 4.0SQL Server2012 / 2016 / 20192019 需要较新的驱动JDBC 驱动mssql-jdbc-9.4.1.jre8.jar对应 JDK 8JDK 11 用 jre11 版本注意一个常见误区很多人在 SQL Server 2019 上使用旧版驱动 sqljdbc4.jar启动时直接报UnsupportedClassVersionError这是因为驱动的 class 文件版本高于 JVM 能识别的版本。我一般会在项目根目录建一个lib文件夹专门放驱动然后在 IDE 里把lib目录下所有 jar 包都加入构建路径——不要只加一个后面遇到驱动类找不到的问题基本都是这一步没做全。编码问题也要提前想清楚。SQL Server 默认排序规则通常是Chinese_PRC_CI_AS在 JSP 页面里 HTML 头部设置的是 UTF-8两端编码不一致就等着中文变问号。后面第四章会单独讲怎么统一编码这里先记住环境阶段就要把 JSP 文件的编码、数据库的排序规则、JDBC 连接串的编码参数三处对齐。2.3 从零跑起一个最小可用的 Web 项目骨架不用急着写业务代码先把一个能访问的空白 JSP 跑起来后面所有操作都基于这个骨架。这里给出最小步骤# 假设 Tomcat 解压在 D:\apache-tomcat-9.0.85 # 第一步确认 JAVA_HOME 已配置 echo %JAVA_HOME% # 第二步把项目打包成 war 或直接用目录部署 # 在 webapps 下新建 student 目录 # 结构D:\apache-tomcat-9.0.85\webapps\student\ # 第三步在 student 目录下新建 index.jsp内容如下面代码块% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% html headtitle学生信息管理系统/title/head body h1系统启动成功/h1 /body /html启动 Tomcat 后浏览器访问http://localhost:8080/student/index.jsp能看到页面就说明骨架通了。这里contentType和pageEncoding必须同时设置缺一个都会导致页面中文乱码。注意JSP 本质上是一个被容器编译成 Servlet 再执行的流程第一次访问会慢一些因为 Tomcat 在后台用 Jasper 编译器把 JSP 转成了.java文件。骨架跑通后接下来整个系统的开发就有了一个可观察、可排错的入口。很多人在这一步还没验证就开始写几十个 JSP结果到最后全乱码排查时无从下手。先把最小闭环打通后面的每一次改动都有对照这是课程设计里救命的习惯。3. 学生信息管理系统的数据层设计表结构、约束与质量3.1 业务需求决定了表的边界不贪多但必须完整学生信息管理系统表面上只需要“学生表的增删改查”但多数课程设计有隐藏需求比如成绩登记、班级统计、用户登录。我在设计时会把表控制在 4 张以内学生表、课程表、成绩表、用户表。这不是偷懒而是“设计与实现”这套作业里表和表之间必须有外键关系否则怎么体现“关系型数据库”但表太多又会带来联表查询和级联删除的复杂度四张表刚好能形成完整业务闭环又不会把自己绕晕。每张表的字段设计有几个原则主键用自增整数而不是学号/工号字符串因为字符串主键在联表查询时效率低且容易改性别、状态这类字段用CHAR(1)或TINYINT不要用VARCHAR(20)存“男/女”凡是需要展示的文本字段统一用NVARCHAR而不是VARCHAR这直接关系到 SQL Server 的中文存储问题。NVARCHAR按 Unicode 存储VARCHAR按代码页存储混用之后比较和写入都会出问题。3.2 直接可用的建表脚本与索引、外键设计下面这份建表脚本是按实际运行为标准写的可以直接复制到 SQL Server Management Studio 里执行-- 用户表存放登录账号 CREATE TABLE [dbo].[t_user] ( id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password NVARCHAR(64) NOT NULL, role CHAR(1) DEFAULT 1 -- 1管理员2教师 ); -- 学生表 CREATE TABLE [dbo].[t_student] ( id INT IDENTITY(1,1) PRIMARY KEY, stu_no NVARCHAR(20) NOT NULL UNIQUE, -- 学号 stu_name NVARCHAR(50) NOT NULL, gender CHAR(1) NOT NULL, -- M/F birth_date DATE, class_name NVARCHAR(50), phone NVARCHAR(20), create_time DATETIME DEFAULT GETDATE() ); -- 课程表 CREATE TABLE [dbo].[t_course] ( id INT IDENTITY(1,1) PRIMARY KEY, course_no NVARCHAR(20) NOT NULL UNIQUE, course_name NVARCHAR(50) NOT NULL, credit DECIMAL(3,1) ); -- 成绩表学生和课程的多对多关系通过它体现 CREATE TABLE [dbo].[t_score] ( id INT IDENTITY(1,1) PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) CHECK (score 0 AND score 100), exam_date DATE, CONSTRAINT FK_score_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT FK_score_course FOREIGN KEY (course_id) REFERENCES t_course(id) ); CREATE INDEX idx_score_student ON t_score(student_id); CREATE INDEX idx_score_course ON t_score(course_id);这段脚本里最值得留意的不是建表本身而是t_score表的设计思路它把“学生”和“课程”两个实体通过外键关联起来这是关系型数据库里典型的关联表设计。成绩表本身不存学生姓名或课程名只存各自的id查询时通过JOIN取名字。这么做避免了一个经典的翻车场景如果学生改名或删除成绩表里残留的冗余文本会造成数据不一致。CHECK约束很容易被忽略但它是评分时的一个亮点也是真实系统中防止脏数据的底线。成绩字段加CHECK (score 0 AND score 100)比在 Java 代码里做判断更底层——任何绕过 Java 直接改数据库的操作都会被拦住。索引的设计方面外键列student_id和course_id上各加一个非聚集索引能显著加快按学生查成绩、按课程统计成绩的查询数据量到几万条时体感很明显。数据库排序规则是个容易忽略的隐藏坑。如果你在安装 SQL Server 时选了默认的Chinese_PRC_CI_AS那么所有NVARCHAR字段的排序和比较默认不区分大小写这在登录校验时会有隐患用户名Admin和admin会被认为是同一个。解决方式是在建用户表时把username的排序规则显式指定为Chinese_PRC_CS_AS或者在查询时用COLLATE Chinese_PRC_CS_AS强制区分。课程设计里不要求做到这一步但如果答辩被问到“大小写敏感问题怎么处理”能答上来会是明显的加分点。3.3 数据访问层的封装思路为什么不要在每个 JSP 里写 JDBC 连接很多初学者的写法是把Class.forName、DriverManager.getConnection、Statement.executeQuery全部写在 JSP 的%! %或% %里然后每个页面复制粘贴一遍。这样写能跑但维护成本极高改一个数据库密码要打开几十个文件逐一替换。更关键的是每次请求都创建一个 Connection用完不关数据库连接池很快被耗尽系统运行一段时间后就会卡死或报Connection refused。我一般会单独抽一层DBUtil把连接的创建和关闭收敛到一个类里。下面是实际项目中用的工具类骨架和调用方式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 URL jdbc:sqlserver://localhost:1433;DatabaseNamestudent_db;encryptfalse; private static final String USER sa; private static final String PASSWORD your_password; static { try { Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有几个参数需要按实际环境调整。encryptfalse很关键新版 SQL Server JDBC 驱动默认开启了 TLS 加密但很多本地开发环境没有配置证书不加这个参数会报The driver could not establish a secure connection to SQL Server。1433是 SQL Server 的默认端口如果你安装时改了端口这里必须同步改。用户名和密码建议用专门建的低权限账号而不是直接使用sa超级管理员——这是习惯问题学校环境可能没人管但在企业里直接用sa会被挑理。有了DBUtil业务代码的写法就会统一成三步拿连接、执行 SQL、释放资源。后面第四章里所有 DAO 层的示例都会基于这个工具类来写避免每个类重复写连接逻辑。这一步做完系统的可维护性和评审观感就已经超过大多数课程设定了。同时强调一下这样封装之后后续换连接池比如 HikariCP时只需要改DBUtil这一个文件。4. 从登录到学生管理JSP Servlet DAO 的代码怎么落到工程里4.1 分层到底怎么分MVC 不是虚的它解决的是“改需求不崩盘”学生信息管理系统虽然小但分层的价值不体现在“代码量多”而是体现在“改需求的时候少改几个文件”。最常见的需求变更就是原来列表页只显示学号、姓名、班级现在要加显示年龄原来删除学生是物理删除现在要求改成逻辑删除学生表加status字段。如果代码不分层这些改动会像打地鼠一样一会儿改 JSP一会儿改 SQL一会儿改 Java。我建议按下面的结构组织包名这是 JSP Servlet 时代最主流的分层方式src/ com.demo.entity/ Student.java / Score.java 等实体类 com.demo.dao/ StudentDao.java / ScoreDao.java 等数据访问类 com.demo.servlet/ LoginServlet.java / StudentServlet.java 等控制器 web/ index.jsp 登录页 student_list.jsp 学生列表页 student_edit.jsp 新增/编辑页 WEB-INF/web.xml Servlet 映射配置 lib/ mssql-jdbc-9.4.1.jre8.jar实体类是纯数据载体只包含字段和getter/setterDAO 是纯数据库操作只包含增删改查方法Servlet 负责接收请求、调用 DAO、把结果放到request作用域、转发或重定向到 JSPJSP 只负责用 EL 表达式和 JSTL 标签把数据渲染出来。这里有一个容易踩的坑Servlet 是单实例多线程的所以绝对不要在 Servlet 里定义可修改的成员变量来存放请求级数据比如private String currentUsername它会产生并发串数据的问题。请求数据一律通过HttpServletRequest的setAttribute传递。4.2 登录功能密码校验、Session 和过滤器拦截登录是整个系统最早写、也最能体现工程质量的模块。先看核心代码再做解释public class LoginServlet extends HttpServlet { 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); UserDao userDao new UserDao(); User user userDao.findUser(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/index.jsp).forward(request, response); } } }登录成功用sendRedirect而失败用forward这是一个值得展开的设计细节重定向是两次请求浏览器地址栏会变成目标地址转发是一次请求地址栏保持原样。失败时用转发是因为我们要在同一个请求里把errorMsg属性带到 JSP 展示成功时用重定向是为了避免用户刷新页面时表单被重复提交。这个点在答辩时经常被问到能讲清楚“为什么成功失败处理方式不一样”比背概念有用得多。对应的UserDao.findUser方法里有一个最重要的安全实践所有查询必须用PreparedStatement而不是直接拼接字符串public User findUser(String username, String password) { String sql SELECT * FROM t_user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setRole(rs.getString(role)); return u; } } return null; } catch (SQLException e) { e.printStackTrace(); return null; } }这里的?占位符配合setString本质上是把用户输入当作“参数”而不是“SQL 代码片段”传给数据库这样 OR 11这类字符串就只能在数据列里老实待着而不会变成查询条件的一部分。这是 SQL 注入防线的核心任何不这么写的 JDBC 代码都是不合格的。另外注意这里用了 try-with-resources 语法Java 7Connection、PreparedStatement、ResultSet都会自动关闭不用手动在 finally 里啰嗦。登录之后还需要一个过滤器统一拦截未登录请求。没有这个过滤器的话用户直接访问student_list.jsp就可以绕过登录系统这是“设计与实现”里很常见的漏洞。过滤器写法如下public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /index.jsp); } else { chain.doFilter(req, resp); } } }注意request.getSession(false)的传参false表示“如果当前没有 Session 就返回 null”而不会像默认的getSession()那样“没有就创建一个”。如果这里不传false那么未登录的请求也会被强制创建一个 Session拦截判断就永远无法命中——这是一个典型的过度默认参数导致拦截失效的坑。过滤器配置需要在web.xml里声明filter filter-nameAuthFilter/filter-name filter-classcom.demo.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时要把登录请求本身排除否则会出现“去登录页也被拦截”的循环跳转。常见做法是在AuthFilter里判断请求路径是否以index.jsp或LoginServlet结尾是则放行。这个判断逻辑建议单独抽一个方法后面每次加白名单路径比如注册页、验证码接口只改这一处。4.3 学生列表与 CRUD分页查询、表单传递和回显学生列表页是系统的主界面它决定了一个课程设计的观感。只写一个SELECT * FROM t_student然后全部循环输出在学生数据只有十几条时没问题但一旦到了百级数据页面渲染会明显变慢。我在实现里建议加上分页分页逻辑放在 SQL 层面而不是内存层面才是真正能应对数据量增长的分页方式。SQL Server 推荐用OFFSET ... FETCH NEXT语法public ListStudent findStudents(int pageNum, int pageSize) { String sql SELECT * FROM t_student ORDER BY id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY; ListStudent list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setStuName(rs.getString(stu_name)); s.setGender(rs.getString(gender)); s.setBirthDate(rs.getDate(birth_date)); s.setClassName(rs.getString(class_name)); s.setPhone(rs.getString(phone)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; }这里有一个稍不注意就会出的坑OFFSET ? ROWS的占位符参数必须是整数类型setInt没问题但如果图省事用setStringSQL Server 会因为参数类型不匹配直接抛异常。另外分页查询必须搭配ORDER BY子句否则 SQL Server 的OFFSET行为是未定义的每次翻页拿到的数据顺序都可能不一致。这是数据库层面的“看起来玄学实际是规范没做”的典型问题。新增和编辑功能通常会共用一个表单页面通过 URL 参数区分是“新增模式”还是“编辑模式”。以新增为例前端表单提交到StudentServletprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String stuNo request.getParameter(stuNo); String stuName request.getParameter(stuName); String gender request.getParameter(gender); String birthDate request.getParameter(birthDate); String className request.getParameter(className); String phone request.getParameter(phone); Student s new Student(); s.setStuNo(stuNo); s.setStuName(stuName); s.setGender(gender); s.setBirthDate(java.sql.Date.valueOf(birthDate)); s.setClassName(className); s.setPhone(phone); StudentDao dao new StudentDao(); boolean ok dao.insertStudent(s); if (ok) { response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(errorMsg, 插入失败学号可能已存在); request.getRequestDispatcher(/student_edit.jsp).forward(request, response); } }表单传参这里最值得讲的是“空值”和“类型转换”。request.getParameter永远返回字符串或 null前端如果没填birthDate就提交Date.valueOf会抛IllegalArgumentException。我是在 DAO 里做防御如果birthDate为 null 或空字符串就把exam_date设为 NULL而不是直接去调用Date.valueOf。这类“前端漏填导致后端 500”的问题在答辩演示时最容易出现因为你打开的是一个你没填过的表单一提交就报错现场改代码的体验非常糟糕。列表和编辑共用的student_edit.jsp里要注意编辑模式下回显数据的写法。最稳定的方案是Servlet 先从数据库查出Student对象存入request作用域JSP 再把对象里对应字段设置到input的value属性上。不要用el表达式拼接value${student.stuName}加上复杂判断更不要在每个输入框里写 JSP 脚本片段。JSTL 和 EL 是 JSP 2.0 规范的一部分用它们能让页面看起来简洁很多也是评审老师认可的“现代 JSP 写法”。检查 JSP 是否正确引入了 JSTL 标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %这段放在文件最顶部。4.4 成绩管理用 JOIN 把两张表缝合起来学生管理只是主数据维护成绩管理才真正体现“教务业务”。成绩查询接口的 SQL 要同时关联学生表和课程表返回的字段里既有学生姓名、学号也有课程号和成绩。对应的 SQL 写成这样SELECT s.stu_no, s.stu_name, c.course_name, sc.score, sc.exam_date FROM t_score sc JOIN t_student s ON sc.student_id s.id JOIN t_course c ON sc.course_id c.id WHERE s.stu_no ? ORDER BY sc.exam_date DESC这里有个验体验的关键点ResultSet里取的列名必须在 SELECT 子句里出现过。如果你写SELECT s.stu_no, s.stu_name, c.course_name, sc.score那么在代码里就只能取stu_no、stu_name、course_name、score这四个列想取exam_date必须先把它写进 SELECT 的子句列表中否则运行时会报Column not found。这种情况看着很低级但联表查询回导致列名来自多个表错误排查时误以为是自己 SQL 语法写错了其实只是漏了列。5. 避坑与排查五条能救命的实战踩坑记录5.1 现象Tomcat 突然起不来端口被占用这是开发期间几乎一定会遇到的第一道坎。我遇到的情况是以前某个项目没关干净java.exe进程还在后台再去启动 Tomcat 就报Port 8080 required by Tomcat v9.0 Server is already in use。原因Windows 下 IDE 或某个遗留的命令行窗口占用了 8080 端口Tomcat 无法绑定同一个端口。解决先不急着改端口先找到占用者。命令行执行netstat -ano | findstr :8080输出的最后一列是进程 PID然后用taskkill /PID PID /F结束掉对应进程再启动 Tomcat。如果确实是多个项目抢端口再改conf/server.xml里的Connector port8080。注意改端口后所有访问链接、以及代码里所有http://localhost:8080的硬编码都要跟着改不然会得到“服务看起来没起来”的错觉。5.2 现象JSP 页面中文全部变成乱码或问号这个坑在 Windows 环境下基本逃不掉。典型表现页面上的中文标题、按钮、提示信息全部变成??或乌黑的一堆乱码后台打印 SQL 语句签名时也出现乱码。原因编译、请求、响应三个环节的编码不一致。SQL Server 的驱动程序默认使用ISO-8859-1或代码页对应字符集来处理字符串JSP 页面声明了 UTF-8数据库表是NVARCHARJava 的内存编码又是 UTF-16三层之间任何一处断掉都会乱码。解决三处对齐。第一处JSP 文件头部统一写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%第二处request.setCharacterEncoding(UTF-8)放在 Servlet 处理 POST 请求的入口或者在web.xml里配置一个CharacterEncodingFilter全局统一第三处JDBC 连接串里追加;characterEncodingutf-8有些驱动版本需要同时写sendStringParametersAsUnicodetrue。数据库表字段继续用NVARCHAR。这三处设完绝大多数乱码问题消停。5.3 现象登录时偶发连接池耗尽或连接超时系统运行一段时间后某次登录请求卡住几十秒然后报Connection refused: connect或The TCP/IP connection to the host failed。原因我在初学者代码里见过太多次这种写法——Connection开了不关、Statement用完不释放连接池或直接创建的物理连接的资源被耗尽数据库端把新连接拒掉。业务量一大问题立刻暴露。解决代码里所有getConnection()的调用必须有对应的close()并且要放在finally或直接在 try 后面自动关里。这种问题最常见的说法是“明明写了 close为什么还报”很可能是因为某个分支路径里return提前退出跳过了close。把资源获取放到 try 语句的入参位置、靠 try-with-resources 自动关闭比手动管理 finally 更不容易漏。5.4 现象删除学生记录时数据库报外键冲突在成绩表已经用student_id关联学生的情况下直接执行DELETE FROM t_student WHERE id ?数据库会抛The DELETE statement conflicted with the REFERENCE constraint。原因t_score表通过外键引用了t_student表存在子记录时删除父记录会被数据库拦截这是关系型数据库的引用完整性规则。解决两种策略按业务需要选。第一种是物理删除前先删子表数据代码里先执行DELETE FROM t_score WHERE student_id ?再执行删除学生第二种是逻辑删除学生表加status字段修改时把status置为停用所有列表查询都带WHERE status 1条件。我在实际项目里推荐逻辑删除学生信息尤其是学号是历史记录物理删了之后成绩统计会缺历史数据答辩时还会被质疑“数据完整性怎么保证”。如果不愿意动表结构那就用第一种在 Service 层把一个学生的删除拆成两步保证顺序最好套上事务。5.5 现象为什么有时数据库能生服务怎么都连不上SQL Server 装好了SSMS 能连上但 Java 程序就是报The TCP/IP connection to host localhost, port 1433 has failed。原因SQL Server 默认安装时 TCP/IP 协议可能是禁用的只启用了 Shared Memory 和 Named Pipes。SSMS 在同一台机器上走内存通道能连但 JDBC 驱动走 TCP/IP 就连不上。另外如果 SQL Server 用于 Windows 认证模式下JDBC 里的用户名密码常常无法匹配域账号也会被拒绝。解决在 SQL Server 配置管理器中找到“SQL Server 网络配置”把对应实例的“TCP/IP”协议启用然后在“IP 地址”选项卡把“TCP 动态端口”清空把“TCP 端口”固定为 1433。同时需要在 SQL Server 里执行CREATE LOGIN ...创建 SQL Server 认证的登录账号或者在连接串中使用integratedSecuritytrue并做好本机认证配置。学校环境里直接新建一个sa或专门的 SQL 登录账号是最省事也是最快的。另外补充一个几乎必踩的坑JDBC 连接串里的数据库实例名。如果是默认实例jdbc:sqlserver://localhost:1433;DatabaseNamestudent_db没问题如果是命名实例比如SQLEXPRESS地址需要写成localhost\\SQLEXPRESS并确认实例名。我见过太多人把端口从 1433 改到 1434 或 1435怎么改都连不上——命名实例的端口根本不是默认的正确姿势是在 SQL Server 配置管理器的“TCP/IP 协议”里查看实际配置端口。6. 让它从“能运行”变成“值得展示”三个立竿见影的进阶动作6.1 用连接池替代裸连系统稳不稳就看这一步如果觉得DBUtil每次新建物理连接太慢直接用 HikariCP 替换它。这是 Java 生态里最主流的连接池配置一个HikariDataSource即可import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:sqlserver://localhost:1433;DatabaseNamestudent_db;encryptfalse); config.setUsername(sa); config.setPassword(your_password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); HikariDataSource dataSource new HikariDataSource(config);替换的时机和注意事项连接池是全局共享的单例整个 Web 应用只需要一个HikariDataSource实例然后所有 DAO 里的DBUtil.getConnection()改成dataSource.getConnection()即可。这样几十个页面里原来低效的“每次新建物理连接”瞬间变成从池子里借还连接响应时间在本地开发可能感觉不明显但到了并发几次以上或部署到服务器后差距非常明显。答辩时被问“你的系统怎么撑住并发”答“用了 HikariCP 连接池”就已经是一个合格的工程答案。6.2 加一个验证码和操作日志从课程设计向“可用系统”迈进如果上一篇已经按部就班写完登录和 CRUD那离“值得一看”的作品还差最后两笔。一是登录页加简单的算术验证码或图形验证码防止机器暴力尝试密码二是在用户表旁加一张操作日志表核心操作新增学生、删除学生、修改成绩都写一条操作记录记录操作者、操作时间、操作类型和被操作对象的 ID。这两件事的代码量都不大验证码可以用 Java 的BufferedImage画日志可以在 DAO 的insertStudent方法里顺带插入一条记录但效果会让系统的完整度和工程味儿提升一大截。6.3 最后的打磨为演示场景做一次“完美彩排”反复自我检查这三个场景确保演示时不出丑第一断网状态或 SQL Server 服务停止时打开页面应该出现友好的提示页而不是一大段堆栈异常第二表单漏填时提交应该在页面上显示“XX 为必填项”而不是后台抛异常第三快速连续两次点击“删除”按钮数据库不会出现重复删除或因主键冲突而报警的难堪场面。前端加一个onsubmitreturn confirm(确定删除)的确认对话框就能避免大多数致命误操作。如果你还能补充解释一个常见问题——“为什么刷新列表页不会导致表单重复提交”——用“重定向 vs 转发”这个知识点来回答就是回应答辩的高光时刻。这是我做过这类系统后最想让你带走的一个习惯数据库脚本写入version_notes注释并在代码里给每个 DAO 关键方法写明“业务含义”远比你等到答辩前一天打开那些看起来一模一样的 Java 文件来回想“这个方法是干嘛的”要省心得多。希望这篇笔记帮到你也祝你一次跑通、答辩顺利。本文还有配套的精品资源点击获取