JavaEE图书管理系统实战:Servlet+JSP+JDBC全流程设计解析
2026/8/31 3:09:10 网站建设 项目流程

简介:这是一套面向计算机专业本科生的JavaEE实战项目资源,专为课程设计、期末大作业及毕业设计准备,覆盖图书管理核心业务全流程,帮助学习者快速掌握SSM(Spring+SpringMVC+MyBatis)框架整合开发与Web系统部署能力。压缩包共248个文件,含35个Java源码、18个JSP页面、14个HTML静态页、13个XML配置文件、24个Jar依赖库及1个SQL数据库脚本,另有演示视频MP4、操作截图GIF和样式资源CSS/JS等,整体79.35MB,结构清晰、模块分明,便于理解MVC分层逻辑与前后端交互机制。已有193人学习下载,项目经导师指导并获99分高分评价,代码完整可直接运行,配套数据库与视频演示大幅降低上手门槛,特别适合零基础实践者完成从环境搭建、功能调试到系统演示的全流程闭环训练。 做JavaEE课程设计的时候,十个项目里九个人选图书管理系统。这个选题确实被用得太多了,但它始终是经典——麻雀虽小五脏俱全,用户管理、图书档案、借书还书、超期统计,一整套CRUD闭环把Servlet、JSP、JDBC、数据库设计这些JavaEE核心知识点全串起来了。对一个刚学完JavaWeb的人来说,这是能最快建立全局认知的项目,没有之一。

我手上这套"JavaEE-图书管理系统源码+数据库+演示视频.zip",就是典型的课程设计交付包:源码目录结构清晰,MySQL脚本一键导入,附带录制好的演示流程。它能解决的痛点很明确——课程设计不知道怎么做、做完了不知道怎么讲、讲的时候没有演示素材。这篇文章我打算把整个项目从技术选型到数据库设计、从分层架构到部署演示全部拆开来讲,重点放在那些文档里不会写的取舍逻辑和踩坑经验上,希望能帮到正在做这个题目的同学。

1. 项目定位与核心需求拆解

1.1 图书管理系统到底在练什么

先聊清楚一个问题:为什么几乎所有JavaEE课程都拿图书管理系统当作业题?因为它是典型的"信息管理类"应用,这类系统在企业里占了很大比重——库存管理、客户管理、订单管理,骨子里都是同一套逻辑:数据从界面进,经过业务层处理,落到数据库里,再按条件查出来展示。

图书管理系统把这条链路压缩到了最小可演示的规模,刚好覆盖JavaEE阶段必须掌握的技术点:

  • JSP负责页面渲染,Servlet负责接收请求和跳转
  • JDBC负责数据库访问,PrepareStatement防注入、ResultSet遍历结果集
  • 事务管理体现在借书还书这种需要同时更新多张表的操作上
  • 过滤器实现编码处理和登录状态校验
  • EL表达式和JSTL标签库解决视图层逻辑代码过多的问题

做完这个项目,你基本就摸清了JavaEE工程的完整生命周期:需求分析、表结构设计、分层编码、部署调试。这也是为什么毕业设计、实训结课都喜欢选它——工作量可控,知识点覆盖全面,答辩的时候也好讲。

1.2 功能清单与角色权限设计

这套系统我按常见的双角色模式来设计,分为管理员和普通读者,对应到数据库里就是user表的一个role字段,而不是拆两张表。

管理员端的功能:

  • 登录和退出登录
  • 图书信息的录入、修改、删除、按条件查询
  • 图书分类管理(增删改)
  • 查看全部借阅记录、处理归还操作
  • 读者列表查询、禁用/启用读者账号

读者端的功能:

  • 注册、登录、修改个人密码
  • 按书名、作者、分类检索图书
  • 借书操作(可借数量校验)
  • 我的借阅记录、当前借阅中的图书列表
  • 在线续借、归还申请

这里有一个常见的设计取舍:借书操作到底谁来做?有的系统设计成读者在前台自助借书,有的设计成必须由管理员在后台操作。我建议课程设计场景下采用读者在前台申请、管理员在后台确认的模式,这样一方面能演示更多的页面交互,另一方面也符合真实图书馆的业务逻辑——图书是物理资产,借出必须有人工审核环节。

