☰
JSP是什么?从原理到实战,一文彻底搞懂Java Web动态页面技术
2026/10/9 11:55:11 网站建设 项目流程

很多人刚接触 Java Web 时,都绕不过“什么是 JSP”这个问题。我第一次看到 JSP,还以为是什么新语言或复杂模板框架,后来才明白:它本质上是把 Java 能力嵌入在网页里、由服务端动态生成内容的一种技术。简单说,JSP(JavaServer Pages)允许你在一个看起来像 HTML 的文件里写 Java 代码,等请求过来时,容器会把这些代码翻译、执行,最终生成纯 HTML 回给浏览器。它既能读数据库数据,也能处理表单参数、管理会话状态,还能和 Servlet 一起搭出经典的 MVC 结构。

这篇文章我不打算照着官方文档念定义,而是从一个开发者的视角,把 JSP 的来龙去脉、运行原理、实际用法和踩过的坑都说透。如果你刚开始学 Java Web,或者要接手一个年代久远的老项目,那么这篇内容能帮你少折腾几个晚上。我会尽量用大白话讲原理,用代码举例子,最后还会放一个可以直接照着跑的登录页 Demo。

1. JSP 到底是什么?先从最小页面看起

1.1 一个最简单的 JSP 页面长什么样

先别扯理论,直接看一段代码。新建一个文件,后缀名是.jsp,比如hello.jsp,内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>第一个 JSP 页面</title> </head> <body> <% String name = "开发者"; %> <h1>你好,<%= name %>!</h1> <p>当前时间:<%= new java.util.Date() %></p> </body> </html>

把这个文件扔到一个支持 JSP 的容器里,启动后访问对应地址,浏览器端就会看到“你好,开发者!”和一个动态刷新的时间。你发现没,这个文件大部分是 HTML,但中间夹着<% %>和<%= %>这类特殊标记,这就是 JSP 的核心表现形式:HTML 负责页面结构,Java 代码负责动态计算。

有些朋友会觉得这看起来像模板引擎,比如 FreeMarker、Thymeleaf 之类的思路确实有点像,但 JSP 其实更“重”,因为页面里的脚本片段最终会被完整编译成 Servlet 类,相当于直接在 Java 代码里拼 HTML。理解这一点,对你后面判断 JSP 的优缺点非常重要。

1.2 动态页面是怎么“动态”起来的

静态 HTML 文件是什么样子,用户访问就是什么样子,它不会因为请求者的身份、当前时间或者数据库状态而改变。但业务系统里显然需要动态页面:同一个“我的订单”页面,不同用户看到的数据必须不同;同一个搜索页,不同关键词得返回不同结果。

JSP 解决的就是这个问题。它允许页面在服务端执行 Java 代码,代码可以查数据库、读 Session、判断权限,然后把结果输出成 HTML 片段。对浏览器来说,JSP 最终返回的仍然是标准 HTML,用户根本看不出背后是 JSP 还是静态文件。换句话说,JSP 是“服务端渲染”的一种老派实现,而今天常说的 SSR(服务端渲染)在思路上和它有很多共通点。

1.3 JSP 出现的历史原因:Servlet 写页面实在太痛苦

在 JSP 出现之前,Java Web 领域主要靠 Servlet。Servlet 也能生成 HTML,但写法非常反人类:

response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><title>首页</title></head>"); out.println("<body>"); out.println("<h1>商品列表</h1>"); for (Product p : products) { out.println("<div>" + p.getName() + " - " + p.getPrice() + "</div>"); } out.println("</body>"); out.println("</html>");

一旦页面结构复杂,这种拼接代码会让人疯掉。尤其要注意 Java 字符串里的双引号需要转义,嵌套几层之后,肉眼根本分不清是 HTML 属性还是 Java 字符串边界。

JSP 的诞生思路就是“反着来”:

  • Servlet 是“在 Java 代码里写 HTML”
  • JSP 是“在 HTML 里写 Java”

这样页面设计师能直接编辑 HTML 结构,后端开发者只关注需要动态替换的部分。虽然现在大家觉得 JSP 老,但站在当时的技术环境下,它确实把 Web 开发的门槛狠狠拉低了一层。

2. JSP 的隐藏机制:请求进来以后发生了什么

2.1 从浏览器发出请求到页面响应的完整链路

