
简介适合高校软件专业学生与 Java Web 初学者的图书管理系统毕业设计文档面向互联网应用开发场景重点解决学校图书管理中读者信息、图书入库与借还流程繁琐等问题。包内为 1 份 docx 设计文稿共 2.34MB内容完整呈现从需求分析、可行性评估到总体设计、数据库设计与系统实现的整个过程。系统采用 JSP、Struts 框架与 MVC 模式后端以 SQL Server 存储并通过 JDBC 连接涵盖系统设置、读者管理、图书管理、图书借还、系统查询、更改口令六大功能模块同时对图书信息表、借阅信息表等核心表结构作出详细设计。读者可借助该文档梳理论文写作脉络了解 Java Web 管理类项目的分层架构与编码思路也可作为课程设计或毕业设计的参考模板。目前已有 384 人学习下载适合需要快速搭建系统或对照撰写设计说明的读者。1. 这个“基于Java Web的图书管理系统”到底解决谁的什么问题前两天一个学弟发来消息说毕设题目是“基于Java Web的图书管理系统的设计与实现.docx”问我网上模板一抓一大把为什么自己照着敲还是跑不起来。我没直接回他先问了一句你们的图书管理系统是给谁用的管理员在后台录书、读者在前台搜书借书还书、超期了要能查出来——这三件事跑通了这个题目就立住了。Java Web这个限定词意味着你不玩Python的Flask也不碰PHP那套老老实实用Servlet、JSP、Tomcat、MySQL把一条完整链路走通。这篇文章就干一件事把这个链路里从建库到部署的所有关键环节拆开每一个步骤给到能抄的参数和代码顺便把那些让你一夜回到解放前的坑提前说清楚。写给三类人做毕设的、想交课程设计的、还有真要在内网搭个图书管理系统但不想上重型框架的开发者。2. 技术选型与骨架搭建为什么这个题目绕不开Java Web目录怎么分2.1 选型拆解纯ServletJSP还是上SSM框架先想清楚你的交付物是什么先把这个题目里最容易被忽视的约束摆出来标题写的是“基于Java Web”不是“基于Spring Boot”。这两个东西差别很大。Spring Boot本质上也是Java Web但你在答辩时如果讲不清楚“为什么不用Servlet非要上框架”反而容易挨问。“基于Java Web”最正统的解读是Java EE那一套体系即Servlet作为控制器、JSP作为视图层、JDBC或者MyBatis作为数据访问层。实际情况是很多学校的毕设题目是多年前定的当时Spring Boot还没像今天这么普及题目里的“Java Web”默认就指ServletJSPJDBC。我的建议很简单除非老师明确允许用Spring Boot否则就用ServletJSPJDBC。原因不光是合规更在于这套组合足够轻代码量可控逻辑都在明面上每一个请求怎么进、怎么出、数据库连接怎么拿、怎么关你都能讲清楚。这恰恰是答辩时老师最爱问的部分。SSM或者Spring Boot存在大量“约定优于配置”的封装遇到问题时排查起来反而更吃力。那题目里的“设计与实现”怎么体现你在论文里把分层讲清楚——实体层、DAO层、Service层、Servlet层、JSP页面层每一层职责单一、相邻层通过接口交互这就是设计。实现部分就是让借书还书这个核心流程能跑通。后半篇文章我会按这个分层逐一展开。2.2 项目目录结构一个能扛住答辩的Java Web工程长什么样很多人建工程是从网上扒一个压缩包下来解压之后目录乱成一团连哪个class对应哪个页面都要猜半天。这里不按Maven的标准骨架说因为很多毕设环境默认用Eclipse或IDEA里的Dynamic Web Project模板那我就按这个模板给你一个可行的目录结构你直接照着建。BookSystem/ ├── src/ │ ├── com/book/entity/ // 实体类Book、Reader、BorrowRecord、Admin │ ├── com/book/dao/ // 数据访问层BookDao、ReaderDao、BorrowDao、AdminDao │ ├── com/book/service/ // 业务层BookService、BorrowService、AdminService │ ├── com/book/servlet/ // 控制器层LoginServlet、BookServlet、BorrowServlet │ ├── com/book/util/ // 工具类DBUtil、DateUtil │ └── com/book/filter/ // 编码过滤器EncodingFilter ├── WebContent/ │ ├── admin/ // 管理员页面book_list.jsp、book_add.jsp、borrow_list.jsp │ ├── reader/ // 读者页面index.jsp、book_search.jsp、my_borrow.jsp │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ // mysql-connector、druid、jstl等jar包 │ ├── css/ │ ├── js/ │ ├── login.jsp │ └── index.jsp └── sql/ └── book_system.sql每层只做一件事entity里的类对应数据库表的字段dao只负责SQL和执行service里写业务规则servlet里只做参数接收、调用service、跳转页面JSP拿数据展示。这四层职责不乱串答辩时你讲架构图一句话的事。项目里的sql目录是很多人不建的我强烈建议你建把建库脚本放进去既是交付物的一部分也是你换电脑之后的最强后悔药。2.3 开发环境与版本搭配JDK、Tomcat、MySQL的版本配不对后面全白干环境搭建这一块出问题最多不在于软件难装而在于版本之间互相不认。这里是经过大量实践验证的版本组合照抄即可。JDK用1.8Tomcat用8.5或者9.0MySQL用5.7或者8.0连接器用对应版本。具体对应关系是Tomcat 8.5支持Servlet 3.1JDK 8完全够用如果MySQL是8.0那mysql-connector-java必须用8.0.x版本同时驱动类名要写成com.mysql.cj.jdbc.Driver并且连接URL里必须带时区参数这是老版本连接器和新版本MySQL之间最大的坑。数据库连接的配置我放在DBUtil里统一管理使用Druid连接池配置文件放src/druid.properties。注意如果你用的是8.0版本的MySQL驱动类、URL时区、连接器版本这三样必须一起改缺一个都会在启动时报错。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/book_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot passwordyour_password initialSize5 minIdle2 maxActive20 maxWait60000serverTimezoneAsia/Shanghai是MySQL 8.0的硬性要求不写就会报Server returns invalid timezone。initialSize5表示启动时初始化5个连接maxActive20是最大连接数。图书管理系统并发量不大20足够调大了反而浪费数据库连接资源。3. 数据库设计与DAO层图书管理系统的数据地基怎么打3.1 表结构设计四张核心表怎么建为什么我建议逻辑外键图书管理系统的数据模型不复杂核心就四张表管理员表、图书表、读者表、借阅记录表。管理员表不跟借阅记录发生关系它是独立的登录凭证。图书表和读者表是一对多关系——一本书可以被不同读者在不同时间借阅但同一时刻只能被一个人持有。借阅记录表是图书和读者之间的关联实体每次借书生成一条记录还书时更新这条记录的还书时间和状态。下面是建库建表SQL。注意我在借阅记录表里没有加物理外键约束注释里写了原因这个是后面避坑章节要展开的重点先按这个建。CREATE DATABASE book_system DEFAULT CHARACTER SET utf8mb4; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), isbn VARCHAR(30) UNIQUE, total_count INT DEFAULT 1, available_count INT DEFAULT 1, create_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(30) UNIQUE, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), max_borrow_count INT DEFAULT 5, current_borrow_count INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME, due_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0-借出中 1-已归还 2-已逾期, -- 有意不加外键约束原因见避坑清单 INDEX idx_reader (reader_id), INDEX idx_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;available_count这个字段是借书逻辑的关键。每次借书成功它减1还书成功它加1。判断一本书能不能借就看available_count是否大于0。这是用空间换简单的方式查询在役图书时直接WHERE available_count 0不用关联借阅记录表统计性能和写法都更友好。3.2 连接池参数Druid的核心配置项按这个调不吃亏Druid连接池在Java Web里的使用率很高。它比DBCP和C3P0的优势在于自带监控统计功能答辩的时候你可以多说一句“用了Druid可以看SQL执行统计”。具体配置已经在前面写过了这里补充三个关键参数maxWait60000是拿连接的超时时间单位毫秒如果60秒拿不到连接就抛异常避免线程无限等下去testWhileIdletrue是空闲时检测连接是否有效默认值就是true还有一个validationQuerySELECT 1需要加上否则空闲检测不知道以什么SQL为准。// DBUtil.java public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(Druid初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }DBUtil用静态代码块初始化类第一次被加载时就会执行保证连接池只建一次。后续所有DAO拿连接都走这个类不会出现“这里new一个连接、那里又new一个”的失控局面。注意一个细节Druid的DruidDataSourceFactory.createDataSource(props)要求driverClassName、url、username、password这几个key必须和配置文件里完全一致拼错任何一个keyDruid不会报错但连接池会静默失败等实际拿连接的时候才炸排查起来很恶心。3.3 DAO层实现用一个BookDao把JDBC的重复代码收干净很多新手写DAO每个方法里都是getConnection、PreparedStatement、ResultSet、close四件套一个类写完两三页哪哪都是复制粘贴。这里给一个简化版BookDao用PreparedStatement防SQL注入把关闭资源的动作放到finally块里。// BookDao.java public class BookDao { public ListBook findBooks(String keyword, int offset, int limit) { String sql SELECT * FROM book WHERE book_name LIKE ? OR author LIKE ? ORDER BY id DESC LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, offset); ps.setInt(4, limit); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setIsbn(rs.getString(isbn)); b.setTotalCount(rs.getInt(total_count)); b.setAvailableCount(rs.getInt(available_count)); list.add(b); } } } catch (SQLException e) { throw new RuntimeException(查询图书失败, e); } return list; } public int countBooks(String keyword) { String sql SELECT COUNT(*) FROM book WHERE book_name LIKE ? OR author LIKE ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { throw new RuntimeException(统计图书数量失败, e); } return 0; } }JDK 7之后的try-with-resources语法让代码看着舒服了很多Connection、PreparedStatement、ResultSet都实现了AutoCloseable能自动释放。三个参数offset和limit配合实现分页keyword用LIKE模糊匹配。这里有个性能习惯要养成查询只取需要的字段不要什么都SELECT *这个库表结构简单就算了以后做真实项目要把这个习惯带在身上。4. 从借书到还书业务层与页面层把主流程跑通4.1 Service层事务边界划在哪才不至于借了书却扣不了库存Service层是整个系统里最容易翻车的地方因为事务的坑几乎全都埋在这里。借书这个动作在业务上包含两件事往borrow_record表插一条记录同时把book.available_count减1。这两件事必须同时成功或者同时失败。如果先减库存再插记录插记录失败库存就莫名其妙少了先插记录再减库存减库存失败书就凭空多了一本已借出记录。用Connection.setAutoCommit(false)手动开启事务两条SQL都用同一个Connection执行最后统一commit或rollback这是ServletJDBC时代最正统的写法。// BorrowService.java public void borrowBook(int readerId, int bookId) { String insertRecord INSERT INTO borrow_record (reader_id, book_id, borrow_time, due_time, status) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0); String updateBook UPDATE book SET available_count available_count - 1 WHERE id ? AND available_count 0; String updateReader UPDATE reader SET current_borrow_count current_borrow_count 1 WHERE id ? AND current_borrow_count max_borrow_count; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(insertRecord); PreparedStatement ps2 conn.prepareStatement(updateBook); PreparedStatement ps3 conn.prepareStatement(updateReader)) { ps1.setInt(1, readerId); ps1.setInt(2, bookId); int recordRows ps1.executeUpdate(); ps2.setInt(1, bookId); int bookRows ps2.executeUpdate(); ps3.setInt(1, readerId); int readerRows ps3.executeUpdate(); if (recordRows 1 bookRows 1 readerRows 1) { conn.commit(); } else { conn.rollback(); throw new RuntimeException(借书失败库存不足或超出可借数量); } } catch (SQLException e) { conn.rollback(); throw new RuntimeException(借书异常已回滚, e); } } catch (SQLException e) { throw new RuntimeException(获取连接失败, e); } }三个executeUpdate的返回值分别是影响行数。UPDATE book SET available_count available_count - 1 WHERE id ? AND available_count 0这行SQL是灵魂库存为0时影响行数为0直接在数据库层面拦住了超借。同样的读者表加了current_borrow_count max_borrow_count条件防止超量借书。三个操作影响行数全是1才提交任何一个不是1就整体回滚。这里的事务边界就是“借书”这个业务动作的完整范围。4.2 控制器与页面登录、图书检索、借还书的前后端数据流怎么接Servlet层的作用是接收参数、调Service、把结果写到Request或Session里、然后转发或重定向到JSP。一个常见的坏味道是把大量业务代码堆在Servlet里这里用LoginServlet给你看标准写法。// LoginServlet.java WebServlet(/login) public class LoginServlet extends HttpServlet { private AdminService adminService new AdminService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); Admin admin adminService.login(username, password); if (admin ! null) { req.getSession().setAttribute(admin, admin); resp.sendRedirect(req.getContextPath() /admin/book_list.jsp); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }WebServlet(/login)是Servlet 3.0的注解方式省掉在web.xml里写一长串servlet和servlet-mapping。注意/login前面不能带项目名req.getContextPath()会自动拼上避免硬编码。登录失败用forward到login.jsp这样Request里的errorMsg能在页面上显示登录成功用sendRedirect这样刷新页面不会重复提交表单。借书和还书的Servlet结构完全一样一个WebServlet(/borrow)一个WebServlet(/returnBook)前面Service层的方法准备好Servlet里只做参数解析、调用和跳转。4.3 跑通主流程后自测哪些环节最容易像黑匣子一样让人抓瞎这套东西跑通之后我建议你先自己走五条路径而不是直接拿给同学测试第一正常登录正确密码和错误密码各一次第二正常搜书关键词有结果和没结果各一次第三借一本库存不为0的书确认库存减1、借阅记录多一条第四借一本库存为0的书确认它借不出去且页面有提示第五还书确认库存加1、记录状态变成已归还。这五条全过主流程就是稳的。第3和第4条很容易出现只走了表面、没验证数据库的情况。页面提示“借书成功”数据库里库存没变这种就是典型黑匣子翻车。每次测试完直接去看一眼book表的available_count和borrow_record表的最新记录别只瞄页面。5. 避坑清单图书管理系统开发与部署的5个常见翻车点5.1 中文乱码乱成一锅粥前端传进来是好的数据库里就变问号现象JSP页面显示正常但往数据库里插入中文书名后查出来全是??。原因三层编码不一致导致的。页面是UTF-8Servlet接收参数时没有设置request.setCharacterEncoding(UTF-8)Tomcat默认按ISO-8859-1解码数据库那边表虽然建成了utf8mb4但连接URL里缺了characterEncodingutf8。解决三层全改成UTF-8。第一层JSP页面头部写pageEncodingUTF-8表单用POST提交第二层写一个EncodingFilter在doFilter里对每个请求和响应统一设置编码// EncodingFilter.java public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }第三层连接URL里带上useUnicodetruecharacterEncodingutf8。这个过滤器在web.xml里配置映射到/*。这里要特别强调这个Filter一定要注册否则你前面两个地方都改了请求参数还是乱码因为Servlet容器默认按ISO-8859-1处理POST参数这个玄学问题几乎每个做图书管理系统的人都遇到过。5.2 404和500交替轰炸注解写了却进不去页面问题藏在路径细节里现象点击“图书管理”菜单浏览器地址栏变成http://localhost:8080/admin/book_list.jsp页面报404或者访问Servlet直接500。原因404分两种情况一种是路径写错了比如JSP放在WebContent/admin/下访问时写成了admin/manager/book_list.jsp另一种是Servlet映射冲突比如两个Servlet都映射到/bookTomcat启动时不会报错但访问时它不知道该找谁。500通常是Servlet里抛了空指针或者ClassNotFoundException最常见的是jar包没放进WEB-INF/lib。解决404先确认路径和实际文件位置一一对应多一层少一层都会翻车500去Tomcat的logs/localhost.日期.log里看堆栈不要只看浏览器里的错误页。这里给个血泪经验WEB-INF下面的JSP不能通过浏览器地址栏直接访问只能通过Servlet内部forward到达。你要是把页面放到了WEB-INF/pages/下面所有写jsp/book_list.jsp的地方都会404这不是路径错是Servlet容器的安全机制。5.3 启动连接池报错MySQL 8的时区和驱动两个坑连着踩现象Tomcat一启动报Cannot create PoolableConnectionFactory堆栈里有Public Key Retrieval is not allowed或者Server returns invalid timezone。原因MySQL 8.0的驱动和连接方式跟5.7不一样。Public Key Retrieval is not allowed是8.0默认使用caching_sha2_password认证需要客户端在第一次连接时获取公钥时区报错则是连接串里没有指定serverTimezone。解决连接URL里加两个参数serverTimezoneAsia/Shanghai和allowPublicKeyRetrievaltrue。这两个是MySQL 8.0的专属需求5.7不需要如果你用的是8.0而且不想加allowPublicKeyRetrievaltrue可以在MySQL端把用户的认证插件改成mysql_native_password但最省事的是在URL里加参数。还有个容易被忽略的8.0的驱动类名是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver少了中间那个cj就会报ClassNotFoundException。5.4 事务回滚不生效三条SQL都执行了报错后数据还是写进去了现象借书时第2条SQL故意写错按理应该回滚但查数据库发现第1条已经插进去了。原因三条PreparedStatement虽然写在一个方法里如果分别从DBUtil.getConnection()拿了三次连接那就是三个独立连接commit和rollback根本不在同一个Connection上怎么可能一起回滚。解决一个事务内所有SQL必须用同一个Connection对象。前面BorrowService的写法已经示范过了在方法开头拿一次连接setAutoCommit(false)所有preparedStatement都从这个conn创建末尾统一commit或rollback。这里要说透一点连接池里拿到的连接是复用的后一个用户拿到的是前一个用户还回去的那条连接。如果你前一个用户没commit就关了连接连接池回收时会把未提交的事务回滚掉但你已经关了连接再想回滚就晚了。所以事务操作里conn.close()必须放在commit或rollback之后。5.5 外键与级联删一本被借出的书整个借阅记录直接失效现象管理员在后台把一本状态为“借出中”的书删了成功提示。但读者端还能看到借阅记录点进去图书信息全部为空借阅记录变成了孤儿数据。原因如果建表时加了FOREIGN KEY (book_id) REFERENCES book(id)以及ON DELETE CASCADE删书会把关联的借阅记录连坐删除读者端那张借阅历史直接少一行数据凭空消失。解决:在设计阶段就选择不加物理外键用逻辑外键——只建普通索引不加REFERENCES约束然后在Service层手动判断删书之前先查borrow_record里有没有status0且book_id目标书的记录有就拒绝删除提示“该图书尚有借出记录请先处理”。已经把物理外键加上去了的同学再加一个判断就好public boolean canDeleteBook(int bookId) { String sql SELECT COUNT(*) FROM borrow_record WHERE book_id ? AND status 0; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bookId); try (ResultSet rs ps.executeQuery()) { return !(rs.next() rs.getInt(1) 0); } } catch (SQLException e) { throw new RuntimeException(检查借阅记录失败, e); } }这样做的好处是业务规则由自己控制删除保护逻辑在代码里可见不会像外键级联那样“静默处理”——数据库替你做了但你可能根本不知道发生了什么。这也是为什么我在建表SQL里特意不加物理外键。面试或者答辩时如果被问你就说“线上系统对数据删除操作要更谨慎物理外键的级联策略不可控所以在应用层实现引用完整性约束”这比“老师教的原样照抄”要好得多。6. 进阶从“能跑”到“扛用”给图书检索加一层Redis缓存这个系统做到前面那一步已经是一个完整可交付的项目了。但如果你想让它在性能或架构意识上看着更“高级”一点有个性价比很高的改进给图书列表的查询加一层Redis缓存。图书管理系统的数据量不大加缓存的实际收益很有限这点我不骗你。但答辩时能讲清楚缓存怎么回事、缓存和数据库的一致性怎么处理比单纯多一个功能更能体现你的设计能力。我一般会这样做把热门搜索的关键词对应的图书列表缓存进Rediskey是book:list:{keyword}:{page}value是JSON序列化后的列表过期时间60秒。查图书的时候先查Redis命中就直接返回没命中再查数据库并把结果写回Redis。注意缓存的是“列表查询”借书还书这种写操作不能只更新Redis、不同步数据库正确做法是写操作走数据库然后删除对应关键词的缓存。删除比更新简单下次查询会自然把新数据加载回Redis这样逻辑上不出错。public ListBook searchWithCache(String keyword, int page, int limit) { String cacheKey book:list: keyword : page; String cached jedis.get(cacheKey); if (cached ! null) { return JSON.parseArray(cached, Book.class); } ListBook result bookDao.findBooks(keyword, (page - 1) * limit, limit); if (!result.isEmpty()) { jedis.setex(cacheKey, 60, JSON.toJSONString(result)); } return result; }这里jedis是Jedis客户端60秒过期时间setex是带过期时间的写入。加了这个之后系统在“高频率搜索相同关键词”的场景下数据库压力会明显下降。如果你是内网使用、几十个读者这个优化可有可无但写进论文里的意义在于你展示了“知道系统瓶颈在哪、有什么手段解决、代价是什么”的工程判断力。我自己的习惯是每次写完一个功能会先跑一遍操作路径再谈下一步优化先把路径覆盖全了再装饰功能。如果这一步走急了后面排查问题时会付出几倍时间。这个教训是在一次线上事故里用加班换回来的写在这里希望你少走这一步弯路。希望帮到你。本文还有配套的精品资源点击获取