☰
JavaEE图书管理系统实战:从Servlet+JSP到并发避坑
2026/10/7 8:51:24 网站建设 项目流程

简介:这是一套面向JavaEE初学者与进阶开发者的图书管理系统完整源码,适合用于课程设计、毕业设计参考或企业级开发入门练手。项目采用MVC与三层架构,整合Servlet、JSP、JDBC、Spring、MyBatis、Struts等技术,覆盖登录认证、图书管理、用户权限、借阅记录等核心业务模块,并配有前端页面与数据库脚本,便于理解企业级Web应用的完整实现链路。压缩包共93个文件,约6.62MB,包含21个java源文件、11个jsp页面、10个xml配置、21个jar依赖及class、properties、css等资源,结构清晰,导入Eclipse或IntelliJ IDEA即可运行调试。目前已有146人学习下载,读者可借此掌握SSM/SSH框架集成、数据库交互与前后端协作思路,快速积累项目经验。

1. 从一份"图书管理系统.zip"说起:JavaEE 课设和真实项目之间差了什么

你拿到一个叫"基于javaee的图书管理系统.zip"的压缩包,解压后大概率看到的是src、WebContent、WEB-INF、web.xml这套结构,或者是一个 Maven 工程加几个 Servlet。这东西能跑,但很多人跑起来之后就卡住了——不知道下一步该改哪里,也不知道面试时怎么讲。图书管理系统几乎是每个 Java 学习者绕不开的练手项目,热搜里"javaee""图书管理系统"常年挂着,但真正把它做成能写进简历、能撑住追问的版本,比例并不高。这篇笔记不讲空泛的架构,而是顺着这个标题,把 JavaEE 环境下图书管理系统从环境搭建、分层设计、核心功能实现到部署排错整条链路拆开,告诉你哪些地方是课设和真实项目的分水岭。适合正在做课程设计的学生、想补一个完整 Web 项目的转行者,以及需要快速交付一个可演示系统的开发者。

2. JavaEE 图书管理系统的技术选型:为什么是 Servlet + JSP 而不是一上来就 Spring Boot

2.1 先搞清楚 JavaEE 到底指什么

很多人把 JavaEE 和 Spring 混为一谈,其实 JavaEE(现在叫 Jakarta EE)是一套规范集合,核心是 Servlet、JSP、JPA、EJB、JMS 这些标准 API。一个"基于 JavaEE 的图书管理系统",最朴素也最标准的形态就是:Servlet 处理请求,JSP 渲染页面,JDBC 或 JPA 访问数据库,部署在 Tomcat 这类 Servlet 容器里。这套东西看起来老,但它是理解 Web 请求生命周期的地基。你在 Spring Boot 里写@RestController觉得理所当然,可如果没亲手配过web.xml里的<servlet-mapping>,就很难理解 DispatcherServlet 到底替你做了什么。

选型上,我一般建议分两种情况。如果是课程设计,要求"基于 JavaEE",那就老老实实用 Servlet + JSP + JDBC,因为老师要看的就是你对这套规范的理解,用 Spring Boot 反而可能被判偏题。如果是自己想做一个能拿得出手的项目,那 Servlet + JSP 可以作为第一阶段,跑通之后再用 Spring Boot 重构一遍,这个对比过程本身就是很好的学习素材。热搜里"vscode配置javaee语言环境"说明不少人想在 VS Code 里搞,我的建议是:JavaEE 项目用 IntelliJ IDEA 社区版加 Tomcat 插件,或者 Eclipse IDE for Enterprise Java,VS Code 虽然能配,但 Servlet 容器的调试体验差一截,容易在环境上耗掉太多时间。

2.2 分层结构怎么切才不返工

一个能维护的图书管理系统,至少分四层:表现层(JSP + Servlet)、业务层(Service)、数据访问层(DAO)、实体层(Entity)。很多人图省事,把 SQL 直接写在 Servlet 里,结果加一个"按分类查询"就要改三处。正确的做法是每层只干一件事。

下面是一个典型的 DAO 层接口和实现,用 JDBC 手写,不依赖任何框架:

// BookDao.java —— 数据访问层接口 public interface BookDao { List<Book> findAll() throws SQLException; List<Book> findByCategory(String category) throws SQLException; Book findById(int id) throws SQLException; int insert(Book book) throws SQLException; int update(Book book) throws SQLException; int deleteById(int id) throws SQLException; }
// BookDaoImpl.java —— JDBC 实现,注意连接从工具类拿 public class BookDaoImpl implements BookDao { @Override public List<Book> findAll() throws SQLException { String sql = "SELECT id, title, author, category, stock FROM book ORDER BY id DESC"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setId(rs.getInt("id")); b.setTitle(rs.getString("title")); b.setAuthor(rs.getString("author")); b.setCategory(rs.getString("category")); b.setStock(rs.getInt("stock")); list.add(b); } } return list; } // 其余方法省略,逻辑相同 }

