那天凌晨两点,办公室的空调已经停了,空气里弥漫着陈旧的咖啡味和一种名为“绝望”的情绪。我盯着屏幕上那行红色的报错,感觉自己的肝也在跟着一起报错。
org.apache.jasper.JasperException: /WEB-INF/views/user/list.jsp(45,2) The attribute [value] has no value
或者更经典的那个,让我连续排查了三天的:
org.apache.jasper.JasperException: Unable to compile class for JSP
这是我在某传统企业做后端维护时的一次真实经历。那个项目有着十年以上的历史,JSP页面里混杂着大量的Java代码、JavaScript、HTML标签,还有各种各样稀奇古怪的脚本片段。老板说,“这个功能很紧急,明天早上演示给客户看”,然后我去翻那堆代码的时候,心态崩了。
今天,我想把这段痛苦的记忆拆解开来,不是为了卖惨,而是为了告诉你:JSP并不是什么复杂的黑魔法,但你如果不尊重它的基础语法,它绝对会用它最原始的方式教训你。 尤其是当Java代码和HTML标签混写的时候,那些肉眼难辨的标点符号,就是让你加班到996的罪魁祸首。
一、 为什么我们还在用JSP?(以及为什么我们要尊重它)
首先要澄清一个误区:很多人觉得JSP是“过时”的技术,应该被Vue、React、Spring Boot彻底抛弃。确实,在现代化的前后端分离架构中,JSP几乎销声匿迹了。但是,在中国的很多传统行业——金融、政务、大型国企的老系统里,JSP依然是主力。
为什么?因为存量巨大,迁移成本高。
这就好比你的公司虽然打算搬进新大楼,但现在的旧办公室还得用,而且里面的电线(Java代码)和家具(HTML结构)已经长在一起了。你要么忍着恶心继续维护,要么花巨资重建。大部分公司选择了前者。
所以,理解JSP,不仅是为了救急,更是为了生存。JSP的本质其实很简单:它是运行在服务器端的模板引擎。
当浏览器请求一个.jsp文件时,Tomcat服务器会先把这个文件“翻译”成一个Java Servlet(.java文件),然后编译成字节码(.class文件),最后执行这个Servlet,生成HTML发给浏览器。
这个“翻译”过程是自动的,但也是最容易出问题的地方。因为在这个过程中,JSP中的任何一段Java代码,都必须严格遵循Java语言的语法规范。如果你写错了,服务器在“翻译”阶段就会直接报错,而你看到的往往只是一串晦涩的堆栈信息。
二、 JSP的三种核心脚本元素:混写的三大“地雷”
JSP允许你在HTML中插入Java代码,这通过三种脚本元素实现。理解它们的区别,是避免语法错误的第一步。
1. 表达式(Expression):<%= %>
这是最简单,也是最容易出bug的地方。表达式用于将Java数据直接输出到浏览器。
正确写法:
<p>当前用户是:<%= user.getName() %></p>
这会生成:<p>当前用户是:张三</p>
常见错误:
很多初学者会忘记=,或者在标签内外乱加空格,或者在表达式内部写分号。
<!-- 错误1:少了等号,这会被当成普通的注释或文本 -->
<p>当前用户是:<% user.getName() %></p>
<!-- 错误2:末尾加了分号,这会导致编译错误 -->
<p>当前用户是:<%= user.getName(); %></p>
<!-- 错误3:标签内部包含了非法的Java语句,比如if/for -->
<p>结果是:<%= if (a > b) { "yes"; } else { "no"; } %></p>
为什么错误3会炸?
因为<%= %>里面只能是一个表达式(Expression),必须能返回一个值,不能是语句(Statement)。if是语句,没有返回值,所以编译器直接懵圈。
2. 脚本段(Scriptlet):<% %>
脚本段用于执行任意Java代码,但它不会直接向页面输出任何内容。通常用来定义变量、逻辑判断、调用方法等。
正确写法:
<%
String name = request.getParameter("name");
if (name != null) {
out.println("欢迎," + name);
}
%>
常见错误:
- 缺少结束标签:写了
<%却忘了写%>,整个后面的HTML都会被视为Java代码,直到遇到下一个%>,页面直接崩坏。 - 括号不匹配:Java是强类型的,括号、引号必须成对出现。少一个右花括号
},编译必挂。 - 分号问题:在脚本段末尾必须加分号,因为这是一段Java代码块。
<!-- 错误:少了一个右花括号,编译器会报“illegal start of expression”或“reached end of file” -->
<%
String name = "Tom"
System.out.println(name);
%>
3. 声明(Declaration):<%! %>
这个现在用得很少,主要用于声明类级别的变量或方法。
<%!
private int counter = 0;
public void increment() {
counter++;
}
%>
注意:声明里的变量属于Servlet类成员变量,多线程下不安全。这就是为什么现在大家更倾向于用<% %>里的局部变量。
三、 那个让我加班三天的Bug:EL表达式与JSTL的“混血”陷阱
回到我那个凌晨的案例。报错信息是The attribute [value] has no value。
代码大概长这样:
<c:forEach var="item" items="${items}">
<tr>
<td><%= item.getId() %></td>
<td><%= item.getName() %></td>
<td>
<input type="hidden" name="itemId" value="<%= item.getId() %>" />
<button onclick="delete(<%= item.getId() %>)">删除</button>
</td>
</tr>
</c:forEach>
看起来很美,对吧?<%= %>输出Java对象属性,<c:forEach>循环遍历。
问题出在哪里?
问题一:EL表达式(${})与脚本表达式(<%= %>)的混用与优先权
JSP2.0引入了EL表达式(Expression Language),目的是为了替代冗长的<%= %>。但是,很多老代码和新代码混杂在一起。
在某些特定版本和配置下,如果你开启了EL解析,又同时使用了<%= %>,可能会产生冲突。更重要的是,EL表达式是“零侵入”的,而<%= %>会打断HTML流的输出。
在我那个案例中,真正的问题其实是类型转换。
// 假设 item.getId() 返回的是 Long 类型
// 而在 HTML 中:
<button onclick="delete(<%= item.getId() %>)">
如果item.getId()返回的是null,那么输出就是<button onclick="delete(null)">。这在JavaScript里是合法的,但在某些旧的IE浏览器或严格的JavaScript引擎中可能报错。但更严重的是,如果这个ID是字符串,比如"A001",那么输出就是delete(A001),JavaScript会去找一个叫A001的变量,而不是字符串"A001",导致undefined错误,业务逻辑完全错乱。
问题二:JSTL标签内的属性值不能直接嵌Java代码
这是最容易踩的坑。看这段代码:
<c:forEach var="item" items="${items}">
<!-- 错误写法:在JSTL属性中直接写Java脚本 -->
<c:set var="currentId" value="<%= item.getId() %>" />
<!-- 错误写法:在属性中混用EL和脚本 -->
<input value="<%= item.getName() %>" />
</c:forEach>
虽然JSP规范允许在属性值中使用脚本表达式,但JSTL标签处理器在解析属性时,可能会先尝试解析EL表达式,然后再处理脚本。这会导致解析顺序混乱,特别是在复杂的嵌套结构中。
正确的做法是:彻底分离。
如果必须用JSTL,就全程用EL:
<c:forEach var="item" items="${items}">
<input value="${item.name}" />
</c:forEach>
如果用Java脚本,就全程用<%= %>,不要引入JSTL循环:
<%
List<Item> items = (List<Item>) request.getAttribute("items");
for (Item item : items) {
%>
<input value="<%= item.getName() %>" />
<%
}
%>
混用是万恶之源。
四、 语法错误的典型模式与排雷指南
除了上面的案例,还有几种高频的错误模式,我帮你整理成了“排雷清单”。
1. 字符集与编码导致的“乱码式”语法错误
有时候,你的代码看起来完全正确,但编译器报错说“illegal character”。
<%
String str = "你好"; // 如果JSP文件保存为UTF-8,但Tomcat默认用ISO-8859-1解析
%>
现象:编译器报错,提示某个字符无法识别,或者位置偏移。 原因:源文件编码与服务器解析编码不一致。 解决:
- 确保JSP文件以UTF-8保存。
- 在JSP头部声明:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> - 在Tomcat的
server.xml中配置Connector:<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
2. 作用域变量的隐式丢失
在JSP中,有四个内置作用域:page, request, session, application。
<%
request.setAttribute("userName", "Alice");
%>
<a href="profile.jsp">查看个人资料</a>
在profile.jsp中:
<%
String name = (String) request.getAttribute("userName"); // 返回null!
out.println(name);
%>
现象:页面正常显示,但数据是null,导致后续逻辑报错(如NullPointerException)。
原因:页面跳转后,request作用域已经失效。
解决:
- 如果是转发(
request.getRequestDispatcher().forward()),request作用域保留。 - 如果是重定向(
response.sendRedirect()),request作用域丢失,应使用session作用域或URL参数传递。
<%
session.setAttribute("userName", "Alice"); // 改用session
%>
3. 脚本let中的分号缺失或多余
这是最“低级”但也最隐蔽的错误。
<%
int a = 10
int b = 20;
%>
现象:编译错误,提示“expected”。 解决:每个Java语句末尾必须加分号。
<%
int a = 10;
int b = 20;
%>
4. 标签嵌套错误
JSP标签必须正确嵌套,就像HTML一样。
<c:if test="${user.login}">
<div>Welcome</div>
<c:forEach items="${menus}" var="menu">
<li><a href="${menu.url}">${menu.name}</a></li>
</c:forEach>
<!-- 忘记关闭c:if -->
现象:编译错误,提示“end tag for … omitted”。 解决:仔细检查每一对开始和结束标签。建议使用IDE(如IntelliJ IDEA或Eclipse)的JSP格式化功能(Ctrl+Alt+L),它会自动帮你修复大部分嵌套错误。
五、 如何彻底避免这些坑?(给开发者的实用建议)
经历了那个月的996,我总结出了一套“防坑法则”,分享给所有还在与JSP搏斗的同行。
1. 强制代码格式化
在IDE中配置JSP格式化规则。不格式化的JSP代码就是地雷阵。
- IntelliJ IDEA:
Settings -> Editor -> Code Style -> HTML/JSP,勾选“Reformat code”。 - Eclipse:
Source -> Format(Ctrl+Shift+F)。
格式化后,标签嵌套、括号匹配、缩进层次一目了然,80%的语法错误可以肉眼发现。
2. 遵循“视图层纯Java”原则
不要在JSP中写复杂的业务逻辑。 JSP只负责展示(View),Java代码只负责获取数据(Model)和简单逻辑(Controller的延伸)。
反例:
<%
// 这是大忌!在JSP里写数据库查询
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db", "user", "pass");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
out.println(rs.getString("name"));
}
%>
正例:
在Servlet中查询数据,放入request或session,JSP只负责遍历展示。
// Servlet
List<User> users = userService.findAll();
request.setAttribute("users", users);
request.getRequestDispatcher("/views/user/list.jsp").forward(request, response);
<!-- JSP -->
<c:forEach var="user" items="${users}">
<p>${user.name}</p>
</c:forEach>
3. 优先使用EL表达式,慎用脚本表达式
只要项目支持JSP 2.0+(现在几乎所有服务器都支持),尽量用${}代替<%= %>。
- EL表达式更安全,自动处理
null值(不会报错,只是不显示)。 - EL表达式更简洁,可读性更强。
- EL表达式与JSTL标签配合得天衣无缝。
只有在你必须调用某个复杂Java对象的方法,且该方法没有对应的getter时,才考虑使用<%= %>。
4. 使用JSTL替代Java脚本
JSTL(JSP Standard Tag Library)提供了丰富的标签库,可以替代大部分<% %>代码。
- 流程控制:用
<c:if>,<c:choose>,<c:forEach>替代if,switch,for。 - 数据操作:用
<c:set>,<c:remove>替代setAttribute,removeAttribute。 - 国际化:用
<fmt:formatDate>,<fmt:message>替代复杂的SimpleDateFormat。
5. 开启JSP预编译(Advanced)
对于生产环境,可以考虑将JSP预编译成Servlet,这样可以在部署前发现语法错误。
在web.xml中配置:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>false</el-ignored>
<page-encoding>UTF-8</page-encoding>
<scripting-invalid>false</scripting-invalid>
</jsp-property-group>
</jsp-config>
同时,使用Maven插件如jaxb2-maven-plugin或Tomcat的tomcat7-maven-plugin进行预编译检查。
六、 给小朋友也能听懂的比喻
如果上面的内容太硬核,我用一个比喻来总结一下。
想象JSP页面就像一台正在表演的舞台剧。
- HTML是舞台的布景和道具(桌子、椅子、灯光),它们是静止的。
- Java代码是演员(程序员写的逻辑),他们需要在舞台上行动。
- JSP标签(
<%= %>,<% %>)是给演员的台本提示。
语法错误就像是什么?
- 忘了写台本提示的结束符:演员拿起台词念了一半,发现后面没词了,只能干站在那里,观众(浏览器)一脸懵逼。
- 台本提示里写了不该写的词:你在给演员的提示里写了“开始跳舞”,但演员(表达式
<%= %>)只能“说话”(输出值),不能“跳舞”(执行语句),于是导演(编译器)就炸毛了。 - 两个演员抢同一个角色:你在同一个地方既用了EL表达式
${},又用了脚本表达式<%= %>,他们互相不知道对方已经做了,结果重复输出,或者互相覆盖,场面混乱。
所以,尊重舞台规则,给演员清晰的台本,不要让它们在同一个位置打架,这场戏才能演得顺畅,你也不用在凌晨两点还在后台补台本。
结语
JSP的语法错误,往往不是因为你不懂Java,而是因为你低估了“混写”的复杂性。
在那个月光惨淡的深夜,我最终解决的方案是:重构。我将所有<%= %>替换为EL表达式,将所有<% %>中的逻辑抽离到Servlet,只保留最必要的JSTL标签。代码量减少了一半,可读性提升了一倍,而我,终于能在晚上十点前回家了。
希望这篇文章能帮你避开那些坑,不再成为996的一员。记住,清晰的代码结构,是最好的防Bug神器。
如果你正在与某个具体的JSP报错搏斗,欢迎在评论区留下你的代码片段,我们一起把它“解剖”清楚。
