
简介这是一套面向计算机专业本科生的JavaEE实战项目资源专为课程设计、期末大作业及毕业设计准备覆盖图书管理核心业务全流程帮助学习者快速掌握SSMSpringSpringMVCMyBatis框架整合开发与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而不是SpringBoot2024年了企业里新项目基本都是SpringBoot学校教JavaEE还在用ServletJSP于是很多同学纠结课程设计到底用哪套我的建议是如果课程要求明确写了JavaEE/JSP/Servlet就老老实实用传统方式。原因有三点第一课程设计考察的是你对课堂知识的掌握程度你用SpringBoot框架把底层细节全封住了答辩时老师追问个Servlet生命周期、过滤器怎么配置你答不上来反而扣分。第二ServletJSP这套路虽然老但它是理解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为例说一下最标准、最不容易出错的建项目流程新建Dynamic Web Project项目名比如叫LibrarySystemTarget Runtime选Tomcat 9。在src下建包结构com.library.bean、com.library.dao、com.library.service、com.library.servlet、com.library.filter、com.library.util。把mysql驱动jar包和jstl的jar包复制到src/main/webapp/WEB-INF/libEclipse里路径是WebContent/WEB-INF/lib。在WebContent下建jsp目录把页面文件放这里css和js放assets目录。修改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 数据模型总览与建表思路图书管理系统数据库设计核心就是三张业务表加一个分类表。建表之前先想清楚实体关系我用最简单的文字描述一下用户读者/管理员与借阅记录一对多图书与借阅记录一对多图书与分类多对一围绕这四张表再加一个管理员日志表可选就够用了。数据库命名上我统一用英文小写加下划线字段名避开关键词比如desc、order、useruser虽然不是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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.3 设计决策为什么把库存拆成stock和remaining图书表里我设计了stock总库存和remaining可借库存两个字段。很多人会问一个字段不就行了吗借出时减一还回时加一。但真实业务里总库存是书的馆藏数量可借库存是实际在架可借的数量两者有各自的含义。比如一本书有5本其中2本被借出3本还在架上stock5、remaining3如果要展示馆藏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转发或重定向到JSPService层业务层处理业务规则比如借书前校验库存和读者身份、密码加密、借阅天数计算负责事务边界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配置driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize5 maxActive20这里需要注意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 ListBook 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()) { ListBook 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设置deleted1而不是物理DELETE。查询条件里统一加上AND deleted0。这样做的好处是历史借阅记录永远可追溯对课程设计而言逻辑删除是加分项。第二修改操作用表单提交后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 借书还书的完整流程借书流程我完整走一遍读者在前台搜索图书点击借阅按钮URL形如/borrowBook?bookId123BorrowServlet接收请求从Session拿loginUser获取userId从参数拿bookId调Service层borrowBook方法方法内部a. 检查用户status是否为1未被禁用b. 查询图书判断remaining是否大于0c. 检查该用户是否已借了这本书且未还防止重复借同一本d. 插入t_borrow记录borrow_timenow()due_timenow() 30天e. 更新t_book的remaining remaining - 1f. commit所有操作跳转到借阅成功或借阅失败页面带上错误信息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 prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${page.list} varbook 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} onclickreturn 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-nameEncodingFilter/filter-name filter-classcom.library.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingEncodingFilter的doFilter里面request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response);数据库端连接URL已经加了characterEncodingutf8mb4表结构统一用utf8mb4JSP页面顶部加上% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。这四层都覆盖到位乱码基本可以杜绝。如果还乱码优先检查一下Tomcat的server.xml里Connector有没有URIEncodingUTF-8配置。GET请求的参数编码处理方式跟POST不同Tomcat 8默认URIEncoding就是UTF-8旧版本需要手动加。5.3 页面布局复用与样式课程设计的前端不要求多惊艳但至少结构清晰、风格统一。我建议把导航栏和页脚抽成公共的JSP片段用% include fileheader.jsp %引入。布局上后台页面用左侧菜单右侧内容区的经典布局前台用户页面用顶部导航中部内容底部页脚的布局。CSS直接手写基础样式不用引入Bootstrap也行但要控制好字体、间距、表格样式、按钮风格这几个维度的一致性看着才会像整体。如果你时间充裕花半小时做一个简单的CSS变量:root里定--primary-color调整主题色只需改一处页面视觉就能统一这在演示的时候也很加分。6. 部署发布与演示视频录制经验6.1 从IDEA/Eclipse导出Web项目并部署项目做完以后要么打WAR包要么直接把整个项目目录复制到Tomcat的webapps下。这里讲一下打包标准流程确认项目能正常构建在IDE里右键项目选择Export - WAR fileEclipse或者用Maven的package如果用了Maven。WAR包命名不能有中文和空格最好是LibrarySystem.war。把WAR包复制到Tomcat的webapps目录下启动Tomcat它会自动解压部署。访问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后访问任意需要数据库的操作页面报500Tomcat控制台显示Cannot create PoolableConnectionFactory或者Communications link failure。排查顺序确认MySQL服务是否启动Windows下services.msc里找MySQL服务Linux下systemctl status mysqld。确认连接参数url里的数据库名、用户名、密码这三项逐字核对。密码有特殊字符如、#要注意在properties文件里转义。确认驱动版本5.x和8.x的驱动包混用会报奇怪的错误统一到8.0.33。确认防火墙本机部署一般没这个问题但如果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中characterEncodingutf8mb4和characterEncodingUTF-8的写法区别。我建议统一用characterEncodingutf8MySQL能正确识别比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 utf8mb4JSP里EL表达式原样输出web.xml版本过低或禁用EL把web-app版本改为3.1或检查page指令isELIgnoredfalseTomcat启动慢随机数源有问题catalina.sh里加-Djava.security.egdfile:/dev/./urandom7.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做持久化前端换成VueElement UI这就是企业级前后端分离的雏形了。很多毕业设计就是用这个思路做的升级版。第三把借阅规则抽出来做成可配置——比如最大借阅数量、借阅天数、续借次数。用一张参数配置表去驱动业务逻辑你会发现代码的扩展性和健壮性明显提升。就我个人做完这套项目的实际体验来说借书还书这个业务点弄明白了后面学Spring声明式事务时会轻松很多。因为事务的本质从来不是某一种框架的API而是一组操作必须绑在一起提交的语义这个语义理解了不管用什么框架都只是换了一个配置方式而已。最后再分享一个实操小技巧源码包里的SQL脚本建议顺手把几类核心数据的INSERT语句也写好比如管理员账号怎么重置密码、怎么快速插入一百本测试图书。这样无论是换一台机器部署、还是给同学复制一份项目都能在三分钟内把环境跑起来。还有演示视频录制前最好先完整演练一遍流程尤其是数据库冷启动后第一次查询可能比较慢提前把该预热的页面都点一遍录出来的视频才会流畅。本文还有配套的精品资源点击获取