共享图书管理系统:JSP+JDBC+Oracle 11g 从文档到可运行系统全解析

发布时间:2026/10/6 15:27:59
共享图书管理系统:JSP+JDBC+Oracle 11g 从文档到可运行系统全解析 简介这份资源是《共享图书管理系统设计与实现》的完整Word文档面向计算机相关专业的课程设计、毕业设计学生以及需要图书管理类项目参考的开发人员帮助解决传统图书借阅管理效率低、个性化服务不足等问题。压缩包内共1个docx文件约13.15MB内容涵盖绪论、相关理论与技术、系统需求分析等章节涉及面向对象、I/O、JSON、JDBC、JSP、Ajax、Tomcat、Eclipse与Oracle 11g等知识点并给出提高图书资源利用率、优化用户体验、增强系统安全性、支持多平台访问及数据分析等开发目标。目前已有109人学习适合需要完整项目文档结构、技术选型说明与需求分析思路的读者参考可据此快速理解共享图书管理系统的设计脉络与实现要点。1. 从一份 docx 到能跑的系统共享图书管理系统到底交付了什么很多人看到「共享图书管理系统设计与实现.docx」第一反应是又一份课程设计文档翻两页就扔进硬盘吃灰。但如果你正卡在毕业设计开题、或者要交一个能演示的 Java Web 项目这份文档的价值不在文字而在它把一套完整系统的骨架摊开了需求分析、E-R 图、数据库表、模块划分、功能测试用例一条线走到底。它对应的技术栈是 JSP JDBC Oracle 11g Ajax后端用 Spring 做装配前端页面动态渲染属于典型的「传统但能跑」的 Java Web 组合。适合谁适合需要一份可复现、可改造成自己项目的学生和初级工程师也适合想快速搭一个图书借阅原型验证业务逻辑的人。它解决的不是高并发而是「从零到一有个能登录、能借书、能还书的完整闭环」。2. 技术选型拆解为什么是 JSP JDBC Oracle 11g 这套老组合2.1 复用式系统与模块化设计原则文档里反复提到「复用式系统」这个词不是凑字数。它的实际含义是把用户管理、图书管理、借阅管理拆成独立模块每个模块内部再分 Dao 层、Service 层、Controller 层层与层之间通过接口调用而不是互相直接 new 对象。这样做的好处是你改借书逻辑时不会把登录功能搞崩。模块化设计原则在需求分析章节里对应的是「高内聚低耦合」落到代码上就是每个功能一个 Servlet 或一个 Controller 方法数据库操作统一走 JDBC 工具类。我一般会这样组织包结构src/ ├── com.library.dao # 数据库操作每个表一个 Dao ├── com.library.service # 业务逻辑借书还书的核心判断 ├── com.library.servlet # 接收前端请求调用 Service ├── com.library.entity # 实体类和数据库表字段一一对应 ├── com.library.util # JDBC 连接工具、JSON 转换工具 └── com.library.filter # 编码过滤器、登录拦截器这个结构不是文档里写死的但按它走后面加「图书推荐」或者「借阅统计」时你只需要新增 Dao 和 Service不用动已有的借还书代码。参数上唯一要注意的是包名统一别一会儿com.library一会儿com.book否则 Spring 扫描不到 Bean启动就报NoSuchBeanDefinitionException。2.2 Oracle 11g 的 JDBC 连接与驱动配置文档指定 Oracle 11g 作为数据库这个选择在课程设计里很常见因为 Oracle 的 E-R 图工具和表空间概念适合教学。但 Oracle 11g 的 JDBC 驱动和 MySQL 不一样它的驱动类是oracle.jdbc.driver.OracleDriver连接 URL 格式是jdbc:oracle:thin:主机:端口:SID。很多人在这一步翻车是因为把 MySQL 的 URL 格式直接套过来结果报IO Error: The Network Adapter could not establish the connection。正确的连接工具类写法public class DBUtil { private static final String DRIVER oracle.jdbc.driver.OracleDriver; // 注意orcl 是 SID不是服务名服务名要用 //主机:端口/服务名 private static final String URL jdbc:oracle:thin:localhost:1521:orcl; private static final String USER library; private static final String PASSWORD library123; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { // 驱动没导入 ojdbc6.jar 时会走到这里 e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明静态代码块只执行一次保证驱动只加载一次。getConnection每次调用返回新连接用完后必须在 finally 里 close否则 Oracle 的连接数很快耗尽报ORA-00020: maximum number of processes exceeded。参数说明localhost换成数据库服务器 IP1521是 Oracle 默认监听端口orcl是安装时创建的数据库 SID。如果你用的是 Oracle 11g Express EditionSID 通常是xeURL 要改成jdbc:oracle:thin:localhost:1521:xe。2.3 JSP Ajax 的前端数据交互方式文档里前端用 JSP 动态渲染页面Ajax 做无刷新更新。这个组合在 2024 年看确实老但它的好处是逻辑直白JSP 在服务器端把数据拼成 HTML 返回Ajax 只负责局部刷新。比如用户借书时点击「借阅」按钮Ajax 发一个 POST 请求到 ServletServlet 调用 Service 判断库存和借阅上限返回 JSON 给前端前端根据success字段决定弹「借阅成功」还是「已达上限」。一个典型的 Ajax 请求写法function borrowBook(bookId) { var xhr new XMLHttpRequest(); xhr.open(POST, BorrowServlet, true); // 必须设置编码否则中文书名传到后端变乱码 xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.onreadystatechange function () { if (xhr.readyState 4 xhr.status 200) { var res JSON.parse(xhr.responseText); if (res.code 200) { alert(借阅成功归还日期 res.dueDate); } else { alert(借阅失败 res.message); } } }; xhr.send(bookId encodeURIComponent(bookId)); }逻辑说明readyState 4表示请求完成status 200表示服务器正常响应。JSON.parse把后端返回的字符串转成对象。参数说明bookId用encodeURIComponent包一层防止特殊字符截断参数。后端 Servlet 里要用request.setCharacterEncoding(UTF-8)和response.setContentType(application/json;charsetUTF-8)否则前端收到的中文是问号。这个坑我在第一次做 JSP 项目时踩过查了两个小时才发现是响应头没设编码。3. 从 E-R 图到建表数据库设计与核心表结构落地3.1 概念设计与局部 E-R 图转物理表文档第 4 章给了局部 E-R 图实体包括用户、图书、图书类型、借阅记录。转换规则很直接一个实体一张表多对多关系拆成中间表。用户和图书之间是借阅关系一个用户可借多本书一本书可被多个用户借过所以借阅记录表要独立出来字段包括借阅 ID、用户 ID、图书 ID、借出日期、应还日期、实还日期、状态。图书和图书类型是一对多类型 ID 作为图书表的外键。建表时最容易忽略的是外键约束和索引。Oracle 11g 里如果不加外键删图书类型时不会报错但图书表里会留下孤儿记录。我一般会在借阅记录表的user_id和book_id上建索引因为查询「某用户的借阅记录」和「某本书的借阅历史」是高频操作。3.2 核心表 DDL 与字段类型选择下面是四张核心表的建表语句字段类型按 Oracle 11g 的规范来-- 用户表 CREATE TABLE t_user ( user_id NUMBER(10) PRIMARY KEY, username VARCHAR2(50) NOT NULL UNIQUE, password VARCHAR2(100) NOT NULL, real_name VARCHAR2(50), phone VARCHAR2(20), role NUMBER(1) DEFAULT 0, -- 0 普通用户1 管理员 create_time DATE DEFAULT SYSDATE ); -- 图书类型表 CREATE TABLE t_book_type ( type_id NUMBER(10) PRIMARY KEY, type_name VARCHAR2(50) NOT NULL, remark VARCHAR2(200) ); -- 图书表 CREATE TABLE t_book ( book_id NUMBER(10) PRIMARY KEY, book_name VARCHAR2(100) NOT NULL, author VARCHAR2(50), publisher VARCHAR2(100), isbn VARCHAR2(20), type_id NUMBER(10), total_count NUMBER(5) DEFAULT 1, -- 总库存 avail_count NUMBER(5) DEFAULT 1, -- 可借数量 CONSTRAINT fk_book_type FOREIGN KEY (type_id) REFERENCES t_book_type(type_id) ); -- 借阅记录表 CREATE TABLE t_borrow ( borrow_id NUMBER(10) PRIMARY KEY, user_id NUMBER(10) NOT NULL, book_id NUMBER(10) NOT NULL, borrow_date DATE DEFAULT SYSDATE, due_date DATE NOT NULL, return_date DATE, status NUMBER(1) DEFAULT 0, -- 0 借出未还1 已归还 CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES t_user(user_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES t_book(book_id) ); -- 高频查询索引 CREATE INDEX idx_borrow_user ON t_borrow(user_id); CREATE INDEX idx_borrow_book ON t_borrow(book_id);逻辑说明NUMBER(10)做主键配合序列使用。VARCHAR2是 Oracle 推荐字符串类型别用VARCHAR。DATE类型存日期和时间SYSDATE是数据库服务器当前时间。参数说明total_count和avail_count分开存借书时avail_count减一还书时加一这样不用每次去 count 借阅记录算库存。status用数字而不是布尔因为 Oracle 没有布尔类型。主键生成我一般用序列加触发器或者直接在 Java 层用SELECT seq.NEXTVAL FROM DUAL取。3.3 借书与还书的库存一致性处理借书功能的核心不是 insert 一条借阅记录而是保证库存扣减和记录插入在同一个事务里。如果先 insert 记录再 update 库存中间抛异常就会出现「记录有了但库存没扣」的脏数据。正确做法是用 JDBC 的事务控制public boolean borrowBook(int userId, int bookId) { Connection conn null; PreparedStatement ps1 null, ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 扣减可借数量条件 avail_count 0 防止超借 String sql1 UPDATE t_book SET avail_count avail_count - 1 WHERE book_id ? AND avail_count 0; ps1 conn.prepareStatement(sql1); ps1.setInt(1, bookId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 库存不足 } // 2. 插入借阅记录应还日期为当前日期加 30 天 String sql2 INSERT INTO t_borrow(borrow_id, user_id, book_id, due_date) VALUES(seq_borrow.NEXTVAL, ?, ?, SYSDATE 30); ps2 conn.prepareStatement(sql2); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { // 关闭资源顺序是后开的先关 try { if (ps2 ! null) ps2.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps1 ! null) ps1.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明setAutoCommit(false)关闭自动提交两条 SQL 要么都成功要么都回滚。avail_count 0放在 UPDATE 的 WHERE 里利用数据库行锁保证并发下不会超借。SYSDATE 30是 Oracle 的日期运算表示当前时间加 30 天。参数说明userId和bookId从 Session 和前端参数取别信任前端传的用户 ID要从登录时存的 Session 里拿否则改个参数就能替别人借书。4. 功能模块实现登录、借阅、管理三大块的代码落地4.1 用户登录与注册的 Servlet 实现登录功能看着简单但坑不少。文档里写了用户登录和管理员登录两个入口实际实现时可以用同一个 Servlet根据role字段区分跳转页面。注册功能要注意用户名唯一性校验不能等 insert 报唯一约束异常才提示要在插入前先查一次。WebServlet(/LoginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserService userService new UserService(); User user userService.login(username, password); if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(currentUser, user); // 根据角色跳不同页面 if (user.getRole() 1) { response.sendRedirect(admin/index.jsp); } else { response.sendRedirect(user/index.jsp); } } }逻辑说明request.setCharacterEncoding(UTF-8)必须在取参数之前调用否则中文用户名乱码。session.setAttribute把用户对象存到会话里后续页面用session.getAttribute(currentUser)取。参数说明role为 1 跳管理员后台为 0 跳用户前台。密码在数据库里存明文是课程设计的常见做法但实际项目里至少要做 MD5 加盐这个文档没提你自己加的时候在 Service 层处理。4.2 借阅记录查询与还书功能还书功能比借书多一步更新借阅记录状态和归还日期同时把库存加回去。查询借阅记录时用户只能看自己的管理员能看所有人的这个权限判断放在 Service 层不要只靠前端隐藏菜单。public ListBorrowRecord getBorrowRecords(int userId, boolean isAdmin) { String sql; if (isAdmin) { sql SELECT b.borrow_id, u.username, bk.book_name, b.borrow_date, b.due_date, b.status FROM t_borrow b JOIN t_user u ON b.user_id u.user_id JOIN t_book bk ON b.book_id bk.book_id ORDER BY b.borrow_date DESC; } else { sql SELECT b.borrow_id, u.username, bk.book_name, b.borrow_date, b.due_date, b.status FROM t_borrow b JOIN t_user u ON b.user_id u.user_id JOIN t_book bk ON b.book_id bk.book_id WHERE b.user_id ? ORDER BY b.borrow_date DESC; } // 执行查询并封装成 List代码略 return null; }逻辑说明管理员查询不带WHERE user_id条件普通用户查询带参数占位符。用 JOIN 而不是子查询因为借阅记录表只存 ID页面要显示用户名和书名。参数说明isAdmin从 Session 里的用户角色判断不要从请求参数取否则普通用户传isAdmintrue就能看所有人记录。4.3 管理员图书管理与类型管理管理员功能包括添加图书、修改图书信息、删除图书类型、查看所有用户。添加图书时total_count和avail_count要设成一样的值否则新书一上架就显示不可借。删除图书类型前要先检查该类型下有没有图书有的话不能删否则外键约束会报错。public boolean deleteBookType(int typeId) { // 先查该类型下是否有图书 String checkSql SELECT COUNT(*) FROM t_book WHERE type_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, typeId); ResultSet rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; // 有图书关联不允许删除 } } catch (SQLException e) { e.printStackTrace(); return false; } // 没有关联才执行删除 String delSql DELETE FROM t_book_type WHERE type_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(delSql)) { ps.setInt(1, typeId); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }逻辑说明先 count 再 delete两步分开。try-with-resources自动关闭连接比手动 finally 简洁。参数说明typeId从请求参数取但删除前要确认当前用户是管理员这个判断在 Filter 里做不在 Service 里重复。5. 避坑与排查这套系统跑不起来时先看这五条5.1 现象启动 Tomcat 报 ClassNotFoundException: oracle.jdbc.driver.OracleDriver原因ojdbc6.jar没放进WEB-INF/lib目录或者放进去但没加到 Build Path。Eclipse 里经常出现「文件在但类找不到」是因为 jar 只在项目根目录没在 Web 部署路径下。解决把ojdbc6.jar复制到WebContent/WEB-INF/lib然后在 Eclipse 里右键项目 → Properties → Deployment Assembly → Add → Java Build Path Entries → 选中 ojdbc6.jar。重启 Tomcat。5.2 现象Ajax 请求返回 200 但前端收不到数据控制台报 JSON 解析错误原因后端返回的是 HTML 片段而不是 JSON或者返回的 JSON 里有中文但响应头没设 UTF-8前端JSON.parse直接抛异常。解决在 Servlet 里加response.setContentType(application/json;charsetUTF-8)并且用PrintWriter out response.getWriter(); out.print(jsonStr); out.flush();输出。别用response.getOutputStream()写字符会乱码。5.3 现象借书时提示成功但库存没变或者库存变了但借阅记录没生成原因JDBC 连接默认自动提交两条 SQL 各自独立执行中间出错不会回滚。或者avail_count的 UPDATE 条件写成了avail_count 0导致库存为 0 时还能借。解决在 Service 方法开头conn.setAutoCommit(false)两条 SQL 都成功后conn.commit()catch 块里conn.rollback()。UPDATE 条件必须是avail_count 0。5.4 现象JSP 页面中文显示为乱码数据库里存进去也是问号原因JSP 页面顶部没声明% page contentTypetext/html;charsetUTF-8 %或者 Tomcat 的server.xml里 Connector 没加URIEncodingUTF-8。解决JSP 第一行加 page 指令Servlet 里request.setCharacterEncoding(UTF-8)和response.setCharacterEncoding(UTF-8)都加上。如果还乱码检查 Oracle 数据库的字符集是不是ZHS16GBK是的话建库时改成AL32UTF8。5.5 现象管理员登录后能看到用户菜单或者普通用户能访问管理员页面原因权限判断只在前端 JSP 里用if隐藏了菜单但 URL 直接输入还能访问。Session 里的 role 字段没在 Filter 里校验。解决写一个AdminFilter拦截/admin/*路径从 Session 取currentUser如果为 null 或role ! 1直接response.sendRedirect(../login.jsp)。Filter 的urlPatterns配/admin/*别配/*否则登录页也被拦。6. 进阶改造把 JSP 换成前后端分离以及测试用例怎么补这套系统跑通之后如果你想让它看起来不那么「课程设计」最值得做的一步是把 JSP 渲染改成前后端分离。具体做法是保留 Servlet 作为接口层但不再forward到 JSP而是统一返回 JSON。前端用一个简单的 HTML 页面加 Ajax 调用或者用 Vue 的 CDN 版本不用搭 Node 环境。这样改完你的项目在答辩时能多说一句「接口可复用前端可替换」比纯 JSP 有说服力。改造时注意三个点。第一登录状态用 Session 还是 Token课程设计级别继续用 Session 就行Servlet 里request.getSession().getAttribute(currentUser)照样能取。第二原来的 JSP 页面里的 Java 代码片段要全部抽到 Servlet 里JSP 只留 HTML 和 JavaScript。第三Ajax 请求的 URL 从xxx.jsp改成xxxServlet返回格式统一成{code: 200, data: ..., message: ...}。测试用例方面文档第 5 章给了功能测试的框架但没给具体数据。我一般会补一张边界测试表测试项输入预期结果实际结果借书库存为 0bookId已借完的书提示库存不足不生成记录一致重复借同一本书同一用户同一 bookId提示已借过不扣库存一致还书后再借归还后再次借阅借阅成功库存减一一致删除有图书的类型typeId有书的类型提示无法删除一致普通用户访问 admin直接输入 admin URL跳转登录页一致这张表填完你的测试章节就不是空话。最后说一个我自己的习惯每次改完 Dao 层的 SQL我都会先在 Oracle 的 SQL Developer 里裸跑一遍确认能查出数据再放进 Java 代码里。因为 JDBC 报的 SQL 异常经常只给一个ORA-00933不告诉你哪一行错了在数据库工具里跑能省一半排查时间。从那以后我每次调 SQL 都强制走一遍这个流程希望帮到你。本文还有配套的精品资源点击获取