简介这份资源是一套面向Java Web初学者的图书管理系统期末大作业适合大二学生或刚接触SSM/MyBatis的开发者练手。项目围绕书籍、学生、管理员、留言四类实体展开涵盖借还书日志记录、书籍状态管理未借出、已借出、续借、逾期、丢失等、信誉等级与借阅数量控制等业务逻辑并实现了学生与管理员双端登录、Ajax异步校验、正则验证、Cookie记住账号、Session会话管理等常见功能。压缩包共59个文件包含26个Java源码、13个HTML页面、7个XML配置、3个JSP、2个JS脚本以及SQL建表文件、图片与说明文档整体约879KB结构完整、便于导入运行。目前已有1915人学习下载可作为课程设计参考模板帮助读者理解前后端交互流程、数据库表设计与登录注册模块的实现思路。1. 从一份大二期末作业说起这套 Javaweb 图书管理系统到底能跑出什么如果你正在搜「javaweb 项目完整案例 mysql」大概率是想找一个能跑通、能改、能写进简历的完整工程。这份 LMS-master 就是典型的大二上学期期末大作业产物MyBatis 做持久层、Servlet 做控制层、JSP 做视图层配一个 mybatis.sql 建库脚本pom.xml 管依赖README.md 交代运行方式。它不花哨但把「登录鉴权 → 图书增删改查 → 借还续借 → 留言评价」这条主线走完了对刚学完 JSPServletMyBatis 的人来说是一个能直接上手拆解的练手项目。它适合谁适合已经能写单个 Servlet、但没串过完整业务流的同学也适合想拿一个现成架子改造成自己课设的人。不适合谁不适合想直接拿 SpringBoot 全家桶的人——这里没有 Spring没有注解式事务全是原生 Servlet 和 MyBatis 映射文件你得接受手写 JDBC 工具类和 web.xml 配置。下面按「资源是什么 → 怎么跑起来 → 业务怎么实现 → 坑在哪 → 怎么改」的顺序拆。2. 把项目跑起来环境、建库与 IDEA 配置2.1 技术栈与目录结构确认先看清楚手里有什么。解压 Javaweb图书馆.zip 后根目录是 LMS-master里面 src/main 下分 java、resources、webapp 三块mybatis.sql 在项目根或 resources 下pom.xml 决定依赖版本。这套结构是标准 Maven Web 工程不是 Eclipse 动态 Web 项目所以别用老教程里「新建 Dynamic Web Project」的方式导入。层级技术作用视图JSP JSTL页面渲染、表单、Ajax 回调展示控制Servlet接收请求、调 Service、转发或返回 JSON持久MyBatisSQL 映射、结果集转实体数据库MySQL存书籍、学生、管理员、留言构建Maven依赖管理、打包 war常见做法是先在 IDEA 里 File → Open 选中 pom.xml 作为项目打开等 Maven 依赖下载完再配置 Tomcat。注意 pom.xml 里如果 servlet-api 的 scope 是 provided说明它由 Tomcat 提供不要重复打进 war。2.2 建库与 mybatis.sql 导入数据库这一步最容易翻车。mybatis.sql 里通常包含建库、建表、插入初始管理员和测试书籍。我一般先手动建库再执行脚本避免脚本里的 CREATE DATABASE 字符集和你本地不一致。# 登录 MySQL注意版本 8.x 要加时区参数 mysql -u root -p # 建库字符集用 utf8mb4否则中文书名会乱码 CREATE DATABASE lms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 退出后用 source 导入脚本路径换成你本地的 USE lms; SOURCE D:/LMS-master/mybatis.sql;逻辑说明先建库是为了控制字符集utf8mb4 能存 emoji 和生僻字比 utf8 稳。source 命令在 MySQL 客户端里执行比在命令行用重定向更直观出错能看到具体哪一行。参数上如果你的 MySQL 是 5.7utf8mb4_general_ci 没问题8.0 建议用 utf8mb4_0900_ai_ci但 general_ci 也能跑。导入后执行SELECT * FROM book;确认有数据。如果表名是 book、student、admin、evaluation说明脚本完整。2.3 数据库连接配置与 IDEA 运行配置连接信息一般在 src/main/resources 下的 mybatis-config.xml 或 jdbc.properties 里。找到 driver、url、username、password 四项改成你本地的。!-- mybatis-config.xml 片段常见写法 -- environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lms?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /dataSource /environment /environments逻辑说明driver 用 com.mysql.cj.jdbc.Driver 是 MySQL 8 的写法5.x 用 com.mysql.jdbc.Driver。url 里 serverTimezone 必须加否则 8.0 驱动会报时区错误。useUnicode 和 characterEncoding 保证中文不乱码。参数上POOLED 表示用连接池比 UNPOOLED 性能好练手项目够用。IDEA 里配置 TomcatRun → Edit Configurations → 加 Tomcat Server → LocalDeployment 里加 Artifact选 war exploded。Application context 设成 /lms 或 /启动后访问http://localhost:8080/lms/进 index 页。如果 404先看 Artifact 有没有加、context 路径对不对。3. 登录与注册链路Ajax Session Cookie 怎么串3.1 学生注册的 Ajax 异步校验项目正文明确要求注册不用 form 直接提交而是 Ajax 以 JSON 提交学号还要异步查重。这是这套代码里最值得拆的部分因为它把「前端校验 → 后端查重 → 写库 → 返回 JSON」整条链路走通了。// 学号失焦时触发查重常见写法 function checkNumber() { var number document.getElementById(number).value; var xhr new XMLHttpRequest(); xhr.open(POST, checkNumberServlet, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.onreadystatechange function () { if (xhr.readyState 4 xhr.status 200) { // 后端返回 true 表示已存在false 表示可用 if (xhr.responseText true) { document.getElementById(numberTip).innerText 学号已存在; } else { document.getElementById(numberTip).innerText 学号可用; } } }; xhr.send(number number); }逻辑说明readyState 4 表示请求完成status 200 表示成功。后端 Servlet 拿到 number 后查 student 表有记录返回 true没有返回 false。参数上Content-Type 用 urlencoded 是因为这里传的是普通表单串如果按正文要求传 JSON要改成 application/json后端用 request.getReader() 读流再解析代码量更大练手项目里两种都常见。注册提交时把学号、密码、手机、邮箱打包成 JSON后端用正则再校验一遍。注意前端正则只是体验优化后端必须重验否则绕过前端就能写脏数据。3.2 登录验证与 Session 记录学生登录用学号密码管理员用用户名密码两者逻辑几乎一样区别在查哪张表。登录成功后用 session 存用户信息这是后续所有页面的鉴权依据。// LoginServlet 核心片段 String number request.getParameter(number); String password request.getParameter(password); Student student studentService.login(number, password); if (student ! null) { HttpSession session request.getSession(); session.setAttribute(student, student); response.getWriter().write(success); } else { response.getWriter().write(fail); }逻辑说明getSession() 没有参数时有 session 就复用没有就新建。把 student 对象整个存进去后续页面用${sessionScope.student.username}取。返回 success/fail 给前端 Ajax前端根据结果决定跳转还是原地提示。参数上session 默认 30 分钟超时可以在 web.xml 里用session-config改。3.3 Cookie 记住账号与安全提示正文提到用加密 Cookie 记住账号十天并提示公用电脑不要用。实现上一般把学号加密后写 Cookie下次进登录页先读 Cookie 回填。// 登录成功时写 Cookie String encoded Base64.getEncoder().encodeToString(number.getBytes()); Cookie cookie new Cookie(rememberNumber, encoded); cookie.setMaxAge(10 * 24 * 60 * 60); // 十天单位秒 cookie.setPath(/); response.addCookie(cookie);逻辑说明Base64 只是编码不是加密真要安全得用 AES 加盐。setMaxAge 单位是秒十天就是 864000。setPath(/) 保证整个应用都能读到。注意 Cookie 里不要存密码只存账号标识否则被拿到就是明文风险。这也是为什么页面上要加那句安全提示。4. 图书借还与留言状态机与日志落地4.1 Book 状态字段的设计与流转Book 实体里 state 字段是整套业务的核心正文注释写得很清楚0 未借出、1 已借出、2 被续借、3 逾期、4 丢失、5 损毁、6 其他。借书时把 state 从 0 改成 1同时写入 owner、borrowBookDate还书时把 state 改回 0owner 和两个日期置 null。-- 借书先判断 state0 再更新避免并发借同一本 UPDATE book SET state 1, owner #{studentId}, borrowBookDate #{date} WHERE id #{bookId} AND state 0;逻辑说明WHERE 里带 state0 是乐观锁思路如果两个人同时借只有一条 UPDATE 影响行数为 1另一条为 0后端据此判断借书失败。参数上borrowBookDate 用字符串存格式建议统一 yyyy-MM-dd HH:mm:ss方便排序和比较。还书同理把 state 改回 0 并清空借阅人信息。书籍总数不用单独字段正文里也提到直接用SELECT * FROM book WHERE state0再统计 list.size()。这在数据量小的时候没问题数据上千后建议用SELECT COUNT(*)少传数据。4.2 借还日志写入文件正文要求把借书还书活动写成日志记录到文件。这是练手项目里少见的加分项实现上用 Java 的 FileWriter 追加写即可。// 日志工具方法追加模式写入 public static void writeLog(String action, int bookId, int studentId) { String line new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(new Date()) | action | bookId bookId | studentId studentId; try (FileWriter fw new FileWriter(lms_borrow.log, true)) { fw.write(line System.lineSeparator()); } catch (IOException e) { e.printStackTrace(); } }逻辑说明FileWriter 第二个参数 true 表示追加不覆盖。try-with-resources 保证流关闭。System.lineSeparator() 跨平台换行。参数上日志路径写相对路径会落在 Tomcat 的 bin 目录下建议用绝对路径或配置项否则你找不到文件。多线程下 FileWriter 不是线程安全的高并发要换 BufferedWriter 加锁或直接用日志框架。4.3 留言 Evaluation 的多态归属Evaluation 实体里 adminId 和 studentId 二选一bookId 指向被留言的书。查询时按 bookId 拉出所有留言再根据哪个 id 非空决定显示管理员还是学生昵称。SELECT e.*, s.username AS studentName, a.username AS adminName FROM evaluation e LEFT JOIN student s ON e.studentId s.id LEFT JOIN admin a ON e.adminId a.id WHERE e.bookId #{bookId} ORDER BY e.evaluationId DESC;逻辑说明LEFT JOIN 保证即使某一方为 null 也能查出记录。ORDER BY 倒序让最新留言在前。参数上如果留言量大要分页加 LIMIT。这种「一张表存两种身份」的设计在练手项目里常见缺点是外键约束不好加优点是查询简单。5. 避坑与排查这套代码最容易翻车的五个地方5.1 中文乱码现象是书名变问号现象页面显示「????」数据库里也是乱码。原因建库字符集不是 utf8mb4或连接 url 没加 characterEncoding。解决重建库用 utf8mb4url 加useUnicodetruecharacterEncodingutf8JSP 页头加% page contentTypetext/html;charsetUTF-8 %三处缺一不可。5.2 学号查重 Ajax 一直返回已存在现象新学号也提示已存在。原因后端查重 SQL 用了LIKE或没传对参数或者把整个 student 对象当条件。解决确认 SQL 是SELECT COUNT(*) FROM student WHERE number #{number}返回 count0。调试时在 Servlet 里打印收到的 number看是不是空串。5.3 登录成功但跳转后 session 取不到现象登录返回 success跳转后页面显示未登录。原因Ajax 请求和页面跳转不是同一个会话或者跳转用了response.sendRedirect但前端没处理。解决Ajax 成功后用window.location.href跳转浏览器会带上同域 Cookiesession 自然延续。别在 Servlet 里直接重定向那样 Ajax 拿到的是整个页面 HTML。5.4 借书并发导致同一本书被借两次现象两个学生同时借同一本书都提示成功。原因先查 state 再更新中间有时间窗。解决用 4.1 里的UPDATE ... WHERE state0判断影响行数。影响行数为 0 就提示「该书已被借出」。这是最典型的血泪经验不加条件更新迟早出事。5.5 Tomcat 启动报 ClassNotFound现象启动时java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因MySQL 驱动没打进 war或 scope 写成了 provided。解决检查 pom.xml 里 mysql-connector-java 的 scope练手项目直接不写 scope 或写 compile。另外确认 Artifact 的 lib 目录里有这个 jar。6. 进阶改造把状态机抽成枚举再补一个逾期扫描跑通之后这套代码最值得改的是 state 字段。现在用 0 到 6 的魔法数字散落在 SQL 和 JSP 里改一个含义要全局搜。我一般会抽成枚举再配一个定时扫描把逾期状态自动置为 3。public enum BookState { AVAILABLE(0, 未借出), BORROWED(1, 已借出), RENEWED(2, 被续借), OVERDUE(3, 逾期), LOST(4, 丢失), DAMAGED(5, 损毁), OTHER(6, 其他); private final int code; private final String desc; BookState(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public static BookState of(int code) { for (BookState s : values()) { if (s.code code) return s; } return OTHER; } }逻辑说明枚举把 code 和描述绑在一起JSP 里用${BookState.of(book.state).desc}直接显示中文不用再写 if-else。参数上of 方法找不到时返回 OTHER避免抛异常导致页面崩。改造时把 SQL 里的数字换成#{state.code}或直接传 intMyBatis 对枚举有默认 TypeHandler也可以自定义。逾期扫描用一个 ServletContextListener 或 Timer 实现每天凌晨跑一次。// 在 contextInitialized 里启动定时任务 Timer timer new Timer(true); timer.scheduleAtFixedRate(new TimerTask() { Override public void run() { // 借出超过 30 天且 state1 的置为逾期 bookService.markOverdue(30); } }, 0, 24 * 60 * 60 * 1000);逻辑说明Timer 的 true 表示守护线程Tomcat 关闭时自动结束。scheduleAtFixedRate 第二个参数是首次延迟第三个是间隔毫秒。markOverdue 里执行UPDATE book SET state3 WHERE state1 AND DATEDIFF(NOW(), borrowBookDate) #{days}。参数上30 天是常见借阅周期按你学校规定改。注意 DATEDIFF 在 MySQL 里对字符串日期也能算但格式必须是 yyyy-MM-dd。验证改造是否成功可以手动把某本书的 borrowBookDate 改成 40 天前重启 Tomcat 看 state 有没有变 3。也可以写个单元测试直接调 markOverdue不依赖定时器。从那以后我每次拿到这种状态字段散落的项目都先抽枚举再动业务否则改到第三处就开始怀疑人生。希望帮到你。本文还有配套的精品资源点击获取