☰
JavaWeb图书管理系统源码解析:从数据库设计到部署避坑指南
2026/10/7 8:43:20 网站建设 项目流程

简介:JavaWeb图书管理系统源码及配套数据库、文档资料包,专门面向高校学生的期末大作业与课程设计需求。系统基于Java与Web技术,覆盖图书查询、借阅、归还等核心功能,代码注释清晰,适合小白学习与二次开发。压缩包内共278个文件,主要包含45个Java源码、19个JSP页面、15个Jar依赖、26个JavaScript脚本、75个GIF演示图以及9个CSS样式表,另含数据库SQL脚本和文档说明,总大小11.67MB,结构紧凑;已有73人浏览学习。文档说明详细描述了设计思路、数据库结构、模块划分和接口定义,数据库脚本可快速建表导入基础数据,附赠资源则提供界面美化辅助。整套项目完整可运行,既可作为高分课程设计参考模板,也能在此基础上扩展新功能,是学习JavaWeb开发的实用案例。

1. JavaWeb图书管理系统:一套能跑通毕业设计与企业内训的完整骨架

图书管理系统大概是 JavaWeb 领域里被复现次数最多的项目,没有之一。每年毕业季、课程设计、培训班结业,JavaWeb图书管理系统源码数据库文档说明这类词条都会集中爆发。原因不复杂:图书管理系统的业务边界足够清晰——图书增删改查、读者管理、借书还书、逾期统计,既能覆盖 Servlet + JSP + JDBC 的核心链路,又能把三层架构、MVC 思想、数据库设计串起来,不像电商系统那样堆满订单状态机和支付回调。我在带团队做内部培训时也常以它为原型,原因是它的功能规模刚好适合展示一个 JavaWeb 项目的完整生命周期:从建表到上线,每一步都能在一屏内讲完,不会把注意力耗散在复杂的领域规则上。

这篇文章直接回答四个问题:这套系统在做什么、源码里哪些部分值得细读、怎么把数据库和项目跑起来、以及最常见的翻车点在哪儿。如果你正打算用 JavaWeb 写图书管理系统,或者接手了别人留下的源码却不知道怎么启动,这篇能帮你省掉大半天的排查时间。文章里所有的路径、命令、代码片段,都按我实际部署过的方案来写,你可以直接照着抄,再根据自己的环境微调参数。

2. 源码结构拆解:先看懂 Maven 工程和分层思想再动手

拿到一份 JavaWeb 图书管理系统源码,第一件事不是急着跑起来,而是先花二十分钟看清目录结构。很多新手栽在第一步:因为用的是 Eclipse,导入后一堆红叉,其实不是代码错了,而是工程的构建方式没对齐。

2.1 Maven 目录与依赖:为什么课程设计普遍从纯 Servlet 转 Maven

早期的图书管理系统课程设计通常是一个普通的 Dynamic Web Project,WEB-INF/lib 下装着一堆手动拷贝的 jar 包:servlet-api、mysql-connector-java、jstl 等等。这种方式的痛点很快就会出现——jar 包版本冲突、换机器后忘记带 lib 目录、无法自动拉取依赖。所以在 2018 年之后的源码里,绝大多数都迁移到了 Maven 结构,核心标志是根目录下有一个 pom.xml。

标准的 Maven Web 工程长这样:

book-manager/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/book/ │ │ │ ├── dao/ # 数据访问层 │ │ │ ├── service/ # 业务层 │ │ │ ├── servlet/ # 控制器层 │ │ │ └── entity/ # 实体类 │ │ ├── resources/ │ │ │ ├── db.properties # 数据库连接参数 │ │ │ └── jdbc.properties │ │ └── webapp/ │ │ ├── static/ # CSS/JS/图片 │ │ ├── WEB-INF/ │ │ │ └── web.xml # Servlet 映射配置 │ │ └── index.jsp

pom.xml 里的关键依赖就几个,不需要额外引入 Spring 全家桶,否则就失去了这个项目的教学意义:

<dependencies> <!-- Servlet API,由 Tomcat 提供,作用域为 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- JSP 支持 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> </dependencies>

