
简介Java图书管理系统源代码是一套面向Java学习者的完整实战项目主要解决图书馆图书录入、查询、借阅、归还、续借及状态跟踪等场景需求。资源采用MVC分层设计涵盖数据库表结构、前端交互界面、登录权限控制等关键模块适合正在学习JavaWeb开发或需要课程设计参考的读者。压缩包共212个文件包含31个Java源文件、112个编译后的class文件、2个jar依赖包和2个db数据库文件以及56张界面预览图整体仅4.01MB目录按业务功能组织如借阅、归还、读者管理等模块便于对照学习。已有1873人下载学习。通过研究这套源码读者能理解从需求分析、数据库建模到后端接口实现、界面交互的完整流程还能掌握异常处理、日志记录、单元测试等工程实践方法对提升项目综合开发能力有明显帮助。1. 一套跑不起来的图书管理系统源代码拆开看反而学到最多某位同学从网上下了一套Java图书管理系统的源代码导入IDE一运行不是数据库连接失败就是页面404折腾两个晚上硬是没跑起来。我过去看了下问题其实不在代码本身——依赖JAR没按规范放到WEB-INF/lib下、数据库初始化脚本没执行、Servlet的URL映射和页面请求路径对不上。把这几处捋顺之后系统跑得相当顺滑。整套系统不依赖Spring Boot、不需要Maven就是最纯粹的JavaWeb三件套JSP负责页面展示、Servlet负责控制跳转、JDBC负责数据访问。管理员登录后可以维护图书信息、管理借阅记录普通用户注册后可以检索图书、发起借阅、归还图书。对正在做JavaWeb课程设计、或者想找一个能逐行看懂并亲手跑起来的参考项目的人来说这套源码的价值不在于功能花哨而是足够简单——简单到每一条请求路径都能从浏览器一路追踪到数据库没有任何黑匣子。这篇笔记按「系统是什么 → 架构怎么搭 → 核心代码怎么走 → 怎么部署跑通 → 怎么动手改」来拆中间章节会贴完整目录结构和核心代码最后演示一个给图书增加封面图片上传的完整二次开发案例。2. 技术选型与整体架构JDBCServletJSP的经典三层如何协作2.1 技术栈全景每一层负责什么、为什么这么选这套系统的技术栈可以归纳成一句话Servlet管控制、JSP管展示、JDBC管数据。控制器接收浏览器请求解析参数并调用DAO层把查询结果塞进request作用域后转发到JSP页面渲染JSP页面里用EL表达式和JSTL标签读取数据生成HTMLDAO层写原生SQL通过JDBC的PreparedStatement执行查询拿到ResultSet后手动映射成实体类对象。三层结构最大的好处是各层职责单一。JSP里不写Java业务逻辑、Servlet里不拼SQL、DAO里不做页面跳转。这套源码在这一点上做得比较规范虽然个别JSP页面里还是能看到%%脚本片段但整体分层意识是明确的。这也正是选这套技术栈做教学和课程设计的原因没有框架的遮掩每一行代码都是自己可控的出问题能够从HTTP请求链路里逐层定位。关于环境版本源码默认是JDK 1.8、Tomcat 8.5、MySQL 5.7的组合。数据库驱动用的是mysql-connector-java 5.1.49JDBC URL不需要额外加useSSL和serverTimezone参数配置最省心。如果你的本机是MySQL 8.x驱动类名要改成com.mysql.cj.jdbc.DriverURL里再补上时区参数这个差异在避坑章节会专门展开。2.2 目录结构逐层解读从src到web的完整地图拿到源码先别急着运行花十分钟把目录结构看清楚。这套系统是标准的Eclipse Dynamic Web Project结构Java源码和Web资源分得清清楚楚src/com/library/ ├── bean/ # 实体类Book.java、User.java、BorrowRecord.java ├── dao/ # 数据访问层BookDao.java、UserDao.java、BorrowRecordDao.java ├── servlet/ # 控制层LoginServlet.java、BookServlet.java、BorrowServlet.java ├── filter/ # 过滤器EncodingFilter.java、LoginFilter.java └── util/ # 工具类DBUtil.java 以及 db.properties 配置文件 web/ ├── admin/ # 管理员功能页面book-list.jsp、book-add.jsp、borrow-list.jsp ├── user/ # 用户功能页面book-search.jsp、my-borrow.jsp ├── login.jsp # 项目入口登录页 ├── register.jsp # 用户注册页 └── WEB-INF/ ├── web.xml # Servlet与过滤器的注册中心 └── lib/ # 项目依赖JAR包mysql-connector等实体类对应数据库表字段与表结构一一映射。DAO层每个方法对应一个SQL操作方法命名也直白findAll、findById、findByKeyword、insert、update、delete。Servlet层每个类对应一组URL映射BookServlet通过action参数区分list、add、update、delete四种操作。Filter统一处理两件事一个是字符编码过滤器做完UTF-8编码转换防止中文乱码另一个是登录拦截过滤器未登录用户直接重定向到login.jsp。这个分层是JavaWeb项目里最主流的写法和Spring MVC的层次结构本质上是一致的——Controller对应ServletService对应DAOView对应JSP——只是把Spring的IOC容器和AOP代理拿掉了而已。理解这套源码之后再去看Spring MVC的项目会发现路由、参数绑定、视图解析这些概念都似曾相识。有一点要提醒如果导入IDE后发现web.xml里注册的Servlet类报红叉多半是Tomcat运行时环境没有配置好属于环境问题而不是代码问题第4章的避坑清单里会专门讲。2.3 数据库设计三张表怎么把业务串起来数据库核心是三张表建表脚本通常是sql文件夹下的library.sql。book表存图书字段有id、name、author、publisher、price、stock、category其中stock字段是库存数每次借阅扣一归还加一。user表存系统用户字段有id、username、password、rolerole字段用admin区分管理员、用normal区分普通用户。borrow表存借阅记录字段有id、user_id、book_id、borrow_time、return_time、statusstatus记录当前状态是已借出还是已归还。三张表的关系不复杂一条借阅记录关联一个用户和一本图书所以borrow表里只存user_id和book_id两个外键字段不做冗余。需要留意的设计细节是建表语句没有写物理外键约束关联关系完全靠应用程序的SQL逻辑保证。常见做法是写JOIN查询时手动关联两张表比如查询某用户当前借了哪些书SELECT b.name, b.author, b.publisher, br.borrow_time, br.status FROM borrow br JOIN book b ON br.book_id b.id JOIN user u ON br.user_id u.id WHERE u.username ?不设物理外键的原因很实际课程设计里经常会有删除图书的需求如果数据库层面绑定了外键约束删除时有借阅记录的图书会被数据库拒绝业务代码就得先把关联记录清理干净绕一大圈。用逻辑外键让代码里控制删除顺序就行实现简单很多。这也是不少生产系统的取舍方式灵活度优先数据库的完整性由应用层把控。2.4 一次请求的完整生命周期从点击按钮到页面渲染理解了这个系统的运行方式后面部署和改造都顺了。以用户搜索图书为例完整链路是这样的用户在book-search.jsp输入书名关键词并提交表单浏览器发起GET请求到BookServlet的映射地址。BookServlet的doGet方法里读取keyword参数调用BookDao的findByKeyword方法执行模糊查询返回一个List 集合。Servlet把集合setAttribute到request对象里然后forward到book-search-result.jspJSP页面用JSTL的forEach标签循环渲染每一条数据。这条链路里最值得注意的点是Servlet转发用的是RequestDispatcher的forward方法转发前后浏览器地址栏不变request里的数据在同一次请求内能从Servlet传递到JSP。如果换成了sendRedirect重定向request里的对象就全部丢失了因为那是两次互相独立的HTTP请求。这是JavaWeb里最容易翻车的地方很多同学发现重定向后页面取不到数据就是因为没分清forward和sendRedirect的语义。3. 核心模块拆解登录鉴权、图书管理和借还书的事务逻辑3.1 登录模块Session会话与Filter拦截的组合拳登录是系统的第一个入口也是Session和Filter两个知识点的集合体。先看LoginServlet的doPost核心代码protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user null) { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(user, user); if (admin.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /admin/book-list); } else { response.sendRedirect(request.getContextPath() /user/book-search); } }逻辑拆开看是四步从request里取参数、调DAO查库、判断结果、按角色分流。有两个细节值得反复看。第一登录失败用forward转发登录成功用sendRedirect重定向。成功场景下如果也用forward用户按F5刷新浏览器就会重复提交表单刷新一次就登录一次交互体验很差所以成功必须用重定向产生一次全新的GET请求。第二重定向路径里拼接了getContextPath()这是项目的部署根路径将来项目改名或者换端口路径依然正确这是写JavaWeb跳转的通用习惯。光有Session还不够。未登录用户可以直接在地址栏输入/user/book-search绕过登录Servlet所以必须加一层Filter拦截所有受保护URLpublic class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码里有个容易忽略的点request.getSession(false)传了false意思是当前请求没有关联Session就直接返回null而不是新建一个。如果写成不带参数的getSession()未登录用户每次访问受保护页面都会被创建一个空Session再被拦截跳转白白消耗服务器内存。这个细节在真实项目里是加分项也是面试常问的考点。Filter在web.xml里注册时配置url-pattern为/admin/和/user/后台和用户区的所有请求都受保护。3.2 图书管理模块一个Servlet用action参数分发四种操作图书管理是后台的核心功能。BookServlet没有为每个操作写一个Servlet类而是用一个Servlet加action参数区分操作类型这种写法在纯Servlet项目里非常常见protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { listBooks(request, response); } else if (add.equals(action)) { addBook(request, response); } else if (update.equals(action)) { updateBook(request, response); } else if (delete.equals(action)) { deleteBook(request, response); } }每个if分支对应一个私有方法私有方法内部再细化处理逻辑。以listBooks为例它的职责链条非常清晰private void listBooks(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { BookDao bookDao new BookDao(); ListBook books bookDao.findAll(); request.setAttribute(books, books); request.getRequestDispatcher(/admin/book-list.jsp).forward(request, response); }第一步调DAO层查询全部图书第二步把结果放到request作用域第三步转发到JSP页面。页面拿到books集合后用JSTL循环渲染成表格每一行带编辑和删除两个操作链接删除链接把图书id拼接在URL上。这种实现模式简单直接业务扩展时只需要在action分发链路上加一个else if分支再加一个私有方法不用改动其他代码。SELECT * FROM book WHERE name LIKE ? OR author LIKE ?DAO里的findByKeyword就是模糊查询PreparedStatement的占位符接收的是一段拼好通配符的字符串比如%Java%。用LIKE做模糊搜索时有一个性能方面的常识如果数据量增长到百万级别前置通配符会让索引失效但图书管理系统的数据量通常停留在几百上千条的量级这种写法完全够用不构成性能瓶颈。3.3 借阅归还模块库存扣减与事务一致性借阅模块是整套系统里业务逻辑最重的部分涉及三步操作查询图书库存、插入借阅记录、扣减库存。这三步必须保证原子性——如果插入借阅记录成功但库存没扣成功或者反过来库存扣了但记录没写入数据就乱套了。所以核心代码里用了手动事务控制public boolean borrowBook(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String checkSql SELECT stock FROM book WHERE id ?; PreparedStatement ps conn.prepareStatement(checkSql); ps.setInt(1, bookId); ResultSet rs ps.executeQuery(); if (rs.next() rs.getInt(stock) 0) { return false; } String insertSql INSERT INTO borrow (user_id, book_id, borrow_time, status) VALUES (?, ?, NOW(), borrowed); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); String updateSql UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; PreparedStatement ps3 conn.prepareStatement(updateSql); ps3.setInt(1, bookId); ps3.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段事务代码把JDBC事务的四个关键动作都覆盖了setAutoCommit(false)关闭自动提交、三条SQL按顺序执行、commit在所有步骤全部成功后才调用、任何一步抛异常立刻执行rollback回滚。这里有一个设计细节值得注意第三步的UPDATE语句里带上了AND stock 0条件相当于把库存扣减的校验做进了SQL层面而不是只靠前面的SELECT判断这在多人同时借同一本书时能避免库存扣成负数。归还流程是反向操作把borrow表里对应记录的status改为returned、return_time更新为当前时间同时把book表里对应的库存加回一。因为涉及两张表的更新同样要包在事务里处理。这套代码里归还方法的逻辑和借阅类似只是SQL语句的方向相反这里就不重复贴了。4. 部署运行全流程与避坑清单从导入项目到正常访问的每一步4.1 环境准备与项目导入把源码跑起来之前先把环境统一。建议的组合是JDK 1.8不要直接用JDK 17老代码的javax.servlet包和较新JDK之间会有兼容性问题、Tomcat 8.5、MySQL 5.7、Eclipse IDE for Enterprise Java and Web Developers。IntelliJ IDEA也可以但社区版不带Web开发支持配置Tomcat需要装插件对新手不友好。导入步骤按这个顺序来。第一步Eclipse里选择File Import Existing Projects into Workspace定位到源码根目录。第二步确认Tomcat运行时已经配好Windows Preferences Server Runtime Environments添加Tomcat 8.5并指向安装目录。第三步在项目Properties的Targeted Runtimes里勾选Tomcat 8.5——这一步不做的话web.xml里注册的Servlet类会集体报红叉。第四步把WEB-INF/lib下的JAR全部加入Build Path。第五步启动MySQL执行sql目录下的建表脚本。这里有个新手最常见的失误导入项目后如果src目录没被识别为源码根目录整个项目的Java文件都会出现找不到类的错误。需要在项目上右键 Build Path Configure Build Path把src添加为Source Folder再顺手把lib下的JAR加进来。4.2 数据库初始化与配置文件修改MySQL里执行完sql脚本后会生成一个db_library数据库里面有三张表和初始数据。管理员账号密码通常在sql脚本或者项目文档里有说明默认一般是admin/admin123。如果找不到可以直接在user表里查一下初始数据里都会写清楚。接着打开src目录下的db.properties文件这是整个系统的配置中心drivercom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/db_library usernameroot password123456三个参数各有各的坑。driver是5.x驱动就写com.mysql.jdbc.Driver如果是8.x驱动就要改成com.mysql.cj.jdbc.Driver写错直接ClassNotFoundException。url里的localhost是数据库地址3306是MySQL默认端口db_library是数据库名要和建库时的命名一致。password改成你本机MySQL的实际密码——如果这里对不上报错信息必然是Access denied for user。DBUtil工具类每次请求都会从这个properties文件读取连接信息所以修改后建议重启一次Tomcat让配置生效。4.3 避坑清单五个常见问题与解决办法第一个坑访问页面报404。现象是部署完成后浏览器访问http://localhost:8080/项目名/login.jsp返回404。原因是URL路径不对或者项目没有成功部署。解决方法是先到Eclipse的Servers视图看项目是否处于Deployed状态再右键项目 Properties Web Project Settings看Context Root是什么URL里的项目名要和它完全一致。第二个坑页面中文全部是问号。现象是JSP页面渲染出来的中文全是乱码。原因是编码不一致需要检查三个位置JSP文件头部的pageEncoding是否UTF-8、数据库连接的URL参数里有没有带characterEncodingUTF-8、EncodingFilter是否注册且位于过滤器链最前面。这套源码自带编码过滤器如果还乱码重点检查filter在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-mapping第三个坑ClassNotFoundException: com.mysql.jdbc.Driver。现象是启动后第一次访问数据库相关页面就抛驱动类找不到。原因是mysql-connector的JAR不在运行时classpath里。Eclipse里光把JAR加进Build Path还不够还要在Build Path Order and Export里勾选导出选项否则部署到Tomcat时JAR不会跟着项目一起进入WEB-INF/lib。这个坑非常隐蔽很多人JAR加了还是会报ClassNotFound就是因为Export没有勾上。第四个坑数据库连接超时或者Access denied。现象是页面上报Connections could not be acquired或者用户密码错误。最常见原因是MySQL服务没启动、db.properties里的用户名密码不对、或者url里的数据库名不存在。排查步骤固定先确认MySQL服务在运行再用命令行客户端登录MySQL验证用户名密码最后执行SHOW DATABASES确认db_library库存在。第五个坑Tomcat端口被占用。现象是启动日志报Address already in use: JVM_Bind。原因是8080端口被其他程序占了。解决方法是打开conf/server.xml把HTTP端口、AJP端口和重定向端口三处都改掉或者干脆关掉占用端口的程序。改完端口之后浏览器访问URL里的端口号也要同步修改。5. 二次开发实战给图书管理加上封面图片上传功能5.1 改造思路与方案选型给图书增加封面图上传是一个能完整串联起数据库、Servlet、JSP三层改造的案例。需求拆成四步第一步数据库加字段第二步实体类和DAO方法同步扩展第三步新增一个处理文件上传的Servlet第四步在图书新增和编辑页面加文件选择框列表页加图片展示列。文件上传的技术方案有两代Servlet 3.0以前要用cos或者commons-fileupload第三方库Servlet 3.0之后原生支持Part接口不需要额外引依赖。当前Tomcat 8.5版本完全支持后者所以就用原生方案代码干净且不增加JAR包。5.2 代码实现从数据库字段到前端页面数据库加字段ALTER TABLE book ADD COLUMN cover VARCHAR(255) DEFAULT NULL;实体类Book加一个cover属性生成getter和setter。BookDao的insert方法要同步扩展String sql INSERT INTO book (name, author, publisher, price, stock, category, cover) VALUES (?, ?, ?, ?, ?, ?, ?); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, book.getName()); ps.setString(2, book.getAuthor()); // ... 其他字段 ps.setString(7, book.getCover());新增一个UploadServlet处理文件上传用WebServlet注解配置映射地址WebServlet(/upload) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part part request.getPart(coverFile); String fileName Paths.get(part.getSubmittedFileName()).getFileName().toString(); String realPath getServletContext().getRealPath(/uploads); File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } part.write(realPath File.separator fileName); request.setAttribute(coverPath, uploads/ fileName); request.getRequestDispatcher(/admin/book-add.jsp).forward(request, response); } }代码逻辑不复杂但有两个易踩的坑。第一个是上传目录不要放在WEB-INF下顺便说一句WEB-INF目录里的文件浏览器无法直接通过URL访问图片放进去页面就显示不出来所以这里用getRealPath(/uploads)把它建在Web根目录下。第二个是getSubmittedFileName()方法在Tomcat 7.0.50之后才可用如果你用的是更老的Tomcat版本会返回null需要自己从Content-Disposition请求头里解析文件名。前端部分form表单要加enctypemultipart/form-data属性然后在表单里放一个typefile的input。注意一个容易踩的细节加了multipart/form-data之后容器会对请求做特殊解析代码里的request.getParameter(bookName)这种取值方式可能会拿不到值需要改用request.getPart或者把图书的其他字段一并放进同一个表单里处理。5.3 验证方法与后续优化本地验证分三步先在新增图书页面选择一张图片提交确认Tomcat的webapps目录下项目文件夹里多了一个uploads目录且文件存在再查数据库对应记录的cover字段有值最后打开图书列表页确认图片能正常渲染。我习惯在上传成功后立刻打开浏览器开发者工具的Network面板确认上传请求返回的是200而不是500这个习惯能帮你省很多排查时间。从那以后我改这个项目的每个模块都会强制走一遍「先改数据库、再改实体类和DAO、再改Servlet、最后改页面」的顺序前端的表单字段和后端的数据映射对不上号是这类项目最常见的翻车现场顺序对了能提前暴露八成的问题。希望这份拆解能帮到你。本文还有配套的精品资源点击获取