news 2026/9/24 18:18:01

JavaEE图书管理系统实战:Spring Boot+MyBatis选型、并发借阅与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaEE图书管理系统实战:Spring Boot+MyBatis选型、并发借阅与避坑指南

简介:这是一套面向JavaEE初学者与课程设计者的图书管理系统完整源码,基于MVC三层架构实现,适合用于毕业设计、课程作业或企业级开发入门练手。压缩包共93个文件,约6.62MB,以java源文件、jsp页面、xml配置、class字节码及jar依赖包为主,涵盖Servlet、JDBC、Struts、Spring等核心技术的实际运用,并附带数据库脚本与前端样式资源。系统按表现层、业务逻辑层与数据访问层组织,包含登录、图书管理、用户管理、借阅记录等模块,目录结构清晰,导入Eclipse或IntelliJ IDEA即可运行调试。目前已有145人学习下载,读者可借此理解JavaEE项目的分层设计、请求处理流程与数据库交互方式,快速掌握从配置到部署的完整开发链路,是提升企业级开发能力的实用参考案例。

1. 从一份「图书管理系统.zip」说起:JavaEE 课设到底在考什么

很多同学拿到「基于 JavaEE 的图书管理系统.zip」这个标题时,第一反应是去搜现成源码,解压、改个包名、跑起来截图交差。但真正做过一轮的人会发现,答辩时老师问的从来不是「你用了什么框架」,而是「借书和还书时库存怎么保证不超卖」「并发下同一本书被两个人同时借走你怎么处理」。这个标题背后考的是一套完整的 JavaEE 分层开发能力:Servlet 或 Spring MVC 做控制层、JSP 或 Thymeleaf 做视图、JDBC 或 MyBatis 做持久化、MySQL 存数据,再配上登录鉴权和借阅业务逻辑。它适合课程设计、毕业设计入门,也适合想从「只会写 main 方法」过渡到「能搭一个完整 Web 应用」的开发者。下面我按自己带学生做课设的路径,把选型、建库、编码、排错一条线讲清楚。

2. 技术选型与开发环境:别在第一步就把自己坑死

2.1 为什么我建议用 Spring Boot + MyBatis 而不是纯 Servlet

标题写的是 JavaEE,严格意义上 Servlet、JSP、EJB 都属于 JavaEE 规范。但如果你现在还用纯 Servlet 手写doGet/doPost,再配一堆web.xml映射,代码量会爆炸,调试也痛苦。常见做法是用 Spring Boot 把 Tomcat 内嵌进去,用 Spring MVC 接管请求分发,用 MyBatis 做 SQL 映射。这样你依然在写 JavaEE 的 Web 应用,只是把容器和配置的脏活交给了框架。

选型对比可以看这张表:

方案开发速度配置复杂度适合场景
纯 Servlet + JSP高,web.xml 冗长教学演示底层原理
Spring MVC + JSP中,需配 DispatcherServlet传统课设
Spring Boot + Thymeleaf低,约定优于配置推荐,快速出活
Spring Boot + Vue 前后端分离中,需处理跨域想加分可尝试

我一般会选 Spring Boot 2.7.x 配 MyBatis,JDK 用 8 或 11,数据库 MySQL 5.7 或 8.0。注意 Spring Boot 3.x 要求 JDK 17,如果你学校机房还是 JDK 8,就别追新。

2.2 用 VS Code 配 JavaEE 环境的最小步骤

热搜里有人搜「vscode 配置 javaee 语言环境」,说明不少人不想装笨重的 Eclipse 或 IDEA。VS Code 确实能跑,但插件要装对。步骤如下:

第一步,安装 Extension Pack for Java,它会自动带上 Language Support、Debugger、Test Runner、Maven 支持。

第二步,安装 Spring Boot Extension Pack,提供 Spring Boot 项目的创建和运行支持。

第三步,确认settings.json里 Java 运行时指向正确的 JDK:

{ "java.configuration.runtimes": [ { "name": "JavaSE-1.8", "path": "C:\\Program Files\\Java\\jdk1.8.0_301", "default": true } ], "java.compile.nullAnalysis.mode": "automatic" }

这段配置告诉 VS Code 用哪个 JDK 编译和运行。path要换成你本机实际安装路径,Windows 用双反斜杠或正斜杠。default: true表示这是默认运行时。如果配错,会出现「The project was not built since its build path is incomplete」这类报错。

第四步,用Ctrl+Shift+P打开命令面板,输入Spring Initializr创建项目,勾选 Spring Web、MyBatis Framework、MySQL Driver、Thymeleaf。生成后 VS Code 会自动识别为 Maven 项目。

