
简介一套基于jspservletmysql的实验室设备管理系统完整项目主要面向JavaWeb初学者、课程设计及毕业设计者用于解决实验室设备登记、分类、申请与人员管理等日常信息化需求覆盖设备信息维护、借用申请、分类统计、用户权限控制等典型模块。压缩包共182个文件包含34个jsp页面、java源码及编译后的class文件以及jar依赖库、css/js前端样式、jpg图片、sql数据库脚本等类型整体约6.21MB目录结构清晰便于部署。目前已有454人学习下载适合作为综合实训或毕业设计的参考模板。系统采用MVC架构视图由jsp承担控制由servlet处理数据通过JDBC存取mysql通过阅读源码与数据库脚本读者可以清晰理解请求转发流程、数据库连接池配置、增删改查实现思路以及设备管理功能从界面到后端实现的全过程对提升JavaWeb项目实践能力很有帮助。1. 实验室设备管理系统为什么值得你花一个周末读完每年 JavaWeb 课设交作业的高峰期“基于jspservletmysql的实验室设备管理系统.zip”这类压缩包就会被反复下载、改个名字又重新提交。别看 JSP 在很多招聘 JD 里已经不出现但它在课设里依旧是霸榜的技术栈因为它把从前端页面到数据库的整条请求链路串在了一个完整项目里。这个系统要处理的核心事就三件设备台账的增删改查、设备的借出与归还、登录用户的权限区分恰好覆盖了 Servlet 阶段最需要练习的知识点。对正在赶课设的学生、想补 JavaWeb 基础的新手、以及准备技术答辩的开发者来说把这个 zip 拆开跑通比看一百段零散 demo 都有用。2. 拿到zip先别急着解压运行先把项目结构和请求链路弄清楚很多人的第一反应是把 zip 解压后直接扔进 Tomcat 的 webapps 目录结果报错一堆、黑窗口一闪而过。我建议你先花十分钟把压缩包内部的结构看清楚再决定怎么部署。这类项目的目录结构是标准的 JavaWeb 分层源码、页面、配置、数据库脚本各司其职看懂了它你就看懂了大多数课设项目。2.1 解压后先看三个目录src、WebRoot、sql打开 zip 第一件事不是找 readme而是确认以下目录是否齐全src/ com/yourname/dao/DeviceDao.java com/yourname/servlet/DeviceServlet.java com/yourname/filter/AuthFilter.java WebRoot/ WEB-INF/web.xml login.jsp index.jsp device/list.jsp device/edit.jsp lib/ mysql-connector-java-8.0.30.jar sql/ lab_device.sqlsrc 是 Java 源码目录正常情况下里面至少要有 dao、servlet、filter 三组包WebRoot 是页面和配置目录web.xml 是所有 Servlet 和过滤器映射的入口lib 是依赖 jar 包必须包含 MySQL 驱动少了它项目一启动就会在 Class.forName 那行报 ClassNotFoundException。sql 目录存放建库脚本有些项目把它叫 db 或 doc名字不重要关键是里面得有 .sql 文件。如果你拿到的 zip 里只有 JSP 和 .class 文件、没有源码那说明是编译好的部署包改不了业务逻辑课设最好别用这种。判断项目完整度的方法也很简单源码里有 DeviceDao.java页面里有 list.jsp数据库脚本里有 CREATE TABLE这三样齐全就说明这条链路能跑通。lib 里的驱动 jar 版本要和本机 MySQL 匹配这一点我在第 5 章单独展开。2.2 从浏览器到数据库一次“设备列表查询”的四段路设备列表是这类系统的第一个业务功能。你在浏览器输入http://localhost:8080/lab/device?actionlistTomcat 会在 web.xml 里找匹配的 Servlet 映射调用 DeviceServlet 的 doGet 方法。Servlet 里不直接拼 HTML而是调 DeviceDao 查询 MySQL把结果放到 request 作用域里再 forward 给 device/list.jsp由 JSP 渲染成表格。web.xml 里对应的映射一般长这样servlet servlet-nameDeviceServlet/servlet-name servlet-classcom.yourname.servlet.DeviceServlet/servlet-class /servlet servlet-mapping servlet-nameDeviceServlet/servlet-name url-pattern/device/url-pattern /servlet-mappingurl-pattern 写/device意味着所有?actionlist、?actionadd之类的请求都会进同一个 Servlet再由 action 参数分流到对应方法。这是课设里最常见的“单 Servlet 多方法”写法优点是配置少缺点是 Servlet 会越写越长到第 6 章我会说怎么拆。servlet-name 只是标识真正决定路由的是 url-pattern改类名的时候记得同步改这里。这条链路里还有个关键点Servlet 转发给 JSP 用的是request.getRequestDispatcher(/device/list.jsp).forward(request, response)不是 sendRedirect。前者是服务器内部转发地址栏不变request 里的数据能带到页面后者是浏览器重定向会丢 request 属性导致列表页永远拿不到数据。很多新手在这里翻车页面空白却不知道去哪查。2.3 导入数据库和启动 Tomcat顺序错了部署就白费第一步不是启动 Tomcat而是先把数据库准备好。用命令行或图形工具执行 sql 目录里的脚本mysql -u root -p lab_device.sql然后在项目的数据库配置文件里改连接信息。老项目通常是一个 db.properties 或 DBUtil.java开头长这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/lab_device?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaisslModeDISABLED jdbc.usernameroot jdbc.password123456driver 写的是 MySQL 8.0 之后的驱动类如果你本地装的是 5.7要改回 com.mysql.jdbc.Driver。url 里的 useUnicode 和 characterEncoding 控制中文字符serverTimezone 解决 8.0 的时区报错sslModeDISABLED 避免 MySQL 8.0.33 以后频繁出现的 SSL 握手告警。password 改成你数据库自己的密码别直接复制。DBUtil 的核心就是加载完配置后用 DriverManager 拿连接public class DBUtil { private static String url jdbc:mysql://localhost:3306/lab_device?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaisslModeDISABLED; private static String user root; private static String password 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }静态块里 Class.forName 的作用是触发驱动类注册这一步不能省。如果这里抛 ClassNotFoundException多数情况是 lib 里的 jar 没放进去或者驱动类写错。启动 Tomcat 前还要确认项目目录放在 webapps 下访问路径就是http://localhost:8080/项目名/设备列表地址。我的启动习惯是先用 mysql -u root -p 登录确认能进库再启动 Tomcat最后看 catalina.out 日志每一步出问题都能把范围缩到最小。3. 数据库设计是这类系统的命门设备表、用户表、借用表怎么建才不返工链路走通之后决定这个系统能不能站稳的是数据库。很多课设评审老师不看前端样式但一定会打开数据库看表结构。设备管理系统的表数量不多三到四张就能覆盖全部业务设计得当后面 Servlet 代码能少写一半。3.1 三张核心表先想清楚状态机再动笔设备管理系统至少要三张表用户表、设备表、借用记录表。先想业务闭环用户登录后查询设备借出时把设备状态从“可用”改成“已借出”同时插入一条借用记录归还时更新记录的归还时间再把设备状态改回“可用”。这个状态流转就是表结构设计的核心。字段设计上我的习惯是t_user: id, username, password, real_name, role t_device: id, device_name, model, location, status, create_time t_borrow: id, user_id, device_id, borrow_time, return_timerole 字段区分管理员和普通用户普通用户只能查管理员能增删改。status 用 tinyint 而不是 varchar0 代表可用1 代表已借出2 代表维修中数字的等值查询走索引更快Java 端只需要一个 switch 就能映射成中文。borrow 表不直接存设备名称而是存 device_id这就是常见的“逻辑外键”查询时通过 JOIN 拿名称避免设备改名后历史记录跟着变。为什么不建物理外键约束我的做法是不建。课设里物理外键能加的分不多反而因为插入顺序和删除限制频繁报错业务代码里的逻辑外键更可控。但不用物理外键不等于不校验借出设备时代码里必须先判断 status0 再更新否则会出现一台设备被两个人同时借走的脏数据这个防守点在 4.2 节给出具体写法。3.2 建库脚本从 CREATE DATABASE 到 INSERT建库脚本通常长这样CREATE DATABASE IF NOT EXISTS lab_device DEFAULT CHARSET utf8mb4; USE lab_device; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT DEFAULT 1 ); CREATE TABLE t_device ( id INT PRIMARY KEY AUTO_INCREMENT, device_name VARCHAR(100) NOT NULL, model VARCHAR(100), location VARCHAR(200), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, device_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, INDEX idx_device_return (device_id, return_time) ); INSERT INTO t_user (username, password, real_name, role) VALUES (admin, 123456, 管理员, 0);脚本执行后要立刻插入一条管理员账号否则登录页永远进不去。密码字段课设范围可以直接明文但论文里要写一句“生产环境应当加密存储”。DEFAULT CHARSET 写 utf8mb4 而不是 utf8因为 utf8mb4 是完整的四字节字符集生僻字和 emoji 都能存MySQL 8.0 默认就是它显式写出来更保险。我特别解释一下最后一行 INDEXidx_device_return(device_id, return_time)是复合索引保证“查某台设备当前有没有未归还记录”走索引。否则随着借用记录增多每次借出前都要全表扫描。课设数据量小感觉不出来但面试聊起索引设计时这个点能证明你不是只会建表。3.3 为什么不许直接拼 SQLPreparedStatement 的两个理由“为什么不用拼接 SQL”是技术答辩必问题。很多同学图省事这样写String sql SELECT * FROM t_device WHERE device_name keyword ;keyword 里如果有单引号这条 SQL 就会断掉如果有人输入 OR 11查询条件永远成立这就是最典型的 SQL 注入。换成 PreparedStatementString sql SELECT * FROM t_device WHERE device_name LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ResultSet rs ps.executeQuery();问号是占位符setString 会把特殊字符转义数据库端把 SQL 模板和参数分开处理关键字永远不会被当成 SQL 执行。另一个实际好处是模板只用解析一次循环执行多条同构 SQL 时性能更好。注意 setString 的下标从 1 开始很多人第一反应写成 setString(0)会立刻报“Parameter index out of range”这个异常我见过太多次不是代码逻辑问题就是下标没对齐。4. Servlet 层的机房纪律登录拦截、设备借还、分页查询这样写能少改三次数据库就位后剩下的活都在 Servlet 层。这些代码写的时候多花十分钟后面改需求能少返工三次。这一章我给三类最常见业务给出可以直接抄的结构。4.1 登录拦截不用每个 Servlet 都塞一遍放进 Filter登录校验是内部页面的准入证但它不该出现在每个 Servlet 里。最干净的做法是写一个过滤器拦截所有页面地址统一检查 sessionpublic 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; Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这段代码的逻辑只有四行精髓从 session 里取登录标记取不到就重定向到登录页取到了就交给下一个过滤器或 Servlet 继续走。注意重定向一定要写request.getContextPath()拼前缀否则部署在带项目名的路径下时会跳错位置。用这个方法你后续新增十个页面一行登录校验都不用再写。只拦 Servlet 还不够理论上别人可以绕过 /device 直接访问 /device/list.jsp。想堵住这条路要么把受保护的 JSP 移到 WEB-INF 下面要么再配一层拦截 *.jsp 的过滤器。我的习惯是把页面挪进 WEB-INF外部 URL 不能直接访问只能由 Servlet 转发进去权限边界才闭合。登录成功后的主页通常就是个人信息展示页面顶部显示当前登录用户的姓名和角色下面接业务模块入口。这个页面是检验 session 是否写入成功的验证点登录后能看到自己的名字说明 session 生命周期正常。4.2 设备借还的状态切换与事务处理借出和归还是这类系统里唯一涉及“两步写库”的操作。借出设备时第一步更新设备状态为已借出第二步往借用记录插一条数据如果第一步成功第二步失败设备就变成“既已借出又没有记录”的脏数据。这两条 SQL 必须包进同一个事务Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); PreparedStatement ps1 conn.prepareStatement( UPDATE t_device SET status 1 WHERE id ? AND status 0); ps1.setInt(1, deviceId); int affected ps1.executeUpdate(); if (affected 0) { throw new RuntimeException(设备当前不在可借状态); } PreparedStatement ps2 conn.prepareStatement( INSERT INTO t_borrow (user_id, device_id) VALUES (?, ?)); ps2.setInt(1, userId); ps2.setInt(2, deviceId); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }这段有两个容易忽略的地方。第一是 UPDATE 语句里带AND status 0这是数据库层面的乐观锁保证只有当前状态为可用时才会更新成功executeUpdate 返回 0 就说明设备已经被借走直接抛异常回滚避免超卖。第二是 finally 里要把 autoCommit 还原成 true因为连接是从连接池借出来的还回去时状态必须是干净的否则下一个使用者的事务边界会被污染。这个细节答辩时讲出来面试官会加印象分。设备状态在列表页展示时可以用背景图坐标定位的方式切换图标。具体做法是一张精灵图把可用、已借、维修三个小图标横向排列在 JSP 里根据 status 输出不同的 background-position。这个方案比三张图片三行 if 判断省请求也比绝对定位固定像素靠谱窗口缩放时不会错位。4.3 条件查询加排序分页参数怎么拼、回显怎么保存设备列表一般都要支持按名称模糊查询、按状态筛选、按时间排序。动态拼 SQL 时最稳的姿势是先写死WHERE 11然后按条件往后追加参数统一装进 Listpublic ListDevice search(String name, Integer status, int pageNo, int pageSize) { StringBuilder sql new StringBuilder(SELECT * FROM t_device WHERE 11); ListObject params new ArrayList(); if (name ! null !name.isEmpty()) { sql.append( AND device_name LIKE ?); params.add(% name %); } if (status ! null) { sql.append( AND status ?); params.add(status); } sql.append( ORDER BY create_time DESC LIMIT ? OFFSET ?); params.add(pageSize); params.add((pageNo - 1) * pageSize); PreparedStatement ps conn.prepareStatement(sql.toString()); for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } // 执行查询并封装结果 }pageNo 从 1 开始传OFFSET 必须是(pageNo-1) * pageSize很多人直接拿页码当 offset从第 0 条开始查第一页永远少一条。另一个坑是 ORDER BY 后面的字段不能用占位符拼排序字段属于结构而非数据只能做白名单校验把允许排序的列名放进一个 Set传进来的找不到就默认 create_time直接把参数拼进 ORDER BY 等于开了一个注入后门。JSP 回显的关键点是“提交后输入框别清空”。表单里的搜索框这样写input namedeviceName value${param.deviceName} / select namestatus option value全部/option option value0 ${param.status 0 ? selected : }可用/option option value1 ${param.status 1 ? selected : }已借出/option /select${param.deviceName} 会从 URL 参数里取回上次输入的值不需要在 Servlet 里多存一遍。分页导航的页码超链接也要把查询参数原样带回去例如list?actionsearchdeviceName${param.deviceName}pageNo${pageNo1}否则用户翻页时会丢搜索条件。这里最常犯的错是把当前页写死成 1用户从第三页点“下一页”又跳回第一页。5. 避坑手册这类项目部署时的 5 个高发问题很多同学拿到的 zip 在自己电脑上跑得挺好换个环境就趴窝。下面这些问题不是代码逻辑错而是环境和认知偏差按我这些年带课设见到的频率排序。5.1 Tomcat 10 下编译报错javax.servlet 不存在现象项目一编译就报“程序包 javax.servlet 不存在”或者运行时 404控制台全是 ClassNotFound。原因Tomcat 从 10.0 开始Servlet API 的包名从 javax.servlet 换成了 jakarta.servlet老代码基于 javax 写放到 Tomcat 10 里自然找不到类。这个是版本迁移的历史遗留问题跟代码本身没关系。解决优先换回 Tomcat 9它是 Servlet 4.0 规范跟老项目完全兼容。如果你坚持用 Tomcat 10就把项目里所有import javax.servlet.*改成import jakarta.servlet.*同时注意 lib 里不要再放 servlet-api 包这个 jar 由 Tomcat 自己提供手动放进去会出现 ClassCastException。我一般直接换 Tomcat 9省事且稳定。5.2 连不上 MySQL驱动类、时区和 SSL 三连错现象启动后访问页面报 ClassNotFoundException通常是 com.mysql.jdbc.Driver 找不到或者报The server time zone valueMySQL 8.0.33 以上还会报 SSL 连接错误。原因MySQL 5.7 时代的教程驱动类是 com.mysql.jdbc.Driver而 MySQL 8.0 的驱动类改成了 com.mysql.cj.jdbc.Driver。很多人装的是 8.0 数据库代码里写的还是老驱动类时区报错是 8.0 新增的校验SSL 告警也是 8.0.33 以后才频繁出现的。如果你按教程装的 mysql 5.7.44则保持老驱动写法即可问题通常出在“数据库 8.0 老教程代码”这种混搭。解决驱动类和数据库版本对齐URL 里加serverTimezoneAsia/Shanghai再加sslModeDISABLED。完整连接串在第 2.3 节直接抄过去。驱动 jar 的版本也建议选与数据库主版本一致的别拿 5.1 的 jar 去连 8.0 的库那会触发各种奇怪的协议错误。5.3 Tomcat 启动闪退日志在哪看现象双击 startup.bat黑窗口一闪而过浏览器访问 8080 就是连不上。原因Tomcat 把错误信息写在日志里不会停在黑窗口上所以黑窗口一闪而过不是没启动是启动失败立刻退出了。最常见的两个8080 端口被占用日志里有Address already in use或者 JAVA_HOME 环境变量没指到 JDK 路径。解决打开 Tomcat 安装目录下的 logs/catalina.outWindows 下可能是 catalina.日期.log看末尾异常堆栈。端口被占用则改 conf/server.xml 里 Connector 的 port 为 8081 或 9090JAVA_HOME 错误则在系统环境变量里重新指定 JDK 路径。记住看日志而不是看黑窗口这是第一条血泪经验。5.4 中文乱码数据库正常但网页是问号现象用 Navicat 查表中文正常但网页显示乱码或变成???。原因三处编码没对齐。JSP 页面本身的 pageEncoding 没设 UTF-8数据库连接 URL 里没带 characterEncodingutf8建表时 DEFAULT CHARSET 用了 latin1 而不是 utf8mb4。任何一个环节断掉中文都会在链路某个地方变乱码。解决把三处统一为 UTF-8。JSP 头部写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %连接 URL 加 characterEncodingutf8SQL 脚本把 DEFAULT CHARSET utf8mb4 写清楚如果库已经建了执行ALTER TABLE t_device CONVERT TO CHARACTER SET utf8mb4。还有一个细节是 Servlet 里 request.setCharacterEncoding(UTF-8) 必须在第一次读取参数前调用否则 POST 表单的中文在入口就坏了。5.5 改了 JSP 代码浏览器刷新还是旧页面现象明明改了 JSP 源码CtrlF5 也按了页面还是旧内容改 Servlet 也不生效。原因有两层。一是浏览器缓存了静态资源二是 Tomcat 没有热部署你改的是源码目录但 webapps 下跑的还是编译后的副本两边不是同一份文件。解决先强制刷新 CtrlF5如果仍无效在 Tomcat 的 manager 页面触发 reload或者直接重启 Tomcat确认你改的是真正运行的那份文件而不是项目副本。JSP 页面加载后想自动刷新一次的话可以在 head 里放meta http-equivrefresh content1但这个只用来调试别留在正式代码里否则用户每 1 秒刷新一次页面性能承受不住。6. 从课设到工程三个改造方向和一条验证主线如果把这个 zip 交上去就完事它只有分数价值。我更推荐把它当成半成品继续改造这里给三个成本最低、收益最明显的方向。第一个改造是把 DBUtil 换成连接池。用 druid 或 HikariCP 都行配置大同小异以 druid 为例在项目里加一个 druid.propertiesinitialSize2 maxActive10 jdbc.urljdbc:mysql://localhost:3306/lab_device?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaisslModeDISABLED jdbc.usernameroot jdbc.password123456原来每次请求都 DriverManager.getConnection 一次换成连接池后连接是池里借来的用完 close 只是归还并发场景下性能天差地别。第二个改造是把 Servlet 里越写越长的业务代码拆出 Service 层DeviceServlet 只负责接参数和跳页面借还判断、状态流转移到 DeviceServiceDAO 只做 SQL。第三个改造是把 JSP 里散落的% %脚本片段换成 EL 和 JSTL页面清爽维护成本也低。改完之后用一条主线验证系统是否完整管理员登录 → 新增一台设备 → 在列表页按名称搜索到它 → 借出 → 确认状态变为已借出 → 归还 → 确认状态回到可用 → 退出登录后直接访问设备列表 URL确认被拦截回登录页。这条路径覆盖了登录、增删改查、状态流转、访问控制能走通这个项目就能交差。我当年改这类项目时最惨的一次是把 Tomcat 8 的项目直接扔进 Tomcat 10全是 jakarta 报错查了半天才意识到不是代码问题。所以环境问题先看版本业务问题再看日志希望帮到你。本文还有配套的精品资源点击获取