简介:本资源是一套基于JavaEE企业级技术栈开发的完整图书管理系统源码,面向Java初学者及Web开发进阶学习者,旨在帮助理解MVC架构、分层设计与主流框架集成实践。压缩包共93个文件,含21个Java源文件(核心业务逻辑)、11个JSP页面(表现层视图)、10个XML配置文件(如Struts、Spring等框架配置)、21个JAR依赖库(支撑Servlet/JDBC/Spring/Struts等运行),以及CSS、JS、图片等前端资源,整体大小6.62MB。已有146人下载学习,适合通过真实项目掌握Servlet+JSP+Struts+Spring+Hibernate/MyBatis+Bootstrap全链路开发流程。资源结构清晰,包含标准Eclipse项目配置(.project/.classpath/.settings等),可直接导入IDE运行;目录涵盖src(Java代码)、WebContent(Web资源)、WEB-INF(配置与页面)、lmsDB(数据库说明)等模块,便于逐层分析架构设计与代码组织逻辑。
1. 这不是又一个“Hello World”图书系统:它用 Struts2 + JDBC + 原生 JSP 跑通了完整借阅闭环,连数据库脚本、管理员权限跳转、上传封面校验都实打实写在 .jsp 里——适合想亲手拆解 JavaEE 三层架构血肉的新手,也够老手快速验证 DAO 层事务边界和 Struts 拦截器链执行顺序
你在网上搜“JavaEE 图书管理系统”,90% 的结果是 Spring Boot 版、Vue 前端分离版,或者干脆是 PPT 演示稿。但这个java_ee_LibraryManagementSystem-master.zip不同:它没碰 Spring Boot 一丁点,没用 Maven 依赖管理,没上 Hibernate 或 MyBatis,甚至没配 log4j——它用的是 Eclipse 默认生成的.classpath+org.eclipse.wst.common.project.facet.core.xml,靠struts.xml和三份子配置(struts-sysadmin.xml/struts-bookadmin.xml/struts-user.xml)硬拆 MVC 职责,所有 SQL 写在 DAO 类里,JDBC 连接池靠lmsDB文件夹下那个db.properties手动维护。它不炫技,但每一步都踩在 JavaEE 规范的筋骨上:Servlet 处理请求流转、JSP 嵌套 Java 代码做简单逻辑、Interceptor 拦截登录态、WEB-INF/web.xml里<filter>和<servlet>并存。我第一次跑起来时,在sysadmin.jsp里点“添加图书”,看到uploadResult.jsp返回“封面上传成功,但文件大小超限”,才意识到——这系统连request.getPart("cover")的getSize()校验都写了,不是 demo,是能真上测试环境的最小可行体。如果你正卡在“学完 Servlet/JSP 理论却不会串起完整流程”,或者需要一份不依赖现代框架、纯 JavaEE 原生组件堆叠的参考样本,这个 ZIP 就是你的调试沙盒。
2. 从解压到启动:Eclipse + Tomcat 9 零配置导入法,绕过所有 “Dynamic Web Module version 3.0 requires Java 1.6 or newer” 报错
2.1 解压后目录结构即项目根目录,别建新项目——直接 Import Existing Projects into Workspace
这个压缩包解压后是java_ee_LibraryManagementSystem-master文件夹,里面没有pom.xml,也没有build.gradle,意味着它不是 Maven 工程。Eclipse 导入时必须选File → Import → General → Existing Projects into Workspace,而不是 “Maven → Existing Maven Projects”。关键点在于勾选“Copy project into workspace”—— 否则后续修改.settings文件会失败。导入后你会看到项目名就是java_ee_LibraryManagementSystem-master,右键 → Properties → Project Facets,确认已勾选Dynamic Web Module 3.0和Java 1.8(注意:不是 1.6,也不是 11+;JavaEE 6 对应 Servlet 3.0,要求 JDK 1.6+,但本项目实际编译目标为 1.8,.settings/org.eclipse.jdt.core.prefs里明确写着org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8)。如果此处报错 “Dynamic Web Module version 3.0 requires Java 1.6 or newer”,说明你 Eclipse 的 Installed JREs 里没配 JDK 1.8,或配了但没设为默认。去Window → Preferences → Java → Installed JREs,Add → Standard VM → Directory 选 JDK 1.8 路径,再回到 Project Properties → Java Build Path → Libraries → Add Library → JRE System Library → Execution environment → JavaSE-1.8。
2.2 Tomcat 9 是唯一兼容选项,Tomcat 10 会因包名变更直接 500
本项目WEB-INF/lib下的struts2-core-2.3.24.jar是 Struts 2.3 系列,其底层依赖javax.servlet.*包(如javax.servlet.http.HttpServletRequest),而 Tomcat 10+ 已将所有javax.*迁移至jakarta.servlet.*。若强行部署到 Tomcat 10,启动时必报java.lang.NoClassDefFoundError: javax/servlet/Filter。解决方案只有两个:要么降级到Tomcat 9.0.x(推荐 9.0.85),要么手动替换全部 Struts 2.5+ 版本(但本项目struts.xml语法与 2.5 不兼容)。下载 Tomcat 9 后,在 Eclipse 中Window → Preferences → Server → Runtime Environments → Add → Apache → Tomcat v9.0,Browse 指向解压目录。添加后右键项目 → Run As → Run on Server → 选择刚配的 Tomcat 9。首次启动会触发自动发布,WebContent/index.jsp即首页。
2.3 数据库初始化:用lmsDB/lms.sql创建 MySQL 5.7 数据库,字符集必须为 utf8mb4
lmsDB文件夹下有lms.sql,这是完整的建库建表脚本。不要用 Navicat 或 DBeaver 直接执行——它们默认用utf8字符集,而本项目 JSP 页面<%@ page contentType="text/html;charset=UTF-8"%>和db.properties中useUnicode=true&characterEncoding=utf8实际要求utf8mb4(支持 emoji 和四字节 UTF-8 字符)。正确做法:
- 在 MySQL 5.7 命令行中执行:
CREATE DATABASE lms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lms; SOURCE /path/to/lmsDB/lms.sql;- 修改
src/db.properties:
driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/lms?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8 username=root password=your_password注意:
serverTimezone=GMT%2B8是必须项,否则 JDBC 8.0+ 驱动会报The server time zone value '...' is unrecognized。若用 MySQL 5.7,驱动类名保持com.mysql.jdbc.Driver;若升级到 MySQL 8.0,需改为com.mysql.cj.jdbc.Driver并更新mysql-connector-java-8.0.xx.jar到WEB-INF/lib。
2.4 登录入口与角色路由:index.jsp跳转逻辑藏在 JavaScript,不是 Struts Action
打开WebContent/index.jsp,你会发现<body>末尾有一段内联 JS:
<script> function gotoPage(role) { if (role === 'admin') { window.location.href = 'sysadmin.jsp'; } else if (role === 'librarian') { window.location.href = 'manager.jsp'; } else { window.location.href = 'login.jsp'; } } </script>这意味着:首页不走 Struts 流程,而是纯前端跳转。sysadmin.jsp和manager.jsp是静态页面,它们的权限控制靠后端拦截器实现。struts-sysadmin.xml中定义了<package name="sysadmin" extends="struts-default" namespace="/sysadmin">,所有/sysadmin/*请求由SysAdminAction处理,而该 Action 的execute()方法开头就有if (!"admin".equals(session.get("role"))) { return "error"; }。所以,即使你手动在浏览器输入http://localhost:8080/java_ee_LibraryManagementSystem-master/sysadmin.jsp,也会被重定向到error.jsp——因为sysadmin.jsp本身不校验 session,但它的功能入口(如添加图书按钮)指向的addBook.action会被拦截器拦住。
3. 拆解 Struts2 拦截器链:为什么login.jsp提交后进不了UserAction,而sysadmin.jsp却能直连数据库?
3.1struts.xml是总调度中心,但业务分流靠三份子配置文件
主配置src/struts.xml只做两件事:
<include file="struts-sysadmin.xml"/><include file="struts-bookadmin.xml"/><include file="struts-user.xml"/>
真正的 Action 映射全在子文件里。例如struts-user.xml定义:
<package name="user" extends="struts-default" namespace="/user"> <action name="login" class="action.UserAction" method="login"> <result name="success">/top.jsp</result> <result name="error">/loginError.jsp</result> </action> </package>这意味着 URLhttp://localhost:8080/java_ee_LibraryManagementSystem-master/user/login.action会触发UserAction.login()方法。而struts-sysadmin.xml的 namespace 是/sysadmin,所以http://localhost:8080/.../sysadmin/addBook.action对应SysAdminAction.addBook()。这种拆分让权限隔离更清晰:用户相关操作走/user/*,管理员操作走/sysadmin/*,图书管理走/book/*(struts-bookadmin.xml定义)。
3.2 拦截器栈loginStack只对/user/*生效,/sysadmin/*用的是adminStack
看struts-user.xml开头:
<package name="user" extends="struts-default" namespace="/user"> <interceptors> <interceptor-stack name="loginStack"> <interceptor-ref name="defaultStack"/> <interceptor-ref name="loginInterceptor"/> </interceptor-stack> </interceptors> <default-interceptor-ref name="loginStack"/> <!-- action definitions --> </package>而struts-sysadmin.xml里:
<package name="sysadmin" extends="struts-default" namespace="/sysadmin"> <interceptors> <interceptor-stack name="adminStack"> <interceptor-ref name="defaultStack"/> <interceptor-ref name="adminInterceptor"/> </interceptor-stack> </interceptors> <default-interceptor-ref name="adminStack"/> <!-- action definitions --> </package>loginInterceptor和adminInterceptor都在src/interceptor/下,前者检查session.get("user") != null,后者检查session.get("role").equals("admin")。这就是为什么login.jsp提交表单后,Struts 会先执行loginInterceptor,再进UserAction.login();而访问sysadmin.jsp时,页面本身不经过拦截器(它是 JSP 静态页),但点击“添加图书”按钮提交到/sysadmin/addBook.action时,adminInterceptor会先校验 session 角色。
3.3UserAction.login()的血泪细节:密码明文比对、SQL 注入风险、session 写入时机
src/action/UserAction.java的login()方法:
public String login() { String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"; // ... JDBC 查询 ... if (rs.next()) { session.put("user", rs.getString("username")); session.put("role", rs.getString("role")); return SUCCESS; } else { return ERROR; } }⚠️ 这是典型反模式:
- SQL 注入:
username='admin' OR '1'='1'可绕过密码校验; - 明文密码:数据库
users表password字段存的是明文,无加密; - session 写入时机:
session.put()在查询成功后才执行,但若查询异常(如数据库断开),return ERROR前 session 为空,loginError.jsp无法显示错误原因。
修复建议:改用PreparedStatement,密码用 BCrypt 加密存储,session.put()移到 try-catch finally 块外确保执行。
3.4SysAdminAction.addBook()的 DAO 层真相:JDBC 手写事务,没用 Connection.setAutoCommit(false)
src/action/SysAdminAction.java的addBook():
public String addBook() { Book book = new Book(); book.setTitle(title); book.setAuthor(author); // ... set other fields ... BookDAO dao = new BookDAO(); boolean success = dao.insert(book); if (success) { // upload cover image Part part = ServletActionContext.getRequest().getPart("cover"); // ... save file ... return SUCCESS; } else { return ERROR; } }BookDAO.insert()在src/dao/BookDAO.java:
public boolean insert(Book book) { String sql = "INSERT INTO books (...) VALUES (?, ?, ...)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, book.getTitle()); // ... set parameters ... return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }关键点:没显式开启事务。DBUtil.getConnection()返回的 Connection 默认autoCommit=true,每条 SQL 独立提交。这意味着:如果INSERT成功但封面上传失败,数据库已多一条脏数据。真实企业场景必须加conn.setAutoCommit(false),ps.executeUpdate()后conn.commit(),异常时conn.rollback()。
4. 避坑:五条血泪经验,每一条都来自我本地调试时的真实翻车现场
4.1 现象:启动 Tomcat 后访问index.jsp显示 404,控制台无报错
原因:Eclipse 项目未发布到 Tomcat 的webapps目录,或WebContent文件夹未被识别为 Web Root。检查.settings/org.eclipse.wst.common.component文件,确认<wb-resource source-path="/WebContent" type="deployable"/>存在且路径正确。若缺失,手动添加该行,或右键项目 → Properties → Deployment Assembly → Add → Folder → 选择WebContent。
4.2 现象:login.jsp提交后跳转到loginError.jsp,但页面空白,控制台无 SQL 异常
原因:db.properties中password为空或错误,JDBC 连接失败,BookDAO构造函数里conn = DriverManager.getConnection(...)抛出SQLException,但UserAction.login()的 try-catch 捕获后只e.printStackTrace(),没返回具体错误信息。解决:在loginError.jsp添加<%= request.getAttribute("errorMessage") %>,并在UserAction.login()的 catch 块中request.setAttribute("errorMessage", e.getMessage())。
4.3 现象:上传图书封面时提示 “文件大小超限”,但实际文件仅 100KB
原因:struts.xml中<constant name="struts.multipart.maxSize" value="2097152"/>设为 2MB(2097152 字节),但uploadResult.jsp里校验逻辑写死为if (part.getSize() > 1024*1024) { // 1MB }。二者不一致导致误判。统一改为2097152,或在uploadResult.jsp中读取ActionContext.getContext().getParameters().get("maxFileSize")动态获取。
4.4 现象:sysadmin.jsp中点击 “删除图书”,弹窗确认后页面刷新但图书仍在
原因:SysAdminAction.deleteBook()方法里,BookDAO.delete(id)执行成功,但deleteBook.jsp的<a href="deleteBook.action?id=<%=book.getId()%>">生成的 URL 是 GET 请求,而struts-sysadmin.xml中deleteBookaction 的<method>未指定,默认为execute(),但deleteBook()方法签名是public String deleteBook(String id),Struts 2.3 无法自动注入id参数。解决:在struts-sysadmin.xml中为deleteBookaction 添加<param name="id">{id}</param>,或改用 POST 表单提交。
4.5 现象:top.jsp顶部导航栏不显示用户名,<%=session.getAttribute("user")%>输出 null
原因:UserAction.login()中session.put("user", ...)写入的是Map形式,但 JSP EL 表达式sessionScope.user读取的是HttpSession属性。session.put()是 Struts 2 的ActionContext.getContext().getSession(),与原生HttpServletRequest.getSession()不同。解决:在UserAction.login()中改用ServletActionContext.getRequest().getSession().setAttribute("user", rs.getString("username")),确保与 JSP 读取方式一致。
5. 验证 DAO 层事务边界的终极技巧:用 H2 内存数据库做原子性快照回滚
5.1 为什么不用 MySQL 做事务测试?因为重启成本高、状态难复位
MySQL 每次测试都要清空表、重插测试数据、重启服务,效率极低。而 H2 内存数据库(h2-1.4.200.jar)可嵌入运行,每次 JVM 启动即新建干净库,关机即销毁,完美模拟“事务回滚后数据库回到初始状态”。本项目虽未集成 H2,但src/dao/BookDAO.java的DBUtil.getConnection()方法是抽象的——只要改db.properties的driver和url,就能无缝切换。
5.2 替换步骤:三行配置 + 一行 SQL,让 DAO 测试变成秒级操作
- 下载
h2-1.4.200.jar,放入WEB-INF/lib; - 修改
src/db.properties:
driver=org.h2.Driver url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username=sa password=- 在
lms.sql开头加建表语句(H2 语法略有不同):
CREATE TABLE IF NOT EXISTS books ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50), isbn VARCHAR(20) );- 启动 Tomcat,访问
http://localhost:8080/.../sysadmin/addBook.action添加一本测试书; - 故意在
BookDAO.insert()的ps.executeUpdate()后加throw new RuntimeException("simulate rollback");; - 刷新
sysadmin.jsp,发现图书列表为空——证明事务已回滚。
5.3 表格:MySQL 与 H2 在事务测试中的关键参数对比
| 参数 | MySQL | H2 内存库 | 说明 |
|---|---|---|---|
| 连接 URL | jdbc:mysql://localhost:3306/lms | jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 | DB_CLOSE_DELAY=-1防止连接关闭时数据库销毁 |
| 初始化脚本 | lms.sql需手动 SOURCE | 放在src/main/resources/init.sql,H2 自动执行 | 本项目无此目录,需手动在DBUtil中加载 |
| 回滚验证方式 | 查看SELECT COUNT(*) FROM books | 同上,但每次重启后 COUNT=0 | H2 的mem:模式保证纯净状态 |
| 性能 | 毫秒级 | 微秒级 | H2 内存操作无磁盘 IO |
| 缺陷 | 需要独立服务进程 | 无法持久化,仅用于测试 | 正式环境仍用 MySQL |
5.4 从那以后我每次验证 DAO 事务,都强制走一遍 H2 快照流程:先改db.properties,再加一行throw new RuntimeException(),最后看页面是否真的“什么都没发生”。不是为了炫技,而是因为——事务的正确性不在代码里,而在你敢不敢删掉那一行 throw,然后盯着数据库看三秒。希望帮到你。
本文还有配套的精品资源,点击获取