简介:基于Servlet+JSP的图书管理系统是一套完整的Java Web课程设计/毕业设计参考项目,面向正在学习Java服务端开发的学生与开发者,演示图书信息增删改查、借还管理及前端交互等典型业务场景。包内共419个文件,约14.91MB,涵盖Java源码、JSP页面、Servlet类、CSS/JS/jQuery前端脚本、SQL脚本、XML配置、JAR依赖库及PDF说明文档,结构清晰便于对照学习。已有2216人浏览学习,项目集成IntelliJ IDEA与Tomcat环境,readME中提供数据库初始化与部署指引。通过阅读源码可重点掌握Servlet请求处理流程、JSP+EL+JSTL页面渲染、jQuery实现Ajax异步刷新等核心知识点,同时包内还包含运行截图与数据库建表语句,适合作为课程设计参考或入门Java Web开发的练手实例。
1. 项目整体设计与技术选型思路
1.1 为什么在 2025 年还选 Servlet + JSP 做图书管理系统
先别急着说“这技术栈太老了”。图书管理系统这个题目,在高校课程设计和毕业设计里出现的频率,高到几乎可以称之为“Java Web 入门必修课”。我见过太多人一上来就选 Spring Boot + Vue + MySQL 全家桶,结果评审老师一问Servlet生命周期答不上来,一问Session底层原理直接卡壳——这其实就失去了做课设最重要的意义。
Servlet + JSP 这套组合,本质上是 Java Web 最底层的“裸奔”方案。它没有 Spring MVC 帮你封装请求路由,没有 MyBatis 帮你屏蔽 JDBC 细节,更没有前端框架帮你自动渲染数据。你写的每一个doGet、doPost、每一条PreparedStatement,都是在直面 HTTP 协议和数据库交互的本质。把这套东西吃透了,后面再学 Spring Boot 那些“高级封装”,你会发现自己是在看懂了电路图之后再去用电器,而不是只会按开关。
那为什么偏偏是图书管理系统?因为这个业务场景足够经典:用户登录注册、图书信息的增删改查、借阅还书流程、分类管理,这几个模块恰好覆盖了 Java Web 开发几乎所有的核心知识点:
- 请求响应模型:前端页面提交表单,后端接收参数并响应
- 状态管理:登录状态用 Session 保存,记住用户信息用 Cookie
- JDBC 操作:连接数据库、执行 SQL、处理结果集
- MVC 思想:JSP 做视图,Servlet 做控制器,JavaBean / DAO 做模型
换句话说,你做完这一个系统,相当于把 Java Web 的基础能力整体过了一遍。这也是为什么即使 Spring Boot 已经火到不行,高校课设题库里依然给 Servlet + JSP 留着一席之地的原因。
1.2 技术栈选型与配套工具的搭配建议
既然确定了 Servlet + JSP 作为核心技术栈,那周边工具和环境的搭配就有讲究了。我结合自己带过项目的经验,给出一套比较稳的推荐配置:
| 组件 | 推荐选型 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 别追求太新的版本,Tomcat 兼容性和课设验收环境是个变量 |
| IDE | Eclipse 或 IDEA | IDEA 社区版够用,但传统课设环境 Eclipse 更“原汁原味” |
| 服务器 | Tomcat 8.5 / 9.0 | 经典配置,网上资料最多,出问题好排查 |
| 数据库 | MySQL 5.7 / 8.0 | 5.7 更稳,8.0 需要注意驱动包差异 |
| 前端 | JSP + JSTL + EL 表达式 | 传统搭配,JSTL 能省掉大量 Java 脚本片段 |
| JDBC 驱动 | mysql-connector-java 5.1.49 / 8.0.x | 版本必须和数据库版本匹配,这是最大的坑之一 |
这里面有个细节我要单独拎出来说:数据库驱动版本不匹配是新手碰到最多的拦路虎。如果你用 MySQL 8.0,驱动类名是com.mysql.cj.jdbc.Driver,URL 还需要带serverTimezone=Asia/Shanghai这样的时区参数;如果你用 MySQL 5.7,驱动类名是com.mysql.jdbc.Driver,URL 不需要时区参数也能跑。这两套配置放在一起就够新手折腾一晚上的。
1.3 功能模块划分:边界清晰,评分点全覆盖
图书管理系统的功能模块,往小了做就是“登录 + 一张表的增删改查”,往大了做可以扯到“多角色权限 + 借阅逾期自动扣费 + 数据可视化大屏”。作为课设或毕设,我的建议是做到“够用且有余量”——基础功能必须完整,再加 1-2 个亮点功能做加分项。
我常用的模块划分方式是:
- 用户模块:注册、登录、注销、密码修改。注意区分管理员和普通用户角色
- 图书管理模块:图书信息的添加、删除、修改、查询。查询要支持模糊搜索,这是最基本的
- 分类管理模块:图书分类的增删改查,一本图书归属一个分类
- 借阅模块:借书、还书、借阅记录查询、借阅状态管理
- 统计模块(加分项):图书总数、借出数量、热门图书排行
这套划分方式的好处是:每个模块的职责清晰,代码量适中,评审老师问起业务逻辑来你能对答如流。更重要的是,模块之间的边界清晰意味着你写代码的时候不容易“拧成一团浆糊”。
2. 核心细节解析与实操要点
2.1 Servlet 的三种注册方式:强烈建议掌握 web.xml 写法
关于 Servlet 的注册方式,现在网上主流推荐的是@WebServlet注解,确实简洁方便。但我要说的是:如果你做的是课设,建议掌握传统 web.xml 的写法。原因很简单——很多学校的上机验收环境版本偏老,而且《Java Web》教材里讲 Servlet 部分时基本都是以 web.xml 为主,你答辩的时候要能讲清楚两种方式的区别。
这里给出一个基于 web.xml 方式编写 Servlet 的完整示例,这是最能体现功底的写法:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>图书管理系统</display-name> <!-- 配置 Servlet --> <servlet> <servlet-name>BookServlet</servlet-name> <servlet-class>com.library.servlet.BookServlet</servlet-class> <!-- 可选:启动时加载,数字越小越优先 --> <load-on-startup>1</load-on-startup> </servlet> <!-- 配置 Servlet 映射 --> <servlet-mapping> <servlet-name>BookServlet</servlet-name> <url-pattern>/book</url-pattern> </servlet-mapping> <!-- 配置欢迎页 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 配置全局编码过滤器,解决中文乱码 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>com.library.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>这段配置里有两个点值得注意。第一是load-on-startup,它决定 Servlet 是“第一次被请求时才加载”还是“服务器启动时就加载”。对于核心 Servlet 来说,配置成启动加载可以避免首次访问时的延迟,也有利于提前暴露配置问题。第二是编码过滤器,这个我在后面专门讲中文乱码问题时会详细展开。
2.2 JSP 页面设计:EL 表达式 + JSTL 才是正确姿势
很多自学 JSP 的人最大的毛病,就是在 JSP 页面里写大量的<% ... %>Java 脚本片段。这种写法虽然也能跑,但页面会变得极其混乱,而且容易引起评审老师的反感——因为这恰恰是 JSP 技术最被诟病的点。
正确做法是用 EL 表达式(${})和 JSTL 标签库来渲染页面。比如展示图书列表的核心代码是这样:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>图书列表</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <h2>图书列表</h2> <div class="toolbar"> <form action="${pageContext.request.contextPath}/book" method="get"> <input type="hidden" name="action" value="search"> <input type="text" name="keyword" placeholder="输入书名或作者"> <button type="submit">搜索</button> </form> <a href="${pageContext.request.contextPath}/book?action=toAdd">新增图书</a> </div> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>编号</th> <th>书名</th> <th>作者</th> <th>分类</th> <th>价格</th> <th>库存</th> <th>操作</th> </tr> <c:forEach items="${bookList}" var="book" varStatus="status"> <tr> <td>${status.count}</td> <td>${book.title}</td> <td>${book.author}</td> <td>${book.categoryName}</td> <td>${book.price}</td> <td>${book.stock}</td> <td> <a href="${pageContext.request.contextPath}/book?action=toEdit&id=${book.id}">编辑</a> <a href="${pageContext.request.contextPath}/book?action=delete&id=${book.id}" onclick="return confirm('确定要删除该图书吗?')">删除</a> </td> </tr> </c:forEach> </table> <c:if test="${empty bookList}"> <p style="color: gray;">暂无数据</p> </c:if> </body> </html>${pageContext.request.contextPath}这个表达式建议背下来,它取的是当前 Web 应用上下文路径,这么写之后,不管项目部署到哪个路径下,链接都不会失效。这个细节在答辩时随口说出来,印象分会加不少。
2.3 Servlet 代码结构:用一个 Servlet 处理一类业务
Servlet 虽然可以直接在doGet或doPost里写逻辑,但一个类只处理一件事会让代码更清晰。我在图书管理系统的 Servlet 里习惯用action参数来区分不同操作:
@WebServlet("/book") public class BookServlet extends HttpServlet { private BookService bookService = new BookService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { listBooks(request, response); } else if ("toAdd".equals(action)) { request.getRequestDispatcher("/book_add.jsp").forward(request, response); } else if ("toEdit".equals(action)) { String id = request.getParameter("id"); Book book = bookService.getById(Integer.parseInt(id)); request.setAttribute("book", book); request.getRequestDispatcher("/book_edit.jsp").forward(request, response); } else if ("delete".equals(action)) { deleteBook(request, response); } else { listBooks(request, response); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("add".equals(action)) { addBook(request, response); } else if ("update".equals(action)) { updateBook(request, response); } } private void listBooks(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Book> bookList = bookService.getAllBooks(); request.setAttribute("bookList", bookList); request.getRequestDispatcher("/book_list.jsp").forward(request, response); } }这种action分发的好处是:一个/book地址就能处理所有图书相关操作,URL 简洁,代码结构清晰。你只需要在 Servlet 里定义好action的枚举值,前端页面通过这些参数来指定要执行的操作。不过要注意,用这种方式时一定要对action为空的情况做兜底处理,否则容易 NullPointerException。
2.4 数据库设计:五张表打天下
数据库设计是整个系统的地基。图书管理系统的表结构我一般推荐五张表起步:
-- 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `real_name` VARCHAR(50), `role` VARCHAR(20) DEFAULT 'USER', -- ADMIN 或 USER `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书分类表 CREATE TABLE `category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `description` VARCHAR(200) ); -- 图书表 CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `author` VARCHAR(100), `publisher` VARCHAR(100), `price` DECIMAL(10,2), `stock` INT DEFAULT 0, `category_id` INT, `publish_date` DATE, `isbn` VARCHAR(50), `cover_url` VARCHAR(255), FOREIGN KEY (`category_id`) REFERENCES `category`(`id`) ON DELETE SET NULL ); -- 借阅记录表 CREATE TABLE `borrow_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `book_id` INT NOT NULL, `borrow_date` DATETIME DEFAULT CURRENT_TIMESTAMP, `return_date` DATETIME, `status` VARCHAR(20) DEFAULT 'BORROWED', -- BORROWED / RETURNED / OVERDUE FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) );这里有个需要说明的外键策略:图书表外键关联分类表时用ON DELETE SET NULL,意思是分类删除后图书仍然保留,只是分类置空。这样设计的好处是避免“删一个分类把所有书都删没了”这种惨案。这个细节值得在答辩时主动提,说明你考虑到了数据完整性。
3. 实操过程与核心环节实现
3.1 项目搭建全流程:从零开始跑通第一个页面
完整的实操流程按这个顺序走,基本不会卡壳:
- 创建数据库:在 MySQL 中执行上述建表 SQL,并插入几条测试数据
- 新建动态 Web 项目:IDEA 里选 Java Enterprise,勾选 Web Application;Eclipse 里选 Dynamic Web Project
- 导入依赖 jar 包:
mysql-connector-java和jstl相关 jar 包放进WEB-INF/lib目录 - 编写实体类:User、Book、Category、BorrowRecord,对应数据库表结构
- 编写数据库工具类:封装获取连接和释放资源的代码
- 编写 DAO 层:每个实体对应一个 DAO,封装增删改查方法
- 编写 Service 层:业务逻辑,比如借书时要判断库存是否充足
- 编写 Servlet:接收请求、调用 Service、转发到 JSP
- 编写 JSP 页面:列表页、编辑页、登录页、注册页
- 配置 web.xml:配置 Servlet 映射、编码过滤器
- 部署到 Tomcat 测试
3.2 JDBC 工具类封装:最容易被忽略的核心基建
JDBC 代码写得不优雅,后面每个 DAO 都会重复大量样板代码。我封装工具类时习惯把连接获取和资源释放放在一起:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有个非常关键的细节:数据库 URL 里的characterEncoding=UTF-8要和 Servlet 里的request.setCharacterEncoding("UTF-8")配合使用。这两处只要有一处缺失,中文数据在存取过程中就会出现乱码。另外,URL 中的useSSL=false是为了避免 MySQL 8.0 在本地连接时弹出 SSL 警告,属于实践中的常规操作。
3.3 登录功能的完整实现:涉及 Session 与拦截器思想
登录是图书管理系统的门面功能,也是状态管理的核心考点。流程上,用户提交用户名密码后,Servlet 查询数据库验证,成功后把用户信息放入 Session,再重定向到主页面;失败则携带错误信息返回登录页。
关键代码片段如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("currentUser", user); response.sendRedirect(request.getContextPath() + "/book?action=list"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }提示:登录成功后应使用
sendRedirect重定向,而不是forward转发。原因是重定向会发起新请求,地址栏会变化,用户刷新页面不会重复提交表单;而直接转发的话,刷新页面会出现重复提交表单的弹窗。
至于“未登录用户不能访问系统页面”这种需求,最朴素的做法是在需要保护的 Servlet 方法开头加 Session 判断:
HttpSession session = request.getSession(); User currentUser = (User) session.getAttribute("currentUser"); if (currentUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; }当然,如果你愿意写 Filter 来实现统一的登录验证会更优雅,但课设阶段先在 Servlet 里做判断也完全够用,量力而行。
3.4 借书还书流程:业务状态机最关键
借书还书是这个系统里最有业务味道的功能。借书时,系统要检查三件事:图书是否存在、库存是否大于 0、用户是否已经借了这本书还没还。还书时,要把库存加回去,同时更新借阅记录的状态。
用状态机来理解就是:可借 —(借书)—> 已借出 —(还书)—> 已归还。这三个状态在数据库里就对应status字段的三种取值。实现时要注意一个细节:借书和还书时都需要同时更新图书表的stock字段和借阅记录表的status字段,这两个更新必须放在同一个事务里,否则会出现“书借出去了但库存没减”的不一致问题。
这里我给一个简单的事务控制模板:
public boolean borrowBook(int userId, int bookId) { Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 操作一:扣减库存 String sql1 = "UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0"; ps1 = conn.prepareStatement(sql1); ps1.setInt(1, bookId); int rows = ps1.executeUpdate(); if (rows == 0) { conn.rollback(); return false; // 库存不足或图书不存在 } // 操作二:插入借阅记录 String sql2 = "INSERT INTO borrow_record(user_id, book_id, status) VALUES(?, ?, 'BORROWED')"; ps2 = conn.prepareStatement(sql2); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps1, null); DBUtil.close(null, ps2, null); } }这个stock = stock - 1 WHERE id = ? AND stock > 0的写法,比“先查库存再更新”的方式好在哪?它把“判断”和“更新”合并成了一条原子性操作。即使多个请求同时借同一本书,数据库也能通过行锁保证不会出现超借的情况。这种写法体现了对并发场景的考虑,和那些先查询再更新的做法拉开了一个档次。
4. 常见问题与排查技巧实录
4.1 中文乱码问题:一次解决,再也不用怕
中文乱码是 Java Web 开发里出现频率最高的问题,没有之一。很多人的处理方式很粗暴——各种地方乱设编码,结果还是乱,最后干脆放弃。乱码的本质是数据的编码和解码方式不一致。在图书管理系统里,乱码可能出现在三个环节:
| 环节 | 原因 | 解决办法 |
|---|---|---|
| 请求参数乱码 | 浏览器提交数据时用 UTF-8,但 Servlet 默认用 ISO-8859-1 解码 | request.setCharacterEncoding("UTF-8"),必须在获取任何参数之前调用 |
| 响应页面乱码 | JSP 页面编码和服务器输出编码不一致 | JSP 文件头部加<%@ page contentType="text/html;charset=UTF-8" %> |
| 数据库存储乱码 | MySQL 连接 URL 未指定字符集 | JDBC URL 加characterEncoding=UTF-8 |
实用的检查手段是:先确定“乱的是哪一层”。我的排查顺序是:先在浏览器里右键查看源码,看页面本身是否乱;如果页面正常、数据乱,那就是数据库连接或数据库表字符集的问题;如果页面本身乱,那就是 JSP 文件编码或响应头的问题。逐层排查,不乱冲乱撞。
4.2 JSP 修改了不生效:八成是部署目录的问题
“JSP 改完刷新页面还是旧的”——这个问题踩过的人都懂。大部分情况下,是因为 IDE 没有自动把修改后的 JSP 文件同步到 Tomcat 的部署目录。IDEA 里表现为out/artifacts目录下的文件没更新,Eclipse 里则是wtpwebapps目录下的文件没更新。
最直接的解决办法是:重新部署项目。在 IDEA 里选中 Tomcat 配置,点“Redeploy”或直接重启 Tomcat。如果修改很频繁,可以考虑开启 Tomcat 的自动加载功能:在conf/context.xml里设置<Context reloadable="true">,这样 JSP 文件变化时会自动重编译加载。但要注意生产环境千万别开这个,性能和安全性都会受影响。
另外还有一种情况:项目里存在多个同名 JSP 文件,一个在src/main/webapp下,另一个在web目录下,IDE 部署的是其中某一个,你改的是另一个。这属于项目结构混乱导致的问题,建议一开始就统一好目录结构,别在 IDEA 里同时乱建多个 Web 根目录。
4.3 数据库连接失败:驱动版本和时区是高频原因
数据库连接失败,报错信息通常有:ClassNotFoundException、Communications link failure、Connection refused等。逐一来分析:
ClassNotFoundException是驱动 jar 包没导入或被漏掉了。检查WEB-INF/lib目录下有没有对应的mysql-connector-javajar 包,如果 IDEA 项目里没有将 jar 包设置成 “Add as Library”,运行时会找不到类。
Communications link failure通常是数据库没启动、端口被占用、或防火墙拦截。先用命令行mysql -u root -p确认数据库能本地连接,再看端口是不是 3306,最后检查 MySQL 服务是否在运行。
The server time zone value is unrecognized是 MySQL 8.0 特有的坑。解决办法是在 JDBC URL 里加serverTimezone=Asia/Shanghai。另外驱动类名也需要注意——MySQL 8.0 对应的驱动类是com.mysql.cj.jdbc.Driver,MySQL 5.x 是com.mysql.jdbc.Driver,写错也会报类找不到。
4.4 端口被占用:Tomcat 启动失败的标准处理姿势
启动 Tomcat 时报Port 8080 was already in use,意思就是 8080 端口被别的程序占了。最简单的处理方式有三种:
- 换端口:修改
conf/server.xml里的端口号,比如改成 8081 - 找到占用进程并结束它:Windows 下用
netstat -ano | findstr 8080查 PID,然后taskkill /F /PID 进程号 - 如果是你自己之前启动的 Tomcat 没关干净,去任务管理器里把 Java 进程结束掉
注意:改端口时要同时改 HTTP 端口和可能用到的 AJP 端口,只改一个会导致另一个端口依旧冲突。另外,改完端口后访问路径也要跟着变,比如原来是
http://localhost:8080/book,改成 8081 后就要访问http://localhost:8081/book。
4.5 登录状态失效:Session 超时和浏览器 Cookie 的关系
登录后过一段时间再操作用户被弹回登录页,这是因为 Session 默认超时时间是 30 分钟。Tomcat 的web.xml里可以配置全局 Session 超时时间:
<session-config> <session-timeout>60</session-timeout> </session-config>单位是分钟,这里配置成 60 分钟。还有一种情况是“换了个浏览器标签一上来就变成未登录”,这通常是因为 Tomcat 的 Session 依赖 Cookie,而浏览器设置禁用了 Cookie。课设验收时如果遇到这个问题,建议直接检查浏览器是否允许了该站点的 Cookie。
最后再分享一个容易被坑的小细节:如果我需要管理员和普通用户显示不同的菜单位,可以在 JSP 里结合 EL 表达式判断sessionScope.currentUser.role的值,不推荐在页面里写大段 Java 脚本,逻辑简单且页面友好才是王道。这套基于 Servlet + JSP 的图书管理系统做完,你会发现后续再去学习 Spring MVC 里DispatcherServlet的工作原理、@RequestMapping的路由映射,都会有一种“原来如此”的通透感——因为你已经亲手把最底层的那套流程走了一遍。
本文还有配套的精品资源,点击获取