JSP服务器端脚本语言入门:从中文乱码排查到核心标签使用实战教程
一、为什么选择JSP?先说说它的身份
JSP的全称是JavaServer Pages,它是Java生态里专门为解决”网页动态内容”而生的一种技术。你可以把它想象成”嵌入了Java代码的HTML文件”。
当浏览器请求一个JSP页面时,Web服务器(比如Tomcat)会先把这个JSP翻译成一个Java Servlet,然后编译成class文件执行,最终把结果以HTML形式返回给浏览器。整个过程对开发者来说几乎是透明的,但你只需要记住这个流程——它能帮你排查很多莫名其妙的问题。
二、中文乱码:JSP新手的”第一道坎”
几乎所有刚接触JSP的同学,都会遇到中文乱码问题。页面显示”䏿–‡”这种鬼样子,浏览器控制台一片茫然。
乱码的本质是什么?
乱码不是因为JSP有问题,而是字符编码不一致导致的。具体来说:
- JSP文件本身的编码(比如GBK)
- Tomcat处理JSP时的默认编码
- HTTP响应头的Content-Type声明的编码
- 浏览器的解析编码
这四处只要有一个对不上,就会出现乱码。
实战排查:一个完整案例
创建一个名为 encoding_test.jsp 的文件,放在Tomcat的webapps目录下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
这是一个用于排查编码问题的JSP页面
请将本文件保存为UTF-8编码格式
--%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>编码测试页</title>
</head>
<body>
<h2>中文测试页面</h2>
<p>这是一段中文:你好,JSP!欢迎学习Java Web开发。</p>
<%-- 输出Java变量中的中文 --%>
<%
String message = "测试成功!编码统一了。";
out.println("<p>Java变量输出:" + message + "</p>");
// 模拟从数据库获取的中文
String dbData = "数据库中的中文数据:用户名称:张三,邮箱:zhangsan@example.com";
out.println("<p>数据库模拟数据:" + dbData + "</p>");
%>
<hr>
<h3>表单提交测试</h3>
<form method="POST" action="encoding_test.jsp">
<label>请输入中文内容:</label>
<input type="text" name="userName" placeholder="例如:张三" />
<input type="submit" value="提交" />
</form>
<%-- 处理POST请求的中文 --%>
<%
request.setCharacterEncoding("UTF-8");
String submittedName = request.getParameter("userName");
if (submittedName != null && !submittedName.isEmpty()) {
out.println("<p style='color:green;'>你提交的内容是:" + submittedName + "</p>");
}
%>
</body>
</html>
四步解决乱码,一套公式走天下
第一处:JSP页面头部声明(最重要!)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page pageEncoding="UTF-8" %>
contentType控制HTTP响应的Content-Type头pageEncoding控制JSP文件本身的编码
第二处:HTML的meta标签
<meta charset="UTF-8">
第三处:Tomcat的server.xml配置
打开Tomcat的 conf/server.xml,找到Connector配置:
<Connector port="8080"
protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
这里 URIEncoding="UTF-8" 是关键,它解决了URL参数中的中文乱码问题。
第四处:Java代码中强制设置编码
// 处理POST请求参数
request.setCharacterEncoding("UTF-8");
// 设置响应编码(备选方案,优先级低于page指令)
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
编码问题排查流程图
页面出现乱码
│
├─ 第一步:检查JSP文件保存编码是否为UTF-8
│ (IDE中右下角状态栏可以看到当前编码)
│
├─ 第二步:检查page指令是否包含charset="UTF-8"
│ (contentType和pageEncoding两个都要有)
│
├─ 第三步:检查Tomcat的server.xml
│ (URIEncoding="UTF-8")
│
├─ 第四步:检查数据库连接编码
│ (JDBC URL中加 ?characterEncoding=UTF-8)
│
└─ 第五步:检查浏览器编码
(F12开发者工具 -> Network -> Response Headers)
数据库连接的编码配置
连接MySQL时也要指定编码,否则从数据库取出的中文也会乱码:
<!-- JDBC连接字符串 -->
jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
// 如果用JNDI配置数据源
<Resource name="jdbc/MyDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="50"
maxIdle="10"
url="jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
username="root"
password="yourpassword"
driverClassName="com.mysql.cj.jdbc.Driver"/>
三、JSP核心标签完全指南
3.1 表达式语言(EL)—— 告别繁琐的Java代码
EL表达式是JSP 2.0引入的特性,目的是让页面开发更简洁,减少Java代码的嵌入。
基本语法和用法
<%-- JSP脚本方式(老式写法)--%>
<%
User user = (User) request.getAttribute("user");
out.println(user.getName());
%>
<%-- EL表达式(推荐写法)--%>
${user.name}
<%-- 带默认值 --%>
${user.name != null ? user.name : '匿名用户'}
<%-- 等价写法,更简洁 --%>
${user.name}
<%-- 获取集合元素 --%>
${users[0].name}
${users[0]['name']}
<%-- 遍历List --%>
<c:forEach var="u" items="${users}">
<p>姓名:${u.name},年龄:${u.age}</p>
</c:forEach>
<%-- 遍历Map --%>
<c:forEach var="entry" items="${userMap}">
<p>Key: ${entry.key}, Value: ${entry.value}</p>
</c:forEach>
EL内置对象速查表
| 内置对象 | 说明 | 示例 |
|---|---|---|
| pageContext | 当前页面的上下文 | ${pageContext.request.contextPath} |
| pageScope | page域中的属性 | ${pageScope.username} |
| requestScope | request域中的属性 | ${requestScope.user} |
| sessionScope | session域中的属性 | ${sessionScope.cart} |
| applicationScope | application域中的属性 | ${applicationScope.appName} |
| param | 请求参数(单值) | ${param.username} |
| paramValues | 请求参数(多值) | ${paramValues.hobby} |
| header | 请求头(单值) | ${header['User-Agent']} |
| headerValues | 请求头(多值) | ${headerValues['Accept']} |
| cookie | Cookie信息 | ${cookie.JSESSIONID.value} |
| initParam | 上下文初始化参数 | ${initParam.dbUrl} |
EL运算符
<%-- 算术运算 --%>
${10 + 5} <!-- 输出: 15 -->
${10 - 5} <!-- 输出: 5 -->
${10 * 5} <!-- 输出: 50 -->
${10 / 5} <!-- 输出: 2.0 (注意:结果是浮点) -->
${10 % 3} <!-- 输出: 1 -->
<%-- 比较运算 --%>
${a == b} <!-- 等价于 a eq b -->
${a != b} <!-- 等价于 a ne b -->
${a < b} <!-- 等价于 a lt b -->
${a <= b} <!-- 等价于 a le b -->
${a > b} <!-- 等价于 a gt b -->
${a >= b} <!-- 等价于 a ge b -->
<%-- 逻辑运算 --%>
${a && b} <!-- 等价于 a and b -->
${a || b} <!-- 等价于 a or b -->
${!a} <!-- 等价于 not a -->
<%-- 空值判断(非常实用) --%>
${empty user} <!-- 判断是否为null或空字符串 -->
${not empty users} <!-- 判断是否不为空 -->
3.2 JSTL核心标签库 —— 页面逻辑的神器
JSTL(JSP Standard Tag Library)是JSP的标准标签库,分为核心标签、格式化标签、SQL标签、XML标签等。最常用的是核心标签库。
导入JSTL
在JSP文件顶部添加:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
如果是Maven项目,需要在 pom.xml 中添加依赖:
<dependencies>
<!-- JSTL核心标签库 -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- 如果需要API(仅compile时需要,runtime由容器提供) -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
3.2.1 <c:out> —— 安全输出
<%-- 基本输出 --%>
<c:out value="${user.name}" />
<%-- 指定默认值(当value为null时显示) --%>
<c:out value="${user.nickname}" default="匿名用户" />
<%-- 防止XSS攻击(转义HTML标签) --%>
<c:out value="${user.bio}" escapeXml="true" />
<%-- 相当于HTML中的<p>会被转义 --%>
<c:out value="<script>alert('xss')</script>" />
<!-- 输出: <script>alert('xss')</script> -->
3.2.2 <c:set> —— 设置变量
<%-- 设置page域变量 --%>
<c:set var="username" value="张三" scope="page" />
<%-- 设置request域变量 --%>
<c:set var="orderTotal" value="99.99" scope="request" />
<%-- 设置session域变量(购物车常用) --%>
<c:set var="cart" value="${sessionScope.cart}" scope="session" />
<%-- 设置对象属性 --%>
<c:set target="${user}" property="name" value="李四" />
<%-- 使用body内容设置值 --%>
<c:set var="greeting" scope="request">
欢迎,${username}!
</c:set>
3.2.3 <c:remove> —— 删除变量
<%-- 删除指定作用域的变量 --%>
<c:remove var="username" scope="session" />
<%-- 删除所有作用域的变量 --%>
<c:remove var="tempData" />
3.2.4 <c:if> —— 条件判断
<%-- 基本if判断 --%>
<c:if test="${user.age >= 18}">
<p>您已成年,可以访问此内容。</p>
</c:if>
<%-- 多条件判断 --%>
<c:if test="${not empty cart and cart.total > 100}">
<p>您的订单金额超过100元,享受免运费!</p>
</c:if>
<%-- 复杂逻辑 --%>
<c:if test="${param.action == 'delete' and user.isAdmin}">
<p>确认删除?</p>
<a href="delete.do?id=${param.id}">确认</a>
</c:if>
3.2.5 <c:choose> / <c:when> / <c:otherwise> —— 多条件分支
<%-- 成绩等级判断 --%>
<c:choose>
<c:when test="${score >= 90}">
<span style="color:green;">优秀</span>
</c:when>
<c:when test="${score >= 80}">
<span style="color:blue;">良好</span>
</c:when>
<c:when test="${score >= 60}">
<span style="color:orange;">及格</span>
</c:when>
<c:otherwise>
<span style="color:red;">不及格</span>
</c:otherwise>
</c:choose>
<%-- 用户状态显示 --%>
<c:choose>
<c:when test="${sessionScope.user ne null}">
<span>欢迎回来,${sessionScope.user.name}</span>
<a href="logout.do">退出登录</a>
</c:when>
<c:otherwise>
<span>您尚未登录</span>
<a href="login.do">登录</a>
<a href="register.do">注册</a>
</c:otherwise>
</c:choose>
3.2.6 <c:forEach> —— 循环遍历
<%-- 遍历List集合 --%>
<c:forEach var="product" items="${productList}">
<div class="product-card">
<h3>${product.name}</h3>
<p>价格:¥${product.price}</p>
<p>库存:${product.stock}件</p>
</div>
</c:forEach>
<%-- 带索引的遍历 --%>
<c:forEach var="item" items="${cartItems}" varStatus="status">
<tr>
<td>${status.count}</td> <!-- 从1开始的序号 -->
<td>${status.index}</td> <!-- 从0开始的索引 -->
<td>${item.name}</td>
<td>${item.price}</td>
<td>
<c:if test="${status.last}">
<strong>合计:¥${item.total}</strong>
</c:if>
</td>
</tr>
</c:forEach>
<%-- 遍历Map --%>
<c:forEach var="entry" items="${userMap}">
<p>用户ID: ${entry.key}, 用户名: ${entry.value.name}</p>
</c:forEach>
<%-- 数值范围循环 --%>
<c:forEach var="i" begin="1" end="10" step="2">
<span>${i} </span>
</c:forEach>
<!-- 输出: 1 3 5 7 9 -->
<%-- 遍历数组 --%>
<c:forEach var="color" items="${colors}">
<span style="background-color:${color}; width:30px; height:30px; display:inline-block; margin:2px;"></span>
</c:forEach>
varStatus属性详解:
| 属性 | 说明 | 示例 |
|---|---|---|
count |
当前迭代的次数(从1开始) | ${status.count} |
index |
当前迭代的索引(从0开始) | ${status.index} |
first |
是否为第一个元素 | ${status.first} |
last |
是否为最后一个元素 | ${status.last} |
begin |
begin属性的值 | ${status.begin} |
end |
end属性的值 | ${status.end} |
step |
step属性的值 | ${status.step} |
3.2.7 <c:forTokens> —— 字符串分割循环
<%-- 分割逗号分隔的字符串 --%>
<c:forTokens items="苹果,香蕉,橙子,葡萄" delims="," var="fruit">
<p>水果:${fruit}</p>
</c:forTokens>
<%-- 分割路径 --%>
<c:forTokens items="/usr/local/bin" delims="/" var="pathPart" begin="1">
<p>路径段:${pathPart}</p>
</c:forTokens>
3.2.8 <c:import> —— 包含外部资源
<%-- 包含本地JSP文件 --%>
<c:import url="/include/header.jsp" />
<c:import url="/include/footer.jsp" />
<%-- 包含远程URL(需要配置catalina.properties允许) --%>
<c:import url="http://example.com/weather.jsp" charEncoding="UTF-8">
<c:param name="city" value="北京" />
</c:import>
<%-- 将包含的内容存入变量 --%>
<c:import url="/include/sidebar.jsp" var="sidebar" />
${sidebar}
3.2.9 <c:redirect> 和 <c:url> —— 重定向和URL处理
<%-- URL处理(自动追加上下文路径和会话ID) --%>
<a href="<c:url value='/shop/product.do?id=1001' />">查看商品</a>
<%-- URL带参数 --%>
<c:url value="/search.do" var="searchUrl">
<c:param name="keyword" value="手机" />
<c:param name="page" value="1" />
</c:url>
<a href="${searchUrl}">搜索</a>
<%-- 重定向 --%>
<c:redirect url="/login.do" />
<%-- 重定向并带参数 --%>
<c:redirect url="/home.do">
<c:param name="msg" value="登录成功" />
</c:redirect>
<%-- 带上下文的URL(解决路径问题) --%>
<img src="<c:url value='/images/logo.png' />" alt="Logo" />
3.3 完整实战案例:用户管理列表页面
下面是一个综合了上述所有知识点的完整案例:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%--
用户管理列表页面
展示需求:
1. 分页显示用户列表
2. 支持按状态筛选
3. 显示用户注册时间的格式化日期
4. 根据用户等级显示不同样式
5. 处理可能的中文乱码问题
--%>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理列表</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Microsoft YaHei', Arial, sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; margin-bottom: 20px; }
/* 筛选栏 */
.filter-bar {
background: #fff; padding: 15px; border-radius: 8px;
margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.filter-bar select, .filter-bar input {
padding: 8px 12px; margin-right: 10px; border: 1px solid #ddd;
border-radius: 4px;
}
.filter-bar button {
padding: 8px 20px; background: #4CAF50; color: #fff;
border: none; border-radius: 4px; cursor: pointer;
}
.filter-bar button:hover { background: #45a049; }
/* 表格 */
table {
width: 100%; border-collapse: collapse;
background: #fff; border-radius: 8px;
overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
th, td {
padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee;
}
th { background: #4CAF50; color: #fff; font-weight: bold; }
tr:hover { background: #f9f9f9; }
/* 状态标签 */
.status-badge {
padding: 4px 10px; border-radius: 12px; font-size: 12px;
}
.status-active { background: #d4edda; color: #155724; }
.status-inactive { background: #f8d7da; color: #721c24; }
.status-banned { background: #fff3cd; color: #856404; }
/* 等级标签 */
.level-badge {
padding: 4px 10px; border-radius: 12px; font-size: 12px;
}
.level-vip { background: #ffeaa7; color: #856404; }
.level-normal { background: #dfe6e9; color: #2d3436; }
.level-new { background: #74b9ff; color: #fff; }
/* 分页 */
.pagination {
margin-top: 20px; text-align: center;
}
.pagination a, .pagination span {
display: inline-block; padding: 8px 14px; margin: 0 4px;
border: 1px solid #ddd; border-radius: 4px; text-decoration: none;
color: #333;
}
.pagination a:hover { background: #4CAF50; color: #fff; border-color: #4CAF50; }
.pagination .current { background: #4CAF50; color: #fff; border-color: #4CAF50; }
/* 空状态 */
.empty-state {
text-align: center; padding: 40px; color: #999;
}
.empty-state i { font-size: 48px; margin-bottom: 10px; }
/* 统计信息 */
.stats-bar {
background: #fff; padding: 15px; border-radius: 8px;
margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex; justify-content: space-around;
}
.stat-item { text-align: center; }
.stat-number { font-size: 24px; font-weight: bold; color: #4CAF50; }
.stat-label { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>👥 用户管理列表</h1>
<%-- 统计信息区域 --%>
<div class="stats-bar">
<div class="stat-item">
<div class="stat-number">${stats.totalUsers}</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-item">
<div class="stat-number" style="color:#4CAF50">${stats.activeUsers}</div>
<div class="stat-label">活跃用户</div>
</div>
<div class="stat-item">
<div class="stat-number" style="color:#e74c3c">${stats.inactiveUsers}</div>
<div class="stat-label"> inactive用户</div>
</div>
<div class="stat-item">
<div class="stat-number" style="color:#f39c12">${stats.vipUsers}</div>
<div class="stat-label">VIP用户</div>
</div>
</div>
<%-- 筛选栏 --%>
<div class="filter-bar">
<form method="GET" action="${pageContext.request.contextPath}/user/list.do">
<select name="status">
<option value="">全部状态</option>
<option value="ACTIVE" <c:if test="${param.status == 'ACTIVE'}">selected</c:if>>活跃</option>
<option value="INACTIVE" <c:if test="${param.status == 'INACTIVE'}">selected</c:if>>不活跃</option>
<option value="BANNED" <c:if test="${param.status == 'BANNED'}">selected</c:if>>已封禁</option>
</select>
<select name="level">
<option value="">全部等级</option>
<option value="VIP" <c:if test="${param.level == 'VIP'}">selected</c:if>>VIP</option>
<option value="NORMAL" <c:if test="${param.level == 'NORMAL'}">selected</c:if>>普通</option>
<option value="NEW" <c:if test="${param.level == 'NEW'}">selected</c:if>>新用户</option>
</select>
<input type="text" name="keyword" placeholder="搜索用户名..."
value="${param.keyword != null ? param.keyword : ''}" />
<button type="submit">筛选</button>
<a href="${pageContext.request.contextPath}/user/list.do"
style="margin-left:10px;padding:8px 20px;background:#999;color:#fff;
text-decoration:none;border-radius:4px;">重置</a>
</form>
</div>
<%-- 用户列表表格 --%>
<table>
<thead>
<tr>
<th>序号</th>
<th>用户ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>等级</th>
<th>状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<%-- 判断是否有数据 --%>
<c:choose>
<c:when test="${empty userPage.list}">
<tr>
<td colspan="8">
<div class="empty-state">
<p>暂无用户数据</p>
<p>点击"添加用户"创建第一个用户</p>
</div>
</td>
</tr>
</c:when>
<c:otherwise>
<%-- 遍历用户列表 --%>
<c:forEach var="user" items="${userPage.list}" varStatus="status">
<tr>
<td>${status.count + (userPage.currentPage - 1) * userPage.pageSize}</td>
<td>${user.id}</td>
<td>
<c:out value="${user.username}" escapeXml="true" />
<c:if test="${user.isVip}">
<span style="color:#f39c12;font-size:12px;">⭐</span>
</c:if>
</td>
<td>${user.email}</td>
<td>
<c:choose>
<c:when test="${user.level == 'VIP'}">
<span class="level-badge level-vip">VIP</span>
</c:when>
<c:when test="${user.level == 'NEW'}">
<span class="level-badge level-new">新手</span>
</c:when>
<c:otherwise>
<span class="level-badge level-normal">普通</span>
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="${user.status == 'ACTIVE'}">
<span class="status-badge status-active">活跃</span>
</c:when>
<c:when test="${user.status == 'INACTIVE'}">
<span class="status-badge status-inactive">inactive</span>
</c:when>
<c:otherwise>
<span class="status-badge status-banned">封禁</span>
</c:otherwise>
</c:choose>
</td>
<td>
<fmt:formatDate value="${user.registerTime}"
pattern="yyyy-MM-dd HH:mm:ss" />
</td>
<td>
<a href="${pageContext.request.contextPath}/user/edit.do?id=${user.id}">编辑</a>
<c:if test="${not user.isSystem}">
<a href="${pageContext.request.contextPath}/user/delete.do?id=${user.id}"
onclick="return confirm('确定要删除用户${user.username}吗?')">删除</a>
</c:if>
</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
<%-- 分页控件 --%>
<c:if test="${not empty userPage}">
<div class="pagination">
<c:if test="${userPage.currentPage > 1}">
<a href="${pageContext.request.contextPath}/user/list.do?page=1&status=${param.status}&level=${param.level}&keyword=${param.keyword}">首页</a>
<a href="${pageContext.request.contextPath}/user/list.do?page=${userPage.currentPage - 1}&status=${param.status}&level=${param.level}&keyword=${param.keyword}">上一页</a>
</c:if>
<c:forEach var="i" begin="1" end="${userPage.totalPages}" step="1">
<c:choose>
<c:when test="${i == userPage.currentPage}">
<span class="current">${i}</span>
</c:when>
<c:otherwise>
<a href="${pageContext.request.contextPath}/user/list.do?page=${i}&status=${param.status}&level=${param.level}&keyword=${param.keyword}">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${userPage.currentPage < userPage.totalPages}">
<a href="${pageContext.request.contextPath}/user/list.do?page=${userPage.currentPage + 1}&status=${param.status}&level=${param.level}&keyword=${param.keyword}">下一页</a>
<a href="${pageContext.request.contextPath}/user/list.do?page=${userPage.totalPages}&status=${param.status}&level=${param.level}&keyword=${param.keyword}">末页</a>
</c:if>
<span style="margin-left:20px;color:#666;">
共 ${userPage.totalPages} 页,${userPage.totalCount} 条记录
</span>
</div>
</c:if>
</div>
</body>
</html>
对应的后端Controller示例(Java):
package com.example.web.controller;
import com.example.model.User;
import com.example.model.UserPage;
import com.example.model.UserStats;
import com.example.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
/**
* 用户列表页面
* @param page 页码,默认第1页
* @param status 状态筛选
* @param level 等级筛选
* @param keyword 关键词搜索
* @param request HTTP请求对象
* @return ModelAndView
*/
@GetMapping("/list")
public ModelAndView list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(required = false) String status,
@RequestParam(required = false) String level,
@RequestParam(required = false) String keyword,
HttpServletRequest request) {
// 关键:处理POST请求的中文编码
request.setCharacterEncoding("UTF-8");
// 查询用户列表
int pageSize = 10;
UserPage userPage = userService.queryUserPage(page, pageSize, status, level, keyword);
// 查询统计信息
UserStats stats = userService.queryUserStats();
ModelAndView mav = new ModelAndView("user/list");
mav.addObject("userPage", userPage);
mav.addObject("stats", stats);
// 将筛选参数放入request域,方便页面回显
Map<String, String> filterParams = new HashMap<>();
if (status != null) filterParams.put("status", status);
if (level != null) filterParams.put("level", level);
if (keyword != null) filterParams.put("keyword", keyword);
mav.addObject("filterParams", filterParams);
return mav;
}
}
3.4 <c:param> —— 参数传递
<%-- 在<c:url>中传递参数 --%>
<c:url value="/search.do" var="searchUrl">
<c:param name="keyword" value="Java编程" />
<c:param name="category" value="技术" />
</c:url>
<a href="${searchUrl}">搜索</a>
<%-- 在<c:import>中传递参数 --%>
<c:import url="/include/sidebar.jsp">
<c:param name="section" value="user" />
<c:param name="lang" value="zh" />
</c:import>
<%-- 在<c:redirect>中传递参数 --%>
<c:redirect url="/login.do">
<c:param name="redirectUrl" value="${pageContext.request.requestURI}" />
</c:redirect>
<%-- 编码处理(特殊字符自动编码) --%>
<c:url value="/search.do">
<c:param name="keyword" value="C++ & Java" />
</c:url>
<!-- 会自动编码为: /search.do?keyword=C%2B%2B%20%26%20Java -->
四、JSP脚本元素速查
虽然EL和JSTL已经能解决大部分问题,但有时仍需要用到JSP的脚本元素:
4.1 三种脚本元素
| 元素 | 语法 | 用途 |
|---|---|---|
| 脚本段 | <% Java代码 %> |
执行Java逻辑 |
| 表达式 | <%= Java表达式 %> |
输出Java表达式结果 |
| 声明 | <%! Java方法/变量 %> |
声明方法或成员变量 |
4.2 实际使用场景
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.Date" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%-- 声明方法(在类级别定义) --%>
<%!
private String getGreeting(String name) {
return "你好," + name + "!";
}
private int calculateAge(int birthYear) {
return new Date().getYear() + 1900 - birthYear;
}
%>
<%-- 脚本段:处理Java逻辑 --%>
<%
String username = "张三";
String greeting = getGreeting(username);
// 将结果存入request域,供EL使用
request.setAttribute("greeting", greeting);
// 计算年龄
int age = calculateAge(1990);
request.setAttribute("age", age);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSP脚本元素示例</title>
</head>
<body>
<h2>JSP脚本元素示例</h2>
<%-- 使用EL表达式输出(推荐) --%>
<p>问候语:${greeting}</p>
<p>年龄:${age} 岁</p>
<%-- 使用表达式直接输出(兼容老版本) --%>
<p>当前时间:<%= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) %></p>
<%-- 条件判断 --%>
<%
if (age >= 18) {
%>
<p>您已成年,可以访问所有内容。</p>
<%
} else {
%>
<p>您尚未成年,部分功能受限。</p>
<%
}
%>
<%-- 循环示例 --%>
<h3>倒计时:</h3>
<%
for (int i = 5; i > 0; i--) {
%>
<span style="color:red;font-weight:bold;"><%= i %></span>
<%
}
%>
<span> GO!</span>
</body>
</html>
五、最佳实践与常见陷阱
5.1 推荐的JSP开发规范
<%--
推荐的文件头规范:
1. contentType和pageEncoding都要指定
2. 引入必要的taglib
3. 使用UTF-8编码
4. 开启EL表达式支持(JSP 2.0+默认开启)
--%>
<%@ page contentType="text/html;charset=UTF-8"
language="java"
pageEncoding="UTF-8"
isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
5.2 常见陷阱与解决方案
陷阱1:EL表达式不生效
<%-- 症状:${user.name} 原样输出,没有被解析 --%>
<%-- 解决方案: --%>
<%-- 1. 检查JSP版本,确保是2.0以上 --%>
<%-- 2. 检查web.xml中的配置 --%>
web.xml 配置(如果是较老的Servlet容器):
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>false</el-ignored>
<page-encoding>UTF-8</page-encoding>
</jsp-property-group>
</jsp-config>
</web-app>
陷阱2:JSTL标签不识别
<%-- 症状:<c:forEach> 标签无法识别 --%>
<%-- 解决方案: --%>
<%-- 1. 确认lib目录下有jstl.jar和standard.jar --%>
<%-- 2. 确认taglib URI正确 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%-- Maven依赖: --%>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
陷阱3:中文参数丢失或乱码
// 错误做法:只设置了response编码
response.setContentType("text/html;charset=UTF-8");
// 但没有设置request编码,POST请求的中文参数会乱码
// 正确做法:同时设置request和response编码
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
陷阱4:el表达式与脚本变量名冲突
<%
String username = "张三";
request.setAttribute("username", username);
%>
<%-- 以下两种写法效果相同,优先查找request域 --%>
${username}
<%= request.getAttribute("username") %>
<%-- 强制指定作用域查找 --%>
${requestScope.username}
${sessionScope.username}
${applicationScope.username}
${pageScope.username}
陷阱5:forEach中变量作用域混乱
<%-- 错误:在forEach外部访问varStatus --%>
<c:forEach var="item" items="${list}" varStatus="status">
<p>${item.name}</p>
</c:forEach>
<p>${status.index}</p> <!-- 这里访问可能得到期望外的结果 -->
<%-- 正确:在forEach内部使用varStatus --%>
<c:forEach var="item" items="${list}" varStatus="status">
<p>
序号:${status.index}
名称:${item.name}
<c:if test="${status.last}">(最后一条)</c:if>
</p>
</c:forEach>
5.3 性能优化建议
<%-- 1. 尽量减少JSP脚本的使用,多用EL和JSTL --%>
<%-- 2. 使用<jsp:include>替代静态包含,避免重复代码 --%>
<%-- 3. 对于大量数据的列表,务必分页显示 --%>
<%-- 4. 使用<c:choose>替代多层嵌套的<c:if> --%>
<%-- 5. 缓存常用的静态内容 --%>
<%-- 静态包含 vs 动态包含 --%>
<%-- 静态包含:编译时插入,速度快,但无法传递参数 --%>
<%@ include file="/include/header.jsp" %>
<%-- 动态包含:运行时执行,可以传递参数,更灵活 --%>
<jsp:include page="/include/header.jsp">
<jsp:param name="title" value="用户管理" />
</jsp:include>
六、调试技巧:如何让JSP页面”说话”
6.1 查看页面翻译结果
Tomcat默认会将JSP翻译成Java源代码,保存在工作目录下:
Tomcat安装目录/work/Catalina/localhost/你的项目名/org/apache/jsp/
找到对应的 .java 文件,可以看到JSP被翻译成的Servlet代码,这对理解JSP工作原理非常有帮助。
6.2 使用浏览器开发者工具排查
F12打开开发者工具
→ Network标签
→ 找到对应的JSP请求
→ 查看Response Headers中的Content-Type
→ 确认是否包含 charset=UTF-8
→ 查看Response Body确认编码是否正确
6.3 打印调试信息
<%-- 在JSP中添加调试信息(开发环境使用) --%>
<%
if (application.getInitParameter("debugMode") != null
&& "true".equals(application.getInitParameter("debugMode"))) {
%>
<div style="background:#fff3cd;padding:10px;margin:10px 0;border:1px solid #ffc107;">
<strong>调试信息:</strong><br>
当前用户:${sessionScope.user != null ? sessionScope.user.name : '未登录'}<br>
请求参数:
<ul>
<c:forEach var="entry" items="${paramMap}">
<li>${entry.key} = ${entry.value[0]}</li>
</c:forEach>
</ul>
请求头:
<ul>
<c:forEach var="entry" items="${headerMap}">
<li>${entry.key} = ${entry.value}</li>
</c:forEach>
</ul>
</div>
<%
}
%>
七、从JSP到现代开发的思考
JSP作为Java Web开发的重要里程碑,虽然现在有了Spring Boot + Thymeleaf、前后端分离等更现代的架构,但理解JSP仍然非常重要:
- 理解Servlet容器的工作机制——JSP本质上是Servlet的另一种写法
- 解决遗留系统维护——很多老系统仍然在使用JSP
- 理解HTTP协议和Web请求流程
- EL和JSTL的设计思想在现代模板引擎中仍然可以看到影子
如果你正在学习Java Web开发,建议按照以下顺序:
HTML/CSS/JavaScript(前端基础)
↓
Java SE(编程基础)
↓
Servlet/JSP(Web基础)
↓
JDBC/数据库(数据层)
↓
Spring/Spring MVC(现代框架)
↓
Spring Boot + 前后端分离(当前主流)
希望这篇教程能帮助你顺利跨过JSP的入门门槛。中文乱码是每个JSP开发者的”成人礼”,跨过这一关,后面的路会顺畅很多。记住,编码问题本质上是”不一致”的问题,找到不一致的地方,问题就解决了一半。
