简介:这是一套基于原生Servlet与JDBC实现的设备维修管理系统JavaWeb源码,适用于计算机、电子信息等专业课程设计、期末大作业或毕设参考。项目围绕设备报修、维修记录、零件与客户管理等典型业务展开,包含项目说明文档和可直接部署运行的完整工程,可帮助读者理解JavaWeb后端请求处理、数据库交互及分层开发思路。
压缩包共248个文件,核心为54个Java源文件与对应class文件,涵盖Dao、Servlet等层次;同时包含50个JS脚本、17个CSS样式及图片资源用于前端界面,另有SQL/DB文件便于初始化数据库。整包仅2.84MB,结构清晰,便于快速下载与本地搭建。
资源已有346人学习下载。除源码外还附带项目说明,特别适合具备一定Java基础、希望参考真实业务场景完成课设或深入钻研Servlet+JDBC机制的开发者。
1. 设备维修管理系统源码+项目说明:为什么原生 Servlet + JDBC 这套"老配方"还值得你从头敲一遍
"设备维修管理系统源码+项目说明(JavaWeb 项目,使用原生 servlet 和 JDBC).zip"——这套东西在很多人眼里是毕业设计急救包,我更愿意把它当成一次完整的 JavaWeb 技术体检。项目说小也小,不过是设备台账、维修工单、用户登录几张表来回查;说大也大,原生 Servlet 的路由分发、JDBC 手写 SQL、Session 认证、分页查询全串在了一起,比照着 Spring Boot 的黑盒要清楚得多。适合两类人:正在做 JavaWeb 课程设计、需要一份讲得清原理的参考;平时用框架写惯了、想回头补基础链路开发的。按"功能拆解 → 数据库设计 → 核心代码 → 排错 → 改造"这条线,往下讲。
2. 这套设备维修管理系统到底做了什么:功能边界、技术选型与三层架构的对应关系
拿到源码先别急着部署,第一步是搞清楚这系统管到哪、不管到哪。设备维修和点检、保养是两套业务,很多教学项目容易把范围扯大,页面做了一堆,逻辑全是重复的。这套系统的核心就一件事:设备坏了以后,报修工单怎么登记、怎么流转、怎么闭环。
2.1 功能清单与角色权限:维修单流转的五个核心场景
设备维修管理系统里,"设备"一般指企业固定资产,比如机床、叉车、打印机;"维修管理"就是记录"这台设备坏了 → 谁报修 → 谁受理 → 修没修好 → 换了什么配件"这一串事件。常见的教学版功能边界,我用一张表给你捋清楚:
| 功能模块 | 角色 | 对应表 | 关键动作 |
|---|---|---|---|
| 登录认证 | 管理员 / 维修工 / 报修人 | user 表 | 用户名密码校验,登录态写入 Session |
| 设备台账 | 管理员 / 报修人 | device 表 | 设备新增、编辑、删除、分页列表 |
| 报修登记 | 报修人 | repair 表 | 选择设备、填故障描述、设定紧急程度 |
| 派工与处理 | 管理员 / 维修工 | repair 表 | 指派接单人、修改维修状态、填处理结果 |
| 统计看板(可选) | 管理员 | 聚合查询 | 本月维修数、按状态分组统计 |
上面这张表是这套 JavaWeb 教学案例最常见的功能切分。手上源码如果缺了"统计看板",不用慌,它本来就是加分项不是及格项——答辩时能把前四个功能讲透,加上事务处理得当,分数不会差。我见过不少项目把"设备点检"和"设备维修"混在一起做,表里既有计划表又有工单表,页面数量翻倍,但实际上点检强调"按时巡检、提前发现隐患",维修强调"坏了走流程",两个需求放一起只会让数据库设计变得臃肿。翻源码时先看 SQL 脚本,如果只有维修相关的表,说明范围控制得干净。
2.2 原生 Servlet 和 JDBC 在这套系统里各管哪一段
很多刚接触 JavaWeb 的同学以为 JavaWeb 就等于 JSP,这是第一个误区。JSP 是视图层技术,真正的控制中枢是 Servlet。在这套设备维修管理系统里,一次完整的请求链路是这样的:
浏览器发起请求 → Tomcat 按 web.xml 里的映射规则找到对应 Servlet → Servlet 调用 Service 层方法 → Service 调用 DAO 层的 JDBC 代码 → 拿到 ResultSet 后封装成实体对象 → 塞进 request 或 Session → 转发到 JSP 渲染页面。
JDBC 在这里只干一件事:用 Java 连上 MySQL,执行 SQL,把结果一行行读出来。原生 JDBC 最烦人的地方是样板代码太多,一个查询往往要写六七行固定套路(加载驱动、获取连接、创建语句、执行、处理结果集、关闭资源),这也是后来 MyBatis 能流行的根本原因。但反过来想,正是这六七行样板代码,让你真正想明白"数据库连接从哪来、用完为什么要还回去、连接池到底解决了什么问题"。
要是你拿到源码后的第一反应是"怎么没有 application.yml",说明你习惯的是 Spring Boot 的思路。原生 Servlet + JDBC 项目里,配置是分散的:数据库连接信息写在 JDBCUtil 的静态代码块里,Servlet 映射写在 web.xml 里,编码过滤器是一个 Filter 类再在 web.xml 注册。这份"分散"本身就是教学价值,跟着黑马 JavaWeb 笔记敲过一遍的同学,对这个结构应该有印象。
2.3 源码+项目说明的目录结构:从解压到看懂包名
拿到 zip 解压后,按这套项目的常见编排方式,目录结构长这样:
equipment-repair/ ├── src/ │ ├── com.xxx.entity // 实体类:User, Device, Repair │ ├── com.xxx.dao // 数据访问层:接口 │ ├── com.xxx.dao.impl // 数据访问层实现 │ ├── com.xxx.service // 业务层:接口 │ ├── com.xxx.service.impl // 业务层实现 │ ├── com.xxx.servlet // 所有 HttpServlet 子类 │ ├── com.xxx.filter // 登录校验、编码过滤器 │ ├── com.xxx.util // DBUtil、DateUtil 等工具类 │ └── jdbc.properties // 数据库连接配置 ├── WebContent/ 或 web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ // mysql-connector-java.jar 等 │ ├── static/ // css、js、images │ ├── login.jsp │ ├── device_list.jsp │ └── repair_list.jsp ├── sql/ │ └── equipment_repair.sql // 建库建表和初始化数据 └── 项目说明.docx 或 README.md一个判断源码质量的技巧:看 DAO 是不是"接口 + 实现类"两层。教学项目为了展示三层架构,往往会把接口和实现分开;但不少 Demo 图省事,只有一个 DAO 类。后者不是错,只是扩展起来耦合重。拿到源码先找 util 包下的 DBUtil,这段代码决定你能不能在一分钟之内把数据库连上。项目说明文档一般会写明 JDK、Tomcat、MySQL 的版本要求和导入步骤,如果跟你本机对不上,优先选 JDK 8 + Tomcat 8.5/9 这套组合,这是原生 Servlet 项目测试覆盖最广的环境。JDK 17 往上跑老项目不是不行,但会遇到模块化导致的各种反射告警,没有折腾的必要。
3. 数据库是这套系统跑起来的地基:设备表、维修单表、用户表的字段设计与初始化 SQL
很多同学拿着源码第一件事是配 Tomcat,我却建议先看 SQL 脚本。设备维修管理系统的核心逻辑全在表结构里,表设计对了,Servlet 代码就是机械劳动;表设计错了,后面每一层都在打补丁。
3.1 设备台账表与维修单表:一对多关系里的三个关键字段
设备表是整个系统的源头,维修单表围绕它做挂载。常见的字段设计是:
CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '设备ID', device_no VARCHAR(32) NOT NULL UNIQUE COMMENT '设备编号,如 SB-2024-001', device_name VARCHAR(64) NOT NULL COMMENT '设备名称', model VARCHAR(64) COMMENT '规格型号', location VARCHAR(128) COMMENT '安装位置/使用车间', purchase_date DATE COMMENT '购置日期', status TINYINT DEFAULT 1 COMMENT '1 正常 2 待维修 3 报废', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备台账表';三个关键字段值得多说几句。device_no 设成 UNIQUE,是为了避免两条一模一样的设备编号在系统里打架,真实企业里设备编号是固定资产标牌上的号码,必须唯一。status 字段别用 VARCHAR 存"正常""待维修",用 TINYINT 存数字,页面展示时再映射成中文,既省空间又方便条件查询。create_time 建议交给数据库默认值,而不是在 Java 代码里 new Date() 塞进去,数据库时钟比应用服务器时间更可靠。
维修单表是重头戏,设计时注意外键的落法:
CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL COMMENT '所属设备,外键', report_user_id INT NOT NULL COMMENT '报修人,来自 user 表', fault_desc VARCHAR(500) NOT NULL COMMENT '故障现象描述', level TINYINT DEFAULT 2 COMMENT '紧急程度 1紧急 2普通 3低', status TINYINT DEFAULT 1 COMMENT '1待受理 2维修中 3已完成 4已关闭', assign_user_id INT COMMENT '接单维修工', repair_result VARCHAR(500) COMMENT '维修结果/更换配件说明', report_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报修时间', finish_time DATETIME COMMENT '完成时间', CONSTRAINT fk_repair_device FOREIGN KEY (device_id) REFERENCES device(id), CONSTRAINT fk_repair_report_user FOREIGN KEY (report_user_id) REFERENCES user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修工单表';注意这里没有 repair_cost 字段。很多教学版源码会加一列维修费用,但真实场景里费用由财务结算,不属于维修工单;课程设计加一列也无妨,但如果页面上没有录入和统计费用,这列就是个半截功能,答辩时容易被问住。宁可不要,也别做半截。另外,字段命名建议统一用下划线风格,和 Java 实体类里的驼峰属性做映射时,用 mapUnderscoreToCamelCase 或手动 rs.getXxx() 都很顺手。
3.2 用户表与角色字段:登录态是怎么在 JDBC 查询里落地的
用户表结构简单,但角色字段的设计直接决定权限拦截的复杂度:
CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '明文或MD5后的密文', real_name VARCHAR(32) COMMENT '姓名', role TINYINT DEFAULT 3 COMMENT '1 管理员 2 维修工 3 普通报修人', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';教学项目的登录密码基本都是 MD5 加密存的,这点需要和答辩老师坦诚:MD5 现在已经被认为不适合做密码哈希了,碰撞攻击成熟,正确做法是 BCrypt 加盐。如果整套源码用的是 MD5,你只要在项目说明里注明"教学演示用,生产环境请替换为 BCrypt",这个缺陷反而成为你懂行的加分项。
登录态在主流程里的 JDBC 查询长这样:
public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, real_name, role FROM user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getInt("role")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }代码说明:这里用 ? 占位符配合 PreparedStatement,而不是把用户名密码拼进 SQL 字符串,这是防 SQL 注入的最低底线,也是 JavaWeb 课程设计的考核点之一。参数说明:setString 的索引从 1 开始,和 SQL 里 ? 的先后顺序一一对应,顺序写反是刚上手 JDBC 时最经典的问题,报错信息往往还是模糊的"Parameter index out of range"。
登录成功后把 user 对象放进 Session 是最关键的一步:
request.getSession().setAttribute("loginUser", user);之后每个受保护页面都能从 Session 里取 role 做角色判断。三层角色用 TINYINT 1/2/3 表示,比用字符串到处 equals 干净,也方便扩展新角色。遇到权限拦截的需求,在 Filter 里读 Session 比在每个 Servlet 里重复判断要优雅得多。
3.3 初始化 SQL 怎么写:测试数据量与常用查询
拿到项目的 sql 文件后,先别急着执行,看三样东西:字符集、存储引擎、数据量。字符集必须 utf8mb4,不能是 latin1,否则中文全变问号;存储引擎统一 InnoDB,外键才能生效;数据量的话,设备表至少 20~30 条,维修单至少 50 条,数据太少分页效果根本看不出来,页面翻两页就到底,"分页查询"功能的演示效果大打折扣。
初始化数据建议用批量 INSERT 或存储过程生成,比如:
-- 生成 30 条设备数据(MySQL 8 的递归 CTE 写法) INSERT INTO device (device_no, device_name, model, location, status) WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 30 ) SELECT CONCAT('SB-2024-', LPAD(n, 3, '0')), CONCAT('设备', n), CONCAT('型号-', n % 5), CONCAT('车间', n % 3 + 1), 1 + (n % 3) FROM seq;生成后的数据应该能支撑一个目标明确的分页查询:按状态、按车间、按设备名称模糊搜索,一页 10 条正好三页。如果 SQL 脚本里只塞了三条数据,说明作者自己都没拿它认真演示过,遇到答辩追问会露怯。
4. 核心代码走读:从 LoginServlet 到 RepairServlet,原生 JDBC 的完整调用链
这章是整套系统的骨架。Servlet 负责接收请求,JDBC 负责和数据库对话,中间夹着 Service 层组织业务规则。看懂这条调用链,后面所有框架都是它的包装。
4.1 登录与 Session 校验:Filter 里的三个判断条件
Servlet 编程最朴素的路径是:每个功能写一个 Servlet 类,重写 doGet/doPost,在 web.xml 或注解里注册路径。登录功能的完整代码大致长这样:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserServiceImpl(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); if (user.getRole() == 1) { resp.sendRedirect(req.getContextPath() + "/admin/index.jsp"); } else { resp.sendRedirect(req.getContextPath() + "/device/list"); } } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }代码说明:@WebServlet 注解是 Servlet 3.0 提供的,Tomcat 7 起支持,能少写一行 web.xml 配置。参数说明:req.getParameter 拿的是表单里 name 属性对应的值,如果 JSP 里 input 的 name 是"account",代码里却取"username",拿回来就是 null,这种低级错误靠打印日志才能发现。
登录校验 Filter 是整套系统的安全底线,三个判断条件一个都不能省:
@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 条件1: 放行登录页、静态资源和登录接口 if (uri.endsWith("/login.jsp") || uri.contains("/static/") || uri.endsWith("/login") || uri.endsWith("/register")) { chain.doFilter(request, response); return; } // 条件2: Session 里有用户就放行 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(request, response); return; } // 条件3: 都不满足,跳回登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); } }getSession(false) 和 getSession() 的区别值得记牢:不带参数的方法会强制创建一个 Session,带 false 则没有就返回 null。在 Filter 里必须用 false,否则每个匿名请求都白白多出一个 Session,服务器内存里塞满垃圾,这是很多 JavaWeb 项目内存飙高的隐形原因。
4.2 设备列表与分页:用原生 JDBC 拼 LIMIT 的两种写法
分页查询是这套系统最能体现原生 JDBC 功力的地方。页面需要两个东西:当前页的数据列表、总记录数。于是 DAO 里通常有两个方法,一个查列表,一个 count 总数:
public List<Device> findByPage(String keyword, Integer status, int pageNo, int pageSize) { StringBuilder sql = new StringBuilder( "SELECT id, device_no, device_name, model, location, status " + "FROM device WHERE 1=1"); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append(" AND (device_name LIKE ? OR device_no LIKE ?)"); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } if (status != null && status != 0) { sql.append(" AND status = ?"); params.add(status); } sql.append(" ORDER BY id DESC LIMIT ?, ?"); params.add((pageNo - 1) * pageSize); params.add(pageSize); // 已省略: 获取连接、循环 setObject、执行查询、封装 List }代码说明:动态 SQL 用 StringBuilder 拼,是原生 JDBC 项目最常见的写法。WHERE 1=1 不是偷懒,是为了让后面每个 AND 不用判断"是不是第一个条件",老派但有效。参数说明:LIMIT 的两个参数,第一个是偏移量 (pageNo-1)*pageSize,第二个是每页条数;params 列表的顺序必须和 SQL 里 ? 的出现顺序完全一致,keyword 为空时 status 参数就顶上,顺序乱掉数据全错。
count 总数的方法和上面几乎一样,只是把 SELECT 字段换成 COUNT(*),去掉 ORDER BY 和 LIMIT。Service 层拿到总数后算出总页数:totalPage = (totalCount + pageSize - 1) / pageSize,这个上取整公式避免了整除丢页的问题。
分页还有一种写法是直接在 JSP 里用 JSTL 的 c:forEach 循环渲染表格,页面上"上一页/下一页"两个按钮的链接就是 list?pageNo=当前页-1&keyword=xxx。这里有个最容易翻车的细节:keyword 必须手动跟着翻页链接走,不然翻到第二页搜索条件就丢了。部分源码会用隐藏域保存查询条件,逻辑等价,但更容易被忽略的是 status 这个下拉框的选中状态也要回显。
4.3 维修单的提交与状态流转:事务应该加在哪一层
设备维修最核心的流程是"报修人提单 → 管理员派工 → 维修工处理 → 关闭单据"。每一步都涉及 update 语句,而"维修完成"这一步涉及两张表的联动:device 状态要从"待维修"改回"正常",repair 表的状态要改成"已完成"。
这两条 update 必须在一个数据库事务里:要么都成功,要么都失败。原生 JDBC 的事务控制代码是很多人从来没写对过的:
public boolean finishRepair(int repairId, int deviceId, String result) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,事务从这里开始 String sql1 = "UPDATE repair SET status=3, repair_result=?, finish_time=NOW() WHERE id=?"; String sql2 = "UPDATE device SET status=1 WHERE id=?"; try (PreparedStatement ps1 = conn.prepareStatement(sql1); PreparedStatement ps2 = conn.prepareStatement(sql2)) { ps1.setString(1, result); ps1.setInt(2, repairId); ps1.executeUpdate(); ps2.setInt(1, deviceId); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }事务必须放在 Service 层而不是 DAO 层。原因很直接:DAO 方法各自拿着自己的 Connection 做单条 SQL,如果把事务写在 DAO 里,两个 DAO 方法各提交各的,无法形成原子操作。上面这段代码把连接从 DBUtil 拿出来后,手动控制 setAutoCommit(false)、commit、rollback,最后在 finally 里恢复自动提交并关闭连接,这才是完整的收尾。
这里有个教学项目典型的坑:用 try-with-resources 把 Connection 也包进去,然后在 conn.commit() 之前连接已经被关闭,事务根本没机会提交。正确的做法是 Connection 用传统 try-catch-finally 管理,PreparedStatement 和 ResultSet 才能放心交给 try-with-resources。没有连接池的原生 JDBC 在这里也会暴露问题:每次执行业务都要新建物理连接,性能差是其次,连接一多 MySQL 的 max_connections 就顶不住了,这个我们到排错部分细说。
5. 跑通与排错:设备维修管理系统在 Tomcat + MySQL 下的避坑记录
下面这几条全是这套 JavaWeb 项目最常见的翻车现场,每一条我都按"现象 → 原因 → 解决"来讲。你在 IDEA 里跑这套系统时遇到任何一个,直接按对应条目处理。
5.1 现象:启动 Tomcat 时控制台报 ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:mysql-connector-java.jar 没有放到 WEB-INF/lib 目录下。很多初学者把 jar 拖到 IDE 的 Libraries 里,编译能过,但 Tomcat 运行时用的是 WEB-INF/lib 下的 jar,编译期和运行期类路径是两回事。
解决:把 mysql-connector-java-x.x.x.jar 复制到 WebContent/WEB-INF/lib(Maven 结构则是 src/main/webapp/WEB-INF/lib)下,右键 Add as Library,重启 Tomcat。判断是不是这个问题,最快的方法是去 Tomcat 实际部署目录看一眼有没有这个 jar。
另一个变体是驱动类名写错。老版本驱动类是 com.mysql.jdbc.Driver,MySQL Connector/J 8.x 换成了 com.mysql.cj.jdbc.Driver。如果项目说明里的连接配置是 5.x 写法、你本地装的是 8.x 驱动,ClassNotFoundException 照样来,把驱动类名换成 8.x 即可。
5.2 现象:数据库连接失败,控制台报 Communications link failure 或 Access denied
Access denied 是账号密码或权限问题。到 MySQL 里执行 SELECT user, host FROM mysql.user,确认连接时用的用户和 host 是否匹配;如果用户名密码都对但依然拒绝,大概率是 '%' 和 'localhost' 的匹配规则问题,新建一个 host 为 '%' 的账号,或者把连接串改成 127.0.0.1 再试。
Communications link failure 集中在三处:MySQL 服务没启动、连接串里 IP 或端口不对、SSL 或时区参数导致握手失败。在 JDBC URL 后面加上这几个参数能解决八成问题:
jdbc:mysql://localhost:3306/equipment_repair?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8useSSL=false 关掉 SSL 握手,恢复老驱动的行为;serverTimezone=Asia/Shanghai 解决 8.x 驱动要求显式指定时区的问题;characterEncoding=utf8 保证中文不乱。注意这里有个容易混淆的点:mysql jdbc useSSL 与 sslMode 实际上是同一种配置在不同驱动版本里的演进,MySQL 8.x 驱动有时需要写 sslMode=DISABLED 才能彻底关掉 SSL 处理,具体看异常栈里提示的是哪个参数。顺带一提,不同数据库驱动的 URL 参数并不通用,比如 PostgreSQL 驱动里的 targetServerType 是控制主备路由的,别把那一套配置照搬到 MySQL 项目里。
5.3 现象:页面上的中文全是问号,或者写入数据库后再查出来全是问号
原因是一整条编码链路断了。Tomcat 请求端、响应端、JSP 页面、MySQL 表结构、连接串,五处至少要统一成 UTF-8。按这个顺序排查:
第一步,JSP 页面头部的 pageEncoding 和 contentType 要同时写 UTF-8;第二步,POST 请求在 Servlet 里先调用 req.setCharacterEncoding("UTF-8"),GET 请求需要在 Tomcat 的 server.xml 里给 Connector 加 URIEncoding="UTF-8";第三步,连接串里 characterEncoding=utf8 不能省;第四步,建表语句的 CHARSET 必须显式写 utf8mb4,而不是依赖 MySQL 默认值。
第五步是最容易忽略的:MySQL 服务器本身的默认字符集。命令行执行 SHOW VARIABLES LIKE 'character_set_server';,如果返回 latin1,哪怕建表时指定了 utf8mb4,部分导入工具在导入过程中还是会乱。改配置文件 my.cnf 的 character-set-server=utf8mb4,重启 MySQL 服务,一劳永逸。
5.4 现象:页面能打开,但点几次查询后越来越慢,最后报 Too many connections
这是典型的 JDBC 资源泄漏。查询方法里的 Connection、PreparedStatement、ResultSet 任何一个没关闭,都会把数据库连接占住。教学项目里最常见的写法是只关了 ResultSet 忘了关 Connection,或者 Connection 在 return 之后就再也没人管了。
解决思路是把三个资源全部放进 try-with-resources:
try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { // 业务处理 }注意 try-with-resources 的关闭顺序和声明顺序是反的,但代码不关心这个问题,三个资源都会被自动关闭。修复后可以用 SHOW PROCESSLIST 查看当前连接数,如果还有大量 Sleep 状态连接挂着,那可能是连接池或事务没提交的问题,和资源泄漏不是一回事,别混在一起排查。
还有一个隐蔽的坑:Connection 放进 try-with-resources 之后,你在里面手动 setAutoCommit(false) 并且没 commit 就退出,资源虽然关闭了,但事务被隐式回滚,数据没写进去,页面上还没有任何报错。事务和连接池的配合,我建议按第 4 章那段代码来写,Connection 不要进 try-with-resources。
5.5 现象:JSP 改了内容,刷新页面还是老样子,像是有缓存
原因:Tomcat 会把 JSP 编译成 Java 类缓存在 work 目录里。JSP 文件本身没有编译错误时,Tomcat 会复用旧的 class,不重新编译。
解决:在 IDEA 里 Build → Rebuild Project,或者手动删除 Tomcat 的 work/Catalina 目录后重启。开发阶段更彻底的办法是在 web.xml 里把 JSP 的 development 参数设为 true,让 Tomcat 每次检测到 JSP 修改就重新编译。
这类问题在教材里常被归为"玄学",其实就是编译缓存没清理。另一个类似的坑是浏览器缓存:静态资源 css/js 被浏览器强缓存,刷新看不到新样式,开发时按 Ctrl+F5 强制刷新,或者给静态资源的引用加上 ?v=2 版本参数,屡试不爽。
6. 进阶改造:把这套源码升级到"能讲出价值"的四个优先项
设备维修管理系统作为 JavaWeb 课程设计,能跑通只是及格线;答辩时讲出"我做了哪些改进"才是拉开差距的地方。按性价比排序,推荐按这张表动刀:
| 优先级 | 改造项 | 改动量 | 答辩价值 |
|---|---|---|---|
| P0 | 用 Druid 连接池替换 DBUtil 里的 DriverManager.getConnection | 改 DBUtil 一处 | 讲清楚连接复用与资源控制 |
| P0 | 密码从 MD5 换成 BCrypt | 改登录、用户新增两处 | 讲清楚密码存储的演进 |
| P1 | 把多个 Servlet 改造成一个 BaseServlet 按方法分发 | 改一个父类加子类 | 体现代码设计能力 |
| P1 | 给 repair 表加索引优化查询 | 改 SQL 脚本 | 建立性能意识 |
| P2 | 把 JSP 里混写的 Java 代码抽成 EL 和 JSTL | 改动页面较多 | 体现视图层规范 |
其中 P0 两项改动量小、说服力强。Druid 连接池的引入大致是这样:
public class DBUtil { private static DruidDataSource dataSource; static { dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/equipment_repair?useSSL=false&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }代码说明:setInitialSize 是启动时预创建的连接数,setMaxActive 是连接池最大上限。换成连接池后,原来 Service 层里手动 setAutoCommit(false)、commit、rollback 的事务代码一行都不用动,连接池负责连接的发放和回收。这个改造能让你顺带把"数据库连接池的申请与归还"这个高频考点吃透。
关于在 IDEA 里运行这套原生 Servlet 项目,配置步骤是固定套路:导入项目后选择 Web 应用模板,配置好 Tomcat Server,把 Artifact 选成 exploded 解压部署模式,在 Run/Debug Configurations 里配好 Application Server 端口就能跑。用 VSCode 写 Servlet 再配环境的人也有,但 JavaWeb 的断点调试还是 IDEA 顺手,建议用 IDEA 跑通之后再考虑其他编辑器。
我最后的习惯是:每次改完代码部署前,先扫一眼 IDEA 的 Build 输出和 Tomcat 的 catalina.out,把异常定位到具体行号再动手。很多问题不是逻辑复杂,而是环境不一致导致的,把 JDK、Tomcat、MySQL、驱动的版本固定下来写进项目说明,能帮后来接手的人省下大量时间。设备维修系统这套源码不算惊艳,但只要你把每条链路走通、每个报错都亲手解决过,再去看 MyBatis、Spring MVC 时,心里会多一份别人没有的底牌:知道这些框架底层在替你做什么、不做什么。希望帮到你。
本文还有配套的精品资源,点击获取