很多人用 JSP 却不知道它内部到底怎么跑,遇到问题就只能乱猜。我建议每个使用者都至少理解一遍这个流程,它并没有多复杂:

  1. 浏览器向服务器发起请求,请求地址指向某个.jsp文件。
  2. Servlet 容器收到请求后,检查这个 JSP 对应的 Servlet 类是否已存在且未过期。
  3. 如果不存在,容器根据 JSP 文件内容生成一个 Java 源文件,这个源文件是一个 Servlet 子类。
  4. 容器接着编译这个源文件,生成.class字节码。
  5. 容器实例化这个类,调用它的_jspService方法,并把request、response等对象传进去。
  6. 页面中的 HTML 标签会变成out.write(...)调用,Java 脚本片段会被原样嵌入方法体。
  7. 执行完整个页面,缓冲区里的内容被刷新成响应,浏览器收到 HTML。

第一次访问 JSP 时,因为要生成源码、编译、加载,通常会比后续访问慢一些。之后同样的 URL 再被访问,容器会直接用缓存好的 Class,速度就正常了。这也解释了为什么你改了 JSP 文件后经常要“重新编译”一次,容器会通过文件修改时间判断是否需要重新翻译。

2.2 九大内置对象到底从哪来

JSP 页面里可以直接使用request、response、session、out这些对象,不用自己 new。很多人把它们当成“魔法”,其实它们是被容器作为参数传进/jspService方法的局部变量,只是 JSP 文件在语法层面帮你宣告好了。

下面是一个常用速查表:

内置对象名类型主要用途
requestHttpServletRequest获取请求参数、属性、请求头等
responseHttpServletResponse设置响应头、状态码、重定向等
outJspWriter向页面输出文本内容
sessionHttpSession保存用户会话状态,比如登录信息
applicationServletContext整个 Web 应用的全局信息
pageContextPageContext当前页面上下文,可以访问其他隐式对象
configServletConfig当前 Servlet 的配置信息
pageObject当前页面对象,一般很少用
exceptionThrowable只有页面声明为错误页时才可用

其中request、response、session、out是最常用的。比如request.getParameter("username")能取到表单参数;session.setAttribute("user", user)能保存登录状态;out.print能在页面任意位置输出字符串。

2.3 指令、脚本元素、EL 和 JSTL,各干各的活

JSP 文件里不只是 HTML 和<% %>两种东西,它还有指令、表达式语言(EL)和标准标签库(JSTL)这些配套工具。一个成熟的 JSP 工程里,它们的职责会分得很清:

  • 指令:用<%@ ... %>表示,作用是给 JSP 编译阶段提供配置。最常见的是page指令,比如<%@ page contentType="text/html;charset=UTF-8" %>控制输出类型和编码;<%@ include file="header.jsp" %>在编译期把另一个文件内容合并进来;<%@ taglib prefix="c" uri="..." %>引入标签库。
  • 脚本片段:<% ... %>里写 Java 代码,可以实现逻辑控制;<%= expression %>会计算表达式并输出结果;<%! ... %>用来声明页面的成员变量和方法,但我不建议你轻易用,因为会引入并发问题。
  • EL 表达式:${user.name}这种写法,用来从 request、session 等作用域读取数据,语法简洁,不会写 Java。
  • JSTL:标准标签库,常见的是c:if、c:forEach、c:choose,可以把循环和判断用标签写出来。

比如一个商品列表,用脚本是这么写:

<% List<Product> list = (List<Product>) request.getAttribute("products"); for (Product p : list) { %> <div><%= p.getName() %> - <%= p.getPrice() %></div> <% } %>

用 JSTL 则是:

<c:forEach var="p" items="${products}"> <div>${p.name} - ${p.price}</div> </c:forEach>

两者效果几乎一样,但可读性差距很大。JSTL 版本更像“模板”,而脚本版本把 Java 循环结构裸露在外面,看久了眼睛疼。这也是老项目不好维护的根源之一:大家把逻辑全往 JSP 里塞,最后页面变得又长又乱。

3. JSP、Servlet、模板引擎:到底该怎么选

3.1 MVC 分工才是 JSP 的正确用法

教科书里总说 JSP 适合做 MVC 的 V(View),因为它天生是页面模板;Servlet 适合做 C(Controller),因为它能处理请求路由和业务调度;JavaBean 或者后面的 Service 层适合做 M(Model),承载数据和业务逻辑。

