☰
Javaweb图书管理系统完整案例:Servlet+MyBatis+MySQL项目实战与避坑指南
2026/10/7 9:03:51 网站建设 项目流程

简介:这是一份面向Java Web初学者与在校学生的练手项目源码,对应大二上学期期末大作业——图书管理系统,适合用来巩固Servlet、JSP、MyBatis与Ajax等知识。项目围绕书籍、学生、管理员、留言四类实体展开,涵盖借还书日志记录、书籍状态管理、信誉等级与借阅数量控制、学号异步查重、Cookie记住账号、Session登录校验等典型业务逻辑,登录注册流程完整,可作为课程设计或自学练手的参考。资源包共59个文件,以26个java源码、13个html页面、7个xml配置、3个jsp及sql脚本、js、图片等为主,压缩包约879KB,结构清晰便于按模块阅读。目前已有1915人学习下载,读者可从中获取完整的项目分层结构、数据库建表脚本与前后端交互实现思路,对照理解Ajax异步验证、正则校验与登录状态保持等常见考点,快速搭建自己的Java Web实践项目。

1. 大二期末的 Javaweb 图书管理系统:一个能跑通的练手项目长什么样

如果你正在搜「javaweb项目完整案例mysql」,大概率是课程设计或期末大作业卡在某个环节了。这份图书管理系统(LMS-master)就是一个典型的大二上学期期末作业级别的 Javaweb 练手项目,技术栈是 Servlet + MyBatis + MySQL,用 Maven 管理依赖,前端是 JSP 页面配合 Ajax 异步交互。它不花哨,但把「学生注册登录、管理员登录、图书借还、留言、信誉积分」这条主线走通了,适合拿来当第一个完整案例拆解。

它解决的核心问题是:让你有一个能部署、能登录、能借书还书的闭环系统,而不是一堆散落的 CRUD 代码。适合谁?适合刚学完 Servlet 和 JDBC、想找一个结构完整但不复杂的项目练手的人;也适合需要交期末作业、想参考登录鉴权和借阅状态机怎么落地的人。源码包里带了mybatis.sql建表脚本和pom.xml,导入 IDEA 配好数据库就能跑,省去从零搭环境的折腾。

2. 环境搭建与数据库初始化:从 zip 到能登录的完整链路

拿到Javaweb图书馆.zip之后,别急着点运行。这个项目是标准 Maven 结构,src/main下分 Java 代码和资源文件,根目录有pom.xml和mybatis.sql。先把数据库跑起来,再让 IDEA 认识这个项目,顺序反了会多花半小时排查玄学问题。

2.1 导入 IDEA 与 Maven 依赖解析

解压后直接用 IDEA 的 Open 打开项目根目录(有pom.xml的那一层),不要用 Import Module 一层层点,容易把目录结构搞乱。打开后 IDEA 会自动识别为 Maven 项目并开始下载依赖,右下角有进度条。

如果依赖一直卡着不动,常见原因是 Maven 中央仓库网络慢。我一般会在settings.xml里配一个国内镜像,然后右键pom.xml→ Maven → Reload project 重新拉。依赖拉完后,检查 Project Structure 里的 SDK 是不是 1.8 或以上,这个项目的 Servlet 版本和 Tomcat 版本要匹配,Tomcat 9 配javax.servlet没问题,Tomcat 10 会因为包名从javax变成jakarta直接报 ClassNotFound,这是新手最容易翻车的地方。

<!-- pom.xml 里关键依赖,确认这几个坐标存在 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> <!-- provided 表示由 Tomcat 提供,不打进 war 包 --> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> </dependency>

scope设为provided的依赖不会被打进最终产物,因为 Tomcat 自己带了 Servlet API,重复打包反而冲突。MySQL 驱动版本要和你本地 MySQL 服务端版本对齐,8.0 的驱动连 5.7 的库通常没问题,但连接 URL 要加时区和 SSL 参数,否则启动就报时区错误。

2.2 执行 mybatis.sql 建表与数据核对

用 Navicat 或命令行登录 MySQL,新建一个库(比如library),字符集选utf8mb4,然后执行项目根目录的mybatis.sql。这个脚本会建出书籍、学生、管理员、留言等表。

# 命令行方式导入,避免图形工具编码问题 mysql -u root -p --default-character-set=utf8mb4 library < mybatis.sql

