1. JSTL核心标签库概述
在Java Web开发中,JSTL(JSP Standard Tag Library)标签库是每个开发者必须掌握的核心技能之一。我第一次接触JSTL是在一个电商项目的数据展示模块,当时需要循环显示商品列表,传统的JSP脚本<% for(int i=0; i<list.size(); i++) { %>让页面变得难以维护,而JSTL的c:forEach标签完美解决了这个问题。
JSTL标签库主要分为四大类:
- 核心标签库(core):包含流程控制、循环、变量操作等基础功能
- 格式化标签库(fmt):处理日期、数字的格式化
- SQL标签库(sql):数据库操作(已过时)
- XML标签库(xml):XML处理(已过时)
要使用JSTL,首先需要在项目中引入jstl-1.2.jar包,然后在JSP页面顶部添加taglib指令:
jsp复制<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
这里的prefix可以自定义,但行业惯例通常使用"c"作为核心标签库的前缀。
2. c:forEach标签详解
2.1 基础语法与属性
c:forEach是JSTL中最常用的循环标签,它的基本语法结构如下:
jsp复制<c:forEach
items="${collection}"
var="item"
varStatus="status"
begin="startIndex"
end="endIndex"
step="stepSize">
<!-- 循环体内容 -->
</c:forEach>
主要属性说明:
- items:要遍历的集合或数组,通常使用EL表达式指定
- var:当前迭代项的变量名
- varStatus:循环状态对象(可选)
- begin:起始索引(从0开始)
- end:结束索引
- step:步长(默认为1)
2.2 遍历不同类型集合
2.2.1 遍历List集合
jsp复制<%
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
pageContext.setAttribute("nameList", names);
%>
<c:forEach items="${nameList}" var="name">
<p>姓名:${name}</p>
</c:forEach>
实际项目中,我们经常从Servlet传递List到JSP页面。例如在Spring MVC中:
java复制model.addAttribute("products", productService.getAllProducts());
然后在JSP中:
jsp复制<c:forEach items="${products}" var="product">
<div class="product-item">
<h3>${product.name}</h3>
<p>价格:¥${product.price}</p>
</div>
</c:forEach>
2.2.2 遍历Map集合
jsp复制<%
Map<String, Object> student = new LinkedHashMap<>();
student.put("id", "S1001");
student.put("name", "赵六");
student.put("age", 21);
pageContext.setAttribute("student", student);
%>
<c:forEach items="${student}" var="entry">
<p>${entry.key}: ${entry.value}</p>
</c:forEach>
在真实项目场景中,Map遍历常用于展示键值对配置信息或JSON格式数据。
2.2.3 遍历数组
jsp复制<%
String[] colors = {"Red", "Green", "Blue"};
pageContext.setAttribute("colorArray", colors);
%>
<c:forEach items="${colorArray}" var="color" varStatus="status">
<p>第${status.index+1}种颜色:${color}</p>
</c:forEach>
2.3 数值范围循环
c:forEach不仅可以遍历集合,还能像传统for循环一样工作:
jsp复制<c:forEach var="i" begin="1" end="10" step="2">
${i}
</c:forEach>
输出结果:1 3 5 7 9
这在生成分页导航、日历等需要数字序列的场景非常有用。
3. varStatus的妙用
varStatus属性提供了循环状态的访问能力,它包含以下属性:
- index:当前迭代的索引(从0开始)
- count:当前迭代的计数(从1开始)
- first:是否为第一次迭代
- last:是否为最后一次迭代
- current:当前项(同var)
3.1 表格隔行变色
jsp复制<table>
<c:forEach items="${userList}" var="user" varStatus="vs">
<tr class="${vs.index % 2 == 0 ? 'even' : 'odd'}">
<td>${vs.count}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</c:forEach>
</table>
3.2 特殊标记首尾项
jsp复制<ul>
<c:forEach items="${categoryList}" var="category" varStatus="vs">
<c:if test="${vs.first}">
<li class="first-item">★ ${category.name}</li>
</c:if>
<c:if test="${not vs.first and not vs.last}">
<li>${category.name}</li>
</c:if>
<c:if test="${vs.last}">
<li class="last-item">${category.name} ★</li>
</c:if>
</c:forEach>
</ul>
4. 性能优化与最佳实践
4.1 避免在循环内执行耗时操作
错误示范:
jsp复制<c:forEach items="${orderList}" var="order">
<%
// 在循环内调用服务方法 - 性能杀手!
List<OrderItem> items = orderService.getItems(order.getId());
pageContext.setAttribute("items", items);
%>
<!-- 显示订单项 -->
</c:forEach>
正确做法是在Servlet中预先加载所有需要的数据:
java复制List<Order> orders = orderService.getAllOrders();
for(Order order : orders) {
order.setItems(orderService.getItems(order.getId()));
}
request.setAttribute("orderList", orders);
4.2 合理使用begin/end实现分页
jsp复制<c:forEach items="${allProducts}" var="product"
begin="${(currentPage-1)*pageSize}"
end="${currentPage*pageSize-1}">
<!-- 显示当前页产品 -->
</c:forEach>
不过更推荐在服务端完成分页,只传递当前页数据到JSP。
4.3 嵌套循环的注意事项
嵌套循环时,内层循环的varStatus变量名需要与外层不同:
jsp复制<c:forEach items="${departments}" var="dept" varStatus="deptStatus">
<h3>${dept.name}</h3>
<ul>
<c:forEach items="${dept.employees}" var="emp" varStatus="empStatus">
<li>${empStatus.count}. ${emp.name}</li>
</c:forEach>
</ul>
</c:forEach>
5. 常见问题排查
5.1 集合为null导致的异常
当items为null时,c:forEach不会报错,而是什么都不做。但有时我们需要明确提示:
jsp复制<c:choose>
<c:when test="${empty userList}">
<p class="alert">暂无用户数据</p>
</c:when>
<c:otherwise>
<c:forEach items="${userList}" var="user">
<!-- 正常显示 -->
</c:forEach>
</c:otherwise>
</c:choose>
5.2 循环内变量覆盖问题
jsp复制<c:set var="total" value="0"/>
<c:forEach items="${cart.items}" var="item">
<c:set var="total" value="${total + item.price * item.quantity}"/>
</c:forEach>
总价:${total}
注意这里使用的是同一作用域的total变量。如果循环嵌套,可能需要使用pageScope、requestScope等明确指定作用域。
5.3 性能优化案例
在一次性能调优中,我发现一个页面加载特别慢,原因是:
jsp复制<c:forEach items="${products}" var="product">
<c:set var="discount" value="${productService.calculateDiscount(product)}"/>
<!-- 显示产品信息 -->
</c:forEach>
优化方案是在服务端批量计算折扣,避免在JSP中频繁调用服务方法。
6. 进阶应用场景
6.1 动态生成JavaScript数据
jsp复制<script>
var productData = [
<c:forEach items="${products}" var="product" varStatus="vs">
{
id: "${product.id}",
name: "${product.name}",
price: ${product.price}
}<c:if test="${not vs.last}">,</c:if>
</c:forEach>
];
</script>
6.2 与JSTL其他标签配合使用
jsp复制<c:forEach items="${students}" var="student">
<c:if test="${student.score >= 60}">
<tr class="pass">
</c:if>
<c:if test="${student.score < 60}">
<tr class="fail">
</c:if>
<td>${student.name}</td>
<td>${student.score}</td>
</tr>
</c:forEach>
6.3 自定义分隔符
jsp复制<c:forEach items="${tags}" var="tag" varStatus="vs">
${tag.name}<c:if test="${not vs.last}">, </c:if>
</c:forEach>
7. 实际项目经验分享
在电商项目中,商品列表页通常需要处理复杂的展示逻辑。我们曾经实现过一个商品列表,需求包括:
- 每行显示4个商品
- 第一行商品需要特殊样式
- 库存紧张的商品需要标红
- 新品需要显示角标
最终实现方案:
jsp复制<c:forEach items="${products}" var="product" varStatus="vs">
<c:set var="row" value="${vs.index / 4}"/>
<div class="product-item
${vs.index < 4 ? 'first-row' : ''}
${product.stock < 10 ? 'low-stock' : ''}">
<c:if test="${product.isNew}">
<span class="new-badge">新品</span>
</c:if>
<!-- 商品内容 -->
</div>
<c:if test="${(vs.index+1) % 4 == 0}">
<div class="clearfix"></div>
</c:if>
</c:forEach>
另一个经验是,在大型列表中,使用begin/end实现服务端分页比客户端分页性能更好:
jsp复制<c:forEach items="${allData}" begin="${param.start}" end="${param.end}" var="item">
<!-- 只显示当前页数据 -->
</c:forEach>
但最佳实践还是应该在服务端完成分页查询,只传递当前页数据到视图层。
