
简介这是一套面向JavaWeb课程设计的图书管理系统完整源码附带数据库脚本适合计算机专业学生完成实训作业或毕业设计参考。系统实现登录、注销、修改密码管理员可对图书进行增删改查并查看读者与借阅记录读者可检索图书、修改个人信息、查看本人借阅记录。后端采用SpringBoot整合MyBatis持久层、Thymeleaf模板、Bootstrap界面与SpringSecurity登录鉴权结构完整且可运行。压缩包共74个文件以23个Java类、15个HTML页面和SQL脚本为主另有MyBatis映射文件、CSS/JS、Maven配置等包体大小15.36MB。已有371人学习下载。借助该源码可快速搭建图书管理课程设计演示项目内含library.sql建库脚本、Maven工程结构和application.yml配置入口按说明修改数据库账号密码后即可启动并访问系统适合在理解SpringBoot业务分层和权限控制时对照阅读。1. 图书管理系统不是玩具项目是 JavaWeb 课程设计里最见功底的完整闭环图书管理系统在 JavaWeb 课程设计中出镜率极高但多数人交上去的版本只有增删改查页面加一张表答辩时被问“你的数据库怎么设计的”“借阅超期怎么算”“图书被借走时并发怎么办”就直接卡住。这个标题里的关键词是“源代码 数据库”意味着评分重点不是页面多好看而是 JavaWeb 三层架构是否完整、数据库设计是否合理、代码能否直接运行。它面向的是正在做课程设计的在校生、需要快速交付毕设模块的本科生以及想用一套经典 CRUD 练习 Servlet JSP MySQL 整合的 Java 初学者。做这套系统的核心价值是用最小成本把请求处理、业务逻辑、数据访问、事务控制、会话管理这几条主线全部走通——这正是 JavaWeb 课程设计真正想考察的东西。本文按从选型到落地再到答辩验证的顺序把图书管理系统的实现路径完整拆开。2. 从需求到数据库设计先定表和关系再写 Java 代码2.1 图书管理系统不是一张 book 表就够的很多人的第一个版本只有 book(id, name, author, price) 一张表借阅记录用一个字段存。这种设计应付不了课程设计的核心需求学生借书、还书、查询历史、统计热门图书。一个合格的图书管理系统至少需要四类实体图书、读者用户、借阅记录、图书分类。分类和图书是一对多图书和借阅记录是一对多读者和借阅记录是一对多。如果系统要求管理员和普通读者分开还需要用户表带角色字段或者单独建管理员表。我一般建议课程设计用 5 张表category分类、book图书、user用户/读者、borrow_record借阅记录、user含角色。再加上一张可选的 book_copy复本表用来处理同一本书有多本但每本独立编号的场景。如果只做基础版book 表里加一个 total 和 available 字段也能凑合但答辩时容易被打“库存扣减没有事务保护”。把表拆清楚后面写代码会顺很多。2.2 用 MySQL 建库建表的完整脚本下面这套建表脚本按课程设计常见规模设计去掉了外键强制约束带来的删除麻烦用逻辑外键代替但索引照建。这样做既方便初始化数据又不会让联表查询变成慢查询。CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(200) ) ENGINEInnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), price DECIMAL(10,2), category_id INT, total INT DEFAULT 1, available INT DEFAULT 1, INDEX idx_category (category_id), INDEX idx_title (title) ) ENGINEInnoDB;CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role ENUM(admin,common) DEFAULT common, real_name VARCHAR(30), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, user_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, due_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 2逾期未还, INDEX idx_book (book_id), INDEX idx_user (user_id) ) ENGINEInnoDB;建表脚本里有两个容易忽略的设计点。第一个是due_time建议用DATETIME而不是时间戳字符串后续算逾期时间可以直接用TIMESTAMPDIFF。第二个是borrow_record.status不要只记“借出/已还”把“逾期未还”也作为独立状态统计时少写很多条件判断。available字段就是可借数量借书时减一还书时加一配合事务保证一致性。分类表加唯一约束防止重复导入分类数据。2.3 数据访问层的连接管理和基础查询JavaWeb 课程设计最常用的连接方式还是 JDBC Druid 连接池。直接用DriverManager每次新建连接答辩时只要被问“数据库连接数过高怎么办”就露馅。用连接池是教科书级别的标准答案配置也简单。!-- src/druid.properties -- driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait60000工具类只需要一个DataSource静态实例全局复用。public class JdbcUtil { private static DataSource dataSource; static { try (InputStream in JdbcUtil.class.getClassLoader().getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }连接池参数里initialSize表示启动时预创建的连接数课程设计项目量级下 5 个足够。maxActive是最大活跃连接数。需要注意的是Druid依赖需要手动引入如果用 Maven 就加druid和mysql-connector-j两个依赖。这段代码看起来简单但它把资源释放和异常处理的责任交给了调用方所以 DAO 层里每个方法都要用 try-with-resources 关闭ResultSet、Statement和Connection否则连接池很快耗尽。3. Servlet 控制层设计从页面请求到 JSON 响应的请求分发3.1 BaseServlet用反射消除 if-else 方法分发JavaWeb 课程设计里的 Servlet 很容易写成一坨addBook、updateBook、deleteBook 各写一个 Servlet 类配置一堆servlet-mapping。维护成本高答辩时也不加分。常见做法是写一个基础 BaseServlet通过请求参数action反射调用子类方法把细粒度操作收敛到同一个控制器里。WebServlet(/admin/book/*) public class BookServlet extends BaseServlet { public String list(HttpServletRequest req, HttpServletResponse resp) throws Exception { req.setCharacterEncoding(UTF-8); int page Integer.parseInt(req.getParameter(page) null ? 1 : req.getParameter(page)); int pageSize 10; BookService service new BookService(); PageBeanBook pageBean service.queryPage(page, pageSize); req.setAttribute(pageBean, pageBean); return admin/book_list.jsp; } public String delete(HttpServletRequest req, HttpServletResponse resp) throws Exception { int id Integer.parseInt(req.getParameter(id)); new BookService().deleteById(id); resp.sendRedirect(book?actionlist); return null; } }对应的 BaseServlet 核心逻辑是反射查找方法“子类不覆写 doGet/doPost统一走 BaseServlet 的 service 方法”。下面这段代码就是反射分发的核心。protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.isEmpty()) { action list; } try { Method method this.getClass().getMethod(action, HttpServletRequest.class, HttpServletResponse.class); if (method.getReturnType() String.class) { String result (String) method.invoke(this, req, resp); if (result ! null) { if (result.startsWith(redirect:)) { resp.sendRedirect(result.substring(redirect:.length())); } else { req.getRequestDispatcher(result).forward(req, resp); } } } } catch (NoSuchMethodException e) { resp.sendError(404, unknown action); } catch (Exception e) { throw new ServletException(action invoke failed: action, e); } }这段设计的逻辑是所有业务方法返回字符串返回redirect:前缀表示重定向否则表单提交用重定向避免重复提交。注意反射方法第一个参数必须是HttpServletRequest。反射有微小性能损耗但对课程设计毫无影响换来的是新增功能时只写一个方法不用改任何配置。这也是把“源代码 数据库”对应的项目结构做得像真实商业项目而不是学生作业的关键一着。3.2 用 Filter 统一编码和登录拦截中文乱码是 JavaWeb 项目最容易暴露的问题。JSP 页面是 UTF-8请求经过doPost时不设置编码数据库里就是一堆问号。用一个全局编码过滤器解决统一处理请求、响应的字符编码。WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }编码过滤器只是基础。更关键的是登录拦截 Filter 要判断 Session 里是否存在用户信息放行login.jsp、loginaction 和静态资源。如果直接写在每个 Servlet 里检查 Session代码会非常冗余而且容易忘。用 Filter 的匹配路径规则WebFilter(urlPatterns {/admin/*, /reader/*, /user/*}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); Object current session null ? null : session.getAttribute(currentUser); if (current null) { ((HttpServletResponse) resp).sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }使用request.getSession(false)而不是getSession()后者会在没有 Session 时自动创建新 Session。课程设计中很多人被这个细节坑过访问未登录的黑名单路径明明应该跳登录页结果每次请求都创建新 Session浪费内存。过滤器的顺序由三个层面决定WebFilter的注解不保证执行顺序多个过滤器时建议用web.xml里的filter-mapping声明编码 Filter 必须在前面登录 Filter 在后面这样进入业务前编码已经正确。3.3 借书还书的 Service 层事务控制图书借出必须同时完成两件事插入一条借阅记录、把 book 表的 available 减一。这两步任何一个失败都会产生脏数据。Servlet 里直接写两个 DAO 调用无法保证原子性必须在 Service 层用事务把它们包起来。public boolean borrowBook(int userId, int bookId) { try (Connection conn JdbcUtil.getConnection()) { conn.setAutoCommit(false); BorrowDao bDao new BorrowDao(); BookDao bookDao new BookDao(); if (!decrBookAvailable(conn, bookId)) { conn.rollback(); return false; } int dueDays 30; Timestamp due new Timestamp(System.currentTimeMillis() dueDays * 24L * 3600 * 1000); bDao.insert(conn, bookId, userId, due); conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); return false; } }这段代码有两个关键设计。第一个是decrBookAvailable的 SQL 必须用条件更新防止超借UPDATE book SET available available - 1 WHERE id ? AND available 0如果用“先 SELECT 再 UPDATE”两个请求同时读到 available1 就会都借出超借无法避免。第二个是事务中所有 DAO 方法都必须接收Connection参数不能在 DAO 内部自己getConnection()否则连接不是同一个事务失效。很多课程设计里把getConnection()放在 DAO 每个方法内部最后发现 rollback 没效果问题就在这。4. 前端页面与 JSP 数据渲染表格展示、分页查询、表单回显4.1 分页查询的 SQL 和 PageBean 封装图书列表页不加分页一次查几百本书页面卡顿课程设计说明书写不出亮点。MySQL 的分页 SQL 用LIMIT offset, rows即可但要顺手查总数才能计算总页数。public class BookDao { public ListBook queryPage(int offset, int pageSize) throws SQLException { String sql SELECT id,isbn,title,author,publisher,price,category_id,total,available FROM book ORDER BY id DESC LIMIT ?,?; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { ListBook list new ArrayList(); while (rs.next()) { Book book new Book(); // 省略属性赋值 list.add(book); } return list; } } } public int count() throws SQLException { String sql SELECT COUNT(*) FROM book; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1); } } }PageBean把分页需要用到的字段集齐page当前页、pageSize每页大小、totalCount总记录数、totalPages总页数、list当前页数据。计算总页数时注意totalPages (int) Math.ceil(totalCount * 1.0 / pageSize)直接在 Java 里除会得到整数截断最后一页数据直接丢失。页面上翻页链接的写法要带上当前查询条件a hrefbook?actionlistpage${pageBean.page 1}下一页/a课程设计的列表页一般只要上一页、下一页、首页、末页四条链接就够了。真正要警惕的是“搜索 分页”组合时翻页丢失搜索关键字的问题。解决方法是把搜索关键字也放进查询条件和分页链接里或者用表单 GET 提交带 hidden 字段。4.2 JSTL 渲染表格与表单回显纯 JSP 脚本片段% for(...) %能用但丑课程设计被老师抽查代码时印象分会低。引入jstl和standard依赖用c:forEach循环渲染表格行。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-hover thead trthISBN/thth书名/thth作者/thth分类/thth库存/thth操作/th/tr /thead tbody c:forEach items${pageBean.list} varbook tr td${book.isbn}/td td${book.title}/td td${book.author}/td td${book.categoryName}/td td${book.available}/${book.total}/td td a hrefbook?actioneditid${book.id}编辑/a a hrefjavascript:void(0) onclickdoDelete(${book.id})删除/a /td /tr /c:forEach /tbody /table这里容易出现的坑是 JSP 里显示book.categoryName报错因为 Book 对象里根本没有该属性。课程设计里常见做法是在 Book 实体里加一个categoryName字段然后在 DAO 的查询 SQL 里通过JOIN category ON book.category_id category.id把分类名查出来再设进去。这样页面写法保持了面向实体的直觉不用在 JSP 里再拿 categoryId 去查分类表。这算是一种轻微冗余但很实用几乎已经成了 JavaWeb 课程设计的事实标准。表单回显的典型场景是编辑图书先查出原数据在value属性里输出。选中状态的选择框用 JSTL 判断。select namecategory_id c:forEach items${categoryList} varc option value${c.id} ${c.id book.categoryId ? selected : }${c.name}/option /c:forEach /select这个三元表达式写法比c:if嵌套简洁得多支持的 EL 版本只要 2.2 以上。别忘了表单提交后如果校验失败要返回错误信息并回显用户之前填的值最简单的做法是把提交的表单数据放到 request 域再 forward 回表单页面。4.3 图书借阅列表与超期状态计算借阅记录页需要展示“当前用户借了哪些书、是否超期”不能只查borrow_record表还要 JOIN book 表拿到书名。SELECT br.id, br.book_id, br.borrow_time, br.due_time, br.return_time, br.status, b.title, b.isbn FROM borrow_record br JOIN book b ON br.book_id b.id WHERE br.user_id ? ORDER BY br.borrow_time DESC超期状态最好在查出来之后在 Java 里计算而不是只依赖数据库里的 status 字段。因为 status 字段是借还时写入的如果系统连续运行好几天没触发定时任务字段可能已经过期。一种简单可靠的办法是查询时判断status 0 due_time NOW()满足条件就把显示状态改成“已逾期”同时可以顺便更新这条记录的 status 为 2。这样即便没有定时任务用户每次打开页面都会被动刷新逾期状态属于带延迟的最终一致性课程设计完全够用。5. 系统管理与报表统计借阅排行、逾期提醒、数据可视化5.1 借阅数量排行的 SQL 与图表展示热门图书排行是课程设计里最容易出彩又能落地的功能。基础是按借阅次数排序用分组聚合就能做。SELECT b.title, COUNT(br.id) AS borrow_count FROM borrow_record br JOIN book b ON br.book_id b.id GROUP BY br.book_id, b.title ORDER BY borrow_count DESC LIMIT 10;COUNT(br.id)统计每个书被借的次数借过又还也会算一次符合“热门”的定义。如果还想看读者排行把分组字段换成user_id即可。图表展示不一定要引入 echarts 这种重量级前端库课程设计阶段可以用简单的 HTML 表格加 CSS 宽度展示柱状效果或者引入一个 ECharts CDN 画柱状图。但要注意若系统需要离线演示CDN 无法加载建议把 echarts.min.js 下载到本地webapp/js目录引用。展示图表的 JSP 页面用内嵌 JSON 的方式把后端数据传给前端script var chartData ${rankListJson}; /scriptrankListJson是 Servlet 里用 Gson 把ListMapString, Object转成的 JSON 字符串注意 JSP 页面中要在script标签里使用${rankListJson}且后端设置c:out防止 XSS。更稳妥的方式是让 JSP 页面通过fetch调一个返回 JSON 的 Servlet 接口页面加载时异步获取数据。5.2 逾期未还书的批量标记与提醒邮件逾期自动标记有两种路线。第一种是每次展示前实时判断前面已经写过。第二种是手动触发一个批量更新任务。课程设计通常不需要太复杂的定时器但可以在管理界面放一个“一键检测逾期”按钮后台执行下面的 SQLUPDATE borrow_record SET status 2 WHERE status 0 AND due_time NOW();这个操作务必放在事务里并且只更新未归还的记录。如果要更专业一点可以加一个 Quartz 定时任务但课程设计评分一般不会因为多加调度框架而显著提升反而增加部署复杂度。如果系统里存了读者的邮箱可以在检测到逾期后顺带通过 JavaMail 发提醒邮件需要注意 QQ 邮箱等需要开启授权码而不是登录密码配置写在配置文件里别硬编码。邮件发送不是必做项但属于“加分项中性价比最高的一个功能”。5.3 数据库备份与初始化数据脚本写数据库课程设计必须能证明你的数据库设计是认真做的。一份带INSERT语句的初始化数据脚本建议至少 20 本图书、10 个读者、30 条借阅记录并且要有几条已经逾期、几条已经归还。这样演示时打开列表页和统计页能立刻看到效果不用现场敲数据。备份脚本简单用mysqldump命令mysqldump -uroot -p --databases library_db library_db_backup_$(date %Y%m%d).sql恢复时先建库再导入mysql -uroot -p library_db_backup_20250101.sqlmysqldump默认导出的是完整建表语句和 INSERT 数据。恢复前确认 MySQL 版本兼容。导入大文件时用source命令会逐条执行慢一些但课程设计的数据量不存在这个问题。把备份脚本放在项目根目录的sql/文件夹里在说明文档中写明如何导入答辩演示时可以直接在你自己的电脑上现场演示从零还原这种细节比写一堆功能介绍更能说服老师。6. 从能跑通到答得清课程设计答辩时的验证清单课程设计的评分不是只看演示更重要的是现场提问。下面这份清单是我自己带学生时会逐项核对的你可以在提交前一条一条过一遍。第一项是用 Navicat 或命令行登录 MySQL执行SHOW TABLES能完整看到 5 张以上的表结构然后SHOW CREATE TABLE book检查字段注释、索引和引擎。如果全部是 MyISAM说明没有真正理解 InnoDB 对事务的支持建议立刻改过来。第二项是打开两个浏览器窗口同时登录同一账号借同一本库存只剩 1 的书观察是否只有一次成功。这一步验证的是前面条件更新WHERE available 0是否生效十个人里有八个会在这里翻车。第三项是在数据库里手动把某本书的available改成负数看列表页是否会显示负库存如果不会且能正常展示说明代码有基础校验但更推荐在 SQL 层面用CHECK约束或应用层做拦截。第四项是把连接池的maxActive改成 1连续快速刷新页面 50 次观察控制台是否有连接泄漏报错如果没有说明资源关闭写得干净。验证完基础功能后重点检查这几个容易被问到的跨查询问题。一是借阅记录分页后点击“已还/未还”筛选时页码是否错乱重点是携带筛选条件到下一页的处理。二是在事务中执行借书后手动抛出异常查看available是否没有被扣减。三是测试管理员删除一本已被借出的图书正确的业务逻辑是禁止删除或改为下架而不是直接 DELETE一个成熟的系统应该在删除前先查borrow_record表中是否有未还记录如果有直接提示。最后一类问题是数据一致性场景读者还书时间超过due_time还书后status应该变成 1 还是 2有些同学会把 status 覆盖成 1但正确做法是保留逾期历史也就是说还书后return_time日期晚于due_time保留“曾逾期”的信息可以通过查询两个时间字段得出不必再单独加字段。你可以用一条 SQL 展示哪些人是曾逾期SELECT user_id, COUNT(*) AS overdue_count FROM borrow_record WHERE return_time due_time GROUP BY user_id;这一条 SQL 能同时回应“你如何证明系统能处理逾期业务”和“你如何统计读者信用”两个问题。建议把它作为数据库设计说明中的一个亮点。课程设计的核心不是功能越多越好而是每一行代码和每一张表都能讲清楚为什么这么做。把上面的清单跑一遍再把这几条 SQL 背熟答辩时你就能从“我实现了什么”自然过渡到“我是怎么保证它正确的”这就已经远超平均水平了。本文还有配套的精品资源点击获取