导入后一定要核对两件事:一是book表的state字段默认值,它决定了书籍初始状态;二是student表的credit、book_cnt、book_renewal这几个信誉相关字段有没有初始值。如果credit是 null,登录后借书逻辑里做数值比较时会抛空指针,页面直接 500。我一般会手动给测试账号补一个credit=100的初始值。

2.3 数据库连接配置与 Tomcat 部署

数据库连不上,后面全是白搭。找到项目里的 MyBatis 配置文件(通常是mybatis-config.xml或db.properties),把 URL、用户名、密码改成你本地的。

<!-- mybatis-config.xml 数据源部分 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/library?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> </dataSource>

serverTimezone不写会报The server time zone value is unrecognized,useSSL=false是本地开发省去证书校验。配好后在 IDEA 里配置 Tomcat:Run → Edit Configurations → 加一个 Tomcat Server → Local,Deployment 里加 Artifact(选war exploded),Application context 设成/或/lms。启动后浏览器访问http://localhost:8080/,能看到 index 介绍页就说明部署成功了。

3. 登录注册模块:Ajax + Session + Cookie 的落地细节

这个项目最有参考价值的部分就是登录注册,因为它把「前端异步校验 + 后端 Session 记录 + Cookie 记住账号」这条链路完整走了一遍。很多人写课程设计时登录就是 form 直接提交,页面刷新体验差,这里用 Ajax 提交 JSON,值得照着抄。

3.1 学生注册的 Ajax 异步校验与 JSON 提交

注册页面的核心诉求是:学号不能重复、密码长度受限、手机号和邮箱格式要对,而且这些校验要在不刷新页面的情况下完成。项目里明确要求不用 form 直接提交,而是用 Ajax 以 JSON 形式发到后台。

// 注册提交:先本地正则校验,再异步查重,最后提交 function checkNumber(number) { // 学号格式:假设 8 到 12 位数字,按你学校规则改 return /^\d{8,12}$/.test(number); } function register() { const data = { number: document.getElementById('number').value, username: document.getElementById('username').value, password: document.getElementById('password').value, tel: document.getElementById('tel').value, email: document.getElementById('email').value }; // 本地正则先拦一道,减少无效请求 if (!checkNumber(data.number)) { alert('学号格式不对'); return; } if (data.password.length < 6) { alert('密码至少6位'); return; } fetch('student?action=checkNumber&number=' + data.number) .then(res => res.text()) .then(text => { if (text === 'exist') { alert('学号已存在'); return; } // 学号可用,正式提交注册 return fetch('student?action=register', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }); }) .then(res => res && res.text()) .then(result => { if (result === 'success') { alert('注册成功'); location.href = 'login.jsp'; // 成功后跳登录页 } else { alert('注册失败,请重试'); // 失败留在当前页 } }); }

这里有两段请求:第一段查学号是否重复,第二段才是真正注册。分开的好处是查重可以单独触发(比如输入框失焦时),注册时再查一次防止并发。后端 Servlet 用request.getParameter或读 JSON body 拿到数据,调 MyBatis 的insert,返回纯文本success或fail。注意Content-Type必须是application/json,后端如果用getParameter读不到 body,得用流读取再解析,这是很多人卡住的地方。

3.2 学生登录:Session 记录与 Cookie 十天免登录

学生登录用学号 + 密码,登录成功后用 Session 存用户信息,同时可选地写一个加密 Cookie 实现「十天内记住账号」。这里要提醒一句:Cookie 存密码是绝对不行的,存的是服务端生成的令牌或加密后的标识。

// 登录 Servlet 核心逻辑 String number = request.getParameter("number"); String password = request.getParameter("password"); Student stu = studentService.login(number, password); if (stu != null) { HttpSession session = request.getSession(); session.setAttribute("student", stu); // 后续页面靠这个判断登录态 // 记住账号:写 Cookie,有效期 10 天 if ("true".equals(request.getParameter("remember"))) { Cookie cookie = new Cookie("studentNumber", number); cookie.setMaxAge(10 * 24 * 60 * 60); // 单位是秒 cookie.setPath("/"); response.addCookie(cookie); } response.getWriter().write("success"); } else { response.getWriter().write("fail"); // 前端在本页提示,不跳转 }

