ARTICLE DETAIL

资讯详情

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

Java原生Servlet+JSP+MySQL宿舍管理系统实战:从零手写吃透JavaWeb全链路

Java原生Servlet+JSP+MySQL宿舍管理系统实战:从零手写吃透JavaWeb全链路 简介这是一套面向Java Web初学者的ServletJSPMySQL宿舍管理系统实战项目适合刚接触Web开发、需要完整练手案例的学生与自学者。项目围绕宿舍信息管理展开涵盖用户注册登录、宿舍数据增删改查等核心业务帮助理解Servlet处理请求、JSP渲染视图、JDBC连接数据库的完整链路。压缩包共63个文件约10.01MB包含18个java源码与18个class编译文件、5个jsp页面、3个jar依赖包以及sql建表脚本、配置说明文档和演示视频源码、数据库与讲解材料配套齐全。已有1160人学习下载。通过该项目可掌握MVC分层组织代码、session保持登录状态、SQL语句操作数据等实用技能并参考配置文档与录屏快速跑通环境是入门Java Web开发、积累课程设计或毕业设计素材的合适选择。1. 从零手写 Servlet JSP MySQL 宿舍管理系统为什么原生 JSP 依然值得练很多同学一上来就问都 2025 年了Spring Boot 一把梭不香吗为什么还要用 Servlet JSP 写宿舍管理系统我自己的血泪经验是——正因为这套东西“土”它才把 HTTP 请求怎么进来、Session 怎么存、JDBC 怎么连、JSP 怎么被编译成 Servlet 这些黑匣子全摊在你面前。你写一遍原生 JSP再去看 Spring MVC 的 DispatcherServlet会有一种“原来你只是帮我包了一层”的顿悟。这篇笔记就围绕“java基于servletjspmysql简单宿舍管理系统jsp原生界面开发”这个标题把环境搭建、表结构设计、登录鉴权、宿舍与学生的增删改查、JSP 页面复用、分页与排错一条线讲透。适合正在做课程设计、毕设选题或者想补 JavaWeb 基础的人跟着敲能跑起来也能看清边界在哪。2. 环境与工程骨架把 Servlet 跑起来的最小闭环2.1 版本选型与依赖清单原生 JSP 项目最怕版本打架。我一般锁定一套“老而稳”的组合JDK 8 或 11、Tomcat 8.5/9.0、MySQL 5.7/8.0、Servlet API 3.1、JSTL 1.2。JDK 别上 17 配老 Tomcat容易在模块化上翻车。MySQL 8.0 记得驱动用com.mysql.cj.jdbc.Driver连接串要带时区和 SSL 参数否则控制台会甩你一脸时区异常。组件推荐版本说明JDK8 / 11与 Tomcat 8.5/9 兼容性最好Tomcat8.5.x / 9.0.x支持 Servlet 3.1/4.0MySQL5.7 / 8.08.0 注意驱动类名与连接参数Servlet API3.1用注解WebServlet免去 web.xmlJSTL1.2JSP 页面循环与判断用目录结构按 Maven 标准来别用 Eclipse 默认那种WebContent老结构后面换 IDE 会后悔dorm-system/ ├── pom.xml └── src/main/ ├── java/com/dorm/ │ ├── dao/ // 数据访问 │ ├── entity/ // 实体类 │ ├── servlet/ // 控制器 │ └── util/ // DBUtil 等工具 └── webapp/ ├── WEB-INF/web.xml ├── css/ js/ └── *.jsp2.2 用 Maven 拉齐依赖pom.xml里把 Servlet、JSP、JSTL、MySQL 驱动一次配好。注意 Servlet 和 JSP 的 scope 用provided因为 Tomcat 自带打进 war 反而冲突。dependencies !-- Servlet APITomcat 已提供scope 必须 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- JSP API 同理 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.1/version scopeprovided/scope /dependency !-- JSTL页面标签库必须打进 war -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies参数说明provided表示编译期可见、运行期由容器提供JSTL 若也写provided页面会报NoClassDefFoundError。MySQL 驱动版本和你的 MySQL 服务端大版本对齐即可8.0.28 对 8.0.x 服务端足够。2.3 数据库连接工具类 DBUtilJDBC 连接不要在每个 Servlet 里new抽一个工具类用静态块加载驱动提供获取和关闭连接的方法。生产环境该上连接池课程设计阶段用 DriverManager 足够但要知道它的边界每次请求都新建物理连接并发一高就顶不住。public class DBUtil { // MySQL 8 连接串必须带时区与 SSL 参数否则报时区异常 private static final String URL jdbc:mysql://localhost:3306/dorm_db?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PWD 你的密码; static { try { // MySQL 8 用 cj 驱动5.x 用 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(驱动加载失败, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } // 统一关闭顺序ResultSet - Statement - Connection public static void close(Connection c, Statement s, ResultSet r) { try { if (r ! null) r.close(); } catch (SQLException ignored) {} try { if (s ! null) s.close(); } catch (SQLException ignored) {} try { if (c ! null) c.close(); } catch (SQLException ignored) {} } }逻辑说明静态块只执行一次保证驱动注册getConn每次返回新连接close按依赖倒序关闭避免游标泄漏。参数上serverTimezone不写会在 MySQL 8 上抛The server time zone value ... is unrecognized这是新手最常见的翻车点之一。3. 表结构设计与登录鉴权Session 到底存了什么3.1 四张核心表怎么建宿舍管理系统的数据关系很清晰一栋楼有多个宿舍一个宿舍住多个学生一个管理员管全部。我一般建四张表admin管理员、dorm宿舍、student学生、building楼栋可选。学生表用dorm_id外键关联宿舍删除宿舍前要先处理学生否则外键约束会拦你。CREATE DATABASE dorm_db DEFAULT CHARSET utf8mb4; USE dorm_db; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL -- 存 MD5 或加盐哈希别存明文 ); CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(16) NOT NULL UNIQUE, -- 如 3-201 capacity INT DEFAULT 4, -- 默认容量mysql 设置默认值为 0 同理 building VARCHAR(32), used INT DEFAULT 0 -- 已住人数冗余字段便于列表展示 ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE, -- 学号 name VARCHAR(32) NOT NULL, gender CHAR(1) DEFAULT 男, phone VARCHAR(20), dorm_id INT, CONSTRAINT fk_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(id) );参数说明utf8mb4支持 emoji 和生僻字capacity和used用DEFAULT给初值避免插入时漏字段导致 NULL外键fk_dorm保证学生不会挂到不存在的宿舍。注意used是冗余字段入住/退宿时要同步更新否则列表显示和实际人数会对不上这是典型的“数据不一致”坑。3.2 登录 Servlet 与 Session 鉴权登录流程表单 POST 到LoginServlet查库比对密码成功则把用户对象塞进HttpSession重定向到列表页失败回登录页带错误提示。关键点是——每个需要登录的页面都要校验 Session否则别人直接敲 URL 就能进。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String u req.getParameter(username); String p req.getParameter(password); AdminDao dao new AdminDao(); Admin admin dao.findByUsername(u); // 密码比对实际项目应比对哈希值 if (admin ! null admin.getPassword().equals(MD5Util.md5(p))) { HttpSession session req.getSession(); session.setAttribute(admin, admin); // 登录凭证 session.setMaxInactiveInterval(30 * 60); // 30 分钟过期 resp.sendRedirect(req.getContextPath() /dorm/list); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }逻辑说明getSession()无参调用会在没有会话时新建setAttribute把管理员对象存进服务端内存浏览器只拿到JSESSIONID。参数setMaxInactiveInterval单位是秒不设则用 Tomcat 默认 30 分钟。注意sendRedirect和forward的区别前者让浏览器重新发请求地址栏变化适合登录成功后者服务器内部转发地址栏不变适合带错误信息回显。3.3 一个登录过滤器顶住所有页面与其在每个 Servlet 里写校验不如用一个Filter统一拦截。放行登录页、登录接口和静态资源其余一律检查 Session。WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r (HttpServletRequest) req; HttpServletResponse p (HttpServletResponse) resp; String uri r.getRequestURI(); // 放行登录相关与静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(/login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } if (r.getSession().getAttribute(admin) null) { p.sendRedirect(r.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }逻辑说明WebFilter(/*)拦截全部请求白名单用endsWith/contains判断简单直接。参数上要注意getRequestURI带上下文路径判断时别写死。这个过滤器是整套系统的安全底线漏了它后面所有页面都是裸奔。4. 宿舍与学生的增删改查Servlet 到 JSP 的完整链路4.1 DAO 层把 SQL 关进一个类里所有 SQL 集中在 DAOServlet 只负责取参、调 DAO、跳页面。这样改表结构时只动一处。以宿舍列表和新增为例public class DormDao { // 查询全部宿舍供列表页展示 public ListDorm findAll() { ListDorm list new ArrayList(); String sql SELECT id, room_no, capacity, building, used FROM dorm ORDER BY room_no; try (Connection c DBUtil.getConn(); PreparedStatement ps c.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Dorm d new Dorm(); d.setId(rs.getInt(id)); d.setRoomNo(rs.getString(room_no)); d.setCapacity(rs.getInt(capacity)); d.setBuilding(rs.getString(building)); d.setUsed(rs.getInt(used)); list.add(d); } } catch (SQLException e) { throw new RuntimeException(查询宿舍失败, e); } return list; } // 新增宿舍 public int insert(Dorm d) { String sql INSERT INTO dorm(room_no, capacity, building, used) VALUES(?,?,?,0); try (Connection c DBUtil.getConn(); PreparedStatement ps c.prepareStatement(sql)) { ps.setString(1, d.getRoomNo()); ps.setInt(2, d.getCapacity()); ps.setString(3, d.getBuilding()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增宿舍失败, e); } } }逻辑说明用PreparedStatement占位符防 SQL 注入别用字符串拼接try-with-resources 自动关连接比手动 close 干净。参数上ORDER BY room_no让列表稳定排序used插入时固定为 0。注意executeUpdate返回受影响行数可用于判断是否成功。4.2 Servlet 控制器一个类管一个模块宿舍模块的 Servlet 用action参数区分操作避免建一堆类。列表、跳新增页、保存、删除都走同一个入口。WebServlet(/dorm/*) public class DormServlet extends HttpServlet { private final DormDao dao new DormDao(); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); // 如 /list、/delete if (/list.equals(path)) { req.setAttribute(dorms, dao.findAll()); req.getRequestDispatcher(/dorm_list.jsp).forward(req, resp); } else if (/delete.equals(path)) { int id Integer.parseInt(req.getParameter(id)); dao.delete(id); resp.sendRedirect(req.getContextPath() /dorm/list); } else if (/add.equals(path)) { req.getRequestDispatcher(/dorm_add.jsp).forward(req, resp); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 表单提交新增 Dorm d new Dorm(); d.setRoomNo(req.getParameter(roomNo)); d.setCapacity(Integer.parseInt(req.getParameter(capacity))); d.setBuilding(req.getParameter(building)); dao.insert(d); resp.sendRedirect(req.getContextPath() /dorm/list); } }逻辑说明WebServlet(/dorm/*)用通配符把子路径都收进来getPathInfo()拿到/list这类后缀做分发。参数上Integer.parseInt要防非数字输入实际项目应加 try-catch 或前端校验。删除后必须重定向否则刷新页面会重复提交。4.3 JSP 页面用 JSTL 循环渲染别写 Java 脚本原生 JSP 最容易犯的错是在页面里写% %脚本。用 JSTL 的c:forEach和c:if页面干净也符合 Model2 思想。列表页核心片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 trth宿舍号/thth楼栋/thth容量/thth已住/thth操作/th/tr c:forEach items${dorms} vard tr td${d.roomNo}/td td${d.building}/td td${d.capacity}/td td${d.used}/td td a href${pageContext.request.contextPath}/dorm/delete?id${d.id} onclickreturn confirm(确认删除)删除/a /td /tr /c:forEach /table逻辑说明c:forEach遍历 Servlet 放进 request 的dorms${pageContext.request.contextPath}拼上下文路径避免部署路径变化导致链接失效。参数上onclick加二次确认防止误删。注意 JSTL 标签库的uri必须和 jar 版本匹配1.2 就是上面这个地址。4.4 学生入住与退宿同步更新冗余字段学生入住时除了更新student.dorm_id还要把对应宿舍的used加一退宿则减一。这两步要么放一个事务里要么接受短暂不一致。课程设计阶段用事务更稳妥。public void checkIn(int studentId, int dormId) { String sql1 UPDATE student SET dorm_id? WHERE id?; String sql2 UPDATE dorm SET used used 1 WHERE id? AND used capacity; Connection c null; try { c DBUtil.getConn(); c.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 c.prepareStatement(sql1); PreparedStatement ps2 c.prepareStatement(sql2)) { ps1.setInt(1, dormId); ps1.setInt(2, studentId); ps1.executeUpdate(); ps2.setInt(1, dormId); int rows ps2.executeUpdate(); if (rows 0) { // 宿舍已满回滚 c.rollback(); throw new RuntimeException(宿舍已满); } c.commit(); } } catch (SQLException e) { try { if (c ! null) c.rollback(); } catch (SQLException ignored) {} throw new RuntimeException(入住失败, e); } finally { try { if (c ! null) c.close(); } catch (SQLException ignored) {} } }逻辑说明setAutoCommit(false)开启事务两步都成功才commitused capacity条件保证不会超员返回 0 行说明满了就回滚。参数上注意used 1是数据库端自增避免并发下读改写丢失更新。这是整套系统里最值得写事务的地方。5. 避坑与排查那些让我熬夜的原生 JSP 问题5.1 中文乱码POST 和 GET 要分开治现象表单提交后数据库里是问号或页面显示乱码。原因Tomcat 默认按 ISO-8859-1 解码请求体JSP 页面没声明编码。解决POST 在读取参数前设req.setCharacterEncoding(UTF-8)GET 在 Tomcat 8 以后默认 UTF-8老版本要在server.xml的 Connector 加URIEncodingUTF-8JSP 顶部写% page contentTypetext/html;charsetUTF-8 %数据库和表用utf8mb4。四处都对齐才彻底。5.2 404路径对不上是常态现象访问/dorm/list报 404。原因WebServlet的路径和浏览器请求不一致或没配web.xml的metadata-complete。解决确认注解路径、上下文路径、web.xml版本是 3.1否则注解不生效。用req.getContextPath()拼链接别写死/项目名。5.3 500 空指针DAO 返回 null 没判现象登录成功但列表页 500日志指向NullPointerException。原因findByUsername查不到返回 null调用方直接.getPassword()。解决DAO 查不到返回 null 是约定调用方必须先判空列表查询返回空集合而不是 null页面c:forEach才不会炸。5.4 连接泄漏忘了关 ResultSet现象跑一会儿就报Too many connections。原因异常路径下连接没关或只关了 Connection 没关 ResultSet。解决统一用 try-with-resources或走 DBUtil.close 按序关闭。压测时用SHOW PROCESSLIST看连接数能直观发现泄漏。5.5 JSTL 报错jar 没打进 war现象页面报The absolute uri ... cannot be resolved。原因JSTL 依赖 scope 写成provided或版本不对。解决改成默认 scope 打进 war确认jstl-1.2.jar在WEB-INF/lib下。这是新手配 JSP 页面时的高频翻车点。6. 分页、复用与验证让原生 JSP 项目更像样6.1 手写分页两个参数搞定列表数据一多就得翻页。核心就两个参数当前页page和每页条数sizeSQL 用LIMIT偏移。先查总数算总页数再查当前页数据。public ListDorm findByPage(int page, int size) { int offset (page - 1) * size; // 偏移量从 0 开始 String sql SELECT * FROM dorm ORDER BY room_no LIMIT ?, ?; ListDorm list new ArrayList(); try (Connection c DBUtil.getConn(); PreparedStatement ps c.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, size); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Dorm d new Dorm(); d.setId(rs.getInt(id)); d.setRoomNo(rs.getString(room_no)); list.add(d); } } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } return list; }参数说明page从 1 开始offset (page-1)*sizeLIMIT ?, ?第一个是偏移第二个是条数。注意 MySQL 的LIMIT偏移大时性能会下降数据量大要改用游标或索引优化。总页数用SELECT COUNT(*)单独查别在分页 SQL 里算。6.2 页面复用include 抽公共头尾每个 JSP 都写一遍导航和页脚是灾难。用% include fileheader.jsp %静态包含或jsp:include pageheader.jsp/动态包含。静态包含在编译期合并适合不变的头部动态包含运行期调用适合内容会变的场景。头部里放导航和${sessionScope.admin.username}显示当前用户一处改处处生效。6.3 怎么验证这套系统真的能用别只看页面能打开。我一般按这几步验证一是用SHOW PROCESSLIST看有没有连接堆积二是故意提交超员入住确认事务回滚且used没变三是直接敲未登录的列表 URL确认被过滤器拦回登录页四是把某条 SQL 写错看异常是否被包装成可读信息而不是堆栈直出。这几步走完系统的健壮性心里就有数了。6.4 一个我常留的习惯我写这类原生 JSP 项目最后一定会留一个README记三件事数据库初始化脚本、Tomcat 版本、连接串参数。因为过两周再打开最先忘的就是这些。原生 JSP 不是终点但它是你理解 JavaWeb 全链路最扎实的一站把 Session、Filter、JDBC 事务、JSTL 这几块吃透再上框架就是降维。希望帮到你。本文还有配套的精品资源点击获取
返回列表