1.3 非功能需求同样影响设计

很多同学做课程设计只盯着功能,忽略非功能需求,结果答辩时被老师一问就卡住。图书管理系统有几个非功能指标直接影响了代码怎么写:

  • 并发性:真实场景下多个读者同时借书,库存字段的更新必须用事务保证原子性。虽然课程设计不会真的压测,但代码里要体现这个意识。
  • 安全性:密码不能明文存库,至少用MD5加盐。SQL注入要通过PreparedStatement避免。登录状态要存在Session而不是Cookie里。
  • 可维护性:分层设计,控制层、业务层、数据访问层分开,这不仅是给老师看的,也是为你后续扩展功能省事。

2. 技术栈选型与环境搭建

2.1 为什么用传统JavaEE而不是SpringBoot

2024年了,企业里新项目基本都是SpringBoot,学校教JavaEE还在用Servlet+JSP,于是很多同学纠结:课程设计到底用哪套?

我的建议是:如果课程要求明确写了JavaEE/JSP/Servlet,就老老实实用传统方式。原因有三点:

第一,课程设计考察的是你对课堂知识的掌握程度,你用SpringBoot框架把底层细节全封住了,答辩时老师追问个Servlet生命周期、过滤器怎么配置,你答不上来反而扣分。

第二,Servlet+JSP这套路虽然老,但它是理解Web应用本质的最佳路径。SpringBoot内嵌Tomcat、自动配置DispatcherServlet,把这些全屏蔽掉了。做过原生的,你才知道那些框架到底帮你做了什么。

第三,实际上手难度并没有想象中高。这个项目涉及的核心依赖就三个:Servlet API、JDBC驱动、JSTL标签库。不涉及Maven的话,直接把jar包丢到WEB-INF/lib下就行,比SpringBoot那套依赖管理还省心。

2.2 开发工具与版本搭配

我这次用的环境组合,也是目前主流的课程设计搭配:

组件版本说明
JDK1.8稳定,兼容性好,Tomcat 9和8.5都支持
IDEIntelliJ IDEA Community / Eclipse EE版社区版免费够用
Tomcat9.0.x对应Servlet 4.0规范
MySQL5.7 / 8.05.7更稳,8.0注意驱动类名变化
MySQL驱动mysql-connector-java 8.0.x用8.x版本,jar包约2MB

这里重点说一下IDE的选择。IDEA社区版其实不支持JavaEE的Web项目模板,很多同学卡在第一步不知道怎么建项目。实际上有两种方案:要么用IDEA旗舰版(学生可以免费申请教育授权),要么用Eclipse IDE for Enterprise Java and Web Developers——装好以后自带Web项目模板和Tomcat集成,省事很多。如果坚持用IDEA社区版,可以手动创建普通Java项目再加Web目录结构,但配置过程对新手不友好,不建议折腾。

2.3 手动搭建项目骨架

我以Eclipse为例说一下最标准、最不容易出错的建项目流程:

  1. 新建Dynamic Web Project,项目名比如叫LibrarySystem,Target Runtime选Tomcat 9。
  2. 在src下建包结构:com.library.bean、com.library.dao、com.library.service、com.library.servlet、com.library.filter、com.library.util。
  3. 把mysql驱动jar包和jstl的jar包复制到src/main/webapp/WEB-INF/lib(Eclipse里路径是WebContent/WEB-INF/lib)。
  4. 在WebContent下建jsp目录,把页面文件放这里,css和js放assets目录。
  5. 修改web.xml文件。(在Servlet 3.0+版本里也可以用注解@WebServlet替代XML配置,但我建议课程设计用XML或注解明确标明,让代码更可读)

这里有个细节:Servlet 3.0之后支持注解配置,就不用再在web.xml里写一大串<servlet><servlet-mapping>了。但如果你用的是Eclipse的Web项目模板,默认的web.xml版本可能是2.5,注解不生效。要检查一下文件头里的web-app标签,把版本改成3.1或者4.0,让注解配置生效,省掉很多模板代码。

