ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零手写Servlet+JSP+MySQL宿舍管理系统:老技术栈实战指南

从零手写Servlet+JSP+MySQL宿舍管理系统:老技术栈实战指南 简介这是一套面向高校计算机相关专业学生的JavaWeb学生宿舍管理系统完整源码与配套使用教程适合作为期末大作业、课程设计或毕业设计的参考方案也便于初学者通过真实项目理解Servlet、JSP与MySQL的整合开发流程。压缩包共202个文件约7.21MB其中包含21个Java源文件、22个JSP页面、73个JavaScript脚本、16个CSS样式表以及7个jar依赖包另附SQL建库脚本、yml与xml配置文件、md说明文档和Bootstrap相关样式资源覆盖后端控制层、前端交互与数据库脚本等完整模块。目前已有248人学习下载。项目源码均经本地编译验证可运行评审分达98分难度适中内容经助教老师审定读者可据此掌握宿舍信息管理、学生入住登记等典型功能的实现思路并借助教程快速完成环境搭建与代码调试。1. 从零手写 ServletJSPMySQL 宿舍管理系统这套老技术栈为什么还值得动手如果你正在搜「javaweb项目完整案例mysql」大概率不是想听 Spring Boot 有多香而是手里有个实打实的活课程设计要交、毕设要选题、或者面试被问到「你写过原生 Servlet 吗」。基于 ServletJSPMySQL 开发学生宿舍管理系统就是这类需求里最经典的一套组合。它不依赖任何重型框架Tomcat 一挂就能跑MySQL 装完就能连整个请求链路从浏览器到数据库全在你眼皮底下没有自动配置帮你藏东西。这套技术栈最大的价值不是「先进」而是「透明」——你能亲眼看到一次 HTTP 请求怎么被 Servlet 接住、怎么查库、怎么把数据塞进 JSP 渲染成页面。适合刚学完 Java 语法、想搞懂 Web 请求全流程的新手也适合需要一份能讲清楚每一行代码来龙去脉的毕设选题。下面我按真实搭建顺序把环境、建表、后端、前端、排错一条线讲透。2. 环境与工程骨架IDEA 里跑通第一个 Servlet 的最小闭环2.1 JDK、Tomcat、MySQL 三件套的版本对齐这套老技术栈最怕版本打架。我一般锁定 JDK 8 或 JDK 11、Tomcat 8.5 或 9.0、MySQL 5.7 或 8.0这四个组合互相兼容性最好。JDK 17 配 Tomcat 9 也能跑但javax.servlet包名在 Tomcat 10 之后变成了jakarta.servlet如果你照着老教程抄代码import 会全线报红这是新手第一个翻车点。MySQL 8.0 的安装注意两件事一是认证插件默认是caching_sha2_password老版本 JDBC 驱动连不上要么升级驱动到 8.x要么建用户时改成mysql_native_password二是时区连接串里不加serverTimezone会报时区错误。安装完用命令行验证一下mysql -u root -p # 输入密码后进入 SELECT VERSION(); SHOW VARIABLES LIKE character_set_server;character_set_server应该是utf8mb4不是的话在my.iniWindows或my.cnfLinux里改character-set-serverutf8mb4再重启。字符集不对后面存中文姓名、宿舍楼名全是问号这个坑我踩过不止一次。2.2 IDEA 创建 JavaWeb 项目的正确姿势新版 IDEA 创建项目时不再直接给「Java Enterprise」入口很多人卡在这一步。稳妥做法是先建一个普通 Java 项目再手动加 Web 支持。# 项目结构Maven 版推荐 dorm-system/ ├── pom.xml ├── src/main/java/com/dorm/ │ ├── servlet/ # 所有 Servlet │ ├── dao/ # 数据库操作 │ ├── entity/ # 实体类 │ └── util/ # DBUtil 等工具 ├── src/main/webapp/ │ ├── WEB-INF/web.xml │ ├── css/ js/ │ └── *.jsppom.xml里只需要三个依赖javax.servlet-apiscope 设 provided因为 Tomcat 自带、mysql-connector-java、jstl。provided 这个 scope 很关键打成 war 时不会把 servlet-api 塞进去否则和 Tomcat 的冲突启动直接报ClassCastException。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency2.3 用 web.xml 还是注解两种 Servlet 注册方式Servlet 3.0 之后可以用WebServlet(/login)注解注册不用再写 web.xml。但宿舍管理系统里我建议关键 Servlet 用注解、全局配置如欢迎页、编码过滤器留在 web.xml这样结构清晰。WebServlet(/student/list) public class StudentListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); // 后续查库、转发到 JSP req.getRequestDispatcher(/student_list.jsp).forward(req, resp); } }doGet里先设编码再设响应类型顺序反了中文一样乱码。forward是服务器内部转发浏览器地址栏不变如果这里用sendRedirectreq 里放的数据就丢了列表页会空白。这是新手最容易混的两个跳转方式记住传数据用 forward跳登录页用 redirect。启动前在 IDEA 的 Run Configuration 里配好 TomcatDeployment 选 war explodedApplication context 设成/dorm。访问http://localhost:8080/dorm/student/list能看到页面最小闭环就通了。3. 数据库设计宿舍管理系统的表结构与 JDBC 连接池3.1 五张核心表怎么建才不返工宿舍管理系统的实体关系不复杂但字段设计要考虑周全否则做到一半要改表。核心是五张表学生、宿舍楼、宿舍房间、入住记录、管理员。CREATE TABLE dorm_building ( id INT PRIMARY KEY AUTO_INCREMENT, building_no VARCHAR(10) NOT NULL UNIQUE COMMENT 楼号, gender TINYINT DEFAULT 0 COMMENT 0男1女, floors INT DEFAULT 6, remark VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dorm_room ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(10) NOT NULL, capacity INT DEFAULT 4, occupied INT DEFAULT 0 COMMENT 已住人数, FOREIGN KEY (building_id) REFERENCES dorm_building(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0, class_name VARCHAR(50), phone VARCHAR(20), room_id INT, status TINYINT DEFAULT 1 COMMENT 1在住0退宿, FOREIGN KEY (room_id) REFERENCES dorm_room(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;occupied这个冗余字段是故意的。每次分配宿舍都去COUNT(*)学生表算已住人数数据量一大就慢而且并发分配时容易超员。用冗余字段配合事务更新分配时先UPDATE dorm_room SET occupiedoccupied1 WHERE id? AND occupiedcapacity影响行数为 0 就说明满了直接拦掉。这个WHERE条件里的occupiedcapacity是防超员的关键别省。3.2 JDBC 连接池为什么不能每次 new Connection新手常写DriverManager.getConnection()然后用完 close功能上没错但每次请求都建一次 TCP 连接几十个并发就顶不住。宿舍管理系统虽然并发不高但养成用连接池的习惯很重要。老项目常用 Druid 或 C3P0我一般用 Druid配置简单。public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据源初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement st, ResultSet rs) { // 依次关闭注意判空顺序 rs - st - conn } }druid.properties里几个必调参数initialSize5、maxActive20、maxWait3000毫秒、validationQuerySELECT 1。maxWait设太小高峰期直接抛获取连接超时设太大请求全堵在那。3000 毫秒对课程设计够用。validationQuery是连接有效性检测MySQL 8 用SELECT 1就行。连接串里useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8三个参数一个都别少。少了useSSL会有警告少了时区会报错少了编码中文乱码。3.3 DAO 层把 SQL 关进一个地方所有 SQL 只写在 DAO 层Servlet 里不出现一句 SQL。这样改表结构时只动 DAO不会满项目找 SQL。以学生查询为例public ListStudent findByPage(int page, int size, String keyword) { ListStudent list new ArrayList(); String sql SELECT s.*, r.room_no, b.building_no FROM student s LEFT JOIN dorm_room r ON s.room_id r.id LEFT JOIN dorm_building b ON r.building_id b.id WHERE s.name LIKE ? OR s.stu_no LIKE ? ORDER BY s.id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, (page - 1) * size); ps.setInt(4, size); ResultSet rs ps.executeQuery(); while (rs.next()) { list.add(mapRow(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; }用PreparedStatement而不是拼字符串一是防 SQL 注入二是预编译性能好。LIMIT ?,?分页在数据量大时越翻越慢因为 MySQL 要扫描前 N 条再丢弃。数据量上万后可以改成「记住上一页最大 id」的游标分页但课程设计级别用LIMIT完全够。4. Servlet 业务逻辑与 JSP 页面渲染请求怎么走完全程4.1 登录鉴权Session 与过滤器的配合登录成功后把用户信息放进 Session后续每个请求用过滤器校验没登录就踢回登录页。这是最省事的鉴权方案。WebFilter(/*) public class AuthFilter implements Filter { private static final ListString WHITE Arrays.asList( /login, /login.jsp, /css/, /js/, /captcha); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String path uri.substring(request.getContextPath().length()); boolean white WHITE.stream().anyMatch(path::startsWith); if (white || request.getSession().getAttribute(admin) ! null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }白名单里必须放静态资源路径否则登录页的 CSS 加载不出来页面裸奔。path::startsWith判断前缀/css/能匹配所有样式文件。过滤器顺序按 web.xml 里声明的先后执行注解方式顺序不确定所以鉴权过滤器我一般还是写在 web.xml 里保证最先执行。4.2 宿舍分配一个必须用事务的操作分配宿舍要同时做两件事更新房间已住人数、更新学生房间号。这两步必须在一个事务里否则更新了房间没更新学生数据就脏了。public boolean assignRoom(int studentId, int roomId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 第一步占床位带容量校验 String sql1 UPDATE dorm_room SET occupied occupied 1 WHERE id ? AND occupied capacity; PreparedStatement ps1 conn.prepareStatement(sql1); ps1.setInt(1, roomId); if (ps1.executeUpdate() 0) { conn.rollback(); return false; // 房间已满 } // 第二步更新学生 String sql2 UPDATE student SET room_id ?, status 1 WHERE id ?; PreparedStatement ps2 conn.prepareStatement(sql2); ps2.setInt(1, roomId); ps2.setInt(2, studentId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) {} return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) {} } }注意setAutoCommit(false)之后finally里要恢复成 true 再归还连接池否则这个连接被下一个请求拿到时还处在手动提交模式它的操作永远不生效这种玄学 bug 能查一下午。executeUpdate()返回 0 就回滚这是防超员的最后一道闸。4.3 JSP 渲染列表JSTL 循环与分页条Servlet 查完数据req.setAttribute(list, list)然后 forward 到 JSPJSP 里用 JSTL 遍历。别在 JSP 里写 Java 脚本片段% %那是 Model1 的老写法维护起来是灾难。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table trth学号/thth姓名/thth楼号/thth房间/thth操作/th/tr c:forEach items${list} vars tr td${s.stuNo}/td td${s.name}/td td${s.buildingNo}/td td${s.roomNo}/td tda hrefdetail?id${s.id}详情/a/td /tr /c:forEach /table${s.name}这种 EL 表达式默认能防 XSS 吗不能。如果姓名里存了script照样执行。输出到 HTML 用c:out value${s.name}/才转义。课程设计里没人攻击你但这是习惯问题面试被问到能加分。分页条用${page}/${totalPage}加前后页链接当前页高亮。总页数在 Servlet 里算好放 request 域别在 JSP 里算。5. 避坑与排查这套老技术栈最容易翻车的五个地方5.1 中文乱码现象是页面问号原因是编码链断了一环现象表单提交的中文存进库变成???或者从库读出来显示乱码。原因通常有三处JSP 页面pageEncoding没设、请求体编码没设、数据库连接串没设characterEncoding。解决JSP 头部% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8%Servlet 里req.setCharacterEncoding(UTF-8)必须在第一次getParameter之前调用连接串加characterEncodingutf8。三处都对齐乱码必消。5.2 404路径对了但访问不到多半是 context path 没算对现象http://localhost:8080/student/list报 404。原因IDEA 里 Application context 设的是/dorm真实路径是/dorm/student/list。解决JSP 里所有链接用${pageContext.request.contextPath}/student/list拼前缀别写死绝对路径。部署后先去 Tomcat 的 webapps 目录确认 war 解压出来的文件夹名那就是 context path。5.3 连接池耗尽现象是请求卡死原因是连接没归还现象用一会儿页面就转圈日志报wait millis 3000, active 20。原因某个 DAO 方法里Connection没在 finally 关闭或者异常路径漏了关闭。解决统一用 try-with-resources或者把关闭逻辑全塞进DBUtil.close()并在 finally 调用。排查时在 Druid 监控页看 active 连接数只增不减就是泄漏。5.4 JSP 修改不生效现象是改了页面刷新没变化原因是 Tomcat 缓存现象改了 JSP 重启都不一定生效。原因war exploded 模式下 IDEA 有时不会同步更新到 Tomcat 工作目录。解决Run Configuration 里 On Update action 选 Update classes and resourcesOn frame deactivation 也选它。还不行就删掉 Tomcat 的 work 目录重新部署JSP 编译产物缓存在那。5.5 MySQL 8 时区报错现象是连接失败原因是 serverTimezone 没配现象启动报The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8 默认时区和 JDBC 驱动解析不匹配。解决连接串加serverTimezoneAsia/Shanghai或者进 MySQL 执行SET GLOBAL time_zone 8:00。前者改代码后者改服务器推荐前者换环境不用重配。6. 进阶技巧把项目从「能跑」推到「能讲」做到这里系统能跑了但离「能拿去答辩、能写进简历」还差一步。我一般会补三个东西成本低但效果明显。第一个是统一返回格式。所有 Servlet 的 AJAX 接口返回{code:200,msg:ok,data:{}}这种结构前端用 fetch 统一处理。这样前端不用为每个接口写不同的解析逻辑出错时code非 200 直接弹提示。实现上写一个Result类加一个writeJson(resp, result)工具方法内部用 Jackson 或 Gson 序列化。第二个是操作日志表。谁在什么时候分配了哪个学生到哪个房间记一张operation_log表。答辩时老师问「你怎么追溯操作」这张表就是答案。写入时机放在业务方法成功后用同一个事务保证业务和日志同生共死。第三个是 SQL 慢查询自查。在 MySQL 里开SET GLOBAL slow_query_log ON; SET long_query_time 1;跑一遍系统所有功能看有没有超过 1 秒的查询。宿舍系统里最容易慢的是「按姓名模糊查学生」和「统计每栋楼入住率」前者加索引也救不了前置通配符后者用GROUP BY配合building_id索引能压到毫秒级。-- 入住率统计building_id 上有索引 SELECT b.building_no, SUM(r.capacity) AS total, SUM(r.occupied) AS used, ROUND(SUM(r.occupied) / SUM(r.capacity) * 100, 1) AS rate FROM dorm_building b JOIN dorm_room r ON r.building_id b.id GROUP BY b.id, b.building_no;这张表的数据直接喂给首页的 ECharts 饼图答辩演示时比干讲代码有说服力得多。最后说个习惯每改完一个功能立刻用 Postman 或浏览器把增删改查各跑一遍别攒到最后一起测。我吃过亏一次性改五个文件然后启动报错根本不知道是哪个改坏的回滚都找不到点。小步验证是这个老技术栈里最省时间的做法。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进