一个典型的流程是这样的:

  1. 浏览器请求某个 URL,容器把请求交给对应的 Servlet。
  2. Servlet 接收参数、调用业务逻辑、查询数据。
  3. Servlet 把数据放进 request 或 session 里,然后通过request.getRequestDispatcher("xxx.jsp").forward(request, response)转发给 JSP。
  4. JSP 从 request 中读取数据,生成 HTML 响应回去。

这样 JSP 页面里不需要关心数据库连接和权限判断,它只负责把“已经准备好的数据”展示出来。如果你现在的工作中遇到 JSP 和 Servlet 协作的代码,按这个思路去梳理,脉络会非常清楚。

3.2 JSP 和 Servlet 其实就是一件事的两种形态

严格来说,JSP 最终会被容器翻译成 Servlet,所以它俩不是两个对立的技术,而是同一个东西的两种语法形态。区别在于:

  • Servlet 更适合做控制逻辑、处理 JSON/XML 接口、设置响应头等非展示工作。
  • JSP 更适合做页面展示,尤其是需要大量 HTML 标签的场景。

如果你要写一个文件下载接口,返回一个流,那用 Servlet 更合适;如果你要渲染一个订单详情页面,里面有表格、图片、超链接,那 JSP 更直观。一些老项目里也存在“用 Servlet 拼 HTML、用 JSP 写业务”的混乱写法,属于典型的反面教材,能避开就避开。

3.3 都 202x 年了,JSP 是不是过时了

这个问题我被问过很多次。坦率地说,新项目再从头用 JSP 建站的情况确实越来越少。原因不难理解:

  • 前端工程化发展太快,用脱离现代构建工具的 JSP 去实现复杂交互很痛苦。
  • JSP 中只要能写 Java,就会有人忍不住写大量业务逻辑,最后变成难维护的“意大利面条”。
  • 前后端分离架构下,后端直接返回 JSON,前端用 Vue/React 等框架渲染,JSP 在中间的角色被弱化了。

但“用得少”不等于“没用”,更不等于“学它没意义”。大量现存的企业系统和老平台的前后端仍由 JSP 支撑,很多公司还有成百上千个.jsp文件在线上跑。你如果看不懂 JSP,接手这种项目会很吃力。

作为对比,我先拿 JSP 和现代模板引擎列一个表:

对比项JSP现代模板引擎(如 Thymeleaf 等)
底层实现编译成 Servlet纯模板解析生成字符串
代码侵入性可以内嵌任意 Java 代码严格限制表达式和标签
前后端解耦较弱,依赖容器更接近纯模板,方便静态预览
生态现状老项目多,新项目少新项目使用更普遍
学习曲线需要懂 Java 和 JSP 规范更简单,容易上手

所以我的建议是:如果你是新手,可以学 JSP 来理解“服务端渲染”和“Servlet 容器”的概念,但别把它当成唯一的技术方向;如果你要维护老项目,那就必须把 JSP 的语法、内置对象、JSTL 和 EL 弄扎实。它不是洪水猛兽,只是个工具,关键看你怎么用。

4. 上手实操:做一个简单的 JSP 登录页

4.1 准备一个能跑 JSP 的环境

JSP 不能直接在浏览器里打开,它必须跑在 Servlet 容器里。常见的做法是下载一个开源 Servlet 容器(比如某个主流容器产品),然后配置好 JDK 环境变量。

我建议新手走最朴素的路线:不用 IDE,直接用命令行和记事本也能跑通,反而能看清项目结构。核心步骤是这样:

  1. 安装 JDK,配好JAVA_HOME。
  2. 下载一个与 JDK 版本兼容的 Servlet 容器,解压到本地。
  3. 在容器的webapps目录下新建一个项目目录,比如demo。
  4. 在demo目录里建WEB-INF目录,里面放一个最基本的web.xml文件(高版本容器允许用注解,但手动项目里保留一个也无妨)。
  5. 把.jsp文件放进demo根目录。
  6. 启动容器,浏览器访问http://localhost:端口/demo/xxx.jsp。

如果不需要写 Servlet,只有一个 JSP 文件,甚至可以不写web.xml。容器会直接扫描.jsp文件并把它当动态页面处理。端口号看容器默认配置,常见的是 8080 或 8000 附近,具体以你下载的版本为准。

