
简介这是一套基于JavaWeb、JSP、Tomcat与MySQL实现的图书管理系统完整源码面向计算机相关专业正在做毕设的学生以及需要项目实战练习的Java学习者可直接作为毕业设计或课程设计参考。资源包共202个文件以50个jsp页面、33个java源文件与33个class编译文件为核心辅以15个jar依赖、11个js脚本、7个css样式及图书封面等gif、jpg图片另含数据库sql脚本与xml、properties配置文件压缩包约4.57MB结构完整、层次清晰。系统涵盖管理员设置、读者类型与档案管理、图书类型与档案管理、图书借阅续借归还、借阅到期提醒及口令修改等模块并配有BorrowDAO、BookDAO、ReaderDAO等数据访问层实现便于理解分层设计与业务逻辑。项目经过严格调试可运行于Tomcat7及以上、MySQL5及以上环境配合Eclipse或IDEA与Navicat即可快速部署。目前已有241人学习下载适合希望掌握JavaWeb开发流程与数据库交互的读者参考借鉴。1. 图书管理系统这套 JavaWeb 老架构为什么还值得动手跑一遍很多人第一次接触 JavaWeb 项目都是从图书管理系统开始的。它足够小业务闭环完整——登录、图书增删改查、借阅归还、用户管理一个都不少又足够典型JSP Servlet Tomcat MySQL 这套组合把「请求怎么进来、页面怎么渲染、数据怎么落库」这条链路完整暴露在你面前。你不需要先啃完 Spring 全家桶才能看懂它打开一个.jsp文件就能顺着往下追。但真正让这套东西值得跑一遍的原因不是「简单」而是「透明」。用框架时一个注解背后发生了什么你未必清楚而在这套架构里web.xml里配了什么、request怎么转发、session存在哪、JDBC 连接什么时候关全是你自己写的出问题也全是你自己能查的。对于想搞明白 Web 请求生命周期、想自己搭一个能跑起来的小系统、或者课程设计需要交一份完整可运行代码的人来说这套组合依然是性价比最高的起点。这篇文章不讲空泛概念只讲怎么把它从零跑起来环境怎么配、数据库怎么建、JSP 页面怎么组织、Tomcat 怎么部署、报错怎么排。新手能照着一步步走通熟手能对照着检查自己的配置哪里埋了雷。2. 环境搭建JDK、Tomcat、MySQL 三件套的版本对齐2.1 为什么版本对齐比装最新版更重要这套技术栈最大的坑不在代码在版本。JSP 编译依赖 JDKTomcat 的运行又依赖 JDK 版本MySQL 的驱动包mysql-connector-java又和 MySQL 服务端版本强相关。三者只要有一个错位你看到的报错往往和真实原因隔了十万八千里。我一般会锁定一套经过大量项目验证的组合JDK 8 Tomcat 8.5 MySQL 5.7 或 8.0。JDK 8 是这套老架构最稳的底座Tomcat 8.5 对 JDK 8 支持成熟MySQL 5.7 和 8.0 都能用区别主要在驱动包和连接串参数上。如果你手头是 JDK 17 甚至更高Tomcat 8.5 启动时可能直接抛UnsupportedClassVersionError这不是代码问题是字节码版本对不上。组件推荐版本说明JDK81.8老架构最稳避免高版本模块化带来的反射限制Tomcat8.5.x与 JDK 8 匹配web.xml规范兼容性好MySQL5.7.44 或 8.0.x5.7 驱动用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver驱动包与 MySQL 对应8.0 必须用 8.x 驱动否则时区报错提示不要盲目追新。JDK 25 配 Tomcat 8.5 这种组合光是启动阶段的类加载就能让你怀疑人生没必要给自己找麻烦。2.2 Tomcat 安装与 IDEA 集成配置Tomcat 本质是一个 Servlet 容器你写的 JSP 和 Servlet 最终都要靠它编译、加载、执行。安装方式有两种下载压缩包解压绿色版或者用安装程序。我推荐压缩包目录干净、好迁移。下载后解压到没有中文和空格的路径比如D:\dev\tomcat8.5。然后配置环境变量CATALINA_HOME指向这个目录。验证是否成功进bin目录执行启动脚本# Windows 下启动 Tomcat cd D:\dev\tomcat8.5\bin startup.bat # Linux / macOS 下启动 cd /opt/tomcat8.5/bin ./startup.sh启动后浏览器访问http://localhost:8080能看到 Tomcat 默认页就说明容器本身没问题。如果端口被占用改conf/server.xml里的Connector port如果闪退多半是JAVA_HOME没配好去看logs/catalina.out。在 IDEA 里集成 Tomcat 的步骤Run→Edit Configurations→ 加号选Tomcat Server→Local→ 在Application server里指向你的 Tomcat 目录 → 在Deployment标签页把项目的war或war exploded加进去。war exploded支持热更新改 JSP 不用重启调试阶段优先用它。2.3 MySQL 建库建表与连接串参数数据库这块先建库再建表。字符集统一用utf8mb4否则中文书名、作者名容易变问号。下面是一份可直接执行的建表脚本覆盖图书、用户、借阅记录三张核心表-- 创建数据库字符集用 utf8mb4 支持完整中文和特殊符号 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) COMMENT 作者, publisher VARCHAR(100) COMMENT 出版社, stock INT DEFAULT 0 COMMENT 库存, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑说明book表用stock字段控制库存借书时减一、还书时加一这是最朴素的库存模型borrow_record用status区分借出和归还return_date为空表示未还。参数上AUTO_INCREMENT让主键自增UNIQUE约束保证用户名不重复ENGINEInnoDB是为了支持事务——借书时「扣库存」和「插记录」必须在一个事务里否则会出现库存扣了但记录没写进去的脏数据。连接串是另一个高频翻车点。MySQL 8.0 必须显式指定时区和 SSL 参数# MySQL 8.0 连接配置 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码serverTimezone不写会报时区错误useSSLfalse避免本地开发时的 SSL 握手警告allowPublicKeyRetrievaltrue是 8.0 新认证插件的要求。如果你用的是 5.7驱动类名换成com.mysql.jdbc.Driver连接串可以简化但加上这些参数也不会有副作用。3. 从请求到页面JSP Servlet 的完整链路怎么串3.1 分层结构为什么不能把 SQL 写进 JSP新手最容易犯的错是在 JSP 页面里直接写 JDBC 代码查数据库。这样写能跑但后患无穷页面和数据库耦合改一个字段要翻遍所有 JSPSQL 散落各处注入风险高调试时你分不清是页面渲染错了还是查询错了。正确的做法是分层JSP 只负责展示Servlet 负责接收请求和调度DAO 负责数据库操作实体类负责数据承载。一个典型的目录结构是这样的src/ com/library/ entity/ Book.java, User.java dao/ BookDao.java, UserDao.java servlet/ BookServlet.java, LoginServlet.java util/ DBUtil.java WebContent/ jsp/ bookList.jsp, login.jsp WEB-INF/ web.xml css/ js/DBUtil统一管理连接获取和释放DAO 里只写 SQLServlet 里只做参数解析和页面跳转。这样任何一层出问题你都能快速定位。3.2 一个完整的查询链路从浏览器到 JSP 渲染以「图书列表」为例走一遍完整链路。用户访问/bookListTomcat 根据web.xml或注解找到对应的 ServletServlet 调用 DAO 查数据把结果放进request转发给 JSPJSP 遍历渲染成 HTML。先看 Servlet// BookServlet.java —— 处理图书列表请求 WebServlet(/bookList) public class BookServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 调用 DAO 查询全部图书 ListBook books bookDao.findAll(); // 2. 把数据放进 request 作用域供 JSP 读取 req.setAttribute(bookList, books); // 3. 转发到 JSP 页面地址栏不变属于服务器内部跳转 req.getRequestDispatcher(/jsp/bookList.jsp).forward(req, resp); } }逻辑说明WebServlet(/bookList)是注解方式注册等价于在web.xml里配url-pattern。forward是转发请求对象还在request里放的数据 JSP 能拿到如果用sendRedirect重定向就是两次请求数据会丢。参数上req.setAttribute的 key 要和 JSP 里EL表达式取的名字一致这里都是bookList。再看 DAO// BookDao.java —— 图书数据访问 public class BookDao { public ListBook findAll() { ListBook list new ArrayList(); String sql SELECT id, book_name, author, publisher, stock FROM book ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setStock(rs.getInt(stock)); list.add(b); } } catch (SQLException e) { e.printStackTrace(); } return list; } }这里用PreparedStatement而不是Statement是为了防 SQL 注入用 try-with-resources 自动关闭连接、语句和结果集避免连接泄漏——连接池被耗尽是这套架构最常见的线上故障。ORDER BY id DESC让新书排前面属于产品细节但用户能感知到。最后是 JSP 渲染%-- bookList.jsp —— 图书列表展示 --% % page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle图书列表/title/head body table border1 trthID/thth书名/thth作者/thth出版社/thth库存/th/tr c:forEach items${bookList} varbook tr td${book.id}/td td${book.bookName}/td td${book.author}/td td${book.publisher}/td td${book.stock}/td /tr /c:forEach /table /body /htmlc:forEach是 JSTL 标签比在 JSP 里写 Java 脚本片段% %干净得多。${book.bookName}走的是实体类的 getter所以实体类里必须有getBookName()。注意 JSP 顶部要声明contentType和charset否则中文乱码。3.3 借阅功能里的事务处理借书这个动作涉及两张表book减库存borrow_record插记录。这两步必须原子化否则会出现「库存扣了但没借阅记录」或者反过来。JDBC 默认自动提交要手动控制事务// BorrowService.java —— 借书事务 public boolean borrow(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 检查库存 String checkSql SELECT stock FROM book WHERE id ? FOR UPDATE; PreparedStatement check conn.prepareStatement(checkSql); check.setInt(1, bookId); ResultSet rs check.executeQuery(); if (!rs.next() || rs.getInt(stock) 0) { conn.rollback(); return false; } // 2. 扣库存 String updateSql UPDATE book SET stock stock - 1 WHERE id ?; PreparedStatement update conn.prepareStatement(updateSql); update.setInt(1, bookId); update.executeUpdate(); // 3. 插入借阅记录 String insertSql INSERT INTO borrow_record(user_id, book_id, status) VALUES(?, ?, 0); PreparedStatement insert conn.prepareStatement(insertSql); insert.setInt(1, userId); insert.setInt(2, bookId); insert.executeUpdate(); conn.commit(); // 三步都成功才提交 return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }关键点在SELECT ... FOR UPDATE它给这行记录加排他锁防止两个用户同时借最后一本书时都读到stock1然后各扣一次变成 -1。setAutoCommit(false)到commit()之间就是一个事务边界任何一步抛异常都走rollback()。参数上FOR UPDATE必须在事务里才生效如果你忘了关自动提交这行锁会立刻释放等于没加。4. 部署与调试IDEA 跑通到 war 包上 Tomcat 的差异4.1 war exploded 与 war 包的区别在 IDEA 里跑项目默认用的是war exploded也就是把编译后的classes、lib、jsp按目录结构展开Tomcat 直接指向这个目录。好处是改 JSP 立即生效改 Java 类重新编译即可不用打包。但war exploded和最终部署的war包行为有差异常见的有两类一是路径问题。war exploded下你的项目根路径可能是/library_war_exploded而war包部署后是/library。如果代码里写了硬编码的绝对路径比如/library_war_exploded/jsp/xxx换环境就 404。正确做法是用request.getContextPath()动态取上下文路径。二是资源加载。war exploded下WEB-INF/classes是散开的war包里是压缩的用getResourceAsStream读配置文件时路径写法要一致统一用类加载器从 classpath 读。打包命令很简单IDEA 里Build→Build Artifacts→Build产物在out/artifacts/下。把war丢进 Tomcat 的webapps目录启动后 Tomcat 会自动解压部署。4.2 部署后 404 和 500 的排查顺序部署到独立 Tomcat 后最常见的两个错误是 404找不到和 500服务器内部错误。排查要有顺序不要瞎改。404 的排查链先确认war是否解压成功看webapps下有没有对应目录→ 再看访问路径是否带了正确的 context path → 然后检查web.xml里url-pattern和实际请求是否匹配 → 最后看WEB-INF/lib下依赖 jar 是否齐全。很多时候 404 是因为war包里漏了libIDEA 打包时没把依赖加进去。500 的排查链直接看logs/catalina.out或logs/localhost.log异常堆栈会告诉你根因。高频原因有三个数据库连不上驱动类名错、连接串错、MySQL 没启动、空指针request.getParameter返回 null 没判空、类找不到ClassNotFoundException通常是 jar 没打进WEB-INF/lib。注意Tomcat 后台管理页面上传 war 被限制 IP 是常见现象manager应用默认只允许本机访问。改conf/tomcat-users.xml加角色再改webapps/manager/META-INF/context.xml里的RemoteAddrValve。但生产环境不建议开放远程部署本地调试用文件拷贝更省事。4.3 中文乱码的三处来源中文乱码是这套架构的经典问题来源有三处要分别处理。第一处是请求参数POST 请求要在读参数前设request.setCharacterEncoding(UTF-8)GET 请求在 Tomcat 8 以后默认 URI 编码是 UTF-8但老版本要在server.xml的Connector上加URIEncodingUTF-8。第二处是响应输出response.setContentType(text/html;charsetUTF-8)。第三处是数据库建库建表和连接串都要utf8mb4。三处只要有一处没对上中文就会变问号或乱码。排查时按「请求进来 → 业务处理 → 响应出去 → 落库」的顺序逐段验证别一上来就改代码。5. 避坑与常见问题那些让我加班到凌晨的报错5.1 驱动类加载失败ClassNotFoundException现象启动或首次查询时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因mysql-connector-java的 jar 没放进WEB-INF/lib或者只加进了 IDEA 的库但没随war打包。另一个常见原因是 MySQL 8.0 用了 5.7 的驱动类名。解决确认WEB-INF/lib下有对应版本的驱动 jar8.0 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver。IDEA 里在Project Structure→Artifacts中把驱动库加到WEB-INF/lib输出目录。5.2 时区与 SSL 报错现象连接 MySQL 8.0 时报The server time zone value ... is unrecognized或 SSL 警告。原因8.0 驱动要求显式指定时区且默认尝试 SSL 连接。解决连接串加serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。这三个参数是 8.0 的标配缺一个都可能报错。5.3 JSP 页面修改不生效现象改了 JSP刷新浏览器还是旧内容。原因Tomcat 对 JSP 有编译缓存或者你部署的是war包而非war exploded改的是源文件但运行的是解压后的旧文件。解决调试阶段用war exploded如果坚持用war改完要重新打包部署。也可以在server.xml的Context上加reloadabletrue但生产环境别开性能损耗大。5.4 连接池耗尽Too many connections现象运行一段时间后报Too many connections或响应越来越慢。原因DAO 里拿了Connection没关或者异常路径下没走finally关闭。每次请求泄漏一个连接很快就把 MySQL 的最大连接数占满。解决所有数据库操作统一用 try-with-resources或者用DBUtil封装关闭逻辑。更规范的做法是引入连接池如 Druid、HikariCP但即便用连接池借出的连接也必须归还否则池子一样会空。5.5 转发与重定向用混导致数据丢失现象登录成功后跳转但下一个页面拿不到用户信息。原因用了sendRedirect重定向这是两次独立请求request作用域的数据不会保留。解决需要传递request数据时用forward需要防止表单重复提交、或者跳转到外部地址时用sendRedirect。登录成功后如果要把用户信息带到后续页面放session而不是request。6. 把这套系统改造成能写进简历的样子跑通只是起点。如果你想让这个项目在面试或课程设计里拿得出手得在它基础上做几处「有含量」的改造而不是停留在增删改查。第一处是引入连接池。手写DBUtil每次DriverManager.getConnection都要建立 TCP 连接性能差。换成 Druid 或 HikariCP配置最小/最大连接数、超时时间这是生产环境的基本功。改造后你要能说清楚为什么连接池能提升性能、连接数怎么定、连接泄漏怎么监控。第二处是加一层过滤器做权限控制。现在每个 Servlet 都要自己判断session里有没有用户重复且容易漏。写一个Filter拦截/admin/*路径统一校验登录态和角色这是 JavaWeb 里最实用的设计模式之一。// AuthFilter.java —— 统一登录校验 WebFilter(/admin/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); // session 为空或没有用户信息说明未登录 if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /jsp/login.jsp); return; } chain.doFilter(req, resp); // 已登录放行 } }WebFilter(/admin/*)表示只拦截管理后台路径getSession(false)表示不存在就不新建避免给未登录用户创建无用 session。这个过滤器一加所有后台 Servlet 里的登录判断都能删掉。第三处是把 SQL 集中管理并加索引。现在 SQL 散在 DAO 里改字段要全局搜。可以抽到常量类或配置文件。另外给borrow_record的user_id、book_id加索引数据量上来后查询速度差别很明显。你可以自己造几千条测试数据用EXPLAIN对比加索引前后的执行计划这个对比过程本身就是很好的面试素材。验证改造是否成功有个简单办法用 JMeter 或写个循环脚本并发请求借书接口观察连接池活跃连接数、响应时间、有没有超卖。如果并发下库存从 1 变成 -1说明事务隔离级别或锁没配对回去检查FOR UPDATE和事务边界。我自己的习惯是每改一处就写一条测试用例固定住行为尤其是事务和权限这种容易回归的地方。这套老架构没有框架帮你兜底好处是每一行逻辑你都看得见坏处是每一个疏忽都会变成线上事故。把它跑通、改好、压一遍你对 Web 系统的理解会比只调框架 API 扎实得多。希望帮到你。本文还有配套的精品资源点击获取