
1. 这不是“过时技术”的怀旧项目而是Java Web开发的底层逻辑训练场你在网上搜“JavaJSPMySQLTomcat 图书管理系统”大概率会看到两类内容一类是高校课程设计作业的源码打包下载另一类是“这技术早该淘汰了”的冷嘲热讽。但真实情况是——我去年带三个应届生做企业级SaaS后台时第一个月让他们手写一个不含任何框架的JSPServlet图书管理Demo不是为了复古而是为了让他们亲手摸清HTTP请求从浏览器发出到数据库返回结果再渲染成HTML页面的完整链路。这个组合看似“老”实则是Java Web世界的“解剖标本”JSP是视图层最直白的表达Servlet是控制器最原始的形态MySQL是数据持久化最基础的接口Tomcat是Servlet容器最标准的实现。它不封装、不抽象、不自动装配所有环节都暴露在你眼皮底下。当你能用这套组合跑通增删改查、用户登录、借阅记录查询你就真正理解了Spring MVC里RequestMapping背后发生了什么明白了MyBatis的#{}和${}区别从何而来也清楚了为什么现代前端框架要拼命解决“服务端渲染 vs 客户端渲染”的权衡问题。这不是教你怎么用现成轮子而是教你亲手锻造轮子的每一根辐条。尤其对刚从Java语法过渡到Web开发的同学跳过这个阶段直接上Spring Boot就像学开车先上高速——表面快但遇到刹车失灵、转向卡滞时连故障点在哪都找不到。2. 四件套的协同关系不是简单拼接而是责任边界清晰的流水线很多人把JavaJSPMySQLTomcat当成四个独立工具堆在一起这是项目失败的第一步。它们实际构成一条严丝合缝的请求处理流水线每个组件只干一件事且必须明确自己的输入输出边界。我画过不下二十张流程图给新人讲解最终浓缩成一张表这张表决定了你代码的健壮性组件核心职责输入来源输出去向关键约束浏览器客户端发起HTTP请求接收HTML响应用户点击、表单提交Tomcat的HTTP端口默认8080只认HTML/CSS/JS不理解Java代码Tomcat接收HTTP请求解析URL调用对应Servlet/JSP返回HTTP响应浏览器的TCP连接Servlet的service()方法 或 JSP编译后的Servlet实例必须部署war包或webapps目录JSP文件需放在WEB-INF外Servlet处理业务逻辑操作数据库决定跳转路径Tomcat传递的HttpServletRequest/Response对象请求转发forward到JSP 或 重定向redirect到新URL不直接生成HTML只做决策和数据搬运JSP将Servlet传来的数据request.setAttribute动态嵌入HTML模板Servlet通过request.setAttribute()传递的数据Tomcat组装成HTTP响应体返回浏览器本质是Servlet的语法糖最终被Tomcat编译成.java文件再编译成.classMySQL持久化存储数据执行SQL查询Servlet通过JDBC驱动发送的SQL语句查询结果集ResultSet或更新行数表结构设计直接影响Servlet代码复杂度连接池配置不当会导致Tomcat线程阻塞举个具体例子用户点击“借书”按钮。浏览器发送POST请求到/borrow?bookId1001userId2001Tomcat根据web.xml或注解找到BorrowServlet将请求和响应对象传入其doPost()方法BorrowServlet从request.getParameter()获取参数用JDBC连接MySQL执行INSERT INTO borrow_records (book_id, user_id, borrow_date) VALUES (?, ?, ?)若插入成功BorrowServlet调用request.setAttribute(msg, 借书成功)然后request.getRequestDispatcher(/success.jsp).forward(request, response)Tomcat将success.jsp编译为success_jsp.java存于work/Catalina/localhost/yourapp/org/apache/jsp/再编译成success_jsp.class最后执行该类的_jspService()方法把% request.getAttribute(msg) %替换成“借书成功”拼成完整HTMLTomcat将HTML通过HTTP响应返回浏览器。这个过程里任何一环出错都会导致整个链条断裂比如JSP里写了% out.print(request.getAttribute(msg)); %但Servlet忘了setAttribute页面就显示null或者MySQL连接字符串写错端口Servlet抛出SQLExceptionTomcat日志里全是红色堆栈。所以调试时永远按这个顺序排查浏览器Network看请求发没发、状态码多少 → Tomcat日志看有没有404或500 → Servlet代码看逻辑分支和异常捕获 → JSP看EL表达式语法和scope范围 → MySQL看表是否存在、字段类型是否匹配。这不是玄学是流水线作业的必然逻辑。3. JSP的致命陷阱别把它当HTML写它本质是Java代码生成器几乎所有初学者的第一个大坑就是把JSP当成纯HTML编辑器。我在IntelliJ IDEA里见过最典型的错误一个book_list.jsp里混着table标签和% while(rs.next()) { %但rs变量根本没在% %块外声明。这种写法看似能跑实则埋下三颗雷作用域混乱、资源泄漏、线程安全危机。JSP的% %脚本片段会被Tomcat原样复制到生成的Servlet的_jspService()方法里而_jspService()是多线程并发调用的。想象一下100个用户同时访问图书列表Tomcat创建100个线程执行同一个book_list_jsp.class如果% Connection conn DriverManager.getConnection(...); %写在脚本片段里就会创建100个数据库连接瞬间耗尽连接池。更隐蔽的是ResultSet rs如果没在% %里显式关闭GC可能来不及回收导致游标泄露。正确的做法是严格遵循MVC分层ServletController负责所有Java逻辑建立数据库连接、执行查询、封装结果为List 、调用request.setAttribute(bookList, bookList)JSPView只做两件事用JSTL标签库遍历数据、用EL表达式输出内容。例如% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${bookList} varbook tr td${book.id}/td td${book.title}/td td${book.author}/td tda hrefborrow.jsp?bookId${book.id}借阅/a/td /tr /c:forEach /table这里${bookList}来自Servlet的setAttributec:forEach是JSTL的标准迭代标签book.id是Book对象的getter方法。JSP里绝不出现Java代码只出现EL表达式和JSTL标签。这不仅是代码整洁问题更是强制你把业务逻辑和展示逻辑彻底分离。我曾让一个实习生把所有% %删掉改用JSTL重写他花了三天才搞定但从此再也没写过内存泄漏的JSP。另外JSP的page指令至关重要% page contentTypetext/html;charsetUTF-8 languagejava %,contentType决定浏览器如何解析响应languagejava明确告诉Tomcat这是Java环境。漏掉charsetUTF-8中文图书名在页面上就是乱码漏掉languagejava某些老版本Tomcat会报错。这些细节不是可选项是流水线上的螺丝钉。4. MySQL与Tomcat的隐性战争连接池不是可选配置而是生存必需品新手常犯的另一个致命错误是在Servlet里每次请求都新建MySQL连接Connection conn DriverManager.getConnection(jdbc:mysql://localhost:3306/bookdb, root, 123456);。这看起来简单直接但实际运行时你会遭遇Tomcat的“静默崩溃”——页面加载越来越慢最终超时。原因在于MySQL默认最大连接数是151而Tomcat默认最大线程数是200。当并发用户超过151新请求就会卡在getConnection()上无限等待Tomcat线程池被占满后续所有请求排队系统假死。这不是代码bug而是资源规划失误。解决方案只有一个必须使用连接池Connection Pool。Tomcat自带的tomcat-jdbc.jar是最稳妥的选择它比DBCP更轻量比HikariCP更兼容老版本。配置在context.xml里位于conf/context.xml或META-INF/context.xmlResource namejdbc/BookDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookdb?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai usernameroot password123456 maxActive50 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue/关键参数解读maxActive50连接池最多允许50个活跃连接远低于MySQL的151上限留出余量给其他应用minIdle5池中始终保持5个空闲连接避免高并发时频繁创建销毁连接的开销maxWait10000当连接池耗尽时请求最多等待10秒超时抛出异常而不是无限阻塞validationQuerySELECT 1testOnBorrowtrue每次从池中借连接前先执行SELECT 1检测连接是否有效防止MySQL因超时自动断开导致的Communications link failure。在Servlet里获取连接的方式也变了不再用DriverManager而是通过JNDI查找Context initCtx new InitialContext(); Context envCtx (Context) initCtx.lookup(java:comp/env); DataSource ds (DataSource) envCtx.lookup(jdbc/BookDB); Connection conn ds.getConnection(); // 从池中获取非新建 // ... 执行SQL ... conn.close(); // 归还连接到池非真正关闭注意conn.close()在这里不是关闭物理连接而是将连接对象归还给连接池。如果忘记写这行连接永远不归还池很快耗尽。我见过最惨的案例一个借阅功能忘了close()上线三天后连接池满所有用户无法借书运维查了六小时才发现是代码里少了一行。所以养成习惯所有getConnection()后面必须有对应的close()且必须放在finally块里。现代IDEA甚至能帮你自动生成try-with-resources但老项目里还是得手动写牢。5. Tomcat的部署黑盒war包不是扔进去就完事目录结构决定生死很多同学把项目文件夹直接拖进webapps目录发现访问http://localhost:8080/yourproject/404第一反应是“Tomcat没启动”。其实更可能是目录结构错了。Tomcat部署Web应用有严格约定它不是文件管理器而是按规范解析war包或目录。一个合法的Web应用目录必须长这样yourproject/ ├── WEB-INF/ │ ├── web.xml ← 部署描述符定义Servlet映射、过滤器等Servlet 3.0可省略 │ ├── classes/ ← 编译后的.class文件如BookServlet.class │ └── lib/ ← 依赖jar包mysql-connector-java.jar、jstl.jar等 ├── index.jsp ← 入口页面放在WEB-INF外 ├── book_list.jsp └── css/ js/ images/ ← 静态资源必须在WEB-INF外核心规则所有Java类文件.class和配置文件web.xml必须在WEB-INF目录下所有用户可直接访问的资源JSP、HTML、CSS、JS、图片必须在WEB-INF外。如果把index.jsp误放进WEB-INFTomcat会拒绝提供因为WEB-INF是受保护目录禁止外部直接访问这是安全机制。同样web.xml如果放在根目录Tomcat根本读不到Servlet映射失效。部署方式有两种目录部署把整理好的yourproject文件夹直接复制到tomcat/webapps/启动Tomcat后它会自动扫描并部署war包部署用jar -cvf yourproject.war .在yourproject目录下打包然后把war包丢进webapps/Tomcat会自动解压并部署。无论哪种部署后务必检查tomcat/work/Catalina/localhost/yourproject/目录——这里存放JSP编译后的Java源码和class文件。如果yourproject目录下没有work子目录说明部署失败。常见失败原因web.xml里servlet-class写的类名和实际classes/下的包路径不一致比如写BookServlet但实际是com.book.servlet.BookServletlib/里缺mysql-connector-java.jar导致Servlet抛ClassNotFoundExceptionindex.jsp里引用link hrefcss/style.css但css/文件夹不在根目录而是放在了WEB-INF/css/浏览器自然404。我建议新手用目录部署因为能看到实时文件结构上线用war包因为便于版本管理和回滚。另外Tomcat的server.xml里Connector port8080 /可以改端口但别轻易动Engine和Host节点除非你真懂集群配置。一个血泪教训某次我把Host appBasewebapps unpackWARstrue autoDeploytrue里的autoDeployfalse结果改完代码重启Tomcat新版本死活不生效查了两小时才发现是自动部署关了必须手动删webapps/yourproject再复制新目录。6. 从零搭建的七步实操避开90%新手会踩的坑现在把所有理论落地手把手带你搭一个最小可行的图书管理系统。这不是复制粘贴教程每一步都标注了“为什么这么干”和“不这么干会怎样”。我用的是Tomcat 9.0、MySQL 8.0、JDK 11所有步骤在Windows和macOS通用。6.1 第一步初始化MySQL数据库与表结构别急着写Java先确保数据层可靠。打开MySQL命令行CREATE DATABASE bookdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookdb; CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, isbn VARCHAR(20), stock INT DEFAULT 0 ); INSERT INTO books (title, author, isbn, stock) VALUES (深入理解Java虚拟机, 周志明, 9787121367622, 10), (Head First Java, Kathy Sierra, 9787508339322, 5);关键点CHARACTER SET utf8mb4支持emoji和生僻字COLLATE utf8mb4_unicode_ci保证中文排序正确。如果用utf8MySQL里的别名实际是utf8mb3某些四字节UTF-8字符会存成问号。stock设DEFAULT 0避免NULL值在Java里引发NPE。6.2 第二步配置Tomcat连接池编辑tomcat/conf/context.xml在Context标签内添加之前说的Resource配置。切记url里的serverTimezoneAsia/Shanghai必须加上否则MySQL 8.0会报The server time zone value XXX is unrecognized。这是时区不匹配的经典错误网上搜到的90%解决方案都是加这行。6.3 第三步创建项目骨架与web.xml在tomcat/webapps/下新建booksys文件夹按前述结构创建WEB-INF/web.xml?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 servlet servlet-nameBookListServlet/servlet-name servlet-classcom.book.servlet.BookListServlet/servlet-class /servlet servlet-mapping servlet-nameBookListServlet/servlet-name url-pattern/booklist/url-pattern /servlet-mapping /web-app为什么用web.xmlServlet 3.0支持注解WebServlet但新手容易写错路径web.xml显式声明更直观且Tomcat 9完全兼容。6.4 第四步编写BookListServletController在booksys/WEB-INF/classes/com/book/servlet/下创建BookListServlet.javapackage com.book.servlet; import javax.naming.Context; import javax.naming.InitialContext; 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 javax.sql.DataSource; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.List; WebServlet(/booklist) public class BookListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListBook bookList new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { Context initCtx new InitialContext(); Context envCtx (Context) initCtx.lookup(java:comp/env); DataSource ds (DataSource) envCtx.lookup(jdbc/BookDB); conn ds.getConnection(); // 从连接池获取 String sql SELECT id, title, author, isbn, stock FROM books; ps conn.prepareStatement(sql); rs ps.executeQuery(); while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setTitle(rs.getString(title)); book.setAuthor(rs.getString(author)); book.setIsbn(rs.getString(isbn)); book.setStock(rs.getInt(stock)); bookList.add(book); } request.setAttribute(bookList, bookList); request.getRequestDispatcher(/book_list.jsp).forward(request, response); } catch (Exception e) { e.printStackTrace(); request.setAttribute(error, 查询图书失败 e.getMessage()); request.getRequestDispatcher(/error.jsp).forward(request, response); } finally { // 必须关闭顺序rs - ps - conn 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(); } // 归还连接 } } }避坑重点Book类必须有无参构造器和getter/setter否则JSP的EL表达式${book.title}无法反射调用finally块里conn.close()是归还不是销毁e.printStackTrace()只用于开发生产环境要记录到日志文件。6.5 第五步编写Book实体类与book_list.jspViewbooksys/WEB-INF/classes/com/book/entity/Book.javapackage com.book.entity; public class Book { private int id; private String title; private String author; private String isbn; private int stock; // getter/setter省略IDEA可自动生成 }booksys/book_list.jsp注意在WEB-INF外% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title图书列表/title /head body h1图书馆藏书/h1 c:if test${not empty error} p stylecolor:red${error}/p /c:if table border1 trthID/thth书名/thth作者/ththISBN/thth库存/thth操作/th/tr c:forEach items${bookList} varbook tr td${book.id}/td td${book.title}/td td${book.author}/td td${book.isbn}/td td${book.stock}/td tda hrefborrow.jsp?bookId${book.id}借阅/a/td /tr /c:forEach /table a hrefadd_book.jsp添加新书/a /body /html关键验证访问http://localhost:8080/booksys/booklist如果看到表格里有两条数据说明Servlet→MySQL→JSP全链路打通。如果空白打开浏览器开发者工具的Console看是否有404静态资源路径错或500后端异常。6.6 第六步引入JSTL与MySQL驱动下载jstl-1.2.jar和mysql-connector-java-8.0.33.jar放入booksys/WEB-INF/lib/。为什么必须放lib下Tomcat的类加载器优先从WEB-INF/lib找jar如果放系统lib目录所有应用共享版本冲突风险高。jstl.jar提供c:forEach等标签没有它JSP会报Cannot find the tag library descriptor。6.7 第七步启动与验证启动Tomcat双击bin/startup.batWindows或./startup.shmacOS观察控制台输出直到出现Server startup in [xxx] milliseconds访问http://localhost:8080/booksys/booklist如果页面显示图书列表恭喜你的第一套Java Web流水线已运转。此时你可以尝试修改book_list.jsp里h1文字刷新页面立即生效JSP热加载修改BookListServlet.java重新编译javac -cp tomcat/lib/servlet-api.jar;WEB-INF/lib/* -d WEB-INF/classes/ WEB-INF/classes/com/book/servlet/BookListServlet.java再刷新看到新逻辑。这就是原始技术的魅力——改动即可见无需重启整个应用。7. 真实项目中的演进路径从JSP到现代架构的平滑迁移有人问“学这个有什么用现在谁还写JSP”我的回答是它不是终点而是理解起点的坐标系。在我参与的三个企业项目中JSP系统都经历了这样的生命周期第一阶段0-1年用JSPServlet快速交付MVP验证业务模型。比如图书管理系统的借阅流程两周内上线运营部门立刻能用第二阶段1-2年在原有JSP页面里嵌入jQuery AJAX实现局部刷新避免整页跳转。例如借阅按钮点击后用$.post(/borrow, {bookId:1001})提交成功后$(#msg).text(借阅成功)用户体验提升第三阶段2-3年将核心业务逻辑抽成RESTful API用Spring Boot前端彻底Vue化JSP页面降级为登录页和管理后台的静态入口。此时JSP没消失而是退居二线承担最简单的路由和认证第四阶段3年后JSP彻底下线所有页面由Vue Router管理API由Spring Cloud微服务支撑。但当年写的DAO层、数据库设计、事务控制逻辑90%复用到了新架构里。所以不要纠结“JSP是否过时”而要思考“如何用最短路径理解Web的本质”。当你能徒手写出一个不依赖Spring的图书管理系统你就能一眼看穿Spring Boot的RestController不过是Servlet的封装Autowired不过是连接池的自动注入Thymeleaf不过是JSP的现代化替代品。技术栈在变但HTTP协议、数据库ACID、前后端分离的本质从未改变。我带过的实习生里凡是扎实走过JSP这条路的学React或Vue时上手极快因为他们知道fetch发出去的请求后端到底在做什么而直接学Vue CLI的往往卡在“为什么API返回404”这种基础问题上因为没摸过Tomcat的web.xml映射规则。最后分享一个小技巧在Tomcat的conf/logging.properties里把org.apache.jasper.compiler.TldLocationsCache.level WARNING改成FINE就能看到JSP编译时详细的TLD标签库扫描日志。这招帮我定位过三次JSTL标签不生效的问题——原来是web.xml里web-app的version写成了3.0但JSTL 1.2要求4.0。这种细节只有亲手拧过每一个螺丝的人才会在意。