Javaweb图书管理系统实战:从建表到分页事务的完整开发指南 简介这份资源是一套面向Java Web初学者与在校学生的图书管理系统实战项目源自大二上学期期末大作业适合作为SSM/MyBatis入门练手与课程设计参考。项目围绕书籍、学生、管理员、留言四类实体展开涵盖借书还书日志记录、书籍状态流转未借出、已借出、续借、逾期、丢失、损毁等、信誉等级与借阅数量控制等业务逻辑并完整实现了学生注册、学生登录、管理员登录三条流程涉及Ajax异步校验、正则表达式验证、Cookie加密记住账号、Session记录用户信息等常见Web技能点。资源包共59个文件包含26个Java源码、13个HTML页面、7个XML配置、3个JSP、2个JS脚本以及SQL建表文件等压缩包约879KB结构清晰便于按模块阅读。目前已有1915人学习下载可帮助读者快速理解Java Web项目的分层组织、数据库设计与前后端交互方式适合对照源码梳理完整开发思路。1. 大二期末的图书管理系统为什么它是 Javaweb 练手的最优解很多同学到大二上学期刚学完 Servlet、JSP、JDBC 和一点 MVC 分层老师就扔下一句“期末大作业做个图书管理系统”。第一反应往往是去搜“javaweb项目完整案例mysql”然后找到一个压缩包改个包名就交上去。但如果你真想把这次作业变成简历上能讲、面试能答的东西图书管理系统恰恰是 Javaweb 练手项目里最合适的那个——它业务闭环短、实体关系清晰、增删改查覆盖全而且天然能塞进分页、登录鉴权、事务这些面试必问的点。这篇文章不聊虚的就按一个真实大二学生的视角从建库建表、搭 Servlet 分层、写 JSP 页面到用 IDEA 跑起来、调通数据库连接、处理中文乱码和 404一步步把“图书管理系统”这个 Javaweb 项目做出来。适合刚学完基础、想自己动手写一遍而不是复制粘贴的同学也适合已经抄完但被老师追问“你这分页怎么实现的”时答不上来的同学。下面所有代码和配置都是我在自己机器上跑通过的你照着改改就能用。2. 动手之前把数据库和项目骨架定下来2.1 图书管理系统到底需要几张表别急着打开 IDEA 新建项目先把数据模型想清楚。一个能交作业、也能讲清楚的图书管理系统最少需要三张表用户表、图书表、借阅记录表。用户表管登录和权限图书表管库存借阅记录表管谁借了哪本书、还没还。-- 建库字符集用 utf8mb4不然后面中文书名会乱码 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 用户表区分管理员和学生 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role VARCHAR(20) DEFAULT student -- admin 或 student ); -- 图书表stock 表示可借库存 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), stock INT DEFAULT 0 ); -- 借阅记录表return_date 为空表示未归还 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, book_id INT, borrow_date DATE, return_date DATE, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) ); -- 插一条管理员账号密码先明文后面说怎么改 INSERT INTO user(username, password, role) VALUES(admin, 123456, admin);这三张表的关系是一个用户能有多条借阅记录一本书也能出现在多条借阅记录里。借书时往 borrow_record 插一条同时把 book.stock 减一还书时更新 return_date再把 stock 加回去。这个逻辑后面写 Service 层时会反复用到。注意外键约束在练习阶段可以加方便你理解关系但如果你后面想用连接池或者批量操作外键有时会拖慢速度作业级别无所谓。2.2 IDEA 里建 Javaweb 项目的正确姿势热搜里“idea运行javaweb项目配置”是很多人卡住的地方。我一般用 Maven 建项目因为依赖好管不用手动往 WEB-INF/lib 里扔 jar 包。打开 IDEAFile → New → Project选 Maven勾选 Create from archetype找到org.apache.maven.archetypes:maven-archetype-webapp。填好 GroupId 和 ArtifactId比如com.library和library-system。建完之后目录结构应该是这样library-system ├── src │ └── main │ ├── java 自己建放 Servlet 和 Dao │ ├── resources 自己建放 db.properties │ └── webapp │ ├── WEB-INF │ │ └── web.xml │ └── index.jsp └── pom.xml如果 java 和 resources 目录没有右键 main 文件夹手动新建然后 Mark Directory as → Sources Root / Resources Root。2.3 pom.xml 里必须加的三个依赖Servlet、JSP、MySQL 驱动这三个缺一不可。Servlet 和 JSP 用 provided 作用域因为 Tomcat 自带MySQL 驱动用 compile打包时要带上。dependencies !-- Servlet APITomcat 提供打包时不带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样 provided -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动注意版本和你本地 MySQL 匹配 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies参数说明javax.servlet-api的 4.0.1 对应 Tomcat 9如果你用 Tomcat 10包名会变成 jakarta.servlet这个坑后面避坑章节会细说。MySQL 驱动 8.0.28 对应 MySQL 8.x连接 URL 要加时区和 SSL 参数否则启动就报错。3. 从 JDBC 工具类到 Servlet 分层把增删改查写扎实3.1 一个能复用的 JDBC 工具类很多同学每个 Servlet 里都写一遍DriverManager.getConnection代码重复不说改数据库密码时要改十几个文件。正确做法是抽一个 DBUtil 类用静态代码块加载驱动提供获取连接和关闭资源的方法。package com.library.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明静态代码块只在类加载时执行一次驱动注册一次就够。URL 里的serverTimezoneAsia/Shanghai是 MySQL 8 必须的不加会报时区错误useSSLfalse关掉 SSL 警告characterEncodingutf8保证中文不乱码。关闭资源时先关 ResultSet 再关 PreparedStatement 再关 Connection顺序不能反。3.2 图书查询与分页的 Dao 写法Dao 层只负责和数据库打交道不掺业务逻辑。图书列表要分页核心 SQL 是LIMIT加两个参数起始索引和每页条数。package com.library.dao; import com.library.util.DBUtil; import java.sql.*; import java.util.*; public class BookDao { // 分页查询page 从 1 开始size 是每页条数 public ListMapString, Object findBooksByPage(int page, int size) { ListMapString, Object list new ArrayList(); String sql SELECT * FROM book LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); // 起始索引 ps.setInt(2, size); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { MapString, Object book new HashMap(); book.put(id, rs.getInt(id)); book.put(name, rs.getString(name)); book.put(author, rs.getString(author)); book.put(stock, rs.getInt(stock)); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 统计总条数用于算总页数 public int countBooks() { String sql SELECT COUNT(*) FROM book; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } return 0; } }参数说明(page - 1) * size是 MySQL 分页的固定公式page1 时从第 0 条开始取。try-with-resources会自动关闭 Connection 和 PreparedStatement比手动 close 更安全。返回 List是为了简单实际项目里应该建 Book 实体类但作业阶段这样写能省不少代码。3.3 借书还书里的事务控制借书不是单纯插一条记录还要扣库存。这两步必须在一个事务里否则插了记录但库存没扣数据就乱了。public boolean borrowBook(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步检查库存 String checkSql SELECT stock FROM book WHERE id ? FOR UPDATE; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs checkPs.executeQuery(); if (!rs.next() || rs.getInt(stock) 0) { conn.rollback(); return false; } // 第二步扣库存 String updateSql UPDATE book SET stock stock - 1 WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); updatePs.executeUpdate(); // 第三步插借阅记录 String insertSql INSERT INTO borrow_record(user_id, book_id, borrow_date) VALUES(?, ?, CURDATE()); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, userId); insertPs.setInt(2, bookId); insertPs.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(); } } }逻辑说明setAutoCommit(false)关闭自动提交后面所有操作都在同一个事务里。FOR UPDATE是行锁防止两个人同时借最后一本书时超借。任何一步抛异常就 rollback全部成功才 commit。finally 里恢复自动提交并关闭连接避免连接池里的连接状态被污染。4. JSP 页面与 Servlet 交互让数据真正显示出来4.1 图书列表页怎么接收分页参数Servlet 负责调 Dao 拿数据把结果塞进 request然后转发给 JSP。分页参数从 URL 的?page1里取没传就默认第一页。WebServlet(/bookList) public class BookListServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int size 5; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } ListMapString, Object books bookDao.findBooksByPage(page, size); int total bookDao.countBooks(); int totalPages (int) Math.ceil((double) total / size); req.setAttribute(books, books); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/bookList.jsp).forward(req, resp); } }参数说明size5是每页显示 5 条你可以改成 10。totalPages用总条数除以每页条数向上取整比如 12 条数据、每页 5 条就是 3 页。forward是服务器内部跳转浏览器地址栏不变适合传数据。4.2 JSP 里用 JSTL 遍历数据并生成分页链接JSP 里别写 Java 代码块用 JSTL 和 EL 表达式页面干净也好维护。需要引入 jstl 的 jar 包Maven 里加javax.servlet:jstl:1.2。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle图书列表/title/head body h2图书列表/h2 table border1 cellpadding8 trthID/thth书名/thth作者/thth库存/thth操作/th/tr c:forEach items${books} varbook tr td${book.id}/td td${book.name}/td td${book.author}/td td${book.stock}/td td c:if test${book.stock 0} a hrefborrow?bookId${book.id}借阅/a /c:if c:if test${book.stock 0}已借完/c:if /td /tr /c:forEach /table !-- 分页链接 -- div 第 ${currentPage} 页 / 共 ${totalPages} 页 c:if test${currentPage 1} a hrefbookList?page${currentPage - 1}上一页/a /c:if c:if test${currentPage totalPages} a hrefbookList?page${currentPage 1}下一页/a /c:if /div /body /html逻辑说明c:forEach遍历 request 里的 books 列表${book.stock 0}判断库存决定显示借阅链接还是“已借完”。分页链接用${currentPage - 1}和${currentPage 1}动态生成EL 表达式里可以直接做算术运算。注意 JSP 顶部一定要写contentTypetext/html;charsetUTF-8否则中文会变成问号。4.3 登录过滤器与 Session 管理不是所有页面都能随便访问借书、还书、后台管理必须登录。用 Filter 拦截检查 Session 里有没有用户信息。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录接口、静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(login.jsp); } else { chain.doFilter(req, resp); } } }参数说明WebFilter(/*)拦截所有请求。放行列表里加上登录页和登录 Servlet否则会死循环。Session 里存的 user 对象在登录成功时request.getSession().setAttribute(user, user)设置。sendRedirect是客户端跳转地址栏会变适合未登录时踢回登录页。5. 避坑与排查那些让我熬夜的报错5.1 中文乱码从数据库到页面的全链路排查现象图书列表里书名显示成????或者书å。原因字符集在某一环没统一。解决数据库建库用 utf8mb4连接 URL 加characterEncodingutf8JSP 页面contentType设 UTF-8Servlet 里request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)都要写。四个地方缺一个都会乱码我一般建个项目模板把这四行固定下来。5.2 404Servlet 路径和 web.xml 版本对不上现象访问bookList报 404。原因要么WebServlet(/bookList)的路径和浏览器里输的不一致要么 web.xml 的metadata-completetrue导致注解不生效。解决检查注解路径确保 web.xml 里web-app标签的 version 是 4.0 且没有metadata-completetrue。如果用了 Tomcat 10Servlet 包名要从javax.servlet改成jakarta.servlet否则类加载失败也是 404。5.3 数据库连接报时区错误现象启动后第一次查数据库就抛The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8 的驱动需要明确时区。解决URL 里加serverTimezoneAsia/Shanghai。如果还报 SSL 警告再加useSSLfalse。这两个参数是 MySQL 8 连接字符串的标配建议直接写进 DBUtil 的 URL 常量里。5.4 借书时库存扣成负数现象两个人同时借最后一本书库存变成 -1。原因先查库存再扣减两步之间没有锁。解决用SELECT ... FOR UPDATE在事务里锁住那一行或者直接用UPDATE book SET stock stock - 1 WHERE id ? AND stock 0根据返回的受影响行数判断是否成功。后者更简单推荐作业阶段用。5.5 JSP 页面修改后不生效现象改了 JSP 代码刷新浏览器还是旧页面。原因Tomcat 缓存了编译后的 JSP。解决在 Tomcat 配置里把development设为true或者每次改完点 IDEA 的 Redeploy。更彻底的办法是改 JSP 后按CtrlF9重新构建再重启 Tomcat。这个坑不致命但很烦习惯就好。6. 进阶技巧把作业变成能讲的项目6.1 用连接池替换 DriverManagerDBUtil 里每次DriverManager.getConnection都新建物理连接性能差。换成 Druid 或 HikariCP作业里加个 Druid 依赖配一个druid.properties代码几乎不用改。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize5 maxActive10然后在 DBUtil 静态块里用DruidDataSourceFactory.createDataSource(properties)创建 DataSourcegetConnection改成dataSource.getConnection()。这样连接复用面试时也能讲“我用了连接池优化数据库访问”。6.2 密码明文改哈希user 表里密码现在是明文登录时直接比对字符串。改成 MD5 或 SHA-256 哈希注册时存哈希值登录时把输入的密码哈希后再比对。import java.security.MessageDigest; public static String sha256(String input) throws Exception { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); }登录 Servlet 里把user.getPassword().equals(password)改成user.getPassword().equals(sha256(password))。这一步改动不大但能让老师看到你有安全意识。6.3 用 Postman 或浏览器直接测接口别每次都点页面测效率低。Servlet 的 GET 接口可以直接在浏览器地址栏输 URL 测比如http://localhost:8080/library-system/bookList?page1。POST 接口用 Postman选 x-www-form-urlencoded填参数。测的时候打开 IDEA 控制台看 SQL 有没有打印我一般在 Dao 里加一句System.out.println(sql)确认参数拼对了。6.4 项目结构分层回顾最后把整个项目的包结构理一遍面试时能画出来就赢了包名职责关键类com.library.util工具类DBUtilcom.library.entity实体类User、Bookcom.library.dao数据访问BookDao、UserDaocom.library.service业务逻辑BorrowServicecom.library.servlet控制层BookListServlet、LoginServletcom.library.filter过滤器LoginFilter这个分层不是摆设它让你在改需求时知道该动哪一层。比如要加“续借”功能Service 层加方法Servlet 加接口JSP 加按钮Dao 不用动。我自己的习惯是每做完一个模块就用 Git 提交一次commit message 写清楚“完成图书分页查询”或“修复借书事务回滚”。这样万一改崩了还能回退也逼自己把功能拆小。大二这个阶段能把一个图书管理系统从建表写到分页、事务、过滤器已经比大多数同学强了。别怕报错那些 404 和乱码都是必经之路。希望帮到你。本文还有配套的精品资源点击获取