Servlet交互_作用域_EL_JSTL详解

O泡李华 14

Servlet 交互 + 作用域 + EL + JSTL 详解

本章位置:第二阶段 Java 核心框架
前置知识:Servlet、JSP、JavaBean、集合框架、HTTP 基础
本章内容:Servlet 与 JSP 数据交互、四大作用域、EL 表达式、JSTL
下一篇:Web CRUD / 项目 - MVC
学习目标:掌握 Servlet 与 JSP 的数据传递方式、四大作用域的区别、EL 表达式取值规则、JSTL 条件与循环标签,并能够完成传统 Java Web 的动态页面展示。


一、本章要解决什么问题

上一章已经会:

Servlet
↓
request.setAttribute(...)
↓
forward
↓
JSP

但是 JSP 中我们还在写:

<%
    Student student =
            (Student)
            request.getAttribute(
                    "student"
            );
%>

<%= student.getName() %>

或者:

<%
    for (
        Student student :
        students
    ) {
%>

    <p>
        <%= student.getName() %>
    </p>

<%
    }
%>

问题很明显:

Java 代码太多

HTML 被 Java 切碎

强制类型转换多

页面很难维护

所以这一章学习:

作用域
EL
JSTL

把 JSP 页面变得:

更像真正的模板页面

二、Servlet 和 JSP 如何交互

最常见流程:

Servlet 查询数据
↓
放入作用域
↓
forward 到 JSP
↓
JSP 读取数据
↓
生成 HTML

例如:

List<Student> students =
        studentService.findAll();

request.setAttribute(
        "students",
        students
);

request
        .getRequestDispatcher(
                "/WEB-INF/views/student-list.jsp"
        )
        .forward(
                request,
                response
        );

JSP:

${students}

这就是最基本的数据交互。


三、什么是作用域

作用域:

Scope

可以理解为:

一个数据能够保存多长时间、能够被哪些请求或页面访问。

JSP / Servlet 中常见四个作用域:

page

request

session

application

范围从小到大:

page
<
request
<
session
<
application

四、page 作用域

作用范围:

当前 JSP 页面

通常通过:

pageContext

保存。

例如:

<%
    pageContext.setAttribute(
            "message",
            "当前页面数据"
    );
%>

读取:

<%= pageContext.getAttribute("message") %>

五、page 作用域什么时候消失

当前 JSP 页面执行结束后:

基本就失效

它是:

范围最小的作用域

六、request 作用域

作用范围:

一次 HTTP 请求

Servlet:

request.setAttribute(
        "student",
        student
);

转发:

request
        .getRequestDispatcher(
                "/WEB-INF/views/detail.jsp"
        )
        .forward(
                request,
                response
        );

JSP 仍然能取:

request.getAttribute(
        "student"
);

七、为什么 forward 后 request 数据还在

因为 forward:

还是同一次请求

流程:

浏览器
↓
Servlet
↓
JSP

request 没换。


八、为什么 redirect 后 request 数据丢失

因为 redirect:

浏览器重新发送一次新请求

原来的:

request.setAttribute(...)

属于:

旧 request

所以新请求拿不到。


九、request 最适合什么数据

最常见:

查询结果

详情对象

表单错误信息

当前页面列表

当前请求临时数据

例如:

学生列表
商品详情
搜索结果

这些数据只需要:

当前页面展示一次

request 最适合。


十、session 作用域

session:

会话作用域

主要针对:

同一个用户的一段连续访问

例如:

HttpSession session =
        request.getSession();

保存:

session.setAttribute(
        "loginUser",
        user
);

十一、session 常见场景

例如:

登录用户

购物车

验证码状态

用户偏好

跨请求临时业务状态

十二、为什么登录用户适合 session

用户登录成功:

登录请求结束后

后面还要访问:

首页

个人中心

订单页

退出

如果只放 request:

下一次请求就没了

所以放:

session

十三、session 获取数据

User user =
        (User)
        session.getAttribute(
                "loginUser"
        );

JSP 后面可以:

${sessionScope.loginUser}

十四、session 删除数据

退出登录:

session.removeAttribute(
        "loginUser"
);

或者直接:

session.invalidate();

十五、invalidate()

session.invalidate();

表示:

让整个当前 Session 失效

常见:

退出登录

十六、session 不是永久保存

Session 通常会因为:

超时

服务器重启

主动 invalidate

会话失效

而消失。

所以:

重要业务数据

不能只存在 Session。


十七、application 作用域

application 本质:

ServletContext

获取:

ServletContext application =
        getServletContext();

保存:

application.setAttribute(
        "onlineCount",
        10
);

十八、application 范围

作用范围:

整个 Web 应用

多个用户:

多个 Session

多个请求

多个 Servlet

