
简介面向JSP动态网站开发入门者的教程配套光盘旨在解决初学者环境配置困难、实例分散、知识点难以串联等问题。全书分基础篇与实验篇基础篇12章依次讲解开发环境配置、页面组成元素、内置对象、文件访问、JDBC数据库操作、JavaBean与Servlet技术并补充HTML、CSS、JavaScript等Web基础实验篇设9个单元上机练习覆盖注册校验、聊天室、购物网站等典型场景后半部分给出宠物诊所、图书查询、购物系统三大综合案例适合职业培训、院校教学及自学参考。资源包共480个文件包含124个JSP页面、66个Java类等大量可直接运行的源码以及配置文件、图片、SQL脚本、CHM帮助文档和音频素材整体压缩后约30.73MB。代码按章节目录组织文件预览中可见Servlet、数据库分页、客户管理等核心类便于对照教材逐步验证、二次开发或移植到课程设计。通过这套光盘可边看案例边动手从搭建TomcatMyEclipse环境开始练习JSP内置对象、JDBC数据库访问、JavaBean组件和Servlet应用最终完成三个完整项目。目前已有730人学习下载是一份从基础到应用、适合入门与进阶的实用资料包。1. 还在学JSP这张光盘里最值得拿走的其实不是代码这两年我偶尔还会被问到一个问题JSP 是不是已经被时代淘汰了每次我都得解释一遍——JSP 确实不再是新项目的主流选型但你去看看那些还在稳定运行的 ERP、OA 和政务系统Java Web 的老底盘有一大半是 JSPServlet 撑起来的。会读、会改、会维护 JSP 页面依然是不少运维和二次开发岗位的硬需求。所以当有人把一本《JSP动态网站开发基础教程与实验指导(从基础到应用)》连同光盘递到我面前时我反而觉得这是个被低估的学习资源——前提是你知道怎么用。这张光盘的价值不在代码本身而在它提供了一个完整的、从语法到应用的对照路径。现在网上教程碎片化严重你搜「JSP 内置对象」能出来几十篇文章但没有一篇告诉你这些对象在完整的注册登录流程里到底怎么协作。光盘里的实验指导和配套案例恰好补上了这块短板。对三类人尤其有用刚学完 Java 基础想走 Web 方向的学生、要从静态页面转向动态开发的转岗者、以及需要维护老系统的在职工程师。接下来我把这条「基础到应用」的路按自己的实操经验拆开讲包括环境搭建、语法关键点、数据库落地以及照着实验做时会踩的那些坑。2. 搭一套能跑通全书的 JSP 开发环境JDK、Tomcat 与 IDE 的选型与配置教程配套的光盘里通常会附带开发环境和示例代码但你别指望它里面的安装包能直接在新系统上跑通。老教程的配套环境一般是很多年前的 JDK 版本和现在的操作系统、IDE 之间有一堆兼容问题。所以正确的做法是把光盘里的代码和实验指导抽出来用你自己手头能拿到的现代工具链去跑。我一般会按「JDK Tomcat IDE」三件套来搭这套组合既能跑通老代码又不会让你学完就接触一堆已经废弃的参数。2.1 JDK 与 Tomcat 的版本搭配先别急着装新版JSP 本质是 Servlet 的一种模板化写法最终要跑在支持 Servlet 规范的容器里。Tomcat 就是最常见的容器。选版本时有一条铁律JDK 版本和 Tomcat 版本要匹配。老教程里的代码用的是 JDK 5/6 时代的语法你拿 JDK 17 去跑 Tomcat 8.5 会直接报 UnsupportedClassVersionError反过来用老 JDK 跑新 Tomcat 也可能缺模块。我建议锁定 JDK 8 加 Tomcat 8.5 或 Tomcat 9.x。JDK 8 是一个分水岭它对老代码的兼容性足够好同时也能支撑 EL 表达式和 JSTL 标签库的正常编译。如果你手头是 Win 10 或 Win 11 系统JDK 8 的安装没有任何阻碍。Tomcat 9 实现了 Servlet 4.0 规范跑 JSP 2.3 语法绰绰有余而且启动脚本比老版本更健壮。部分教程会教你用 Tomcat 7但如果实验内容涉及 WebSocket 或新语法Tomcat 7 可能支撑不到位。先下载并解压 Tomcat放到一个没有空格和中文的路径下。Windows 上我习惯用解压版而不是安装版解压版目录清晰出问题也好排查。通常目录结构是这样的D:\apache-tomcat-9.x.x ├── bin # 启动和关闭脚本 ├── conf # 核心配置server.xml、web.xml ├── lib # Servlet/JSP API 和公共 jar 包 ├── logs # 运行日志排查问题第一站 ├── webapps # 部署 Web 应用的目录 └── work # JSP 编译后的 .java 和 .class 文件需要注意work目录。JSP 第一次被请求时才会编译成 Servlet编译产物就放在这里。很多「改了代码不生效」的诡异问题说到底都是 work 目录里的旧编译结果在作怪。后面避坑章节我会单独展开。2.2 配置环境变量与启动验证一条命令确认能跑JDK 装完后需要配置JAVA_HOME和PATH。很多新手跳过这一步直接启动 Tomcat结果双击 startup.bat 窗口一闪而过什么输出都看不到。正确的做法是先把环境变量配好再手动启动。Windows 上环境变量配置如下注意路径以你自己的实际安装位置为准JAVA_HOME C:\Program Files\Java\jdk1.8.0_xxx CATALINA_HOME D:\apache-tomcat-9.x.x PATH %JAVA_HOME%\bin;%CATALINA_HOME%\bin配完后打开新的命令行窗口注意必须是新窗口旧窗口不认新环境变量执行java -version确认 JDK 生效再执行startup.bat启动 Tomcat。看到Server startup in [xxx] milliseconds就说明容器起来了。接着打开浏览器访问http://localhost:8080能看到默认首页就代表三件套已经通了。这里有个关键参数要提Tomcat 默认端口是 8080如果这个端口被别的程序占用了启动日志里会报Address already in use。我一般会直接改 server.xml 里的端口而不是去杀掉占用进程因为你可能同时开着别的开发服务。修改方式如下!-- conf/server.xml 中 Connector 配置 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /把port改成 8081 或 9090 都可以改完重启 Tomcat。改端口的另一个好处是能避开本机其他 Java 进程的端口抢占减少开发时的互相干扰。2.3 在 IDE 里关联 Tomcat 与创建第一个动态 Web 项目环境变量跑通后进入 IDE 阶段。用 Eclipse 还是 IntelliJ IDEA 都能做 JSP 开发我的建议是教程光盘里的代码大多基于 Eclipse 的工程结构如果你希望「导入即用」用 Eclipse 的兼容性最省事如果你更习惯 IDEA那导入时需要手工配置 Facets 和 Artifact。Eclipse 里关联 Tomcat 的操作路径是Window - Preferences - Server - Runtime Environments点 Add 选择你解压的 Tomcat 版本然后指向CATALINA_HOME。创建动态 Web 项目时注意Dynamic web module version选 3.1 或 4.0对应 Tomcat 8.5/9 的规范等级。Target runtime 必须选到刚才关联的 Tomcat 实例否则项目编译时找不到 Servlet API。第一个验证页面不要写复杂代码就写一个最简单的 JSP 文件% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % !DOCTYPE html html head title环境验证/title /head body h1JSP 环境已就绪/h1 p当前时间% new java.util.Date() %/p /body /html这个页面做了三件事通过page指令指定页面编码为 UTF-8避免中文乱码用表达式% %输出服务器当前时间通过contentType告诉浏览器以 HTML 和 UTF-8 解析响应。如果这个页面能正常显示中文和动态时间说明 JDK、Tomcat、IDE 三者已经打通后面的实验才谈得上有意义。很多学员在第一个页面就翻车十有八九是编码没设统一或者在 IDE 里部署时没有自动发布到 webapps 目录。3. 从语法到页面JSP 内置对象、EL 与 JSTL 是「基础到应用」的分水岭JSP 的语法体系看着零散其实可以分成三条线脚本元素声明、表达式、脚本片段、指令page、include、taglib、动作与内置对象。新手最容易犯的错是把这三条线混在一起记结果写页面时不知道该用%! %还是% %也不知道request和session到底谁管谁。这一章我按实际写页面的顺序来拆。3.1 JSP 页面在第一次请求时发生了什么理解编译与生命周期JSP 能写的核心原因在于它不是一个独立技术而是 Servlet 的「模板化皮肤」。第一次有请求到达某个 JSP 页面时Tomcat 会把该 JSP 翻译成一个 Java 文件再编译成 class 加载进 JVM。翻译后的类会继承一个 Servlet 基类页面上写的 HTML 模板变成out.write(...)语句脚本片段变成_jspService方法里的 Java 代码。这个机制解释了三个常见现象。第一为什么首次访问 JSP 页面总是比后续访问慢——因为第一次在编译。第二为什么改了 JSP 不重启容器也能生效——Tomcat 通过文件时间戳检测到 JSP 变更后会重新翻译编译。第三为什么 JSP 里能直接用request、response、session这些对象——它们是_jspService方法的局部变量由容器注入不需要你声明。生命周期方面你需要掌握两点。jspInit()在页面首次编译后执行一次适合放初始化资源jspDestroy()在页面被卸载时执行适合释放资源。不过在实际开发中这两个方法用得不多因为重逻辑都在 Servlet 和 Service 层。理解生命周期的主要收益是排错时能快速判断问题出在「编译阶段」还是「运行阶段」。3.2 三种脚本元素与 page/include 指令的适用边界脚本元素有三种写法各自有严格的适用场景。声明用%! %用于声明成员变量和方法表达式用% %用于向页面输出内容注意结尾不能加分号脚本片段用% %用于嵌入多行 Java 逻辑。看下面的对比%-- 声明成员变量整个页面共享 --% %! private int visitCount 0; public String getVisitInfo() { return 累计访问 visitCount; } % %-- 脚本片段每次请求都会执行 --% % visitCount; String userName request.getParameter(name); if (userName null || userName.trim().isEmpty()) { userName 匿名用户; } % %-- 表达式输出到响应体 --% p你好% userName %/p p% getVisitInfo() %/p注意这里有个新手常踩的坑%! %声明的成员变量是实例变量多个请求线程会共享它所以visitCount不是线程安全的。教学案例这么写没问题但如果你把这个思路带到真实项目里去计数一定会出并发统计失真的事故。正确做法是使用application内置对象配合同步块来计数或者把计数逻辑放到数据库。page指令的常用属性就三个contentType控制响应类型和编码pageEncoding控制页面文件本身的编码import引入需要的类。include指令用于静态包含它是在编译期把被包含文件的内容原样复制进来。静态包含和动态包含的差异是静态包含的变量可以互相访问动态包含jsp:include是运行时合并输出两者混用会导致页面结构混乱。3.3 九大内置对象按场景分类记忆重点盯 request、session 与 application九大内置对象记起来并不难分类记忆就行。与请求相关的有request和response与上下文相关的有session、application、pageContext、page与输出相关的有out其他还有config和exception。它们能直接使用的原因是容器在_jspService方法里已经实例化好了。真正决定程序正确性的是作用域。request作用域只在一次请求内有效转发后还能取到session作用域在同一个浏览器会话内有效用户关闭浏览器或超时才失效application作用域在应用生命周期内全局有效所有用户共享。下面这段代码演示了三种作用域的写入与读取% // request 作用域只在本次请求中有效 request.setAttribute(currentPage, login.jsp); // session 作用域同一用户跨请求共享 session.setAttribute(loginUser, userName); // application 作用域所有用户共享 application.setAttribute(appVersion, 1.0.0); % %-- 通过 EL 表达式读取不同作用域 -- p当前页面${currentPage}/p p登录用户${loginUser}/p p应用版本${appVersion}/psession和application是新手最容易混的两个。记住一个场景就能分清购物车存 session因为每个用户有自己的购物车网站计数器存 application因为所有用户看到的应该是同一个总数。此外pageContext是一个入口对象可以通过它拿到其他八个对象的引用在 EL 表达式里它是作用域查找的起点。3.4 用 EL 和 JSTL 替换页面里的 Java 脚本应用层的起点光盘里的教程如果只教你写脚本片段那它教到的是「基础」部分。真正从基础跨越到应用标志就是你开始用 EL 表达式和 JSTL 标签库替代页面里的 Java 逻辑。原因很朴素脚本片段会让页面变成一团糨糊前端工程师看不懂 Java后端工程师烦改 HTML而 ELJSTL 的组合让页面回归模板的本质。一个典型的 EL 表达式是${param.username}它等价于request.getParameter(username)。更关键的是 EL 支持链式取值比如${sessionScope.loginUser.name}能自动触发getName()方法调用。JSTL 核心标签库处理流程控制看下面的示例% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:choose c:when test${empty sessionScope.loginUser} p您还没有登录请a hreflogin.jsp先登录/a/p /c:when c:otherwise p欢迎回来${sessionScope.loginUser.userName}/p /c:otherwise /c:choose fmt:formatDate value${today} patternyyyy-MM-dd HH:mm:ss /c:choose类似 Java 里的 switchc:when的test属性接收 EL 表达式empty关键字可以同时判断 null 和空字符串。fmt:formatDate用于日期格式化避免手工拼接日期字符串带来的格式不统一。把脚本片段换成 JSTL 标签后页面里只剩 HTML 和少量标签维护成本明显下降。这个替换动作也是后面第 6 章 MVC 重构的基础。如果你连 EL 和 JSTL 都没用熟直接跳到三层架构会非常吃力。4. JSP 与数据库对话JDBC、连接池与 DAO 层的一次完整落地动态网站的核心价值在于数据能流动用户在前端输入信息程序写入数据库下次访问时再读出来。实验指导里最常见的案例就是用户注册和登录。这一部分我按工程化的路径来拆解而不是照搬课本上的「演示代码」。两者最大的区别在于演示代码把数据库逻辑写在 JSP 页面里工程化代码把它们拆到单独的 DAO 层。4.1 JDBC 四步走加载驱动、建连接、执行、释放JDBC 是 JSP 访问数据库的底层通道无论你后面是否引入框架这四步都必须理解加载驱动、建立连接、执行 SQL、释放资源。教科书版本的写法在本地跑没问题但有个致命隐患——连接是用完就关还是用完放回池子直接决定了你的网站能不能撑住并发访问。先看一个标准的 JDBC 访问代码骨架import java.sql.*; public class UserDao { // 参数说明useUnicode 与 characterEncoding 强制指定 UTF-8 // 解决中文写入数据库乱码的问题 private static final String URL jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your password; public User queryByUsername(String username) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { Class.forName(com.mysql.cj.jdbc.Driver); conn DriverManager.getConnection(URL, USER, PASSWORD); String sql SELECT id, username, password, nickname FROM user WHERE username ?; ps conn.prepareStatement(sql); ps.setString(1, username); rs ps.executeQuery(); if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setNickname(rs.getString(nickname)); return user; } return null; } catch (Exception e) { e.printStackTrace(); return null; } finally { // 释放顺序不能乱先结果集再语句最后连接 if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } } }这个骨架里有几个参数值得解释。?是占位符配合setString(1, username)使用这是防 SQL 注入的第一道防线。Class.forName在 JDBC 4.0 之后可以省略但老教程里都有保留它对老数据库驱动更保险。finally块里的释放顺序是血泪经验总结如果先关连接再关语句某些驱动会抛异常虽然不至于崩溃但日志里的堆栈会让你误以为代码有 bug。4.2 PreparedStatement 的注入防线与参数化写法实验指导的早期案例通常用Statement拼接 SQL比如Statement stmt conn.createStatement(); String sql SELECT * FROM user WHERE username username ; ResultSet rs stmt.executeQuery(sql);这种写法在登录场景里有致命缺陷。如果用户在用户名输入框里填 OR 11拼接出来的 SQL 变成了WHERE username OR 11恒为真直接绕过密码验证。这就是 SQL 注入的基本原理。虽然现在的框架都内置防注入机制但 JSP 实验课如果让你手写 JDBC你必须有意识地把Statement换成PreparedStatement。PreparedStatement的防注入原理是预编译。SQL 先发送给数据库编译明确定义?占位符的位置和类型之后setString传入的值只作为参数处理不再参与 SQL 结构的拼接。即便用户输入了恶意的 OR 11它也会被当作一个普通的字符串字面量处理。除了防注入预编译还有性能收益。同一 SQL 语句在循环里多次执行时数据库不需要每次重新编译。不过这个收益在单个查询场景下感知不明显别把它当成万能优化手段。还需要记住表名和列名不能用?占位符因为它们属于 SQL 结构的一部分预编译只能处理值。4.3 连接池选型与配置从教学案例到工程化的第一步JSP 实验课里用DriverManager.getConnection没问题但如果你把这个模式直接搬到一个正在运行的生产网站里数据库很快会被连接创建和销毁的开销拖垮。每次请求建立一次物理连接的开销很大高并发下几十个请求就能让数据库超时。这时候需要连接池。连接池的核心理念是预创建一批连接放到池子里请求来了借走用完归还而不是关闭。Tomcat 自身就提供了连接池实现配置写在 web.xml 或 context.xml 里JSP 页面通过 JNDI 获取数据源。下面是一个在 Tomcat 里配置 MySQL 连接池的常见写法!-- META-INF/context.xml放在你的 Web 应用目录下 -- Context Resource namejdbc/shopDB typejavax.sql.DataSource driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/shop?useUnicodetrueamp;characterEncodingUTF-8 usernameroot passwordyour password maxActive20 maxIdle8 minIdle4 maxWait5000 initialSize5 / /Context参数含义maxActive是池中最大连接数maxIdle是最大空闲连接数minIdle是最小空闲连接数maxWait是请求连接的最大等待毫秒数超过后抛异常initialSize是启动时预创建的连接数。这些参数不是拍脑袋填的maxActive一般按(CPU 核数 * 2) 磁盘数估算同时要参考数据库的max_connections上限。拿到数据源对象的标准方式是 JNDI 查找Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/shopDB); Connection conn ds.getConnection();注意这里拿到的conn在用完后调用close()并不会真正关闭物理连接而是把连接归还给池子。新手往往理解不了这一点误以为连接池泄漏其实close()在池化场景下的语义是「归还」。4.4 DAO 模式落地一个用户注册模块的完整骨架DAOData Access Object模式的核心是让数据库操作和业务逻辑分居两层。JSP 页面或 Servlet 只负责接收参数和跳转页面DAO 类专门负责 SQL 操作。这样做的直接收益是日后数据库从 MySQL 换成其他数据库只需要改 DAO 层页面代码一行不动。一个用户注册的 DAO 方法需要支持两种操作按用户名查询判断用户是否已存在、插入新用户。第一个操作 4.1 节已经写过插入操作类似。真正容易出问题的是事务处理——注册时可能要同时向用户表和用户详情表写入数据任何一个失败都必须回滚。下面是一个带事务控制的注册方法骨架public boolean register(User user) { Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 从连接池获取 conn.setAutoCommit(false); // 关闭自动提交开启事务 String insertUser INSERT INTO user(username, password, nickname) VALUES(?, ?, ?); ps conn.prepareStatement(insertUser); ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getNickname()); int rows ps.executeUpdate(); // 这里还可以执行第二条 SQL例如插入用户扩展信息 // 如果第二条失败会抛出异常触发布回滚 conn.commit(); // 全部成功才提交 ps.close(); return rows 1; } catch (Exception e) { try { if (conn ! null) conn.rollback(); // 异常时回滚 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(ps, conn); } }setAutoCommit(false)是事务开启的起点commit()是成功提交rollback()是失败回滚。这是三个动作缺一个都不算完整事务。注意rollback()必须写在catch块里而不是finally块里否则即使没有异常也会执行回滚把成功提交的数据撤销。到这里一个实验课级别的用户模块已经有了完整的工程化路径JSP 页面收集参数Servlet 调用 DAODAO 管理连接和事务。这就是「从基础到应用」的关键一跃——环境、语法、数据库三个环节的衔接全部打通。5. 跟着实验指导动手时最容易翻车的 5 个坑及排查路径JSP 是老技术但老技术的坑一点不少。我见过太多学员卡在同一个地方代码照着抄了一模一样运行结果就是不对。这一章把高频踩坑点集中整理出来每一条都按「现象 → 原因 → 解决」来写你照着排查比从头查日志快得多。5.1 中文乱码全链路排查从页面到数据库再到响应头现象页面显示???或æ±å数据库里存的中文变成问号或者页面接收到的表单中文全是乱码。原因字符编码链路断裂。浏览器提交表单用的是页面指定的编码服务器读取请求参数用的是 Tomcat 默认的 ISO-8859-1数据库连接串没有指定 UTF-8三处不一致就会出乱码。血泪经验是乱码问题不是单点问题是一条链路上每一环都要统一。解决从上到下统一设置为 UTF-8。JSP 页面里pageEncodingUTF-8和contentTypetext/html; charsetUTF-8两个都要写。数据库连接串加useUnicodetruecharacterEncodingUTF-8。另外在 web.xml 里配置一个编码过滤器最省心filter filter-nameencodingFilter/filter-name filter-classcom.demo.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滤器类里就是request.setCharacterEncoding(UTF-8)一行核心代码但url-pattern必须写成/*覆盖所有请求路径只写某个目录会漏掉其他页面。5.2 改完 JSP 刷新不生效缓存与重编译现象修改了 JSP 页面的布局或文案刷新浏览器还是旧内容有时重启 Tomcat 后第一次访问还是旧的。原因有两层缓存。一是 Tomcat 对 JSP 的重新编译依赖文件时间戳如果你的 IDE 保存文件时没有更新时间戳某些网络挂载盘或版本控制工具会这样Tomcat 就不会触发重编译。二是浏览器端缓存了响应内容尤其是静态资源。还有一个隐蔽因素Eclipse 部署项目时有时没有把最新文件同步到webapps目录下发布环节静默失败。解决先清浏览器缓存确认问题不在客户端。然后删掉 Tomcat 的work/Catalina目录下的缓存编译结果重启 Tomcat。在 IDE 里执行Clean并重新Publish到服务器。我在实际排错中养成的习惯是改完 JSP 后直接到webapps目录下检查一下文件时间戳确认 IDE 确实发布过去了。5.3 驱动 jar 包找不到WEB-INF/lib 与 Maven 依赖的差别现象写好了Class.forName(com.mysql.cj.jdbc.Driver)运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因JDBC 驱动 jar 包没有放进正确的位置。老教程里让你把 jar 包复制到WEB-INF/lib下但如果你用了 IDE 的项目构建功能复制的文件可能没被标记为部署资源。另一个常见情况是 jar 包放进了项目根目录或lib目录但没映射到部署结构里。解决确认驱动 jar 包在发布后的WEB-INF/lib目录下而不是仅仅在你本地项目的某个文件夹里。在 Eclipse 中右键项目 -Build Path-Configure Build Path-Add JARs然后右键项目 -Properties-Deployment Assembly确认 jar 包映射到了WEB-INF/lib。用 Maven 构建的话在pom.xml里声明依赖即可但要注意scope不能写provided。5.4 连接池配置把服务器拖垮参数不是越大越好现象项目启动正常跑几分钟后数据库连接数飙满页面打开越来越慢最终所有请求超时日志里出现Connection is not available, request timed out。原因连接池参数设置不合理。最常见的是maxActive设置过大比如直接填 200数据库本身连接数上限才 100连接池还没来得及报错数据库先挂了。另一种情况是代码里拿到了连接但没归还finally块里漏写了close()连接池连接被占用后永远不回来。解决maxActive设置要参考数据库实际能力不要随便填 100 以上。检查所有getConnection()是否和close()成对出现用jconsole观察连接池的活跃连接数和空闲连接数。如果连接数一直涨不回落基本可以断定是泄漏——去查哪些方法没有在finally块里释放连接。重启能暂时缓解但必须修代码否则是定时炸弹。5.5 404 与跳转路径的混沌根路径和相对路径的取舍现象页面 A 跳转到页面 B直接写hrefb.jsp在本目录下能用但经过 Servlet 转发到/user/index.jsp后所有链接都 404。原因相对路径是相对于当前 URL 的解析路径不是相对于项目的根目录。Servlet 转发会把浏览器地址栏的 URL 变成项目根路径 映射路径原本在jsp/user目录下有效的相对链接全部失效。这是 JSP 开发里最磨人的路径问题没有之一。解决所有跳转和链接统一使用绝对路径以项目上下文路径为开头。在 JSP 里用${pageContext.request.contextPath}动态获取项目根路径a href${pageContext.request.contextPath}/user/index.jsp首页/a也可以直接在 JSP 页面开头设置base标签让后续所有相对链接自动基于项目根路径解析base href${pageContext.request.contextPath}/这两种方式二选一即可混用反而会出现双重前缀。记住一条判断标准凡是forward转发的页面里面尽量不要写相对路径因为你无法保证转发前后 URL 的层级一致。6. 最后一课把脚本式 JSP 重构为 MVC 三层结构的验证方法实验做到这里你已经能写出一个页面里塞满 Java 代码、但功能完整的注册登录模块。接下来这一步才是能否从「基础」进入「应用」的分水岭把脚本式 JSP 重构为 Servlet JSP JavaBean 的三层结构。重构的目标不是改功能而是让代码的角色变清晰——JSP 只干活视图负责展示、Servlet 负责控制接收参数、调业务、跳转、JavaBean 负责数据模型封装属性和业务逻辑。我建议从你实验里最小的一个功能模块开始改比如「用户登录」。改造后的目录结构应该是这样的src/main/java ├── com.demo.bean # User.java 实体类 ├── com.demo.dao # UserDao.java 数据库访问 ├── com.demo.servlet # LoginServlet.java 控制器 └── com.demo.filter # EncodingFilter.java 编码过滤器 WebContent ├── login.jsp # 登录页面只含 HTML 和 EL 表达式 ├── index.jsp # 登录成功后的首页 └── WEB-INF ├── web.xml └── lib三个角色的协作关系是用户在login.jsp输入账号密码表单提交到LoginServletServlet 调用UserDao.queryByUsername验证然后把结果放进session或request作用域再forward到index.jsp。JSP 页面里通过${sessionScope.loginUser}读取数据不再写任何 Java 脚本。重构完成后可以用三步验证你是否真的拆干净了。第一步全局搜索 JSP 文件里的%标签指令标签除外数量应为 0。第二步打开login.jsp的源码应该只有 HTML、CSS、JSTL 标签和 EL 表达式。第三步把UserDao里的 SQL 从 MySQL 改成标准 SQL页面的展示和交互不应该发生任何变化——如果你发现改 DAO 时影响到页面逻辑说明分层没有彻底。我自己带人学时有个习惯重构完成后会让对方再改一个小需求比如「登录成功后展示最近登录时间和 IP」。这个改动如果只需要动 Servlet 和 JSP 各一个位置说明结构是健康的如果要在三个文件里找来找去说明围绕业务逻辑还是没有收敛。JSP 这套东西学了值不值得到最后比拼的不是你会写多少标签而是你能不能在一个存量项目里快速定位一处修改的影响范围。希望这些经验在你动手时真的帮到你少走几段弯路。本文还有配套的精品资源点击获取