2.4 关于vscode配置JavaEE环境的补充

最近有同学问我,能不能用VSCode写JavaEE项目?说实话,VSCode装Extension Pack for Java插件以后,写普通Java代码、跑Maven项目是没问题的,但跑JavaEE动态Web项目(需要Servlet容器支持)体验很差——一是没有像Eclipse里Tomcat集成那样一键部署,二是调试JSP页面比较麻烦。个人建议,课程设计项目别在VSCode上折腾了,老老实实用Eclipse或IDEA,效率高一倍。这个坑我试过,最后放弃了。

3. 数据库设计与核心建模

3.1 数据模型总览与建表思路

图书管理系统数据库设计,核心就是三张业务表加一个分类表。建表之前先想清楚实体关系,我用最简单的文字描述一下:

  • 用户(读者/管理员)与借阅记录:一对多
  • 图书与借阅记录:一对多
  • 图书与分类:多对一

围绕这四张表,再加一个管理员日志表(可选),就够用了。数据库命名上,我统一用英文小写加下划线,字段名避开关键词比如descorderuser(user虽然不是MySQL的保留词,但有些版本里容易出问题,建议加反引号或改名叫sys_user)。

3.2 四张核心表的DDL脚本

-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), role CHAR(1) DEFAULT '1', -- '0'管理员 '1'普通读者 status INT DEFAULT 1, -- 1正常 0禁用 phone VARCHAR(20), email VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_code VARCHAR(30) NOT NULL UNIQUE, -- 图书编号 book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), price DECIMAL(10,2), category_id INT, stock INT DEFAULT 1, -- 总库存 remaining INT DEFAULT 1, -- 可借库存 location VARCHAR(50), -- 馆藏位置 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录表 CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, -- 应还时间 return_time DATETIME, -- 实际归还时间,NULL表示未还 status TINYINT DEFAULT 0, -- 0借阅中 1已归还 2已续借 3超期未还 renew_count INT DEFAULT 0, -- 续借次数 CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES t_book(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.3 设计决策:为什么把库存拆成stock和remaining

图书表里我设计了stock(总库存)和remaining(可借库存)两个字段。很多人会问,一个字段不就行了吗?借出时减一,还回时加一。但真实业务里,总库存是书的馆藏数量,可借库存是实际在架可借的数量,两者有各自的含义。比如一本书有5本,其中2本被借出,3本还在架上,stock=5、remaining=3,如果要展示"馆藏5本,可借3本"就用这两个字段,不用每次COUNT一下借阅表。这是用冗余字段换查询效率的典型设计,做课程设计时这么写,老师一定会觉得你考虑得周到。

3.4 借书还书的事务逻辑

借书操作是典型的跨表事务,涉及两步更新:插入一条借阅记录 + 图书表的remaining字段减1。这两个操作是原子的,必须同时成功或同时失败。如果先插入借阅记录但remaining没减,下次借书就会超借;如果先减了remaining但插入失败,书会被锁住借不出去。

用JDBC的写法就是在Service层开启手动事务,获取连接后设置setAutoCommit(false),执行完两个操作后commit,任何一个异常就rollback,在finally里恢复自动提交并关闭资源。这个点我在后面的Spring中会再具体展开。

4. 后端分层架构与核心功能实现

4.1 三层架构的职责边界

JavaEE的传统三层架构,建议严格按这个分层来组织代码,不越级调用:

  • Servlet层(Web/控制层):接收请求参数,调Service处理,把结果放request/session,转发或重定向到JSP
  • Service层(业务层):处理业务规则,比如借书前校验库存和读者身份、密码加密、借阅天数计算;负责事务边界
  • DAO层(数据访问层):封装JDBC操作,每一个方法对应一条SQL或一个原子操作,不包含业务逻辑
  • Bean层(实体层):就是JavaBean,属性对应表字段,加上getter/setter

这里最容易犯的错误,是把业务逻辑写在Servlet里,或者让Service层直接暴露SQL细节。举个例子,检查"这本书还能不能借",正确的做法是Service层调DAO层的queryById(bookId)拿到图书信息,然后在Service里判断remaining > 0,而不是在Servlet里写判断。原因很简单:Servlet是Web层的入口,将来如果换一个调用场景(比如做REST API、写单元测试),业务规则必须保证一致,而这一致性只有Service层能保证。

4.2 JDBC工具类与数据库连接管理

数据库连接不能每操作一次就创建一次,正确的做法是使用连接池。课程设计项目我建议引入Druid连接池,它足够简单,配置一个properties文件就能用。

public class DbUtils { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(DbUtils.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) { } } } } }