都可能访问同一个 application 数据。


十九、application 适合什么

例如:

应用级配置

全局统计

全局缓存

公共数据

但一定注意:

线程安全

二十、application 不适合保存登录用户

如果:

application.setAttribute(
        "loginUser",
        user
);

那么:

整个系统所有用户共享

会严重混乱。

登录用户应该:

session

二十一、四大作用域对比

作用域 对象 生命周期 常见用途
page pageContext 当前 JSP 页面内部临时数据
request request 一次请求 查询结果、错误信息
session session 用户会话 登录用户、购物车
application ServletContext 整个应用 全局配置、共享统计

二十二、作用域选择原则

推荐:

能用小作用域,就不要用大作用域。

例如:

当前页面展示学生列表

用:

request

不要:

session

更不要:

application

二十三、为什么小作用域更好

因为大作用域可能:

占内存更久

产生脏数据

状态难管理

用户之间互相影响

线程安全复杂

二十四、EL 是什么

EL:

Expression Language

中文:

表达式语言

作用:

让 JSP 更方便地读取作用域中的数据。

传统:

<%= request.getAttribute("name") %>

EL:

${name}

二十五、EL 基本语法

${表达式}

例如:

${name}

二十六、Servlet 设置数据

request.setAttribute(
        "name",
        "张三"
);

JSP:

${name}

输出:

张三

二十七、EL 默认查找作用域顺序

如果写:

${name}

EL 会按:

page
↓
request
↓
session
↓
application

顺序查找。

找到第一个就使用。


二十八、为什么最好知道明确作用域

如果多个作用域都有:

name
${name}

可能读到:

范围最近的那个

所以有时可以明确写:

${requestScope.name}

二十九、EL 四个 Scope 对象

pageScope

requestScope

sessionScope

applicationScope

例如:

${requestScope.student}

${sessionScope.loginUser}

${applicationScope.onlineCount}

三十、EL 访问 JavaBean 属性

Servlet:

Student student =
        new Student();

student.setName(
        "张三"
);

student.setAge(
        20
);

request.setAttribute(
        "student",
        student
);

JSP:

${student.name}

${student.age}

三十一、${student.name} 底层是什么

它不是直接访问:

private String name;

而是按照 JavaBean 规则:

找到 name 属性
↓
调用 getName()

所以前面学习:

JavaBean
内省
Getter

现在真正用上了。


三十二、boolean 属性

例如:

public boolean isEnabled() {

}

EL:

${user.enabled}

仍然可以按照 Bean 属性规则读取。


三十三、EL 访问 Map

Servlet:

Map<String, Object> data =
        new HashMap<>();

data.put(
        "name",
        "张三"
);

request.setAttribute(
        "data",
        data
);

JSP:

${data.name}

也可以:

${data["name"]}

三十四、点语法和中括号语法

${student.name}

适合:

普通属性名

中括号:

${data["user-name"]}

适合:

包含特殊字符
动态 key

三十五、EL 访问 List

Servlet:

List<String> names =
        List.of(
                "张三",
                "李四",
                "王五"
        );

request.setAttribute(
        "names",
        names
);

JSP:

${names[0]}

结果:

张三

三十六、EL 访问数组

String[] colors =
        {
                "red",
                "blue"
        };

JSP:

${colors[0]}

三十七、EL 取请求参数

EL 提供:

param

例如浏览器:

/search?keyword=java

JSP:

${param.keyword}

三十八、多值参数

EL:

paramValues

例如 checkbox:

${paramValues.hobby[0]}

三十九、EL 请求头

header

例如:

${header["User-Agent"]}

四十、EL Cookie

EL:

cookie

例如:

${cookie.JSESSIONID.value}

四十一、EL initParam

可以读取:

ServletContext 初始化参数

例如:

${initParam.appName}

四十二、pageContext

EL 中:

${pageContext}

是一个特殊内置对象。

非常常用:

${pageContext.request.contextPath}

四十三、contextPath

推荐 JSP 页面:

${pageContext.request.contextPath}

例如:

<form
        action="${pageContext.request.contextPath}/login"
        method="post"
>

比传统:

<%= request.getContextPath() %>

更干净。


四十四、EL 运算符

EL 支持:

算术

比较

逻辑

empty

三元

四十五、算术

${10 + 20}

${10 - 3}

${10 * 2}

${10 / 2}

${10 % 3}

四十六、比较运算

${age > 18}

${score >= 60}

${a == b}

${a != b}

也支持文字形式:

gt

ge

lt

le

eq

ne

四十七、逻辑运算

${age >= 18 && enabled}

${a || b}

${!enabled}

也可以:

and
or
not

四十八、empty

非常常用:

${empty student}

${empty students}

${empty message}

可以判断:

null

空字符串

空集合

空数组

四十九、not empty

${not empty students}

表示:

students 不为空

五十、三元表达式

${score >= 60 ? "及格" : "不及格"}

适合:

简单展示判断

不要塞太复杂的业务规则。


五十一、EL null 的特点

如果:

${student.name}

但 student 不存在,

很多情况下 EL 会:

输出空字符串

而不是像传统 Java 那样直接 NPE。

这让 JSP 页面更友好。


五十二、但不能因此忽略业务校验

页面没报错:

不代表数据正确

Servlet / Service 仍然应该:

校验数据
处理不存在

五十三、JSTL 是什么

JSTL:

JSP Standard Tag Library

中文:

JSP 标准标签库

主要用于:

条件判断

循环

变量

URL

格式化

让 JSP 少写 Java 脚本。


五十四、为什么需要 JSTL

传统 JSP:

<%
    if (
        score >= 60
    ) {
%>

    及格

<%
    } else {
%>

    不及格

<%
    }
%>

JSTL:

<c:choose>

    <c:when
            test="${score >= 60}"
    >
        及格
    </c:when>

    <c:otherwise>
        不及格
    </c:otherwise>

</c:choose>

虽然标签多一点,

但:

HTML 结构更清楚

五十五、Tomcat 8.5 中 JSTL 依赖

传统 javax Servlet 环境可以添加:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

注意:

Tomcat 8.5
通常是 javax 体系

Tomcat 10+ Jakarta 环境:

依赖坐标和标签库版本会不同

不要混用。


五十六、引入 JSTL Core 标签库

JSP 顶部:

<%@ taglib
        prefix="c"
        uri="http://java.sun.com/jsp/jstl/core"
%>

之后可以使用:

<c:if>

<c:forEach>

<c:choose>

五十七、prefix=“c”

c

只是:

标签前缀

所以:

<c:if>

表示:

使用 JSTL Core 标签

五十八、c:set

设置变量:

<c:set
        var="name"
        value="张三"
/>

读取:

${name}

五十九、c:set 指定作用域

<c:set
        var="name"
        value="张三"
        scope="request"
/>

scope 可以:

page

request

session

application

六十、c:remove

删除变量:

<c:remove
        var="name"
        scope="session"
/>

六十一、c:if

条件判断:

<c:if
        test="${student.score >= 60}"
>

    <span>
        及格
    </span>

</c:if>

六十二、c:if 没有 else

JSTL 的:

<c:if>

本身没有:

else

如果需要:

if / else

使用:

c:choose

六十三、c:choose

<c:choose>

    <c:when
            test="${student.score >= 90}"
    >
        优秀
    </c:when>

    <c:when
            test="${student.score >= 60}"
    >
        及格
    </c:when>

    <c:otherwise>
        不及格
    </c:otherwise>

</c:choose>

六十四、c:forEach

这是最重要的 JSTL 标签之一。

Servlet:

request.setAttribute(
        "students",
        students
);

JSP:

<c:forEach
        items="${students}"
        var="student"
>

    <p>
        ${student.name}
    </p>

</c:forEach>

六十五、items

items="${students}"

表示:

要遍历的数据

可以是:

List
Set
数组
Map

六十六、var

var="student"

表示:

当前遍历元素变量名

然后:

${student.name}

六十七、varStatus

<c:forEach
        items="${students}"
        var="student"
        varStatus="status"
>

可以使用:

status.index

status.count

status.first

status.last

六十八、index 和 count

${status.index}

从:

0

开始。

${status.count}

从:

1

开始。


六十九、first / last

${status.first}

${status.last}

返回:

boolean

可以判断:

是不是第一条

是不是最后一条

七十、c:forEach 数字循环

<c:forEach
        begin="1"
        end="5"
        var="i"
>

    ${i}

</c:forEach>

输出:

1 2 3 4 5

七十一、step

<c:forEach
        begin="1"
        end="10"
        step="2"
        var="i"
>

得到:

1 3 5 7 9

七十二、遍历 Map

Servlet:

Map<String, Integer> scores =
        new HashMap<>();

scores.put(
        "张三",
        90
);

scores.put(
        "李四",
        85
);

request.setAttribute(
        "scores",
        scores
);

JSP:

<c:forEach
        items="${scores}"
        var="entry"
>

    ${entry.key}
    =
    ${entry.value}

</c:forEach>

七十三、c:out

安全输出:

<c:out
        value="${student.name}"
/>

七十四、为什么推荐 c:out

如果用户输入:

<script>
    alert(1)
</script>

直接:

${comment.content}

在某些场景下可能被浏览器当 HTML 解释。

c:out 默认会:

转义 XML / HTML 特殊字符

可以降低:

XSS 风险

