简介:这是一套基于JavaEE企业级技术栈开发的完整图书管理系统源码,面向Java初学者与进阶开发者,用于深入理解MVC架构、分层设计及Web应用全链路开发实践。资源包含93个文件,涵盖21个Java业务类、11个JSP页面、10个XML配置文件(如struts.xml及多模块Action映射)、21个JAR依赖包(含Struts2、Hibernate、MySQL驱动等),以及CSS、JS、图片等前端资源,整体压缩包6.62MB,结构清晰体现标准Web项目布局(src、WebContent、WEB-INF、META-INF等)。已有146人学习下载,适合通过真实项目掌握Servlet/JSP基础、Struts2框架集成、数据库交互(lmsDB)、用户权限管理(sysadmin.jsp/login.jsp)及Eclipse/IDEA导入调试全流程。
1. 为什么一个“基于JavaEE的图书管理系统”至今仍是校招面试官手里的压轴题?
不是因为它多酷炫——它没有实时推荐、不接大模型、连扫码借阅都得手动输ISBN;而是因为,它像一把手术刀,能精准剖开一个应届生对JavaEE技术栈的真实掌控力:Servlet生命周期是否真懂?JSP里EL表达式和JSTL混用时作用域怎么塌缩?Tomcat部署时WEB-INF/classes和lib目录谁先加载?数据库连接池配了但没设validationQuery,查半天才发现是连接超时自动断开却没重连……这些细节藏在.zip包解压后的webapp/目录深处,藏在web.xml里那几行看似过时的 配置里,更藏在你第一次点击“添加图书”按钮后控制台刷出的NullPointerException堆栈最底层。这个项目不考你会不会调API,而考你敢不敢直面容器启动日志里那行红色的SEVERE: Error filterStart——它不拒绝你,但它会诚实记录你漏掉的每一个web.xml拼写错误、每一份忘记复制到WEB-INF/lib的jar包、每一次把MySQL驱动放错位置的徒劳重启。适合刚学完Servlet+JSP+JDBC三件套、正卡在“能写HelloWorld但跑不通登录页”阶段的同学;也适合想用最小闭环验证自己是否真吃透JavaEE分层结构的老手——毕竟,连图书增删改查都稳不住的系统,谈分布式事务就是空中楼阁。
2. 从.zip解压到Tomcat成功启动:四步走通JavaEE运行时环境
2.1 解压后第一眼该盯住哪三个目录?
拿到基于javaee的图书管理系统.zip,别急着双击IDE打开。先用命令行解压(避免Windows资源管理器解压时乱码):
unzip -q "基于javaee的图书管理系统.zip" ls -l # 你大概率会看到: # drwxr-xr-x 5 user user 160 Jun 12 10:23 src/ # drwxr-xr-x 3 user user 96 Jun 12 10:23 webapp/ # -rw-r--r-- 1 user user 24K Jun 12 10:23 lib/必须立刻确认的三个物理路径(缺一不可):
src/:Java源码根目录,里面应该有com.example.bookstore.servlet.*这类包结构,不是所有项目都用Maven,很多老项目直接扔.java文件在这里webapp/:Web资源根目录,重点检查是否存在WEB-INF/web.xml(没有它,Tomcat根本不知道这是个JavaEE应用)lib/:第三方jar包存放处,必须包含mysql-connector-java-5.1.47.jar或类似版本(注意:JavaEE 7规范要求容器提供JDBC驱动,但实际部署中99%项目仍需手动放jar)
提示:如果
webapp/下直接有.jsp文件(如index.jsp),说明项目用的是传统JSP+Servlet模式,而非Spring Boot;若看到pom.xml,则大概率是Maven项目,但标题明确写“基于JavaEE”,所以优先按非Maven方式处理。
2.2 Tomcat 8.5+ 环境配置:为什么vscode配置javaee语言环境总失败?
VS Code里装了Java Extension Pack,写了Servlet类却标红HttpServlet?不是插件问题,是JavaEE API未显式引入。JavaEE规范本身不提供实现,只定义接口,必须让编译器知道去哪里找javax.servlet.*这些类:
- 下载
tomcat-lib而非tomcat-bin:去Apache官网下载apache-tomcat-8.5.99.tar.gz(选8.5.x,兼容Java 8且保留完整JavaEE 7支持),解压后进入lib/目录 - 在VS Code中添加Java Build Path:
- 右键项目 →
Java: Configure Classpath→Add Library→User Library - 新建库名
Tomcat8.5-EE,添加以下4个jar(缺一不可):servlet-api.jar(核心Servlet接口)jsp-api.jar(JSP编译依赖)el-api.jar(EL表达式解析)jstl.jar(如果JSP里用了<c:forEach>等标签)
- 右键项目 →
# 验证是否生效:在任意Servlet里写 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getSession().setAttribute("test", "ok"); // 不再报红 }注意:不要用
javaee-api-7.0.jar这种纯接口jar——它会导致运行时报NoClassDefFoundError,因为缺少Tomcat的具体实现。必须用Tomcat自带的*-api.jar,它们是接口+空实现的混合体,专为编译期设计。
2.3 把项目塞进Tomcat:war包生成与手动部署的取舍
项目没提供pom.xml?那就放弃Maven打包,用最原始但最可控的方式:
# 进入项目根目录(含src/和webapp/的同级目录) mkdir -p target/bookstore/WEB-INF/{classes,lib} cp -r src/* target/bookstore/WEB-INF/classes/ cp -r webapp/* target/bookstore/ cp lib/*.jar target/bookstore/WEB-INF/lib/ # 手动补全web.xml(若缺失,用此最小模板) cat > target/bookstore/WEB-INF/web.xml << 'EOF' <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>BookStore</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> EOF # 生成war包(注意路径不能错!) cd target && zip -r bookstore.war bookstore/为什么不直接复制整个文件夹到webapps/?
Tomcat 8.5+默认启用autoDeploy="true",但遇到webapp/目录下有META-INF/MANIFEST.MF或WEB-INF/classes/为空时,会静默跳过部署。生成war包强制触发解压校验,失败时会在logs/catalina.out里打印清晰错误。
2.4 启动Tomcat并验证端口绑定
# 进入Tomcat/bin/,用调试模式启动(关键!) ./catalina.sh run -Djava.util.logging.config.file=../conf/logging.properties # Windows用户用 catalina.bat run观察控制台输出的三行黄金日志:
INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [...] bookstore.warINFO [main] org.apache.jasper.servlet.TldScanner.scanJars At least one JAR was scanned for TLDs(说明JSP引擎已加载)INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [...] bookstore.war has finished in [xxx] ms
如果卡在
Scanning JARs超过10秒,大概率是lib/里混入了spring-boot-starter-web.jar这类胖jar——JavaEE容器无法解析Spring Boot的嵌套jar结构,必须剔除。
3. 数据库连接池配置:为什么你填了URL密码还是连不上MySQL?
3.1 web.xml里藏着的连接池入口:context.xml vs web.xml之争
老项目常把数据库配置硬编码在Servlet里(Class.forName("com.mysql.jdbc.Driver")),但规范做法是交给容器管理。检查webapp/WEB-INF/下是否有context.xml:
<!-- webapp/WEB-INF/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/BookDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" minIdle="5" initialSize="5" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/bookdb?useSSL=false&serverTimezone=UTC" username="root" password="123456"/> </Context>关键点解析:
name="jdbc/BookDB":JNDI查找名,代码里用ctx.lookup("java:comp/env/jdbc/BookDB")获取maxTotal="20":连接池最大连接数,别设成100——MySQL默认max_connections=151,多个应用共用时会挤爆url中的&:XML实体转义,写成&会直接导致Tomcat启动失败(Parser Error)com.mysql.cj.jdbc.Driver:MySQL 8.0+驱动类名,若用旧版MySQL(5.7),需改为com.mysql.jdbc.Driver
3.2 Servlet中获取DataSource的正确姿势
// 在任意Servlet的doGet/doPost里 protected void doGet(HttpServletRequest req, HttpServletResponse resp) { try { Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/BookDB"); Connection conn = ds.getConnection(); // 此处可能抛SQLException // ...业务逻辑 conn.close(); // 必须close!否则连接永不归还池 } catch (NamingException | SQLException e) { e.printStackTrace(); // 开发期必须打全栈,生产环境记log resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } }注意:
conn.close()调用的是连接池的回收方法,不是真正关闭物理连接。若忘记close,池内连接耗尽后所有请求都会卡在ds.getConnection()这行,表现为页面白屏无日志。
3.3 MySQL 8.0+ 的血泪适配:时区与SSL的双重暴击
即使URL、账号密码全对,仍连不上?八成是MySQL 8.0的默认策略作祟:
| 现象 | 原因 | 解决方案 |
|---|---|---|
java.sql.SQLException: The server time zone value 'XXX' is unrecognized | MySQL 8.0默认时区为SYSTEM,JDBC驱动无法解析 | URL末尾加&serverTimezone=Asia/Shanghai |
java.sql.SQLException: Access denied for user 'root'@'localhost' | MySQL 8.0默认认证插件改为caching_sha2_password | 执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; |
java.sql.SQLException: Could not create connection to database server. | JDBC驱动版本与MySQL不匹配 | 用mysql-connector-java-8.0.28.jar(对应MySQL 8.0)或5.1.47.jar(兼容MySQL 5.7/8.0) |
-- 在MySQL命令行执行(替换你的密码) ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;4. 图书CRUD功能落地:从JSP表单到Servlet事务控制的完整链路
4.1 index.jsp里的表单陷阱:method="post"却没配web.xml映射
常见翻车点:点击“添加图书”按钮没反应,浏览器地址栏变成/bookstore/addBook?title=xxx——说明表单被当GET提交了:
<!-- webapp/index.jsp --> <form action="addBook" method="post"> <!-- 必须是post! --> <input type="text" name="title" required> <input type="submit" value="添加"> </form>但web.xml里没声明addBook这个Servlet:
<!-- webapp/WEB-INF/web.xml --> <servlet> <servlet-name>AddBookServlet</servlet-name> <servlet-class>com.example.bookstore.servlet.AddBookServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>AddBookServlet</servlet-name> <url-pattern>/addBook</url-pattern> <!-- 必须和form action完全一致 --> </servlet-mapping>注意:
<url-pattern>支持/addBook(精确匹配)、/add/*(路径匹配)、*.do(扩展名匹配),但不要用/addBook/*——这会导致/addBook/123也能命中,而你的Servlet没做路径参数解析。
4.2 AddBookServlet里的事务边界:为什么一条insert失败整条回滚?
protected void doPost(HttpServletRequest req, HttpServletResponse resp) { Connection conn = null; PreparedStatement ps = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 关键!开启事务 String sql = "INSERT INTO books(title, author, isbn) VALUES(?, ?, ?)"; ps = conn.prepareStatement(sql); ps.setString(1, req.getParameter("title")); ps.setString(2, req.getParameter("author")); ps.setString(3, req.getParameter("isbn")); ps.executeUpdate(); // 模拟另一个操作(如记录日志) ps = conn.prepareStatement("INSERT INTO logs(action) VALUES(?)"); ps.setString(1, "ADD_BOOK"); ps.executeUpdate(); conn.commit(); // 两个insert都成功才提交 resp.sendRedirect("listBooks.jsp"); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} // 必须rollback! } req.setAttribute("error", "添加失败:" + e.getMessage()); req.getRequestDispatcher("error.jsp").forward(req, resp); } finally { closeQuietly(ps, conn); // 自定义工具方法,避免finally里再抛异常 } }血泪经验:
conn.setAutoCommit(false)后,必须显式调用commit()或rollback()。若catch块里没rollback,连接归还池时状态未知,下次从池取连接可能带着未提交的脏数据。
4.3 listBooks.jsp的EL表达式避坑:requestScope vs pageScope
<!-- webapp/listBooks.jsp --> <c:forEach items="${requestScope.books}" var="book"> <tr> <td>${book.title}</td> <td>${book.author}</td> <td><a href="deleteBook?id=${book.id}">删除</a></td> </tr> </c:forEach>为什么${books}取不到值?
Servlet里是这样存的:req.setAttribute("books", bookList);—— 这个属性默认在requestScope,必须写${requestScope.books}。若写成${books},JSP会按顺序搜索page→request→session→application,但pageScope里没这个变量,最终返回空。
玄学调试法:在JSP顶部加
<c:out value="${requestScope.books}"/>,如果输出[com.example.bookstore.model.Book@123abc]说明对象存在,只是EL没遍历;如果输出空字符串,说明Servlet没成功setAttribute。
5. 避坑指南:那些让JavaEE新手debug到凌晨三点的致命细节
5.1 现象:Tomcat启动成功,但访问http://localhost:8080/bookstore显示404
原因:项目名不等于war包名。Tomcat把bookstore.war解压成bookstore/目录,但web.xml里<display-name>只影响管理界面显示,实际上下文路径由war包名决定。若你把war包命名为mybook.war,就必须访问/mybook。
解决:
- 方案1:重命名war包为
bookstore.war - 方案2:在
conf/server.xml的<Host>节点里加<Context path="/bookstore" docBase="mybook" /> - 方案3(推荐):启动时用
-Dcatalina.base指定独立工作目录,避免污染主Tomcat
5.2 现象:JSP里中文显示为????,数据库存进去也是乱码
原因:三处编码未统一:
- Tomcat:
conf/server.xml中<Connector>标签缺URIEncoding="UTF-8" - MySQL:建库时没指定
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - JSP页面:
<%@ page contentType="text/html;charset=UTF-8" %>漏写charset
解决:
<!-- conf/server.xml --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 这行必须加 -->5.3 现象:<c:forEach>遍历不出数据,但<%= request.getAttribute("books") %>能打印出对象地址
原因:JSTL标签库未正确声明。webapp/WEB-INF/web.xml里没配taglib,或JSP顶部漏了声明:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!-- 必须有! -->同时检查:lib/下是否有jstl.jar和standard.jar(JSTL 1.2需要这两个),缺一不可。
5.4 现象:删除图书后刷新页面,被删的书又回来了
原因:浏览器缓存了GET请求结果。点击删除链接<a href="deleteBook?id=123">是GET请求,Tomcat处理完后返回302重定向到listBooks.jsp,但浏览器可能从缓存读取旧页面。
解决:
- 方案1(立即生效):在
deleteBookServlet末尾加resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - 方案2(治本):把删除改成POST表单,用
<form method="post" action="deleteBook"><input type="hidden" name="id" value="${book.id}"/></form>
5.5 现象:ClassNotFoundException: com.mysql.jdbc.Driver,但jar明明在lib里
原因:Tomcat 8.5+的类加载机制变化。lib/下的jar只对当前应用可见,但com.mysql.jdbc.Driver需要被Tomcat的common类加载器识别(因为连接池初始化在容器启动时)。
解决:
- 将
mysql-connector-java-5.1.47.jar复制到$CATALINA_HOME/lib/(全局生效) - 或在
context.xml里用driverClassName="com.mysql.cj.jdbc.Driver"(MySQL 8.0+)
6. 让系统真正可用的三个进阶技巧:从能跑通到可交付
6.1 给图书列表加搜索框:不用框架的原生AJAX实践
与其重写整个JSP,不如用最轻量的原生fetch改造搜索:
<!-- webapp/listBooks.jsp --> <input type="text" id="searchInput" placeholder="输入书名搜索..." oninput="searchBooks()"> <div id="bookList"></div> <script> function searchBooks() { const keyword = document.getElementById('searchInput').value; fetch('searchBooks?keyword=' + encodeURIComponent(keyword)) .then(r => r.text()) .then(html => document.getElementById('bookList').innerHTML = html); } // 页面加载时初始化 window.onload = () => searchBooks(); </script>对应的SearchBooksServlet只需返回HTML片段:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) { String keyword = req.getParameter("keyword"); List<Book> books = bookDao.searchByTitle(keyword); // 你的DAO方法 StringBuilder html = new StringBuilder(); for (Book b : books) { html.append("<tr><td>").append(b.getTitle()) .append("</td><td>").append(b.getAuthor()) .append("</td></tr>"); } resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write(html.toString()); }这样做的好处:零额外依赖、SEO友好(服务端渲染)、响应快(只传表格HTML,非JSON)。比引入jQuery或Vue简单十倍,且完全兼容JavaEE容器。
6.2 日志分级:把System.out.println换成真正的日志门面
System.out.println("DEBUG: add book " + title)在生产环境是灾难。换成SLF4J(Tomcat自带):
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AddBookServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(AddBookServlet.class); protected void doPost(...) { log.debug("开始添加图书:{}", title); // {}占位符避免字符串拼接 try { // ...业务逻辑 log.info("图书添加成功,ID={}", bookId); } catch (Exception e) { log.error("添加图书失败", e); // 自动带堆栈 } } }配置文件conf/logging.properties关键行:
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = java.util.logging.ConsoleHandler6.3 防止SQL注入的最后一道防线:PreparedStatement参数化
哪怕你坚信用户只会输正常书名,也要把所有外部输入塞进?占位符:
// ❌ 危险!拼接SQL String sql = "SELECT * FROM books WHERE title LIKE '%" + keyword + "%'"; // ✅ 安全!预编译 String sql = "SELECT * FROM books WHERE title LIKE ?"; ps.setString(1, "%" + keyword + "%"); // 注意:通配符%要写在参数值里,不在SQL里为什么?能防注入?
PreparedStatement会把?参数当作纯数据发送给MySQL,MySQL绝不会把它当SQL语法解析。即使keyword是' OR '1'='1,最终执行的也是:
SELECT * FROM books WHERE title LIKE '%\' OR \'1\'=\'1%'——一个毫无危害的字符串匹配。
我带过的实习生里,有3个人在addBook里漏了ps.setString()的类型转换(比如把int型id用setString()传),导致MySQL报Data truncation却死活找不到原因。后来我们定下铁律:所有PreparedStatement参数必须用setXxx()方法,且类型与数据库字段严格对应;宁可多写一行Integer.parseInt(),也不图省事用setObject()。这套流程跑下来,一个原本要debug两天的图书系统,现在新人3小时就能从解压部署到增删改查全通。希望帮到你。
本文还有配套的精品资源,点击获取