在Java Web开发的早期阶段,JSP(JavaServer Pages)与JSTL(JSP Standard Tag Library)的结合是构建动态视图的主流技术之一。<c:forEach> 标签作为JSTL核心库中用于循环遍历的利器,极大地简化了在页面上展示集合数据的代码,正是由于其依赖特定的环境配置和语法规范,开发者在初次使用或在不熟悉的环境中部署时,常常会遇到各种报错,本文旨在系统性地梳理这些常见错误,分析其根源,并提供清晰、可行的解决方案,帮助开发者快速定位并修复问题。

最经典的错误:"According to TLD or attribute directive in tag file, attribute items does not accept any expressions"
这个报错信息是JSP初学者在使用 <c:forEach> 时最常遇到的“拦路虎”。
错误根源分析: 这个错误的核心原因在于JSP容器(如Tomcat)未能将 识别为EL(Expression Language)表达式,而是将其视为一个普通的字符串,这通常由以下两个主要因素导致:
- JSTL标签库未正确引入: JSP页面顶部缺少或写错了
taglib指令,容器不知道<c:forEach>是什么,更不用说如何处理其属性中的EL表达式了。 - JSTL依赖缺失或版本不兼容: Web应用的
WEB-INF/lib目录下没有包含JSTL的实现库(.jar文件),或者引入的JAR包版本与Servlet/JSP的API版本不匹配(使用Jakarta EE 9+的JSTL库配合Java EE 8的容器)。
解决方案:
-
检查
taglib指令: 确保在JSP页面的最上方添加了正确且完整的taglib指令,对于传统的Java EE(javax)环境,应使用:<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
对于较新的Jakarta EE(jakarta)环境,URI有所不同:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!-- 注意:即使在Jakarta EE中,这个URI通常也有效,因为实现库会做兼容性处理 --> <!-- 或者使用Jakarta命名空间的URI,如果库支持 -->
-
添加正确的JSTL依赖:
- Maven项目: 在
pom.xml中添加依赖,对于Servlet 4.0及以下版本(Java EE 8):<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> - Jakarta EE 9+ 项目: 需要使用Jakarta版本的JSTL:
<dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId> <version>2.0.0</version> </dependency> - 传统项目: 手动下载相应版本的JSTL JAR包(如
jstl-1.2.jar)并将其放入WEB-INF/lib目录。
- Maven项目: 在
类型不匹配错误:"For input string: ..." 或 Attribute items does not accept type ..."
当 <c:forEach> 的 items 属性接收到了一个它无法遍历的类型时,就会抛出此类错误。
错误根源分析:
<c:forEach> 标签的 items 属性设计用于接收以下类型的对象:
- 任何实现了
java.util.Collection接口的对象(如List,Set)。 - 数组(如
String[],int[])。 java.util.Map对象(遍历时,var变量将是Map.Entry)。java.util.Iterator或java.util.Enumeration。
当你传入一个 null 值,或者一个普通单一对象(如一个 String、Integer)时,标签库无法理解如何“迭代”它,从而报错。