七十五、escapeXml

<c:out
        value="${content}"
        escapeXml="true"
/>

默认通常:

true

不要轻易关闭。


七十六、c:url

用于生成 URL:

<c:url
        value="/student/list"
        var="studentListUrl"
/>

使用:

<a
        href="${studentListUrl}"
>
    学生列表
</a>

七十七、c:url 的价值

它可以帮助处理:

Context Path
URL 编码

传统写法:

${pageContext.request.contextPath}/student/list

也很常用。


七十八、c:param

可以给 URL 添加参数:

<c:url
        value="/student/detail"
        var="detailUrl"
>

    <c:param
            name="id"
            value="${student.id}"
    />

</c:url>

结果类似:

/demo/student/detail?id=1

七十九、c:redirect

<c:redirect
        url="/login.jsp"
/>

可以重定向。

但请求控制更推荐:

Servlet

八十、c:catch

可以捕获标签体异常:

<c:catch
        var="error"
>

    ...

</c:catch>

但 JSP 不应该承担复杂异常处理。


八十一、JSTL Core 常用标签总结

必须掌握:

c:set

c:remove

c:if

c:choose

c:when

c:otherwise

c:forEach

c:out

c:url

c:param

八十二、EL + JSTL 学生列表

Servlet:

@WebServlet(
        "/student/list"
)
public class StudentListServlet
        extends HttpServlet {

    private final StudentService
            studentService =
            new StudentService();

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws ServletException,
            IOException {

        List<Student> students =
                studentService.findAll();

        request.setAttribute(
                "students",
                students
        );

        request
                .getRequestDispatcher(
                        "/WEB-INF/views/student-list.jsp"
                )
                .forward(
                        request,
                        response
                );
    }
}

八十三、student-list.jsp 标准版

<%@ page
        contentType="text/html;charset=UTF-8"
        pageEncoding="UTF-8"
%>

<%@ taglib
        prefix="c"
        uri="http://java.sun.com/jsp/jstl/core"
%>

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title>
        学生列表
    </title>

</head>

<body>

<h1>
    学生列表
</h1>

<c:choose>

    <c:when
            test="${empty students}"
    >

        <p>
            暂无学生数据
        </p>

    </c:when>

    <c:otherwise>

        <table
                border="1"
        >

            <thead>

            <tr>

                <th>
                    序号
                </th>

                <th>
                    学号
                </th>

                <th>
                    姓名
                </th>

                <th>
                    年龄
                </th>

                <th>
                    专业
                </th>

                <th>
                    成绩
                </th>

            </tr>

            </thead>

            <tbody>

            <c:forEach
                    items="${students}"
                    var="student"
                    varStatus="status"
            >

                <tr>

                    <td>
                        ${status.count}
                    </td>

                    <td>
                        <c:out
                                value="${student.studentNo}"
                        />
                    </td>

                    <td>
                        <c:out
                                value="${student.name}"
                        />
                    </td>

                    <td>
                        ${student.age}
                    </td>

                    <td>
                        <c:out
                                value="${student.major}"
                        />
                    </td>

                    <td>
                        ${student.score}
                    </td>

                </tr>

            </c:forEach>

            </tbody>

        </table>

    </c:otherwise>

</c:choose>

</body>

</html>

八十四、传统脚本版和 EL/JSTL 版对比

传统:

<%
    List<Student> students =
            (List<Student>)
            request.getAttribute(
                    "students"
            );

    for (
        Student student :
        students
    ) {
%>

    <%= student.getName() %>

<%
    }
%>

EL / JSTL:

<c:forEach
        items="${students}"
        var="student"
>

    ${student.name}

</c:forEach>

明显更:

清晰

接近 HTML

减少 Java 代码

八十五、登录状态显示案例

登录成功:

request
        .getSession()
        .setAttribute(
                "loginUser",
                user
        );

JSP:

<c:choose>

    <c:when
            test="${not empty sessionScope.loginUser}"
    >

        欢迎:
        <c:out
                value="${sessionScope.loginUser.username}"
        />

    </c:when>

    <c:otherwise>

        <a
                href="${pageContext.request.contextPath}/login.jsp"
        >
            登录
        </a>

    </c:otherwise>

</c:choose>

八十六、退出登录

Servlet:

@WebServlet(
        "/logout"
)
public class LogoutServlet
        extends HttpServlet {

    @Override
    protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException {

        HttpSession session =
                request.getSession(
                        false
                );

        if (
            session != null
        ) {

            session.invalidate();
        }

        response.sendRedirect(
                request.getContextPath()
                        + "/login.jsp"
        );
    }
}

八十七、getSession(false)

request.getSession(
        false
);

表示:

如果已有 Session
就返回

如果没有
不要新建

