老话说得好,“麻雀虽小,五脏俱全”。一个大学生公寓管理系统,放在课程设计或者毕业设计的选题里,看着不起眼,但它几乎把 Java Web 开发的核心环节全占了:前端页面交互、后端 Servlet 逻辑、数据库表设计、项目部署调试。尤其是用 JSP 来写这套系统,你要是能把它彻底跑通、跑明白,那对 Servlet 生命周期、Session 管理、JDBC 操作这些基本功的理解,绝对比看十遍书都管用。
这篇博文我就拿一个实际交付的项目来复盘——JSP 大学生公寓管理系统(项目编号 tj767),从技术选型、模块拆解、数据库设计,到开发环境搭建、本地调试、服务器部署,再到那些年我们踩过的坑,一条线完整讲透。无论你是正在做课程设计的学生,还是想快速上手 Java Web 项目的开发者,这套流程都能直接“抄作业”。
1. 项目核心需求与技术选型思路
1.1 公寓管理到底在管什么
很多同学容易被“管理系统”四个字唬住,一上来就想着造个大而全的东西,结果做出来四不像。实际上,大学生公寓管理系统的核心痛点很明确,无非就是这四件事:
- 人:学生信息的管理,包括入住、调宿、退宿、毕业离校;
- 物:宿舍楼、房间、床位的分配与状态追踪;
- 钱:水电费缴纳、住宿费统计、报修产生的费用;
- 事:来访登记、晚归记录、日常报修、公告通知。
把这个表梳理清楚,项目的功能边界就出来了——别去做什么课表管理、图书借阅,那不是公寓管理员操心的事。我当时做的时候,第一步就是画业务流程图,把管理员、宿管员、学生三类角色的操作路径全部列出来,再逐个映射成页面和接口,这样后续写代码跟填空一样,效率极高。
1.2 为什么 JSP 依然是“黄金选择”
你可能想问,现在都前后端分离了,Spring Boot 都出到 3.x 了,为什么还要用 JSP 这种“老古董”?这个问题我在各种技术群里见人吵过无数遍。但说实话,对于课程设计、毕业设计、以及刚入门的 Java Web 学习者来说,JSP 有着不可替代的优势:
- 上手门槛低:Java 基础 + 一点点 HTML 标签知识就能开写,不需要像 Spring Boot 那样理解 IOC、AOP、自动装配一大堆概念;
- 原理透明:JSP 本质上是 Servlet,每次请求都会经历“JSP → Servlet → Service → DAO → 数据库”的完整链路,你写一遍就能把整个 Java Web 请求流程彻底搞懂;
- 天然适合教学:绝大数高校的 Java Web 课程还在讲 JSP + Servlet,你做这个项目可以直接对着课程要求来,不会跑偏;
- 部署轻量:一个 Tomcat + MySQL 就能跑起来,老电脑也毫无压力。
当然,我也承认 JSP 有它的痛点,比如前端后端耦合严重、页面里嵌 Java 代码可读性差。所以我在实际编码时做了一个折中:JSP 页面只负责展示和简单的流程跳转,业务逻辑全部下沉到 JavaBean(Service + DAO)。这样既保持了 JSP 的直观性,又不至于把页面写得没法维护。
1.3 完整技术栈一览
这套项目交付时附带的技术栈如下,我用表格列出来,方便你对照查漏:
| 技术方向 | 选型 | 说明 |
|---|---|---|
| 前端页面 | JSP + JSTL + EL 表达式 + Bootstrap | 为什么用 Bootstrap?因为原生 HTML 写出来实在太丑,Bootstrap 能让你在不会 CSS 的情况下也做出能看的界面 |
| 后端控制 | Servlet 3.0 | 负责接收请求、调用 Service、转发或重定向到 JSP 页面 |
| 业务层 | JavaBean / Service 类 | 处理业务规则,比如分配床位时要校验房间容量和性别限制 |
| 数据访问 | JDBC + 连接池(Druid) | 为什么用 Druid?监控功能好用,而且德鲁伊本身的性能比裸 JDBC 好太多,尤其在频繁开关连接的场景下 |
| 数据库 | MySQL 5.7+ | 免费、稳定、网上资料多。5.7 就好,别追新用 8.0 的坑 |
| 服务器 | Tomcat 8.5 | 和 JDK 8 完美搭配,兼容 JSP 2.3 规范 |
| 开发工具 | Eclipse / IDEA + Navicat | 看个人习惯,IDEA 的智能提示对新手更友好一些 |
注意:我特意选择了 Servlet 3.0 而非老旧的 Servlet 2.5,原因是 3.0 支持注解配置,可以省掉大量 web.xml 映射配置,让项目结构清爽很多。对于新手来说,少写配置就少踩坑。
这套组合的另一个好处是:环境要求低,任何一台电脑都能跑起来,不至于因为软件版本太新导致各种兼容问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统模块设计与功能拆解
2.1 三类角色,分清各自权限
做管理系统,第一件事不是写代码,而是搞清楚“谁能干什么”。我把系统用户分成三类,数据权限从粗到细:
| 角色 | 核心权限 | 涉及页面范围 |
|---|---|---|
| 系统管理员 | 维护基础数据、宿舍楼信息、管理员账号分配、全局统计 | 学生管理、宿舍管理、楼栋管理、财务报表、系统设置 |
| 宿管员 | 日常进出登记、学生入住调宿处理、报修审核、水电抄表 | 登记管理、宿舍管理(部分)、报修处理、水电录入 |
| 学生 | 查看自己的住宿信息、报修、查看水电费账单 | 个人中心、报修、账单查询 |
这个权限模型不需要搞太复杂的数据库字段,用最简单的方式就行:在 user 表里加一个 role 字段(admin、dorm_manager、student)。然后在 Servlet 的父类里统一做一次权限过滤。当时我写了一个 BaseServlet,在 service() 方法里先判断当前用户的角色,再根据 URL 判断该角色是否有访问权限,没有就直接跳转到错误页。这样做的好处是,所有页面访问都会经过这层过滤,不用在每个 JSP 里重复写权限判断代码,最大程度避免了“权限漏洞”这种课程设计答辩时的致命伤。
如果你做这个项目,我强烈建议也搞一个类似的基本权限校验机制。哪怕实现得粗糙一点,也比完全没有强——答辩老师问起来,你能说清楚这套流程,绝对是加分项。
2.2 核心功能模块逐个拆
先说最重要的学生信息管理。这个模块要做的不只是简单的增删改查,还要关联到宿舍分配状态。例如新生入住时,系统要自动判断该学生所在院系是否与宿舍楼匹配,如果该楼栋已满,就要提示管理员选择其他楼栋。这个逻辑如果写成 SQL 会很难维护,所以我把校验逻辑放在 Service 层:
java复制public boolean assignDormitory(Student student, int roomId) {
// 1. 校验房间是否存在
Room room = roomDao.findById(roomId);
if (room == null) {
return false;
}
// 2. 校验性别是否匹配(公寓楼分男女)
if (!room.getGender().equals(student.getGender())) {
return false;
}
// 3. 校验房间是否已满
int currentCount = studentDao.countByRoomId(roomId);
if (currentCount >= room.getCapacity()) {
return false;
}
// 4. 分配床位并更新房间状态
studentDao.updateRoom(student.getId(), roomId);
roomDao.updateOccupiedCount(roomId, currentCount + 1);
return true;
}
代码不复杂,但把关键的校验逻辑都覆盖到了。实际做的时候我还在这个 Service 方法里加了事务控制——用 JDBC 的 setAutoCommit(false) 把第 4 步的两个操作包在一起,防止出现“房间人数更新了但学生没分配成功”的数据不一致问题。这个细节在答辩时特别容易引起老师兴趣。
其次是宿舍房间管理。这一块核心是“房间状态图”:空闲、部分入住、已满、维修中。我建议在 room 表里加一个 status 字段,用数字 0/1/2/3 表示四种状态,而不是每次查询时动态计算人数。为什么?因为频繁地 COUNT(*) 在大数据量下性能会迅速劣化,而且当学生退宿时,更新状态会比计算来得更直接。当然,状态字段可能因为异常操作和真实数据不一致,所以我做了一个定时任务(每天凌晨执行),重新统计每个房间的实际入住人数并同步状态,保证数据尽量正确。
然后是报修管理。这个模块很能体现系统的“实用性”——学生提交报修,宿管审核后派单,维修完成由学生确认。我们用了最简单的表格驱动方式:报修单包含单号、报修人、宿舍号、报修类型(水电、门窗、家具等)、描述、状态(待处理 / 处理中 / 已完成)、提交时间。状态流转用几个按钮触发,后端 Servlet 只接收 action=submit/audit/finish 等参数来改变状态。报修功能的难点不在代码,而在表单校验和状态流转控制——比如“已完成”的报修单不能再被“审核”,这个逻辑要写清楚。我最开始就是因为漏了这个,结果测试时一条单子能被审核三次,后来加了状态机校验才解决。
水电费管理其实最“烦”。因为抄表数据是手动录入的,容易出现漏录、错录。我当时就在录入页面做了防错校验:当期读数必须大于上期读数,否则直接拦截,并给出提示。在计算费用时,用 (当前读数 - 上期读数) × 单价 得出,针对不同宿舍类型(四人间、六人间、单人间)设置不同单价,并且支持整楼批量录入,减少宿管员的工作量。这个模块的普通逻辑并不复杂,但批量操作和校验逻辑是最容易出 bug 的地方,你要是在做这个项目,一定要重点测试。
2.3 页面流转与 Servlet 请求流程
整套系统的页面流转遵循经典的 MVC 模式:
code复制浏览器 → JSP页面(View) → Servlet(Controller) → JavaBean/Service(Model) → DAO → MySQL
以“学生入住”为例,完整链路是:
- 宿管员打开
student_add.jsp,填写学生信息表单,点击提交; - 表单 POST 到
StudentServlet?action=add; - StudentServlet 接收参数,封装成 Student 对象,调用 StudentService;
- StudentService 执行前面说的
assignDormitory逻辑,校验房间、分配床位; - 操作成功则重定向到
StudentServlet?action=list&page=1,刷新列表页;失败则转发回student_add.jsp,并携带错误信息。
这里有个很重要的细节:操作成功一定要用重定向,而不是直接转发。因为如果直接转发到列表页,用户按 F5 刷新时表单会再次提交,造成重复数据。我当时在这上面吃过大亏——测试时发现同一条学生记录出现了三条,后来改成重定向才解决。这个经验你一定会用得上。
3. 数据库设计与核心表结构
3.1 编码规范与建库建表
数据库设计是整套系统的地基,地基打不好,后续全是补丁。我建库时统一用了 utf8mb4 编码,只因为两件事:一是为了支持中文,二是为了兼容一些生僻字和 emoji 表情(虽然系统里用不到 emoji,但统一编码能省掉莫名其妙的乱码问题)。
sql复制CREATE DATABASE IF NOT EXISTS dormitory_system
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_general_ci;
注意:
utf8mb4和utf8的区别在于前者支持四字节的 Unicode 字符。MySQL 老的 utf8 其实只支持到三字节,遇到特殊字符(比如某些人的姓名里带生僻字)会直接报错或者乱码。所以即使你自己学习用,也建议直接用 utf8mb4。
3.2 核心表结构逐个拆解
这个系统我一共设计了 8 张核心表。这里挑最关键的 4 张讲清楚,其余的就一笔带过:
student(学生表)——这是系统的核心主表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 学生ID |
| student_no | VARCHAR(20) | 学号,唯一索引 |
| name | VARCHAR(50) | 姓名 |
| gender | CHAR(1) | 男/女 |
| college | VARCHAR(100) | 学院 |
| major | VARCHAR(100) | 专业 |
| phone | VARCHAR(20) | 联系电话 |
| room_id | INT | 外键,关联 room 表 |
| bed_no | VARCHAR(10) | 床位编号 |
| status | TINYINT | 0=在读 1=毕业 2=退宿 |
| create_time | DATETIME | 创建时间 |
学号设置为唯一索引非常关键,这保证了同一个学生不会被重复录入。status 字段是逻辑删除的标志,不要物理删除学生数据,因为账单、报修记录都关联着这些数据,删了就全没了。
room(房间表):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 房间ID |
| building_no | VARCHAR(20) | 楼栋编号,比如“1号楼” |
| room_no | VARCHAR(20) | 房间号 |
| capacity | INT | 房间容量(4/6/8) |
| occupied | INT | 当前已住人数 |
| gender | CHAR(1) | 房间类型(男/女) |
| status | TINYINT | 0=空闲 1=部分入住 2=已满 3=维修中 |
我们可以写一个简单的查询,找出所有还有空位的男生宿舍:
sql复制SELECT building_no, room_no, capacity, occupied
FROM room
WHERE gender = '男'
AND status IN (0, 1)
ORDER BY building_no, room_no;
这个查询在新生入学的场景中特别常用,我建议你在列表页加一个筛选条件,直接在页面上输入性别,系统自动显示可分配房间。
water_electric(水电表)——这里我做了个设计决策:把每月抄表记录和缴费账号分开,用一张表记录每个月每个宿舍的用量:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 记录ID |
| room_id | INT | 房间ID |
| year | INT | 年份 |
| month | INT | 月份 |
| water_usage | DOUBLE | 用水量(吨) |
| electric_usage | DOUBLE | 用电量(度) |
| water_fee | DECIMAL(10,2) | 水费 |
| electric_fee | DECIMAL(10,2) | 电费 |
| status | TINYINT | 0=未缴纳 1=已缴纳 |
注意,我加了 (room_id, year, month) 的唯一索引,防止同一房间同一个月被重复录两次。之前在测试阶段就因为没有索引,数据里出现了同一房间同一个月的两条抄表记录,导致费用统计翻倍。
visit(访客登记表)——这个表设计就很简单了:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 登记ID |
| visitor_name | VARCHAR(50) | 访客姓名 |
| visitor_phone | VARCHAR(20) | 访客电话 |
| student_id | INT | 被访学生ID |
| room_id | INT | 访问房间 |
| visit_time | DATETIME | 进入时间 |
| leave_time | DATETIME | 离开时间 |
| remark | VARCHAR(255) | 备注 |
3.3 数据库连接与连接池配置
JDBC 直接获取连接太慢,而且并发一高就报连接数不够。所以我用了阿里的 Druid 连接池。在 src/druid.properties 里配置:
properties复制driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/dormitory_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username=root
password=yourpassword
# 初始化连接数
initialSize=5
# 最大连接数
maxActive=20
# 最小空闲连接数
minIdle=5
然后用一个工具类读取配置,别在 DAO 里每次手动 DriverManager.getConnection():
java复制public class DbUtils {
private static DruidDataSource dataSource;
static {
try {
Properties prop = new Properties();
prop.load(DbUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void closeAll(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) {} }
}
}
closeAll 方法特别重要,很多人只关闭了 Connection 就完事,结果 Statement 和 ResultSet 敞着口子,时间一长就报 “too many open connections”。记住,Connection 借用之后必须归还,尤其是用了连接池之后,conn.close() 其实是还给池子而不是真正关闭,不归还的话池子很快就被耗尽。
4. 开发环境搭建与关键配置
4.1 环境版本选型与避坑
“开发环境初始化配置”这一步看着不起眼,实际上一堆人卡在第一关。不是 JDK 装错,就是 Tomcat 起不来。我推荐一套绝对兼容的组合:
- JDK 8(即 1.8)——无论 Tomcat 8.5 还是 IDE 都对它支持最好;
- Tomcat 8.5.x——对 JSP 4 和 Java EL 3.0 支持很好;
- MySQL 5.7——稳定,且和 JDK 8 搭配不会有版本冲突;
- IDEA 2021+ 或 Eclipse——两者都行,如果电脑内存有限就用 Eclipse,资源占用低很多。
这个组合在网上有海量教程和问题帖子,你搜索任何一个报错信息基本都能找到答案。别用 JDK 17 或 JDK 21 配 Tomcat 9,虽然理论上兼容,但遇到诡异的反射报错时你会后悔为什么不听劝。
4.2 项目目录结构规范
我建项目的目录结构如下,遵守了 Maven 的规范(虽然这是普通 Web 项目):
code复制dormitory-system/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── com/dorm/controller/(Servlet 类)
│ │ │ ├── com/dorm/service/(业务逻辑)
│ │ │ ├── com/dorm/dao/(数据库操作)
│ │ │ ├── com/dorm/entity/(实体类:Student、Room 等)
│ │ │ └── com/dorm/utils/(工具类)
│ │ └── resources/
│ │ └── druid.properties
│ └── webapp/
│ ├── admin/(管理员页面)
│ ├── dorm/(宿管员页面)
│ ├── student/(学生页面)
│ ├── common/(公共:403页、404页、错误页)
│ ├── static/(css、js、images)
│ └── WEB-INF/
│ ├── web.xml
│ └── lib/(JDBC驱动、Druid jar包等)
把页面按角色分目录,是特别朴素但高效的做法。一方面避免权限混乱,另一方面也方便写权限过滤规则——比如我在 BaseServlet 里可以根据 URL 前缀自动判断所需角色,不用针对每个 URL 单独配置。
4.3 数据库导入与 Navicat 连接
拿到项目源码后,第一件事是建库导入。用 Navicat 连接本地 MySQL,右键“新建数据库”,字符集选 utf8mb4,排序规则选 utf8mb4_general_ci,然后直接“运行SQL文件”,选择 dormitory_system.sql 即可。导入后刷新表列表,8 张核心表应该都在,并且已经有部分测试数据。
注意一点:如果你用 Navicat 导入 SQL 时报 “Unknown collation”,大概率是 SQL 文件里写的排序规则版本太新(比如 utf8mb4_0900_ai_ci),而你的 MySQL 是 5.7 不支持这个排序规则。解决办法是编辑 SQL 文件,把那些 utf8mb4_0900_ai_ci 全部替换成 utf8mb4_general_ci。
4.4 IDEA 配置 Tomcat 的细节
用 IDEA 配置 Tomcat 是新手最容易卡壳的环节。先打开 Run → Edit Configurations → + → Tomcat Server → Local,选择 Tomcat 安装目录。然后在 Deployment 标签页点 + → Artifact → 选择 war exploded,Application context 改成 /dorm。
这里有两个容易踩的坑,我必须强调:
- URL 的端口要对。Tomcat 默认是 8080,如果你改了其他端口,访问地址也要改。我最开始测试时改了端口忘了自己改过,折腾了一个小时才发现是端口的问题。
- Artifact 别选错。选
war是打包好的压缩包,部署速度慢;选war exploded是解压目录,支持热部署,改了 Java 代码立刻生效,开发阶段一定要用 exploded。
5. 调试部署全流程实录
5.1 本地调试的关键步骤
配置好环境后,本地调试的完整步骤可以分阶段验证:
第一阶段:验证基础环境。启动 Tomcat,浏览器访问 http://localhost:8080/dorm/,能看到管理员登录页面说明 Tomcat 和项目部署没问题。
第二阶段:测试登录与权限。用管理员账号 admin/admin123 登录,观察是否能正常跳转到管理员首页。如果登录后停留不动,打开浏览器 F12 控制台看请求状态码和响应信息,大概率是数据库连接写错了或者表名不匹配。
第三阶段:功能冒烟测试。按业务流程走一遍:新增一条学生记录 → 给学生分配宿舍 → 查看宿舍列表确认床位数量变化 → 提交一条报修单 → 以管理员身份审核 → 确认维修完成。这整个过程跑通,项目主流程就没啥大问题了。
第四阶段:异常场景测试。比如给一间已满的房间分配学生,看系统是否能正确拦截;把自己做成管理员账号去访问学生页面,看是否能正常被权限过滤器拦截。这些异常场景是答辩老师的“重点攻击区”,提前准备好就稳了。
5.2 部署到远程服务器的流程
如果你需要把项目部署到云服务器(比如阿里云/腾讯云的 ECS),让其他人也能通过公网 IP 访问,操作也不复杂:
- 在服务器上安装 JDK 8、Tomcat 8.5、MySQL 5.7(或直接用宝塔面板一键安装);
- 本地 Navicat 连接服务器 MySQL,导入
dormitory_system.sql; - 把项目的
druid.properties里的数据库地址改成服务器的公网 IP 或内网 IP,重新编译打包成dorm.war; - 把
dorm.war上传到服务器 Tomcat 的webapps目录; - 重启 Tomcat,访问
http://服务器IP:8080/dorm/。
注意:服务器安全组一定要放行 8080 端口,否则外部无法访问。我第一次部署时忘开端口,死活打不开页面,最后检查安全组规则才发现问题。
5.3 数据库导出与项目交付清单
既然是“程序 + 源码 + 数据库 + 调试部署 + 开发环境”的完整交付,数据库脚本的导出质量直接影响交付体验。用 Navicat 转储 SQL 文件时,建议勾选“包含 DROP TABLE 语句”和“包含 IF NOT EXISTS”选项,这样无论对方数据库里有没有旧表,导入都不会冲突。还要勾选“使用扩展插入”,每条 INSERT 语句可以一次插入多行数据,SQL 文件体积小、导入速度快。
完整交付清单建议这样包含:
code复制dormitory-system/
├── 源码/
│ ├── src/(完整Java源码)
│ ├── webapp/(所有JSP页面和静态资源)
│ └── README.md(项目说明、账号密码、运行步骤)
├── 数据库/
│ └── dormitory_system.sql(建库建表语句+初始化数据)
├── 部署文档/
│ ├── 开发环境搭建手册.md
│ └── 部署指南.md(本地部署+远程服务器部署)
└── 成品演示视频.mp4(可选)
6. 常见问题排查与避坑经验
6.1 最高频的五个故障及解法
我整理了一下实际调试过程中最常踩的坑,做成一个速查表,方便你遇到问题时直接对照:
| 问题现象 | 产生原因 | 解决办法 |
|---|---|---|
| 页面访问 404 | 部署的 Artifact 名称和访问路径不一致 | 检查 IDEA 的 Application context 是否设置正确 |
| 数据库中文乱码 | 数据库连接 URL 没带 characterEncoding | URL 上加上 characterEncoding=utf8 |
| 空指针异常(NullPointerException) | 多半是 request.getParameter() 拿到 null,强转失败 |
在 Service 层开头加参数校验,为 null 时直接返回错误信息 |
| 报错 “javax.servlet.jsp.JspException: /WEB-INF/tags/...” | JSTL jar 包没引入 | 在 WEB-INF/lib 下放入 jstl-1.2.jar |
| 报错 “Too many connections” | 连接池连接被耗尽,Connection 没有关闭 | 检查 DAO 层是否每次用完都调用了 DbUtils.closeAll() |
6.2 “数据库连接失败”排查实录
“访问数据库时发生错误”是后台系统最常见的报错提示,但它的原因千奇百怪。我当时遇到过一个非常经典的情况:本机测试没问题,部署到服务器一运行就报 Access denied for user 'root'@'localhost'。排查了半个多小时才发现是 druid.properties 里的密码和服务器数据库密码不一致。这个错误提示太具有欺骗性了,让人以为是权限配置问题,其实是密码错了。
还有一种情况是 MySQL 8.0 的认证插件问题。MySQL 5.7 默认用 mysql_native_password,MySQL 8.0 默认用 caching_sha2_password,而老版本的 JDBC 驱动不支持后者。解决方案有两种:要么把数据库用户的插件改回 mysql_native_password,要么升级 JDBC 驱动到 mysql-connector-java 8.0.x。我的建议是直接用 8.0.x 驱动,一劳永逸。
6.3 JSP 页面中文乱码的“终极解法”
中文乱码问题在 JSP 项目里是个老生常谈但又烦人的问题。乱码产生的原因通常是三层不一致:页面编码、请求编码、数据库编码。
页面头部写死编码:
jsp复制<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
服务器端处理 POST 请求时强制指定编码:
java复制request.setCharacterEncoding("UTF-8");
这两种方式都做了之后,90% 的乱码都能解决。剩下的 10% 出在数据库连接 URL 上,加上 characterEncoding=utf8 即可。注意,在 Tomcat 8 之后,GET 请求的编码问题已经内置处理,不需要额外设置 URIEncoding,所以你不用在 server.xml 里乱改配置。
6.4 值得留意的安全性细节
虽然这是课程设计级别的项目,但我在做的时候还是顺手加了一些基本的安全防护。比如在登录时,密码不是明文存储,而是用了 MD5(虽然 MD5 不算强哈希,但作为教学项目已经比明文好了太多);在 SQL 操作中,全部使用 PreparedStatement 而不是 Statement,能有效避免 SQL 注入(这也是面试必问的考点);在用户输入上增加了一些基本的正则校验,比如电话号码必须 11 位数字。这些细节在答辩和面试时都能成为你的亮点。
如果时间充裕,还可以进一步加一个简单的验证码功能(用 servlet 动态生成图片验证码),这在课程设计里算“加分项”,而且实现并不复杂,大概 50 行代码就能搞定。
6.5 扩展思路与加分建议
项目主体做完之后,如果你想让它显得更有“完成度”和“技术含量”,我建议从下面几个方向扩展:
- 数据可视化:管理员首页加一个统计面板,用 ECharts 画柱状图、饼图,展示各楼栋入住率、男生女生比例、月度报修数量趋势。把数据以图形化方式展示,这个功能很直观,答辩时特别吸睛。
- 报表导出:将月度水电费记录导出成 Excel 文件,用 Apache POI 实现,大概 60 行代码,就是一个独立功能。
- PWA / 响应式适配:宿舍管理员大部分时间用手机查看信息,把页面改成响应式(Bootstrap 自带),至少让核心页面在手机上不乱版。
但请记住:扩展功能是在主流程完全稳定的前提下做的。先确保核心增删改查和状态流转经得起推敲,再去加花活。不然答辩时主流程出 bug,基本就凉了。
最后再分享一个我个人的感受:这套 JSP 公寓管理系统做完之后,最大的收获不是“我学会了一门技术”,而是终于建立了“一个 Web 项目是怎么从零到一跑起来”的完整认知。你掌握了 Tomcat 部署原理、数据库表设计技巧、MVC 分层思想之后,后面再学 Spring Boot、MyBatis 这些框架,会发现都是同一套逻辑的封装和演进。框架会过时,但这些基本功永远不会。把这套系统吃透,以后简历上写“熟悉 Java Web 开发全流程”时,你心里是有底的。
