
1. 先搞清楚JSP页面到底是怎么跑起来的很多刚接触Java Web的同学看到JSP里那些尖括号加百分号的写法第一反应是这玩意儿有点像HTML但里面又混着Java代码。这种直觉没错但如果停留在JSP就是HTML里嵌Java的层面后面遇到变量不生效、页面报错找不到符号这类问题就完全不知道怎么排查了。我个人的建议是在学三种语法标签之前先花十分钟搞清楚一个问题浏览器拿到的到底是什么其实浏览器根本不认识JSP文件服务器比如Tomcat拿到.jsp请求之后会先把JSP翻译成一个Java文件再编译成class最后执行这个class把输出的HTML响应给浏览器。换句话说JSP本质上就是一个Servlet——只不过写起来比Servlet方便太多。这个翻译过程是理解所有语法的钥匙。JSP文件里那些HTML标签会被原封不动地扔进Java代码的输出语句里而JSP里的Java代码片段会被直接搬进一个名叫_jspService的方法内部JSP里声明的成员变量则会变成这个Servlet类的属性。提到这个转换过程是因为接下来的三个标签——% %、% %、%! %——分别对应了不同位置的代码搬运规则。搞懂你的代码被搬到了哪里你就真正掌握了JSP语法。从实际教学和做项目的经验看这三种标签在各类课程设计、毕业设计里出现频率极高比如个人信息展示页面、毕业论文管理系统、后台管理列表基本都离不开它们。一个能把这三种标签用得干净利落的人写出来的JSP页面会非常清爽用不好的人页面里全是嵌套混乱的尖括号改一个需求能找半天代码。这章内容定位是JSP基础语法核心就围绕% %、% %、%! %三种标签展开把各自的作用、区别、常见坑和适用场景讲透。适合刚学完Servlet、开始接触JSP的初学者也适合那些JSP能跑但说不清原理、遇到报错只能瞎试的开发者。2. 三种标签逐一拆解写法、用法、典型场景2.1% %脚本片段写在方法里的代码% %官方叫Scriptlet中文常译作脚本片段或脚本小程序。它的规则很简单你在里面写的Java代码会被原样放进_jspService方法里执行。比如下面这段% String name 张三; int age 20; if (age 18) { out.println(name 是成年人); } %这段代码会被翻译进_jspService方法的内部也就是说这里面的变量都是局部变量。它在_jspService方法内部声明也只能在这个方法内部使用。你在一个% %里声明的变量在后面的另一个% %里依然能用因为它们在翻译之后处于同一个方法作用域内。这一点很关键。很多人误以为每个% %是独立的其实不是——它们最终都会被搬到同一个方法体里所以可以跨块使用变量。脚本片段里可以写几乎任何Java语句if/else、for循环、while循环、switch、方法调用、变量定义、异常处理都没问题。最常见的用法就是循环输出列表数据。比如展示一个用户列表ul % ListUser userList (ListUser) request.getAttribute(userList); for (User user : userList) { % li% user.getName() % - % user.getEmail() %/li % } % /ul注意这个写法for循环的左大括号在第一个% %里循环体是HTML和% %混排右大括号在第三个% %里。这在翻译后会形成一个完整的循环结构HTML会作为循环体的一部分被重复输出。这是JSP页面中非常经典且高频的写法几乎所有动态列表页面都长这样。还有一个重要特点脚本片段里可以使用JSP的内置对象——request、response、out、session、application等不需要自己创建。因为_jspService方法的签名里已经准备好了这些参数和变量。初学阶段你可能不关心底层是怎么实现的但至少要知道脚本片段里能直接用的对象在表达式和声明里不一定能用声明里就基本用不了后面会说原因。2.2% %表达式等号代表直接输出% %的学名叫表达式功能和out.print()几乎等价。它的作用是计算括号内Java表达式的值然后把结果以字符串形式输出到页面当前位置。% String name 李四; % p欢迎您% name %/p这里% name %翻译后相当于out.print(name)页面最终显示欢迎您李四。很多人第一次写JSP会在表达式末尾加分号这是最常见的错误之一。% name; %是错的编译直接报错。原因也好理解如果它被翻译成out.print(name;)那这行Java代码本身就是非法的。表达式的核心是计算一个值并打印不是执行一条语句所以里面不能写分号。表达式的适用范围比脚本片段窄一些它只能写一个Java表达式不能写多条语句、不能写循环、不能写变量声明。你可以写% 1 2 % !-- 输出 3 -- % request.getParameter(id) % !-- 输出请求参数 -- % userList.size() % !-- 输出集合大小 -- % user.getName() % !-- 输出对象属性 --表达式也支持字符串拼接和三元运算比如% age 18 ? 成年 : 未成年 %都会先计算结果再输出。在页面排版上表达式是三种标签中最常用的因为它能干净利落地把动态数据嵌入HTML的任意位置。但它也有边界只能输出一个值如果逻辑复杂需要先计算结果那得靠脚本片段提前算好或者用后面要说的EL表达式来替代。这里有个新手常犯的思维惯性错误把表达式当作赋值语句使用。% name 王五 %虽然也能编译通过——因为Java里赋值表达式本身有值会输出王五——但这样写意义不明还会让页面逻辑混乱。表达式的正确定位是读取和展示不是处理和赋值。2.3%! %声明写在类里的变量和方法%! %的学名叫声明它里面写的Java代码会被翻译到类成员的位置——也就是Servlet类的字段或方法定义区域而不是_jspService方法内部。%! private int visitCount 0; private String getGreeting() { return Hello, (visitCount) times; } %上面这段会被翻译成Servlet类的成员变量visitCount和成员方法getGreeting()。所以你看声明标签可以定义变量、定义方法这些变量和方法的生命周期是整个Servlet实例的生命周期。这里要特别强调一个非常重要的区别%! %里声明的变量是成员变量% %里声明的变量是_jspService方法里的局部变量。这不仅仅是位置不同还直接影响了线程安全和作用范围。成员变量所有访问这个JSP页面的用户共享同一个实例变量。一个用户修改了它的值另一个用户能看到。局部变量每次请求进入_jspService方法都会重新创建请求结束就销毁每个用户之间不互相影响。所以%! %里声明的计数器多用户访问时是共享的这点可以用来统计页面总访问次数但也正是它容易引发并发问题的地方——两个用户同时修改一个变量数据就可能错乱。另一个容易踩的坑是声明标签里无法直接使用内置对象。因为_jspService方法里那些参数和局部变量request、response、out等都是方法内的东西在类成员方法里没有对应变量所以你在%! %里写out.println()编译直接报错找不到符号。如果确实需要在一个声明方法里输出内容可以把out作为参数传进去比如%! private void printUser(JspWriter out, User user) throws Exception { out.println(user.getName()); } %然后在脚本片段里调用% printUser(out, user); %这种写法在复杂的JSP页面中偶尔会用但不推荐大规模使用。逻辑复杂到需要封装方法的时候更好的做法是抽取到Servlet或者JavaBean里而不是堆在JSP的声明标签中。2.4 三种标签的翻译位置对比一个JSP文件翻译后的结构大致是public class index_jsp extends HttpJspBase { // 来自 %! % 的成员变量和方法 public void _jspService(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 来自 % % 的脚本片段代码 // 来自 % % 的表达式输出语句 } }用一个表格来看更清楚标签翻译位置是否可以定义方法内置对象可用性生命周期% %脚本片段_jspService方法内部不能方法内不能再定义方法全部可用每次请求独立执行% %表达式_jspService方法内部是out.print()的简写不能全部可用每次请求独立输出%! %声明Servlet类的成员位置可以定义成员方法不能直接用Servlet类实例级别全局共享理解了这个结构你以后看JSP编译报错信息会容易很多。Tomcat把JSP翻译成的Java源码就在apache-tomcat-xxx/work/Catalina/localhost/项目名/org/apache/jsp/目录下。报错的时候照着行号去翻那个生成的.java文件一眼就能看出来你的代码被搬到了哪里、问题出在哪。3. 作用域和生命周期为什么同样一个变量结果天差地别3.1 局部变量和成员变量的真实差异写JSP时最常见的困惑就是同一个变量摆在% %里和摆在%! %里为什么结果完全不一样我们看一个直观的例子%! int globalCount 0; % % int localCount 0; % % globalCount; localCount; % pglobalCount % globalCount %/p plocalCount % localCount %/p你刷新这个页面两次结果会是什么globalCount每次刷新都会累加因为它是Servlet实例的成员变量存在整个Servlet实例的生命周期里而localCount每次刷新都是1因为它是_jspService方法里的局部变量方法一执行完就销毁了。这种行为差异是所有JSP页面并发问题的基础。如果多个用户同时访问全局计数器的累加就不是线程安全的需要加锁或用AtomicInteger才能保证正确性。很多新手在做一个统计网站访问量的功能时很自然地就在%! %里放了个计数器这在单用户测试时正常一上生产就乱了——原因就在这里。顺便说一句就算只是学基础语法也建议养成尽量少用%! %定义可变状态的习惯。JSP页面本来应该偏向于展示逻辑成员变量放在Servlet或Java类里统一管理比散落在JSP声明里要可控得多。3.2 内置对象的可用边界JSP有九大内置对象包括request、response、out、session、application、pageContext、config、page、exception。在% %和% %里这九个都能直接用因为它们都是_jspService方法上下文里的东西。但在%! %里情况就不一样了。前面说过%! %的代码在类成员位置那些方法里才有的局部变量根本不存在。所以%! void doSomething() { out.println(hello); // 编译错误找不到 out } %这行代码会直接编译报错。能不能解决可以把out作为参数传进去或者通过pageContext拿——但pageContext本身也是内置对象在声明里同样不能直接用。所以本质上%! %里想做输出操作就是别扭的这不是JSP设计给你的定位。声明标签的正确用途是定义工具方法比如一个格式化日期的方法、一个判断是否登录的方法再在脚本片段里调用它。但说句实在话到了后期学了EL表达式和JSTL之后连这种用法都不太推荐了——所有需要Java逻辑的地方都应该放进Servlet控制器里提前算好JSP只负责展示。3.3 JSP编译报错时的排查思路我接触过不少学JSP的同学遇到编译报错第一反应是去搜索引擎复制错误信息但很多时候错误提示在Tomcat日志里报错的是翻译后的Java文件而不是你的JSP文件。这时候看work目录下的生成源码是最快的定位方式。举个例子如果你在JSP里写了%! void test() { int x 10; } out.println(x); // 本意是想在声明里用out %编译会报错。如果你去看翻译后的Java文件会看到类似提示。这时关键在于你能立刻意识到out在类成员方法里不存在问题出在使用位置而非语法拼写。在实践中我还见过一种奇怪的情况JSP页面语法完全没问题但一访问就报Unable to compile class for JSP。后来发现是类路径里缺了Servlet API相关的jar包。JSP翻译生成的Java类本身要依赖Servlet接口如果你的Web工程没有正确引入Tomcat的servlet-api就会出现这种编译失败。这个问题和标签语法本身无关但排查的时候也要有个意识——编译错误不一定是你JSP语法的错也可能是环境问题。4. 混合编排的实战套路动态表格、条件渲染和页面布局4.1 动态表格脚本片段控制循环表达式输出数据JSP页面最核心的日常用法就是用脚本片段做流程控制、用表达式输出数据。看一个典型的用户列表表格table border1 tr th编号/th th姓名/th th年龄/th th状态/th /tr % ListUser users (ListUser) request.getAttribute(users); if (users ! null) { for (User user : users) { % tr td% user.getId() %/td td% user.getName() %/td td% user.getAge() %/td td % if (user.getAge() 18) { % 成年 % } else { % 未成年 % } % /td /tr % } } else { % trtd colspan4暂无用户数据/td/tr % } % /table这段代码在翻译之后逻辑很清晰if判断是否为空for循环遍历表格的行在循环体内生成。注意暂无用户数据那个% } else { %的写法——等号脚本块里只有花括号结构HTML内容在花括号之间这正好利用了脚本片段会被原样搬运到_jspService方法里的特性。这种混合写法的可读性在于JSP标签负责跳转、HTML负责外观。很多初学者会把整段Java代码都塞进一个% %里用out.println(trtd...)手工拼HTML字符串这是一种极其不推荐的做法——字符串拼接的转义比HTML模板丑得多而且极易出错。4.2 条件区块控制一段区域的显隐在一个个人信息展示页面里经常需要根据状态显示不同的内容板块。比如用户已登录显示欢迎横幅未登录显示登录按钮% if (session.getAttribute(currentUser) ! null) { % div classwelcome-banner 欢迎回来% ((User) session.getAttribute(currentUser)).getName() % /div % } else { % div classlogin-tip a hreflogin.jsp请先登录/a /div % } %这个场景下JSP脚本片段只负责决定显示哪一段HTML而不负责生成HTML本身。这是最推荐的写法HTML保持HTML的样子Java代码只做控制和数据准备。这里还有一个值得注意的细节当Java逻辑复杂、表达式里有类型转换和多重括号时可读性会下降。你可以先在脚本片段里把变量算好再用表达式输出% User currentUser (User) session.getAttribute(currentUser); String welcomeMsg currentUser null ? : 欢迎回来 currentUser.getName(); % div classwelcome-banner% welcomeMsg %/div这种做法在翻译后的代码里实质上只是多了个局部变量赋值但对JSP源码的可读性提升非常明显。4.3 页面布局中穿插动态属性的写法除了输出文本内容表达式还能用来输出HTML属性的值。比如设置图片路径、链接地址、表单的隐藏字段img src% product.getImagePath() % alt% product.getName() % / a hrefdetail.jsp?id% product.getId() %查看详情/a input typehidden nameproductId value% product.getId() /这个写法非常常用——把Java表达式的值嵌入HTML标签的属性中相当于在字符串拼接中注入数据。初学者需要特别留意的是当属性的值本身包含特殊字符比如英文双引号、尖括号时有可能导致HTML结构错乱。更稳妥的做法是把需要输出的值提前做一次HTML转义如果用了JSTL的c:out标签则默认会做转义这一点后续讲EL表达式时可以展开。关于图片坐标定位的需求补充一句在JSP页面里给图片做坐标定位本质上跟JSP语法没有直接关系而是用CSS的position、top、left、z-index等属性定位。如果图片位置需要根据数据库或请求参数动态变化可以在JSP里把坐标值算出来再拼到style属性里比如div styleposition:absolute; top:% point.getY() %px; left:% point.getX() %px; img srcmarker.pgn / /div这里的point.getY()是从后台传来的坐标数据。思路仍然是JSP负责算出值HTML/CSS负责呈现。4.4 多媒体内容的嵌入写法有些同学做毕业设计时会问JSP页面里怎么播放mp4视频。这个其实也和JSP语法没什么关系视频播放靠的是HTML5的video标签JSP要做的只是动态拼接视频地址video controls width640 height360 source src% video.getUrl() % typevideo/mp4 您使用的浏览器不支持HTML5视频播放 /video同样的逻辑video.getUrl()是从Servlet或JavaBean里取出来的视频路径比如/uploads/xxx.mp4。文件上传和存储属于后端处理JSP层只负责把地址渲染到页面上。网易云的搜热点里也提到了jsp实现mp4视频播放其实真正要解决的往往是文件路径映射如何通过URL访问到服务器上的视频文件这属于Web应用静态资源映射的范畴而不是JSP标签的问题。5. 常见错误和排查实录我在实际调试中遇到的典型问题5.1 分号误加和表达式语法错误在表达式% %后面不加分号这是JSP语法里最经典的新手错误。Tomcat编译时报错信息通常会指向翻译后的Java文件某一行比如org.apache.jasper.JasperException: Unable to compile class for JSP因为表达式里带分号翻译后的out.print(name;)在Java语法中非法。解决方法很简单——去掉分号。但这条错误信息的难点在于它不会直接告诉你表达式不该加分号而是让你看翻译后的Java源码。所以学会去work目录查生成文件是排查JSP错误的基本功。这种错误还有一个变体有人会把表达式当脚本片段用试图在% %里写if语句。这在Java语法上完全不合法因为if是语句不是表达式没有值可以输出。你要是需要做判断用% %加out.print或者干脆用EL表达式加JSTL的c:if。5.2 声明与脚本中同名的变量冲突如果同时写了%! String message 成员; % % String message 局部; %这能编译通过吗能。但使用时要小心在_jspService方法内写message访问的是局部变量局部变量会遮蔽成员变量如果方法内某个地方没有局部变量的定义那访问的就是成员变量。这种同名变量的遮蔽关系很容易让人在调试时晕头转向。我见过有人在页面里定义了局部变量和成员变量同名结果怎么改都不生效最后发现是变量作用域理解错了。这种问题需要记住一个原则写% message %时翻译后的代码在_jspService方法内所以优先找局部变量。想明确访问成员变量可以加this.message% this.message %但说实在的这种同名写法是不良实践正常项目中应该完全避免。5.3 内置对象在声明里不可用的问题前面已经提过——在%! %里直接使用out、request、response一定会编译报错。遇到这个报错很多新手的反应是我明明在别的标签里就是这么用的怎么这里不行然后开始怀疑是不是Tomcat坏了。其实核心问题在于对代码翻译位置的理解。如果确实需要在一个声明方法里使用内置对象请通过参数传入%! void printHeader(JspWriter out, HttpServletRequest request) throws Exception { out.println(当前路径 request.getRequestURI()); } %这样的写法虽然合法但使用频率并不高。绝大多数情况下你不需要在声明里写方法需要处理逻辑就去Servlet需要展示再去JSP。5.4 脚本片段过度嵌套造成可读性灾难有的同学写JSP会在HTML的各个角落都塞上% %导致一个页面的标签数量比HTML标签还多。比如这样% if (a) { % % if (b) { % % if (c) { % p三个条件都满足/p % } % % } % % } %这种三重嵌套虽然能跑但是一旦某个花括号没配对你能想象找错要花多长时间吗更关键的是JSP页面被翻译成Java之前并不检查标签里的Java语法是否有括号配平的问题翻译后的Java源码如果出现结构错误编译报错信息会让人完全摸不着头脑。我的经验是脚本片段里尽量少写逻辑尤其不要多层嵌套。如果条件分支多了提前在Servlet里把结果计算成一个变量页面里只做一次判断如果多个数据需要循环展示考虑用JSTL的c:forEach不仅语法简洁还自动处理了变量作用域的问题。5.5 JSP修改后不生效的假bug还有一种特别坑的情况你在JSP里改动了一行代码刷新页面发现没有变化。排除浏览器缓存之后最常见的原因是Tomcat没有重新编译JSP。Tomcat默认会在JSP文件修改时间变化后自动重新编译但如果你的开发环境设置了modificationTestInterval参数或者IDE没有把修改后的文件同步到部署目录就会出现改了没反应的情况。解决方法是检查部署目录下JSP文件的时间戳或者直接重启Tomcat。等到学习更深入之后可以了解JSP的预编译机制用jspc工具把JSP编译成Servlet类部署到生产环境能避开第一次访问时的编译延迟。这类问题表面上和技术无关但在实际调试中极其消耗时间。所以我的建议是不要只盯着浏览器多看服务器日志和部署目录里的文件确定修改真的生效了再继续排查。6. 和其他技术的关系Servlet、EL表达式、JSTL怎么选6.1 和Servlet的分工边界早期Java Web没有JSP时页面响应全靠Servlet。你需要在自己写的Servlet类里用out.println()一个字符串一个字符串地拼HTML写一个完整的列表页恨不得几百行代码改版式就等于改Java代码。JSP出现以后这种分工变得清晰Servlet负责处理请求、调用业务逻辑、准备数据JSP负责接收数据、渲染显示。从这套分工去看三种标签你应该有这样的直觉JSP里的Java代码越少越好能做数据处理的地方都应该在Servlet里做完。%! %声明变量和方法、写复杂逻辑就是违反这种分工的典型。6.2 与EL表达式的进阶对比JSP 2.0之后出现了EL表达式用${}来取值比如${user.name}。EL表达式能直接访问request/session/application等作用域中的JavaBean属性不再需要写% %脚本。它的好处是更简洁、更不易出错!-- 脚本片段表达式的老写法 -- % User user (User) request.getAttribute(user); % p% user.getName() %/p !-- EL表达式的新写法 -- p${user.name}/pEL表达式后面还会专门讲这里先建立概念EL适合做读取和显示但它不是万能的——它不能执行复杂的Java逻辑比如循环、条件计算不能调用带有复杂参数的方法。如果要做循环遍历配合JSTL的c:forEach使用才是正解。我在指导毕设时见过不少这样的代码项目里已经引入了JSTL但JSP页面还在大量使用% %和% %。能用但整页密密麻麻的尖括号维护起来非常痛苦。如果项目是新的建议两种实现方案里优先ELJSTL如果是在老项目上做维护三种标签的使用就要和老代码风格保持一致方便统一阅读。6.3 关键技术选型的建议以JSP做课程设计或毕业设计为例我的建议是页面显示数据优先用${}EL表达式简单直观。循环遍历集合优先用c:forEach。条件判断生成不同区块优先用c:if或c:choose。需要在页面里执行一些简单Java逻辑可以用% %但要控制复杂度。尽量避免使用%! %声明页面内的变量和方法——把逻辑放到Servlet和Service层才是Web开发的正规思路。当然这不意味着% %和% %过时了。很多老系统、老教材、以及部分对JSP内置对象直接操作的场景依然要靠它们。学基础务必学扎实但运用时要明白哪种方式更合理、更易于维护。7. 动手练一个完整小案例把三种标签综合用起来结合前面讲的知识点我们做一个带登录判断的用户信息展示页面。这个案例会综合使用% %控制流程、% %输出数据、%! %声明辅助方法——虽然实际项目里%! %用得少但通过这个案例你能直观感受三种标签各自的角色定位。先创建一个user.jsp页面思路是模拟一个用户对象判断是否登录展示基本信息并通过一个声明的方法格式化签名。% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 % %! // 声明一个辅助方法生成用户签名 private String getSignature(User user) { if (user null) { return 【未登录】; } return user.getName() 的个人主页; } % % // 模拟登录状态 User currentUser (User) session.getAttribute(loginUser); if (currentUser null) { currentUser new User(1L, 张三, zhangsanexample.com, 22); session.setAttribute(loginUser, currentUser); } String signature getSignature(currentUser); % !DOCTYPE html html head meta charsetUTF-8 title个人中心/title /head body h1% signature %/h1 % if (currentUser ! null) { % ul liID% currentUser.getId() %/li li姓名% currentUser.getName() %/li li邮箱% currentUser.getEmail() %/li li年龄% currentUser.getAge() %/li li点击量% session.getAttribute(visitCount) null ? 0 : session.getAttribute(visitCount) %/li /ul % } else { % p请登录后查看个人中心/p % } % pa hrefindex.jsp返回首页/a/p /body /html案例里的getSignature方法属于%! %声明它在页面整理时被编译为Servlet类的成员方法currentUser、signature属于脚本片段中的局部变量所有输出都是% %表达式完成的而if分支控制显示哪个区块依然靠% %。跑起来之后你可以试着做这些实验把%! %里的getSignature改为脚本片段中的局部方法定义——会报错因为Java不允许在方法内定义方法。把% signature %改成% out.print(signature); %——效果一样。把% signature %改成% signature; %——编译失败记住表达式不能带分号。在%! %里直接写out.println()——编译失败内置对象不能在声明里直接用。加一个%! int visitCount 0; %在页面里visitCount并输出——刷新后你会发现它能累加而同样写在% %里的局部变量不会。这五个实验做下来你对三种标签的差异应该就不是背概念而是有切身体感了。8. 关于JSP基础语法的几条经验总结聊到最后回到最初那句话JSP的三种标签只是工具真正重要的是理解代码被翻译后去了哪里。回顾一下几个最重要的结论第一% %的代码在方法内部是局部变量可以操作页面流程控制、使用内置对象。第二% %是一个值输出是out.print()的简写里面千万不能写分号也不能写语句。第三%! %的代码在类成员位置可以定义方法、成员变量但不能直接用内置对象且成员变量是全局共享的、有并发风险。从实际项目角度讲不管用什么标签组合JSP页面的终极目标都是尽量只做展示。你写的Java代码越多越要想想这个逻辑放在Servlet或者JavaBean里是不是更好。这是从能写出来到写得让别人能维护的分水岭。如果你正在做一个课程设计或毕业设计比如毕业论文管理过程系统、个人信息展示页刚开始的时候可以大胆用% %和% %把功能跑通毕竟这是学习过程的一部分。但功能稳定之后建议返回去把页面里的脚本逻辑用EL表达式和JSTL替代一部分你会发现页面清爽了不止一个档次改起需求来也没有那种动一处崩三处的体验了。JSP的坑往往是同一个错误反复踩才有印象希望这篇文章能让你少踩几个。