ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 配 MyBatisJDK 用 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」这类报错。第四步用CtrlShiftP打开命令面板输入Spring Initializr创建项目勾选 Spring Web、MyBatis Framework、MySQL Driver、Thymeleaf。生成后 VS Code 会自动识别为 Maven 项目。提示VS Code 跑 Spring Boot 项目时如果控制台中文乱码在application.properties里加server.servlet.encoding.charsetUTF-8和server.servlet.encoding.forcetrue。2.3 数据库建表图书、用户、借阅记录三张核心表图书管理系统的数据模型不复杂但字段设计直接影响后面业务能不能写顺。我一般建三张表book、user、borrow_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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;book表里total_copies是馆藏总数available_copies是可借数量这两个字段分开是为了处理「借出后库存减少但总数不变」的逻辑。borrow_record用status标记借阅状态borrowed表示未还returned表示已还。外键约束保证不会出现借阅记录指向不存在的书。字符集用utf8mb4而不是utf8否则书名里的生僻字或 emoji 会插入失败。3. 借阅业务的核心代码从 DAO 到 Service 再到 Controller3.1 MyBatis 映射文件与库存扣减 SQL持久层我用 MyBatis 的 XML 映射因为 SQL 可控调优方便。先看BookMapper.xml里最关键的库存扣减语句update iddecreaseAvailable UPDATE book SET available_copies available_copies - 1 WHERE id #{bookId} AND available_copies 0 /update update idincreaseAvailable UPDATE book SET available_copies available_copies 1 WHERE id #{bookId} AND available_copies lt; total_copies /updatedecreaseAvailable的WHERE条件里带了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时id1 这行被锁住第二个事务的相同 UPDATE 会阻塞直到第一个事务提交。提交后第二个事务重新读取最新库存如果库存为 0affected返回 0借阅失败。整个过程不需要应用层加锁。如果你用的是 MyBatis 二级缓存注意UPDATE会清空缓存但查询缓存可能返回旧数据。我一般直接关掉二级缓存在application.properties里配mybatis.configuration.cache-enabledfalse避免缓存和数据库不一致。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.urljdbc:mysql://localhost:3306/library?useSSLfalseserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password你的密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver注意 MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver。serverTimezone不配会报时区错误。5.2 现象借书接口返回成功但库存没变原因MyBatis 的UPDATE语句执行了但事务没提交或者Transactional注解没生效。解决先确认Transactional加在 public 方法上且这个方法是被 Spring 代理调用的。如果同一个类里 A 方法调 B 方法B 方法上的Transactional不会生效因为没走代理。另外检查decreaseAvailable的返回值如果返回 0 说明WHERE条件没匹配到可能是bookId传错或库存已经是 0。5.3 现象中文书名插入数据库变成问号原因数据库连接 URL 没指定字符集或者表字符集是latin1。解决URL 加characterEncodingutf8建表用utf8mb4。如果已经建了表执行ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4;转换。5.4 现象VS Code 里 Maven 依赖下载不下来原因默认仓库在国外网络不稳定。解决在settings.xml里配国内镜像。找到 Maven 安装目录下的conf/settings.xml在mirrors标签里加mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror然后在 VS Code 的settings.json里指定java.configuration.maven.userSettings: 路径/settings.xml。5.5 现象拦截器放行了登录接口但 Session 还是空原因前后端分离时前端用 fetch 或 axios 默认不带 cookieSession 依赖 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?userId1bookId1seq 1 10生成 1 到 10 十个数字xargs -P 10表示并行 10 个进程每个进程发一个借书请求。如果库存只有 5 本跑完后查数据库available_copies应该是 0borrow_record里最多 5 条borrowed记录。如果出现负数或记录超过 5 条说明并发控制有漏洞。我自己的习惯是每次改完库存相关代码都跑一遍这个命令看数据库结果对不对。这个习惯帮我省了很多答辩时的尴尬。另外如果你想让系统看起来更完整可以加一个定时任务每天凌晨检查逾期记录用Scheduled(cron 0 0 1 * * ?)注解但记得在启动类加EnableScheduling。做课设最怕的不是功能少而是核心逻辑有硬伤。把借还书的并发和事务搞扎实比堆十个花哨页面都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表