news 2026/10/20 13:32:46

JSP+SQL Server学生信息管理系统开发实战:从环境搭建到避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+SQL Server学生信息管理系统开发实战:从环境搭建到避坑指南

简介:这是一套基于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 转发)调用 JDBC,JDBC 驱动把 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.1,9.0 对应 Servlet 4.0
SQL Server2012 / 2016 / 20192019 需要较新的驱动
JDBC 驱动mssql-jdbc-9.4.1.jre8.jar对应 JDK 8;JDK 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head><title>学生信息管理系统</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;DatabaseName=student_db;encrypt=false"; 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(); } } } }

这里有几个参数需要按实际环境调整。encrypt=false很关键:新版 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/setter;DAO 是纯数据库操作,只包含增删改查方法;Servlet 负责接收请求、调用 DAO、把结果放到request作用域、转发或重定向到 JSP;JSP 只负责用 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 '1'='1这类字符串就只能在数据列里老实待着,而不会变成查询条件的一部分。这是 SQL 注入防线的核心,任何不这么写的 JDBC 代码都是不合格的。另外注意这里用了 try-with-resources 语法(Java 7+),Connection、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-name>AuthFilter</filter-name> <filter-class>com.demo.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</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 List<Student> findStudents(int pageNum, int pageSize) { String sql = "SELECT * FROM t_student ORDER BY id " + "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"; List<Student> 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没问题;但如果图省事用setString,SQL Server 会因为参数类型不匹配直接抛异常。另外分页查询必须搭配ORDER BY子句,否则 SQL Server 的OFFSET行为是未定义的,每次翻页拿到的数据顺序都可能不一致。这是数据库层面的“看起来玄学,实际是规范没做”的典型问题。

新增和编辑功能通常会共用一个表单页面,通过 URL 参数区分是“新增模式”还是“编辑模式”。以新增为例,前端表单提交到StudentServlet:

protected 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 prefix="c" uri="http://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 port="8080">。注意:改端口后所有访问链接、以及代码里所有http://localhost:8080的硬编码都要跟着改,不然会得到“服务看起来没起来”的错觉。

5.2 现象:JSP 页面中文全部变成乱码或问号

这个坑在 Windows 环境下基本逃不掉。典型表现:页面上的中文标题、按钮、提示信息全部变成??或乌黑的一堆乱码,后台打印 SQL 语句签名时也出现乱码。

原因:编译、请求、响应三个环节的编码不一致。SQL Server 的驱动程序默认使用ISO-8859-1(或代码页对应字符集)来处理字符串,JSP 页面声明了 UTF-8,数据库表是NVARCHAR,Java 的内存编码又是 UTF-16,三层之间任何一处断掉都会乱码。

解决:三处对齐。第一处,JSP 文件头部统一写<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;第二处,request.setCharacterEncoding("UTF-8")放在 Servlet 处理 POST 请求的入口,或者在web.xml里配置一个CharacterEncodingFilter全局统一;第三处,JDBC 连接串里追加;characterEncoding=utf-8(有些驱动版本需要同时写sendStringParametersAsUnicode=true)。数据库表字段继续用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 认证的登录账号,或者在连接串中使用integratedSecurity=true并做好本机认证配置。学校环境里,直接新建一个sa或专门的 SQL 登录账号是最省事也是最快的。

另外补充一个几乎必踩的坑:JDBC 连接串里的数据库实例名。如果是默认实例,jdbc:sqlserver://localhost:1433;DatabaseName=student_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;DatabaseName=student_db;encrypt=false"); 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 为必填项”而不是后台抛异常;第三,快速连续两次点击“删除”按钮,数据库不会出现重复删除或因主键冲突而报警的难堪场面。前端加一个onsubmit="return confirm('确定删除?')"的确认对话框就能避免大多数致命误操作。如果你还能补充解释一个常见问题——“为什么刷新列表页不会导致表单重复提交”——用“重定向 vs 转发”这个知识点来回答,就是回应答辩的高光时刻。

这是我做过这类系统后最想让你带走的一个习惯:数据库脚本写入version_notes注释并在代码里给每个 DAO 关键方法写明“业务含义”,远比你等到答辩前一天打开那些看起来一模一样的 Java 文件来回想“这个方法是干嘛的”要省心得多。希望这篇笔记帮到你,也祝你一次跑通、答辩顺利。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:41:02

多合一读卡器OCX控件:统一接口与工程实践

简介&#xff1a;这份资源是华大推出的多合一通用读写读卡器OCX控件&#xff0c;面向需要在Windows应用中集成读卡器读写功能的开发者&#xff0c;尤其适合门禁、会员管理、数据采集等场景的软件集成。控件以ActiveX技术封装&#xff0c;提供统一接口兼容多种读卡器设备&#x…

作者头像 李华
网站建设 2026/10/17 8:01:34

AI视频生成的“记忆失灵症”:为什么AI讲不好一个连贯的故事

你有没有试过用AI生成一段多镜头的短片&#xff1f;比如让它拍一个学生找回笔记本的小故事&#xff1a;她把书包放进储物柜&#xff0c;跑去教室拿忘带的笔记本&#xff0c;再回到储物柜。这本该是件挺简单的事&#xff0c;可你会发现一个诡异的现象&#xff1a;明明书包上一个…

作者头像 李华
网站建设 2026/10/20 14:57:58

用 Claude Code 重构遗留系统:从评估到落地的完整实践指南

1. 引言&#xff1a;为什么选择 Claude Code 重构遗留系统遗留系统往往承载着核心业务逻辑&#xff0c;却因技术债累积、文档缺失和人员流动而难以维护。Claude Code 作为 AI 编程助手&#xff0c;能够在代码理解、重构方案设计和自动化修改等方面提供有力支持。本文将从评估、…

作者头像 李华
网站建设 2026/10/17 1:24:12

微信小程序+Java后端悬赏系统实战:从登录态到分布式接单锁

简介&#xff1a;这是一套面向计算机专业本科生的微信小程序毕业设计实战项目&#xff0c;聚焦悬赏信息发布场景&#xff0c;兼顾课程设计与期末大作业需求&#xff0c;适合Java后端与小程序前端初学者快速上手。资源包含完整可运行系统&#xff1a;基于SSM或SpringBoot构建的J…

作者头像 李华
网站建设 2026/10/19 15:33:00

Shapefile数据处理全攻略:从读取清洗到投影转换与交付校验

简介&#xff1a;面向GIS数据分析、城乡规划与环境研究人员的国家基础地理信息系统矢量数据集&#xff0c;涵盖全国主要湖泊、铁路、公路、行政边界、县级驻地、主要河流及县级统计数据等关键要素&#xff0c;可用于地图制图、空间统计与区域分析。压缩包共93个文件&#xff0c…

作者头像 李华
网站建设 2026/10/18 13:23:14

基于SSM+MySQL的酒店管理系统:环境搭建、数据库设计与核心业务实战

简介&#xff1a;基于SSM和MySQL的酒店管理系统项目代码及数据库&#xff0c;专为毕业设计、期末大作业和课程设计准备&#xff0c;适合Java初学者和在校学生参考。项目围绕酒店日常运营场景&#xff0c;涵盖了房间管理、用户登录、订单预订、评论互动等核心业务模块&#xff0…

作者头像 李华