新闻详情

新闻详情

首页 / 资讯中心 / 详情

JSP+Servlet+MySQL设备管理实战:从建表到部署避坑指南

发布时间:2026/10/1 17:28:03来源:尧图网络
JSP+Servlet+MySQL设备管理实战:从建表到部署避坑指南
简介基于JSP、Servlet与MySQL技术栈实现的实验室设备管理系统完整源码包面向Java Web方向的初学者及有课程设计、毕业设计需求的读者能够帮助读者快速掌握设备信息管理、设备借用申请、用户权限控制等典型功能模块的开发。压缩包共包含182个文件主要涵盖JSP动态页面、Java源文件、编译后的class文件、数据库SQL脚本、CSS和JavaScript前端样式与交互资源以及项目配置文件和图片素材整体大小约6.21MB目录结构清晰便于按模块逐一查阅。资源内包含完整的项目层次从表示层到业务逻辑层再到数据访问层均有代码示例并附有MySQL建表语句可快速初始化数据库并启动运行。目前该资源已有454人学习或下载适合用于课程设计参考或二次开发。通过研读源码可以深入理解MVC设计思想、设备增删改查与借用审批的实现细节是一套完整且可直接利用的Java Web项目案例。1. 基于 jspservletmysql 的设备管理为什么我劝你别急着换框架实验室设备管理这个题目在毕业设计和课程设计里出现频率极高。设备台账要登记、借出要记录、归还要核销很多人第一反应是上 Spring Boot Vue结果环境装了一整天代码还没开始写。其实对于一个几十行表单、几个列表页、一次登录校验的系统jspservletmysql 这个经典 JavaWeb 组合反而是最快的路线——它没有复杂的自动配置所有请求流向都看得见出了问题翻一翻 servlet 代码就能定位。这套系统适合两类人一类是做毕设或课设的学生需要用最短时间交付一个完整可演示的项目另一类是校内实验室或小单位的管理者设备规模不超过几百台不想为一个小工具专门养一套前后端分离的框架。本文就按这套技术栈从建表、写 servlet 到打包 war 部署把完整落地路径和踩过的坑讲清楚。2. 先把表和三层结构立住数据库建表与 servlet 生命周期2.1 设备、借还、用户三张表的设计思路与建表 SQL设备管理系统最核心的实体就是设备本身。设备表不能只设计 model、name、status 三个字段还要考虑后续统计口径。我一般会在设备表里加上 category设备分类、location存放位置、purchase_date购置日期、price单价、principal责任人这些字段。分类用于筛选位置用于盘点购置日期和价格用于报废评估责任人用于明确保管归属。借还记录表是第二个核心它需要记录 device_id、user_id、borrow_time、return_time、operator_id。注意 return_time 要允许为空空值代表「未归还」。这个表会越滚越大所以必须给 device_id 和 borrow_time 建索引否则数据量到几万条以后设备查询会明显变慢。索引是很便宜的性能投资现在不建以后查询慢再补就是一次全表扫描的教训。用户表按最小权限模型设计只保留 id、username、password、role、real_name。password 字段注意长度定 64因为后面即使用 MD5 或 SHA-256 加盐32 位也放不下两种算法的结果。角色 role 用字符串表示admin 和 user 两种足够用没必要做成角色表。下面是建表 SQL我用 MySQL 5.7 的 InnoDB 引擎来写字符集统一 utf8mb4。utf8mb4 比 utf8 多支持 emoji 和特殊符号虽然设备名里一般不会有 emoji但学生交作业时如果某个设备备注里贴了表情符号utf8 的表会直接报错这点不用赌。CREATE DATABASE IF NOT EXISTS lab_device DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE lab_device; CREATE TABLE t_device ( id INT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(50) NOT NULL UNIQUE COMMENT 设备编号, device_name VARCHAR(100) NOT NULL COMMENT 设备名称, category VARCHAR(50) COMMENT 设备分类, model VARCHAR(100) COMMENT 规格型号, location VARCHAR(100) COMMENT 存放位置, purchase_date DATE COMMENT 购置日期, price DECIMAL(10,2) DEFAULT 0 COMMENT 单价(元), status TINYINT DEFAULT 1 COMMENT 1可用2借出3维修4报废, principal VARCHAR(50) COMMENT 责任人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category (category), KEY idx_status (status) ) ENGINEInnoDB COMMENT设备台账表; CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL, user_id INT NOT NULL, borrow_time DATETIME NOT NULL, return_time DATETIME DEFAULT NULL, operator_id INT COMMENT 经办人, remark VARCHAR(255) COMMENT 备注, KEY idx_device_id (device_id), KEY idx_borrow_time (borrow_time), CONSTRAINT fk_borrow_device FOREIGN KEY (device_id) REFERENCES t_device (id) ) ENGINEInnoDB COMMENT借还记录表; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 加盐哈希后的密码, real_name VARCHAR(50) NOT NULL, role VARCHAR(20) DEFAULT user COMMENT admin或user, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT用户表;建表时有几个细节要解释。t_device 的 device_code 我加了 UNIQUE 约束这是为了后面做批量导入时能通过 code 判断是否重复比用 id 判断更可靠。t_borrow 的外键写在建表语句里如果后面导入数据时发现外键报错可以在导入前临时 SET FOREIGN_KEY_CHECKS0但平时查询不要关。status 字段用 TINYINT 不用 VARCHAR是因为页面下拉列表的选项数值在代码里好判断而且状态值做索引占用空间小。借还记录表和设备表之间是一对多关系一台设备可以被不同用户在不同时间借用但同一时刻同一个设备只能被一个人借走。这个业务规则我放在 servlet 层判断不在数据库里做因为数据库的 check 约束在 MySQL 5.7 之前很多版本并不强制执行写在代码里反而直观。2.2 servlet 生命周期与请求转发控制层该做什么不该做什么传统 jsp 项目里servlet 是控制层jsp 是视图层mysql 通过 dao 层访问。很多新手会把业务逻辑写在 jsp 里用%直接连数据库这种写法在作业里能跑但一旦页面里出现数据库异常整个页面直接变成 500 错误页排查问题要同时翻 jsp 和数据库日志非常被动。我规范的做法是servlet 只做三件事——接收参数、调用 dao、决定转发到哪个 jsp。判断逻辑里可以放简单的业务规则比如「设备是否可借」但复杂计算应该放到 service 层。servlet 生命周期是面试题也是实际排查问题的基础。一个 servlet 从创建到销毁经历 init、service、destroy 三个阶段。init 在第一次请求到达时执行只执行一次适合做资源初始化service 方法每次请求都会执行doGet 和 doPost 是它的两个分支destroy 在应用卸载或 Tomcat 关闭时执行用来释放资源。实际部署中一个常见的误解是认为每个请求都会创建一个新的 servlet 实例。事实是默认情况下整个应用只有一个 servlet 实例所有请求共用它。所以 servlet 里的成员变量是线程不安全的如果写了非 final 的成员变量来暂存请求参数高并发时会出现数据串号——A 用户提交的设备名显示到 B 用户的页面上。这个坑很隐蔽我第一次遇到时以为是浏览器缓存问题后来断点调试才发现是成员变量被多线程共享。所以 servlet 里除了静态常量和 final 引用不要放可写的实例字段。请求转发和重定向也要分清楚。转发用 request.getRequestDispatcher(deviceList.jsp).forward(request, response)地址栏不变request 对象在转发前后是同一个所以 jsp 里可以直接用 request.setAttribute 存的数据。重定向用 response.sendRedirect(deviceList?pageNo2)浏览器会发起第二次请求地址栏会变成新的 URL之前的 request 里的 attribute 全部丢失。如果想通过重定向传参只能手动拼在 URL 后面servlet 里再解析这种方式传中文极其容易乱码。在设备管理系统里新增数据后我习惯用重定向作用是防止表单重复提交。如果新增后是转发到列表页用户按 F5 刷新浏览器会重新提交上一次的 POST 请求导致同一台设备被插入两条记录。重定向以后F5 刷新只是重新 GET 列表页不会再触发 POST。2.3 为什么我不直接 JDBC 裸连而是用连接池含参数配置刚学 servlet 时我写过直接在 dao 里这样的代码每个方法开头 DriverManager.getConnection(url, user, password)用完就 close。这种方式在作业里没问题但在系统演示时如果设备列表页一分钟内有十几个人同时访问Tomcat 端会出现连接数耗尽页面就卡住转圈。原因是这样的裸连每秒创建一个新连接数据库端要频繁处理握手和鉴权MySQL 的默认并发连接数很快被打满。连接池的作用是预先建好一批连接放在池子里请求来了直接拿用完归还而不是关闭。JavaWeb 传统项目最常用的是阿里的 Druid 或 Apache 的 DBCP我习惯用 Druid因为它的监控页面在调试时能直接看出 SQL 慢在哪里。Druid 是一个 jar 包Maven 仓库里有没有 Maven 就下载 jar 放进 WebRoot/WEB-INF/lib 目录同时把 mysql-connector-java 的 jar 也放进去。Druid 的配置文件我一般写成 druid.properties 放在 src 根目录内容如下driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/lab_device?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait30000 minIdle5 timeBetweenEvictionRunsMillis60000 validationQuerySELECT 1 testWhileIdletrue testOnBorrowfalse poolPreparedStatementstrue注意 url 里的几个参数。useUnicodetrue 和 characterEncodingutf8 是为了让中文能在数据库连接里正常读写useSSLfalse 是为了避免 MySQL 5.7 驱动连接时强制 SSL 握手导致的告警serverTimezoneAsia/Shanghai 是 MySQL 8.x 驱动的必选项5.7 驱动不写也能跑但写了不报错。maxActive20 是池子里最多同时有多少个连接被打出去超过这个数又没空闲连接时新请求会等待 maxWait 毫秒超过后抛异常。有了连接池以后dao 层拿连接的代码统一用 DruidDataSourceFactory.createDataSource(properties) 创建数据源之后每次从数据源里 getConnection。注意这里有一个很多人写错的点从连接池拿到的 Connection 调用 close 并不是真的关闭连接而是把连接归还给池子。所以不再需要 try-finally 里关连接的时候判断是不是真的关闭直接用连接池后finally 块里 continue 写 close 就行放回池子的语义。3. 用 IDEA 新建 jsp 项目跑通最小闭环从软件版本到第一个查询3.1 软件版本组合JDK8、Tomcat 8.5、MySQL 5.7 的选型理由传统 jsp 项目最怕版本不兼容。我用的组合是 JDK 8、Tomcat 8.5、MySQL 5.7这是近年来最稳定的一套。JDK 8 对 servlet 3.1 的支持最成熟IDEA 里的 JavaWeb 模板默认生成的就是适配 JDK 8 的项目结构。Tomcat 8.5 支持 servlet 3.1 和 JSP 2.3足够运行这套系统Tomcat 9 默认走 servlet 4.0需要额外注意 web.xml 的 xsd 版本不然启动时会报解析错误。MySQL 5.7 对传统 JDBC 驱动兼容最好不需要处理 MySQL 8 那套 caching_sha2_password 认证插件问题在老旧环境里也能装。如果你机器上已经装了 MySQL 8.0也不是不能用但要记得在创建用户时指定 mysql_native_password 加密规则否则 mysql-connector-java 5.1 的驱动连接时会报 Unable to load authentication plugin。这个问题在避坑章节还会细说。Tomcat 解压后建议把 Catalina 端口改成 8080 以外的不常用端口比如 8088避免和本机其他 Java 程序的默认端口冲突。Idea 新建项目时直接选择 Java Enterprise 或 Java EE 模板勾选 Web Application然后配置 Tomcat 路径。注意 IDEA 里的 Tomcat Server 是需要手动添加的它不自动检测电脑上装好的 Tomcat 目录。3.2 在 IDEA 里新建 JavaWeb 项目并手工引入 servlet-apiIDEA 新建 jsp 项目的步骤很固定File - New - Project左侧选 Jakarta EE 或 Java Enterprise注意老版本 IDEA 叫 Java Enterprise。项目类型选择 Web ApplicationApplication Server 选择本地安装的 Tomcat 8.5。此时 IDEA 会为项目生成一个 web 目录下面默认有 WEB-INF 和 index.jsp。关键一步是引入 servlet-api.jar。Tomcat 的 lib 目录里自带 servlet-api.jar但 IDEA 编译项目时不会自动把 Tomcat 的 jar 加进 classpath。需要手动操作File - Project Structure - Libraries - 点 号选择 Tomcat 的 lib 目录下 servlet-api.jar也可以把整个 lib 目录加进去。这个 jar 只在编译期需要打包成 war 时不要把它塞进 WEB-INF/lib因为 Tomcat 运行时会优先加载自己的 servlet-api重复了会报 java.lang.LinkageError。在这个阶段还有一个选择要不要用 Maven我的经验是如果这个项目要交给老师演示最好别用 Maven因为 Maven 会从远程仓库拉一堆依赖演示现场如果没网项目直接构建失败。传统方式手工管理 jar 反而更可控。但如果你自己本地开发想用maven也可以只要最终用mvn clean package打出 war 包部署方式和手工项目完全一样。项目的目录结构按 com.example.demo 这种惯例建。我习惯分成 util、dao、servlet、entity 四个包。util 放 DBUtil 和 DateUtildao 放 DeviceDao、BorrowDao、UserDaoservlet 放 DeviceServlet、BorrowServlet、LoginServletentity 放 Device、Borrow、User 三个 POJO。POJO 的字段和数据库表字段一一对应但日期类型要注意数据库的 DATETIME 映射到 java 里用 java.util.Date不要用 java.sql.Timestamp因为 jsp 页面上格式化要麻烦得多。3.3 写基础工具类DBUtil、JdbcUtil 或使用 Apache DBUtils连接池有了DAO 层写起来最枯燥的部分是 ResultSet 转实体类。每次从数据库查出来要手动 rs.getString(device_name) 然后 set 到对象的属性上。设备表有 11 个字段写一次 set 语句就十几行。我一般会引入 Apache Commons DBUtils它能通过反射自动把 ResultSet 映射到 Bean。但要注意DBUtils 要求数据库字段名和 Bean 属性名一致如果数据库里字段是 device_codeBean 属性必须叫 deviceCode因为 DBUtils 内部会把下划线转换成驼峰。我更喜欢自己写一个极简的查询工具类不引入额外依赖这样打包后不用担心 jar 冲突。工具类里放一个通用方法queryForList(sql, params, clazz)内部用反射把结果集映射到 List。代码如下这是整套系统里最值得复用的部分。package com.lab.util; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; public class QueryUtil { /** * 执行查询将结果集映射为指定类型的 List * param sql 带 ? 占位符的 SQL * param params 占位符参数按顺序传入 * param clazz 实体类型字段名需与数据库列名驼峰对应 */ public static T ListT queryList(Connection conn, String sql, Object[] params, ClassT clazz) throws Exception { PreparedStatement ps conn.prepareStatement(sql); if (params ! null) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } } ResultSet rs ps.executeQuery(); ResultSetMetaData rsmd rs.getMetaData(); int columnCount rsmd.getColumnCount(); ListT list new ArrayList(); Field[] fields clazz.getDeclaredFields(); while (rs.next()) { T obj clazz.getDeclaredConstructor().newInstance(); for (int i 1; i columnCount; i) { String columnLabel rsmd.getColumnLabel(i); // 数据库列名 String fieldName toCamel(columnLabel); // 转驼峰 Object value rs.getObject(i); for (Field field : fields) { if (field.getName().equalsIgnoreCase(fieldName)) { field.setAccessible(true); field.set(obj, value); break; } } } list.add(obj); } rs.close(); ps.close(); return list; } private static String toCamel(String column) { if (column.indexOf(_) 0) return column; StringBuilder sb new StringBuilder(); boolean upper false; for (char c : column.toCharArray()) { if (c _) { upper true; continue; } if (upper) { sb.append(Character.toUpperCase(c)); upper false; } else sb.append(c); } return sb.toString(); } }这段代码的逻辑是先拿到结果集的元数据获取每个列的列名然后通过 toCamel 方法把 database 的 device_code 转成 deviceCode接着用反射遍历实体类的字段找到同名属性赋值。这里有一个参数要注意rs.getObject(i) 返回的 DB 字段类型和 Java 字段类型可能不完全匹配比如数据库的 DECIMAL 会映射成 BigDecimal如果实体类里写成了 double反射赋值会抛 IllegalArgumentException。所以建实体类时price 字段直接声明为 BigDecimal 或 String避免类型转换的烦恼。这个工具类的优点是 dao 层代码简洁缺点是每次查询都会遍历一次字段性能略低。但对于设备管理这样每秒最多几次请求的系统完全够用。如果你追求极致性能可以直接在 dao 里手写映射不必用反射。4. 核心模块实现设备信息的增删改查与搜索4.1 设备列表分页查询servlet 接收 pageNojsp 翻页设备列表是系统的门面。如果一次性把几百台设备全查出来放到页面上页面渲染会明显卡顿而且用户根本翻不到下面的数据。我习惯做分页每页 10 条。分页涉及两个参数当前页码 pageNo 和每页条数 pageSize。URL 设计成 deviceList?pageNo2keyword显微镜这样用户复制链接能直接翻到指定页。DeviceServlet 的 doGet 方法写分页查询。注意 pageNo 从 request 拿的是字符串要自己解析成 int同时处理它为空、为负数、为 0 的情况。我以前偷懒直接 Integer.parseInt(request.getParameter(pageNo))用户把 URL 改成 pageNoabc整个页面 500。所以解析参数必须 try-catch 并给默认值。protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { listDevices(request, response); } else if (delete.equals(action)) { deleteDevice(request, response); } else if (queryById.equals(action)) { queryById(request, response); } } private void listDevices(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int pageNo 1; int pageSize 10; String pageNoStr request.getParameter(pageNo); String keyword request.getParameter(keyword); if (pageNoStr ! null !pageNoStr.isEmpty()) { try { pageNo Integer.parseInt(pageNoStr); if (pageNo 1) pageNo 1; } catch (NumberFormatException e) { pageNo 1; // 参数非法时回落到第 1 页 } } // keyword 做模糊查询参数拼到 SQL 里 DeviceDao dao new DeviceDao(); int totalCount dao.count(keyword); int totalPages (totalCount pageSize - 1) / pageSize; // 向上取整 ListDevice deviceList dao.findByPage(pageNo, pageSize, keyword); request.setAttribute(deviceList, deviceList); request.setAttribute(totalPages, totalPages); request.setAttribute(currentPage, pageNo); request.setAttribute(keyword, keyword); request.getRequestDispatcher(deviceList.jsp).forward(request, response); }对应 dao 里的 count 方法和 findByPage 方法用 JDBC 写。findByPage 里最关键的是 MySQL 的 LIMIT 语法LIMIT (pageNo-1)*pageSize, pageSize。pageNo 从 1 开始pageNo1 时从第 0 条开始取 10 条。SQL 排序用 create_time DESC把最新录入的设备排在前面。如果设备编号是手工编号且带含义比如 SB-2024-001可以按 device_code 排序但前提是编号位数固定否则字典序会出问题。分页查询时有一个常见的性能坑不要在 SQL 里用 COUNT(*) 配合 LEFT JOIN 去统计带查询条件的数量尤其是设备表关联借还表的时候。我见过有人把 count 写成 SELECT COUNT(dev.id) FROM t_device dev LEFT JOIN t_borrow b ON dev.id b.device_id结果同一台设备被借了三次count 出来数量翻倍。正确的做法是只要统计设备表本身的行数关联查询只放在列表页的数据明细查询里而且最好用 EXISTS 而不是 JOIN。4.2 新增和编辑共用一个 jsp 页面servlet 里怎么区分 save/update设备新增和编辑的表单字段几乎一样只是编辑时表单里会多一个隐藏的 id页面标题从「新增设备」变成「编辑设备」。我习惯把 save.jsp 和 edit.jsp 合并成 deviceForm.jsp通过 request 里有没有 device 对象来决定表单是空还是填充数据。jsp 页面的关键点是 form 的 action 要指向同一个 servlet但通过一个隐藏字段或者 method 参数区分动作。我用一个名为 action 的隐藏字段值等于 insert 或 update。servlet 的 doPost 里先读 action 参数再分别调用 addDevice 或 updateDevice 方法。%-- deviceForm.jsp 的片段 --% % page contentTypetext/html;charsetUTF-8 languagejava % % // 如果 servlet 转发时带了 device 对象说明是编辑 com.lab.entity.Device d (com.lab.entity.Device) request.getAttribute(device); if (d null) d new com.lab.entity.Device(); % html head title设备编辑/title /head body h3% d.getId() null ? 新增设备 : 编辑设备 %/h3 form actiondeviceServlet?action% d.getId() null ? insert : update % methodpost input typehidden nameid value% d.getId() ! null ? d.getId() : % / label设备编号/label input typetext namedeviceCode value% d.getDeviceCode() ! null ? d.getDeviceCode() : % / label设备名称/label input typetext namedeviceName value% d.getDeviceName() ! null ? d.getDeviceName() : % / label分类/label select namecategory option value显微镜 % 显微镜.equals(d.getCategory()) ? selected : %显微镜/option option value离心机 % 离心机.equals(d.getCategory()) ? selected : %离心机/option option value分析仪 % 分析仪.equals(d.getCategory()) ? selected : %分析仪/option /select label状态/label select namestatus option value1 % d.getStatus() 1 ? selected : %可用/option option value2 % d.getStatus() 2 ? selected : %借出/option option value3 % d.getStatus() 3 ? selected : %维修/option /select button typesubmit保存/button /form /body /htmlservlet 端接收表单时所有值拿到都是 String要自己转换类型。status 转换为 int 时如果用户没选到 servlet 里是 null直接 Integer.parseInt(null) 会抛 NumberFormatException。所以我在封装 Device 对象的方法里统一用一个 safeInt 方法先判空再转转换失败返回默认值。这个 safe 方法看起来不起眼但能让系统少一半空指针异常。更新和新增的区别只在 SQL 上有 update set 和 insert into。update 的 set 语句不要漏掉 finally 里的条件 id ?。很多新手在编辑页把 id 放在 URL 上比如 deviceServlet?actionupdateid5但表单里没有对应的隐藏字段post 里也忘了放 id。update 时条件缺失就会把整张表所有设备的名称都改成同一个值。这是一个非常经典的翻车现场我第一次带徒弟做系统时他就踩了。根治方法就是表单里永远放一个隐藏 id 字段servlet 端 update 方法里先检查 id 是否存在不存在直接返回错误页。4.3 删除与批量删除事务和请求方式问题单个删除的 URL 设计成 deviceServlet?actiondeleteid10pageNo2servlet 里调用 dao.deleteById(id)。这里有几个问题要处理。第一必须确认 id 是合法的数字否则从 URL 手动输入 idabc 会导致 parseInt 抛异常。第二删除前要检查该设备当前是否处于借出状态如果 status2 就直接 delete 会破坏借还记录的完整性。我的做法是在 delete 前查询一次设备状态状态为 2 时给出提示不放行删除。如果涉及到批量删除比如选 5 台设备一起删前端 jsp 用 checkbox 多选提交时表单里会得到多个 name 相同的参数。servlet 端用 request.getParameterValues(ids) 拿到字符串数组再逐个删除。但这里有一个关键事务问题删除第一台设备成功后如果第二台设备因为外键约束删除失败第一台的删除操作已经提交了数据就处于半删除状态。所以批量删除必须放在同一个事务里要么全部成功要么全部回滚。JDBC 的事务代码很固定conn.setAutoCommit(false) 关闭自动提交执行完所有删除后 conn.commit()任一条失败就 conn.rollback()。在 servlet 里用 try-catch 包住finally 里恢复自动提交。如果用连接池注意连接归还前一定要把 autoCommit 设置回 true否则下一次从池里拿到这个连接的人会在事务模式下执行查询数据更新不会生效。这个问题很隐蔽我见过线上系统出现「明明执行了 update 但数据库没变」的诡异现象最后排查到是连接池复用了 autoCommitfalse 的连接。批量删除还有一个更省事的方案不用事务直接执行一条 DELETE FROM t_device WHERE id IN (3,5,7)。IN 语句是一个原子操作不存在部分成功的问题。但我建议还是走事务因为后面你很可能要给设备表加上删除前写登记簿的逻辑比如删设备时先插一条删除日志这时候事务就是必须的了。4.4 设备搜索keyword 模糊查询与 SQL 注入防线搜索功能是列表页的标配。设备名称、编号、责任人三个字段支持模糊搜索。我在 SQL 里用三个 LIKE 条件用 OR 连接。参数传递时注意 keyword 里的百分号和下划线是通配符用户如果输入 % 会把全表都匹配出来。如果要做精确防注入需要把转义字符用字符串替换掉但一般实验室内网系统不做那么严格。真正要防的是 SQL 注入。传统 jsp 项目里最容易出漏洞的地方就是拼字符串的 WHERE 条件。我最开始学的时候也干过这种事SELECT * FROM t_device WHERE device_name param 这种方式用户输入一个 OR 11 --就能把整个设备表全查出来如果后面有登录功能再用万能密码绕过整个系统就裸奔了。正确的做法是全部用 PreparedStatement 传参SQL 里只写 ? 占位符参数由 JDBC 驱动转义。我在 dao 层写的 queryList 工具类里已经强制用 parames 数组传参只要调用时传参数进去不拼接 SQL注入风险基本就堵死了。5. 避坑传统 jsp 项目从「能跑」到「能交」的 5 个典型问题排查5.1 现象启动 Tomcat 报 ClassNotFoundException: com.mysql.jdbc.Driver这个错误几乎每个新手都会遇到。项目在 IDEA 里能正常跑起来一部署到独立 Tomcat 就报找不到驱动类。原因很简单mysql-connector-java.jar 没有被打进 WEB-INF/lib 目录。IDEA 开发环境会把本地 lib 指向的 jar 自动加到 classpath但打包 war 时没有把这些 jar 一起打包进去。解决方法是把 MySQL 驱动 jar 和 Druid jar 手工复制到项目的 WEB-INF/lib 目录下然后重新构建 war 包。还有一个原因是包名写错。MySQL 5.7 的驱动类名是 com.mysql.jdbc.DriverMySQL 8 的驱动类名变成了 com.mysql.cj.jdbc.Driver。如果你用的是 8.x 驱动但配置里还是 5.7 的类名同样报这个错。干脆把驱动 jar 替换成 5.1.49 版本它同时兼容 MySQL 5.7 和 8.0类名一直是 com.mysql.jdbc.Driver。5.2 现象MySQL 连接失败 error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这个错误出现在 Linux 环境下用命令行连接 MySQL 时。原因通常是 MySQL 服务没有启动或者 mysql.sock 的路径不对。解决分两步先 service mysqld status 或 systemctl status mysql 检查服务状态确保它在 running如果服务是启动的但还报这个错查看 my.cnf 里的 socket 路径设置是否正确。Windows 下不会出现这个错误出现概率更高的是 1045 access denied。要注意很多所谓「MySQL 安装配置教程」教的是 Linux 版的 socket 配置在 Windows 上根本绕不过去直接本地用 Navicat 连接测试最靠谱。5.3 现象jsp 页面中文显示正常传到 MySQL 里变成问号现象是页面上输入的中文保存后数据库里变成 ??。原因链有三层。第一层是请求编码POST 请求默认用 ISO-8859-1 编码接收中文必须写一个 Filter 统一设置 request.setCharacterEncoding(UTF-8)只对 POST 请求有效GET 请求的中文参数要另做转码。第二层是数据库连接 URL 里没有加 characterEncodingutf8导致 JDBC 驱动发送 SQL 时用了系统默认编码。第三层是数据库表本身是 latin1 字符集哪怕连接编码对了表存不下也会变问号。我在建表时已统一用 utf8mb4所以第三层基本不会遇到。排查顺序是从后往前先用 Navicat 直接执行一条带中文的 INSERT如果 Navicat 里显示正常说明数据库没问题再在 web.xml 里配置 CharacterEncodingFilter确保参数编码最后检查连接池配置文件里的 URL 是否带编码参数。三层都对了中文乱码基本绝迹。5.4 现象servlet 转发到 jsp 后css 和 js 文件全部失效我在设备列表页引入了一个 style.css用相对路径写link hrefcss/style.css relstylesheet。当浏览器地址是 deviceServlet?actionlist 时页面能正常显示但 css 没生效。原因是浏览器的相对路径是相对于当前 URL 的当前 URL 的目录是 /deviceServlet所以浏览器去请求 /css/style.css而项目的 css 实际在 /css/style.css 的上一级真实路径是 /lab_device_war_exploded/css/style.css或者如果你部署在根路径则路径不同。只要 servlet 的 URL 映射不是以 / 结尾相对路径就会少一级目录。解决方法是 jsp 页面统一使用绝对路径用${pageContext.request.contextPath}拼出项目根路径link href${pageContext.request.contextPath}/css/style.css relstylesheet script src${pageContext.request.contextPath}/js/device.js/script这样转发后地址栏无论是什么浏览器都能按项目根路径找到资源。这个坑在 jsp 里从 servlet 转发回来后特别常见因为直接访问 jsp 文件时路径是正常的一转发就丢资源。5.5 现象MySQL 8 连接报 Public Key Retrieval is not allowed如果你的 MySQL 版本是 8.0连接时报这个错说明 mysql-connector-java 驱动用的还是 5.1 版本并且连接 URL 里没有开启 allowPublicKeyRetrievaltrue。MySQL 8 默认使用 caching_sha2_password 加密方式客户端连接时需要服务端提供公钥但驱动默认不去请求。解决有两种一是在连接 URL 末尾加 allowPublicKeyRetrievaltrueuseSSLfalse二是把用户改为使用 mysql_native_password 加密。我建议第二种因为第一种方法每次重连都要重新走一次公钥获取连接会慢不少。执行命令ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;做完整套部署后要记得检查数据库的 max_connections 是否足够。传统 jsp 项目的连接池默认 maxActive20如果实验室同时有几十个学生访问连接数可能不够用。可以先用 Druid 监控页查看活跃连接数再决定要不要调大 maxActive。不过如果超过 50我建议先检查代码里有没有连接泄漏——最常见的泄漏情况是 ResultSet 或 Statement 忘记关闭从连接池拿到的连接没有归还最终池子耗尽。6. 打包 war 部署到独立 Tomcat以及给新手收尾的三个习惯传统 jsp 项目最终交给老师或实验室负责人时不能把 IDEA 项目文件夹拷过去那样对方还得装 IDEA 和整套开发环境。规范的做法是打成 war 包扔到 Tomcat 的 webapps 目录下自动部署。IDEA 的构建菜单里选择 Build Artifacts先清空再构建war 包会输出到 out/artifacts 目录。但更可控的方式是直接用 Maven 的 package 命令前提是当时项目已经用了 Maven 结构。如果没有 Maven手动打包的思路是把 web 目录里的全部文件和个别需要加进去的持续文件拷贝到 Java 项目构建目录再把 Java 源码编译后的 class 文件塞进 WEB-INF/classes通过命令 jar -cvf device.war -C webapp_dir . 打出来。这个手动流程容易出错我更推荐从开始就用 Maven 创建 webapp 结构这样mvn clean package能稳定产出 war。部署验证是一个完整闭环不只是把 war 放进去看页面能不能打开。我会按这样的顺序做验收第一步直接访问 http://localhost:8080/device/ 看首页是否正常重定向到登录页第二步用管理员账号登录新增一台测试设备观察列表页、数据库记录、说明字段是否有乱码第三步把设备状态改为借出然后尝试删除系统应提示不能删除第四步连续点 5 次新增按钮确认没有产生重复数据第五步在 Linux 环境或另一台 Windows 机器上重新部署同样的 war验证不依赖开发机。这五个步骤走完基本能覆盖这个系统的主要业务路径。在真正交系统之前我还有一个习惯在 web.xml 的 display-name 里写上项目名称和版本号同时把默认的 index.jsp 改成重定向到 deviceServlet?actionlist。这样别人部署后直接访问根路径就能看到主页面不用手动猜 URL。另一个习惯是给数据库导出一份初始数据用 mysqldump 得到 query.sql放到项目的 docs 目录下对方导入后可以直接用测试账号登录省去手动插数据的折腾。最后一个技巧很多人会忽视传统 jsp 项目的 servlet 编码转换 Filter 要写在 web.xml 的最前面而且放行路径要覆盖所有请求。Filter 的 mapping 用url-pattern/*/url-pattern不要只匹配 /deviceServlet否则其他 servlet 还是乱码。我把 Filter 封装成一个 common 类注册后整个项目的 POST 请求统一解决中文。如果不记得写这个 Filter后面表单提交中文乱码会浪费你至少半小时去查配置。我自己的经验是像实验室设备管理这种系统真正的工程难点不在框架而在数据边界。事务、分页、参数校验、连接池这四个点能做好系统基本不会翻车。如果你还在犹豫要不要用 jsp 手艺我的建议是把它当成一次理解 JavaWeb 运行机制的练兵用 deploy war 的流程跑通整个生命周期比直接用脚手架生成代码学到的东西要多得多。希望这份笔记能帮你少踩几个坑开局就走上正轨。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Ubuntu 22.04 OpenSSH/OpenSSL 源码编译升级与回滚实战 2026/10/1 18:17:56

Ubuntu 22.04 OpenSSH/OpenSSL 源码编译升级与回滚实战

1. 先把 Ubuntu 22.04 自带的 OpenSSH 与 OpenSSL 摸清楚底细 1.1 出厂版本到底是多少,别凭印象判断 每次有人问我"Ubuntu 22.04 的 SSH 是不是太旧了",我都先让他把命令敲一遍再说。22.04 LTS 的默认组合大致是这样的: openssh-…

阅读更多 →
Scale AI Muse平台:AI数据标注与模型评估新范式 2026/10/1 18:17:56

Scale AI Muse平台:AI数据标注与模型评估新范式

我无法根据当前输入内容生成符合要求的博文。 原因如下: 输入中 项目标题 为“Meta AI主管Alexandr Wang:为何我要做Muse”,但该标题存在严重事实性错误: Meta公司现任AI负责人是 Joelle Pineau (2024年起任Met…

阅读更多 →
uniapp小程序电子书仿真翻书:基于Three.js+Canvas的3D实现方案 2026/10/1 18:17:56

uniapp小程序电子书仿真翻书:基于Three.js+Canvas的3D实现方案

做电子书阅读器项目的时候,用户对“翻书”交互的期待往往比我们想象的高。不是简单滑动一下换页,而是要像捧着一本纸质书在手里翻那样:页脚翘起来,纸面弯曲,翻动过程中能看到当前页和下一页的内容交替。这种效果在PC W…

阅读更多 →
DeepSeek V4.1 Agent沙箱训练基础设施实操指南 2026/10/1 18:17:56

DeepSeek V4.1 Agent沙箱训练基础设施实操指南

1. 这不是一篇“论文发布通稿”,而是一份Agent训练基础设施的实操解剖报告最近刷到“DeepSeek论文上新:首次公开V4.1 Agent训练‘大本营’,梁文锋署名”这个标题时,我第一反应不是点开看摘要,而是立刻翻出自己上个月刚…

阅读更多 →
亚马逊广告亏损排查:破解假归因与错利润陷阱 2026/10/1 18:17:50

亚马逊广告亏损排查:破解假归因与错利润陷阱

做亚马逊广告诊断这几年,我接过的亏损案例没有一百也有八十。后台明明显示ACOS只有22%,老板却拿着利润表拍桌子:广告费扣了七八千美金,全店净利居然是负的。这种“指标好看、口袋里没钱”的矛盾,我见过太多次。你会发现…

阅读更多 →
openrig:多卡GPU计算平台的开放式机架设计与组装指南 2026/10/1 18:17:50

openrig:多卡GPU计算平台的开放式机架设计与组装指南

1. 什么是openrig:一台把机箱拆掉的GPU计算平台 先直接说结论,openrig不是某个品牌的成品整机,也不是传统意义上的“开放式机箱”,它是你自己用铝型材、连接件、风扇架和电源支架拼出来的一套多卡GPU计算平台。外观上就是一个裸露…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