ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于JavaWeb的图书管理系统:从三层架构到借阅事务实现详解

基于JavaWeb的图书管理系统:从三层架构到借阅事务实现详解 简介一份基于JavaWeb的图书管理系统课程设计文档结合JavaWeb、HTML/CSS与SQL Server技术面向需要完成图书管理类课设或入门JavaWeb项目的在校学生。系统完整设计了登录、查询、借还、管理四个子系统管理员可对图书和读者进行增删改查普通用户可进行图书检索、借阅与归还并设有超期未还限制借书的规则数据库部分绘制了“书籍—书库”存放关系与“用户—书籍”借阅关系两类E-R图并转换为用户表、书籍表、借阅记录表等关系模式覆盖了从需求分析、概念设计到关系模式转换的完整过程。资源包共1个docx文件约12.82MB文档内包含源代码说明、文件目录截图和E-R关系图并附有功能层次图与实现步骤便于对照代码与报告梳理流程。目前已有588人学习下载适合作为课程设计参考、答辩准备的复习资料。1. 基于JavaWeb的图书管理系统这个经典课题到底解决什么问题每个学 Java 的人大概率绕不开一个图书管理系统老师用 Servlet JSP JDBC 布置课设面试官让它讲清楚模块划分。基于 JavaWeb 的图书管理系统本质上就是用 Web 最原始的三层架构把登录、图书管理、借阅归还这一串业务串起来。它能解决的问题很具体——让一个零基础的人理解 HTTP 请求怎么进入 Servlet、业务逻辑该写在 Service 还是 DAO、SQL 和页面数据如何交换。它适合两类人刚学完 JavaSE 的初学者以及拿着这个课题准备课设答辩的学生。别急着上 Spring Boot这套系统的价值在于结构简单你能看清每一行代码在做什么。2. 技术选型与三层架构Servlet JSP JDBC 这套组合还值不值得上2.1 为什么不用 Spring Boot先看图书管理系统的实际体量很多人在开始前会犹豫现在都 2025 年了为什么还要用 Servlet JSP JDBC 这种“老掉牙”的组合我的答案是图书管理系统的核心数据模型只有用户、图书、借阅记录这几张表业务动作无非是增删改查加一个借还流程。这样的体量Spring Boot 里最常用的 autoconfigure 帮你把连接池、事务管理器、Web 容器、参数绑定全包办了反而让学习者看不到请求从哪儿进来、SQL 在哪儿执行、Connection 为什么不能共用。常见的 JavaWeb 三层架构落到这个项目上是这样的Servlet 只做参数接收和页面跳转Service 层负责业务流程和事务边界DAO 层只写 SQL。教科书里常说的“高内聚低耦合”在这套系统里就是“Servlet 不写 SQL、DAO 不写业务判断、JSP 不连数据库”。很多网上流传的 javaweb 项目完整案例往往把 SQL 直接写在 Servlet 里跑得通但你不值得学那种写法。这不是说 Spring Boot 不好。而是如果你连 Servlet 的 doGet/doPost 都没亲手写过连 PreparedStatement 和 ResultSet 都没处理过那 Spring Boot 的自动配置在你眼里就是个黑匣子。第一次遇到报错时你连猜方向都猜不到。先把这套最原始的组合跑通再去看 Spring Boot 的 DispatcherServlet、MyBatis 的 Mapper 代理才是有意义的认知升级。2.2 目录骨架先落地webapp、src、pom 各自管什么确定技术栈之后第一步是把项目骨架立起来。我一般用 Maven 建项目目录结构如下src/main/java/com/example/library/ ├─ controller/BookServlet.java ├─ controller/LoginServlet.java ├─ controller/BorrowServlet.java ├─ service/BorrowService.java ├─ dao/BookDao.java ├─ dao/UserDao.java ├─ dao/BorrowDao.java └─ util/DBUtil.java src/main/webapp/ ├─ WEB-INF/web.xml ├─ login.jsp ├─ list.jsp └─ borrow.jsp pom.xmlMaven 的 webapp 结构里src/main/webapp是 Web 资源的根目录JSP、CSS、图片都放这里WEB-INF/web.xml是整个应用的部署描述符Servlet 3.0 之后大部分配置可以用注解替代但保留一个 web.xml 仍然是稳妥做法Tomcat 启动时不会因为缺描述符而报目录扫描的错。src/main/java按 controller/service/dao/util 分包强迫自己遵守依赖方向controller 调 serviceservice 调 dao谁也不能跨层。pom.xml 里需要依赖很少常见做法是这三组坐标dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjsp-api/artifactId version2.2/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency第一组依赖的 scope 必须写 provided意思是编译时需要、发布时由 Tomcat 自己提供。如果写成默认的 compile打出来的 WAR 会包一份 servlet-api和 Tomcat 自带的重复轻则启动告警重则方法冲突。第三组依赖是 MySQL 8 的官方驱动注意这里用的是mysql-connector-java这个经典坐标。驱动内部类名在 8.0 版本已经换了后面第 5 章会专门讲这个坑。2.3 前端 JSP 与后端 Servlet 怎么互相找到请求路径的约定javaweb 新手最常见的困惑是我写了WebServlet(/list)浏览器到底访问什么地址这里有两个容易被忽略的拼图。第一个是应用上下文路径也就是 IDEA 里 Artifacts 配的 Application context常见设置为/library那完整访问地址就是http://localhost:8080/library/list。第二个是 JSP 里的表单和链接action 要写上下文路径加 Servlet 路径不能只写/list。比较省事的做法是在 JSP 用 EL 表达式取上下文路径form action${pageContext.request.contextPath}/login methodpost这样即使换一个 context 名字页面里的路径也不会断。如果你看到页面 404、而 Servlet 代码确实存在先别怀疑代码打开浏览器开发者工具看 Network 面板里实际请求的 URL是少了/library还是多了后缀八成问题就在这里。3. 数据库设计与 IDEA 运行配置建四张表然后把项目跑起来3.1 图书管理系统的四张核心表字段和约束一次定清楚很多初学者一上来就写代码结果写到借阅功能时发现“这本书被谁借了”没地方存只好回头改表。图书管理系统最常见的库表设计是四张表用户表、图书表、借阅记录表外加一张可选的图书分类表。分类表不是必选项如果只做课设演示直接在图书表里放一个 category 字符串字段就够了如果你想练多表查询再拆出 category 表也不迟。字段设计里有一个关键决策图书表要不要冗余一个可借数量 available我的建议是保留。查询“哪些书可借”时如果每次都用total - count(借阅记录中未归还的)来算数据库压力大而且借阅列表页要反复关联统计SQL 写起来很别扭。冗余 available 字段的代价就是每次借书和还书必须同步更新它这件事由第 4 章的事务代码来保证。核心字段清单如下表名字段类型约束或说明useridint主键自增userusernamevarchar(50)唯一登录名userpasswordvarchar(128)存哈希值不要明文userreal_namevarchar(50)显示用姓名bookidint主键自增bookisbnvarchar(20)图书编号可加唯一索引booktitlevarchar(100)书名列表页按它搜索booktotalint馆藏总数bookavailableint当前可借数borrow_recordbook_idint外键指向 book.idborrow_recorduser_idint外键指向 user.idborrow_recordborrow_timedatetime借出时间borrow_recordreturn_timedatetime归还时间为空表示未还借阅记录表没有设置“唯一未归还”的数据库约束因为这个约束在 SQL 里不好写return_time为空时MySQL 的唯一索引不会把多个 NULL 当作重复值所以“同一人同一本书未归还”的检查放 Java 代码里做更靠谱。3.2 建表 SQLutf8mb4 字符集与索引怎么写下面这份建表 SQL 可以直接在 MySQL 8.0 里执行create database library default character set utf8mb4 collate utf8mb4_unicode_ci; use library; create table user ( id int primary key auto_increment, username varchar(50) not null unique, password varchar(128) not null, real_name varchar(50), role varchar(10) not null default STUDENT ) engineInnoDB default charsetutf8mb4; create table book ( id int primary key auto_increment, isbn varchar(20) not null, title varchar(100) not null, author varchar(50), publisher varchar(100), category varchar(30), total int not null default 1, available int not null default 1, create_time datetime not null default current_timestamp, index idx_book_title (title) ) engineInnoDB default charsetutf8mb4; create table borrow_record ( id int primary key auto_increment, book_id int not null, user_id int not null, borrow_time datetime not null default current_timestamp, return_time datetime default null, index idx_borrow_return (return_time), constraint fk_borrow_book foreign key (book_id) references book(id), constraint fk_borrow_user foreign key (user_id) references user(id) ) engineInnoDB default charsetutf8mb4;字符集这里建议直接写utf8mb4不是utf8。utf8mb4 是 MySQL 8 的默认字符集能存 emoji 和生僻字而老版本utf8实际是 utf8mb3最多三个字节遇到特殊字符会报Incorrect string value错误。表引擎必须指定 InnoDB外键约束和事务支持都依赖它。索引方面借阅记录表给return_time加了索引因为“查询所有未归还记录”是超期统计的常规动作这个查询会大量扫描where return_time is null图书表的title加索引配合like前缀搜索。注意 MySQL 的like %关键字%不走索引这里加索引主要服务前缀查询全模糊搜索在数据量小的课设里不苛求。3.3 IDEA 运行 javaweb 项目的关键配置Tomcat 与 artifacts表建好之后接下来是把项目在 IDEA 里跑起来。这里用 IDEA 下载版或旗舰版都行流程差异不大。常见的做法是New Project 选 Maven不勾选模板GroupId 填com.exampleArtefactId 填library。手动在src/main下新建webapp/WEB-INF/web.xmlIDEA 会弹出 Web 模块标记提示确认即可。打开 Run 菜单的 Edit Configurations点左上角加号选 Tomcat Server - Local。在 Server 标签页把 Application server 指向本机 Tomcat 目录。切到 Deployment 标签页点加号选 Artifact再选library:war exploded。这里必须选 war exploded也就是解压目录部署改 JSP 不用重启刷新页面就能看到效果。Application context 写/library启动后访问http://localhost:8080/library/login.jsp。漏掉第 5 步是 IDEA 跑 javaweb 项目最常见的失误。Artifact 如果没添加Tomcat 启动后访问任何路径都是 404。另一个容易翻车的点是 Tomcat 版本与你用的 Servlet API 版本不匹配这个话题第 5 章会展开。数据库连接工具类我一般这样写public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码的逻辑很直白静态代码块里加载驱动getConnection每次返回一个新的物理连接。参数里characterEncodingutf8负责让 JDBC 层按 UTF-8 编码发 SQLserverTimezoneAsia/Shanghai是 MySQL 8 和 Java 时间类型之间的必要约定不配会报时区异常useSSLfalse是本地开发关掉 SSL 握手省掉证书告警。这里要提醒一句Class.forName在 JDBC 4.0 之后可以省略但保留它能让新手清楚看到“驱动加载”这一步排查驱动问题时也方便。4. 核心功能实现登录、分页查询、借阅归还的最小闭环4.1 登录校验Session 里该存什么不该存什么登录是 JavaWeb 项目里最容易写出安全隐患的功能。常见错误写法是把整个用户对象塞进 Session再把密码一起带走。我一般只存用户 ID、用户名和角色这三个字段不存密码。UserDao 的查询方法public User findByLogin(String username, String password) throws SQLException { String sql select id, username, real_name, role from user where username ? and password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setRealName(rs.getString(real_name)); u.setRole(rs.getString(role)); return u; } return null; } } }这里用 PreparedStatement 而不是字符串拼接是为了防 SQL 注入。?占位符让参数和 SQL 语句分离哪怕用户名里带引号也不会被当成 SQL 执行。密码比对这里以明文为例实际课设答辩时至少要对密码做哈希再入库这属于安全加分项不是核心业务。LoginServlet 拿到用户后执行request.getSession().setAttribute(loginUser, user)再设置session.setMaxInactiveInterval(30 * 60)表示 30 分钟不操作就过期。Session 的作用域是整段会话所以登录状态要放这里而不是 request。后续每个 Servlet 在处理业务前先从 Session 里取 loginUser取不到就跳回登录页。为了避免每个 Servlet 都重复写这段判断最优雅的做法是写一个 Filter 拦截未登录请求规则是login.jsp和/login放行其余全部检查 Session。4.2 图书列表与分页DAO 层 ResultSet 的常规处理图书列表是图书管理系统里最常规的页面但分页是大多数课设项目的坎。数据量十本二十本时不分页无所谓一旦造了几千条演示数据一页渲染全部记录会让 JSP 响应变得很慢答辩时显得很不专业。BookDao 的分页查询public ListBook findPage(int page, int pageSize) throws SQLException { int offset (page - 1) * pageSize; String sql select id, isbn, title, author, publisher, category, total, available from book order by id desc limit ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setTitle(rs.getString(title)); // 其余字段按同样方式封装 list.add(b); } } } return list; }offset (page - 1) * pageSize这条公式是分页的核心第 1 页从第 0 条开始第 2 页从第 pageSize 条开始。MySQL 的limit ?, ?第一个参数是偏移量第二个是返回行数。JSP 渲染时建议用 JSTL 的c:forEach循环表格行同时把当前页 currentPage、总页数 totalPage 存在 request 里翻页链接写成list?page${currentPage 1}并做边界判断第一页不显示上一页最后一页不显示下一页。总页数的计算还有一个容易出错的点totalPage (totalCount pageSize - 1) / pageSize。如果直接totalCount / pageSize当总数是 10、每页 5 条时结果是 2 没错但总数是 11 时结果还是 2会丢掉最后一页的一条数据。加上pageSize - 1是为了向上取整这个“1 再除”的技巧图书管理系统里到处用得上。4.3 借阅与归还事务边界放在哪一层才算对借阅功能牵涉两张表的写操作book表的 available 减一以及borrow_record表插入一条记录。这两步必须保证原子性要么都成功要么都失败。如果你在 Servlet 里分别调 DAO 的两次 update中间任何一步抛异常库里就会出现“数量扣了但没借阅记录”的脏数据。事务边界要放在 Service 层由 Service 统一拿一个 Connection开启事务执行多条 SQL 后提交public boolean borrow(int bookId, int userId) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String deductSql update book set available available - 1 where id ? and available 0; try (PreparedStatement ps conn.prepareStatement(deductSql)) { ps.setInt(1, bookId); if (ps.executeUpdate() 0) { conn.rollback(); return false; } } String insertSql insert into borrow_record(book_id, user_id, borrow_time) values(?, ?, now()); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, bookId); ps.setInt(2, userId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }这段代码有两个值得细看的地方。第一更新书库时把available 0放进 where 条件里用executeUpdate()的返回值判断是否扣减成功。如果返回 0说明库存已经没了这是并发下防止超借的关键——比你先用 select 查再 update 要可靠得多。第二先回滚再抛异常finally里把自动提交恢复为 true避免这个 Connection 归还后被下一个业务带着错误状态继续跑。归还逻辑方向相反先查 borrow_record 里return_time is null的那条记录更新它的 return_time 为 now()再把book.available available 1同样在同一个事务里完成。这里额外提醒一句借书前要查一下这个用户有没有未归还的同名书规则“同一本书未归还不能再借”在 Service 层判断不要指望数据库约束替你兜底。5. 图书管理系统运行中几个常见问题与排查清单5.1 中文乱码问号从哪来三处编码设置如何对齐现象页面上书名显示成???或者数组库插入中文后变成乱码。原因编码在四层链路里只要断一环就乱——JSP 页面编码、Tomcat 接收请求的编码、JDBC 连接编码、MySQL 表字符集。解决按顺序对齐。JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %写一个实现 Filter 接口的过滤器在 doFilter 里执行request.setCharacterEncoding(UTF-8)再chain.doFilterJDBC URL 带characterEncodingutf8建表时表结构用 utf8mb4。GET 请求的参数编码还涉及 Tomcat 的URIEncodingTomcat 8 之后默认 UTF-8老版本还要在 server.xml 里补配置。排查时从浏览器 Network 面板看响应头再逐层看数据库表字符集别一上来就怀疑代码逻辑。5.2 Tomcat 10 的 javax 与 jakarta一启动就 404 的经典原因现象本地 Tomcat 启动正常访问项目路径直接 404IDEA 里的 catalina 日志也没有明显异常。原因Tomcat 10 开始从 Java EE 迁移到 Jakarta EEServlet 的包名从javax.servlet变成了jakarta.servlet。如果你的 pom 或代码里 import 的还是javax.servlet.*编译可能通过部署后容器却找不到对应的 Servlet 类请求自然全部 404。解决方案一把本地 Tomcat 换回 9.xTomcat 9 仍然使用javax包网上大量 javaweb 笔记和完整案例默认就是 9方案二如果必须用 Tomcat 10把代码里所有import javax.servlet改成import jakarta.servlet同时 pom 里 Servlet API 依赖也换成jakarta.servlet:jakarta.servlet-api。这个坑典型到只要你把 IDEA 运行 javaweb 项目配置的学习成本算进去我就会劝你用 Tomcat 9 起步。5.3 mysql-connector 驱动类变化ClassNotFoundException 排查现象项目能编译一运行访问数据库就报ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 8 之后驱动类名换了旧驱动叫com.mysql.jdbc.Driver新驱动叫com.mysql.cj.jdbc.Driver。如果你在 DBUtil 里还写旧类名8.x 驱动根本不认。解决把 Class.forName 的参数改成新类名同时检查 pom 里依赖的版本号如果写的是5.1.x那还是老驱动类名对应旧的如果写8.0.x务必用新类名。另一个伴随问题MySQL 8 默认认证插件是 caching_sha2_passwordJDBC 连接时可能报Public Key Retrieval is not allowed解决办法是在 JDBC URL 末尾加allowPublicKeyRetrievaltrue。这类问题查起来全靠日志定位第一次遇到会觉得玄学其实都是版本漂移。5.4 连接没关干净MySQL 异常断开与 try-with-resources现象项目刚启动时正常跑一段时间后报The last packet successfully received from the server was X milliseconds ago。原因每开一个连接没有关闭MySQL 的 wait_timeout 到了自动断掉空闲连接下一次请求拿到的是一个死连接。这个坑在图书管理系统这类课设里太常见了因为很多人只在getConnection()时记得开finally 里忘了关 ResultSet 和 PreparedStatement。解决所有 JDBC 操作统一用 try-with-resources 写法把 Connection、PreparedStatement、ResultSet 都声明在 try 括号里Java 7 之后会自动 close代码量大减。另外Connection的关闭要放在最外层和 Statement 的关闭顺序相反先关 ResultSet 再关 Statement 最后关 Connectiontry-with-resources 会自动按声明逆序处理你只管把三个对象都写在括号里就好。5.5 外键约束删不掉书就先查借阅记录现象删除某本图书时数据库报Cannot delete or update a parent row: a foreign key constraint fails。原因borrow_record 表通过外键引用了 book 表只要这本书有过借阅记录 delete 就会违反外键约束。解决两个方向。方向一是先删除或迁移该书的借阅记录再删书适合强制清理方向二是在 book 表加一个deleted字段删除操作改成update book set deleted 1 where id ?列表查询默认带where deleted 0历史借阅记录仍然能关联到书的信息。我一般推荐方向二图书管理系统的业务特点是借阅历史要长期保留物理删除会让统计页面缺数据。建表时也别给外键加on delete cascade否则删一本测试书会连带删掉所有借阅历史课设答辩演示数据一下子就没了后悔药都找不着。6. 跑通之后借阅超期判断与低成本演进6.1 超期判断放 SQL 还是 Java一条带 datediff 的查询借阅流程跑通后下一个必然需求是超期名单。常见做法是设定借阅期限为 30 天查询所有未归还且超期的记录。SQL 方案比 Java 逐行计算更简洁select br.id, u.username, b.title, br.borrow_time, datediff(now(), br.borrow_time) as borrowed_days, case when datediff(now(), br.borrow_time) 30 then 已超期 else 正常 end as status_text from borrow_record br join user u on u.id br.user_id join book b on b.id br.book_id where br.return_time is null order by borrowed_days desc;datediff(now(), borrow_time)返回两个日期相差的自然日数不精确到小时。如果业务要求按小时计算改用timestampdiff(hour, borrow_time, now()) / 24。这个查询结果可以直接喂给 JSP 渲染成表格也可以加上where过滤只看超期记录。数据量大了以后别忘了 borrow_record 的 return_time 字段已经建过索引这个查询走索引会快很多。6.2 低成本演进批量造数、搜索条件、日志与连接池超期功能补上之后这个图书管理系统的核心业务就完整了。剩下的演进按性价比排序先把 DBUtil 换成 HikariCP 连接池pom 里加一个依赖把getConnection()改成从HikariDataSource取这能治第 5 章说的连接断开的病再做图书搜索BookDao 加一个支持多个条件拼接的方法条件用动态 SQL 拼但值仍然用 PreparedStatement 占位最后给系统加访问日志在 Filter 里打印请求路径和耗时即可。这三件事做完这个 javaweb 图书管理系统就不再是课设水平而是具备了一个小型 Java Web 工程该有的骨架。我自己的习惯是代码全部跑通后一定会拿一条完整链路自测注册用户、登录、借书、还书、再次借书、删除图书、查看超期名单。这个流程能暴露绝大多数脏数据问题尤其是 available 总数是否守恒——借了几本就应该少几本还回来就必须加回来。我以前做课设时在借阅事务这个环节踩过坑后来养成一个条件反射凡是涉及两条以上 SQL 的写操作先问一句事务边界在哪。希望帮到你。本文还有配套的精品资源点击获取
返回列表