ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb图书馆管理系统源码部署与借还权限排查指南

JavaWeb图书馆管理系统源码部署与借还权限排查指南

简介:面向Java Web课程设计、毕业设计与图书馆信息化入门实践的一套完整源码包,围绕图书信息、读者账户、借阅归还与权限管理展开,采用Servlet/JSP/JavaBean与MySQL实现典型的MVC分层。压缩包共209个文件,约1.91MB,以51个JSP页面、32个Java源文件与32个class文件为主体,另含MySQL数据表文件、9个依赖jar、SQL脚本及properties、tld、xml等配置,覆盖视图、业务逻辑、数据库与部署环节。已有359人学习,适合需要对照完整项目理解请求流转、数据库表关联、登录会话与JSTL/EL用法的读者。源码保留DAO、实体类与表单对象等分层结构,可据此梳理图书、读者、借阅模块的CRUD实现,并借助附带数据库文件快速还原运行环境,用于课程作业改造或二次开发参考。

1. 别急着双击:从 zip 到可登录后台,先判断这份 JavaWeb 图书馆系统源码能不能跑

把 zip 解压之后,看到一堆.jsp、.java、web.xml和sql文件,很多人第一反应是找main方法,结果当然找不到。这不是一个双击就能启动的桌面程序,而是一套典型的 JavaWeb 图书馆管理系统源码,MySQL 版本,靠 Servlet 容器跑起来。它适合课程设计、毕业设计原型、JavaWeb 全链路练手,也适合想补“登录、CRUD、借还书、分页、权限过滤”这条线的人。

判断它能不能用,先看三件事:WEB-INF/lib下有没有数据库驱动,src里有没有 JDBC 工具类或db.properties,根目录有没有.sql建库脚本。三样齐了,基本具备本地复现条件;缺驱动可以补,缺 SQL 文件就得从实体类反推表结构,成本立刻上去。下面按“拆骨架、搭环境、跑业务、排坑、二次验收”的顺序走,尽量让你少在 404 和中文乱码上耗时间。

2. 拆包看骨架:JSP+Servlet+JDBC 分层、web.xml 与数据库连接的落点

2.1 目录结构不是玄学:WEB-INF、lib、src、sql 各自管什么

老式 JavaWeb 项目最常见的结构是 Eclipse Dynamic Web Project 或 IDEA Web 项目:src放 Java 源码,WebContent或web放 JSP、CSS、JS,WEB-INF放web.xml和lib,sql放建库脚本。别小看目录名,导入 IDE 后 404 多半是 Web 根目录指错了。先把压缩包解压到纯英文路径,路径里不要带空格和中文,后面 Tomcat 部署会少一个玄学问题。

LibrarySystem/ ├── src/ │ ├── com/library/bean/ # 实体类:Book、User、Borrow │ ├── com/library/dao/ # DAO:JDBC 增删改查 │ ├── com/library/servlet/ # Servlet:接收请求、调 DAO、转发 JSP │ └── com/library/util/ # DBUtil、分页工具、编码过滤器 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、欢迎页、过滤器 │ │ └── lib/ # mysql-connector-java-*.jar │ ├── admin/ # 后台 JSP │ ├── login.jsp │ └── index.jsp └── sql/library.sql # 建库、建表、初始数据

这份结构不是强制标准,但能帮你快速定位:业务入口在servlet,SQL 在dao,连接参数在util或db.properties。如果lib下没有驱动 jar,不要急着从不明来源下载,优先用本机 Maven 仓库或 MySQL 官方驱动包,版本与 MySQL 服务端匹配即可。常见做法是 MySQL 5.7 配 5.1.x 驱动,MySQL 8.0 配 8.0.x 驱动,驱动类名分别是com.mysql.jdbc.Driver和com.mysql.cj.jdbc.Driver,写错就是ClassNotFoundException。

2.2 从 login.jsp 到 UserServlet:一次请求怎么穿过三层

别一上来改业务,先跟着登录请求走一遍。login.jsp表单提交到LoginServlet,Servlet 取username、password,调UserDao.findByUsernameAndPassword(),DAO 用PreparedStatement查user表,查到就写session,重定向到后台首页;查不到就转发回登录页带错误提示。这条链路跑通,说明容器、数据库、驱动、Session 都是活的。

// LoginServlet 的核心逻辑,省略异常处理 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 先设请求编码,再取参数 String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.login(username, password); // 内部用 PreparedStatement if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/admin/index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } }

逻辑说明:先setCharacterEncoding再取参数,顺序反了中文用户名可能已经乱码;登录成功用sendRedirect避免刷新重复提交,失败用forward保留请求属性。参数说明:req.getContextPath()拿到项目上下文,部署名改了也不用手改跳转路径;session里存的 key 要和后台 JSP 里取的 key 一致,常见是loginUser或user,不一致就是“登录成功但后台一直跳登录页”。

2.3 数据库连接与事务边界:JDBC 工具类要改哪几个参数

连接参数一般集中在DBUtil或db.properties。你要改的只有四项:URL、用户名、密码、驱动类。URL 里最容易翻车的是时区和编码:MySQL 8.0 常见写法要带serverTimezone=Asia/Shanghai,老项目如果没带,插入datetime可能报时区错误;useUnicode=true&characterEncoding=UTF-8用于避免存取中文乱码。不要直接把生产库密码写进代码,课程设计也养成放配置文件、加注释的习惯。

# db.properties 常见写法,按本机实际情况改 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码
// DBUtil 读取配置并获取连接,省略 close 封装 static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); }

逻辑说明:静态块只加载一次驱动和配置,getResourceAsStream从类路径找db.properties,放在src根下即可。参数说明:Class.forName在 JDBC 4.0 之后理论上可省略,但老项目显式写更稳;useSSL=false只为本地调试减少证书干扰,别照搬到线上。事务边界要放在 Service 或 Servlet 的一次业务操作里:借书时“插借阅记录 + 扣库存”必须同一个Connection,setAutoCommit(false)后统一commit,异常rollback。如果每个 DAO 方法各自拿连接,事务是假的,库存扣了记录没插,后面只能人肉对账。

3. 把环境搭起来:JDK、Tomcat、MySQL 与 IDE 的版本取舍和导入步骤

3.1 版本组合与坑位:JDK8/Tomcat8.5/MySQL5.7 还是 8.0

这类 JSP+Servlet 项目,我一般先按 JDK8 + Tomcat8.5 + MySQL5.7 试,因为老web.xml、老 JSP 标签、老驱动在这套组合下兼容性最好。不是绝对,但能少踩javax.servlet包名、Unsupported major.minor version、JSP 编译级别不一致的坑。如果你本机只有 MySQL8.0,也能跑,注意驱动换 8.0.x、URL 加时区、driver写com.mysql.cj.jdbc.Driver。JDK 高于 8 时,先看项目编译级别,别让 IDE 用 17 编译、Tomcat 用 8 跑。

组件保守组合说明
JDK8老 JSP/Servlet 项目兼容性最好
Tomcat8.5支持 Servlet 3.1,web.xml写法通用
MySQL5.7 或 8.08.0 要换驱动、加时区
IDEIDEA/Eclipse能配 Artifact 或 Deployment Assembly 即可

提示:不要同时装多个 Tomcat 还都开 8080,启动日志里Address already in use先查端口,不要先怀疑代码。

3.2 建库、导 SQL、改 db.properties:三步让项目连上本地库

先建库再导表。用命令行或客户端都行,命令行更直观:登录 MySQL,source指向解压后的sql文件。导入后use library; show tables;确认表存在。常见表有user、book、borrow、category,字段名以 SQL 文件为准,不要凭实体类猜。然后改db.properties的库名、用户名、密码,库名要和 SQL 文件里的CREATE DATABASE或USE一致。

# 登录 MySQL 后执行,路径换成你解压后的 sql 文件 mysql -u root -p CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; USE library; SOURCE D:/work/LibrarySystem/sql/library.sql; SHOW TABLES;

逻辑说明:先显式建库并指定字符集,避免 SQL 文件里没写库名时导到别的库;SOURCE是 MySQL 客户端命令,不是 SQL 语句,路径用正斜杠或双反斜杠。参数说明:utf8mb4比utf8更稳,能存 emoji 和生僻字;如果 SQL 文件里已有CREATE DATABASE,重复执行可能报库已存在,先看文件头再决定是否手动建。导完一定要抽查book表有没有初始数据,空表登录进去看不到书,不一定是查询写错。

3.3 IDEA/Eclipse 导入与 Artifact 部署:context path 和端口别撞车

IDEA 导入时选“从现有源码导入”,把src标成 Sources Root,WebContent或web标成 Web Resource Directory,WEB-INF/lib里的 jar 加进模块依赖。然后配 Tomcat Server,Deployment 里加 Artifact,Application context 建议设成/library,别用/,否则 JSP 里写死的/admin/...路径可能对不上。Eclipse 则是 Dynamic Web Project 的 Deployment Assembly,把src编译输出和WebContent映射到/。