4.2 先写一个登录表单页

在项目根目录下新建login.jsp,内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>用户登录</title> </head> <body> <h2>登录</h2> <form action="dologin.jsp" method="post"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <button type="submit">登录</button> </form> </body> </html>

注意表单的action指向dologin.jsp,提交方式是post,input的name分别叫username和password。这些名字要和后端获取参数时保持一致,否则你会拿到 null。

4.3 再写一个处理登录逻辑的 JSP

新建dologin.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.Objects" %> <% request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 演示环境写死账号,真实项目绝不能这么做 if ("admin".equals(username) && "123456".equals(password)) { session.setAttribute("loginUser", username); response.sendRedirect("welcome.jsp"); } else { out.println("<script>alert('用户名或密码错误');location='login.jsp';</script>"); } %>

这段代码里有几个点可以拆开讲:

  • request.setCharacterEncoding("UTF-8")必须在读取参数之前调用,否则 POST 表单里的中文可能变乱码。
  • session.setAttribute把登录名放进了会话,后面页面能读到。
  • response.sendRedirect是重定向,浏览器地址栏会变化,相当于重新发起一次请求。
  • 用out.println输出一段 JavaScript,是简单粗暴但有效的错误提示方式。

在真实项目里,你要把账号校验换成数据库、密码哈希和结构化异常处理,但作为理解 JSP 数据流转的例子,这样写足够清晰。顺便说一下,页面里不应该出现<% %>里塞整段业务代码的写法,这里是为了演示,请勿照抄进正式工程。

4.4 再写一个欢迎页

新建welcome.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Object user = session.getAttribute("loginUser"); if (user == null) { response.sendRedirect("login.jsp"); return; } %> <html> <head> <title>欢迎</title> </head> <body> <h2>欢迎回来:<%= user %></h2> </body> </html>

这里用了最简单的会话判断:如果session里没有登录用户,就跳回登录页。很多老项目用的就是这种模式。实际开发中建议在框架层面用拦截器或者过滤器统一做登录校验,而不是每个 JSP 都写一遍。

4.5 中文乱码问题怎么彻底解决

JSP 涉及的中文乱码,绝大多数都是“字符集不统一”造成的。你至少要保证以下四处一致:

  • 页面文件保存的编码,必须是 UTF-8。
  • JSP 头部的contentType里声明charset=UTF-8。
  • 获取 POST 参数前调用request.setCharacterEncoding("UTF-8")。
  • 容器处理 GET 请求中 URL 参数的编码也要设置为 UTF-8(不同容器配置方式不同,底层逻辑都是让容器按 UTF-8 解码)。

只要有一处是 GBK、ISO-8859-1,或者文件保存编码和声明编码不一致,就会出现那些“问号”和“方块字”。排查时可以先用最简单的一行文字测试,再把过滤器、连接器配置逐步加上,通常很快能定位。

5. 实战避坑清单:这些坑我基本都踩过

5.1 别把 JSP 写成“意大利面条”

新手最容易犯的错误,是把所有逻辑都堆进 JSP。我曾经见过一个列表页里,从查询数据库到拼接 HTML 全部用<% %>写完,超过六百行,整个页面像一锅粥。后面再加需求,改动一处就要在一个混合了 HTML、Java、JavaScript、CSS 的大文件里找半天。

应对办法其实不复杂:

  • 数据准备交给 Servlet 或后台接口,JSP 只负责展示。
  • 循环、判断尽量用 JSTL 标签,不要用<% for %>。
  • 展示输出用 EL 表达式${xxx},不要写<%= complexMethod() %>。
  • 如果页面里实在需要一段 Java 工具方法,把它抽到工具类里,然后通过 request 属性传入。

长期维护的项目,代码整洁度比炫技重要得多。你少在 JSP 里写一行业务,后面的人救你头的概率就低一分。

5.2 静态包含和动态包含千万别搞混

JSP 有两种包含其他页面的方式:

  • <%@ include file="head.jsp" %>是静态包含,在 JSP 编译阶段直接把文件内容复制进来。包含之后,两个文件共享同一个作用域,如果两边定义了同名的变量,会直接编译报错。
  • <jsp:include page="head.jsp" />是动态包含,运行时把请求转发给被包含页面,展示完结果再回来。它更灵活,可以传参数,但性能上略有开销。