退出登录时很适合。


八十八、getSession()

默认:

request.getSession();

如果没有 Session:

会创建一个

八十九、Session ID

Servlet 容器通常通过:

JSESSIONID

识别用户 Session。

浏览器一般通过:

Cookie

保存。


九十、为什么不同用户 Session 不一样

因为每个浏览器会话通常携带:

不同 JSESSIONID

Tomcat 根据 ID 找:

对应 HttpSession

九十一、Session 超时

web.xml 可以配置:

<session-config>

    <session-timeout>
        30
    </session-timeout>

</session-config>

通常单位:

分钟

九十二、Session 超时后

例如:

30 分钟无操作

Session 可能失效。

如果:

loginUser

在 Session 中:

用户需要重新登录

九十三、登录状态判断

Servlet:

HttpSession session =
        request.getSession(
                false
        );

User loginUser = null;

if (
    session != null
) {

    loginUser =
            (User)
            session.getAttribute(
                    "loginUser"
            );
}

如果:

loginUser == null

说明:

未登录

九十四、以后为什么要学 Filter

如果每个 Servlet 都写:

if (
    loginUser == null
) {

    redirect login;
}

大量重复。

后面可以用:

Filter

统一做:

登录校验
编码处理
日志

九十五、作用域名称冲突案例

假设:

request.setAttribute(
        "name",
        "request-name"
);

session.setAttribute(
        "name",
        "session-name"
);

JSP:

${name}

会优先找到:

request

因为查找顺序:

page
request
session
application

九十六、明确指定作用域

${requestScope.name}

${sessionScope.name}

这样最清楚。


九十七、作用域数据覆盖不是同一个 Map

即使 key 都是:

name

它们分别存在:

不同作用域

所以不会互相真正覆盖。

只是:

EL 自动查找时
存在优先级

九十八、EL 访问嵌套属性

例如:

class Student {

    private School school;
}

School:

class School {

    private String name;
}

JSP:

${student.school.name}

相当于:

student.getSchool().getName()

九十九、EL 动态 Map key

${data[key]}

如果 key 变量值:

name

就等价:

${data["name"]}

一百、EL 中字符串

${status == "ENABLED"}

可以用于简单展示判断。

但复杂业务规则不要堆在 JSP。


一百零一、JSTL if 展示按钮

例如:

<c:if
        test="${sessionScope.loginUser.role == 'ADMIN'}"
>

    <button>
        删除用户
    </button>

</c:if>

注意:

前端隐藏按钮
不等于真正权限控制

一百零二、为什么页面权限不安全

用户可以:

自己构造 HTTP 请求

即使按钮隐藏,

仍可能直接调用:

删除接口

所以权限必须:

后端再次校验

一百零三、EL / JSTL 只是展示层

应该用于:

展示判断
循环
页面输出

不应该承担:

真正业务权限
事务
数据库逻辑

一百零四、表单错误信息

Servlet:

request.setAttribute(
        "error",
        "用户名不能为空"
);

JSP:

<c:if
        test="${not empty error}"
>

    <p>
        <c:out
                value="${error}"
        />
    </p>

</c:if>

一百零五、表单回显

Servlet:

request.setAttribute(
        "form",
        student
);

JSP:

<input
        name="name"
        value="${form.name}"
>

一百零六、HTML 属性中的 XSS 风险

如果直接:

value="${form.name}"

用户输入包含:

"
<
>

可能造成 HTML 属性注入风险。

传统 JSP 项目应:

谨慎转义

实际生产项目通常使用:

安全模板机制
框架转义
输入验证

一百零七、c:out 更适合文本节点

例如:

<p>
    <c:out
            value="${form.name}"
    />
</p>

安全性通常比直接输出更好。


一百零八、JSTL 格式化标签库预览

除了 core:

c

还有:

fmt

用于:

日期
数字
国际化

引入:

<%@ taglib
        prefix="fmt"
        uri="http://java.sun.com/jsp/jstl/fmt"
%>

一百零九、格式化数字

<fmt:formatNumber
        value="${student.score}"
        pattern="0.00"
/>

一百一十、格式化日期

如果对象里是旧式:

java.util.Date

可以:

<fmt:formatDate
        value="${user.createTime}"
        pattern="yyyy-MM-dd HH:mm:ss"
/>

现代 Java:

LocalDateTime

在老 JSTL 环境中未必能直接处理得很好,

实际可在后端格式化或使用额外方案。


一百一十一、URL 编码

使用:

<c:url>

和:

<c:param>

可以减少手动拼接 URL 参数的问题。


一百一十二、详情链接案例

<c:url
        value="/student/detail"
        var="detailUrl"
>

    <c:param
            name="id"
            value="${student.id}"
    />