提示:VS Code 跑 Spring Boot 项目时,如果控制台中文乱码,在application.properties里加server.servlet.encoding.charset=UTF-8server.servlet.encoding.force=true

2.3 数据库建表:图书、用户、借阅记录三张核心表

图书管理系统的数据模型不复杂,但字段设计直接影响后面业务能不能写顺。我一般建三张表:bookuserborrow_record

CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), total_copies INT DEFAULT 1, available_copies INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT 'reader', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME, status VARCHAR(20) DEFAULT 'borrowed', FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

book表里total_copies是馆藏总数,available_copies是可借数量,这两个字段分开是为了处理「借出后库存减少但总数不变」的逻辑。borrow_recordstatus标记借阅状态,borrowed表示未还,returned表示已还。外键约束保证不会出现借阅记录指向不存在的书。字符集用utf8mb4而不是utf8,否则书名里的生僻字或 emoji 会插入失败。

3. 借阅业务的核心代码:从 DAO 到 Service 再到 Controller

3.1 MyBatis 映射文件与库存扣减 SQL

持久层我用 MyBatis 的 XML 映射,因为 SQL 可控,调优方便。先看BookMapper.xml里最关键的库存扣减语句:

<update id="decreaseAvailable"> UPDATE book SET available_copies = available_copies - 1 WHERE id = #{bookId} AND available_copies > 0 </update> <update id="increaseAvailable"> UPDATE book SET available_copies = available_copies + 1 WHERE id = #{bookId} AND available_copies &lt; total_copies </update>

decreaseAvailableWHERE条件里带了available_copies > 0,这是防超卖的关键。如果只写WHERE id = #{bookId},两个线程同时读到库存为 1,都执行扣减,库存就变成 -1。加上这个条件后,数据库行锁会保证只有一个事务能更新成功,另一个返回影响行数 0。increaseAvailable里的available_copies < total_copies是防止还书时库存超过馆藏总数。

对应的 Mapper 接口:

@Mapper public interface BookMapper { int decreaseAvailable(@Param("bookId") Integer bookId); int increaseAvailable(@Param("bookId") Integer bookId); Book selectById(@Param("id") Integer id); }

@Param注解让 XML 里可以用#{bookId}引用参数。如果方法只有一个参数且是基本类型,MyBatis 也能识别,但多参数时必须加,否则报Parameter 'bookId' not found

3.2 Service 层事务控制:借书方法的完整实现

Service 层是业务逻辑的核心,借书操作要同时做三件事:插入借阅记录、扣减库存、返回结果。这三步必须在一个事务里。