我早年在一个公共头部文件里写了个变量叫basePath,然后被静态包含到了多个页面,其中一个页面也声明了同名变量,结果容器报了一堆“Duplicate local variable”错误。排查半天才发现是因为静态包含全被复制进来了。所以,能分清两者的语义再去用,否则就尽量统一风格。

5.3 错误页、缓存头、上下文路径这些细节

JSP 页面默认会把异常信息直接显示在页面上,既难看又暴露细节。正规做法是专门做一个错误页,通过page指令标记:

<%@ page isErrorPage="true" %> <html> <body> <h2>出错了</h2> <pre><%= exception.getMessage() %></pre> </body> </html>

然后在可能出错的页面里声明:

<%@ page errorPage="error.jsp" %>

这样容器捕获到未处理异常后,会转发到错误页,而不是输出一堆堆栈堆给人看。注意exception对象只有在isErrorPage="true"的页面里才能用,其他页面直接用会编译失败。

另外,JSP 里如果用了相对路径,经常会因为 URL 层级不同而找不到 CSS、JS 或图片。最简单的做法是用${pageContext.request.contextPath}拼接项目的上下文根:

<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">

这样无论请求路径在哪一层,资源路径都能正确解析。这个细节在你做动态页面、前端又要跳转多个路径时非常重要。

5.4 一张速查表解决高频故障

我做 JSP 项目时,遇到的高频问题基本都能归到下面几类:

现象可能原因排查方向
访问 JSP 报 404文件没放对目录,或项目未部署成功检查webapps下目录结构和容器的日志
页面报 500JSP 编译出错或脚本抛异常查看容器日志中的堆栈,定位到.jsp对应行
中文全部变成问号编码不一致检查文件保存编码、contentType、请求解码
javax.servlet相关类找不到缺少 Servlet API 依赖或容器版本不匹配确认项目编译时是否引入了容器提供的 API
JSTL 标签不生效没引入标签库或 jar 包缺失检查taglib指令的 uri 和依赖是否正确
页面改了但不生效容器缓存了旧 Class清理容器临时目录,或重启容器

需要特别提醒的是,JSP 的报错信息往往会在页面顶部输出一大段英文,很多人被吓到,其实核心信息就在org.apache.jasper.JasperException之后的 Caused by 那一行。把日志往后面翻,看到Java.lang...才是真正的原因。

5.5 几个能提升效率的小工具与习惯

日常用 JSP 开发时,我给自己定了几条规矩:

  1. 所有页面统一从page指令声明编码和contentType,不要偷懒。
  2. 公共头部和尾部用静态包含或动态包含抽离,不要每个页面复制粘贴。
  3. 页面里只用 EL 和 JSTL,除非遇到标签完全搞不定的极端情况,才考虑局部脚本。
  4. 不能确定依赖 jar 是否齐全时,先在容器日志里看有没有ClassNotFoundException。
  5. 上线前用浏览器的“查看源代码”看看最终生成的 HTML 是否符合预期,有时候 JSP 里多了一个空格都会影响布局。

这些习惯听起来很基础,但很多项目就是因为忽略它们,最后变得越来越难改。维护老代码时,我最怕的不是 JSP 语法难,而是毫无章法的混写。

6. 最后再分享一点我做 JSP 项目的真实体会

如果你正准备学 JSP,我的建议是从“看懂”开始,而不是急着用它在生产环境里大干一场。先用一个登录页、一个列表页跑通流程,再尝试用 EL 和 JSTL 把脚本元素替换掉,体会什么叫“模板式页面”。

我自己每次接手陌生 JSP 项目,第一件事不是看业务文档,而是在整个工程里搜索<%和<%=。如果脚本元素出现频率非常高,那说明这个项目大概率有重构空间;如果页面基本是静态 HTML 加 JSTL 标签,维护性就还不错。这个方法能让你在两分钟内判断系统的健康状况,比读半天代码快多了。

JSP 这条路走了很多年,被无数项目使用过,也被无数人吐槽过。它的核心问题不是技术本身,而是它太容易让人写出难以维护的代码。反过来说,如果你能克制住往页面里堆 Java 的冲动,JSP 在服务端渲染领域仍然是一套简单可靠的方案。希望这篇内容能帮你少踩一些我当年踩过的坑,也让你在看到老系统里那些.jsp文件时,心里能多几分底气和从容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询