</c:url>

<a
        href="${detailUrl}"
>
    查看
</a>

一百一十三、删除按钮推荐 POST

不要:

<a href="/student/delete?id=1">
    删除
</a>

更推荐:

<form
        action="${pageContext.request.contextPath}/student/delete"
        method="post"
>

    <input
            type="hidden"
            name="id"
            value="${student.id}"
    >

    <button
            type="submit"
    >
        删除
    </button>

</form>

一百一十四、隐藏字段

<input
        type="hidden"
        name="id"
        value="${student.id}"
>

会随表单一起提交。

但注意:

hidden 不代表安全

用户仍然可以修改。

后端必须再次校验。


一百一十五、Servlet 接收 hidden 参数

String idText =
        request.getParameter(
                "id"
        );

和普通表单参数一样。


一百一十六、分页页面案例

Servlet:

request.setAttribute(
        "pageNum",
        pageNum
);

request.setAttribute(
        "totalPages",
        totalPages
);

JSP:

<c:forEach
        begin="1"
        end="${totalPages}"
        var="page"
>

    <a
            href="${pageContext.request.contextPath}/student/list?pageNum=${page}"
    >
        ${page}
    </a>

</c:forEach>

一百一十七、当前页高亮

<c:choose>

    <c:when
            test="${page == pageNum}"
    >

        <strong>
            ${page}
        </strong>

    </c:when>

    <c:otherwise>

        <a
                href="${pageContext.request.contextPath}/student/list?pageNum=${page}"
        >
            ${page}
        </a>

    </c:otherwise>

</c:choose>

一百一十八、搜索条件回显

Servlet:

request.setAttribute(
        "keyword",
        keyword
);

JSP:

<input
        name="keyword"
        value="${keyword}"
>

一百一十九、Servlet 与 JSP 数据交互完整模型

浏览器
↓
Servlet
↓
request.getParameter()
↓
Service
↓
Mapper
↓
Database
↓
Servlet
↓
request.setAttribute()
↓
forward
↓
JSP
↓
EL
↓
JSTL
↓
HTML
↓
浏览器

一百二十、request 和 session 的关键区别

例如:

学生列表

只当前页面用:

request

登录用户跨很多请求:

session

不要反过来。


一百二十一、Session 滥用案例

错误:

session.setAttribute(
        "students",
        studentService.findAll()
);

如果只是当前列表页面需要:

没必要放 Session

会导致:

数据过期
占用内存
其他页面误用

一百二十二、application 滥用案例

错误:

application.setAttribute(
        "currentStudent",
        student
);

多个用户:

可能互相覆盖

一百二十三、线程安全再提醒

Session:

一个用户范围

但一个用户也可能:

同时发多个请求

application:

整个系统共享

所以对其中:

可变对象

操作仍要考虑线程安全。


一百二十四、EL 常见错误:属性没有 Getter

Java:

private String name;

但没有:

getName()

JSP:

${student.name}

可能读取失败。

所以 JavaBean:

Getter 很重要

一百二十五、EL 常见错误:attribute 名不一致

Servlet:

request.setAttribute(
        "studentList",
        list
);

JSP:

${students}

自然拿不到。


一百二十六、JSTL 常见错误:taglib 没引入

写:

<c:forEach>

但没写:

<%@ taglib
        prefix="c"
        uri="http://java.sun.com/jsp/jstl/core"
%>

JSP 会报:

标签无法识别

一百二十七、JSTL 常见错误:依赖没加

即使写了 taglib,

项目没有 JSTL 实现:

也可能报找不到标签库

Tomcat 8.5 项目通常需要:

javax.servlet:jstl:1.2

一百二十八、JSTL 常见错误:Tomcat 版本体系混用

Tomcat 8.5:

javax.servlet

Tomcat 10+:

jakarta.servlet

JSTL 依赖也要跟着匹配。


一百二十九、EL 常见错误:禁用了 EL

老项目可能出现:

isELIgnored="true"

这会让:

${name}

不执行。

现代一般保持:

EL 开启

一百三十、EL 输出成原样 ${name}

如果页面直接显示:

${name}

而不是值,

检查:

EL 是否被禁用

JSP 版本

web.xml 版本

文件是不是被当普通 HTML 返回

一百三十一、JSP 不能改成 .html 后还期待 EL 执行

普通:

.html

不会经过 JSP 引擎。

所以:

${student.name}

写在纯 HTML 中:

不会被 JSP 解析

一百三十二、forward 后 URL 为什么不变

因为:

浏览器只请求了一次

Servlet 在服务器内部:

把请求交给 JSP

所以地址栏仍然是:

Servlet URL

这是正常现象。


一百三十三、这样反而是 MVC 推荐行为

例如浏览器:

/student/list