setMaxAge的单位是秒,10 天就是10*24*60*60,写成10*24*60就变成 10 分钟了,这个坑我踩过。Session 默认 30 分钟过期,如果用户操作到一半掉线,多半是 Session 超时,可以在web.xml里调session-timeout。登录失败时后端只返回fail,前端在当前页弹提示,不跳转,这样用户不用重新输一遍。

3.3 管理员登录与页面跳转控制

管理员登录逻辑和学生几乎一样,区别是查admin表、Session 里存的是admin对象。跳转控制上,学生登录成功进学生菜单页,管理员进管理页。项目里提到两者之间可以相互跳转,实现方式就是页面上放两个链接,或者用response.sendRedirect做重定向。

// 登录态拦截:在需要登录的 Servlet 或 Filter 里判断 Object user = request.getSession().getAttribute("student"); if (user == null) { response.sendRedirect("login.jsp"); // 没登录踢回登录页 return; }

常见做法是写一个 Filter 统一拦截/student/*和/admin/*路径,避免每个 Servlet 都写一遍判断。重定向用sendRedirect(地址栏变化),转发用forward(地址栏不变),登录成功后跳转用重定向更合适,防止用户刷新页面重复提交。

4. 图书借还与信誉积分:状态机与业务逻辑的坑

借书还书看着简单,其实是这个项目里业务逻辑最密的地方。书籍有 7 种状态,学生有信誉等级和借阅数量限制,这些字段之间互相牵制,写不好就是一堆 if-else 堆在一起,改一个地方崩三个地方。

4.1 书籍状态字段的设计与流转

Book类里state字段用整数表示状态:0 未借出、1 已借出、2 已续借、3 逾期、4 丢失、5 损毁、6 其他。这个设计的好处是查询方便,SELECT * FROM book WHERE state=0就能统计可借数量,不用额外维护一个总数。

// 借书:先判断状态,再更新 owner 和日期 public String borrowBook(Integer bookId, Integer studentId) { Book book = bookMapper.selectById(bookId); if (book.getState() != 0) { return "该书当前不可借"; // 状态不是 0 直接拒绝 } book.setState(1); book.setOwner(studentId); book.setBorrowBookDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); bookMapper.update(book); return "success"; }

还书时要把state改回 0,同时把owner、borrowBookDate、returnBookDate清空或置 null。这里有个坑:如果只改state不清owner,下次统计「谁借了书」时会把已还的书也算进去。续借则是把state改成 2,并延长归还日期,逻辑上要判断当前是否已逾期,逾期的不允许续借。

4.2 信誉积分与借阅数量限制

Student类里的credit、bookCnt、bookRenewal是联动的。信誉等级决定这个学生每月能借几本、能续借几次。常见做法是写一个方法根据credit返回可借数量。

// 根据信誉分返回每月可借数量 public int getMaxBorrow(Integer credit) { if (credit >= 90) return 5; if (credit >= 70) return 3; if (credit >= 50) return 2; return 1; // 信誉低于 50 只能借 1 本 }

借书前先查这个学生当前已借数量(SELECT COUNT(*) FROM book WHERE owner=? AND state IN (1,2)),再和上限比较。逾期不还会扣信誉分,扣到阈值以下就限制借阅。这套逻辑用数据库触发器或定时任务都能做,但课程设计阶段在 Service 层用 Java 判断就够了,别过度设计。

4.3 借还日志记录到文件

项目摘要里提到「可以将借书还书的活动写成日志,记录到某个文件当中」。这是个加分项,实现也简单,用 Java 的FileWriter追加写就行。

// 追加写借还日志,注意用 try-with-resources 自动关流 public void writeLog(String action, Integer bookId, Integer studentId) { String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " | " + action + " | bookId=" + bookId + " | studentId=" + studentId; try (FileWriter fw = new FileWriter("borrow.log", true); BufferedWriter bw = new BufferedWriter(fw)) { bw.write(line); bw.newLine(); } catch (IOException e) { e.printStackTrace(); // 日志失败不能影响主流程 } }

FileWriter第二个参数true表示追加模式,不写就每次覆盖。日志文件路径建议用绝对路径或配置项,用相对路径时工作目录取决于 Tomcat 启动位置,容易找不到文件。日志写入失败要 catch 住,不能让它把借书主流程带崩。

5. 避坑与排查:那些让项目跑不起来的常见问题

这一章全是血泪经验,每条都是「现象 → 原因 → 解决」的结构,照着排查能省不少时间。

现象:启动 Tomcat 报ClassNotFoundException: javax.servlet.http.HttpServlet。原因:Tomcat 10 及以上版本把 Servlet API 的包名从javax.servlet改成了jakarta.servlet,而项目用的是旧包名。 解决:换 Tomcat 9,或者把项目里所有javax.servlet改成jakarta.servlet并升级依赖版本。课程设计阶段建议直接用 Tomcat 9,改动最小。

现象:页面能打开,但一登录就 500,后台报NullPointerException。原因:多半是 MyBatis 映射文件的字段名和数据库列名对不上,或者credit等字段为 null 参与运算。 解决:检查resultMap里的column和实体类属性是否一一对应,给数据库里可能为 null 的数值字段设默认值 0。

现象:Ajax 提交注册,后端getParameter拿到的全是 null。原因:前端用application/json发的 body,getParameter只能读表单编码的参数,读不了 JSON body。 解决:后端改用request.getReader()读字符流,再用 JSON 库(如 fastjson、jackson)解析;或者前端改成application/x-www-form-urlencoded提交。

现象:中文书名或留言存进数据库变成乱码。原因:数据库、表、连接三处字符集不一致。 解决:建库时用utf8mb4,连接 URL 加characterEncoding=utf8,JSP 页面顶部设pageEncoding="UTF-8",三处统一。

现象:Cookie 记住账号功能失效,关掉浏览器再打开还是要重新登录。原因:setMaxAge参数写错单位,或者setPath没设导致路径不匹配。 解决:确认setMaxAge用的是秒(10 天 = 864000),setPath("/")让整个应用都能读到。

6. 进阶技巧:用 Filter 统一登录拦截与日志增强

项目跑通之后,想让代码更规范,我一般会做两件事:加一个登录拦截 Filter,把借还日志从「写文件」升级成「文件 + 控制台」双输出,方便调试。

6.1 登录拦截 Filter 的写法

不用在每个 Servlet 里判断 Session,写一个 Filter 拦所有请求,放行登录页、注册页和静态资源。

@WebFilter("/*") public class LoginFilter implements Filter { 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("register.jsp") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("action=login") || uri.contains("action=register")) { chain.doFilter(req, resp); return; } // 判断登录态 if (request.getSession().getAttribute("student") == null && request.getSession().getAttribute("admin") == null) { response.sendRedirect("login.jsp"); return; } chain.doFilter(req, resp); } }

@WebFilter("/*")表示拦截所有路径,放行规则用uri.endsWith或contains判断。注意别把登录请求本身也拦了,否则永远登不进去。这个 Filter 加上之后,新增页面不用再操心登录判断,代码干净很多。

6.2 日志增强与验证方法

把第 4 章的writeLog稍微改一下,同时输出到文件和控制台,调试时不用去翻文件。

public void writeLog(String action, Integer bookId, Integer studentId) { String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " | " + action + " | bookId=" + bookId + " | studentId=" + studentId; System.out.println("[BORROW-LOG] " + line); // 控制台实时看 try (FileWriter fw = new FileWriter("borrow.log", true); BufferedWriter bw = new BufferedWriter(fw)) { bw.write(line); bw.newLine(); } catch (IOException e) { System.err.println("日志写入失败:" + e.getMessage()); } }

验证方法很简单:借一本书,看控制台有没有输出,再去项目工作目录找borrow.log文件,确认内容追加而不是覆盖。还书后再看一次,确认两条记录都在。如果文件没生成,检查 Tomcat 的工作目录,IDEA 里默认是项目根目录,不是webapp目录。

6.3 参数速查表

配置项推荐值说明
Tomcat 版本9.x兼容 javax.servlet 包名
JDK 版本1.8项目依赖兼容性最好
MySQL 字符集utf8mb4支持中文和特殊字符
连接 URL 参数serverTimezone=Asia/Shanghai&useSSL=false避免时区和证书报错
Cookie 有效期864000 秒即 10 天
Session 超时30 分钟web.xml 可调

从那以后我每次拿到一个 Javaweb 项目,都强制先核对 Tomcat 版本和 Servlet 包名,再动数据库配置,最后才跑代码。顺序对了,能少熬好几个晚上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询