拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb学生宿舍管理系统开发解析:架构、数据库与部署避坑

JavaWeb学生宿舍管理系统开发解析:架构、数据库与部署避坑

简介:这份javaweb学生宿舍管理系统源码与数据库合辑,面向Java Web初/中级学习者、毕业设计及期末大作业需求者,提供一套可直接运行的宿舍管理解决方案,涵盖学生信息、楼栋房间、水电费、管理员等核心模块,难度适中,评审分高达98分。压缩包共133个文件,约3.3MB,主要包含20个Java源码与20个class编译文件、18个JSP页面、52个JavaScript脚本及5个CSS样式,另有SQL数据库脚本与XML配置,可直接导入IDE运行并还原数据库,便于学习前后端交互与增删改查实现。目前已有144人学习,适合用来快速搭建完整项目框架、理解业务逻辑,也可作为课程设计参考。资源结构清晰,源码均经本地编译验证,无需二次调试即可运行,能有效节省查阅与排错时间,是完成高分课设或答辩的有力支撑。

1. 这套 JavaWeb 学生宿舍管理系统,不是凑数的课设代码

学生宿舍管理系统是 JavaWeb 课程设计和毕业设计里出现频率最高的题目之一,但很多人拿到的源码只能在答辩时点几下按钮,问到底层逻辑就卡壳。这套系统之所以值得做,是因为它的业务关系足够真实:学生、楼栋、宿舍、报修、卫生检查是典型的多表关联,再加上登录权限、状态流转、宿舍分配这类需要事务的写操作,正好把 JavaWeb 的 Servlet、JSP、过滤器、数据库连接池全串起来,比单纯做图书管理更能体现项目完整性。

如果你手里已经有一份带数据库脚本的 javaweb 学生宿舍管理系统源码,目标不应该只是把它跑通,而是把里面的表结构、请求链路和分配逻辑读明白,改成自己答辩能讲清楚的设计。这篇以一套常见的“管理员+学生+宿管”角色模型为例,从架构、数据库、核心代码到 IDEA 运行配置和坑,一步步拆开讲。新手能照着复现,熟手也能直接绕过几个典型雷区。

2. 技术栈与架构:为什么 JavaWeb 宿舍管理系统多用 Servlet + JSP + MySQL

2.1 三层架构在这个项目里的落点

拿到源码后,第一件事不是点运行,而是先看包结构和 web.xml。学生宿舍管理系统虽然是课设体量,但一套规范的分层代码通常会拆成四个包:controller(Servlet)、service(业务)、dao(持久层)、model(实体类),再加上 filter 做登录拦截。

我一般会把对应关系讲成三层模型:JSP 页面负责输入输出,Servlet 只做参数接收和页面转发,Service 层处理宿舍分配、报修状态这些业务规则,DAO 层用 JDBC 或 MyBatis 访问 MySQL。放进这个项目里就是:

层次在本项目里的具体落点典型类或文件
表示层JSP 页面,按角色文件夹拆分pages/admin、pages/student、pages/repair
控制层Servlet + FilterLoginServlet、StudentServlet、RepairServlet、LoginFilter
业务层事务和规则校验StudentService、DormService、RepairService
持久层SQL 读写StudentDao、DormDao、RepairDao

这种分法的价值在于答辩时你可以直接画出来:请求从 JSP 发给 Servlet,Servlet 调 Service,Service 调 DAO,DAO 查 MySQL,结果一层层返回。很多低分项目的问题就出在 JSP 里直接写 JDBC,业务和展示揉在一起,一旦换数据库或改表结构,页面全崩。

2.2 Servlet、JSP、MyBatis 的取舍与依赖版本选择

市面上的 JavaWeb 学生宿舍管理系统源码主要有两类,一类是纯 Servlet + JSP + JDBC,另一类是 Servlet + JSP + MyBatis。我自己更推荐后者,因为宿舍分配、报修列表都有动态查询条件,MyBatis 的 where 标签和 resultMap 能少写很多拼接 SQL。

如果你是准备交课程设计,Maven 工程是最好的组织方式。下面是一份我常用的依赖清单,版本不是越新越好,要跟你的 JDK 和 Tomcat 匹配:

<dependencies> <!-- Servlet API,Tomcat 8.5 对应 3.1,Tomcat 9 对应 4.0 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API,编译时需要,运行时由容器提供 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库,用来在 JSP 里做 forEach / if --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动:本地 MySQL 是 5.7 就用 5.1.49,8.0 就用 8.0.30 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- MyBatis 核心依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> </dependencies>

参数说明:这里有两个最容易翻车的版本点。第一,servlet-api 的 scope 必须写成 provided,否则 Tomcat 运行时会和你 WEB-INF/lib 下的同名包冲突,报 ClassCastException。第二,mysql-connector-java 用 8.0 时连接串里必须带 serverTimezone=Asia/Shanghai,否则默认时区会差 8 小时,而且 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver,不是老的 com.mysql.jdbc.Driver。

2.3 从源码目录反推包结构

拿到源码后,我习惯先在 IDE 里把项目树展开,对照这个结构看是不是分层。这里是一份常见的高分项目目录组织:

dormitory-management/ ├── pom.xml ├── src/main/java/com/dorm/ │ ├── controller/ │ │ ├── LoginServlet.java │ │ ├── StudentServlet.java │ │ ├── DormServlet.java │ │ ├── RepairServlet.java │ │ └── NoticeServlet.java │ ├── service/ │ │ ├── StudentService.java │ │ ├── DormService.java │ │ └── RepairService.java │ ├── dao/ │ │ ├── StudentDao.java │ │ ├── DormDao.java │ │ └── RepairDao.java │ ├── model/ │ │ ├── Student.java │ │ ├── Dorm.java │ │ └── Repair.java │ ├── filter/ │ │ └── LoginFilter.java │ └── util/ │ └── DBUtil.java ├── src/main/resources/ │ ├── mybatis-config.xml │ ├── db.properties │ └── mapper/ │ ├── StudentMapper.xml │ ├── DormMapper.xml │ └── RepairMapper.xml ├── src/main/webapp/ │ ├── WEB-INF/web.xml │ ├── jsp/ │ │ ├── login.jsp │ │ ├── admin/ │ │ ├── student/ │ │ └── repair/ │ ├── css/ │ └── js/ └── sql/ └── dorm.sql

这个结构最直接的好处是答辩时能按层讲:util 是公共工具,filter 是权限入口,controller 里每个实体一个 Servlet,service 和 dao 一一对应。如果拿到的源码把所有类都塞在一个包里,或 JSP 和 Java 混在根目录,那项目质量基本要打折扣,建议自己手动拆一下再交。

3. 数据库设计是宿舍管理的高分区:表结构、关系与初始化脚本

3.1 核心表:学生、宿舍、楼栋、报修、卫生检查、系统用户

宿舍管理系统的数据库设计能不能拿高分,关键在于你如何解释实体关系。学生和宿舍之间不是简单的直接外键,通常还要通过“宿舍分配记录”来记录历史和换宿情况;宿舍和楼栋是多对一;报修单和宿舍是多对一;卫生检查和宿舍也是多对一。

一张好的数据库表设计至少要有六张核心表:

表名用途关键字段
sys_user登录账号,管理员、学生共用user_id, username, password, role, student_id
student学生档案student_id, name, gender, major, dorm_id
building宿舍楼栋building_id, building_name, manager
dorm宿舍房间dorm_id, building_id, room_no, capacity, used_count, gender_type
repair报修记录repair_id, dorm_id, user_id, content, status, create_time
inspection卫生检查记录inspection_id, dorm_id, score, comment, inspect_date

这几张表里最值得答辩讲解的是 dorm 表的设计:capacity 表示宿舍容量,used_count 表示当前已住人数。性别通过 gender_type 限制整栋楼或整层,不能只靠学生表里的 gender 字段做逻辑判断,否则会出现男学生分进女生宿舍的严重 bug。

3.2 建表 SQL 脚本与关系设计

下面是我通常会在课程设计里用到的一套建表脚本核心部分,注意看外键和唯一约束:

CREATE DATABASE IF NOT EXISTS dorm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dorm_db; CREATE TABLE building ( building_id INT PRIMARY KEY AUTO_INCREMENT, building_name VARCHAR(20) NOT NULL UNIQUE, manager VARCHAR(20) DEFAULT NULL ) ENGINE=InnoDB AUTO_INCREMENT=100; CREATE TABLE dorm ( dorm_id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(10) NOT NULL, capacity INT NOT NULL DEFAULT 4, used_count INT NOT NULL DEFAULT 0, gender_type TINYINT NOT NULL DEFAULT 0 COMMENT '0男 1女', CONSTRAINT fk_dorm_building FOREIGN KEY (building_id) REFERENCES building(building_id), UNIQUE KEY uk_room (building_id, room_no) ) ENGINE=InnoDB; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, name VARCHAR(30) NOT NULL, gender TINYINT NOT NULL, major VARCHAR(50), phone VARCHAR(11), dorm_id INT DEFAULT NULL, CONSTRAINT fk_student_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(dorm_id) ) ENGINE=InnoDB; CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT 'STUDENT', student_id VARCHAR(20) DEFAULT NULL, CONSTRAINT fk_user_student FOREIGN KEY (student_id) REFERENCES student(student_id) ) ENGINE=InnoDB; CREATE TABLE repair ( repair_id INT PRIMARY KEY AUTO_INCREMENT, dorm_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已完成 3已确认', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_repair_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(dorm_id), CONSTRAINT fk_repair_user FOREIGN KEY (user_id) REFERENCES sys_user(user_id) ) ENGINE=InnoDB;

这段 DDL 里有三个容易被忽略的细节。第一,student_id 我用 VARCHAR(20),因为很多学校学号是数字加字母,用 INT 会溢出或丢掉前导零。第二,dorm 表里的唯一约束是 (building_id, room_no),而不是 room_no 单独唯一,因为不同楼栋可能有相同的房间号。第三,sys_user 通过 student_id 和 student 表关联,而不是直接拷贝学生的姓名和性别,这样可以保证改档案时登录信息不用动。

实际运行脚本时,如果 MySQL 版本是 5.5 或更老,utf8mb4 可能不被支持,那就退回 utf8。但如果能支持,我建议优先 utf8mb4,否则学生填的 emoji 或生僻字在报修内容里会变成问号。插入数据时,password 字段不要存明文,至少要做 MD5 加盐,否则数据库文件一旦泄露,账号全裸奔。

3.3 数据库连接池参数和 MySQL 连接串

很多源码跑不起来,不是代码写错,而是 db.properties 里的配置不对。下面是一份既能用纯 JDBC 也能给 MyBatis 引用的配置:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dorm_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456 # 连接池参数 jdbc.initialSize=5 jdbc.maxActive=20 jdbc.maxWait=10000 jdbc.minIdle=2

参数要说清楚,答辩时老师很可能指着 url 问。useUnicode=true 和 characterEncoding=utf8 保证页面表单里提交的中文写入 MySQL 不乱码;serverTimezone=Asia/Shanghai 解决 8.0 驱动的时区异常;useSSL=false 因为本机开发不需要加密,否则 MySQL 5.7 的 SSL 警告会让人误以为连接失败;allowPublicKeyRetrieval=true 是 MySQL 8.0 配合 caching_sha2_password 时必加的,不加就会看到 “Public Key Retrieval is not allowed”。

连接池那里,initialSize 是启动时创建的连接数,maxActive 是最大连接数,课设项目并发不超过 50 时,maxActive 设 20 足够。maxWait 是拿连接时的最大等待毫秒数,设 10000 表示等 10 秒后抛异常,可以防止数据库卡死时请求全部堆积在多线程里。这里最常见的错误是有人把 maxActive 调到几百,觉得性能好,实际会耗尽 MySQL 的默认连接数,导致其他客户端连不上。

4. 把业务流程写成源码:登录权限、宿舍分配与报修状态流转

4.1 登录校验与权限拦截:一个 Filter 顶掉所有重复判断

学生宿舍管理系统的页面虽然不少,但权限模型很简单:未登录只能进 login.jsp,管理员能进 admin 下所有页面,学生只能进自己的页面。如果把这段判断写进每个 Servlet,代码会非常臃肿。一个 LoginFilter 就能统一拦截所有 /pages 目录下的 JSP 请求。

下面是一个精简版过滤器的核心逻辑:

package com.dorm.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/pages/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); // 未登录用户统一跳回登录页,不继续向下放行 if (session == null || session.getAttribute("login_user") == null) { resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); return; } // 已登录用户继续访问目标资源 chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("登录过滤器初始化"); } @Override public void destroy() { System.out.println("登录过滤器销毁"); } }