页面实际 JSP:

/WEB-INF/views/student-list.jsp

用户地址栏仍然:

/student/list

这更符合:

Controller URL

而不是暴露:

JSP 文件路径

一百三十四、EL 和 JSTL 不负责数据库访问

不要写成:

JSP
↓
JSTL
↓
数据库

正确:

Servlet / Service
↓
数据库
↓
准备数据
↓
JSP 展示

一百三十五、综合案例:学生管理列表

Servlet:

@WebServlet(
        "/student/list"
)
public class StudentListServlet
        extends HttpServlet {

    private final StudentService
            studentService =
            new StudentService();

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws ServletException,
            IOException {

        String keyword =
                request.getParameter(
                        "keyword"
                );

        List<Student> students;

        if (
            keyword == null
            || keyword.isBlank()
        ) {

            students =
                    studentService.findAll();

        } else {

            students =
                    studentService.findByName(
                            keyword
                    );
        }

        request.setAttribute(
                "students",
                students
        );

        request.setAttribute(
                "keyword",
                keyword
        );

        request
                .getRequestDispatcher(
                        "/WEB-INF/views/student-list.jsp"
                )
                .forward(
                        request,
                        response
                );
    }
}

一百三十六、列表 JSP

<%@ page
        contentType="text/html;charset=UTF-8"
        pageEncoding="UTF-8"
%>

<%@ taglib
        prefix="c"
        uri="http://java.sun.com/jsp/jstl/core"
%>

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title>
        学生管理
    </title>

</head>

<body>

<h1>
    学生管理
</h1>

<form
        action="${pageContext.request.contextPath}/student/list"
        method="get"
>

    <input
            type="text"
            name="keyword"
            value="${keyword}"
            placeholder="请输入姓名"
    >

    <button
            type="submit"
    >
        搜索
    </button>

</form>

<c:choose>

    <c:when
            test="${empty students}"
    >

        <p>
            暂无数据
        </p>

    </c:when>

    <c:otherwise>

        <table
                border="1"
        >

            <thead>

            <tr>

                <th>
                    #
                </th>

                <th>
                    学号
                </th>

                <th>
                    姓名
                </th>

                <th>
                    年龄
                </th>

                <th>
                    专业
                </th>

                <th>
                    操作
                </th>

            </tr>

            </thead>

            <tbody>

            <c:forEach
                    items="${students}"
                    var="student"
                    varStatus="status"
            >

                <tr>

                    <td>
                        ${status.count}
                    </td>

                    <td>
                        <c:out
                                value="${student.studentNo}"
                        />
                    </td>

                    <td>
                        <c:out
                                value="${student.name}"
                        />
                    </td>

                    <td>
                        ${student.age}
                    </td>

                    <td>
                        <c:out
                                value="${student.major}"
                        />
                    </td>

                    <td>

                        <c:url
                                value="/student/detail"
                                var="detailUrl"
                        >

                            <c:param
                                    name="id"
                                    value="${student.id}"
                            />

                        </c:url>

                        <a
                                href="${detailUrl}"
                        >
                            查看
                        </a>

                    </td>

                </tr>

            </c:forEach>

            </tbody>

        </table>

    </c:otherwise>

</c:choose>

</body>

</html>

一百三十七、这个页面已经接近标准传统 MVC

JSP 中:

没有 JDBC

没有 MyBatis

没有 Service

没有 Java for 脚本

没有 Java if 脚本

只剩:

HTML
EL
JSTL

这就是比较合理的 JSP 写法。


一百三十八、练习题 1:四大作用域

分别:

page

request

session

application

存一个:

name

然后使用:

${name}

观察优先级。


一百三十九、练习题 2:Scope 对象

分别输出:

${pageScope.name}

${requestScope.name}

${sessionScope.name}

${applicationScope.name}

一百四十、练习题 3:JavaBean EL

Student:

name
age
major

Servlet setAttribute 后,

JSP 使用:

${student.name}
${student.age}
${student.major}

一百四十一、练习题 4:List + c:forEach

Servlet:

List<Student>

JSP 使用:

c:forEach

输出表格。


一百四十二、练习题 5:c:choose

根据:

score

显示:

优秀

及格

不及格

一百四十三、练习题 6:empty

如果:

students 为空

显示:

暂无数据

否则:

显示表格

一百四十四、练习题 7:Session 登录

登录成功:

session.setAttribute(
        "loginUser",
        user
);

首页:

显示欢迎信息

退出:

invalidate

一百四十五、练习题 8:c:url

生成:

/student/detail?id=1

不要手工拼参数。


一百四十六、练习题 9:Map 遍历

Map:

Java=90

MySQL=85

Spring=95

使用:

c:forEach

输出 key/value。