这段配置里有个细节值得注意:servlet-api 和 jsp-api 的 scope 必须是 provided。原因是 Tomcat 自己已经带了这两个 jar,如果你的工程里又以默认 scope 引入了,运行时就会出现类冲突,报java.lang.NoSuchMethodError或者一堆莫名其妙的ClassCastException,这是从 Eclipse 传统工程转 Maven 时最常遇到的坑。mysql-connector-java 的版本号建议和你本地的 MySQL 版本对应,5.7 用 8.0.x 驱动没问题,但反过来 MySQL 8.0 用 5.1.x 驱动会报时区错误。

2.2 实体类映射与数据流:从 JSP 到数据库的四层路径

这个项目的典型数据流是:JSP 页面发起请求 → Servlet 接收参数 → Service 层处理业务 → DAO 层操作数据库 → 返回结果渲染回 JSP。很多源码里省略了 Service 层,让 Servlet 直接调用 DAO,这在课程设计里可以接受,但如果你要做规范化项目,Service 层不能省,它承担了事务控制和方法复用两件事。

实体类对应数据库里的表,拿 Book 实体做例子:

package com.book.entity; import java.util.Date; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private Date publishDate; private Integer categoryId; private Integer stock; private Integer remaining; private String location; // 构造方法、getter/setter 省略 // 注意:属性名与数据库字段的驼峰-下划线映射关系 }

这里有个容易写错的细节:如果表的字段是category_id,实体类是categoryId,那么手写 JDBC 映射时要自己处理这种命名转换。有些源码用了 MyBatis 或 DbUtils 工具类,会自动做驼峰转换,但手写 JDBC 时漏掉一个字段,就会导致查询结果里某个属性永远为 null。排查这种问题时有一个土办法:在 DAO 层的返回路径上临时加打印,把 ResultSet 里取到的值逐字段输出,通常两轮就能定位。

DAO 层的标准写法是封装一个 BaseDao,把获取连接、关闭连接、执行增删改查这些 Boilerplate 代码抽出来,子类只写具体的 SQL。常见的做法是用 Apache Commons DbUtils,它的 QueryRunner 能把 ResultSet 自动映射到 Bean,比裸写 JDBC 省一半代码,同时保留了 SQL 的显式控制——这点很重要,ORM 框架的自动 SQL 在这个规模的项目里反而是累赘,出了问题不好回溯。

3. 数据库设计与初始化脚本:六张表搞定核心业务闭环

图书管理系统的数据库设计是这份源码里最值得抄的部分。一个功能完整的系统通常需要六张核心表,外加一到两张辅助表。很多网上流传的源码只给了三张表——用户表、图书表、借阅表——这样的设计做演示还行,一跑真实业务就会暴露问题:图书分类没有独立表,逾期罚金没有地方记录。

3.1 建表脚本与字段约束:为什么分类和借阅表要独立

下面是我通常用来初始化系统的 SQL 脚本,按依赖顺序执行:

-- 1. 创建数据库,UTF-8 字符集 CREATE DATABASE IF NOT EXISTS db_book_manager DEFAULT CHARACTER SET utf8mb4; USE db_book_manager; -- 2. 读者表(管理员和学生共用一个表,用 role 区分) CREATE TABLE t_reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '密码,建议 MD5 存储', real_name VARCHAR(50) NOT NULL COMMENT '真实姓名', role VARCHAR(20) DEFAULT 'STUDENT' COMMENT 'ADMIN 或 STUDENT', max_borrow INT DEFAULT 5 COMMENT '最大可借数量', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 3. 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT '分类名称', description VARCHAR(200) ) ENGINE=InnoDB; -- 4. 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT 'ISBN 编号', name VARCHAR(200) NOT NULL COMMENT '书名', author VARCHAR(100) COMMENT '作者', publisher VARCHAR(100) COMMENT '出版社', publish_date DATE COMMENT '出版日期', category_id INT COMMENT '外键,关联 t_category.id', stock INT DEFAULT 1 COMMENT '馆藏数量', remaining INT DEFAULT 1 COMMENT '可借数量', location VARCHAR(50) COMMENT '馆藏位置,如 A-01-02', INDEX idx_category (category_id) ) ENGINE=InnoDB; -- 5. 借阅记录表 CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATE NOT NULL COMMENT '借出日期', due_date DATE NOT NULL COMMENT '应还日期', return_date DATE COMMENT '实际归还日期,NULL 表示未还', status VARCHAR(20) DEFAULT 'BORROWING' COMMENT 'BORROWING/RETURNED/RENEWED', FOREIGN KEY (reader_id) REFERENCES t_reader(id), FOREIGN KEY (book_id) REFERENCES t_book(id) ) ENGINE=InnoDB; -- 6. 罚金记录表(超期或损坏赔偿) CREATE TABLE t_fine ( id INT PRIMARY KEY AUTO_INCREMENT, borrow_id INT NOT NULL, reader_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL DEFAULT 0, fine_type VARCHAR(30) COMMENT 'OVERDUE/DAMAGED/LOST', status VARCHAR(20) DEFAULT 'UNPAID' COMMENT 'UNPAID/PAID', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (borrow_id) REFERENCES t_borrow(id) ) ENGINE=InnoDB;