<!-- web.xml 里至少确认 welcome-file 和 servlet 映射 --> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.library.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

逻辑说明:welcome-file决定访问http://localhost:8080/library/时先打开哪个页面;url-pattern决定表单action写/login还是/loginServlet。参数说明:如果项目用注解@WebServlet("/login"),web.xml里重复映射会冲突,二者留一种;JSP 里的action="${pageContext.request.contextPath}/login"比写死/library/login更耐改。部署后先访问欢迎页,能出登录框再谈登录逻辑,出不来先看 Tomcat 日志里项目有没有启动成功。

4. 核心业务跑通:图书 CRUD、借阅归还、分页搜索与权限拦截

4.1 图书表与借阅表怎么关联:外键、状态字段和库存扣减

图书馆系统的核心不是“能增删改查图书”,而是“借出去还能还回来,库存和记录对得上”。常见设计是book表存stock或total,borrow表存book_id、user_id、borrow_time、return_time、status。借书时先查库存是否大于 0,再插借阅记录、扣库存;还书时更新借阅状态、回加库存。外键看项目 SQL 有没有建,没建也要在代码里保证book_id存在,否则借阅记录就是孤儿数据。

-- 借书前的库存检查与扣减,实际要放在同一事务里 SELECT id, stock FROM book WHERE id = ? FOR UPDATE; UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0; INSERT INTO borrow(user_id, book_id, borrow_time, status) VALUES (?, ?, NOW(), 'BORROWED');

逻辑说明:FOR UPDATE在事务里锁住当前图书行,防止两个人同时借最后一本;stock > 0是第二次防线,更新影响行数为 0 就说明库存没了,直接回滚。参数说明:status用枚举字符串或数字都行,关键是全项目统一,JSP 显示时别一个地方写1一个地方写BORROWED。如果项目没有stock字段,而是用total - 已借数实时算,SQL 会复杂一些,但事务边界同样不能少。

4.2 分页查询的 SQL 与 Servlet 参数:page、pageSize 和总数

图书列表不分页,数据一多就卡,演示也难看。分页公式很固定:page从 1 开始,pageSize默认 10,offset = (page - 1) * pageSize。先COUNT(*)拿总数算总页数,再LIMIT offset, pageSize拿当前页。Servlet 里对page做空值和越界保护,别让用户手改 URL 传page=-1把 SQL 搞崩。

// BookServlet 列表方法的核心参数处理 int page = 1; int pageSize = 10; String pageStr = req.getParameter("page"); if (pageStr != null && pageStr.matches("\\d+")) { page = Integer.parseInt(pageStr); } int total = bookDao.count(keyword); int totalPages = (total + pageSize - 1) / pageSize; if (page < 1) page = 1; if (page > totalPages && totalPages > 0) page = totalPages; List<Book> books = bookDao.findPage(keyword, (page - 1) * pageSize, pageSize); req.setAttribute("books", books); req.setAttribute("page", page); req.setAttribute("totalPages", totalPages); req.getRequestDispatcher("/admin/bookList.jsp").forward(req, resp);

逻辑说明:matches("\\d+")过滤负数和非数字;totalPages用向上取整,避免 11 条数据只显示 1 页;越界回正防止空页。参数说明:keyword要同时传给count和findPage,否则总数和列表对不上;LIMIT的两个参数在 MySQL 里是offset, size,别写成page, pageSize。JSP 里翻页链接要带keyword,不然搜完点下一页搜索条件丢了。

4.3 借书还书为什么要放事务里:伪代码与回滚点

借还书是典型的多表操作,最怕“借阅记录插了、库存没扣”或者“库存扣了、记录没插”。把两次写操作放进同一个Connection,关自动提交,成功一起提交,失败一起回滚。下面用伪代码表达,重点是连接传递和异常回滚,不要在每个 DAO 里各自getConnection()。

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); int updated = bookDao.decreaseStock(conn, bookId); // stock - 1 if (updated == 0) { throw new RuntimeException("库存不足"); } borrowDao.insert(conn, userId, bookId); // 插借阅记录 conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw new ServletException("借书失败", e); } finally { DBUtil.close(conn); // 记得恢复 autoCommit 再关 }

逻辑说明:decreaseStock返回影响行数,0 表示库存不足或书不存在,直接抛异常触发回滚;insert和decreaseStock必须接收同一个conn。参数说明:finally里关闭连接前最好setAutoCommit(true),如果项目用了连接池,不恢复会影响下一个借连接的人。还书同理:更新borrow.status和book.stock + 1要在一个事务里,别只改状态不还库存。

