简介:这是一套面向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-8和server.servlet.encoding.force=true。
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 ) 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_record用status标记借阅状态,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 < total_copies </update>decreaseAvailable的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 READ,UPDATE语句会对匹配的行加排他锁。当第一个事务执行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.Driver。serverTimezone不配会报时区错误。
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。
做课设最怕的不是功能少,而是核心逻辑有硬伤。把借还书的并发和事务搞扎实,比堆十个花哨页面都管用。希望帮到你。
本文还有配套的精品资源,点击获取