一百四十七、练习题 10:综合列表页

实现:

搜索

空数据提示

列表循环

详情链接

登录用户名显示

全部使用:

EL + JSTL

不要写 JSP Java 脚本。


一百四十八、必须掌握的 EL 内置对象

pageScope

requestScope

sessionScope

applicationScope

param

paramValues

header

headerValues

cookie

initParam

pageContext

一百四十九、必须掌握的 EL 写法

${name}

${student.name}

${students[0]}

${map.key}

${map["key"]}

${param.id}

${sessionScope.loginUser}

${empty students}

${not empty students}

${score >= 60 ? "及格" : "不及格"}

${pageContext.request.contextPath}

一百五十、必须掌握 JSTL 标签

<c:set>

<c:remove>

<c:if>

<c:choose>

<c:when>

<c:otherwise>

<c:forEach>

<c:out>

<c:url>

<c:param>

一百五十一、必须回答的问题

学完后应该能够回答:

1. JSP 四大作用域是什么?

2. page/request/session/application 生命周期有什么区别?

3. 为什么当前页面数据更适合放 request?

4. 登录用户为什么适合放 session?

5. application 为什么不能保存当前登录用户?

6. EL 是什么?

7. ${student.name} 实际读取什么?

8. EL 默认查找作用域顺序是什么?

9. requestScope 和 sessionScope 有什么作用?

10. empty 可以判断什么?

11. JSTL 是什么?

12. c:if 和 c:choose 有什么区别?

13. c:forEach 的 items、var、varStatus 分别是什么?

14. status.index 和 status.count 有什么区别?

15. c:out 为什么比直接输出更安全?

16. c:url 有什么作用?

17. Session 为什么可以保持登录状态?

18. getSession(false) 有什么作用?

19. 为什么前端隐藏按钮不等于权限控制?

20. 为什么 EL/JSTL 不能承担业务逻辑?

一百五十二、本章知识结构

Servlet / JSP 数据交互
│
├─ Scope
│   ├─ page
│   ├─ request
│   ├─ session
│   └─ application
│
├─ EL
│   ├─ Bean 属性
│   ├─ List
│   ├─ Map
│   ├─ param
│   ├─ scope 对象
│   ├─ empty
│   └─ 运算符
│
├─ JSTL Core
│   ├─ c:set
│   ├─ c:if
│   ├─ c:choose
│   ├─ c:forEach
│   ├─ c:out
│   └─ c:url
│
├─ Session
│   ├─ 登录用户
│   ├─ JSESSIONID
│   ├─ timeout
│   └─ invalidate
│
└─ MVC
    ├─ Servlet
    ├─ Service
    ├─ Mapper
    └─ JSP

一百五十三、从 JSP 脚本到 EL/JSTL

以前:

<%
    Student student =
            (Student)
            request.getAttribute(
                    "student"
            );
%>

<%= student.getName() %>

现在:

${student.name}

以前:

<%
    for (
        Student student :
        students
    ) {
%>

    ...

<%
    }
%>

现在:

<c:forEach
        items="${students}"
        var="student"
>

    ...

</c:forEach>

这就是:

JSP 从 Java 脚本页面
向模板页面的进化

一百五十四、从这一章到 Web CRUD

现在我们已经具备:

Servlet 接收请求

Servlet 获取参数

Service 处理业务

MyBatis 操作数据库

request 传递页面数据

session 保存登录状态

EL 输出数据

JSTL 循环和判断

下一篇就可以真正做:

Web CRUD

也就是:

学生管理系统 / 用户管理系统

完整实现:

列表

新增

修改

删除

详情

搜索

分页

登录

MVC 分层

一百五十五、本章总结

这一章最核心的三个关键词:

Scope

EL

JSTL

四大作用域:

page
当前 JSP

request
一次请求

session
当前用户会话

application
整个 Web 应用

选择原则:

能用小作用域,就不要使用更大的作用域。

EL 负责:

方便地取数据

例如:

${student.name}

${sessionScope.loginUser.username}

${empty students}

JSTL 负责:

页面条件

页面循环

安全输出

URL 构造

例如:

<c:if>

<c:choose>

<c:forEach>

<c:out>

<c:url>

传统 MVC 的完整数据流:

浏览器
↓
Servlet
↓
Service
↓
Mapper
↓
MySQL
↓
Servlet
↓
request.setAttribute
↓
forward
↓
JSP
↓
EL + JSTL
↓
HTML
↓
浏览器

到这里,Servlet + JSP 页面交互体系已经基本完整。

下一篇按照课程表进入:

Web CRUD / 项目 - MVC

会把前面的:

Servlet

JSP

EL

JSTL

MyBatis

Service

Mapper

真正整合成一个完整的 Web 项目。