资讯动态

慕仁大学图书馆管理系统:JSP+Spring+JDBC+Servlet 老架构实战与避坑

发布时间:2026/10/6 5:02:10 来源:尧图企业网站定制
简介这份资源是慕仁大学图书馆管理系统的完整项目源码采用JSP、Spring、JDBC与Servlet技术栈实现面向学习Java Web开发的学生与初级开发者可用于课程设计、毕业设计或SSM入门练手。系统分为学生用户端与管理员端学生端支持图书查询、预约、个人信息与借阅记录查看以及图书挂失管理员端涵盖学生信息增删改查、图书管理、借阅归还处理并设有超级管理员与普通管理员的权限控制功能闭环完整。压缩包为rar格式大小约2.8MB内含项目源码等文件目录结构清晰便于按模块阅读与二次开发。目前已有128人学习下载适合需要参考完整业务逻辑、理解分层架构与权限设计的读者对照学习快速掌握从数据库操作到前端交互的实现思路。1. 慕仁大学图书馆管理系统JSP Spring JDBC Servlet 这套老架构还值得动手吗如果你手上有一个「慕仁大学图书馆管理系统」的课程设计或二次开发需求技术栈写的是 JSP Spring JDBC Servlet第一反应大概率是这组合是不是有点老但真到落地阶段你会发现这套架构反而是最容易被讲清楚、被跑通、被答辩追问细节的一套。它没有 Spring Boot 自动装配的黑匣子也没有 MyBatis 的映射玄学控制流从 Servlet 到 Service 再到 JDBC 一目了然出问题能顺着调用链一路查到 SQL。它适合三类人一是要交课程设计、必须手写分层的学生二是接手了老馆藏系统、需要在不推翻架构的前提下加功能的一线维护者三是想借一个完整业务把 Servlet 生命周期、Spring IOC、JDBC 事务这三块基础打穿的人。这篇笔记就按「这是什么 → 怎么搭 → 坑在哪 → 怎么进阶」的顺序把慕仁大学图书馆管理系统从零到能跑讲一遍参数和目录结构都给到能直接抄的程度。2. 先把分层和依赖理清JSP、Servlet、Spring、JDBC 各自站哪一班岗2.1 四件套的职责边界与请求流转很多人做这个系统翻车不是代码写错而是没想清楚一次「借书」请求到底经过谁。慕仁大学图书馆管理系统的典型调用链是这样的浏览器发起/borrow?bookId1001Tomcat 把请求交给BorrowServletServlet 只做参数接收和结果转发真正的业务判断交给 Spring 容器里的BorrowServiceService 通过 JDBC 操作borrow_record和book两张表最后 Servlet 把结果塞进 request 域转发给borrow_result.jsp渲染。这里四个技术点的分工必须钉死JSP只负责展示理想状态下不写一行 Java 业务逻辑最多用 JSTL 做循环和判断。Servlet控制器负责收参数、调 Service、选视图不碰 SQL。Spring通过 IOC 管理 Service 和 DAO 的生命周期通过 AOP 或编程式事务管借书这种跨表操作。JDBC最底层的数据访问负责连接、PreparedStatement、结果集映射。为什么不用 Spring Boot 一把梭因为这套架构的价值就在于「每一层都看得见」。你在web.xml里配 Servlet在applicationContext.xml里配 Bean在 DAO 里手写Connection这些配置虽然啰嗦但答辩时老师问「Spring 是怎么把你的 Service 注入到 Servlet 的」你能指着WebApplicationContextUtils那行代码答上来而不是含糊说一句「自动装配的」。2.2 工程目录结构与依赖版本怎么定目录结构建议直接按下面这个来别自己发明library-system/ ├── src/main/java/ │ ├── com/muren/library/controller/ # Servlet │ ├── com/muren/library/service/ # 业务接口与实现 │ ├── com/muren/library/dao/ # JDBC 数据访问 │ ├── com/muren/library/entity/ # Book、User、BorrowRecord │ └── com/muren/library/util/ # DBUtil、PageUtil ├── src/main/resources/ │ ├── applicationContext.xml # Spring 核心配置 │ ├── jdbc.properties # 数据库连接参数 │ └── log4j.properties ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 与监听器注册 │ │ └── views/ # 放 JSP禁止直接访问 │ ├── css/ js/ images/ │ └── index.jsp └── pom.xml依赖版本上Spring 用 5.3.x 系列5.3.41 是常见稳定版Servlet API 用 4.0.1 并设providedJSP 用 2.3.xMySQL 驱动用 8.0.x连接池用 Druid 1.2.x。这里有个血泪经验Servlet API 一定要标provided否则打包进WEB-INF/lib会和 Tomcat 自带的冲突启动直接报ClassCastException。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope !-- 关键Tomcat 已提供不能打进 war -- /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.41/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency参数说明scopeprovided表示编译期可见、运行期由容器提供Spring 的spring-context会传递带入spring-core、spring-beans不用重复声明MySQL 8 驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver写错会报驱动加载失败。2.3 Spring 容器在 Web 环境里怎么启动纯 Java 项目里你new ClassPathXmlApplicationContext就行但 Web 项目里 Servlet 是 Tomcat 创建的Spring 容器得靠监听器启动。在web.xml里加ContextLoaderListener它会在应用启动时读取applicationContext.xml把容器塞进ServletContext。context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listenerServlet 里拿 Bean 的标准写法是WebApplicationContext ctx WebApplicationContextUtils .getRequiredWebApplicationContext(getServletContext()); BorrowService borrowService ctx.getBean(BorrowService.class);逻辑说明getRequiredWebApplicationContext从ServletContext里取监听器存进去的容器取不到会直接抛异常比getWebApplicationContext更安全。参数上contextConfigLocation不写默认找/WEB-INF/applicationContext.xml写了classpath:前缀就从类路径找建议显式写清楚避免部署到不同容器时路径解析不一致。3. 从建库到借书跑通JDBC 数据层与 Spring 事务的落地写法3.1 建表与连接池参数怎么配先把库建出来字段类型别乱选ISBN 用varchar(20)而不是数字因为可能带横杠库存用int并加CHECK (stock 0)兜底。CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, author VARCHAR(100), stock INT NOT NULL DEFAULT 0, total INT NOT NULL DEFAULT 0 ); CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0 -- 0 借出 1 已还 );连接参数放jdbc.properties用 Druid 连接池jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/muren_library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password参数说明serverTimezone必须写MySQL 8 不写会报时区异常useSSLfalse本地开发关掉省事生产环境要开characterEncodingutf8保证中文书名不乱码。连接池初始连接数设 5、最大 20 对课程设计足够设太大反而拖慢启动。3.2 DAO 层手写 JDBC 的模板与资源释放JDBC 最烦的是资源释放写不好就是连接泄漏。我一般抽一个DBUtil用 Spring 注入DataSourceDAO 里只关心 SQL。public class BookDaoImpl implements BookDao { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource dataSource; } Override public int decreaseStock(long bookId) { String sql UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, bookId); return ps.executeUpdate(); // 返回受影响行数0 表示库存不足 } catch (SQLException e) { throw new RuntimeException(扣减库存失败, e); } } }逻辑说明try-with-resources自动关闭Connection和PreparedStatement不用手写 finally。SQL 里AND stock 0是关键它把「判断库存」和「扣库存」合成一条原子语句避免先查后改的并发超借。参数说明executeUpdate返回受影响行数Service 层拿到 0 就该抛「库存不足」业务异常而不是继续插借阅记录。3.3 借书业务的事务边界与 Spring 配置借书要同时做两件事扣库存、插借阅记录。这两步必须在一个事务里否则扣了库存插记录失败书就凭空少了。用 Spring 的DataSourceTransactionManager加 XML 声明式事务最直观。bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean bean idborrowService classcom.muren.library.service.impl.BorrowServiceImpl property namebookDao refbookDao/ property namerecordDao refrecordDao/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method nameborrow* propagationREQUIRED rollback-forException/ tx:method namequery* read-onlytrue/ /tx:attributes /tx:advice aop:config aop:pointcut idsvcPoint expressionexecution(* com.muren.library.service..*(..))/ aop:advisor advice-reftxAdvice pointcut-refsvcPoint/ /aop:config逻辑说明tx:advice定义事务规则borrow*开头的方法走 REQUIRED 传播query*走只读事务减少锁开销。rollback-forException保证业务异常也回滚默认只回滚RuntimeException这点很多人栽过。参数说明propagationREQUIRED表示有事务就加入、没有就新建是绝大多数业务方法的正确选择read-onlytrue只是给数据库的优化提示不是强制。Service 实现里就干净了public void borrow(long userId, long bookId) { int rows bookDao.decreaseStock(bookId); if (rows 0) { throw new BizException(库存不足借阅失败); } recordDao.insert(new BorrowRecord(userId, bookId, new Date())); }4. Servlet 与 JSP 联调请求分发、中文乱码和分页这几处最容易翻车4.1 Servlet 注册方式与请求参数接收Servlet 有两种注册方式web.xml配置和WebServlet注解。老项目里web.xml更常见也更好排查因为所有映射集中在一处。servlet servlet-nameborrowServlet/servlet-name servlet-classcom.muren.library.controller.BorrowServlet/servlet-class /servlet servlet-mapping servlet-nameborrowServlet/servlet-name url-pattern/borrow/url-pattern /servlet-mappingServlet 里接收参数并调用 Serviceprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 必须在 getParameter 之前 long userId Long.parseLong(req.getParameter(userId)); long bookId Long.parseLong(req.getParameter(bookId)); try { borrowService.borrow(userId, bookId); req.setAttribute(msg, 借阅成功); } catch (BizException e) { req.setAttribute(msg, e.getMessage()); } req.getRequestDispatcher(/WEB-INF/views/borrow_result.jsp).forward(req, resp); }逻辑说明setCharacterEncoding必须放在第一次getParameter之前否则不生效这是中文乱码最常见的根因。转发用forward而不是redirect因为要带 request 域的提示信息。参数说明Long.parseLong对空串或非数字会抛NumberFormatException生产代码应先做空值和格式校验别直接裸转。4.2 JSP 页面只做展示的写法与分页JSP 里用 JSTL 遍历别写% %脚本片段。分页是图书馆系统的刚需PageUtil算好起止行DAO 用LIMIT查询。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${page.list} varbook tr td${book.title}/td td${book.author}/td td${book.stock}/td /tr /c:forEach /table a hrefbook?pageNum${page.pageNum - 1}上一页/a a hrefbook?pageNum${page.pageNum 1}下一页/a逻辑说明${page.list}从 request 域取分页对象c:forEach渲染行。参数说明pageNum要做边界判断小于 1 归 1大于总页数归总页数否则会查出空列表或越界。分页 SQL 是SELECT * FROM book LIMIT ?, ?两个参数分别是(pageNum-1)*pageSize和pageSize注意 MySQL 的 LIMIT 偏移从 0 开始。4.3 静态资源与 JSP 直接访问的防护WEB-INF下的 JSP 浏览器无法直接访问只能通过 Servlet 转发这是天然的保护。但 CSS、JS、图片要放在webapp根目录下否则页面样式全丢。常见做法是把视图统一放WEB-INF/views/静态资源放webapp/static/在 JSP 里用${pageContext.request.contextPath}/static/css/main.css引用避免部署路径变化导致 404。5. 避坑与排查这套老架构里最耗时间的 5 个问题5.1 启动报 NoSuchBeanDefinitionException现象Tomcat 启动后访问 Servlet抛NoSuchBeanDefinitionException: No bean named borrowService available。原因applicationContext.xml没被加载或者 Bean 的 id 和getBean用的名字不一致。解决先确认web.xml里ContextLoaderListener配了、contextConfigLocation路径对再检查getBean用的是类名还是 id用getBean(BorrowService.class)按类型取最稳。5.2 中文书名入库变问号现象JSP 提交的中文书名存进数据库变成???。原因三个环节任一没设 UTF-8——JSP 页面pageEncoding、Servlet 的setCharacterEncoding、JDBC URL 的characterEncoding。解决三处一起改JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 在取参数前设编码URL 加characterEncodingutf8。数据库和表的字符集也要是utf8mb4。5.3 借书后库存扣了但记录没插进去现象并发测试时库存少了借阅记录却没生成。原因事务没生效Service 方法没被 AOP 代理或者异常被 catch 后没重新抛出事务管理器感知不到。解决确认aop:config的切点表达式覆盖到 Service 包catch 块里要么重新抛异常要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。5.4 连接池耗尽报 wait millis 超时现象压测一会儿就报wait millis 60000, active 20, maxActive 20。原因DAO 里Connection没关或者事务方法里嵌套查询开了新连接没释放。解决全部改用try-with-resources检查是否有在循环里getConnection的写法把maxActive适当调大只是缓解根治还是靠释放。5.5 JSP 页面刷新后重复提交现象借书成功后按 F5又借了一本。原因表单提交后直接 forward 到结果页刷新会重发上一次 POST。解决成功后用response.sendRedirect重定向到列表页PRG 模式提示信息可以放 session 里取一次就删避免刷新重复提交。6. 进阶把 JDBC 裸写升级成模板化并给这套系统加一层可验证的测试6.1 用 JdbcTemplate 干掉重复的样板代码手写 JDBC 的getConnection、setXxx、close三件套写多了就是体力活。Spring 的JdbcTemplate能把这些收掉同时保留你对 SQL 的完全控制特别适合这种不想上 ORM 的老系统。Repository public class BookDaoImpl implements BookDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate new JdbcTemplate(dataSource); } Override public ListBook findByPage(int offset, int size) { String sql SELECT id, isbn, title, author, stock FROM book ORDER BY id LIMIT ?, ?; return jdbcTemplate.query(sql, new Object[]{offset, size}, (rs, rowNum) - { Book b new Book(); b.setId(rs.getLong(id)); b.setTitle(rs.getString(title)); b.setStock(rs.getInt(stock)); return b; }); } }逻辑说明query的第三个参数是RowMapper用 Lambda 把结果集映射成实体比手写while(rs.next())清爽。参数说明new Object[]{offset, size}按顺序绑定占位符注意LIMIT的两个参数都是 int别传成字符串。改造后 DAO 里不再出现Connection连接释放交给模板泄漏风险大幅下降。6.2 给借书逻辑补一个可跑的单元测试老架构最缺的就是测试答辩时能跑一个测试用例说服力完全不一样。用 JUnit 4 Spring Test 加载容器测「库存不足时抛异常且不插记录」。RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(locations classpath:applicationContext.xml) public class BorrowServiceTest { Autowired private BorrowService borrowService; Test(expected BizException.class) public void borrow_shouldFail_whenStockIsZero() { borrowService.borrow(1L, 9999L); // 9999 号书库存为 0 } }逻辑说明ContextConfiguration加载和 Web 环境同一份 Spring 配置保证测的就是线上那套 Bean。Test(expected BizException.class)断言抛异常同时因为事务回滚测试不会污染数据库。参数说明测试库建议单独建一个muren_library_test在jdbc.properties里用 Maven profile 切换别拿生产库跑测试。6.3 一个我踩过的坑和现在的习惯早期我图省事把事务切点写成execution(* com.muren.library.service.*.*(..))只匹配了 service 包下第一层类结果 Service 实现放在service.impl子包里事务压根没生效库存和记录对不上查了整整一个下午。现在的习惯是切点一律写service..*(..)双点匹配所有子包改完配置先写一个「故意抛异常」的测试验证回滚确认事务真的在起作用再往下写业务。这套 JSP Spring JDBC Servlet 的组合虽然老但每一层都能被验证、被追问、被替换把上面这些参数和坑过一遍慕仁大学图书馆管理系统就能稳稳跑起来后面加预约、加罚款、加推荐都是在这个骨架上长肉。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