这里的关键点是:用PreparedStatement而不是Statement,防止 SQL 注入;用 try-with-resources 自动关闭连接,避免连接泄漏;DBUtil负责从连接池拿连接,不要每次DriverManager.getConnection。参数说明上,stock字段用 int 存库存,category用 varchar 存分类名,如果分类要支持多级,就得单独建一张category表用外键关联,这是新手最容易偷懒的地方。

2.3 数据库表设计的最小可用集合

图书管理系统的表不用多,但字段要想清楚。最少三张表:book(图书)、user(用户)、borrow_record(借阅记录)。book表里stock表示可借数量,borrow_record里要有borrow_date、return_date、status(借出/已还/逾期)。下面这张表对比了课设常见做法和更稳妥的做法:

字段课设常见做法更稳妥的做法原因
图书分类直接存字符串单独 category 表 + 外键分类改名时不用全表更新
库存只存总数总数 + 已借出数,可借数计算得出避免并发借阅时库存对不上
借阅状态只存是否归还状态枚举 + 时间戳能算逾期、能查历史
用户密码明文BCrypt 哈希安全底线

3. 用 Servlet + JSP 跑通图书管理的最小闭环:从登录到借书

3.1 环境搭建与项目骨架

先确认 JDK 版本。JavaEE 项目用 JDK 8 或 11 最稳,JDK 17 以上有些老容器会报模块化相关的错。Tomcat 用 9.x,因为 Tomcat 10 之后包名从javax.servlet变成了jakarta.servlet,很多老代码直接编译不过。这是热搜里"vscode配置javaee语言环境"最容易翻车的地方——装完插件发现 import 报红,八成是 Tomcat 版本和 Servlet 包名对不上。

项目骨架用 Maven 管理依赖,pom.xml里至少要有:

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> </dependencies>

servlet-api的 scope 必须是provided,因为 Tomcat 自己带了这套 API,打包进 WAR 会冲突。Druid 做连接池,比手写连接管理省心得多。数据库连接配置放在druid.properties里,不要硬编码在 Java 代码中。

3.2 登录与权限过滤器的实现

登录流程是:JSP 表单提交到LoginServlet,Servlet 调 Service 查用户,验证通过后把用户信息放进HttpSession,然后重定向到图书列表页。这里必须配一个LoginFilter,否则任何人直接访问bookList.jsp都能看到数据。

// LoginFilter.java —— 拦截未登录请求 @WebFilter("/*") public class LoginFilter implements Filter { private static final List<String> WHITE_LIST = Arrays.asList("/login.jsp", "/login", "/css/", "/js/"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI().substring(request.getContextPath().length()); // 白名单直接放行 for (String white : WHITE_LIST) { if (uri.startsWith(white)) { chain.doFilter(req, resp); return; } } // 检查 session Object user = request.getSession().getAttribute("currentUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

逻辑说明:白名单里的路径不拦截,其余路径检查 session 里有没有currentUser。参数上,@WebFilter("/*")表示拦截所有请求,WHITE_LIST里要包含登录页、登录接口和静态资源目录,否则 CSS 和 JS 也会被拦,页面样式全丢。注意uri.startsWith对静态资源目录的判断,如果写成equals就会漏掉/css/style.css这种具体文件。

3.3 借书与还书的业务逻辑

借书不是简单地把stock减一。正确流程是:先查这本书可借数是否大于零,再查这个用户有没有超期未还的书,两个条件都满足才写借阅记录并扣库存。这三步必须在一个事务里,否则并发时会出问题。

// BorrowService.java —— 借书业务,手动控制事务 public boolean borrowBook(int userId, int bookId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查库存 BookDao bookDao = new BookDaoImpl(conn); Book book = bookDao.findById(bookId); if (book == null || book.getStock() <= 0) { conn.rollback(); return false; } // 2. 检查用户是否有超期未还 BorrowDao borrowDao = new BorrowDaoImpl(conn); if (borrowDao.hasOverdue(userId)) { conn.rollback(); return false; } // 3. 扣库存 + 写记录 bookDao.decreaseStock(bookId); borrowDao.insertRecord(userId, bookId, new Date()); conn.commit(); return true; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

参数说明:userId和bookId从 session 和请求参数拿,new Date()是借出时间。关键点是 DAO 实现类要支持传入外部 Connection,这样多个 DAO 操作才能共享同一个事务。如果每个 DAO 方法自己开连接,事务就失效了。还书逻辑类似,把stock加回去,更新借阅记录的状态和归还时间。

4. 图书管理系统部署与排错的避坑清单

4.1 中文乱码:现象是页面显示问号,原因是编码链没统一

现象:JSP 页面提交的中文书名存进数据库变成???,或者从数据库读出来显示乱码。原因通常有三处:JSP 页面没声明pageEncoding、请求体没设setCharacterEncoding、数据库连接 URL 没加useUnicode。解决方法是三处都统一成 UTF-8:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里在取参数前调request.setCharacterEncoding("UTF-8"),JDBC URL 写成jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8。注意setCharacterEncoding必须在getParameter之前调用,顺序反了不生效。

