多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JSP是什么?从原理到实战,一文彻底搞懂Java Web动态页面技术

JSP是什么?从原理到实战,一文彻底搞懂Java Web动态页面技术 很多人刚接触 Java Web 时都绕不过“什么是 JSP”这个问题。我第一次看到 JSP还以为是什么新语言或复杂模板框架后来才明白它本质上是把 Java 能力嵌入在网页里、由服务端动态生成内容的一种技术。简单说JSPJavaServer Pages允许你在一个看起来像 HTML 的文件里写 Java 代码等请求过来时容器会把这些代码翻译、执行最终生成纯 HTML 回给浏览器。它既能读数据库数据也能处理表单参数、管理会话状态还能和 Servlet 一起搭出经典的 MVC 结构。这篇文章我不打算照着官方文档念定义而是从一个开发者的视角把 JSP 的来龙去脉、运行原理、实际用法和踩过的坑都说透。如果你刚开始学 Java Web或者要接手一个年代久远的老项目那么这篇内容能帮你少折腾几个晚上。我会尽量用大白话讲原理用代码举例子最后还会放一个可以直接照着跑的登录页 Demo。1. JSP 到底是什么先从最小页面看起1.1 一个最简单的 JSP 页面长什么样先别扯理论直接看一段代码。新建一个文件后缀名是.jsp比如hello.jsp内容如下% page contentTypetext/html;charsetUTF-8 languagejava % 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;charsetUTF-8); PrintWriter out response.getWriter(); out.println(html); out.println(headtitle首页/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 却不知道它内部到底怎么跑遇到问题就只能乱猜。我建议每个使用者都至少理解一遍这个流程它并没有多复杂浏览器向服务器发起请求请求地址指向某个.jsp文件。Servlet 容器收到请求后检查这个 JSP 对应的 Servlet 类是否已存在且未过期。如果不存在容器根据 JSP 文件内容生成一个 Java 源文件这个源文件是一个 Servlet 子类。容器接着编译这个源文件生成.class字节码。容器实例化这个类调用它的_jspService方法并把request、response等对象传进去。页面中的 HTML 标签会变成out.write(...)调用Java 脚本片段会被原样嵌入方法体。执行完整个页面缓冲区里的内容被刷新成响应浏览器收到 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 contentTypetext/html;charsetUTF-8 %控制输出类型和编码% include fileheader.jsp %在编译期把另一个文件内容合并进来% taglib prefixc uri... %引入标签库。脚本片段% ... %里写 Java 代码可以实现逻辑控制% expression %会计算表达式并输出结果%! ... %用来声明页面的成员变量和方法但我不建议你轻易用因为会引入并发问题。EL 表达式${user.name}这种写法用来从 request、session 等作用域读取数据语法简洁不会写 Java。JSTL标准标签库常见的是c:if、c:forEach、c:choose可以把循环和判断用标签写出来。比如一个商品列表用脚本是这么写% ListProduct list (ListProduct) request.getAttribute(products); for (Product p : list) { % div% p.getName() % - % p.getPrice() %/div % } %用 JSTL 则是c:forEach varp items${products} div${p.name} - ${p.price}/div /c:forEach两者效果几乎一样但可读性差距很大。JSTL 版本更像“模板”而脚本版本把 Java 循环结构裸露在外面看久了眼睛疼。这也是老项目不好维护的根源之一大家把逻辑全往 JSP 里塞最后页面变得又长又乱。3. JSP、Servlet、模板引擎到底该怎么选3.1 MVC 分工才是 JSP 的正确用法教科书里总说 JSP 适合做 MVC 的 VView因为它天生是页面模板Servlet 适合做 CController因为它能处理请求路由和业务调度JavaBean 或者后面的 Service 层适合做 MModel承载数据和业务逻辑。一个典型的流程是这样的浏览器请求某个 URL容器把请求交给对应的 Servlet。Servlet 接收参数、调用业务逻辑、查询数据。Servlet 把数据放进 request 或 session 里然后通过request.getRequestDispatcher(xxx.jsp).forward(request, response)转发给 JSP。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直接用命令行和记事本也能跑通反而能看清项目结构。核心步骤是这样安装 JDK配好JAVA_HOME。下载一个与 JDK 版本兼容的 Servlet 容器解压到本地。在容器的webapps目录下新建一个项目目录比如demo。在demo目录里建WEB-INF目录里面放一个最基本的web.xml文件高版本容器允许用注解但手动项目里保留一个也无妨。把.jsp文件放进demo根目录。启动容器浏览器访问http://localhost:端口/demo/xxx.jsp。如果不需要写 Servlet只有一个 JSP 文件甚至可以不写web.xml。容器会直接扫描.jsp文件并把它当动态页面处理。端口号看容器默认配置常见的是 8080 或 8000 附近具体以你下载的版本为准。4.2 先写一个登录表单页在项目根目录下新建login.jsp内容如下% page contentTypetext/html;charsetUTF-8 languagejava % html head title用户登录/title /head body h2登录/h2 form actiondologin.jsp methodpost div 用户名input typetext nameusername /div div 密码input typepassword namepassword /div button typesubmit登录/button /form /body /html注意表单的action指向dologin.jsp提交方式是postinput的name分别叫username和password。这些名字要和后端获取参数时保持一致否则你会拿到 null。4.3 再写一个处理登录逻辑的 JSP新建dologin.jsp% page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.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(scriptalert(用户名或密码错误);locationlogin.jsp;/script); } %这段代码里有几个点可以拆开讲request.setCharacterEncoding(UTF-8)必须在读取参数之前调用否则 POST 表单里的中文可能变乱码。session.setAttribute把登录名放进了会话后面页面能读到。response.sendRedirect是重定向浏览器地址栏会变化相当于重新发起一次请求。用out.println输出一段 JavaScript是简单粗暴但有效的错误提示方式。在真实项目里你要把账号校验换成数据库、密码哈希和结构化异常处理但作为理解 JSP 数据流转的例子这样写足够清晰。顺便说一下页面里不应该出现% %里塞整段业务代码的写法这里是为了演示请勿照抄进正式工程。4.4 再写一个欢迎页新建welcome.jsp% page contentTypetext/html;charsetUTF-8 languagejava % % 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里声明charsetUTF-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 filehead.jsp %是静态包含在 JSP 编译阶段直接把文件内容复制进来。包含之后两个文件共享同一个作用域如果两边定义了同名的变量会直接编译报错。jsp:include pagehead.jsp /是动态包含运行时把请求转发给被包含页面展示完结果再回来。它更灵活可以传参数但性能上略有开销。我早年在一个公共头部文件里写了个变量叫basePath然后被静态包含到了多个页面其中一个页面也声明了同名变量结果容器报了一堆“Duplicate local variable”错误。排查半天才发现是因为静态包含全被复制进来了。所以能分清两者的语义再去用否则就尽量统一风格。5.3 错误页、缓存头、上下文路径这些细节JSP 页面默认会把异常信息直接显示在页面上既难看又暴露细节。正规做法是专门做一个错误页通过page指令标记% page isErrorPagetrue % html body h2出错了/h2 pre% exception.getMessage() %/pre /body /html然后在可能出错的页面里声明% page errorPageerror.jsp %这样容器捕获到未处理异常后会转发到错误页而不是输出一堆堆栈堆给人看。注意exception对象只有在isErrorPagetrue的页面里才能用其他页面直接用会编译失败。另外JSP 里如果用了相对路径经常会因为 URL 层级不同而找不到 CSS、JS 或图片。最简单的做法是用${pageContext.request.contextPath}拼接项目的上下文根link relstylesheet href${pageContext.request.contextPath}/css/style.css这样无论请求路径在哪一层资源路径都能正确解析。这个细节在你做动态页面、前端又要跳转多个路径时非常重要。5.4 一张速查表解决高频故障我做 JSP 项目时遇到的高频问题基本都能归到下面几类现象可能原因排查方向访问 JSP 报 404文件没放对目录或项目未部署成功检查webapps下目录结构和容器的日志页面报 500JSP 编译出错或脚本抛异常查看容器日志中的堆栈定位到.jsp对应行中文全部变成问号编码不一致检查文件保存编码、contentType、请求解码javax.servlet相关类找不到缺少 Servlet API 依赖或容器版本不匹配确认项目编译时是否引入了容器提供的 APIJSTL 标签不生效没引入标签库或 jar 包缺失检查taglib指令的 uri 和依赖是否正确页面改了但不生效容器缓存了旧 Class清理容器临时目录或重启容器需要特别提醒的是JSP 的报错信息往往会在页面顶部输出一大段英文很多人被吓到其实核心信息就在org.apache.jasper.JasperException之后的 Caused by 那一行。把日志往后面翻看到Java.lang...才是真正的原因。5.5 几个能提升效率的小工具与习惯日常用 JSP 开发时我给自己定了几条规矩所有页面统一从page指令声明编码和contentType不要偷懒。公共头部和尾部用静态包含或动态包含抽离不要每个页面复制粘贴。页面里只用 EL 和 JSTL除非遇到标签完全搞不定的极端情况才考虑局部脚本。不能确定依赖 jar 是否齐全时先在容器日志里看有没有ClassNotFoundException。上线前用浏览器的“查看源代码”看看最终生成的 HTML 是否符合预期有时候 JSP 里多了一个空格都会影响布局。这些习惯听起来很基础但很多项目就是因为忽略它们最后变得越来越难改。维护老代码时我最怕的不是 JSP 语法难而是毫无章法的混写。6. 最后再分享一点我做 JSP 项目的真实体会如果你正准备学 JSP我的建议是从“看懂”开始而不是急着用它在生产环境里大干一场。先用一个登录页、一个列表页跑通流程再尝试用 EL 和 JSTL 把脚本元素替换掉体会什么叫“模板式页面”。我自己每次接手陌生 JSP 项目第一件事不是看业务文档而是在整个工程里搜索%和%。如果脚本元素出现频率非常高那说明这个项目大概率有重构空间如果页面基本是静态 HTML 加 JSTL 标签维护性就还不错。这个方法能让你在两分钟内判断系统的健康状况比读半天代码快多了。JSP 这条路走了很多年被无数项目使用过也被无数人吐槽过。它的核心问题不是技术本身而是它太容易让人写出难以维护的代码。反过来说如果你能克制住往页面里堆 Java 的冲动JSP 在服务端渲染领域仍然是一套简单可靠的方案。希望这篇内容能帮你少踩一些我当年踩过的坑也让你在看到老系统里那些.jsp文件时心里能多几分底气和从容。
返回列表