db.properties配置:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username=root password=你的密码 initialSize=5 maxActive=20

这里需要注意:MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,如果用的是MySQL 5.x,驱动类名是com.mysql.jdbc.Driver,版本不匹配会直接报ClassNotFoundException。还有url里的serverTimezone参数在高版本MySQL驱动下必填,否则就会报时区错误。

4.3 图书增删改查与分页查询实现

图书管理是后台核心模块,我先说分页。分页是老生常谈:用LIMIT子句加偏移量。但有一个细节容易被忽略——总页数的计算。总记录数应该用单独的COUNT查询,而不是一次性查询所有记录再计算,否则数据量大了内存就爆。

SELECT COUNT(*) FROM t_book WHERE book_name LIKE CONCAT('%', ?, '%') SELECT * FROM t_book WHERE book_name LIKE CONCAT('%', ?, '%') ORDER BY id DESC LIMIT ?, ?

在DAO层的写法,注意PreparedStatement传参是1开始的下标:

public List<Book> searchBooks(String keyword, int offset, int pageSize) { String sql = "SELECT * FROM t_book WHERE book_name LIKE ? OR author LIKE ? ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, offset); ps.setInt(4, pageSize); try (ResultSet rs = ps.executeQuery()) { List<Book> list = new ArrayList<>(); while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); book.setRemaining(rs.getInt("remaining")); list.add(book); } return list; } } catch (SQLException e) { e.printStackTrace(); } return Collections.emptyList(); }

增删改是基础操作,这里不展开每行代码,重点说三个细节:

第一,删除操作要谨慎。图书如果存在关联的借阅记录,删除图书会导致外键报错。我的方案是逻辑删除——在图书表加一个deleted字段,删除时执行UPDATE设置deleted=1,而不是物理DELETE。查询条件里统一加上AND deleted=0。这样做的好处是历史借阅记录永远可追溯,对课程设计而言,逻辑删除是加分项。

第二,修改操作用表单提交后,Servlet里要逐字段set属性再调用Service更新,注意空值处理。比如价格字段在JSP表单里如果不填,会提交空字符串,直接用request.getParameter("price")拿到后再转换成BigDecimal就可能报NumberFormatException。我习惯在Servlet里写一个参数转换的小工具方法,空串统一转成null。

4.4 用户认证与会话管理

登录是整个系统的守门员。我用Session保存登录用户,密码用MD5加盐存储,登录时先查库里存好的盐值,再拼接密码做MD5比较。

public User login(String username, String password) { User user = userDao.findByUsername(username); if (user != null) { // 加盐加密比较 String encrypted = MD5Utils.md5(password + user.getSalt()); if (encrypted.equals(user.getPassword())) { return user; } } return null; }

登录成功后,在LoginServlet里执行:

HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(60 * 30); // 三十分钟无操作自动失效

然后通过一个Filter统一拦截未登录的访问。这里有个拦截设计的关键点:登录接口本身、登录页面、注册页面、css/js资源必须放行,其他所有请求都检查Session里有没有loginUser。我在web.xml或者注解里配置Filter的urlPatterns为/*,在doFilter内部通过判断requestURI来做白名单逻辑:

String uri = request.getRequestURI(); if (uri.endsWith("/login.jsp") || uri.endsWith("/login") || uri.endsWith("/register.jsp") || uri.endsWith("/register")) { chain.doFilter(request, response); return; } if (uri.contains("/assets/")) { chain.doFilter(request, response); return; } User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; }

再配合角色判断:管理员后台的URL(比如manag前缀)除了登录态还要校验role是否为0,不通过就跳转无权限页面。这套登录拦截逻辑,不管什么JavaEE项目都是通用的。

4.5 借书还书的完整流程

借书流程,我完整走一遍:

  1. 读者在前台搜索图书,点击"借阅"按钮,URL形如/borrowBook?bookId=123
  2. BorrowServlet接收请求,从Session拿loginUser获取userId,从参数拿bookId
  3. 调Service层borrowBook方法,方法内部:
    • a. 检查用户status是否为1(未被禁用)
    • b. 查询图书,判断remaining是否大于0
    • c. 检查该用户是否已借了这本书且未还(防止重复借同一本)
    • d. 插入t_borrow记录,borrow_time=now(),due_time=now() + 30天
    • e. 更新t_book的remaining = remaining - 1
    • f. commit所有操作
  4. 跳转到"借阅成功"或"借阅失败"页面,带上错误信息

Service层事务代码:

public boolean borrowBook(int userId, int bookId) throws Exception { Connection conn = null; try { conn = DbUtils.getConnection(); conn.setAutoCommit(false); // 1. 查用户状态 // 2. 查图书库存 // 3. 查是否重复借阅 // 4. 插入借阅记录 // 5. 更新库存 conn.commit(); return true; } catch (Exception e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

这个事务逻辑是项目的精华部分,答辩的时候老师必问。你一定要能说清楚:为什么这两步必须放在同一个事务里?如果事务B提交成功、事务A失败回滚,数据库会变成什么样?把回滚机制讲明白了,整个系统就答到位了。

5. 前端页面与交互设计要点

5.1 JSP + EL + JSTL的渲染方式

传统JavaEE的前端,JSP里最忌讳的就是写一坨<% %>Java脚本代码,页面乱而且复用性差。我用EL表达式+JSTL标签库来渲染列表和条件判断,页面干净很多。

图书列表的JSP核心片段:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${page.list}" var="book"> <tr> <td>${book.bookCode}</td> <td>${book.bookName}</td> <td>${book.author}</td> <td>${book.publisher}</td> <td>${book.price}</td> <td>${book.remaining}/${book.stock}</td> <td> <a href="${pageContext.request.contextPath}/bookDetail?bookId=${book.id}">详情</a> <c:if test="${book.remaining > 0}"> <a href="${pageContext.request.contextPath}/borrowBook?bookId=${book.id}" onclick="return confirm('确认借阅这本书吗?')">借阅</a> </c:if> </td> </tr> </c:forEach>

注意EL表达式拿不到值最常见的原因:Servlet里setAttribute的对象没有对应的getter方法,或者变量名不一致。调试的时候,先看Servlet的setAttribute代码,再看JSP里的${对象.属性},属性名必须完全对应Bean里的getXxx去掉get后首字母小写。这个对应关系新手很容易搞混。

5.2 中文乱码的根治方案

中文乱码是JavaEE项目里出现频率最高的问题,没有之一。它可能出现在三处:请求参数、响应输出、数据库读写。我的习惯是一套组合拳预防:

请求端,在web.xml配置编码过滤器,拦截所有请求:

<filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.library.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

EncodingFilter的doFilter里面:

request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response);

数据库端,连接URL已经加了characterEncoding=utf8mb4,表结构统一用utf8mb4,JSP页面顶部加上<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>

这四层都覆盖到位,乱码基本可以杜绝。如果还乱码,优先检查一下Tomcat的server.xml里Connector有没有URIEncoding="UTF-8"配置。GET请求的参数编码处理方式跟POST不同,Tomcat 8+默认URIEncoding就是UTF-8,旧版本需要手动加。

5.3 页面布局复用与样式

课程设计的前端不要求多惊艳,但至少结构清晰、风格统一。我建议把导航栏和页脚抽成公共的JSP片段,用<%@ include file="header.jsp" %>引入。

布局上,后台页面用左侧菜单+右侧内容区的经典布局,前台用户页面用顶部导航+中部内容+底部页脚的布局。CSS直接手写基础样式,不用引入Bootstrap也行,但要控制好字体、间距、表格样式、按钮风格这几个维度的一致性,看着才会像整体。

如果你时间充裕,花半小时做一个简单的CSS变量(:root里定--primary-color),调整主题色只需改一处,页面视觉就能统一,这在演示的时候也很加分。

6. 部署发布与演示视频录制经验

6.1 从IDEA/Eclipse导出Web项目并部署

项目做完以后,要么打WAR包,要么直接把整个项目目录复制到Tomcat的webapps下。这里讲一下打包标准流程:

  1. 确认项目能正常构建:在IDE里右键项目选择Export -> WAR file(Eclipse)或者用Maven的package(如果用了Maven)。
  2. WAR包命名不能有中文和空格,最好是LibrarySystem.war
  3. 把WAR包复制到Tomcat的webapps目录下,启动Tomcat,它会自动解压部署。
  4. 访问http://localhost:8080/LibrarySystem/即可看到项目首页。

启动后有个常见的坑:如果端口被占用,Tomcat会提示Port 8080 required by Tomcat v9.0 Server is already in use。Windows下用命令行netstat -ano | findstr 8080找到PID,然后在任务管理器结束进程,或者改Tomcat的server.xml把端口改成8081。

6.2 数据库脚本导入与初始化数据

源码包里的database.sql文件,同时包含建表语句和初始化数据。这些初始化数据不是随手乱写的——我把演示用的账号和几本有代表性的书都准备好了,方便录制演示视频时直接演示而不用临时录入。

初始化数据包括:

  • 管理员账号:admin / admin123(角色0)
  • 读者账号:zhangsan / 123456(角色1)
  • 图书:覆盖计算机、文学、历史三个分类,共十几本,还安排了一本库存为0的书用来演示"不可借"的边界情况
  • 借阅记录:几条处于不同状态的记录,包括借阅中、已归还、超期未还

导入方式:

mysql -u root -p < database.sql

或者用Navicat/DBeaver直接运行SQL脚本。注意:如果MySQL是8.0版本,导入时如果sql文件里有DROP TABLE IF EXISTS就不会报错,没有的话需要先手动删掉旧表。

6.3 演示视频录制脚本设计

很多人忽略演示视频的重要性,其实课程设计答辩时,老师不可能完全跟着你的节奏看完整项目。一个三到五分钟的演示视频,如果能按业务逻辑清晰展示,通过率会大大提升。

我录视频的习惯是分四段:

  • 第一段(约30秒):启动项目,展示首页,介绍系统名称和技术栈。
  • 第二段(约1分钟):以管理员身份登录,演示图书添加、修改、删除,重点演示分页查询。
  • 第三段(约1分钟):以读者身份登录,演示搜索图书、借书、查看借阅记录。
  • 第四段(约1分钟):回到管理员视角,演示借出审核、图书归还处理、借阅记录查询。

录制工具用OBS Studio或者Win10自带的录屏功能都可以,分辨率建议1080P,声音清晰即可,不需要背景音乐。最后用剪映或者Premiere简单剪辑,配上字幕,导出MP4格式。视频文件注意别太大,五分钟左右压缩到50MB以内就行。

7. 踩坑记录与常见问题速查

7.1 数据库连接失败

这个报错应该是最常见的。现象:启动Tomcat后,访问任意需要数据库的操作,页面报500,Tomcat控制台显示Cannot create PoolableConnectionFactory或者Communications link failure

排查顺序:

  1. 确认MySQL服务是否启动:Windows下services.msc里找MySQL服务,Linux下systemctl status mysqld
  2. 确认连接参数:url里的数据库名、用户名、密码这三项,逐字核对。密码有特殊字符(如@、#)要注意在properties文件里转义。
  3. 确认驱动版本:5.x和8.x的驱动包混用会报奇怪的错误,统一到8.0.33。
  4. 确认防火墙:本机部署一般没这个问题,但如果MySQL跑在另一台机器上,注意3306端口放行。

7.2 Session失效导致跳转死循环

过滤器校验Session如果写得不好,可能出现"访问登录页跳登录页"的死循环。这是因为过滤器对所有请求都拦截了,包括login.jsp本身,导致请求被反复重定向。

解决方案我在前面已经写过——在Filter里放行login、register等白名单路径,放行静态资源路径。这里再强调一次:过滤器的放行逻辑要写在Session校验之前,别反了。

7.3 外键约束导致删除失败

管理员删除一个被借阅过的图书分类时,会报类似Cannot delete or update a parent row: a foreign key constraint fails的错误。

原因和解决办法:数据库层面存在外键关联,不能物理删除。我的方案是:对分类做修改时,不提供真正意义上的删除操作,而是把分类下的图书先移到"未分类"(category_id设为NULL),或者只允许删除无关联图书的分类。前端把删除按钮做个判断,有子图书时给出提示,显得更专业。

7.4 关于中文乱码的最终武器

如果你按照前面5.2的组合拳配置了还是乱码,有一个地方容易漏:数据库连接池的properties文件里,url中characterEncoding=utf8mb4characterEncoding=UTF-8的写法区别。我建议统一用characterEncoding=utf8,MySQL能正确识别,比utf8mb4更稳。表结构和JDBC连接都统一成UTF-8,逻辑简单不容易出错。

7.5 常见问题速查表

问题现象可能原因解决方案
页面404访问路径与Servlet映射不一致检查@WebServlet注解值和浏览器地址栏URL
页面500空指针request.getParameter取到null就调用方法增加参数校验,用StringUtils.isEmpty判空
登录后跳回登录页Session过期或登录校验失败确认登录成功后setAttribute了loginUser
数据能插入但全乱码数据库表不是utf8mb4修改表字符集ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4
JSP里EL表达式原样输出web.xml版本过低或禁用EL把web-app版本改为3.1+,或检查page指令isELIgnored=false
Tomcat启动慢随机数源有问题catalina.sh里加-Djava.security.egd=file:/dev/./urandom

7.6 课程设计答辩的常见追问与回答思路

这个问题虽然不在项目代码范围内,但对读者来说价值很大。老师最喜欢围绕下面几个点提问:

  • 为什么事务要放在Service层?——因为一个业务操作可能包含多个DAO调用,只有Service层能界定哪些DAO操作属于同一个业务单元,事务边界必须和业务边界保持一致。
  • include指令和include动作的区别?——指令是编译期静态包含,动作是运行期动态包含;静态包含编译成一个Servlet,动态包含分别编译再合并输出。
  • GET和POST的区别?——GET参数在URL上,长度有限制,适合查询;POST参数在请求体,适合提交数据、登录操作。幂等性和缓存语义也可以提。
  • 为什么用PreparedStatement?——预编译减少SQL解析开销,防止SQL注入,参数占位符可读性高。

8. 扩展方向:从课程设计到生产级项目

这套JavaEE图书管理系统做完之后,如果你想继续深入,有几个很自然的扩展方向,而且每一步都衔接得上:

第一,把JDBC换成Spring JDBC Template或者MyBatis,你会发现DAO层的代码量减少一半,这是JavaEE学习向Spring生态过渡的必然路径。

第二,引入Spring Boot重写一遍,用JPA或MyBatis-Plus做持久化,前端换成Vue+Element UI,这就是企业级前后端分离的雏形了。很多毕业设计就是用这个思路做的升级版。

第三,把借阅规则抽出来做成可配置——比如最大借阅数量、借阅天数、续借次数。用一张参数配置表去驱动业务逻辑,你会发现代码的扩展性和健壮性明显提升。

就我个人做完这套项目的实际体验来说,借书还书这个业务点弄明白了,后面学Spring声明式事务时会轻松很多。因为事务的本质从来不是某一种框架的API,而是"一组操作必须绑在一起提交"的语义,这个语义理解了,不管用什么框架都只是换了一个配置方式而已。

最后再分享一个实操小技巧:源码包里的SQL脚本,建议顺手把几类核心数据的INSERT语句也写好,比如管理员账号怎么重置密码、怎么快速插入一百本测试图书。这样无论是换一台机器部署、还是给同学复制一份项目,都能在三分钟内把环境跑起来。还有,演示视频录制前最好先完整演练一遍流程,尤其是数据库冷启动后第一次查询可能比较慢,提前把该预热的页面都点一遍,录出来的视频才会流畅。

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

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

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

立即咨询