关于这个脚本有几点参数说明:密码字段的长度我特意留到 100,而不是 32。原因是你如果后续想把密码存储从 MD5 升级到 SHA-256 加盐,或者集成 Spring Security 的 BCrypt,长度不够就得改表结构。很多"毕业设计级"源码直接用 VARCHAR(32) 存 MD5,这在我见过的大部分项目里其实是没有问题的,但既然你已经看到这里,不如一开始就留够余地。借阅表里加了一个RENEWED状态,表示续借过,这在需求文档里经常被忽略,但实际运行一两周后就会用到——学生在借阅记录里续借是最常见的操作之一,没有这个状态就只能删除原记录再新增一条,历史痕迹就丢了。

3.2 JDBC 连接配置与连接池:db.properties 的坑和 Druid 的选择

数据源配置一般放在resources/db.properties,这是源码里必看的文件,因为这个文件里的配置写错,整个项目都会起不来。下面这份配置覆盖了 c3p0、dbcp、druid 三种常见连接池的参数写法,你自己择其一即可:

# 数据库连接基础配置 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/db_book_manager?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 # Druid 连接池参数 druid.initialSize=5 druid.maxActive=20 druid.minIdle=5 druid.maxWait=60000 druid.testWhileIdle=true druid.validationQuery=SELECT 1

配置里有几个版本相关的坑集中说一下。MySQL 5.7 虽然也可以用com.mysql.cj.jdbc.Driver,但如果你用的是 5.1.x 驱动,必须写成com.mysql.jdbc.Driver,写错了会报 ClassNotFoundException。MySQL 8.0 以上则强制使用 cj 驱动。serverTimezone=Asia/Shanghai这个参数在 MySQL 8.0 里不是可选的,你如果不加,连接时会直接报时区异常The server time zone value is unrecognized,这是在全网被问烂的第一大坑。useSSL=false也是为了减少本地的鉴权噪音,本地开发不需要 SSL。

密码这个字段,如果你是从网上下载的源码,最常见的情况是作者用的是 root/123456 或者 root/root,而你自己本地密码不一致。改完 db.properties 记得重新编译,因为配置文件在 resources 目录下,有些构建工具不会自动触发资源目录的重新拷贝。

关于连接池选型:对这个项目而言,Druid 是最稳妥的选择,因为它自带了监控页面,你可以直接访问/druid/index.html查看 SQL 执行次数和最慢的语句,这在调试时就是后悔药。c3p0 和 dbcp 也可以跑,但到了排查慢 SQL 的时候你就知道监控面板的好处了。

4. 核心功能逐个实现:从登录验证到图书借阅的完整链路

源码里的功能模块可以分成三块:登录与权限控制、图书管理(增删改查)、借还书流程。这三个模块覆盖了 JavaWeb 开发里最常用的技术点:Session、Filter、MVC 转发、参数绑定、事务。下面按模块拆解,每一步都有可用的代码骨架。

4.1 登录验证与 Session 控制:Filter 的作用域映射

