JavaEE图书管理系统实战:Struts2+JDBC+Tomcat完整部署与原理剖析

发布时间:2026/10/26 17:28:35
JavaEE图书管理系统实战:Struts2+JDBC+Tomcat完整部署与原理剖析

简介:本资源是一套基于JavaEE企业级开发标准实现的完整图书管理系统,面向Java初学者与Web开发进阶学习者,聚焦MVC分层架构、数据库交互与Web应用部署等核心能力训练。压缩包共93个文件,含21个Java源码文件(业务逻辑与控制器)、11个JSP页面(用户界面展示)、10个XML配置文件(Struts框架路由与数据源定义)、21个JAR依赖库(涵盖Servlet、JDBC、Struts及工具类),以及CSS、JS、图片等前端资源,整体大小6.62MB。已有146人下载学习,适合在Eclipse或IntelliJ IDEA中导入运行,可快速掌握Servlet+JSP+Struts+MySQL的典型JavaEE技术栈整合实践。项目采用清晰的分包结构(src下含action、bean、interceptor等模块),内置登录认证、图书增删改查、管理员与普通用户双角色权限管理,并附带完整的WEB-INF配置与Tomcat部署说明,是理解企业级Web应用分层设计与工程化落地的优质教学案例。

1. 这不是又一个“Hello World”图书系统:它用 Struts2 + JDBC + Tomcat 跑通了真实借阅闭环,新手照着部署能登录、查书、还书,老手一眼看出 DAO 层没硬编码 SQL、Interceptor 拦了未登录跳转——适合想补全 JavaEE 企业级开发链路的实战派

你可能已经下载过十几个标着“Java 图书管理系统”的压缩包,解压后发现:没有数据库脚本、login.jsp 里写死 admin/123、BookServlet 里 new 了 Connection 却没 close、连 web.xml 都缺<filter>配置。这个java_ee_LibraryManagementSystem-master.zip不是教学玩具——它用 Struts2 的struts-sysadmin.xml和struts-bookadmin.xml实现了角色分离(管理员 vs 普通用户),用lmsDB文件夹里完整的 MySQL 建库建表语句+初始数据撑起真实业务逻辑,interceptor包下LoginInterceptor.java真正拦截了/sysManage.action的未授权访问。它不依赖 Spring Boot 自动装配,而是用最原始的web.xml+struts.xml+JDBCUtils手动管理连接池,让你看清 JavaEE 时代“容器托管”和“手动控制”的边界在哪。如果你正卡在“学完 Servlet/JSP 却写不出可运行的完整系统”,或者需要一份能放进简历项目栏、经得起面试追问的 JavaEE 落地案例——它就是那个能跑起来、能改代码、能调 Bug 的“血肉之躯”,不是骨架图。


2. 从解压到首页:五步完成本地部署,关键在 WEB-INF/lib 里这 7 个 JAR 包的版本对齐

2.1 解压与项目结构识别:别急着导入 IDE,先确认三个核心目录是否存在

解压java_ee_LibraryManagementSystem-master.zip后,你会看到根目录下有src、WebContent、lmsDB三个不可删的文件夹:

  • src:Java 源码,含bean(实体类)、action(Struts2 控制器)、interceptor(拦截器)、dao(数据访问)
  • WebContent:Web 资源,等同于传统 WAR 包的webapp目录,WEB-INF/web.xml是入口配置
  • lmsDB:必须优先看——里面lms.sql是建库脚本,data.sql是初始化图书/用户数据,db.properties定义了 JDBC 连接参数

提示:WebContent/WEB-INF/classes是编译输出目录,但本项目默认用 Eclipse 导出,所以build/classes可能为空;实际运行时 Tomcat 会自动编译src下的.java文件到WEB-INF/classes,无需手动复制。

2.2 数据库准备:MySQL 5.7+ 执行 lms.sql,重点检查字符集与存储引擎

进入lmsDB目录,用 MySQL 客户端执行:

-- 创建数据库(注意字符集!) CREATE DATABASE lms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入表结构 SOURCE lms.sql; -- 导入测试数据(含管理员账号 admin/admin,普通用户 user/123) SOURCE data.sql;

