资讯详情 Java小型图书管理系统完整实现:Servlet+JSP+JDBC+MySQL实战教程
📅 2026/10/5 12:10:16
简介《Java小型图书管理系统》源码包是一套面向Java初、中级学习者的完整实战项目覆盖图书入库、出库、借阅、归还等典型业务场景既适合课程设计也可作为毕业设计的参考案例。压缩包内共72个文件包括44个已编译的class文件、22个java源文件、1个MySQL 8.0驱动jar包以及Eclipse工程配置文件.project、.classpath、prefs和两张界面示例图片整体大小仅2.29MB目录结构清晰方便逐层研读。源码完整保留了业务逻辑与界面代码读者可以对照学习Java基础语法、面向对象设计、MVC分层思想、JDBC数据库访问、Swing图形界面构建以及异常处理等核心技能还能借助配套驱动直接连接数据库进行运行调试。目前已有403人学习使用适合希望从零上手Java应用开发、快速理解小型图书管理系统设计思路的开发者。1. 这套“小系统”到底在做什么一个能独立交付的Java Web闭环每次有人拿着“java小型图书管理系统含完整代码及工具”来问我我基本能猜到对方的状态要么在做毕业设计要么刚学完Java基础想找个能跑通的项目练手要么就是培训机构作业交不上来求救。这个系统说小但五脏俱全——读者管理、图书录入、借还书、逾期判断全是关系型数据库最常见的动作做一遍等于把Servlet、JSP、JDBC、MySQL的老底都过了一遍。它适合想快速看到一个“能登录、能查询、能增删改”的完整闭环的人也适合想拿来做Java面试题储备的求职者。下面这套方案我按单人开发、短时间交付的要求来拆先定技术路线再落数据库和业务代码最后讲清楚用什么工具把它跑起来。2. 技术选型与项目骨架为什么“越老”的技术越好交付2.1 技术栈怎么选Servlet/JSP仍是轻量首选给课设或练手项目选技术栈我先讲一个反直觉的结论不要一上来就Spring Boot。Spring Boot MyBatis确实是企业主流但它把Servlet容器、依赖注入、自动配置全包了对Java新手来说像个黑匣子出了问题不知道看哪个日志。小型图书管理系统的核心诉求是“在最短时间内看到请求→数据库→页面”这条完整链路ServletJSPJDBC反而是最能讲清楚Java Web原理的组合面试时被问到“Servlet生命周期”“过滤器怎么用”也更容易接住。我推荐的组合是JDK 8或JDK 11、Tomcat 9、MySQL 5.7或8.0、Maven 3.6以上。如果你电脑上已装了IDEA终极版直接用IDEA的Tomcat集成功能即可这样连单独下载解压Tomcat的步骤都省了。有人问要不要换成PHP图书管理系统或Python版不是它们做不了而是标题既然定位Java就最好全程用Java实现借还书的库存扣减逻辑在各语言里思路完全一样但排查问题和面试表述的语境是Java的。关于版本有个注意点JDK版本较新时Tomcat和编译插件版本也要跟着升级否则报“不支持发行版本”的错误。JDK 1.8配Tomcat 9很稳JDK 11以上建议用Tomcat 10或最新版。别在环境上浪费太多时间环境变量配置只需要把JAVA_HOME、MAVEN_HOME指对即可装完用命令行敲一句版本号验证。免费入门资源现在就很多遇到不认识的概念直接查Java基础部分的资料就能补上。2.2 项目目录结构三层分包怎么拆才能既见全貌又好扩展创建一个Maven工程后我建议严格按“表现层-业务层-数据层”拆包。这样拆不是装样子而是为了让你第二天回来改代码时不用翻整个项目才知道某段逻辑在哪。完整结构如下book-manager/ ├── pom.xml └── src/main/ ├── java/com/example/book/ │ ├── servlet/ # Servlet控制器接收请求 │ ├── service/ # 业务逻辑接口与实现 │ ├── dao/ # 数据访问层JDBC操作 │ ├── model/ # 实体类 │ └── util/ # DBUtil、字符串工具 ├── resources/ │ └── db.properties # 数据库连接配置 └── webapp/ ├── WEB-INF/ │ └── web.xml └── jsp/ # 页面文件servlet层只做参数接收和页面跳转service层放借书还书这类业务判断dao层只写单表增删改查。这个划分有一个直接好处将来如果想把Serlvet换掉或加一个REST接口页面和业务都不需要大改。model目录放Book、Reader、Borrow三个实体类属性名尽量和数据库字段对应JSP取数据时会省很多事。2.3 Maven依赖与工具清单最小可运行的pom.xml如果从零建项目pom.xml建议这样写。我特意只保留最必要的依赖因为依赖拉多了一旦网络不好构建半天卡在下载上那种翻车体验足够劝退一个新手。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdbook-manager/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies /projectservlet-api的scope必须是provided因为Tomcat自带Servlet实现如果打包进去会和容器冲突。mysql-connector-java选择8.x版本对应的JDBC驱动类是com.mysql.cj.jdbc.Driver这和旧版5.x的类名不一样照抄旧博客容易踩坑。JSTL用来在JSP里写循环和条件少了它在页面上就只能写一堆Java代码维护起来很痛苦。配套工具我一般会准备这几样IDEA或Eclipse写代码Maven做构建Navicat或MySQL Workbench当数据库工具Git做版本备份。你没看错工具是整个交付流程里最容易被人忽略的“后悔药”。代码写坏了大不了回退数据库字段删错了没有备份才会让人崩溃。3. 从建表到核心业务图书、读者、借阅三张表的完整落地3.1 建表语句主外键和索引一次设对图书管理系统最关键的不是代码是表结构。我见过不少人把借阅记录还书后直接删除过两天要查历史就抓瞎也有人把图书库存写在另一个表里查列表要关联三张表。下面这个建表脚本是跑过很多次、也帮人改过很多次课设后的稳定版本你直接执行即可。CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_manager; CREATE TABLE t_reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) UNIQUE NOT NULL, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20) ); CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), isbn VARCHAR(20) UNIQUE, category VARCHAR(30), total_count INT DEFAULT 0, avail_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, due_date DATETIME, return_date DATETIME, status TINYINT DEFAULT 0 COMMENT 0未还 1已还, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES t_book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES t_reader(id) ); CREATE INDEX idx_borrow_status ON t_borrow(status);数据库使用utf8mb4而不是utf8这是第一个关键点。utf8在MySQL里最多存3字节遇到表情符号或少数生僻字符会直接报错而utf8mb4完全兼容建库时顺手写上字符集能避免后面页面中文全是问号的尴尬。t_reader里的reader_no是学号或工号这类唯一标识用unique保证不会录入重复。t_book中的total_count和avail_count分别代表总量和可借量还书时要保证avail_count不超过total_count。t_borrow是借阅关联表book_id和reader_id都是外键。status字段用TINYINT而不是字符串好处是查询快、存储省0表示未还1表示已还。我还加了state索引因为“查所有未还记录”是这个系统最频繁的查询场景。注意due_date要在这里留字段不然后面想加“逾期未还”功能还得改表改表可比建表麻烦得多。3.2 JDBC连接类写一个能抗住高并发的DBUtil很多人建好表就开始写各种DAO第一件事却是把同样的连接代码复制到每个方法里。这样写跑是能跑但代码极其啰嗦。更关键的是如果不用同一个Connection做事务借书时扣减库存和插入借阅记录就会各自开连接一个成功一个失败数据就完全对不上了。所以我先做一个DBUtil统一管理连接。package com.example.book.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream input DBUtil.class .getClassLoader() .getResourceAsStream(db.properties)) { Properties properties new Properties(); properties.load(input); Class.forName(com.mysql.cj.jdbc.Driver); url properties.getProperty(jdbc.url); user properties.getProperty(jdbc.user); password properties.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }对应的db.properties放在resources目录jdbc.urljdbc:mysql://localhost:3306/book_manager?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.userroot jdbc.password你的密码代码里的Class.forName显得古老但在原生JDBC里必须写它负责把驱动注册到DriverManager。url参数里的serverTimezoneAsia/Shanghai是mysql-connector 8.x必须加的不加它启动时会报时区错误useSSLfalse是避免没有配置SSL证书时控制台刷一堆警告。这样封装之后DAO层只需要一行DBUtil.getConnection()就能拿到连接代码清爽得多。3.3 BookDao的增删改查与模糊查询参数化SQL是底线表建好、连接类写好接下来是数据访问层。图书管理的核心是BookDao要支持按书名模糊搜索、新增图书、修改库存、删除图书。这一层最大的坑是把用户输入直接拼进SQL也就是SQL注入。这种写法无论是出于安全问题还是面向对象编程的基本素养都不该出现在交付代码里。package com.example.book.dao; import com.example.book.model.Book; import com.example.book.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { public ListBook searchBooks(String keyword) { String sql SELECT id, book_name, author, isbn, category, total_count, avail_count FROM t_book WHERE book_name LIKE ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setIsbn(rs.getString(isbn)); book.setCategory(rs.getString(category)); book.setTotalCount(rs.getInt(total_count)); book.setAvailCount(rs.getInt(avail_count)); list.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public int addBook(Book book) { String sql INSERT INTO t_book(book_name, author, isbn, category, total_count, avail_count) VALUES(?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getIsbn()); ps.setString(4, book.getCategory()); ps.setInt(5, book.getTotalCount()); ps.setInt(6, book.getTotalCount()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } public Book findById(int id) { String sql SELECT * FROM t_book WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setIsbn(rs.getString(isbn)); book.setCategory(rs.getString(category)); book.setTotalCount(rs.getInt(total_count)); book.setAvailCount(rs.getInt(avail_count)); return book; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }为什么坚持用PreparedStatement而不拼字符串因为prepareStatement会先把SQL结构发送给数据库再用参数填充占位符用户的输入永远只是“值”而不会变成“SQL命令”。比如书名输入一个单引号参数化的方式会把它转义成普通字符拼接方式却可能让整个查询报错或者带来安全隐患。addBook方法里新书的avail_count直接等于total_count这是常识但容易漏。findById供后面借还书逻辑复用。你在写的时候还要补一个updateStock方法负责在借书时减少avail_count、还书时增加avail_count别把这个逻辑放到页面层否则一个页面写成一个逻辑、换一个页面写法就变了项目会越改越乱。4. 页面与服务端打通Servlet请求流转与JSP回显的两个硬骨头4.1 用Servlet接请求从doGet到doPost的职责划分数据库层做完系统还只是半成品必须让浏览器能访问到这些方法。Servlet在Java Web项目里扮演调度员的角色用户在页面点击“查询”请求就落到某个ServletServlet调Service或Dao拿到结果后通过forward把数据带去JSP并渲染成HTML。这里最常见的做法是一个业务一个Servlet而不是把所有逻辑塞进一个总入口好查也好改。以图书列表查询为例package com.example.book.servlet; import com.example.book.dao.BookDao; import com.example.book.model.Book; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/book/list) public class BookListServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 如果用了过滤器这里的编码设置可以去掉 String keyword req.getParameter(keyword); if (keyword null) { keyword ; } ListBook list bookDao.searchBooks(keyword); req.setAttribute(bookList, list); req.getRequestDispatcher(/jsp/bookList.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }doGet负责从地址栏访问、刷新和跳转链接doPost负责表单提交。这里让doPost调用doGet是为了减少重复代码也符合“查询请求不应修改数据”的原则。encode的setCharacterEncoding写在Servlet里能解决多半中文乱码但如果你用了过滤器就不要再在每个Servlet里重复设置否则等于没写。req.setAttribute把查询结果放进请求作用域forward到JSP后JSP可以直接用EL表达式取出bookList。4.2 JSP页面回显数据用JSTL避免Java代码满天飞JSP页面容易写丑主要是因为在HTML里插了一堆% %代码块。正确姿势是配合JSTL标签和EL表达式。下面这段是图书列表页的核心循环片段代码短但作用直观。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title图书列表/title /head body form action${pageContext.request.contextPath}/book/list methodget input typetext namekeyword value${param.keyword} placeholder输入书名关键字/ button typesubmit查询/button /form table border1 tr th编号/th th书名/th th作者/th thISBN/th th可借数/th th操作/th /tr c:forEach items${bookList} varbook tr td${book.id}/td td${book.bookName}/td td${book.author}/td td${book.isbn}/td td${book.availCount}/td td a href${pageContext.request.contextPath}/book/borrow?bookId${book.id}借阅/a a href${pageContext.request.contextPath}/book/return?bookId${book.id}归还/a /td /tr /c:forEach /table /body /htmlc:forEach是JSTL的循环标签items指向Servlet传过来的Listvar是循环里的临时名。${book.bookName}看起来像直接访问字段实际上调用的Book类的getBookName()方法所以实体类必须写getter/setter这是初学最容易漏的一步——页面不报错也不显示值排查半天才发现是实体类没生成getter。form里的action用${pageContext.request.contextPath}拼接这是我在这个项目里反复强调的一个细节。如果部署后项目的访问路径带上下文名写死绝对路径就会404加上它以后无论系统部署在Tomcat的哪个目录下都能自动适配。表格最后一列的借阅和归还链接就是把当前图书的id传给对应Servlet的入口。4.3 借书还书的事务控制一个最小且不会写坏数据的写法借书这个动作要注意它不是单条SQL的事情得先检查avail_count大于0再把它减一同时插入一条借阅记录。这中间只要任何一步失败库存和记录就对不上比如库存减了但没生成借阅记录书就凭空丢了。所以必须用数据库事务把三步包在一起我习惯直接放到Servlet层演示连接的控制因为这样最直观。package com.example.book.servlet; import com.example.book.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; WebServlet(/book/borrow) public class BorrowBookServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String bookIdStr req.getParameter(bookId); String readerNoStr 20230001; // 从登录用户中获取 req.setCharacterEncoding(UTF-8); if (bookIdStr null) { resp.sendRedirect(req.getContextPath() /book/list); return; } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); int bookId Integer.parseInt(bookIdStr); // 1. 查询当前可借数量 String checkSql SELECT avail_count FROM t_book WHERE id ? FOR UPDATE; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs checkPs.executeQuery(); if (!rs.next() || rs.getInt(1) 0) { conn.rollback(); resp.sendRedirect(req.getContextPath() /book/list?msg库存不足); return; } // 2. 扣减库存 String updateSql UPDATE t_book SET avail_count avail_count - 1 WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); updatePs.executeUpdate(); // 3. 插入借阅记录 String insertSql INSERT INTO t_borrow(book_id, reader_id, due_date, status) SELECT ?, id, DATE_ADD(NOW(), INTERVAL 30 DAY), 0 FROM t_reader WHERE reader_no ?; PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, bookId); insertPs.setString(2, readerNoStr); insertPs.executeUpdate(); conn.commit(); } catch (SQLException e) { try { if (conn ! null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } throw new ServletException(借书失败, e); } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } resp.sendRedirect(req.getContextPath() /book/list?msg借阅成功); } }说一下这段代码为什么这么写。第一步查询用了“SELECT ... FOR UPDATE”它是SQL里的行级锁把这条图书记录的库存锁住防止两个人同时借同一本书时都看到库存为1然后一起扣减成负数。这一步对不支持行级锁的MyISAM引擎不生效所以建表时我特意用的默认InnoDB引擎。第二步和第三步用executeUpdate做修改和插入最后统一commit。如果中间抛异常catch块里rollback整个借书动作回到起点。还书逻辑正好相反把borrow记录update成已还再把avail_count加回去。这里要注意归还操作里必须用同一个Connection来做两步否则等于没用事务。事务边界定在Servlet层确实不够“企业级”但对这种小系统来说逻辑清晰、能照着改比引入Spring的Transactional反而更容易理解。5. 避坑指南中文乱码、404、驱动缺失与数据库时区怎么查5.1 页面中文全部变成问号现象浏览器展示的书名、作者全是“???”数据库里看到的是正常的文字前端也不报错。原因分两层数据库层面建库时没指定utf8mb4或者JSP页面没有声明UTF-8编码。两种情况的表现相似但排查起来完全两条路。如果数据库里直接存的就是问号说明写入时的连接参数或表结构字符集有问题如果数据库里正常、页面显示乱码说明响应编码没设置对。解决先确认建表脚本里DEFAULT CHARACTER SET utf8mb4有没有执行再用db.properties里的url加上characterEncodingutf8参数。JSP页面顶部写% page contentTypetext/html;charsetUTF-8 %Servlet里写req.setCharacterEncoding(UTF-8)。一层层加每加一层刷新一次页面不要一次改一堆地方否则你根本不知道是哪一步救回来的。5.2 Tomcat启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver现象启动项目点开页面控制台一片红核心报错是找不到驱动类。原因mysql-connector-java这个jar包没有进到项目的运行类路径里。常见的情况有几种依赖写进pom但没刷新Maven、war包没打进去、或者直接把jar下载到桌面上以为就行了。在IDEA里表现为运行时不报错、部署到独立Tomcat就报错。解决先确认pom.xml里有依赖然后执行一次mvn clean package到target目录下解压war包看WEB-INF/lib里有没有mysql-connector开头的文件。没有的话就检查依赖scope是不是写成了provided默认是compile才对。还有一个很隐蔽的坑如果你在IDEA里添加了外部libMaven构建时不会自动打包要统一把依赖交给Maven管不要两套机制混着用。5.3 页面404但控制台没有任何异常现象打开浏览器访问http://localhost:8080/book/list出现404页面Tomcat日志里没有任何报错。原因这往往不是代码问题而是部署路径和访问路径对不上。如果你直接把项目压成war放进webapps访问地址还要加项目名比如book-manager/book/list。还有种情况是web.xml里没有配置context-path或者IDE部署时用了war exploded方式访问上下文名会多一层。解决先看IDEA运行窗口顶部显示的“Context path”是什么访问路径一定是“上下文名 注解里的URL”。要验证到底部署了什么直接进Tomcat安装目录下的webapps看看有没有对应的war或解压目录。响应快且稳的做法是把上下文名设置成空字符串“/”这样访问地址最短本地测试最省心。5.4 连接MySQL时报The server time zone value时区错误现象第一次运行DBUtil时抛SQLException提示timezone相关的错误。原因MySQL驱动8.x对时区更严格数据库连接url没指定serverTimezone时驱动不知道用哪个时区。很多博客里的连接串还是老写法没这个参数。解决在jdbc.url后加上?serverTimezoneAsia/ShanghaiuseSSLfalse。如果你在云服务器上部署数据库时区可能不是北京时间用最稳妥的Asia/Shanghai指定即可。这个报错其实是个善意提醒它告诉我查问题先看连接串别一上来就怀疑驱动版本。5.5 借书成功但库存没扣或借阅记录没生成现象页面提示借阅成功但book表里avail_count没变化或者t_borrow里没有新记录。原因借书方法的三个SQL不是在一个事务里执行的。如果每个DAO方法内部都独立获取连接、独立提交那么扣库存和插记录互不理解彼此的状态。我见过有人把update库存放在一个方法、insert换另一个方法表面看代码挺正常实际执行时因为连接不相同事务彻底失效。解决把所有需要一起成功或一起失败的SQL放到同一个Connection里执行参考上面BorrowBookServlet的写法用setAutoCommit(false)开事务、commit收尾、catch里rollback。记住这句话事务的范围就是Connection的范围。你拿到哪个连接事务就管到哪个连接的数据库操作。6. 交付前再检查一遍三处验证与一套能复用的启动顺序这个系统做完以后不要急着关IDE就以为万事大吉。我每次都会用一套固定的验证顺序跑通了才算真的交付。第一步是命令行重建InnoDB数据库用MySQL自带工具sql脚本初始化。许多“在我电脑上好好的换台机器就坏”的怪问题罪魁祸首就是只在IDE里导入过数据却忘记了项目根本依赖的是初始化脚本。把下面这段记下来每次交付前敲一遍mysql -uroot -p sql/init.sql mvn clean package第一行通过MySQL命令行工具把建库建表脚本重新执行一次这能直接暴露SQL语句里的字符集、外键顺序问题第二行用Maven完整构建确保依赖能拉全、代码能一次性编译。构建结果会生成一个war包把它丢到Tomcat的webapps目录启动后再用浏览器从头走一遍流程。接着验证整个业务链路新增一本书、修改一本书、借书、还书、模糊查询、查看未还记录。重点看两个数据是否正确对应借书后t_book的avail_count少一还书后加回来t_borrow的状态字段在还书后变成1。如果这些基本动作都正常系统才算真正可交付。顺手还要看一眼Tomcat的catalina.out日志有没有隐藏的异常堆栈被Tomcat自己吃掉这个习惯能救你很多次。最后一个技巧是“备份比什么都重要”给项目建一个Git仓库每完成一个功能就commit一次。这不仅是合理使用工具更是给自己留后悔药。我见过太多同学改坏一行代码后想回退却找不到原版急出一身汗。把数据库init.sql也提交进去这样就算整台电脑重装系统把仓库克隆下来重新跑一遍命令系统几分钟就能复活。这个项目我已经带人做过很多轮每次都觉得Java Web的入门姿势就应该像这样不依赖复杂框架只靠Servlet、JSP、JDBC和MySQL把每一步都看穿。自己亲手敲一遍建表、连接、增删改查和事务控制后面再去看Spring Boot的自动配置和MyBatis的Mapper会从容得多。希望这套流程能帮你少走几次弯路顺顺利利把它跑起来。本文还有配套的精品资源点击获取