解决方案:
- 后端数据准备: 确保在Servlet或Controller中,你存入request、session或application作用域的是一个有效的集合对象。
// Servlet示例 List<String> userList = new ArrayList<>(); // ... 填充数据 ... request.setAttribute("users", userList); // 确保这里是List,不是单个String - 前端空值判断: 这是一个非常重要的防御性编程习惯,在调用
<c:forEach>之前,使用<c:if>判断集合是否为null或空,这不仅能防止报错,还能在无数据时给用户一个友好的提示。<c:if test="${not empty users}"> <ul> <c:forEach items="${users}" var="user"> <li>${user}</li> </c:forEach> </ul> </c:if> <c:if test="${empty users}"> <p>暂无用户数据。</p> </c:if>
属性名拼写错误
这是一个看似低级但实则非常常见的错误,由于JSTL标签的属性名是固定的,任何拼写错误都会导致标签无法被正确解析。
错误根源分析:
开发者可能会不小心将 items 写成 item,var 写成 variable,varStatus 写成 status 等,JSP容器在解析TLD(Tag Library Descriptor)文件时,找不到对应的属性定义,从而抛出异常。
解决方案:
熟悉并牢记 <c:forEach> 的核心属性,以下是一个清晰的属性列表,可供参考:
| 属性名 | 类型 | 描述 | 是否必须 |
|---|---|---|---|
items |
支持迭代的类型 | 要被遍历的集合对象。 | 否(与begin/end二选一) |
var |
String | 存储当前迭代元素的变量名。 | 是 |
varStatus |
String | 存储迭代状态的变量名(类型为LoopTagStatus)。 |
否 |
begin |
int | 迭代的起始索引(包含)。 | 否 |
end |
int | 迭代的结束索引(包含)。 | 否 |
step |
int | 迭代的步长,默认为1。 | 否 |
在编写代码时,请仔细核对属性名,确保与上表完全一致。
嵌套循环中的变量名冲突
在处理复杂数据结构(如分类下的商品列表)时,经常需要使用嵌套的 <c:forEach>,如果内外层循环的 var 属性使用了相同的名称,就会引发逻辑错误,虽然通常不会直接报错,但会导致数据展示异常。
错误根源分析:
当内外层循环都使用 var="item" 时,内层循环的 item 会覆盖(shadow)外层循环的 item,在内层循环中,你无法再访问到外层循环的当前元素。
解决方案: 为每一层循环使用具有明确含义且唯一的变量名。
错误示例:

<c:forEach items="${categories}" var="item">
<h3>${item.name}</h3>
<c:forEach items="${item.products}" var="item"> <!-- 这里的item覆盖了外层的item -->
<p>${item.name}</p>
</c:forEach>
</c:forEach>
正确示例:
<c:forEach items="${categories}" var="category">
<h3>${category.name}</h3>
<c:forEach items="${category.products}" var="product">
<p>${product.name}</p>
</c:forEach>
</c:forEach>
通过使用 category 和 product 这样描述性的变量名,代码的可读性和正确性都得到了极大的提升。
相关问答FAQs
问题1:如果我想循环一个固定的次数,比如从1到10,必须要在后端创建一个List吗?
解答: 完全不需要。<c:forEach> 提供了非常方便的 begin、end 和 step 属性来支持这种场景,你只需要指定起始和结束数字即可,标签会自动为你生成一个整数序列,要打印1到10的数字,可以这样写:
<ul>
<c:forEach begin="1" end="10" var="index">
<li>当前数字: ${index}</li>
</c:forEach>
</ul>
在这个例子中,var="index" 会在每次循环时被赋值为1, 2, 3, ..., 10。step 属性可以用来控制增量,默认为1,step="2" 会生成1, 3, 5, ...
问题2:我的 <c:forEach> 循环没有报任何错误,但页面上什么内容都没有显示,这是为什么?
解答: 这种情况通常不是语法错误,而是逻辑问题,最常见的原因是传递给 items 属性的集合对象是 null 或者是一个空集合(empty)。<c:forEach> 标签在遇到 null 或空的 items 时,会静默地跳过整个循环,不会产生任何输出,也不会抛出异常。
排查步骤:
- 检查后端: 确认在你的Servlet或Controller中,确实向作用域(如request)中设置了数据,并且这个数据集合不是空的,可以在设置断点进行调试。
- 使用
c:out输出: 在JSP页面上,可以在循环外部尝试直接输出这个集合,看它是什么状态。<c:out value="${myList}" />,如果页面显示[],说明是空列表;如果什么都没有,则可能是null。 - 加强前端判断: 正如文中多次强调的,养成使用
<c:if test="${not empty myList}">包裹循环的习惯,这样,即使列表为空,你也可以显示一条提示信息,如“暂无数据”,而不是让用户面对一片空白,这极大地提升了用户体验。