4.4 登录与权限过滤:Filter 拦 /admin/* 的最小实现

后台页面不能靠“登录后不显示链接”来保护,直接输 URL 也能进。加一个Filter拦/admin/*,从session取登录用户,没有就重定向到登录页。过滤器在web.xml里配,或者用@WebFilter注解。注意放行登录页、静态资源和登录 Servlet,别把登录请求也拦了,否则永远登不进去。

@WebFilter("/admin/*") public class AdminFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; Object user = req.getSession().getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

逻辑说明:@WebFilter("/admin/*")只拦后台路径,登录页和前台不受影响;取不到用户就重定向,return防止继续往下走。参数说明:loginUser要和登录 Servlet 里存的 key 完全一致;如果项目还有管理员和普通用户角色,在 Filter 里再加角色判断,别只判断有没有登录。过滤器生效前提是web.xml的metadata-complete没设为true,否则注解不扫描。

5. 避坑与排查:中文乱码、驱动报错、404/500、日期与金额的 5 个血泪现场

5.1 中文乱码:request、response、JDBC URL、JSP 四处都要对齐

现象:登录页输入中文书名能查到,但新增图书后列表里显示问号;或者表单提交的中文参数到 Servlet 变成乱码。原因通常不是一处,而是请求编码、响应编码、数据库连接编码、JSP 页面编码四个地方各说各话。解决顺序是先看request.setCharacterEncoding("UTF-8")有没有在取参数前执行,再看 JSP 顶部pageEncoding="UTF-8",然后看 JDBC URL 有没有characterEncoding=UTF-8,最后看库和表是不是utf8mb4。

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
// 过滤器里统一设编码,比每个 Servlet 手写更稳 req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8");

逻辑说明:JSP 的pageEncoding管源文件编译编码,contentType管响应头;过滤器统一处理能避免漏改某个 Servlet。参数说明:Tomcat8 之后 GET 请求默认按 UTF-8 解,老 Tomcat7 可能还要在server.xml的 Connector 上加URIEncoding="UTF-8"。如果只有 POST 乱、GET 不乱,优先查请求体编码;如果数据库里已经是问号,改代码也救不回来,只能重新录入。

5.2 ClassNotFound/驱动版本:mysql-connector 放哪、Driver 写什么

现象:启动或第一次连库时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,或者No suitable driver found。原因无非三种:驱动 jar 没放进WEB-INF/lib;放进了但没加到部署产物;驱动版本和类名不匹配。MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,还写老类名就可能找不到。解决:确认 jar 在WEB-INF/lib,IDEA 的 Artifact 里能看到,db.properties的jdbc.driver改成对应类名,URL 加serverTimezone。

检查顺序: 1. WEB-INF/lib 下有没有 mysql-connector-java-*.jar 2. IDEA -> Project Structure -> Artifacts -> WEB-INF/lib 是否包含该 jar 3. db.properties 的 driver 与 jar 版本是否匹配 4. 重启 Tomcat,不要只点重新部署

逻辑说明:很多“明明放了 jar 还报错”是 Artifact 没包含lib,编译能过、运行找不到。参数说明:不要同时放 5.1 和 8.0 两个驱动,类路径冲突时谁先加载看运气;useSSL=false只建议本地用,线上按实际证书配置。改完配置一定重启容器,热部署对WEB-INF/lib不一定生效。

5.3 404 与 500:context path、web.xml、异常栈先看哪一行

现象:访问http://localhost:8080/library/login.jsp报 404;或者登录后 500,页面只给一句“服务器内部错误”。原因:404 多半是 context path 不对、Web 根目录没映射、JSP 放错位置;500 要看 Tomcat 日志里第一行Caused by,常见是空指针、SQL 语法错、列名不对。解决:先访问http://localhost:8080/看 Tomcat 首页,再访问项目根看欢迎页;500 去logs/localhost.*.log找异常栈,从最底层Caused by往上读。

# Linux/macOS 下快速看日志尾部 tail -n 100 tomcat/logs/localhost.$(date +%Y-%m-%d).log

逻辑说明:404 是路径问题,500 是代码或 SQL 问题,别混在一起猜;日志里java.sql.SQLSyntaxErrorException通常直接告诉你哪个表或列不存在。参数说明:context path在 IDEA 的 Deployment 里改,改完浏览器地址也要跟着改;JSP 放在WEB-INF下不能被浏览器直接访问,这是正常的,要通过 Servlet 转发。

5.4 日期与库存:借阅时间、归还状态、并发扣减的坑

现象:借书后borrow_time显示成前一天或0000-00-00;还书后库存加了两次;两个人同时借最后一本,库存变成 -1。原因:日期字段用java.util.Date和java.sql.Date混用、时区不一致;还书逻辑没判断当前状态,重复点击重复加库存;扣库存没加stock > 0条件或没锁行。解决:数据库用DATETIME,Java 用LocalDateTime或java.sql.Timestamp,JDBC URL 统一时区;还书先UPDATE borrow SET status='RETURNED' WHERE id=? AND status='BORROWED',影响行数为 1 才加库存;借书用事务加FOR UPDATE或stock > 0条件更新。

-- 还书:先改状态,成功才加库存 UPDATE borrow SET status = 'RETURNED', return_time = NOW() WHERE id = ? AND status = 'BORROWED'; -- 影响行数为 1 再执行 UPDATE book SET stock = stock + 1 WHERE id = ?;

逻辑说明:状态条件保证重复还书不会重复加库存;两条 SQL 仍要放同一事务。参数说明:NOW()用数据库时间,比 Java 端new Date()更少时区争议;如果项目字段是return_date不是return_time,以 SQL 文件为准,别照抄。

5.5 SQL 注入与 XSS:课程设计里最容易被忽略的两处

现象:搜索框输入' OR '1'='1能把所有书查出来;图书简介里写一段<script>,列表页弹窗。原因:DAO 用字符串拼接 SQL,JSP 用${book.desc}直接输出没转义。解决:所有带参数的 SQL 用PreparedStatement,不要拼"where name like '%" + keyword + "%'";JSP 输出用<c:out value="${book.desc}"/>或fn:escapeXml。课程设计也要养成习惯,后面加评论、公告功能时更明显。

// 错误写法:拼接 SQL String sql = "select * from book where name like '%" + keyword + "%'"; // 正确写法:占位符 String sql = "select * from book where name like ?"; ps.setString(1, "%" + keyword + "%");

逻辑说明:PreparedStatement会把参数当数据而不是 SQL 片段,%由 Java 拼在参数值里,不破坏 SQL 结构。参数说明:LIKE的%不要拼进 SQL 字符串;如果项目用 MyBatis,#{}是预编译,${}是拼接,搜索框别用${}。XSS 方面,后台录入的简介、公告都要在输出时转义,别只在保存时替换几个字符。

6. 二次开发与验收:把课程设计改成能演示的图书馆系统

6.1 加一个借阅历史查询:从 SQL 到 JSP 的最小改动

想加“我的借阅历史”,不用动大架构。先写 SQL 联表查:borrow关联book,按user_id和借阅时间倒序;DAO 加一个findByUserId;Servlet 从 session 取用户 id,塞进 request;JSP 用<c:forEach>渲染。改动点控制在三处,别顺手重构登录,演示前重构是大忌。

SELECT b.id, bk.name AS book_name, b.borrow_time, b.return_time, b.status FROM borrow b JOIN book bk ON b.book_id = bk.id WHERE b.user_id = ? ORDER BY b.borrow_time DESC;

逻辑说明:联表拿书名,前端不用再根据book_id查一次;ORDER BY让最近借的在前。参数说明:user_id从 session 取,不要从 URL 传,否则改个参数就能看别人的借阅记录。JSP 里状态用<c:choose>转成“借阅中/已归还”,比直接显示BORROWED更像演示。

6.2 验收清单:10 分钟走完登录、检索、借还、权限

验收项操作通过标准
登录正确/错误密码各一次正确进后台,错误有提示
图书检索搜书名关键字、翻页结果和总数一致,翻页不丢条件
借书借一本有库存的书借阅记录新增,库存减 1
还书重复点还书只加一次库存,状态正确
权限退出后直接访问/admin/index.jsp跳回登录页
乱码新增中文书名并搜索列表显示正常

这张表我一般演示前走一遍,比临时点页面稳。尤其“重复还书”和“退出后直接访问后台”两个点,最容易在答辩时被问到。

6.3 我踩过的部署习惯:每次改配置先备份、先看日志

从那以后我每次改db.properties、web.xml或驱动版本,都先复制一份.bak,再重启 Tomcat 看日志第一屏。因为 JavaWeb 项目的故障经常不是代码写错,而是部署产物没更新、端口被占、驱动没进lib。先看日志能省掉一半“重启试试”的时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进