4.2 404 和 500:现象是访问路径报错,原因是映射和依赖没对上

现象:访问/bookList报 404,或者页面报 500 但控制台看不到具体异常。404 多半是@WebServlet("/bookList")的路径和表单 action 不一致,或者web.xml里url-pattern写错。500 则要看 Tomcat 日志,常见的是ClassNotFoundException(驱动没放进WEB-INF/lib)或NullPointerException(连接池没初始化)。解决方法是先看 Tomcat 的logs/catalina.out,再检查pom.xml依赖的 scope 是不是写成了provided导致运行时缺失。

4.3 连接池耗尽:现象是系统跑一会儿就卡死,原因是连接没关

现象:系统刚启动正常,点几次之后所有请求都超时,日志里出现wait millis 60000。原因是 DAO 里拿了连接没关,或者事务异常时没在 finally 里释放。解决方法是所有连接操作都用 try-with-resources,或者在 finally 块里显式 close。Druid 可以在配置里加removeAbandoned=true和removeAbandonedTimeout=180做兜底,但这只是后悔药,根本还是要保证代码里不泄漏。

4.4 并发借书超卖:现象是库存变成负数,原因是检查与扣减分离

现象:两个人同时借同一本书,库存显示 1,结果两条借阅记录都写成功了,库存变成 -1。原因是"查库存"和"扣库存"是两条独立 SQL,中间有时间窗口。解决方法有两种:一是用UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0,根据返回的影响行数判断是否成功;二是在事务里用SELECT ... FOR UPDATE锁行。前者更轻量,推荐优先用。

4.5 JSP 里写 Java 代码:现象是页面越来越难改,原因是没做前后分离

现象:bookList.jsp里既有 HTML 又有while(rs.next())还有数据库连接代码,改一个字段要动三个地方。原因是把业务逻辑写进了视图层。解决方法是 JSP 只负责渲染,数据由 Servlet 通过request.setAttribute传过来,用 JSTL 的<c:forEach>遍历。这一步做完,后面想换成 Thymeleaf 或者前后端分离都会轻松很多。

5. 从能跑到能讲:图书管理系统的进阶改造与验证方法

把系统跑起来只是起点,真正拉开差距的是你能不能把它讲清楚、改得动。我一般会做三件事来验证这个项目到底扎不扎实。

第一件是加一个"借阅排行榜"功能,按借阅次数倒序查前 10 本书。这个功能会逼你把borrow_record和book做 join,还要处理没有借阅记录的书不显示的问题。SQL 大概是SELECT b.title, COUNT(*) AS cnt FROM borrow_record r JOIN book b ON r.book_id = b.id GROUP BY b.id ORDER BY cnt DESC LIMIT 10。写完之后你会自然理解为什么索引要加在borrow_record.book_id上。

第二件是把 DAO 层从 JDBC 换成 JPA 或者 MyBatis,对比一下代码量的变化。你会发现 MyBatis 的 Mapper 接口加 XML 能把 JDBC 里那些rs.getInt全干掉,但事务控制反而更依赖框架配置。这个对比能让你在面试时说出"为什么选 MyBatis 而不是 JDBC"这种有分量的话。

第三件是写一个简单的压力验证。不用 JMeter,用 Java 自带的ExecutorService起 50 个线程同时借同一本书,看库存会不会出问题:

// ConcurrencyTest.java —— 验证并发借书是否超卖 public class ConcurrencyTest { public static void main(String[] args) throws Exception { int threads = 50; ExecutorService pool = Executors.newFixedThreadPool(threads); CountDownLatch latch = new CountDownLatch(threads); BorrowService service = new BorrowService(); AtomicInteger success = new AtomicInteger(); for (int i = 0; i < threads; i++) { final int userId = i % 10 + 1; // 模拟 10 个用户 pool.submit(() -> { try { if (service.borrowBook(userId, 1)) { // 都借 id=1 的书 success.incrementAndGet(); } } catch (Exception e) { e.printStackTrace(); } finally { latch.countDown(); } }); } latch.await(); pool.shutdown(); System.out.println("借阅成功次数: " + success.get()); // 如果库存初始为 5,成功次数应该 <= 5,否则就是超卖 } }

这段代码的关键是CountDownLatch让所有线程同时开始,模拟真实并发。跑完看成功次数和数据库里的库存对不对得上。如果成功次数大于初始库存,说明你的扣减逻辑有并发漏洞,回去用UPDATE ... WHERE stock > 0改。

最后说个我自己的习惯:每做完一个模块,我都会把web.xml或者注解里的 URL 映射、数据库表结构、关键 SQL 单独抄到一个notes.md里。不是为了交作业,而是过两周回头看时,不用重新读一遍代码就知道当时怎么设计的。图书管理系统这个题目被做烂了,但烂题目里能做出自己的东西,靠的就是这些别人懒得记的细节。希望帮到你。

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

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

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

立即咨询