关键参数说明:

  • utf8mb4:避免图书名含 emoji 或生僻字(如《¥》《𠈓》)时乱码,比utf8更彻底
  • InnoDB:lms.sql中所有表均指定ENGINE=InnoDB,确保事务支持(借书/还书需原子性)
  • data.sql中user表的role字段值为0(管理员)或1(普通用户),这是LoginInterceptor判断权限的依据

2.3 Tomcat 配置:8.5.x 是黄金版本,9.x 需手动降级 EL 表达式语法

本项目基于 Servlet 3.0 + JSP 2.2,强烈建议使用 Apache Tomcat 8.5.90(非最新版)。原因:

  • index.jsp中${pageContext.request.contextPath}写法在 Tomcat 9+ 默认禁用 EL 表达式,需额外配置web.xml
  • struts.xml使用<package name="default" extends="struts-default">,依赖 Struts2.3.x,与 Tomcat 8.5 兼容性最佳

操作步骤:

  1. 下载apache-tomcat-8.5.90.zip,解压到无中文路径(如D:\tomcat85)
  2. 将WebContent整个文件夹复制到D:\tomcat85\webapps\lms(注意:不是lms.war,是直接放文件夹)
  3. 启动D:\tomcat85\bin\startup.bat,访问http://localhost:8080/lms

2.4 依赖 JAR 包校验:WEB-INF/lib 缺这 7 个,部署必报 ClassNotFound

打开WebContent\WEB-INF\lib,必须存在以下 JAR(版本已验证):

JAR 名称作用版本要求
struts2-core-2.3.32.jarStruts2 核心必须 2.3.x,2.5+ 会因ActionSupport方法签名变更报错
xwork-core-2.3.32.jarXWork 框架与 struts2-core 版本严格匹配
ognl-3.0.6.jarOGNL 表达式解析低于 3.0.6 无法解析#session.user.role
freemarker-2.3.22.jarFreemarker 模板引擎Struts2 默认视图技术
commons-fileupload-1.3.1.jar文件上传支持uploadResult.jsp依赖此包处理封面上传
mysql-connector-java-5.1.47.jarMySQL JDBC 驱动5.1.x 系列,8.0+ 驱动需改db.properties的 URL 格式
commons-dbcp-1.4.jar数据库连接池JDBCUtils.java中BasicDataSource来自此包

注意:若你用 IntelliJ IDEA 导入,不要勾选 "Create module from existing sources",而应选择 "Import project" → "Import project from external model" → "Eclipse",否则src下的包结构会被 IDE 错误识别为default package。

2.5 启动验证:三步确认系统活了,而非只显示 404

  1. 首页加载:http://localhost:8080/lms/index.jsp应显示带搜索框的图书列表页,左上角有“登录”链接
  2. 登录成功:用admin/admin登录,跳转至sysManage.jsp,页面顶部显示“欢迎管理员”
  3. 功能连通:在sysManage.jsp点击“添加图书”,填入 ISBN、书名、作者,提交后刷新列表应出现新书——证明BookAction→BookDAO→ MySQL 全链路通畅

若卡在第 1 步,检查web.xml中welcome-file-list是否为:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

若卡在第 2 步,查看 Tomcat 日志logs/catalina.out,搜索SQLException或NoClassDefFoundError,大概率是mysql-connector-java.jar版本不对或db.properties中密码错误。


3. 拦截器、Action 与 DAO:拆解三层协作链,看 Struts2 如何把 HTTP 请求变成数据库操作

3.1 LoginInterceptor:不是装饰器,是请求守门人,它决定谁能看到 sysManage.jsp

src/interceptor/LoginInterceptor.java是整个权限体系的基石。它的intercept(ActionInvocation invocation)方法逻辑如下:

public String intercept(ActionInvocation invocation) throws Exception { // 1. 获取 session 中的 user 对象 Map<String, Object> session = ActionContext.getContext().getSession(); User user = (User) session.get("user"); // 2. 判断是否为管理员(role=0)且访问的是管理员路径 String actionName = invocation.getProxy().getActionName(); if (user == null || user.getRole() != 0) { if (actionName.startsWith("sysManage") || actionName.startsWith("bookAdmin")) { // 3. 未登录或非管理员,重定向到 login.jsp return "login"; } } // 4. 放行 return invocation.invoke(); }

关键点说明:

  • actionName.startsWith("sysManage"):匹配sysManage.action、sysManageAdd.action等,不是按 URL 路径匹配,而是 Struts2 解析后的 Action 名
  • session.get("user"):LoginAction.java在登录成功后session.put("user", user),此处复用,避免重复查库
  • 返回"login":触发struts.xml中<result name="login">/login.jsp</result>,实现无感跳转

3.2 BookAction:Struts2 的 Action 不是 Servlet,它靠反射调用 execute() 并绑定属性

src/action/BookAction.java继承ActionSupport,核心方法:

public String execute() throws Exception { // 1. 从 request 获取参数(Struts2 自动注入,无需 request.getParameter) String isbn = this.isbn; // 由 struts.xml 中 <param name="isbn">${isbn}</param> 绑定 String title = this.title; // 2. 调用业务层 BookService bookService = new BookService(); boolean result = bookService.addBook(isbn, title, author, price); // 3. 根据结果返回不同视图 if (result) { this.addActionMessage("添加成功!"); return SUCCESS; // 对应 struts-bookadmin.xml 中 <result name="success">/sysManage.jsp</result> } else { this.addActionError("添加失败,请检查ISBN是否重复"); return ERROR; } }

参数绑定原理:

  • this.isbn能直接取值,是因为 Struts2 的ParametersInterceptor在execute()前已将请求参数映射到 Action 属性
  • isbn字段必须有 public setter:public void setIsbn(String isbn) { this.isbn = isbn; }
  • 若字段名与表单 name 不一致,需在struts-bookadmin.xml中显式<param name="isbn">${book.isbn}</param>

3.3 BookDAO:JDBCUtils 是灵魂,它让 Connection 不再是“一次性用品”

src/dao/BookDAO.java中addBook()方法:

public boolean addBook(String isbn, String title, String author, double price) { String sql = "INSERT INTO book (isbn, title, author, price) VALUES (?, ?, ?, ?)"; try (Connection conn = JDBCUtils.getConnection(); // 关键:从连接池获取 PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, isbn); ps.setString(2, title); ps.setString(3, author); ps.setDouble(4, price); return ps.executeUpdate() > 0; // executeUpdate() 返回影响行数 } catch (SQLException e) { e.printStackTrace(); return false; } }

JDBCUtils.java的核心:

public static Connection getConnection() throws SQLException { // 初始化时读取 db.properties,创建 BasicDataSource if (dataSource == null) { Properties props = new Properties(); props.load(JDBCUtils.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = new BasicDataSource(); dataSource.setDriverClassName(props.getProperty("driver")); dataSource.setUrl(props.getProperty("url")); dataSource.setUsername(props.getProperty("username")); dataSource.setPassword(props.getProperty("password")); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxIdle(10); // 最大空闲连接 } return dataSource.getConnection(); // 从连接池取,非 new Connection() }

提示:db.properties中url=jdbc:mysql://localhost:3306/lms?useUnicode=true&characterEncoding=utf8mb4的characterEncoding=utf8mb4必须与数据库字符集一致,否则中文插入为??。

3.4 视图层:JSP 不是静态页面,它是 Struts2 标签 + EL 表达式的动态引擎

sysManage.jsp中图书列表渲染:

<s:iterator value="bookList" status="st"> <tr> <td><s:property value="isbn"/></td> <td><s:property value="title"/></td> <td><s:property value="author"/></td> <td><s:property value="price"/></td> <td> <s:a action="bookDelete" namespace="/" cssClass="btn btn-danger"> <s:param name="isbn">%{isbn}</s:param>删除 </s:a> </td> </tr> </s:iterator>

Struts2 标签解析:

  • <s:iterator>:遍历BookAction中private List<Book> bookList;的 getter 返回值
  • %{isbn}:OGNL 表达式,取当前迭代对象Book的isbn属性
  • <s:a>:生成<a href="bookDelete.action?isbn=978...">,namespace="/"表示根命名空间,对应struts.xml中<package name="default" namespace="/">

4. 避坑指南:这 4 个血泪问题让 80% 的人卡在部署前 30 分钟,附现象、原因与一键修复命令

4.1 现象:Tomcat 启动成功,但访问http://localhost:8080/lms报 404,日志无报错

原因:WebContent文件夹未正确放入webapps,或文件夹名不是lms(Tomcat 以文件夹名为上下文路径)
解决:

  • 确认D:\tomcat85\webapps\lms\WEB-INF\web.xml存在
  • 检查web.xml中<display-name>lms</display-name>与文件夹名一致
  • 一键验证:在webapps目录下执行dir /ad,应看到lms文件夹(非lms.war)

4.2 现象:登录页能打开,输入admin/admin后跳回login.jsp,无任何提示

原因:db.properties中数据库密码错误,或lms数据库未创建,导致LoginAction查询user表时ResultSet为空
解决:

  • 用 MySQL 客户端直连:mysql -u root -p -D lms,执行SELECT * FROM user WHERE username='admin';
  • 若报错Unknown database 'lms',重新执行lms.sql
  • 若查询无结果,检查data.sql是否执行成功(Query OK, 2 rows affected)

4.3 现象:点击“添加图书”报HTTP Status 500 – java.lang.NoClassDefFoundError: org/apache/commons/dbcp/BasicDataSource

原因:commons-dbcp-1.4.jar缺失或版本不匹配(如用了commons-dbcp-2.0.jar,其包路径为org.apache.commons.dbcp2)
解决:

  • 进入WebContent\WEB-INF\lib,执行dir commons-dbcp*,确认存在commons-dbcp-1.4.jar
  • 若存在多个版本,删除commons-dbcp-2.x.jar,只保留1.4
  • 验证命令:jar -tf commons-dbcp-1.4.jar | findstr "BasicDataSource"应输出org/apache/commons/dbcp/BasicDataSource.class

4.4 现象:图书列表显示??乱码,但数据库中中文正常

原因:JDBC 连接 URL 缺少characterEncoding=utf8mb4,或 MySQL 服务端字符集非utf8mb4
解决:

  • 检查lmsDB/db.properties中url=行末尾是否有?useUnicode=true&characterEncoding=utf8mb4
  • 登录 MySQL 执行:
SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%';
  • 确保character_set_server和collation_server为utf8mb4,否则修改my.cnf:
[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  • 重启 MySQL 服务

5. 从 Struts2 切换到 Spring MVC:三步改造清单,保留 DAO 层,重写 Controller 与配置

5.1 为什么换?不是为了时髦,而是解决 Struts2 的两个硬伤

  • 硬伤 1:XML 配置爆炸——struts.xml+struts-sysadmin.xml+struts-bookadmin.xml共 3 个文件,路由分散难维护
  • 硬伤 2:拦截器耦合重——LoginInterceptor里硬编码actionName.startsWith("sysManage"),新增管理员功能需改拦截器
    Spring MVC 的优势:
  • @RequestMapping("/sysManage")直接标注在 Controller 方法上,路由一目了然
  • @PreAuthorize("hasRole('ADMIN')")用注解替代拦截器,权限规则集中到 SecurityConfig

5.2 保留什么?DAO 层与 JDBCUtils 完全复用,零改造

BookDAO.java和JDBCUtils.java不需任何修改,因为:

  • Spring JDBC Template 本质仍是Connection+PreparedStatement
  • JDBCUtils.getConnection()返回的Connection可直接传给JdbcTemplate
  • 你只需在BookService.java中将new BookDAO()替换为@Autowired private BookDAO bookDAO;

5.3 新增配置:web.xml + spring-mvc.xml + pom.xml 三处关键改动

Step 1:web.xml 中替换 Struts2 Filter 为 Spring DispatcherServlet

<!-- 删除原 Struts2 filter --> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <!-- 新增 Spring MVC DispatcherServlet --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

Step 2:创建 WEB-INF/spring-mvc.xml,启用注解与组件扫描

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 扫描 Controller 包 --> <context:component-scan base-package="controller"/> <!-- 启用 MVC 注解驱动 --> <mvc:annotation-driven/> <!-- 视图解析器:JSP 前缀 /WEB-INF/jsp/,后缀 .jsp --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>

Step 3:pom.xml 添加 Spring 依赖(Maven 项目)

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.31</version> </dependency>

注意:spring-webmvc5.3.x 兼容 JDK 8,与本项目现有环境无缝衔接;若用 JDK 11,需升至 5.3.31+。

5.4 Controller 重写:用 @Controller 替代 Action,用 @RequestParam 替代属性注入

新建controller/BookController.java:

@Controller @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; // GET /book/list → 返回图书列表 @GetMapping("/list") public String list(Model model) { List<Book> bookList = bookService.getAllBooks(); model.addAttribute("bookList", bookList); return "book/list"; // 对应 /WEB-INF/jsp/book/list.jsp } // POST /book/add → 处理添加请求 @PostMapping("/add") public String add(@RequestParam String isbn, @RequestParam String title, @RequestParam String author, @RequestParam double price, Model model) { boolean result = bookService.addBook(isbn, title, author, price); if (result) { model.addAttribute("message", "添加成功!"); } else { model.addAttribute("error", "添加失败!"); } return "redirect:/book/list"; } }

对比 Struts2:

  • @RequestParam显式声明参数,比this.isbn更安全(避免空指针)
  • return "redirect:/book/list"实现重定向,防止表单重复提交
  • model.addAttribute()替代ActionContext.getContext().put(),更符合 Spring 生态

6. 验证系统健壮性的 3 个真实场景测试,以及我每次重构前必做的 5 分钟 checklist

6.1 场景测试 1:并发借书——模拟 100 个用户同时借同一本书

目的:验证BookDAO.updateStock()的事务隔离性
操作:

  1. 在book表中设stock=1(仅剩 1 本)
  2. 用 JMeter 创建线程组:100 个线程,循环 1 次,HTTP 请求POST /book/borrow?isbn=978...
  3. 检查borrow_record表记录数是否为 1(而非 100)

预期结果:只有 1 条记录,其余请求因stock <= 0被拒绝
底层机制:BookDAO.borrowBook()中UPDATE book SET stock = stock - 1 WHERE isbn = ? AND stock > 0的AND stock > 0是乐观锁,配合 MySQL 的InnoDB行锁,确保高并发下库存不超卖

6.2 场景测试 2:SQL 注入防御——在搜索框输入' OR '1'='1

目的:确认BookDAO.searchBooks()使用PreparedStatement而非字符串拼接
操作:

  • 在index.jsp搜索框输入:' OR '1'='1
  • 提交后观察返回结果

预期结果:返回空列表,或报错You have an error in your SQL syntax(因 PreparedStatement 参数化后,单引号被转义)
代码验证:检查BookDAO.java中searchBooks()方法:

String sql = "SELECT * FROM book WHERE title LIKE ?"; // ✅ 正确:? 占位符 // String sql = "SELECT * FROM book WHERE title LIKE '%" + keyword + "%'"; // ❌ 危险:字符串拼接

6.3 场景测试 3:文件上传安全——尝试上传 .jsp 文件作为图书封面

目的:验证UploadAction的文件类型白名单控制
操作:

  • 修改uploadResult.jsp中<input type="file" name="cover">
  • 选择一个伪造的.jsp文件(内容为<% out.print("hacked"); %>)
  • 提交上传

预期结果:页面提示“不支持的文件类型”,WebContent/upload/目录下无文件生成
机制定位:UploadAction.java中validate()方法:

public void validate() { if (coverFileName != null && !coverFileName.toLowerCase().endsWith(".jpg") && !coverFileName.toLowerCase().endsWith(".png")) { this.addFieldError("cover", "只支持 JPG/PNG 格式!"); } }

6.4 我的 5 分钟重构 checklist:每次改代码前强制执行,省去 80% 的回滚时间

  1. 数据库备份:mysqldump -u root -p lms > lms_backup_$(date +%Y%m%d).sql
  2. JAR 包快照:cd WebContent\WEB-INF\lib && zip -r lib_backup.zip *.jar
  3. 配置文件 diff:用 Beyond Compare 对比db.properties与db.properties.bak,确认连接参数未被覆盖
  4. 日志开关:在log4j.properties中临时开启log4j.logger.com.lms=DEBUG,捕获 DAO 层 SQL
  5. 浏览器缓存清空:Ctrl+F5 强制刷新,避免旧 JS/CSS 导致界面异常

从那以后我每次改struts.xml或web.xml,都强制走一遍这 5 步——去年有次误删了<filter-mapping>,靠第 2 步的lib_backup.zip5 分钟就恢复了全部依赖,没耽误联调。希望帮到你。

本文还有配套的精品资源,点击获取