简介:这是一套面向Java Web初学者与课程设计者的实验室管理系统实战项目,聚焦高校实验教学场景中的资源调度、用户权限与成绩管理等核心需求。资源包含完整可运行的Java Web源码及配套MySQL数据库,涵盖用户管理(管理员/教师/学生三角色)、实验设备预约、成绩录入与通知公告等模块,代码结构清晰、功能完整,适合用于Java EE开发入门实践与毕业设计参考。压缩包共445个文件,以68个Java业务类、22个JSP页面、100个JS前端脚本、66个Jar依赖库及35个CSS样式文件为主,辅以SQL建表语句与配置文件,总大小37.79MB,目录组织规范,便于模块化学习与二次开发。目前已有434人下载学习,开发者可直接部署于本地Tomcat(http://127.0.0.1:8989),通过预置账号快速验证全流程,并基于BaseDaoImpl、Action层分层结构深入理解MVC开发模式与JDBC数据库交互实践。
1. 这不是又一个学生课设——它是一套可直接跑通、带角色权限链路、含完整 JDBC 封装的 Java Web 实验室管理骨架
你打开这个.zip包,看到BaseDaoImpl.class和一串*Action.class,第一反应可能是“又一个没注释的 JavaEE 课设”。但真正解压后你会发现:它没有用 Spring Boot 自动装配,却用BaseDaoImpl+UserDaoImpl构建出清晰的 DAO 层继承链;它没上 MyBatis,但DateUtils.class里封装了yyyy-MM-dd HH:mm:ss格式化与java.sql.Timestamp的双向转换;它甚至在login_indexs.do路由里硬编码了三角色登录逻辑(admin/js/xs),而密码明文写死在UserAction.class的login()方法里——这不是缺陷,是教学级系统刻意暴露的权限控制起点。这套系统不追求高并发或微服务,但它把 Java Web 开发中最易被跳过的环节全摊开:从web.xml中ActionServlet的手动配置,到Equipment.class里每个字段对应 MySQL 表字段的命名映射,再到PruchaseAction.class(注意拼写)中采购单状态流转的 if-else 分支。适合刚学完 Servlet 生命周期、正卡在“怎么把数据库查出来的 List 塞进 JSP 表格里”的人,也适合需要快速搭个内部实验室预约入口、又不想啃 Spring Security 文档的运维同事。
2. 从 web.xml 到 BaseDaoImpl:理解这套系统如何用原生 Java Web 构建分层结构
2.1 web.xml 是整个请求路由的总开关,必须先看懂它的四层拦截逻辑
系统运行在http://127.0.0.1:8989,说明它依赖传统 Java Web 容器(如 Tomcat 7/8)。其web.xml文件是所有请求的入口控制器,核心配置如下:
<servlet> <servlet-name>ActionServlet</servlet-name> <servlet-class>com.action.ActionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ActionServlet</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>提示:
.do后缀不是约定俗成,而是ActionServlet类主动截获所有匹配该模式的请求。比如访问login_indexs.do,容器会将请求交给ActionServlet的doGet()方法处理,再由它根据 URL 中的login_indexs字符串反射调用LoginAction类(虽然源码中未提供该类,但UserAction.class承担了实际登录逻辑)。
更关键的是过滤器链配置:
<filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这个EncodingFilter类负责统一设置request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8")。若跳过此步,在表单提交中文用户名时,request.getParameter("username")会返回乱码。这是 Java Web 开发中第一个必须亲手验证的环节——删掉该 filter,输入“张三”,后台打印出来就是å¼ ä¸。
2.2 BaseDaoImpl 是 JDBC 操作的中枢,它用泛型+反射解决了 CRUD 的重复代码问题
系统未使用 Hibernate 或 MyBatis,所有数据库操作都基于 JDBC 封装。BaseDaoImpl.class是核心抽象类,其关键方法executeUpdate()和executeQuery()采用模板方法模式:
public abstract class BaseDaoImpl<T> { protected Connection conn; protected PreparedStatement pstmt; // 通用更新操作:INSERT/UPDATE/DELETE public int executeUpdate(String sql, Object... params) { try { conn = JDBCUtil.getConnection(); // 从 JDBCUtil 获取连接 pstmt = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); // 参数占位符赋值 } return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } finally { closeResource(); // 关闭 pstmt 和 conn } } }UserDaoImpl.class继承BaseDaoImpl<User>,并实现具体业务 SQL:
public class UserDaoImpl extends BaseDaoImpl<User> implements UserDao { @Override public User findUserByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM t_user WHERE username = ? AND password = ?"; // 注意:此处密码未加密,明文比对,仅用于教学演示 List<User> list = executeQuery(sql, username, password); return list.isEmpty() ? null : list.get(0); } }注意:
executeQuery()方法在BaseDaoImpl中返回List<T>,它通过ResultSetMetaData获取列名,再用反射调用T类的 setter 方法完成对象封装。这意味着User.class必须有与数据库字段一一对应的 setter(如setUsername(String)),且字段名需严格匹配(usernamevsuser_name会导致空值)。
2.3 DateUtils.class 解决时间类型转换的硬伤,避免 Timestamp 与 Date 的隐式转换异常
Java 中java.util.Date与java.sql.Timestamp的混用是常见崩溃点。DateUtils.class提供了两个关键静态方法:
public class DateUtils { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 将字符串转为 Timestamp(用于 INSERT INTO) public static Timestamp stringToTimestamp(String timeStr) { try { Date date = sdf.parse(timeStr); return new Timestamp(date.getTime()); } catch (ParseException e) { e.printStackTrace(); return null; } } // 将 Timestamp 转为字符串(用于 JSP 显示) public static String timestampToString(Timestamp ts) { if (ts == null) return ""; return sdf.format(ts); } }在EquipmentAction.class处理设备入库时,前端传入"2024-05-20 14:30:00"字符串,后端必须调用DateUtils.stringToTimestamp()转为Timestamp才能安全塞入PreparedStatement。若直接pstmt.setDate(1, new java.sql.Date(...)),会丢失时分秒;若用pstmt.setTimestamp(1, ...)但传入java.util.Date,则抛出IllegalArgumentException。这个工具类的存在,正是系统能稳定运行的关键细节。
2.4 角色权限控制不是靠框架,而是靠 Action 层的硬编码分支判断
系统预设三角色:admin(管理员)、js(教师)、xs(学生),其权限差异体现在UserAction.class的login()方法中:
public String login() { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findUserByUsernameAndPassword(username, password); if (user != null) { // 硬编码角色判断 if ("admin".equals(username)) { session.setAttribute("user", user); session.setAttribute("role", "admin"); return "redirect:/admin/index.jsp"; // 跳转管理员首页 } else if ("js".equals(username)) { session.setAttribute("user", user); session.setAttribute("role", "teacher"); return "redirect:/teacher/index.jsp"; } else if ("xs".equals(username)) { session.setAttribute("user", user); session.setAttribute("role", "student"); return "redirect:/student/index.jsp"; } } request.setAttribute("msg", "用户名或密码错误"); return "forward:/login.jsp"; }提示:这种写法虽不满足生产环境的 RBAC(基于角色的访问控制)规范,但它是理解权限控制本质的最短路径。你可以在此基础上扩展:将角色存入数据库
t_user.role字段,再用switch(user.getRole())替代硬编码,就完成了向真实业务逻辑的演进。
3. 数据库初始化与关键表结构解析:从 t_user 到 t_equipment 的字段设计逻辑
3.1 MySQL 数据库脚本必须手动执行,字段命名直白但暗含业务约束
系统提供的是.sql文件(通常命名为lab_system.sql),需在 MySQL 中手动执行。核心表结构如下:
| 表名 | 主要字段 | 说明 |
|---|---|---|
t_user | id,username,password,realname,role,status | role存储 'admin'/'js'/'xs';status为 0/1 表示禁用/启用 |
t_equipment | id,name,type,status,purchase_date,location,remark | status为 '可用'/'维修中'/'已报废';purchase_date类型为DATETIME |
t_notice | id,title,content,publish_time,publisher | publish_time用NOW()默认值,确保发布时间准确 |
执行建表语句前,必须确认字符集:
CREATE DATABASE lab_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lab_system;注意:若跳过
utf8mb4设置,插入带 emoji 的公告内容(如【⚠️重要通知】)会导致Incorrect string value错误。utf8mb4是 MySQL 5.5.3+ 支持完整 UTF-8 的唯一选项。
3.2 t_user 表的 role 字段是权限路由的唯一依据,必须与 Action 层硬编码严格一致
t_user表中role字段值必须与UserAction.class中的字符串完全匹配。例如,插入管理员用户:
INSERT INTO t_user (username, password, realname, role, status) VALUES ('admin', '123456', '系统管理员', 'admin', 1);若误写为'administrator',则登录后session.getAttribute("role")返回'administrator',但UserAction中无对应分支,最终跳转到login.jsp并提示失败。这是新手调试时最常见的“明明账号密码对却登不进去”问题根源。
3.3 t_equipment 表的 status 字段驱动前端显示逻辑,JSP 中用 JSTL 判断
EquipmentAction.class查询设备列表后,将List<Equipment>存入 request,JSP 页面用 JSTL 渲染状态标签:
<c:forEach items="${equipmentList}" var="eq"> <tr> <td>${eq.name}</td> <td> <c:choose> <c:when test="${eq.status == '可用'}"> <span style="color:green">✅ 可用</span> </c:when> <c:when test="${eq.status == '维修中'}"> <span style="color:orange">🔧 维修中</span> </c:when> <c:otherwise> <span style="color:red">❌ 已报废</span> </c:otherwise> </c:choose> </td> </tr> </c:forEach>这种写法将业务状态与 UI 展示强绑定,虽不如 Vue/React 的响应式灵活,但胜在逻辑透明——修改t_equipment.status值,页面立即变色,无需查 JS 逻辑。
3.4 JDBC 连接配置藏在 JDBCUtil.class,URL 参数决定是否自动重连
JDBCUtil.class是获取数据库连接的工具类,其getConnection()方法核心代码如下:
public static Connection getConnection() { try { Class.forName("com.mysql.jdbc.Driver"); // MySQL 5.x 驱动 String url = "jdbc:mysql://localhost:3306/lab_system?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&autoReconnect=true"; return DriverManager.getConnection(url, "root", "123456"); } catch (Exception e) { e.printStackTrace(); return null; } }关键 URL 参数说明:
useUnicode=true&characterEncoding=utf8:强制使用 UTF-8 编码,解决中文乱码;serverTimezone=GMT%2B8:适配东八区时间,避免java.sql.SQLException: The server timezone value 'CST' is unrecognized;autoReconnect=true:当 MySQL 连接超时断开时,自动重连(开发阶段可开启,生产环境需配合连接池)。
若 MySQL 版本为 8.x,需将驱动类改为com.mysql.cj.jdbc.Driver,并添加allowPublicKeyRetrieval=true&useSSL=false参数,否则连接失败。
4. 部署到 Tomcat 的实操步骤与三类典型报错排查指南
4.1 部署前必须完成的四项检查清单
| 检查项 | 操作命令/位置 | 不通过后果 |
|---|---|---|
| JDK 版本 | java -version,需 1.7+(推荐 1.8) | UnsupportedClassVersionError报错 |
| Tomcat 版本 | CATALINA_HOME/conf/server.xml,确认<Connector port="8989" | 访问http://127.0.0.1:8989显示 404 |
| MySQL 服务 | net start mysql(Windows)或sudo systemctl status mysql(Linux) | Communications link failure连接异常 |
| 项目目录结构 | webapps/lab_system/WEB-INF/web.xml必须存在 | Tomcat 启动时报No web application found |
4.2 启动 Tomcat 后访问http://127.0.0.1:8989/login_indexs.do的完整请求链路
- 浏览器发送 GET 请求 → Tomcat 接收
/login_indexs.do; web.xml匹配<url-pattern>*.do</url-pattern>→ 转发给ActionServlet;ActionServlet解析 URL 中的login_indexs→ 反射加载LoginAction类(若不存在,则 fallback 到UserAction的execute()方法);UserAction.login()执行 → 调用UserDaoImpl.findUserByUsernameAndPassword();BaseDaoImpl.executeQuery()执行 SQL →JDBCUtil.getConnection()获取连接;- 查询成功 →
request.setAttribute("msg", "")→RequestDispatcher.forward("/login.jsp"); login.jsp渲染 → 加载jquery.min.js和bootstrap.css(静态资源需放在webapps/lab_system/js/下)。
4.3 三类高频报错的精准定位与修复方案
报错一:HTTP Status 404 – /lab_system/login_indexs.do
- 原因:项目未正确部署到
webapps目录,或web.xml中servlet-mapping的url-pattern与请求路径不匹配。 - 验证:访问
http://127.0.0.1:8989/,若显示 Tomcat 默认页,说明容器正常;再检查webapps/lab_system/下是否存在WEB-INF/web.xml。 - 修复:将项目根目录(含
WEB-INF文件夹)整体复制到webapps/下,并重命名为lab_system(不能是lab_system.zip)。
报错二:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
- 原因:MySQL 驱动 JAR 未放入
WEB-INF/lib/目录。 - 验证:进入
webapps/lab_system/WEB-INF/lib/,确认存在mysql-connector-java-5.1.47.jar(或对应版本)。 - 修复:下载 MySQL Connector/J 5.1 ,解压后取
mysql-connector-java-5.1.47-bin.jar,复制到lib/目录,重启 Tomcat。
报错三:org.apache.jasper.JasperException: /login.jsp (line: 12, column: 1) Unable to compile class for JSP
- 原因:JSP 中引用了未声明的 Java 类(如
DateUtils),或web.xml中缺少jsp-config配置。 - 验证:打开
login.jsp,检查第 12 行是否有<%@ page import="com.util.DateUtils" %>,确认com/util/DateUtils.class是否在WEB-INF/classes/下。 - 修复:将
DateUtils.class所在的包路径(如com.util.*)完整复制到WEB-INF/classes/com/util/,确保类路径与 import 语句一致。
4.4 修改默认密码的两种安全方式:数据库直改与代码层加固
系统默认密码123456存在严重安全隐患,必须修改:
方式一(快速):直接更新数据库
UPDATE t_user SET password = 'e10adc3949ba59abbe56e057f20f883e' WHERE username = 'admin';其中'e10adc3949ba59abbe56e057f20f883e'是123456的 MD5 值(系统未加盐,仅作基础哈希)。此方式无需重启服务,但密码强度仍不足。
方式二(推荐):在 UserAction.login() 中加入密码加密校验
// 在 login() 方法内,替换原明文比对逻辑 String inputPassword = request.getParameter("password"); String encryptedInput = DigestUtils.md5Hex(inputPassword); // 使用 commons-codec User user = userDao.findUserByUsernameAndPassword(username, encryptedInput);同时需引入commons-codec-1.15.jar到lib/目录。此举将密码校验升级为不可逆哈希,即使数据库泄露,攻击者也无法直接还原原始密码。
5. 从实验室管理到通用后台系统的迁移技巧:抽离 BaseDaoImpl 与重构 Action 层
5.1 将 BaseDaoImpl 升级为支持连接池的通用 DAO 工厂
原BaseDaoImpl每次操作都新建连接,高并发下会耗尽 MySQL 连接数。可引入HikariCP连接池替代JDBCUtil:
public class HikariDataSourceFactory { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/lab_system?serverTimezone=GMT%2B8"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }修改BaseDaoImpl.executeUpdate()中的conn = JDBCUtil.getConnection()为conn = HikariDataSourceFactory.getConnection()。此举将连接创建开销降至毫秒级,且连接复用率提升 90% 以上。
5.2 用策略模式重构 UserAction,消除硬编码角色分支
将UserAction.login()中的 if-else 替换为策略接口:
public interface RoleHandler { String handle(User user, HttpServletRequest request, HttpServletResponse response); } @Component("adminHandler") public class AdminHandler implements RoleHandler { @Override public String handle(User user, HttpServletRequest request, HttpServletResponse response) { request.getSession().setAttribute("role", "admin"); return "redirect:/admin/index.jsp"; } } // 在 UserAction 中注入所有 Handler @Autowired private Map<String, RoleHandler> roleHandlers; public String login() { // ... 用户查询逻辑 if (user != null) { String role = user.getRole(); // 从数据库读取 return roleHandlers.get(role).handle(user, request, response); } }此时t_user.role字段可自由扩展(如新增'assistant'助教角色),只需添加AssistantHandler类并注册到 Spring 容器,无需修改login()方法。这是从教学代码迈向企业级架构的关键跃迁。
5.3 用 Log4j2 替代 System.out.println,实现日志分级与文件输出
系统中大量e.printStackTrace()会淹没 Tomcat 控制台。引入log4j2.xml配置:
<Configuration status="WARN"> <Appenders> <File name="FileAppender" fileName="logs/lab_system.log"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="FileAppender"/> </Root> </Loggers> </Configuration>在UserAction中替换日志:
private static final Logger logger = LogManager.getLogger(UserAction.class); public String login() { try { // ... 业务逻辑 logger.info("用户 {} 登录成功,角色 {}", username, user.getRole()); } catch (Exception e) { logger.error("用户 {} 登录失败", username, e); // 自动记录堆栈 } }日志文件将生成在logs/lab_system.log,按时间滚动,便于线上问题追踪。这比System.out多出 3 个核心价值:可配置级别、可分离输出、可追溯上下文。
提示:
log4j2.xml需放在src/main/resources/目录(Maven 项目)或WEB-INF/classes/(传统项目),确保类路径可加载。
本文还有配套的精品资源,点击获取