逻辑说明:@WebFilter("/pages/*") 是 Servlet 3.0 之后的注解写法,等价于在 web.xml 里配 filter-mapping。req.getSession(false) 这里传 false 很关键,如果传 true,过滤器会在没有 session 时自动创建一个,那“未登录检测”就永远失效了。跳转时必须用 req.getContextPath() 拼出项目根路径,否则部署在带名称的 context 下时,redirect 地址会少一段路径。

如果你拿到的源码没用注解,而是在 web.xml 里配置,也很正常。要注意的是 filter 的顺序:如果有多个过滤器,字符编码过滤器要放在 LoginFilter 之前,因为要先设置 request 的编码,否则登录页提交中文账号时,后端拿到的是乱码。

4.2 宿舍分配的并发与事务:这是最值得讲的业务点

宿舍管理系统里,宿舍分配是核心难点,也是答辩时最可能被追问的点。表面看只是更新学生表的 dorm_id 和宿舍 used_count,但这里有两个隐藏问题:一是宿舍可能已经满了,分配前要判断 used_count < capacity;二是两个学生同时操作时,如果没有事务和锁,就可能把 4 人间分到第 5 个人。

我一般建议在 Service 层手写一个分配方法,逻辑要闭环:

public boolean assignDormitory(String studentId, int dormId) { // 第一步:检查宿舍是否可住 Dorm dorm = dormDao.selectById(dormId); if (dorm == null) { return false; } if (dorm.getUsedCount() >= dorm.getCapacity()) { return false; // 宿舍已满 } // 第二步:更新宿舍人数和学生归属(后续可以包进事务) int rows = dormDao.increaseUsedCount(dormId); if (rows > 0) { int result = studentDao.updateDorm(studentId, dormId); return result > 0; } return false; }

这段代码的核心逻辑是“先查后再更新”。但不能只说功能,要说并发问题:如果两个线程同时读到 used_count = 3,capacity = 4,它们都会认为还可以分配,然后各自 update,最终 used_count 变成 5,超卖就发生了。所以更可靠的写法要么用数据库锁,要么在 update 时带上 used_count 条件。

-- 第一方案:原子条件更新,防止超卖 UPDATE dorm SET used_count = used_count + 1 WHERE dorm_id = #{dormId} AND used_count < capacity; -- 受影响行数为 0 时说明宿舍已满,直接放弃

这个 UPDATE 语句值得单独讲:它把“检查”和“更新”合成了一个原子操作。InnoDB 下,这条语句会锁定 dorm_id 对应的行,后续事务必须等它提交才能继续,所以从根上解决了并发分配超卖的问题。如果把这一步和 update student 一起放进事务,就能保证要么宿舍加了人学生也分配成功,要么都不变。

事务边界要注意,在 MyBatis 里你可以在 mapper 接口方法上配合使用 SqlSession,也可以用 Spring 的 @Transactional。课设源码如果没引入 Spring,我建议在 Service 里统一获取 Connection、设置 setAutoCommit(false),报错时 rollback,成功时 commit。很多项目只做了超卖判断没做回滚,数据库出现学生 dorm_id 指向不存在宿舍的脏数据,答辩时很容易被问穿。

4.3 报修状态机的 Servlet 更新与 JSP 展示

报修是宿舍管理系统里最能体现“状态流”的功能。一条报修记录从学生提交开始,要经历:待处理 -> 处理中 -> 已完成 -> 学生确认。不同角色能执行的操作不一样:

当前状态宿舍管理员可执行学生可执行
待处理设为处理中取消报修
处理中设为已完成追加备注
已完成无确认或重新报修
已确认归档无

Servlet 阶段处理这种状态最好用整型字段加校验,不要允许任意跳状态。下面是一段常见的更新逻辑:

int status = Integer.parseInt(req.getParameter("status")); int repairId = Integer.parseInt(req.getParameter("repairId")); if (status != Repair.STATUS_ACTIVE && status != Repair.STATUS_DONE) { request.setAttribute("error", "非法状态流转"); request.getRequestDispatcher("/jsp/repair/list.jsp").forward(request, response); return; } boolean ok = repairService.updateStatus(repairId, status, currentUser);

逻辑说明:先从 request 拿到目标和目标状态,再判断状态值是否在白名单里。比较关键的是“当前状态”也要带进查询条件,防止学生把别人报修单的状态乱改。updateStatus 的 SQL 最好带 user_id 条件,属于当前用户才更新。

JSP 展示时,不要直接用数字显示“0、1、2”,可以用一个小的 Map 做映射,比如利用 JSTL 的 forEach:

<c:forEach items="${repairList}" var="r"> <tr> <td>${r.content}</td> <td> <c:choose> <c:when test="${r.status == 0}">待处理</c:when> <c:when test="${r.status == 1}">处理中</c:when> <c:when test="${r.status == 2}">已完成</c:when> <c:otherwise>已确认</c:otherwise> </c:choose> </td> </tr> </c:forEach>

这里想提醒一下,JSP 里做业务判断会显得项目很业余,但课设层次用 JSTL 做状态映射是可接受的。如果想让代码再干净一点,可以在实体类加一个 getStatusText() 方法,根据 status 返回中文描述,页面直接写 ${r.statusText}。这也是加分点,因为体现了对实体设计和表现层分离的理解。

5. 在 IDEA 里运行 JavaWeb 项目:完整配置与避坑排查

5.1 拿到源码后的运行环境核对

不管是自己写还是从别人那里拿到 javaweb 学生宿舍管理系统源码,第一件事不是启动 IDEA,而是先把运行环境对齐。否则你会在各种诡异的报错里消耗掉大半天,最后发现是版本不匹配。

我通常按下面这个顺序核对:

检查项推荐环境说明
JDKJDK 8大多数课设源码基于 Java 8,用 JDK 11 不一定会有问题,但如果用了老版 Tomcat 会有坑
TomcatTomcat 8.5 或 9.0对应 Servlet 3.1/4.0,注解才能生效
MySQLMySQL 5.7 或 8.0导入 sql 脚本时注意编码用 utf8mb4
IDEAIntelliJ IDEA 2020+Ultimate / Community 都行,社区版要装 Smart Tomcat 插件
Maven3.6+如果源码没有 pom.xml 而是 lib 目录,可以跳过

这里有一个最常见的翻车点:毕业设计源码年代比较早,还在用 Eclipse 的 Dynamic Web Project,拿到 IDEA 里后不会被直接识别为 Web 工程。正确的做法是在 Project Structure 里的 Modules 添加 Web 支持,指定 web.xml 路径和 Web 根目录,然后再配置 Artifact。如果什么都不加就点 Run,IDEA 只能跑普通 Java 类,根本找不到 Tomcat 的 Deploy 按钮。

5.2 Tomcat 数据源与部署配置步骤

如果你拿到的源码用的是纯 JDBC,只有 db.properties,没有 Spring 管理的数据源,那配置相对简单。只要把 mysql-connector-java.jar 放进 WEB-INF/lib,然后确认 db.properties 里密码正确即可。

在 IDEA 里把 Tomcat 配好并成功跑起来,核心是三步:

第一步,打开 Run/Debug Configurations,新增 Tomcat Server Local,选择本机 Tomcat 目录。注意这里的 Tomcat Home 不能只填解压目录,要能看到 conf/server.xml 和 bin/catalina.sh 才算有效。

第二步,切换到 Deployment 页签,点击 + 添加 Artifact。如果你看到的是 Maven project,Artifact 类型一般是dorm-management:war exploded。选 Exploded 而不是打包后的 war,因为这样改了 JSP 不用重启 Tomcat,热部署体验好很多。

第三步,在 Application context 填/dorm,这样访问地址就是 http://localhost:8080/dorm/jsp/login.jsp。同时检查 IDEA 默认给出的 Server port 和 HTTP port,8080 若被你本机的其他服务占了,改成 8081,但要同步改后面跳转链接里的端口。

配置完之后还有一个老生常谈的步骤:在 Project Structure -> Artifacts 里,双击 main class 或 lib 不会自动把所有依赖打进 WEB-INF/lib。你需要检查 Output Layout 里有没有 Available Elements 下的 jar 包,没有就右键选 Put into /WEB-INF/lib,否则启动时会疯狂报 ClassNotFoundException。

5.3 避坑:JavaWeb 宿舍管理系统最常见的五个运行问题

这部分是直接从实际运行经验里踩出来的,按“现象 -> 原因 -> 解决”写,遇到问题可以先对照自己是不是这些情况。

问题一:Tomcat 启动成功,但浏览器访问 404。

现象:Tomcat 控制台出现Deploying web application archive后又快速结束,访问项目路径却 404。

原因:IDEA 的 Artifact 没有正确生成,或者 context path 设成了/ROOT,访问时路径不对。

解决:检查 Deployment 里 Application context 是否包含模块名,比如/dorm;再确认 Artifact 名称和输出的 exploded war 目录存在。如果改完还 404,把 Tomcat 和项目彻底 clean 后重启一次。

问题二:连接数据库报Public Key Retrieval is not allowed。

现象:日志堆栈里出现这条明确错误,程序连不上 MySQL。

原因:MySQL 8.0 默认认证插件是 caching_sha2_password,第一次连接时需要从服务器拉取公钥,而连接串没允许。

解决:在 jdbc.url 末尾追加allowPublicKeyRetrieval=true&useSSL=false。不要改成useSSL=true,本机开发证书没配置,反而会出现 SSL 握手失败。

问题三:页面中文全是问号或乱码。

现象:登录后学生姓名、报修内容里的中文显示成????。

原因:数据库连接串没设 characterEncoding,或者 MySQL 表默认的字符集是 latin1,也可能 JSP 页面没设 pageEncoding。

解决:第一步,SQL 脚本建库时加上DEFAULT CHARSET=utf8mb4;第二步,jdbc.url 加characterEncoding=utf8;第三步,确认 JSP 头部有<%@ page contentType="text/html;charset=UTF-8" %>。这三个地方缺一个都会乱。

问题四:报ClassNotFoundException: com.mysql.cj.jdbc.Driver。

现象:启动后调用 DAO 时直接抛 ClassNotFound。

原因:MySQL 驱动依赖没有打进 WEB-INF/lib。用 Maven 时不注意 exported 状态,或者 IDEA 没有把 jar 放进 Artifact。

解决:检查 Project Structure 中 Artifact Output Layout 里有没有 mysql-connector-java.jar,没有则手动加入。如果是手动添加的 jar 包,最好把它复制到webapp/WEB-INF/lib目录,而不是只放在 lib 库里。

问题五:过滤器导致无限重定向,页面提示“无法访问”。

现象:登录页也进不去,浏览器地址栏反复跳到 login.jsp。

原因:LoginFilter 把/pages/*全都拦截了,但登录页本身放在/jsp/login.jsp,通常不会死循环。陷入循环的常见原因是过滤器把 /jsp/* 也拦截,而放行的 URL 又跳回登录页,这个跳转再次被过滤。

解决:过滤器 Mapping 只拦截需要权限的路径,放行 login.jsp、css、js、图片资源。最常见做法是@WebFilter(urlPatterns = "*.jsp")然后在过滤器内部判断/login.jsp不拦截,而不是把所有/都拦下来由自己判断放行。

6. 做完再加分的三个技巧:日志、并发验证和账号安全

项目跑通后,如果要让答辩老师觉得“这人是真做过的”,别急着交。我会自己补三件小事,每件都不复杂,但效果很明显。

第一,是给关键业务留日志。尤其是在宿舍分配和报修状态流转这两处,使用 slf4j + log4j2,哪怕只是在控制台输出,也能在演示出 bug 时快速定位。我习惯在分配宿舍成功时打一行:分配成功 studentId=2023001, dormId=101, 剩余床位=1。不要小看这行字,老师在答辩现场问“你怎么知道分配成功”,这就是证据。

第二,是验证宿舍分配在并发下的表现。用一个简单脚本,或者直接在数据库客户端开两个事务窗口,同时执行UPDATE dorm SET used_count = used_count + 1 WHERE dorm_id=1 AND used_count < capacity;,你会看到一个事务成功、一个受影响行数为 0。这就是前面说的原子条件更新的验证方式。这个结论比背一百遍“乐观锁”要扎实。

第三,是把自己写源码时的密码处理讲清楚。如果源码里还是明文密码,建议改成 MD5 加盐,哪怕只是简单拼接一个固定盐值再加密。课上项目不会真的上生产,但你演示出 hash 后的密码字段,老师的评价通常是“安全意识不错”。

说回我自己的习惯:每次拿到一个 JavaWeb 课设源码,我都先看数据库脚本,再看过滤器,最后才看 Servlet。数据库和权限是学生宿舍管理系统最容易出现低级错误的两个地方,只要这两块稳住,这个高分项目基本就没跑了。希望这篇笔记能帮你把这份源码从“跑通”推到“讲透”的程度。

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

返回列表