登录接口是所有功能的基础,代码在 LoginServlet 里。这个类通常继承 HttpServlet,重写 doGet 和 doPost,处理逻辑集中在 doPost 上:

package com.book.servlet; import com.book.dao.ReaderDao; import com.book.entity.Reader; import com.book.util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { private final ReaderDao readerDao = new ReaderDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String code = request.getParameter("verifyCode"); // 1. 验证码校验,防止暴力破解 String sessionCode = (String) request.getSession().getAttribute("code"); if (sessionCode == null || !sessionCode.equalsIgnoreCase(code)) { request.setAttribute("error", "验证码不正确"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 2. 用户验证,密码加密后比对 Reader reader = readerDao.findByUsername(username); if (reader != null && reader.getPassword().equals(MD5Util.md5(password))) { // 登录成功,写入 Session HttpSession session = request.getSession(); session.setAttribute("loginUser", reader); session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作失效 // 记住登录状态一周,使用持久化 Cookie if ("on".equals(request.getParameter("rememberMe"))) { Cookie cookie = new Cookie("autoLogin", username); cookie.setMaxAge(7 * 24 * 3600); cookie.setPath(request.getContextPath()); response.addCookie(cookie); } response.sendRedirect(request.getContextPath() + "/index"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

这段代码里的几个关键参数值得展开说。setMaxInactiveInterval(30 * 60)是 Session 的过期时间,单位是秒,30 分钟对课程设计来说刚好,太短用户频繁被踢,太长内存里挂一堆无效会话。setPath(request.getContextPath())是 Cookie 的路径参数,必须是项目的上下文路径,否则 Cookie 在跨页面访问时会丢。MD5Util.md5()是写死的工具类,本质是对用户输入的密码做 MD5 然后和数据库里存的 MD5 值比对——密码不在数据库中以明文流通,这是所有 JavaWeb 项目的底线。MD5 本身不够安全这我知道,但课程设计级别用 MD5 是默认做法,如果你做企业项目,把 MD5Util 换成 BCrypt 那一套即可,调用方式和逻辑不用变。

没有 Filter 的登录验证是不完整的。下面的 AuthFilter 负责拦截未登录的请求:

package com.book.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 放行静态资源和登录相关请求 String uri = req.getRequestURI(); String ctxPath = req.getContextPath(); String path = uri.substring(ctxPath.length()); if (path.startsWith("/static/") || path.equals("/login") || path.equals("/register") || path.startsWith("/verifyCode")) { chain.doFilter(request, response); return; } // 检查用户是否已登录 HttpSession session = req.getSession(false); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(request, response); } else { resp.sendRedirect(ctxPath + "/login.jsp"); } } @Override public void init(FilterConfig filterConfig) { } @Override public void destroy() { } }

这个 Filter 有个隐患值得提:白名单里的路径必须和实际项目保持一致。如果项目里加了验证码生成接口/verifyCode,你漏放这个路径,用户就会被无限重定向到登录页,因为验证码请求也被拦截了。排查这类问题就一个思路:在 Filter 入口处打印 path 变量,看看实际请求路径是什么,再对比白名单。

4.2 图书管理模块:分页查询与条件搜索的组合

图书管理部分是源码里 DAO 层最典型的一段代码。分页是固定的三板斧:LIMIT 参数、总记录数 COUNT 查询、页码计算。下面是一段常见的分页查询写法:

public class BookDao { public List<Book> findPage(String keyword, int currentPage, int pageSize) throws SQLException { String sql = "SELECT * FROM t_book WHERE name LIKE ? OR author LIKE ? " + "ORDER BY id DESC LIMIT ?, ?"; String likePattern = "%" + keyword + "%"; // 使用 DbUtils 的 QueryRunner 简化 JDBC QueryRunner runner = new QueryRunner(DruidUtil.getDataSource()); return runner.query(sql, new BeanListHandler<>(Book.class), likePattern, likePattern, (currentPage - 1) * pageSize, pageSize); } public int count(String keyword) throws SQLException { String sql = "SELECT COUNT(*) FROM t_book WHERE name LIKE ? OR author LIKE ?"; QueryRunner runner = new QueryRunner(DruidUtil.getDataSource()); Long total = runner.query(sql, new ScalarHandler<>(), "%" + keyword + "%", "%" + keyword + "%"); return total.intValue(); } }

分页参数这里有一个最常见的经验值:pageSize 在图书管理列表页设为 10 是合理的,因为每行图书信息比较长,一页塞 20 条以上,用户看起来会很累。(currentPage - 1) * pageSize是 LIMIT 的 offset 计算公式,currentPage 从 1 开始,这是 MySQL 分页的习惯,配合前端的页码组件正好。LIMIT 的 offset 参数不能直接拼接字符串,否则就是 SQL 注入漏洞,上面的写法用了占位符,这是底线。

分页之外,新增图书时的 ISBN 重复校验也是一个容易忽略的点。合理的做法是执行 INSERT 之前先 SELECT 一次,确保 isbn 不重复。同时 t_book 表的 isbn 字段本身也要建立 UNIQUE 索引,双保险。并发场景下 SELECT 再 INSERT 会出现竞态,但对图书管理系统这种低并发场景,前置检查加唯一索引已经足够。

4.3 借书还书模块:事务边界和状态机的写法

借书是一个典型的需要事务的流程:先检查读者可借数量,再检查图书剩余库存,然后插入借阅记录,最后扣减图书库存。这四步操作任何一步失败,前面做的修改都要回滚。如果源码里每个 DAO 方法各开一个 Connection,那事务就是废的——四个方法各自提交,出了问题只能半成功。

标准做法是在 Service 层控制同一个 Connection:

public class BorrowService { public void borrowBook(int readerId, int bookId) throws Exception { // 获取连接,开启事务 Connection conn = DruidUtil.getConnection(); conn.setAutoCommit(false); try { // 按 id 锁定读者记录 ReaderDao readerDao = new ReaderDao(); // 借阅记录入库 BorrowDao borrowDao = new BorrowDao(); // 1. 查询并锁定读者 Reader reader = readerDao.findById(readerId); // 校验读者当前借阅数量 // 2. 查询并锁定图书 Book book = bookDao.findById(bookId); if (book.getRemaining() <= 0) { throw new BusinessException("图书已借完"); } // 3. 插入借阅记录,应还日期 = 当前日期 + 30 天 Date dueDate = DateUtil.addDays(new Date(), 30); borrowDao.insert(readerId, bookId, new Date(), dueDate); // 4. 扣减库存 bookDao.decreaseRemaining(bookId); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } } }

这里借期 30 天是从业务角度定的,如果你做的是高校图书管理系统,将借期改成 60 天更贴合实际,这是一个值得在教学演示之外留意的问题。事务的写法侧重理解conn.setAutoCommit(false)和conn.commit()这对方法。还有一个细节:Tomcat 默认的连接池配置在并发量上来后会出问题,这里使用 Druid 连接池替代 Tomcat 自带的配置方案,本质上是让数据库连接的管理权从容器手里转移到项目自己手里,这样事务的边界才能由代码完全控制。很多网上流传的源码把事务方法写在 DAO 里,四个 DAO 方法各开各的连接,那种写法跑演示没问题,并发一多就出脏数。

5. 部署与运行避坑:从 IDEA 到 Tomcat 的 5 个高频翻车现场

这一章集中写运行源码时最高频的报错和排查思路。每一条都是真实的踩坑记录,按"现象 → 原因 → 解决"三连写。

5.1 IDEA 导入 Maven 工程后依赖标红或缺失

现象是 pom.xml 文件打开后没有任何报错,但项目里所有 import 的类都标红,Maven 面板里 dependencies 列表是空的。

原因多半是 IDEA 没有正确识别 Maven 工程的根目录,或者是 Maven 配置指向了本地仓库之外的位置。检查顺序:File → Project Structure → Modules 里删掉多余的 root,重新 Add → Import Module 选择 pom.xml;然后检查 Maven settings 里的 local repository 路径和 settings.xml,如果手动配置过镜像仓库,确认阿里云镜像是https://maven.aliyun.com/repository/public而不是过时的http://maven.aliyun.com/nexus/content/groups/public,后者在 IDEA 新版里会被强制 HTTPS 拦截导致依赖拉不下来。

解决后执行一次mvn clean compile看控制台输出。这一步能暴露 90% 的环境问题,比在 IDE 里反复刷新面板高效得多。

5.2 Tomcat 启动后访问 404

现象是 Tomcat 正常启动,控制台没有异常,但访问http://localhost:8080/报 404,或者访问项目名报 404。

原因通常是 Artifact 没打对,或者部署描述符里的上下文路径和 URL 不一致。在 IDEA 里打开 Run → Edit Configurations,找到 Tomcat Server,看 Deployment 标签页下面的 Application context 是什么。如果源码里的请求路径是按/book-manager来写的,而你把 Application context 改成了/,那所有request.getContextPath()拼接出来的路径就全错了。

解决办法是把 Application context 改成和源码请求路径一致的名称。保持/book-manager不变,然后所有内部跳转都用request.getContextPath()这个动态前缀来拼接,不要在 JSP 里写死/book-manager/login,起码那样还有一处可改。

5.3 数据库连接报错 java.sql.SQLException: Access denied for user

现象是项目启动后第一次访问数据库相关页面直接报Access denied for user 'root'@'localhost' (using password: YES)或者Communications link failure。

原因参看三层:db.properties 里的用户名密码和本地 MySQL 不一致;MySQL 服务没启动;MySQL 8.0 驱动和 url 参数不匹配导致连接被拒。最高频的是第一层。用命令行测mysql -uroot -p你的密码能通,八成就是配置文件的问题。改完 db.properties 后,记得去 target/classes 目录确认修改生效,因为 IDEA 有时候不会自动同步 resources 目录的变更。

Access denied还有另一个隐藏原因:MySQL 8.0 默认的认证插件是caching_sha2_password,而你的驱动版本是 5.1.x,两者协商失败也会报这个错。解决要么换驱动版本,要么在 MySQL 里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';把认证方式切回去。

5.4 JSP 页面中文乱码,数据库里也是乱码

现象是页面上所有中文显示成问号,数据库里存进去的中文也是问号,但英文正常。

原因一般不是单一层面,而是从请求参数到数据库存储的整条链路里某一环断了。链路由四段组成:JSP 页面编码、请求参数编码、数据库连接编码、数据库表字符集。我通常按这个顺序排查:先确认 JSP 头部有<%@ page contentType="text/html;charset=UTF-8" language="java" %>;再确认 Tomcat 的 server.xml 里 Connector 增加了URIEncoding="UTF-8";然后检查 db.properties 里 url 带了useUnicode=true&characterEncoding=utf8;最后看建表语句是不是用了 utf8mb4。这条链上任何一处断了,结果就是乱码。

最隐蔽的一环是 server.xml。Tomcat 8 之后默认 URIEncoding 就是 UTF-8,但如果你用的是 Tomcat 7 或者某些改过配置的发行版,默认是 ISO-8859-1,GET 请求传递的中文参数在这里就会被解码成乱码。不要猜,直接在 server.xml 里显式加上URIEncoding="UTF-8"最省心。

5.5 高版本 JDK 运行旧源码报错 UnsupportedClassVersionError 或 IllegalAccessError

现象是控制台报java.lang.UnsupportedClassVersionError或者java.lang.IllegalAccessError: class X cannot access class Y。

原因通常是源码编译用的 JDK 版本和你本地运行环境的 JDK 版本不一致。很多课程设计源码是用 JDK 8 写的,你本地如果装的是 JDK 17 或者 21,直接跑就会出现UnsupportedClassVersionError。这个报错的含义是 class 文件的版本号高于 JVM 支持的最大版本号。

解决方法是调整项目的 language level 和 Java Compiler 的 target bytecode version。在 IDEA 里把 Project Structure 里的 SDK 改成 JDK 8,如果本地没有 JDK 8,可以用 OpenJDK 8 或者 Temurin 8。另一种做法是给 pom.xml 显式指定编译参数:

<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties>

但要注意:maven.compiler.source和target只影响编译产物,不影响 IDEA 本身的运行环境设置。你仍然需要把 Project SDK 和 Module SDK 都指到 JDK 8 上,IDEA 运行 Tomcat 时用的是 SDK 配置,不是 Maven 的编译参数。

上述五条坑的排查顺序是固定的:项目导入错误 → 启动配置错误 → 数据库连接错误 → 字符编码错误 → JDK 版本错误。如果把顺序反过来,你会浪费大量时间在一个并不存在的"代码 bug"上。

6. 进阶验证与优化:给源码加一个压测脚本和简单的日志追踪

把项目跑起来只是开始,真正验证它能不能用,需要做两件事:一个是功能路径验证,一个是轻量的性能压测。功能路径验证可以写成 Shell 脚本,用 curl 模拟登录、查询、借书、还书的一整条链路,这样每次改动代码后,回归测试只需要跑一遍脚本。

下面是一个可用的验证脚本骨架,按你的端口和上下文路径改一下就能用:

#!/bin/bash # 功能链路验证脚本 - 图书管理系统 BASE_URL="http://localhost:8080/book-manager" USERNAME="admin" PASSWORD="123456" # 1. 获取登录页的 Cookie curl -s -c cookies.txt "$BASE_URL/login.jsp" > /dev/null # 2. 模拟登录,-L 跟随重定向 curl -s -b cookies.txt -c cookies.txt -L -X POST \ -d "username=$USERNAME&password=$PASSWORD&verifyCode=0000" \ "$BASE_URL/login" > /tmp/login_result.html # 3. 检查登录结果是否包含期望的关键字 if grep -q "欢迎" /tmp/login_result.html; then echo "PASS: 登录成功" else echo "FAIL: 登录未成功,检查返回内容" exit 1 fi # 4. 访问图书列表第一页 curl -s -b cookies.txt "$BASE_URL/book/list?currentPage=1&pageSize=10" \ | grep -c "书名" || echo "WARN: 未找到书名关键字" # 5. 发起借书请求 curl -s -b cookies.txt -X POST \ -d "bookId=1&readerId=1" \ "$BASE_URL/borrow" | grep -q "借阅成功" \ && echo "PASS: 借书接口正常" \ || echo "FAIL: 借书接口异常"

这个脚本有几个实用的点:cookies.txt用于维持 Session,避免每次请求都要重新登录;verifyCode=0000是因为登录接口往往依赖验证码,你需要在本地把验证码逻辑改成固定值,或者关掉验证码校验,否则脚本没法自动化;grep 关键字要按你页面里的实际文案调整,中文关键字直接写进脚本没毛病,记得把脚本文件保存为 UTF-8。

压测方面不建议一上来就用 JMeter,开销太大。先给 Druid 开启监控页,在 db.properties 里加druid.web-stat-filter.enabled=true和druid.stat-view-servlet.enabled=true,启动后访问/druid/index.html,能看到每一页查询的 SQL 执行次数、最慢 SQL 的耗时。最慢的 SQL 通常是指定了LIMIT但扫描了全表的分页查询,原因是 t_book 表没有为name和author建立索引。在这个项目里,CREATE INDEX idx_book_name ON t_book(name)能解决 80% 的慢查询。图书数量级通常在几百到几千条,索引收益看似微薄,但一旦上万条数据后,全表扫描和索引查询的耗时差距会拉到 10 倍以上。这一点也算是我自己的血泪经验:给表加了合适的索引,比任何代码层面的奇技淫巧都靠谱得多。

最后说一个我自己以前吃过亏的经验:接手任何 JavaWeb 源码,第一件事是看pom.xml,第二件事是看db.properties,第三件事是运行最小测试用例验证数据库连通性。这三步走完,剩下的功能跑不起来,大概率是代码逻辑问题,走读两遍就能发现;反过来如果跳过这三步直接启动项目,你会在类冲突、端口占用、JDK 版本这种环境问题上耗掉一整天,最后发现源码根本没问题。希望这篇笔记能帮你把时间花在真正有产出的事情上——把功能跑通,然后往里面加你自己的设计和细节。好的图书管理系统源码,就是一个你可以在上面放心折腾的骨架。

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

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

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

立即咨询