@Service public class BorrowService { @Autowired private BookMapper bookMapper; @Autowired private BorrowRecordMapper recordMapper; @Transactional(rollbackFor = Exception.class) public String borrowBook(Integer userId, Integer bookId) { // 先扣库存,利用数据库行锁保证原子性 int affected = bookMapper.decreaseAvailable(bookId); if (affected == 0) { return "库存不足或图书不存在"; } // 库存扣减成功后再插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setStatus("borrowed"); recordMapper.insert(record); return "借阅成功"; } }

@Transactional注解让方法内的数据库操作要么全成功要么全回滚。rollbackFor = Exception.class表示遇到任何异常都回滚,默认只回滚RuntimeException,如果抛的是检查型异常不会回滚,这是个常见坑。先扣库存再插记录的顺序也有讲究:如果先插记录再扣库存,扣库存失败时记录已经插入,虽然事务会回滚,但高并发下锁竞争更激烈。先扣库存能让失败路径更短。

3.3 Controller 层参数接收与统一返回格式

控制层负责接收前端请求、调用 Service、返回 JSON。我用一个简单的Result类统一返回格式:

@RestController @RequestMapping("/api/borrow") public class BorrowController { @Autowired private BorrowService borrowService; @PostMapping("/do") public Result borrow(@RequestParam Integer userId, @RequestParam Integer bookId) { if (userId == null || bookId == null) { return Result.error("参数缺失"); } String msg = borrowService.borrowBook(userId, bookId); return "借阅成功".equals(msg) ? Result.ok(msg) : Result.error(msg); } }

@RestController等于@Controller@ResponseBody,返回值自动序列化成 JSON。@RequestParam接收表单或 URL 参数,如果前端用 JSON body 传参,要改成@RequestBody配一个 DTO 对象。参数校验我放在 Controller 里做简单判空,复杂校验用@Valid注解加 JSR-303 实现。

Result类结构:

public class Result { private int code; private String msg; private Object data; public static Result ok(Object data) { Result r = new Result(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static Result error(String msg) { Result r = new Result(); r.code = 500; r.msg = msg; return r; } // getter/setter 省略 }

这样前端拿到code判断成功失败,msg展示提示,data渲染列表。比直接返回字符串规范,也方便后面加分页数据。

4. 登录鉴权与并发借阅:两个最容易翻车的地方

4.1 Session 登录拦截器的配置与放行规则

图书管理系统一般分管理员和读者两种角色,登录后才能借书。我用 Session 存用户信息,配一个拦截器校验。

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}"); return false; } return true; } }

拦截器注册:

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register"); } }

addPathPatterns("/api/**")表示拦截所有 API 请求,excludePathPatterns放行登录和注册接口,否则用户永远登不进来。这里有个血泪经验:如果你把静态资源路径也拦了,登录页的 CSS 和 JS 加载不出来,页面会裸奔。所以拦截路径要精确到/api/**,不要图省事写/**

4.2 用数据库行锁解决并发借同一本书

前面 Service 层的decreaseAvailable已经用了条件更新来防超卖,但还有一种情况:两个用户同时借同一本书,库存都够,但借阅记录插入顺序和库存扣减顺序不一致,导致某个用户看到「借阅成功」但记录里没有。这需要用数据库事务隔离级别和行锁来保证。

MySQL InnoDB 默认隔离级别是REPEATABLE READUPDATE语句会对匹配的行加排他锁。当第一个事务执行UPDATE book SET available_copies = available_copies - 1 WHERE id = 1 AND available_copies > 0时,id=1 这行被锁住,第二个事务的相同 UPDATE 会阻塞,直到第一个事务提交。提交后第二个事务重新读取最新库存,如果库存为 0,affected返回 0,借阅失败。整个过程不需要应用层加锁。

如果你用的是 MyBatis 二级缓存,注意UPDATE会清空缓存,但查询缓存可能返回旧数据。我一般直接关掉二级缓存,在application.properties里配mybatis.configuration.cache-enabled=false,避免缓存和数据库不一致。

4.3 还书逻辑与逾期判断

还书比借书多一步:更新借阅记录状态和还书时间,同时恢复库存。

@Transactional(rollbackFor = Exception.class) public String returnBook(Integer recordId) { BorrowRecord record = recordMapper.selectById(recordId); if (record == null || "returned".equals(record.getStatus())) { return "记录不存在或已归还"; } record.setStatus("returned"); record.setReturnDate(new Date()); recordMapper.updateById(record); int affected = bookMapper.increaseAvailable(record.getBookId()); if (affected == 0) { throw new RuntimeException("库存恢复失败"); } return "归还成功"; }

逾期判断可以在查询借阅记录时计算:borrow_date加 30 天和当前时间比较,超过就是逾期。我一般不在数据库存逾期标记,因为时间在变,存了就要定时任务更新,不如查询时动态算。

5. 避坑与排查:那些让我熬夜的报错

5.1 现象:启动报Failed to configure a DataSource

原因:Spring Boot 自动配置检测到 classpath 下有 MySQL 驱动,但application.properties里没配数据库连接信息,或者配了但格式不对。

解决:检查application.properties是否有这四行:

spring.datasource.url=jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=你的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

注意 MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.DriverserverTimezone不配会报时区错误。

5.2 现象:借书接口返回成功但库存没变

原因:MyBatis 的UPDATE语句执行了但事务没提交,或者@Transactional注解没生效。

解决:先确认@Transactional加在 public 方法上,且这个方法是被 Spring 代理调用的。如果同一个类里 A 方法调 B 方法,B 方法上的@Transactional不会生效,因为没走代理。另外检查decreaseAvailable的返回值,如果返回 0 说明WHERE条件没匹配到,可能是bookId传错或库存已经是 0。

5.3 现象:中文书名插入数据库变成问号

原因:数据库连接 URL 没指定字符集,或者表字符集是latin1

解决:URL 加characterEncoding=utf8,建表用utf8mb4。如果已经建了表,执行ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4;转换。

5.4 现象:VS Code 里 Maven 依赖下载不下来

原因:默认仓库在国外,网络不稳定。

解决:在settings.xml里配国内镜像。找到 Maven 安装目录下的conf/settings.xml,在<mirrors>标签里加:

<mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

然后在 VS Code 的settings.json里指定"java.configuration.maven.userSettings": "路径/settings.xml"

5.5 现象:拦截器放行了登录接口但 Session 还是空

原因:前后端分离时,前端用 fetch 或 axios 默认不带 cookie,Session 依赖 JSESSIONID 这个 cookie。

解决:前端请求加credentials: 'include',后端 CORS 配置里加allowCredentials(true),且allowedOrigins不能是*,要写具体域名。如果同源部署就没这个问题。

6. 进阶技巧:用乐观锁替代行锁,以及一个验证并发的小工具

行锁在并发量不大时够用,但如果你想把系统做得更「像生产环境」,可以试试乐观锁。思路是在book表加一个version字段,每次更新带上版本号:

ALTER TABLE book ADD COLUMN version INT DEFAULT 0; UPDATE book SET available_copies = available_copies - 1, version = version + 1 WHERE id = #{bookId} AND available_copies > 0 AND version = #{version}

Service 层先查出版本号,更新时传入,如果affected为 0 说明版本被改过,重试或返回失败。乐观锁适合读多写少的场景,图书管理系统里借书频率远低于查书,所以乐观锁其实更合适。缺点是重试逻辑要自己写,代码比行锁方案多几行。

验证并发是否真的防住了,不用写复杂的 JMeter 脚本,用curl配合xargs就能模拟:

seq 1 10 | xargs -P 10 -I {} curl -s -X POST \ "http://localhost:8080/api/borrow/do?userId=1&bookId=1"

seq 1 10生成 1 到 10 十个数字,xargs -P 10表示并行 10 个进程,每个进程发一个借书请求。如果库存只有 5 本,跑完后查数据库,available_copies应该是 0,borrow_record里最多 5 条borrowed记录。如果出现负数或记录超过 5 条,说明并发控制有漏洞。

我自己的习惯是每次改完库存相关代码,都跑一遍这个命令,看数据库结果对不对。这个习惯帮我省了很多答辩时的尴尬。另外,如果你想让系统看起来更完整,可以加一个定时任务每天凌晨检查逾期记录,用@Scheduled(cron = "0 0 1 * * ?")注解,但记得在启动类加@EnableScheduling

做课设最怕的不是功能少,而是核心逻辑有硬伤。把借还书的并发和事务搞扎实,比堆十个花哨页面都管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:16:54

2026年头戴式耳机怎么选?10款热门机型横评推荐

又是一年盘点时间。前两天在后台看到一条留言&#xff0c;问我“2026年了&#xff0c;头戴式耳机到底还有没有买的必要”&#xff0c;说实话这个问题本身就很能代表一部分人的心态&#xff1a;手机厂商都在卷TWS&#xff0c;头戴式耳机这个品类看着好像没那么“便携”&#xff…

作者头像 李华
网站建设 2026/9/24 18:16:25

基于YOLOv8的智慧工厂危险区域闯入识别系统:从训练到部署

简介&#xff1a;这份资源面向计算机、人工智能、自动化等专业的在校学生与教师&#xff0c;提供一套可直接运行的智慧工厂危险区域闯入识别方案&#xff0c;适合作为毕业设计、课程设计或大作业的完整参考。项目以YOLOv8目标检测为核心&#xff0c;配套可视化界面&#xff0c;…

作者头像 李华
网站建设 2026/9/24 18:16:20

远程3D渲染全攻略:GPU算力云、远程桌面与硬件选型实战指南

远程3D渲染这个词&#xff0c;这几年被提得越来越频繁。2026年再看这件事&#xff0c;我觉得核心就一句话&#xff1a;把算力留在机房&#xff0c;人回家。过去做三维设计的人&#xff0c;基本被一台高性能工作站绑在工位上&#xff0c;机器在哪&#xff0c;人就得在哪。现在不…

作者头像 李华
网站建设 2026/9/24 18:16:00

Windows 开机自动录屏 8 种方案详解:从任务计划程序到 FFmpeg 实战

Windows 上实现开机自动录屏&#xff0c;听上去是个很小的需求&#xff0c;真做起来却有一堆讲究。我最近因为要给一台无人值守的机器做操作记录&#xff0c;把 Win10 22H2 和 Win11 23H2 上能用的开机自动录屏方案挨个测了一遍&#xff0c;发现最大的坑不是"找不到方法&q…

作者头像 李华
网站建设 2026/9/24 18:15:58

AI提效实战:DeepSeek、Kimi、Cursor三款工具组合使用指南

每天被各类消息、文档、报表轮番轰炸&#xff0c;加班到八九点成了不少职场人的日常。我大概花了三周时间&#xff0c;把市面上能叫得出名字的AI工具挨个用了一遍&#xff0c;最后真正留在工作流里、每天都离不开的其实只有三款。它们不是那种“看起来很酷但用不上”的玩具&…

作者头像 李华