JavaWeb 复习讲义
原始讲义
第四部分 状态保持与视图:让服务器记住你

第8章 JSP、EL 与 JSTL

难度:★★★★☆ | 前置知识:第5章 Tomcat 与 Servlet、第6章 Request 与 Response、第7章 会话技术 | 本章解决:把"在 Java 里拼 HTML"翻转成"在 HTML 里取数据",再用 EL 与 JSTL 把页面里最后一点 Java 代码也请出去

本章导读

【考点】

本章在卷子上的形态很固定:一道"JSP 原理"简答(翻译成 Servlet、_jspService、九个内置对象),一道"EL/JSTL 读程序题"(给一段 JSP 与一个 Servlet,问页面输出什么),一道"MVC 与三层架构"的概念题,最后是一道综合设计题。记忆负担最轻、分值最稳的是 8.3 的原理与 8.7、8.8 的语法,优先保住。

8.1 JSP 是什么

8.1.1 先从只用 Servlet 写页面的痛说起

先不看 JSP,先看一个已经会写的场景:用户登录成功之后,页面上要显示"某某某,欢迎您"。这个用户名是动态展示的,谁登录就展示谁的名字。

只用 Servlet 怎么实现?思路是:Servlet 里校验完用户名密码,把用户名存进 session,然后由 response.getWriter() 拿到的字符输出流,把整个 HTML 页面一个字一个字写出去。这段代码的形状大致如下(下面只是让你体会"形状",真实代码里要拼的标签比这多得多):

PrintWriter writer = response.getWriter();
writer.write("<html>");
writer.write("<head><title>登录成功</title></head>");
writer.write("<body>");
writer.write("<h1>" + username + ",欢迎您</h1>");
writer.write("</body>");
writer.write("</html>");

这就是原文所说的:代码有大量使用到 writer 对象向页面写标签内容,这样我们的代码就显得很麻烦;将来如果展示的效果出现了问题,排错也显得有点力不从心。

麻烦在哪里?把这句话拆开看:

痛点具体表现
引号地狱HTML 属性本身要双引号,Java 字符串也要双引号,于是满屏 \",或者被迫全用单引号
语法割裂HTML 的缩进、闭合、层级是靠肉眼维护的,它在 Java 里只是一堆字符串,IDE 不会帮你检查标签有没有闭合
改动成本高前端说"表格加一列",你改的是 Java 源码,改完要重新编译、重启服务器
排错困难页面结构错了,浏览器只会给你一个歪掉的页面;异常栈里只有 write() 的行号,看不出是哪个标签的问题
无法分工前端工程师不会 Java,后端工程师不精 HTML,两边只能靠"你给我静态页、我给你改 JSP"来回倒手

8.1.2 JSP 的定义

JSP(全称:Java Server Pages):Java 服务端页面。 它是一种动态的网页技术,其中既可以定义 HTML、JS、CSS 等静态内容,还可以定义 Java 代码的动态内容,也就是 JSP = HTML + Java。

下面就是一段最朴素的 JSP 代码:

<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>JSP,Hello World</h1>
        <%
            System.out.println("hello,jsp~");
        %>
    </body>
</html>

上面代码中,h1 标签的内容是展示在页面上的,而 Java 的输出语句 System.out.println 是输出在 IDEA 的控制台里的。这两种输出走的是两条完全不同的路,一个是 HTTP 响应体,一个是服务器进程的标准输出。这个区别后面会反复用到(见 8.6 节 out 与 System.out 的对比)。

JSP 能做什么?还是那个登录后展示用户名的例子。用 JSP 实现时,页面里基本都是 HTML 标签,只在要显示用户名的地方写一句 Java 表达式:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
    <h1><%=session.getAttribute("username")%>,欢迎您</h1>
</body>
</html>

可以看出,这样操作起来要比用 Servlet 实现舒服很多。于是得到 JSP 的定位:

JSP 作用:简化开发,避免了在 Servlet 中直接输出 HTML 标签。

8.1.3 JSP 与 Servlet 的分工

既然两者都能生成页面,那到底谁干什么?答案是分工,而不是二选一:

维度ServletJSP
主要职责接收请求、处理业务、调用模型、控制跳转把域中的数据渲染成 HTML 展示
擅长的事写 Java 逻辑(判断、循环、调 Service、访问数据库)写 HTML 结构(表格、表单、样式)
不擅长的事拼 HTML 字符串写复杂业务逻辑
本质一个 Java 类一个会被翻译成 Java 类的页面
由谁编写后端工程师偏前端,后端也能改
本章后半段的角色Controller(控制器)View(视图)

记法是:Servlet 负责"想",JSP 负责"显示"。 这句话在 8.9 节的 MVC 里会被正式命名,在 8.10 节的案例里会实际运行。

【对比】

很多人把 JSP 理解成"升级版的 Servlet",这是错的。JSP 在能力上是 Servlet 的子集(它编译后就是一个 Servlet),在用途上却是 Servlet 的补充:Servlet 用来写逻辑,JSP 用来写展示。两者的关系是"同一种东西的两种写法,各自适合不同的工作",而不是"新的替换旧的"。

【易错点】 不要在 JSP 里写业务逻辑(查数据库、算金额、校验权限),也不要在 Servlet 里 write("<table>")。这两件事都"能跑",但是都违反分工:前者让页面没法交给前端改,后者让排版没法维护。判断标准很简单:这段代码里如果有 new、有 SQL、有 if 判断业务规则,它就不该出现在 JSP 里。

8.2 JSP 快速入门

光看定义没用,先跑通一个最小的 JSP,建立"文件放哪、怎么访问、结果长什么样"的直觉。

8.2.1 搭建环境

创建一个 Maven 的 web 项目,项目结构如下:

jsp-demo
├── pom.xml
└── src
    └── main
        ├── java
        └── webapp
            └── hello.jsp

pom.xml 文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jsp-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>
        </plugins>
    </build>
</project>

<packaging> 必须是 war,因为这是一个要部署到容器里的 Web 工程;provided 表示这个 jar 由 Tomcat 提供,不打进 war 包。

8.2.2 导入 JSP 依赖

在 dependencies 标签中导入 JSP 的依赖,如下:

<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
    <scope>provided</scope>
</dependency>

该依赖的 scope 必须设置为 provided,因为 Tomcat 中已经有这个 jar 包了,所以在打包时我们是不希望将该依赖打进到我们的 war 包中的。

【补充】

为什么"重复的 jar 包"是灾难?如果 jsp-api 被打进 WEB-INF/lib,容器里就会出现两份 javax.servlet.jsp.* 的类:一份来自 Tomcat 自己的 jasper.jar,一份来自你的 war 包。一旦两者版本不一致(比如 war 里是 2.2、Tomcat 里是 2.3),就可能抛 java.lang.LinkageError 或 NoSuchMethodError。现象很诡异:编译完全正常,一访问 JSP 就 500。凡是容器已经提供的 API(servlet-api、jsp-api、el-api),一律 provided。

8.2.3 创建 JSP 页面

在项目的 webapp 目录下创建 JSP 页面,通过这种方式创建一个名为 hello.jsp 的页面。

【易错点】

JSP 文件必须放在 webapp 目录(或其子目录)下,不能放在 WEB-INF 里:WEB-INF 下的资源浏览器无法直接访问,只能由服务器内部转发过去。另外文件名后缀必须是 .jsp,如果你建成了 hello.html 再往里写 <% %>,浏览器只会把这段代码当普通文字原样显示出来。

8.2.4 编写代码

在 hello.jsp 页面中书写 HTML 标签和 Java 代码,如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>hello jsp</h1>

    <%
        System.out.println("hello,jsp~");
    %>
</body>
</html>

第一行的 <%@ page ... %> 是 page 指令,告诉容器"这个页面输出的是 HTML、编码是 UTF-8、脚本用 Java 写"。这一行几乎每个 JSP 都会有,8.4.1 节会把它的每个属性拆开讲。

8.2.5 测试

启动服务器,并在浏览器地址栏输入 http://localhost:8080/jsp-demo/hello.jsp,你可以在页面上看到 hello jsp 这行大字。

同时也可以在 IDEA 的控制台看到输出的 hello,jsp~ 内容。

请注意两者的去向差异:页面上出现的是 hello jsp(HTML 标签被浏览器解析了),控制台出现的是 hello,jsp~(System.out 的输出回到了服务器进程,根本没有进 HTTP 响应)。如果你希望一段文字出现在浏览器里,用 out.print() 或 <%= %>;用 System.out.println() 是永远到不了浏览器的。

【考点】

访问一个 JSP 时,Tomcat 内部发生了四步:翻译(.jsp → xxx_jsp.java)→ 编译(.java → .class)→ 加载(类加载器加载)→ 执行(_jspService)。第一次访问会明显变慢,就是因为前三步;之后每次访问只执行第四步。这个"第一次慢、后面快"的现象,是判断一个 JSP 是否已经被编译过的直观办法。

8.3 JSP 原理

我们在 8.1 节说 JSP 就是一个页面,那么在 JSP 中写 HTML 标签,我们能理解;但是为什么还可以写 Java 代码呢?因为 JSP 就是一个 Servlet。 这一节把这句话拆开验证一遍。这是本章唯一一段"看不见但必须懂"的内容,也是简答题的高频出处。

8.3.1 访问 JSP 的四步

浏览器访问一个 JSP 时,Tomcat 内部按下面四步走:

  1. 浏览器第一次访问 hello.jsp 页面;
  2. Tomcat 会将 hello.jsp 转换为名为 hello_jsp.java 的一个 Servlet;
  3. Tomcat 再将转换的 Servlet 编译成字节码文件 hello_jsp.class;
  4. Tomcat 会执行该字节码文件,向外提供服务。

把四步画成一条流水线,就是下面这样:

        ①浏览器访问 /hello.jsp
浏览器 ────────────────────────────────► Tomcat
                                            │
                                            │ ②翻译
                                            │   hello.jsp
                                            │        ↓ 生成 Servlet 源码
                                            │   hello_jsp.java
                                            │        ↓ 编译
                                            │   hello_jsp.class
                                            │        ↓ ③加载、④执行
                                            ▼
        响应一段 HTML  ◄─────────  _jspService() 里 out.write("...")

.jsp 是给人写的源文件,.java 和 .class 是 Tomcat 替你补上的"中间产物",你从头到尾不用手写一行。这正是 JSP 被称为"Servlet 的另一种写法"的字面含义。

注意第 1 步里的"第一次"三个字。翻译和编译只在第一次访问(或者 JSP 文件被修改之后)发生,之后 Tomcat 直接复用已经编译好的 .class。这就是为什么改完 JSP 刷新页面就能看到效果,而改完 Servlet 必须重启服务器:JSP 自带"改了就重新编译"的能力,Servlet 没有。

8.3.2 去哪儿找翻译出来的 .java 文件

这不是比喻,文件是真真切切生成在磁盘上的。到项目所在磁盘目录下找下面这个路径:

target\tomcat\work\Tomcat\localhost\jsp-demo\org\apache\jsp

这个目录下就能看到转换后的 Servlet:

org\apache\jsp
├── hello_jsp.java
└── hello_jsp.class

打开 hello_jsp.java 文件,就能看到 Tomcat 替我们写的那一坨 Java 代码。

【补充】

这个 work 目录是 Tomcat 的"工作目录",专门存放 JSP 翻译后的中间产物。它的完整路径规律是 工作目录\Tomcat\localhost\<项目上下文路径>\org\apache\jsp\,其中"项目上下文路径"就是你在浏览器地址栏里 localhost:8080/ 后面那一段(示例里是 jsp-demo)。用 IDEA 的内置 Tomcat 插件跑项目时,工作目录被重定向到了工程的 target\tomcat\work 下,所以你在项目里能直接看到;如果部署到独立的 Tomcat 安装目录,则要去 <Tomcat安装目录>\work\Catalina\localhost\<项目名>\ 下找。排错时的用法:JSP 报 500 且错误信息指向某一行 .java 时,就去这个目录打开对应的 _jsp.java,把行号对上,你就知道是自己的哪一段 JSP 出的问题。

8.3.3 继承链:它是怎么变成 Servlet 的

打开 hello_jsp.java,看类的声明,会看到它继承了名为 HttpJspBase 的类。那 HttpJspBase 又是什么?到 Tomcat 源码里找下面这个目录:

apache-tomcat-8.5.68-src\java\org\apache\jasper\runtime

该目录下就有 HttpJspBase 类,查看该类的继承关系,可以看到:

public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {
    // ...
}

可以看到该类继承了 HttpServlet;那么 hello_jsp 这个类就间接地继承了 HttpServlet,也就说明 hello_jsp 是一个 Servlet。整条链子画出来是:

hello_jsp  →  HttpJspBase  →  HttpServlet  →  GenericServlet  →  Object
(Tomcat 生成)  (Jasper 提供)   (Servlet 规范)    (Servlet 规范)

8.3.4 _jspService():每次访问真正执行的方法

继续阅读 hello_jsp 类的代码,可以看到有一个名为 _jspService() 的方法,该方法就是每次访问 JSP 时自动执行的方法,和 Servlet 中的 service 方法一样。

把生成代码里与本节相关的部分简化出来(去掉了异常处理与 Jasper 内部的样板代码),结构是这样的:

public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase {

    public void _jspService(final javax.servlet.http.HttpServletRequest request,
                            final javax.servlet.http.HttpServletResponse response)
            throws java.io.IOException, javax.servlet.ServletException {

        final javax.servlet.jsp.PageContext pageContext;
        javax.servlet.http.HttpSession session = null;
        final javax.servlet.ServletContext application;
        final javax.servlet.ServletConfig config;
        javax.servlet.jsp.JspWriter out = null;
        final java.lang.Object page = this;

        try {
            response.setContentType("text/html;charset=UTF-8");
            pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
            application = pageContext.getServletContext();
            config = pageContext.getServletConfig();
            session = pageContext.getSession();
            out = pageContext.getOut();

            out.write("<html>\r\n");
            out.write("<head>\r\n");
            out.write("<title>Title</title>\r\n");
            out.write("</head>\r\n");
            out.write("<body>\r\n");
            out.write("<h1>hello jsp</h1>\r\n");

            System.out.println("hello,jsp~");

            out.write("</body>\r\n");
            out.write("</html>\r\n");
        } catch (java.lang.Throwable t) {
            // 异常处理
        }
    }
}

而在 _jspService() 方法中可以看到往浏览器写标签的代码:out.write("<html>\r\n")、out.write("<h1>hello jsp</h1>\r\n")……以前我们自己写 Servlet 时,这部分代码是由我们自己来写的,现在有了 JSP 后,由 Tomcat 完成这部分功能。 你在 JSP 里敲的每一个 HTML 字符,最后都变成了这样一行 out.write()。

这段代码里藏着本章后半部分所有知识点的源头,逐行读一遍:

8.3.5 由这段代码得出的四个结论

第一,JSP 的 HTML 是"写出去",Java 代码是"执行掉"。HTML 部分变成 out.write() 的参数,Java 脚本部分原封不动地留在方法体里执行。所以 <% System.out.println(...) %> 打印到控制台,而 <h1> 打印到浏览器。

第二,JSP 的九大内置对象就是 _jspService 里的局部变量,没有魔法可言。8.6 节会专门列表说明,此处只要记住这个结论。

第三,JSP 是单实例多线程的。容器只创建一个 hello_jsp 实例,每次请求开一个线程执行 _jspService。所以 JSP 里的局部变量(<% %> 里定义的)是线程安全的,而 <%! %> 声明的成员变量是所有请求共享的,不安全,这就是 8.4.4 节要讲的常见错误。

第四,首次访问慢是正常的。翻译要读文件、解析标签、生成 Java 源码,编译要调 JDK 编译器,都是毫秒级的重活;之后就直接执行字节码了。

【对比】

JSP 与 Servlet 的对应关系,一张表说清:

JSP 里的东西翻译成 Servlet 后的样子
整个 .jsp 文件一个类 xxx_jsp,继承 HttpJspBase(间接继承 HttpServlet)
页面上的 HTML 文本out.write("...") 一行行写出
<% ... %> 脚本片段原样放进 _jspService() 方法体
<%= 表达式 %>out.print(表达式)
<%! ... %> 声明放进类体,成为成员变量 / 成员方法
<%@ page ... %>影响 _jspService 开头那几行(setContentType、getPageContext 的参数等)
request、response 等九个内置对象_jspService 的形参或局部变量
service() 方法被 _jspService() 取代,HttpJspBase 的 service() 负责调用它

【考点】 "JSP 和 Servlet 的关系"标准答案三句话:① JSP 会被 Tomcat 翻译成 Java 类,该类继承 HttpJspBase,HttpJspBase 继承 HttpServlet,所以 JSP 就是 Servlet;② JSP 的处理逻辑写在 _jspService() 方法里,等价于 Servlet 的 service();③ 区别在于 JSP 擅长输出 HTML(写起来像网页),Servlet 擅长处理逻辑(写起来像 Java)。

8.4 JSP 的组成:指令、脚本、表达式、声明、注释

一个 JSP 文件,从头到尾只可能由五类东西组成:指令(<%@ %>)、脚本片段(<% %>)、表达式(<%= %>)、声明(<%! %>)、注释(<%-- --%>),剩下的就是原样的 HTML 和普通文本。这一节把五类逐个拆开。

JSP 脚本用于在 JSP 页面内定义 Java 代码。在之前的入门案例中,我们在 JSP 页面里定义的 Java 代码就是 JSP 脚本。

8.4.1 指令

指令的语法是 <%@ 指令名 属性="值" %>,它在翻译期起作用,用来告诉 Tomcat"这个页面该怎么翻译"。JSP 有三种指令:page、include、taglib。

(1)page 指令:描述整个页面的属性

page 指令最常用,属性也最多。常用的几个必须记住:

属性取值示例作用
languagejava指定脚本语言,固定为 java,可省略
contentTypetext/html;charset=UTF-8相当于 response.setContentType(),告诉浏览器响应体的类型与编码
pageEncodingUTF-8告诉 Tomcat 用什么编码读取这个 .jsp 源文件
importjava.util.List等价于 Java 的 import,是唯一可以写多次的属性
isELIgnoredtrue / false是否忽略 EL 表达式,默认 false(即 EL 生效)
sessiontrue / false是否自动创建 session,默认 true;写 false 则内置对象 session 不可用
errorPage/error.jsp本页出错时跳转到哪个页面
isErrorPagetrue / false本页是否是错误处理页,只有 true 才能用内置对象 exception
buffer8kb / noneout 输出缓冲区大小
autoFlushtrue / false缓冲区满时是否自动刷新到客户端

用法示例如下(import 属性最典型的写法就是每个类写一行):

<%@ page import="com.itheima.pojo.Brand" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

也可以把多个类写在一个 import 里,用逗号分隔:

<%@ page import="java.util.List, java.util.ArrayList, com.itheima.pojo.Brand" %>
【易错点】

contentType 和 pageEncoding 长得像,作用完全不同,这是最容易被问倒的一个点。pageEncoding 管的是"Tomcat 读 .jsp 文件时按什么编码解码",影响的是源码里的中文能不能被正确翻译;contentType 里的 charset 管的是"响应发给浏览器时按什么编码",影响的是浏览器显示中文会不会乱码。只写 contentType 时,容器会退而用它的 charset 去读源文件,多数情况能蒙对;但如果两个都没写,Tomcat 按 ISO-8859-1 读源文件,源码里的中文会变成乱码,而且报错信息会指向一行你根本没写错的代码。最省事的做法就是像原文那样,两个都写 UTF-8。

【补充】 isELIgnored 是唯一一个"让 EL 失效"的开关,默认 false。什么时候会用到它?当页面里本来就要显示 ${...} 这些字符时,比如讲 JavaScript 模板字符串、或者展示一段 JSP 教程代码。这时如果 EL 生效,${} 会被当成表达式求值,取不到值就输出空串,你的字面量就"消失"了。解决方式有两种:用 <%-- --%> 包住不行(注释会整段删掉),正确做法是 <%@ page isELIgnored="true" %>,或者用 \${...} 转义。

(2)include 指令:静态包含

include 指令用于在当前页面中静态包含另一个文件,语法是:

<%@ include file="header.jsp" %>

在被包含的文件里可以写 HTML,也可以写 Java 代码片段(因为它在翻译期就被原样合并进来了)。典型用途是把页头、页脚、导航栏抽出来复用。

【对比】

include 指令(静态包含)和 <jsp:include> 动作(动态包含)是最经典的一组易混概念,区别不在"能不能包含",而在"什么时候包含":

对比项<%@ include file="x.jsp" %><jsp:include page="x.jsp"/>
所属指令动作(Action)
发生时机翻译期,合并进一个 .java运行期,每次请求时调用
产物只有一个 class主页面和子页面各有一个 class
变量共享同一个 _jspService 里的局部变量不共享,是两个独立的方法
传参不能传参可以用 <jsp:param name="x" value="1"/> 传参
效率高(只编译一次)略低(每次请求都要包含一次)
适用场景页头页脚、固定的公共片段内容会变、需要独立处理的片段

记忆口诀:指令是"抄进来",动作是"调过去"。

(3)taglib 指令:引入标签库

taglib 指令用于引入 JSTL 之类的标签库,语法是:

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

prefix 是使用标签时的前缀(写了 c 就能用 <c:if>),uri 是标签库的唯一标识。这个指令 8.8 节会详细讲,此处只要认识它的长相。

8.4.2 脚本片段 <% %>

JSP 脚本有三个分类,第一类是脚本片段:

在 hello.jsp 中书写:

<%
    System.out.println("hello,jsp~");
    int i = 3;
%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,i 变量定义在了 _jspService() 方法中,它就是一段普通的 Java 代码,可以写 if、for、调方法、声明局部变量。

<%%> 里面写的是 Java 代码,而外边写的是 HTML 标签。这两者可以任意地交错、嵌套,这正是 JSP 写循环生成表格行的原理:

<%
    for (int i = 0; i < brands.size(); i++) {
        Brand brand = brands.get(i);
%>
    <tr>
        <td><%=brand.getBrandName()%></td>
    </tr>
<%
    }
%>

注意上面这段代码的括号是"跨出去又跨回来"的:for 的左大括号在第一个脚本里,右大括号在第三个脚本里,中间的 HTML 属于循环体。这在 Java 里不可能出现,但是在 JSP 里完全合法,因为翻译后它们本来就拼在同一个方法体里。

8.4.3 表达式 <%= %>

第二类脚本是表达式:

在 hello.jsp 中书写:

<%="hello"%>
<%=i%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,该脚本的内容被放在了 out.print() 中,作为参数。

三个必须注意的点:

8.4.4 声明 <%! %>

第三类脚本是声明:

在 hello.jsp 中书写:

<%!
    void show(){}
    String name = "zhangsan";
%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,该脚本的内容被放在了成员位置:show() 成了这个类的方法,name 成了这个类的成员变量。

【易错点】

声明有两处容易出错的地方,后果都很严重。第一处:不能用内置对象。 <%! %> 里的代码在方法外面,而 request、out、session 都是 _jspService 的局部变量,所以 <%! String n = request.getParameter("name"); %> 直接编译报错"找不到符号"。第二处:成员变量是线程不安全的。 容器只创建一个 JSP 实例,所有请求的线程共享同一个成员变量:

<%! int count = 0; %>
<% count++; %>
你是第 <%=count%> 位访问者

这段代码看着像计数器,实际上是并发 bug 的教科书案例:两个用户同时访问,可能都读到 99,都加 1 写回 100,最终少算一次。结论:计数、累加这类"跨请求状态",应该放 application 域或数据库,绝不能用 <%! %> 成员变量。

8.4.5 注释

JSP 里有三种"注释",它们的行为完全不同,必须分清:

写法名称会被发给浏览器吗会被编译进 .java 吗
<%-- 注释 --%>JSP 注释不会不会,翻译时整段丢弃
<!-- 注释 -->HTML 注释会,用户在"查看源代码"里能看到会,作为 out.write() 的内容
// 注释、/* 注释 */Java 注释不会会,留在脚本片段里

用法演示:

<%--
    c:if:来完成逻辑判断,替换 java if else
--%>
<c:if test="${status ==1}">
    启用
</c:if>
【易错点】

用 HTML 注释 <!-- --> 去"注释掉"一段 JSP 代码,是最常见的误用。因为 HTML 注释在翻译期是普通文本,它不会阻止里面的 JSP 代码被执行:

<!-- <% System.out.println("我还是执行了"); %> -->

控制台照样打印。更糟的是,如果里面是 <%= %> 输出,浏览器"查看源代码"里还能看到注释里的内容。要注释 JSP 代码,一律用 <%-- --%>。

【对比】 <%= %> 与 ${} 是本章最该先分清的一对,它们都能把数据显示到页面上,但根本不是一回事:

对比项<%= 表达式 %>${ 表达式 }
出身JSP 语法,JSP 1.0 就有EL 表达式,JSP 2.0 引入
能力能执行任意 Java 代码(调方法、算表达式、new 对象)只能取值和做简单运算,不能 new、不能写语句
取不到值时表达式返回 null 时输出字符串 "null"输出空字符串(什么都不显示)
遇到 null 对象brand.getBrandName() 里 brand 为 null 直接抛 NullPointerException${brand.brandName} 中 brand 为 null 时返回 null,不抛异常
可读性页面里混 Java,前端看不懂页面上没有 Java 关键字,前端能看懂
现代用法已不推荐,只用于极少数场景推荐,配合 JSTL 使用

结论:新写的页面里,能用 ${} 就不用 <%= %>。 这也是本章后面几节要走的路线。

8.4.6 案例:用脚本展示品牌数据

需求:使用 JSP 脚本展示品牌数据。

说明两点:素材里提供了一个 brand.html 静态页面;在本案例中,数据不从数据库中查询,而是在 JSP 页面上写死。

第一步,准备数据。 把 Brand.java 文件放置到项目的 com.itheima.pojo 包下,在 brand.jsp 中准备一些数据:

<%
    // 查询数据库
    List<Brand> brands = new ArrayList<Brand>();
    brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
    brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
    brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));
%>

注意:这里的类是需要导包的(Brand、List、ArrayList),用 page 指令的 import 属性导入。

第二步,把静态表格改成动态。 先看静态版本,表格里是三行写死的数据:

<table border="1" cellspacing="0" width="800">
    <tr>
        <th>序号</th>
        <th>品牌名称</th>
        <th>企业名称</th>
        <th>排序</th>
        <th>品牌介绍</th>
        <th>状态</th>
        <th>操作</th>
    </tr>
    <tr align="center">
        <td>1</td>
        <td>三只松鼠</td>
        <td>三只松鼠</td>
        <td>100</td>
        <td>三只松鼠,好吃不上火</td>
        <td>启用</td>
        <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    <!-- 另外两行同理:优衣库、小米 -->
</table>

第三步,套上循环。 很自然会写出这样的代码,但是这行代码是错的:

<%
    for (int i = 0; i < brands.size(); i++) {
        //获取集合中的 每一个 Brand 对象
        Brand brand = brands.get(i);
    }
%>
<tr align="center">
    <td>1</td>
    <td>三只松鼠</td>
    <!-- ... -->
</tr>

问题在于:tr 在 for 循环外面,循环执行了三遍但什么也没输出,页面永远只有一行。上面的 for 循环需要将 tr 标签包裹起来,这样才能实现循环的效果,代码改进为:

<%
    for (int i = 0; i < brands.size(); i++) {
        //获取集合中的 每一个 Brand 对象
        Brand brand = brands.get(i);
%>
    <tr align="center">
        <td>1</td>
        <td>三只松鼠</td>
        <td>三只松鼠</td>
        <td>100</td>
        <td>三只松鼠,好吃不上火</td>
        <td>启用</td>
        <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
<%
    }
%>

注意:<%%> 里面写的是 Java 代码,而外边写的是 HTML 标签。 现在能循环三次了,但三行数据一模一样,因为 td 里还是写死的。

第四步,把 td 也改成动态的。 使用 <%= %> 输出每个对象的属性:

<table border="1" cellspacing="0" width="800">
    <tr>
        <th>序号</th>
        <th>品牌名称</th>
        <th>企业名称</th>
        <th>排序</th>
        <th>品牌介绍</th>
        <th>状态</th>
        <th>操作</th>
    </tr>

    <%
        for (int i = 0; i < brands.size(); i++) {
            //获取集合中的 每一个 Brand 对象
            Brand brand = brands.get(i);
    %>
        <tr align="center">
            <td><%=brand.getId()%></td>
            <td><%=brand.getBrandName()%></td>
            <td><%=brand.getCompanyName()%></td>
            <td><%=brand.getOrdered()%></td>
            <td><%=brand.getDescription()%></td>
            <td><%=brand.getStatus() == 1 ? "启用":"禁用"%></td>
            <td><a href="#">修改</a> <a href="#">删除</a></td>
        </tr>
    <%
        }
    %>
</table>

成品代码如下(可以直接抄进 webapp/brand.jsp):

<%@ page import="com.itheima.pojo.Brand" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    // 查询数据库
    List<Brand> brands = new ArrayList<Brand>();
    brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
    brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
    brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));
%>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input type="button" value="新增"><br>
<hr>
<table border="1" cellspacing="0" width="800">
    <tr>
        <th>序号</th>
        <th>品牌名称</th>
        <th>企业名称</th>
        <th>排序</th>
        <th>品牌介绍</th>
        <th>状态</th>
        <th>操作</th>
    </tr>
    <%
        for (int i = 0; i < brands.size(); i++) {
            Brand brand = brands.get(i);
    %>
    <tr align="center">
        <td><%=brand.getId()%></td>
        <td><%=brand.getBrandName()%></td>
        <td><%=brand.getCompanyName()%></td>
        <td><%=brand.getOrdered()%></td>
        <td><%=brand.getDescription()%></td>
        <td><%=brand.getStatus() == 1 ? "启用":"禁用"%></td>
        <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    <%
        }
    %>
</table>
</body>
</html>

测试:在浏览器地址栏输入 http://localhost:8080/jsp-demo/brand.jsp,页面上就出现了三行品牌数据,第一行三只松鼠(启用)、第二行优衣库(禁用)、第三行小米(启用),最后一列都是"修改 删除"两个链接。

页面是出来了,但你应该已经感觉到哪里不对劲:这个文件里一半是 HTML、一半是 Java,<% 和 %> 把 tr 的缩进都打乱了,读完 for 那一行还要往下找它的右大括号。这正是 8.6 节要清算的问题。

8.5 九大内置对象与四大作用域

8.5.1 什么叫"内置"

回头看一眼 8.3.4 那段翻译出来的代码,_jspService() 的开头有一串局部变量:

final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;

这些变量是 Tomcat 在翻译 JSP 时自动加上去的。它们叫内置对象(也叫隐含对象):容器在翻译期替你声明并赋好值,你在 <% %> 和 <%= %> 里可以直接用,不需要 new,也不需要 request.getSession() 去拿。

一共九个,必须能背出来。按用途分成四组更好记:

内置对象类型从哪来干什么
requestHttpServletRequest_jspService 的形参一次请求:取参数、存 request 域、转发
responseHttpServletResponse_jspService 的形参一次响应:设状态码、设响应头、拿输出流
sessionHttpSessionpageContext.getSession()一次会话:存登录用户等跨请求数据
applicationServletContextpageContext.getServletContext()整个应用:全局配置、全局计数
configServletConfigpageContext.getServletConfig()读 <servlet> 的初始化参数(JSP 里极少用)
outJspWriterpageContext.getOut()向浏览器输出字符(带缓冲区)
pageObjectthis当前 JSP 实例本身,等价于 Servlet 里的 this
pageContextPageContext容器创建页面上下文,能拿到其余八个,也能存取四个域
exceptionThrowablepageContext.getException()异常对象,只有 isErrorPage="true" 的页面才有

记忆口诀:"一请求一响应,一会话一应用,一输出一配置,一页面一上下文一异常"。

【考点】

"九大内置对象"是必背题,答题时按"对象名 + 类型 + 作用"三件套写。容易漏的是 config、page、exception 这三个不常用的;最容易答错的是 exception:它不是每个页面都有,只有 page 指令写了 isErrorPage="true" 的页面才会有,普通页面里用它会编译报错"找不到符号"。

【易错点】 内置对象只在 <% %>、<%= %> 这种脚本里能用,在 <%! %> 声明里不能用(原因见 8.4.4)。另外 out 是 JspWriter 而不是 PrintWriter,两者不是同一个类型,不能互相赋值。

8.5.2 四大作用域

九个对象里有四个是"域对象",它们的共同点是都提供了 setAttribute(String name, Object value)、getAttribute(String name)、removeAttribute(String name) 三个方法,区别只在活得多久、谁能看见:

作用域对应的域对象有效范围生命周期典型用途
pagepageContext当前 JSP 页面当前请求处理完即失效页面内的临时变量
requestrequest当前一次请求(含转发)请求结束即失效Servlet 查出的数据交给 JSP 展示
sessionsession当前一次会话(一个浏览器)会话超时或销毁登录用户信息、购物车
applicationapplication整个应用(所有用户共享)服务器关闭或应用重载全局配置、在线人数

四句话记法:page 是"这一页",request 是"这一次",session 是"这个人",application 是"所有人"。

范围是从小到大的:page ⊂ request ⊂ session ⊂ application。

8.5.3 查找顺序:同名了怎么办

四个域可以存同名的 key,那么取值时取到谁?规则是从小到大、找到即止:

page  →  request  →  session  →  application
(找到就返回,不再往下找)

这一点在 EL 里同样成立(8.7.3 节会正式讲)。看一个能跑的验证:

<%
    pageContext.setAttribute("name", "我是page");
    request.setAttribute("name", "我是request");
    session.setAttribute("name", "我是session");
    application.setAttribute("name", "我是application");
%>
${name}

页面上输出的是 我是page。把第一行删掉,输出变成 我是request;再把第二行删掉,变成 我是session,依次退让。

【易错点】

查找顺序带来的经典 bug:A 页面往 request 里放了 brands,B 页面(另一个功能)往 session 里放了一份同名但内容不同的 brands,于是 A 页面刷新几次之后突然显示成了 B 的数据。因为 A 的 request 域数据在转发结束后就没了,而 session 里的还在,${brands} 就"顺手"取到了 session 里那份。排查办法:不要靠猜,用 ${requestScope.brands} 精确指定域(8.7.5 节),或者干脆换一个不重名的 key。

8.5.4 out 与 response.getWriter() 的冲突

JSP 里有两个"能往浏览器写字"的东西:内置对象 out(JspWriter,带缓冲区),和 response.getWriter()(PrintWriter,直接写)。

【易错点】

在同一个 JSP 里同时用 out.print() 和 response.getWriter().print(),后写的那一段可能完全不出现,也可能顺序颠倒。原因是 JspWriter 有自己的缓冲区(默认 8kb,对应 page 指令的 buffer),内容先攒在缓冲区里,页面结束时才真正刷到 response 的输出流;而 response.getWriter() 是绕过缓冲区直接写的。两者混用,输出顺序就乱了,甚至抛 IllegalStateException。

规则:一个响应只用一个输出流。 在 JSP 里,永远用 out 或 <%= %>,不要碰 response.getWriter()。

8.6 JSP 的缺点与视图技术的演进

8.4.6 的 brand.jsp 已经能运行了,但是它同时暴露了 JSP 的全部问题。这一节把这些问题和它后面的技术走向说清楚:理解了这一节,你才知道为什么还要学 EL 和 JSTL,也才知道为什么今天的新项目不用 JSP。

8.6.1 JSP 的缺点

由于 JSP 页面内既可以定义 HTML 标签,又可以定义 Java 代码,造成了以下问题:

把这几条归纳成一条:JSP 的问题出在非把 Java 和 HTML 写进同一个文件,并不在于"能写 Java"。 两者耦合在一起,任何一方改动都会牵连另一方。

8.6.2 视图技术的四个阶段

由于上述的问题,JSP 已逐渐退出历史舞台,以后开发更多的是使用 HTML + Ajax 来替代。下来对技术的发展进行简单的说明:

  1. 第一阶段:使用 Servlet 既实现逻辑代码编写,也对页面进行拼接。这种模式我们之前也接触过(第 6 章那些 writer.write("<html>"))。
  2. 第二阶段:随着技术的发展,出现了 JSP,人们发现 JSP 使用起来比 Servlet 方便很多,但是还是要在 JSP 中嵌套 Java 代码,也不利于后期的维护。
  3. 第三阶段:使用 Servlet 进行逻辑代码开发,而使用 JSP 进行数据展示。
  4. 第四阶段:使用 Servlet 进行后端逻辑代码开发,而使用 HTML 进行数据展示。而这里面就存在问题,HTML 是静态页面,怎么进行动态数据展示呢?这就是 Ajax 的作用了。

本章走的正是第三阶段:接下来用 EL 表达式和 JSTL 标签库替换掉 JSP 中的 Java 代码。注意,这里替换的是页面里的 Java,"不用 JSP"这种理解是错的。Servlet 做逻辑、JSP 做展示,页面里一个 <% %> 都不留。

8.6.3 那为什么还要学 JSP

既然 JSP 已经逐渐退出历史舞台,为什么还要学?原因有两点:

第二点尤其重要。你只有亲手写过 writer.write("<td>" + brand.getBrandName() + "</td>"),再亲手写过 <c:forEach items="${brands}">,才能体会后者好在哪。技术演进的价值,只有对比过才看得见。

8.6.4 今天的项目用什么

【补充】

第四阶段之后还有第五阶段,也就是今天的主流做法。把三条技术路线对照着看,你就明白 JSP 到底被谁取代了:

方案谁负责渲染页面长什么样代表技术现状
Servlet 拼字符串服务端 Javawriter.write("<td>...")原生 Servlet早已淘汰
JSP + EL + JSTL服务端 JSP 引擎HTML 里嵌 ${} 与 <c:...>JSP 2.3老项目维护,新项目不用
模板引擎服务端模板引擎HTML 里嵌模板语法Thymeleaf、FreeMarker、VelocitySpring Boot 官方推荐 Thymeleaf
前后端分离浏览器里的 JS纯 HTML + JS,数据靠 Ajax 拿Vue、React + RESTful 接口当前主流

为什么模板引擎能取代 JSP?因为 Thymeleaf 的页面可以直接用浏览器打开(它是合法的 HTML,模板属性写在 th:text 这种自定义属性里,浏览器会忽略),前端改样式不需要后端重启、也不需要 JSP 容器。而 JSP 的 <% %> 浏览器根本认不出来,必须部署到 Tomcat 才能看到效果。

为什么前后端分离又能取代模板引擎?因为渲染从服务器搬到了浏览器:后端只返回 JSON({"id":1,"brandName":"三只松鼠"}),Vue 拿到 JSON 后自己拼 DOM。这样后端不用管页面长什么样,前端不用等后端发版,两边可以并行开发,这才是 8.6.1 里"不利于团队协作"那条缺点的最终解法。

顺带回答一个常见疑问:"JSP 是不是被淘汰了、学了没用?" 不是。JSP 的四个域对象、pageContext、EL 的取值规则,在 Thymeleaf 里概念是一模一样的(Thymeleaf 也能取 request、session 里的属性);而"Servlet 做逻辑、模板做展示"的分工思想,正是 8.9 节 MVC 的内核。语法会过时,分层不会。

8.7 EL 表达式

8.7.1 概述

EL(全称 Expression Language)表达式语言,用于简化 JSP 页面内的 Java 代码。

EL 表达式的主要作用是获取数据:从域对象中获取数据,然后将数据展示在页面上。

而 EL 表达式的语法也比较简单,${expression}。例如:${brands} 就是获取域中存储的 key 为 brands 的数据。

对照一下就看出它省了什么。同样是输出品牌名:

<%-- JSP 脚本写法 --%>
<td><%=brand.getBrandName()%></td>

<%-- EL 写法 --%>
<td>${brand.brandName}</td>

省掉了 <%= %>、省掉了 get、省掉了 (),页面看起来就是纯粹的 HTML 加一个占位符。而且前端工程师能看懂 ${brand.brandName},看不懂 <%=brand.getBrandName()%>,这就是"把 Java 从页面里赶出去"的第一层含义:语法更短只是表面收益,页面上不再有 Java 的痕迹才是重点。

8.7.2 代码演示

先定义一个 Servlet,在 Servlet 中封装一些数据并存储到 request 域对象中,并转发到 el-demo.jsp 页面:

@WebServlet("/demo1")
public class ServletDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 准备数据
        List<Brand> brands = new ArrayList<Brand>();
        brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
        brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
        brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));

        //2. 存储到request域中
        request.setAttribute("brands",brands);

        //3. 转发到 el-demo.jsp
        request.getRequestDispatcher("/el-demo.jsp").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

注意:此处需要用转发,因为转发才可以使用 request 对象作为域对象进行数据共享。

在 el-demo.jsp 中通过 EL 表达式获取数据:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    ${brands}
</body>
</html>

在浏览器的地址栏输入 http://localhost:8080/jsp-demo/demo1,页面上显示的是一串 [Brand{id=1, brandName='三只松鼠', ...}, Brand{...}, Brand{...}],它直接把 List 的 toString() 结果打印出来了。

这个结果说明两件事:第一,EL 确实取到了 request 域里的对象;第二,EL 只负责"取",不负责"排版"。要在页面上排成表格,得靠 8.8 节的 <c:forEach>。

8.7.3 四大域与查找顺序

JavaWeb 中有四大域对象,分别是:

EL 表达式获取数据,会依次从这 4 个域中寻找,直到找到为止。例如 ${brands},EL 表达式获取数据,会先从 page 域对象中获取数据,如果没有再到 request 域对象中获取数据,如果再没有再到 session 域对象中获取,如果还没有才会到 application 中获取数据。

顺序:page → request → session → application,找到即停。

【考点】

这个查找顺序是 EL 部分最高频的考点,几乎每年都考。答题时一定要写出"依次从 page、request、session、application 中找,找到就停止"这几个关键词。常见的错误答案是"从 request 开始找":EL 是从 page 开始的,因为 pageContext 也是域对象。

8.7.4 取值规则

EL 能取的东西,本质只有三类:属性、下标、方法结果。逐个看。

(1)取 JavaBean 的属性

${brand.brandName} 的底层会调用 getBrandName() 方法,并不会直接读取字段。规则是:${对象.属性名} 会去找 get属性名()(首字母大写),找不到就找 is属性名()(针对 boolean)。所以:

(2)取数组、List 的元素

用中括号加下标,下标从 0 开始:

${brands[0].brandName}    <%-- 第一个品牌的名称 --%>
${arr[2]}                 <%-- 数组第三个元素 --%>
${brands[10]}             <%-- 越界时返回 null,不抛异常 --%>

(3)取 Map 的值

两种写法等价:

${map.brandName}          <%-- 点号写法,key 是合法标识符时可用 --%>
${map['brandName']}       <%-- 中括号写法,key 含特殊字符时必须用这种 --%>
${map['user-name']}       <%-- key 里有减号,只能用中括号 --%>

(4)嵌套取值

可以一路点下去,中间任何一层是 null,最终结果就是 null(不抛异常):

${brand.owner.address.city}

(5)直接取集合的大小

${brands.size()}          <%-- 调用 List 的 size() 方法 --%>
${fn:length(brands)}      <%-- 或使用 JSTL 函数,见 8.8.6 --%>
【补充】

EL 2.2(Tomcat 7 起)之后,${} 里还可以直接调用方法并传参,例如 ${brand.getBrandName().toUpperCase()}、${brands.get(0).id}。但这是 EL 里唯一"越界"的地方:它让页面重新能写 Java 逻辑,与"把 Java 赶出页面"的初衷相悖。课程与考试只要求掌握属性取值,方法调用知道有这回事即可。

8.7.5 十一个隐式对象

EL 也有自己的"内置对象",一共 11 个。注意:它们与 JSP 的九个内置对象无关,是 EL 引擎在求值时提供的隐式对象,只能写在 ${} 里面。

前五个与"域"有关,后六个与"请求数据"有关:

隐式对象类型作用
pageContextPageContext页面上下文,唯一一个不是 Map 的,能拿到 request、session、servletContext 等
pageScopeMap<String,Object>只从 page 域取值
requestScopeMap<String,Object>只从 request 域取值
sessionScopeMap<String,Object>只从 session 域取值
applicationScopeMap<String,Object>只从 application 域取值
paramMap<String,String>请求参数,等价于 request.getParameter()
paramValuesMap<String,String[]>请求参数数组,等价于 request.getParameterValues()
headerMap<String,String>请求头,等价于 request.getHeader()
headerValuesMap<String,String[]>请求头数组
cookieMap<String,Cookie>所有 Cookie,key 是 cookie 名
initParamMap<String,String>web.xml 里 <context-param> 的全局参数

用法举例:

<%-- 精确指定从 request 域取,避免与 session 里的同名数据混淆 --%>
${requestScope.brands}

<%-- 直接取请求参数:访问 /test?username=zhangsan 时输出 zhangsan --%>
${param.username}

<%-- 取请求参数数组的第一个值:/test?hobby=篮球&hobby=足球 --%>
${paramValues.hobby[0]}

<%-- 取请求头(名字里有减号,必须用中括号) --%>
${header['User-Agent']}

<%-- 取 Cookie 的值:cookie.名字.value --%>
${cookie.JSESSIONID.value}

<%-- 取 web.xml 中的全局参数 --%>
${initParam.pageSize}
【易错点】

${cookie.JSESSIONID} 输出的是 javax.servlet.http.Cookie@1a2b3c 这样的对象地址,不是会话 ID。因为 cookie 这个 Map 的 value 是 Cookie 对象,要拿值必须再加一层 .value。这是初学 EL 时最常见的"输出乱码"现象之一。

【考点】 EL 的 11 个隐式对象与 JSP 的 9 个内置对象经常被混着考。区分要点:JSP 的九个内置对象是 Java 对象,写在 <% %> 里用;EL 的十一个隐式对象是"取数的名字",写在 ${} 里用。 两者的交集是 pageContext:它既是 JSP 内置对象,也是 EL 隐式对象,这也是为什么 ${pageContext.request.contextPath} 能成立。

8.7.6 运算符

EL 支持完整的运算,写法与 Java 基本一致,只是多了几个"字母版"的替代写法:

类别运算符字母写法说明
算术+ - * / %div mod/ 的结果是浮点数,${5/2} 输出 2.5
比较== != < > <= >=eq ne lt gt le ge两种写法完全等价
逻辑&& || !and or not
空判断empty—判断 null、空串、空集合、空数组
三元条件 ? 值1 : 值2—
括号( )—改变优先级

为什么要提供字母版? 因为 < 和 > 在 HTML/XML 里有特殊含义。当你把比较运算写在标签属性里时,尖括号会跟标签的尖括号打架:

<%-- 危险:< 可能被当成标签开始 --%>
<c:if test="${brand.ordered < 100}">

<%-- 安全:用 lt --%>
<c:if test="${brand.ordered lt 100}">

实际上现代的 JSP 解析器大多能正确处理属性里的 <,但养成用 lt/gt 的习惯不吃亏,尤其在老版本容器里能省掉一堆莫名其妙的解析错误。

empty 是 EL 最实用的一个运算符,它一次性覆盖了四种"空":

${empty brands}      <%-- brands 为 null        → true --%>
${empty brands}      <%-- brands 是空集合 size=0 → true --%>
${empty ""}          <%-- 空字符串              → true --%>
${empty arr}         <%-- 空数组                → true --%>
${not empty brands}  <%-- 非空                  → true --%>

列表页里最常用的一行就是 <c:if test="${empty brands}">暂无数据</c:if>。

三元运算符在页面里极其常见,用来把 0/1 翻译成中文:

${brand.status == 1 ? "启用" : "禁用"}
【易错点】

${5 / 2} 输出 2.5 而不是 2。EL 的 / 是浮点除法,两个整数相除也会得到小数。想要整除的结果,要么用 ${5 div 2}(同样是 2.5,div 只是别名),要么在 Java 层就处理好。另外 ${1 + "2"} 输出 3:EL 会做类型转换,字符串 "2" 被转成数字。这类隐式转换在比较时更容易出问题:${"10" > 9} 是 true(转成数字比),但是 ${"10" > "9"} 是 false(都是字符串,按字典序比)。

8.7.7 取不到值的时候:null 与空串

这是 EL 与 <%= %> 行为差异最大的地方,也是考试中容易出错的地方。

规则:EL 求值的结果如果是 null,输出为空字符串,什么都不显示。 它不会输出 "null" 四个字母,也不会抛异常。

对比一下同一个场景:

<%-- request 域里没有 key 为 username 的数据 --%>
<%=request.getAttribute("username")%>   <%-- 输出:null --%>
${username}                             <%-- 输出:(空,什么都没有) --%>

三条细则要背下来:

  1. 域里没有这个 key → ${username} 返回 null → 输出空串。
  2. 对象是 null,去取它的属性 → ${brand.brandName} 中 brand 为 null → 返回 null,不抛 NullPointerException(这是 EL 与 Java 最大的不同,也是它比 <%= %> 安全的地方)。
  3. 对象存在,但属性名写错了 → ${brand.brandNam}(少了个 e)→ 抛异常 javax.el.PropertyNotFoundException: Property [brandNam] not found on type [com.itheima.pojo.Brand],页面 500。

第 3 条特别值得强调:"EL 不报错"只对 null 成立,对"名字写错"不成立。 很多人以为 EL 永远不会报错,于是拼错属性名之后对着一个 500 页面发呆。看到 PropertyNotFoundException 就直接去核对 JavaBean 的 getter 名字。

【补充】

${pageContext.request.contextPath} 是 JSP 里出现频率最高的一句 EL,值得单独讲透。

它做的事是:pageContext → 拿 request 对象 → 调 getContextPath() → 得到当前 Web 应用部署的上下文路径(例如 /brand-demo;如果部署在根路径下,则是空字符串 "")。

为什么需要它?看本章案例里这种写法:

<a href="/brand-demo/selectByIdServlet?id=${brand.id}">修改</a>

路径里写死了 /brand-demo。一旦项目改名、或者部署到别的路径(/shop、/),所有链接全部 404,而且你得到处去改。改成:

<a href="${pageContext.request.contextPath}/selectByIdServlet?id=${brand.id}">修改</a>

部署到 /brand-demo 时它生成 /brand-demo/selectByIdServlet?id=1,部署到 / 时它生成 /selectByIdServlet?id=1,一处都不用改。

为什么不能直接写 /selectByIdServlet?因为 HTML 里以 / 开头的路径是"站点根路径"(http://localhost:8080/),而不是"应用根路径"(http://localhost:8080/brand-demo/)。少了上下文这一段,请求就打到了 Tomcat 的根应用上,404。

一个配套的小技巧:在页面顶部用 <c:set> 把它存成一个短变量,后面就能简写:

<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<a href="${ctx}/selectByIdServlet?id=${brand.id}">修改</a>

【考点】 判断题常考:"JSP 中 ${brand.brandName} 在 brand 为 null 时会不会抛空指针?"答案:不会,EL 返回 null 并输出空串;但如果是 brand 存在而属性名不存在,会抛 PropertyNotFoundException。

8.8 JSTL 标签

EL 解决了"取值",但解决不了"循环"和"判断"。<c:forEach> 就是来补这个缺口的。

8.8.1 概述

JSP 标准标签库(Jsp Standarded Tag Library,简称 JSTL),使用标签取代 JSP 页面上的 Java 代码。如下代码就是 JSTL 标签:

<c:if test="${flag == 1}">
    男
</c:if>
<c:if test="${flag == 2}">
    女
</c:if>

上面代码看起来是不是比 JSP 中嵌套 Java 代码舒服多了?而且前端工程师对标签是特别敏感的,他们看到这段代码是能看懂的。

JSTL 提供了很多标签,按功能分成五类:核心标签(core)、格式化标签(fmt)、SQL 标签、XML 标签、函数标签(functions)。我们只对两个最常用的标签进行讲解:<c:forEach> 标签和 <c:if> 标签。

JSTL 使用也是比较简单的,分为如下三步:

第一步,导入坐标:

<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>

注意这两个依赖都没有 provided,因为 Tomcat 不自带 JSTL,必须打进 war 包。

第二步,在 JSP 页面上引入 JSTL 标签库:

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

第三步,使用标签。 下面两节分别讲 <c:if> 和 <c:forEach>。

【易错点】

忘记写 taglib 指令,是 JSTL 最高频的错误,症状还很迷惑人:

  • 完全没引 taglib:JSP 不认识 <c:if>,会把它当成一个未知的 HTML 标签原样输出。页面上会直接显示出 <c:if test="${flag == 1}"> 男 </c:if> 这行字,不报错,但也没生效。很多人以为是"EL 没取到值",原因在于标签根本没被解析。
  • uri 写错(比如写成 http://java.sun.com/jstl/core,少了个 /jsp):JSP 翻译期直接报错,信息大意是 无法在 web.xml 或使用此应用程序部署的 JAR 中解析绝对 URI: [http://java.sun.com/jstl/core],页面 500。
  • prefix 写错:prefix="c" 但代码里写 <c:if> 却用了 <%@ taglib prefix="jstl" ... %>,症状同第一条,标签原样输出。

记住这个 uri 的准确写法:http://java.sun.com/jsp/jstl/core。它是个"假 URL",浏览器打不开,只是 JSTL 规范约定的唯一标识符。

8.8.2 if 标签

<c:if>:相当于 if 判断。

<c:if test="${flag == 1}">
    男
</c:if>
<c:if test="${flag == 2}">
    女
</c:if>

代码演示。 定义一个 Servlet,在该 Servlet 中向 request 域对象中添加键是 status、值为 1 的数据:

@WebServlet("/demo2")
public class ServletDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 存储数据到request域中
        request.setAttribute("status",1);

        //2. 转发到 jstl-if.jsp
        request.getRequestDispatcher("/jstl-if.jsp").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

定义 jstl-if.jsp 页面,在该页面使用 <c:if> 标签:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <%--
         c:if:来完成逻辑判断,替换 java if else
    --%>
    <c:if test="${status ==1}">
        启用
    </c:if>

    <c:if test="${status ==0}">
        禁用
    </c:if>
</body>
</html>

注意:在该页面一定要引入 JSTL 核心标签库:

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

浏览器访问 /demo2,页面上显示"启用",因为 status 是 1,第一个 <c:if> 成立,第二个不成立。

【易错点】

<c:if> 没有 else。这是从 Java 转过来最容易撞的墙:很多人写 <c:if test="...">启用</c:if><c:else>禁用</c:else>,而 c:else 这个标签根本不存在,JSP 会把它当未知标签原样输出。原文示例之所以写两个 <c:if>(一个判 ==1、一个判 !=1),正是因为这个原因。要写多分支,用下一节的 <c:choose>。

【补充】 <c:if> 除了 test,还有两个可选属性:var(把判断结果存进一个变量)和 scope(存到哪个域,默认 page)。例如:

<c:if test="${brand.status == 1}" var="enabled" scope="request"/>
该品牌是否启用:${enabled}

这种写法在"同一个判断结果要在页面多处复用"时很有用,可以避免把同一个表达式写三遍。

8.8.3 choose / when / otherwise

这是原文没有展开、但实际开发一定会用的一组标签。它才是 JSTL 里的 if-else if-else。

<c:choose>
    <c:when test="${brand.status == 1}">
        <td>启用</td>
    </c:when>
    <c:when test="${brand.status == 0}">
        <td>禁用</td>
    </c:when>
    <c:otherwise>
        <td>未知</td>
    </c:otherwise>
</c:choose>

三条规则:

【对比】

<c:if> 与 <c:choose> 怎么选?只有一个分支、或者两三个互不排斥的独立判断,用 <c:if>;互斥的多分支(只会命中一个),用 <c:choose>。 用三个 <c:if> 去表达互斥的三分支,不仅啰嗦,而且逻辑上允许"同时成立",是隐藏的 bug 源。

8.8.4 forEach 标签(用法一:遍历集合)

<c:forEach>:相当于 for 循环。Java 中有增强 for 循环和普通 for 循环,JSTL 中的 <c:forEach> 也有两种用法。

用法一类似于 Java 中的增强 for 循环。涉及到的 <c:forEach> 中的属性如下:

如下代码,是从域对象中获取名为 brands 的数据(该数据是一个集合),遍历它,并给该集合中的每一个元素起名为 brand(是 Brand 对象)。在循环里面使用 EL 表达式获取每一个 Brand 对象的属性值:

<c:forEach items="${brands}" var="brand">
    <tr align="center">
        <td>${brand.id}</td>
        <td>${brand.brandName}</td>
        <td>${brand.companyName}</td>
        <td>${brand.description}</td>
    </tr>
</c:forEach>

代码演示:Servlet 还是使用之前的名为 ServletDemo1。定义名为 jstl-foreach.jsp 页面,内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input type="button" value="新增"><br>
<hr>
<table border="1" cellspacing="0" width="800">
    <tr>
        <th>序号</th>
        <th>品牌名称</th>
        <th>企业名称</th>
        <th>排序</th>
        <th>品牌介绍</th>
        <th>状态</th>
        <th>操作</th>
    </tr>

    <c:forEach items="${brands}" var="brand" varStatus="status">
        <tr align="center">
            <%--<td>${brand.id}</td>--%>
            <td>${status.count}</td>
            <td>${brand.brandName}</td>
            <td>${brand.companyName}</td>
            <td>${brand.ordered}</td>
            <td>${brand.description}</td>
            <c:if test="${brand.status == 1}">
                <td>启用</td>
            </c:if>
            <c:if test="${brand.status != 1}">
                <td>禁用</td>
            </c:if>
            <td><a href="#">修改</a> <a href="#">删除</a></td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

注意这里的写法:${status.count} 输出的是 1、2、3,而 ${brand.id} 被注释掉了。因为主键是数据库里的 id,可能会跳号(删掉数据后不连续),而"序号"应该永远是 1、2、3。这是业务上常见的一个小细节。

varStatus 提供五个属性,都是只读的:

属性含义示例(第一次循环 / 最后一次)
index当前索引,从 0 开始0 / 2
count当前是第几个,从 1 开始1 / 3
first是否是第一个(boolean)true / false
last是否是最后一个(boolean)false / true
current当前这个元素对象本身第 1 个 Brand 对象

典型用法:

<%-- 隔行变色:偶数行加灰色背景 --%>
<c:forEach items="${brands}" var="brand" varStatus="s">
    <tr <c:if test="${s.index % 2 == 0}">bgcolor="#f2f2f2"</c:if>>
        <td>${s.count}</td>
        <td>${brand.brandName}</td>
    </tr>
</c:forEach>
【易错点】

varStatus 的值是字符串变量名(varStatus="status"),不是 ${status},也不加 ${}。写成 varStatus="${status}" 会直接报错。同理,var="brand" 也是变量名,循环体里才用 ${brand.xxx} 去取。

8.8.5 forEach 标签(用法二:数字循环)

用法二类似于 Java 中的普通 for 循环。涉及到的 <c:forEach> 中的属性如下:

实例代码:从 0 循环到 10,变量名是 i,每次自增 1:

<c:forEach begin="0" end="10" step="1" var="i">
    ${i}
</c:forEach>

三个细节值得注意:

8.8.6 常用函数库 fn

JSTL 还提供了一个函数库(functions),用于在 EL 里做字符串处理,比如判断品牌名里有没有"小米"、截断过长的描述、统计集合大小。

引入方式(注意 uri 和 prefix 都变了):

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

常用函数如下(只能写在 ${} 里面,不是标签):

函数作用示例结果
fn:length(x)集合/数组/字符串的长度${fn:length(brands)}3
fn:contains(s, sub)是否包含子串${fn:contains(brand.description, '好吃')}true
fn:startsWith(s, p)是否以 p 开头${fn:startsWith(brand.brandName, '小米')}true
fn:endsWith(s, p)是否以 p 结尾${fn:endsWith(brand.companyName, '公司')}true
fn:indexOf(s, sub)子串首次出现的下标${fn:indexOf('abcabc', 'b')}1
fn:substring(s, b, e)截取 [b, e)${fn:substring('abcdef', 1, 3)}bc
fn:replace(s, a, b)把 a 替换成 b${fn:replace('a-b', '-', '/')}a/b
fn:split(s, sep)按分隔符切成数组${fn:split('a,b,c', ',')[0]}a
fn:toUpperCase(s)转大写${fn:toUpperCase('abc')}ABC
fn:toLowerCase(s)转小写${fn:toLowerCase('ABC')}abc
fn:trim(s)去掉首尾空格${fn:trim(' a ')}a
fn:escapeXml(s)转义 HTML 特殊字符${fn:escapeXml('<b>')}&lt;b&gt;

两个最实用的场景:

<%-- 描述太长时截断,避免撑破表格 --%>
<td>${fn:length(brand.description) > 20 ? fn:substring(brand.description, 0, 20) : brand.description}</td>

<%-- 转义用户输入,防止 XSS:用户输入 <script> 时原样显示成文字 --%>
<td>${fn:escapeXml(brand.description)}</td>
【易错点】

fn: 前缀的函数不能当标签用。写成 <fn:length value="${brands}"/> 是错的,因为 fn 是函数库不是标签库,它没有标签,只能写在 ${} 内部。另外 fn:length 对 null 返回 0,不会抛异常,所以 ${fn:length(brands)} 在 brands 为 null 时安全地输出 0。

【考点】 JSTL 的五个标签库 uri 只需要记住两个:核心库 http://java.sun.com/jsp/jstl/core(前缀习惯用 c)、函数库 http://java.sun.com/jsp/jstl/functions(前缀习惯用 fn)。考试如果问"怎么引入 JSTL",答案必须是"先加 jstl 与 standard 两个 Maven 坐标(或把 jar 放进 WEB-INF/lib),再在页面顶部写 <%@ taglib %> 指令",两步缺一不可。

8.9 MVC 模式和三层架构

MVC 模式和三层架构是一些理论的知识,将来我们使用它们进行代码开发会让代码的维护性和扩展性更好。这两块内容看着像"背概念",却是 8.10 节案例的主干:不先把职责分清楚,那个案例就会写得很乱。

8.9.1 MVC 模式

MVC 是一种分层开发的模式,其中:

控制器(Servlet)用来接收浏览器发送过来的请求,控制器调用模型(JavaBean)来获取数据,比如从数据库查询数据;控制器获取到数据后再交由视图(JSP)进行数据展示。

MVC 的好处:

把 MVC 和本章的知识对上号:

角色本章里是谁干什么
ControllerSelectAllServlet、AddServlet 等 Servlet收请求、调 Service、存域、转发
ModelBrand、BrandService、BrandMapper数据与业务
Viewbrand.jsp、addBrand.jsp用 EL/JSTL 渲染

8.9.2 三层架构

三层架构是将我们的项目分成了三个层面,分别是表现层、业务逻辑层、数据访问层。

而整个流程是:浏览器发送请求,表现层的 Servlet 接收请求并调用业务逻辑层的方法进行业务逻辑处理,而业务逻辑层方法调用数据访问层方法进行数据的操作,依次返回到 Servlet,然后 Servlet 将数据交由 JSP 进行展示。

三层架构的每一层都有特有的包名称:

层包名
表现层com.itheima.controller 或者 com.itheima.web
业务逻辑层com.itheima.service
数据访问层com.itheima.dao 或者 com.itheima.mapper

后期我们还会学习一些框架,不同的框架是对不同层进行封装的。

【补充】

为什么要分三层,而不是让 Servlet 直接写 SQL?看一个具体的好处:BrandService.selectAll() 这个方法,将来如果被"查询所有品牌"和"导出品牌 Excel"两个功能同时调用,业务逻辑只写一遍。如果 SQL 直接写在 Servlet 里,两个 Servlet 就得各写一遍,改一次规则要改两处,这就是"重复"的代价。分层的本质是把"会变的东西"隔离在各自的层里:换数据库只动 DAO 层,换页面只动 View,换业务规则只动 Service。

8.9.3 MVC 与三层架构的关系

通过 MVC 和三层架构的学习,有些人肯定混淆了。那它们有什么区别和联系?

MVC 模式中的 C(控制器)和 V(视图)就是三层架构中的表现层,而 MVC 模式中的 M(模型)就是三层架构中的业务逻辑层和数据访问层。

可以将 MVC 模式理解成是一个大的概念,而三层架构是对 MVC 模式实现架构的思想。那么我们以后按照要求将不同层的代码写在不同的包下,每一层里功能职责做到单一,将来如果将表现层的技术换掉,而业务逻辑层和数据访问层的代码不需要发生变化。

【对比】

这是本章最容易混淆的一组概念,下面用一张表把差异固定下来:

对比项MVC 模式三层架构
是什么一种设计模式,描述"请求怎么被处理、数据怎么被展示"一种架构分层,描述"代码怎么分包组织"
三个部分Model、View、Controller表现层、业务逻辑层、数据访问层
划分依据按职责角色划分按代码层次划分
对应关系C + V = 表现层;M = 业务逻辑层 + 数据访问层同上,反过来看
关注点请求的流向代码的归属
谁包含谁MVC 是大概念三层架构是 MVC 的实现方式

记忆要点:MVC 回答"谁处理请求、谁展示数据",三层架构回答"这段代码应该放在哪个包"。 两者回答的是同一件事的两个视角,并不构成竞争关系。

【考点】 "MVC 和三层架构有什么区别和联系"是简答题常客。标准答案三段:① MVC 是设计模式,分 Model/View/Controller;三层架构是架构分层,分表现层/业务逻辑层/数据访问层;② MVC 的 C 和 V 对应三层架构的表现层,M 对应业务逻辑层和数据访问层;③ MVC 是大概念,三层架构是它的实现思想,两者结合能让职责单一、便于分工与替换技术。

8.10 综合案例:品牌数据的增删改查

需求:完成品牌数据的增删改查操作。

这个功能我们之前一直在做,而这个案例是将今天学习的所有内容(包含 MVC 模式和三层架构)进行应用,并将整个流程贯穿起来。案例结束后你会发现:属于 JSP 的代码只有四个页面,其余全是 Servlet、Service、Mapper,这就是"分工"的样子。

【对比】

本节案例照原文保留,它的 DAO 层用的是 MyBatis(BrandMapper 接口 + BrandMapper.xml 映射)。而本讲义第10章讲的是 JDBC,第11章会用 JDBC 把同一个 tb_brand 案例从头到尾重做一遍。两处的关系是:业务逻辑、Servlet、JSP、过滤器完全一样,只有 DAO 层的实现换了一套写法。所以你有两条路:想先理解"三层怎么串起来",就读本节;想和本讲义已讲的 JDBC 知识衔接起来,就先跳到第11章,再回来看本节 8.10.2~8.10.5 里的 JSP 渲染部分。第11章 11.4.6 有一个【对比】块,专门列出"把本节这套 MyBatis 写法换成 JDBC 要改哪三处、哪些文件一行都不用动"。

8.10.1 环境准备

环境准备工作,分以下步骤实现:创建新的模块 brand_demo 并引入坐标;创建三层架构的包结构;创建数据库表 tb_brand;创建实体类 Brand;准备 MyBatis 基础环境(Mybatis-config.xml、BrandMapper.xml、BrandMapper 接口)。

(1)创建工程。 创建新的模块 brand_demo,引入坐标。我们只要分析出要用到哪些技术,那么需要哪些坐标也就明确了:

pom.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>brand-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>

        <!--servlet-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <!--jsp-->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>

        <!--jstl-->
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>
        </plugins>
    </build>
</project>

(2)创建包。 创建不同的包结构,用来存储不同的类,包结构如下:

brand-demo
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com/itheima
        │       ├── mapper    BrandMapper.java
        │       ├── pojo      Brand.java
        │       ├── service   BrandService.java
        │       ├── utils     SqlSessionFactoryUtils.java
        │       └── web       SelectAllServlet.java、AddServlet.java 等
        ├── resources
        │   ├── mybatis-config.xml
        │   └── com/itheima/mapper/BrandMapper.xml
        └── webapp
            ├── index.html
            ├── brand.jsp
            ├── addBrand.jsp
            └── update.jsp

(3)创建表。 建库建表语句如下:

-- 删除tb_brand表
drop table if exists tb_brand;
-- 创建tb_brand表
create table tb_brand
(
    -- id 主键
    id           int primary key auto_increment,
    -- 品牌名称
    brand_name   varchar(20),
    -- 企业名称
    company_name varchar(20),
    -- 排序字段
    ordered      int,
    -- 描述信息
    description  varchar(100),
    -- 状态:0:禁用 1:启用
    status       int
);

-- 添加数据
insert into tb_brand (brand_name, company_name, ordered, description, status)
values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
       ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
       ('小米', '小米科技有限公司', 50, 'are you ok', 1);

(4)创建实体类。 在 pojo 包下创建名为 Brand 的类:

public class Brand {
    // id 主键
    private Integer id;
    // 品牌名称
    private String brandName;
    // 企业名称
    private String companyName;
    // 排序字段
    private Integer ordered;
    // 描述信息
    private String description;
    // 状态:0:禁用 1:启用
    private Integer status;

    public Brand() {
    }

    public Brand(Integer id, String brandName, String companyName, String description) {
        this.id = id;
        this.brandName = brandName;
        this.companyName = companyName;
        this.description = description;
    }

    public Brand(Integer id, String brandName, String companyName, Integer ordered, String description, Integer status) {
        this.id = id;
        this.brandName = brandName;
        this.companyName = companyName;
        this.ordered = ordered;
        this.description = description;
        this.status = status;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBrandName() {
        return brandName;
    }

    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public Integer getOrdered() {
        return ordered;
    }

    public void setOrdered(Integer ordered) {
        this.ordered = ordered;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return "Brand{" +
                "id=" + id +
                ", brandName='" + brandName + '\'' +
                ", companyName='" + companyName + '\'' +
                ", ordered=" + ordered +
                ", description='" + description + '\'' +
                ", status=" + status +
                '}';
    }
}

(5)准备 MyBatis 环境。 定义核心配置文件 mybatis-config.xml,并将该文件放置在 resources 下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--起别名-->
    <typeAliases>
        <package name="com.itheima.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///db1?useSSL=false&amp;useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="1234"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--扫描mapper-->
        <package name="com.itheima.mapper"/>
    </mappers>
</configuration>

在 resources 下创建放置映射配置文件的目录结构 com/itheima/mapper,并在该目录下创建映射配置文件 BrandMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.BrandMapper">

</mapper>

8.10.2 查询所有

当我们点击 index.html 页面中的"查询所有"这个超链接时,就能查询到品牌列表的数据。点击"查询所有"超链接需要先请求后端的 Servlet,由 Servlet 跳转到对应的页面进行数据的动态展示。

(1)编写 BrandMapper。 在 mapper 包下创建 BrandMapper 接口,在接口中定义 selectAll() 方法:

/**
 * 查询所有
 * @return
 */
@Select("select * from tb_brand")
List<Brand> selectAll();

(2)编写工具类。 在 com.itheima 包下创建 utils 包,并在该包下创建名为 SqlSessionFactoryUtils 工具类:

public class SqlSessionFactoryUtils {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        //静态代码块会随着类的加载而自动执行,且只执行一次
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSessionFactory getSqlSessionFactory(){
        return sqlSessionFactory;
    }
}
【补充】

为什么不每个方法里都 new SqlSessionFactoryBuilder().build(...) 一次?因为构建 SqlSessionFactory 要解析整个 XML、建立数据库连接池配置,是重量级操作,而 SqlSessionFactory 本身是线程安全的、可以复用。把它放进 static 代码块,"类加载时执行一次",整个应用就只有一份。这是"静态代码块 + 单例"的经典写法,第 10 章 JDBC 里的连接池也是同一个思路。

(3)编写 BrandService。 在 service 包下创建 BrandService 类:

public class BrandService {

    SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();

    /**
     * 查询所有
     * @return
     */
    public List<Brand> selectAll(){
        //调用BrandMapper.selectAll()

        //2. 获取SqlSession
        SqlSession sqlSession = factory.openSession();
        //3. 获取BrandMapper
        BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

        //4. 调用方法
        List<Brand> brands = mapper.selectAll();

        sqlSession.close();

        return brands;
    }
}

(4)编写 Servlet。 在 web 包下创建名为 SelectAllServlet 的 Servlet,这个 Servlet 的逻辑如下:调用 BrandService 的 selectAll() 方法进行业务逻辑处理,并接收返回的结果;将上一步返回的结果存储到 request 域对象中;跳转到 brand.jsp 页面进行数据的展示。

@WebServlet("/selectAllServlet")
public class SelectAllServlet extends HttpServlet {
    private BrandService service = new BrandService();

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

        //1. 调用BrandService完成查询
        List<Brand> brands = service.selectAll();
        //2. 存入request域中
        request.setAttribute("brands",brands);
        //3. 转发到brand.jsp
        request.getRequestDispatcher("/brand.jsp").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(5)编写 brand.jsp 页面。 将静态页面 brand.html 拷贝到项目的 webapp 目录下,并将该页面改成 brand.jsp 页面,在表格中使用 JSTL 和 EL 表达式从 request 域对象中获取名为 brands 的集合数据并展示出来:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<hr>
<table border="1" cellspacing="0" width="80%">
    <tr>
        <th>序号</th>
        <th>品牌名称</th>
        <th>企业名称</th>
        <th>排序</th>
        <th>品牌介绍</th>
        <th>状态</th>
        <th>操作</th>
    </tr>

    <c:forEach items="${brands}" var="brand" varStatus="status">
        <tr align="center">
            <td>${status.count}</td>
            <td>${brand.brandName}</td>
            <td>${brand.companyName}</td>
            <td>${brand.ordered}</td>
            <td>${brand.description}</td>
            <c:if test="${brand.status == 1}">
                <td>启用</td>
            </c:if>
            <c:if test="${brand.status != 1}">
                <td>禁用</td>
            </c:if>
            <td><a href="${pageContext.request.contextPath}/selectByIdServlet?id=${brand.id}">修改</a> <a href="#">删除</a></td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

可以看到整页没有一行 Java 代码:循环靠 <c:forEach>,判断靠 <c:if>,取值靠 ${}。和 8.4.6 那个 brand.jsp 对比一下,这就是本章要走的距离。

(6)测试。 启动服务器,并在浏览器输入 http://localhost:8080/brand-demo/index.html,看到"查询所有"的超链接,点击该链接就可以查询出所有的品牌数据。

如果这时你看到品牌名称、企业名称两列是空的,原因是查询到的字段名和实体类对象的属性名没有一一对应:数据库里叫 brand_name、company_name,Java 里叫 brandName、companyName,MyBatis 默认按同名映射,对不上就填 null。解决办法是在映射配置文件中使用 resultMap 标签定义映射关系。映射配置文件内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.BrandMapper">

    <resultMap id="brandResultMap" type="brand">
        <result column="brand_name" property="brandName"></result>
        <result column="company_name" property="companyName"></result>
    </resultMap>
</mapper>

并且在 BrandMapper 接口中的 selectAll() 上使用 @ResultMap 注解指定使用该映射:

/**
 * 查询所有
 * @return
 */
@Select("select * from tb_brand")
@ResultMap("brandResultMap")
List<Brand> selectAll();

重启服务器,再次访问就能看到我们想要的数据了。

【易错点】

这里有两处拼写细节会让人卡半小时:type="brand" 用的是别名(因为 mybatis-config.xml 里配了 <package name="com.itheima.pojo"/>),如果写成全限定名 com.itheima.pojo.Brand 也可以;注解名是 @ResultMap(Result + Map),@Resultmap 与 @ResuleMap 都是错的写法。另外,resultMap 的 id 必须和 @ResultMap("...") 里写的字符串完全一致,多一个空格都会报"Result Maps collection does not contain value for ..."。

8.10.3 添加

点击"新增"按钮后,会先跳转到 addBrand.jsp 新增页面,在该页面输入要添加的数据,输入完毕后点击"提交"按钮,将数据提交到后端,后端进行数据添加操作,并重新将所有的数据查询出来。

(1)编写 BrandMapper 方法。 在 BrandMapper 接口中定义 add(Brand brand) 方法:

@Insert("insert into tb_brand values(null,#{brandName},#{companyName},#{ordered},#{description},#{status})")
void add(Brand brand);

(2)编写 BrandService 方法。 在 BrandService 类中定义添加品牌数据方法 add(Brand brand):

/**
 * 添加
 * @param brand
 */
public void add(Brand brand){

    //2. 获取SqlSession
    SqlSession sqlSession = factory.openSession();
    //3. 获取BrandMapper
    BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

    //4. 调用方法
    mapper.add(brand);

    //提交事务
    sqlSession.commit();
    //释放资源
    sqlSession.close();
}
【易错点】

增删改操作必须 commit()。MyBatis 的 openSession() 默认是手动提交事务的,如果你只调了 mapper.add(brand) 就 close(),数据库里什么都不会变,而且不报错,你会对着"添加成功"的页面困惑半天。查询不用 commit,是因为查询不改变数据。另一种写法是 factory.openSession(true),参数 true 表示自动提交。

(3)改进 brand.jsp 页面。 需要在表格上面添加"新增"按钮:

<input type="button" value="新增" id="add"><br>

并给该按钮绑定单击事件,当点击了该按钮需要跳转到添加品牌数据的页面:

<script>
    document.getElementById("add").onclick = function (){
        location.href = "${pageContext.request.contextPath}/addBrand.jsp";
    }
</script>

注意:这个 script 标签建议放在 body 结束标签前面。

(4)编写 addBrand.jsp 页面。 从静态页面 addBrand.html 拷贝到项目的 webapp 下,并改成 addBrand.jsp 动态页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>添加品牌</title>
</head>
<body>
<h3>添加品牌</h3>
<form action="${pageContext.request.contextPath}/addServlet" method="post">
    品牌名称:<input name="brandName"><br>
    企业名称:<input name="companyName"><br>
    排序:<input name="ordered"><br>
    描述信息:<textarea rows="5" cols="20" name="description"></textarea><br>
    状态:
    <input type="radio" name="status" value="0">禁用
    <input type="radio" name="status" value="1">启用<br>

    <input type="submit" value="提交">
</form>
</body>
</html>

(5)编写 Servlet。 在 web 包下创建 AddServlet,这个 Servlet 的逻辑如下:设置处理 POST 请求乱码的字符集;接收客户端提交的数据;将接收到的数据封装到 Brand 对象中;调用 BrandService 的 add() 方法进行添加的业务逻辑处理;跳转到 selectAllServlet 资源重新查询数据。

@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {
    private BrandService service = new BrandService();

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

        //处理POST请求的乱码问题
        request.setCharacterEncoding("utf-8");

        //1. 接收表单提交的数据,封装为一个Brand对象
        String brandName = request.getParameter("brandName");
        String companyName = request.getParameter("companyName");
        String ordered = request.getParameter("ordered");
        String description = request.getParameter("description");
        String status = request.getParameter("status");

        //封装为一个Brand对象
        Brand brand = new Brand();
        brand.setBrandName(brandName);
        brand.setCompanyName(companyName);
        brand.setOrdered(Integer.parseInt(ordered));
        brand.setDescription(description);
        brand.setStatus(Integer.parseInt(status));

        //2. 调用service 完成添加
        service.add(brand);

        //3. 转发到查询所有Servlet
        request.getRequestDispatcher("/selectAllServlet").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(6)测试。 点击 brand.jsp 页面的"新增"按钮,会跳转到 addBrand.jsp 页面;点击"提交"按钮,就能看到包含刚添加数据的新列表页。

注意最后一步用的是 getRequestDispatcher(...).forward(...) 而不是 sendRedirect:因为转发是服务器内部跳转,浏览器地址栏还停在 /addServlet,刷新页面会重复提交表单。真实项目里解决"表单重复提交"用的正是第 9 章的 Filter 或者 PRG(Post-Redirect-Get)模式,这里先埋个伏笔。

8.10.4 修改

点击每条数据后面的"修改"按钮会跳转到修改页面。在修改页面可以看到当前行的数据回显到表单,修改完点击"提交"按钮将数据提交到后端,后端再将数据存储到数据库中。

所以修改功能要从两方面实现:数据回显和修改操作。

(一)回显数据

当点击"修改"按钮时不能直接跳转到 update.jsp 页面,而是需要先带着当前数据的 id 请求后端程序,后端程序根据 id 查询数据,将数据存储到域对象中跳转到 update.jsp 页面进行数据展示。这就是 brand.jsp 里那个 href 为什么要带 ?id=${brand.id}。

(1)编写 BrandMapper 方法。 在 BrandMapper 接口中定义 selectById(int id) 方法:

/**
 * 根据id查询
 * @param id
 * @return
 */
@Select("select * from tb_brand where id = #{id}")
@ResultMap("brandResultMap")
Brand selectById(int id);

(2)编写 BrandService 方法。 在 BrandService 类中定义根据 id 查询数据方法 selectById(int id):

/**
 * 根据id查询
 * @return
 */
public Brand selectById(int id){
    //调用BrandMapper.selectAll()
    //2. 获取SqlSession
    SqlSession sqlSession = factory.openSession();
    //3. 获取BrandMapper
    BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
    //4. 调用方法
    Brand brand = mapper.selectById(id);
    sqlSession.close();
    return brand;
}

(3)编写 Servlet。 在 web 包下创建 SelectByIdServlet,这个 Servlet 的逻辑如下:获取请求数据 id;调用 BrandService 的 selectById() 方法进行数据查询的业务逻辑;将查询到的数据存储到 request 域对象中;跳转到 update.jsp 页面进行数据展示。

@WebServlet("/selectByIdServlet")
public class SelectByIdServlet extends HttpServlet {
    private BrandService service = new BrandService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 接收id
        String id = request.getParameter("id");
        //2. 调用service查询
        Brand brand = service.selectById(Integer.parseInt(id));
        //3. 存储到request中
        request.setAttribute("brand",brand);
        //4. 转发到update.jsp
        request.getRequestDispatcher("/update.jsp").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(4)编写 update.jsp 页面。 拷贝 addBrand.jsp 页面,改名为 update.jsp,并做出以下修改:title 标签内容改为"修改品牌";form 标签的 action 属性值改为 updateServlet;input 标签要进行数据回显,需要设置 value 属性。

品牌名称:<input name="brandName" value="${brand.brandName}"><br>
企业名称:<input name="companyName" value="${brand.companyName}"><br>
排序:<input name="ordered" value="${brand.ordered}"><br>

textarea 标签要进行数据回显,需要在标签体中使用 EL 表达式:

描述信息:<textarea rows="5" cols="20" name="description">${brand.description} </textarea><br>

单选框使用 if 标签需要判断 brand.status 的值是 1 还是 0,在指定的单选框上使用 checked 属性,表示被选中状态:

状态:
<c:if test="${brand.status == 0}">
    <input type="radio" name="status" value="0" checked>禁用
    <input type="radio" name="status" value="1">启用<br>
</c:if>

<c:if test="${brand.status == 1}">
    <input type="radio" name="status" value="0" >禁用
    <input type="radio" name="status" value="1" checked>启用<br>
</c:if>
【易错点】

textarea 的回显值必须写在开始标签和结束标签之间,不能写成 value 属性:

<!-- 错误:textarea 没有 value 属性,用户会看到一个空框 -->
<textarea name="description" value="${brand.description}"></textarea>

<!-- 正确 -->
<textarea name="description">${brand.description}</textarea>

因为 <textarea> 在 HTML 规范里是"双标签内容型"控件,它的值就是标签体里的文本。同样属于"内容型"回显的还有 <select> 的 <option selected>,而 <input>、<radio>、<checkbox> 才是用 value 和 checked 属性。

(二)修改数据

在修改页面进行数据修改,点击"提交"按钮,会将数据提交到后端程序,后端程序会对表中的数据进行修改操作,然后重新进行数据的查询操作。

(1)编写 BrandMapper 方法。 在 BrandMapper 接口中定义 update(Brand brand) 方法:

/**
 * 修改
 * @param brand
 */
@Update("update tb_brand set brand_name = #{brandName},company_name = #{companyName},ordered = #{ordered},description = #{description},status = #{status} where id = #{id}")
void update(Brand brand);

(2)编写 BrandService 方法。 在 BrandService 类中定义修改方法 update(Brand brand):

/**
 * 修改
 * @param brand
 */
public void update(Brand brand){
    //2. 获取SqlSession
    SqlSession sqlSession = factory.openSession();
    //3. 获取BrandMapper
    BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
    //4. 调用方法
    mapper.update(brand);
    //提交事务
    sqlSession.commit();
    //释放资源
    sqlSession.close();
}

(3)编写 Servlet。 在 web 包下创建 UpdateServlet,这个 Servlet 的逻辑如下:设置处理 POST 请求乱码的字符集;接收客户端提交的数据;将接收到的数据封装到 Brand 对象中;调用 BrandService 的 update() 方法进行修改的业务逻辑处理;跳转到 selectAllServlet 资源重新查询数据。

@WebServlet("/updateServlet")
public class UpdateServlet extends HttpServlet {
    private BrandService service = new BrandService();

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

        //处理POST请求的乱码问题
        request.setCharacterEncoding("utf-8");
        //1. 接收表单提交的数据,封装为一个Brand对象
        String id = request.getParameter("id");
        String brandName = request.getParameter("brandName");
        String companyName = request.getParameter("companyName");
        String ordered = request.getParameter("ordered");
        String description = request.getParameter("description");
        String status = request.getParameter("status");

        //封装为一个Brand对象
        Brand brand = new Brand();
        brand.setId(Integer.parseInt(id));
        brand.setBrandName(brandName);
        brand.setCompanyName(companyName);
        brand.setOrdered(Integer.parseInt(ordered));
        brand.setDescription(description);
        brand.setStatus(Integer.parseInt(status));

        //2. 调用service 完成修改
        service.update(brand);

        //3. 转发到查询所有Servlet
        request.getRequestDispatcher("/selectAllServlet").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(4)存在的问题与修复。 update.jsp 页面提交数据时是没有携带主键数据的,而后台修改数据需要根据主键进行修改。

针对这个问题,我们不希望页面将主键 id 展示给用户看,但是又希望在提交数据时能将主键 id 提交到后端。此时就想到了在学习 HTML 时学到的隐藏域,在 update.jsp 页面的表单中添加如下代码:

<%--隐藏域,提交id--%>
<input type="hidden" name="id" value="${brand.id}">

update.jsp 页面的最终代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改品牌</title>
</head>
<body>
<h3>修改品牌</h3>
<form action="${pageContext.request.contextPath}/updateServlet" method="post">

    <%--隐藏域,提交id--%>
    <input type="hidden" name="id" value="${brand.id}">

    品牌名称:<input name="brandName" value="${brand.brandName}"><br>
    企业名称:<input name="companyName" value="${brand.companyName}"><br>
    排序:<input name="ordered" value="${brand.ordered}"><br>
    描述信息:<textarea rows="5" cols="20" name="description">${brand.description} </textarea><br>
    状态:
    <c:if test="${brand.status == 0}">
        <input type="radio" name="status" value="0" checked>禁用
        <input type="radio" name="status" value="1">启用<br>
    </c:if>

    <c:if test="${brand.status == 1}">
        <input type="radio" name="status" value="0" >禁用
        <input type="radio" name="status" value="1" checked>启用<br>
    </c:if>
    <input type="submit" value="提交">
</form>
</body>
</html>
【易错点】

隐藏域只是看不见,不是安全。用户按 F12 就能改 value,所以"修改别人的品牌"这种越权操作,后端必须再校验一次"这条记录是不是属于当前登录用户"。同理,type="hidden" 提交的值在用 Integer.parseInt() 之前要判空:如果用户手工删掉这个 input,request.getParameter("id") 返回 null,parseInt(null) 会抛 NumberFormatException,页面直接 500。

8.10.5 删除

删除与前面三个功能的固定流程完全一致,只是 SQL 从 update 换成了 delete、参数从 Brand 对象换成了一个 id。按同样的五步走一遍。

(1)编写 BrandMapper 方法:

/**
 * 根据id删除
 * @param id
 */
@Delete("delete from tb_brand where id = #{id}")
void deleteById(int id);

(2)编写 BrandService 方法:

/**
 * 删除
 * @param id
 */
public void deleteById(int id){
    SqlSession sqlSession = factory.openSession();
    BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
    mapper.deleteById(id);
    //提交事务
    sqlSession.commit();
    sqlSession.close();
}

(3)编写 Servlet:

@WebServlet("/deleteServlet")
public class DeleteServlet extends HttpServlet {
    private BrandService service = new BrandService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 接收id
        String id = request.getParameter("id");
        //2. 调用service删除
        service.deleteById(Integer.parseInt(id));
        //3. 转发到查询所有Servlet
        request.getRequestDispatcher("/selectAllServlet").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(4)修改 brand.jsp 的删除链接:

<td>
    <a href="${pageContext.request.contextPath}/selectByIdServlet?id=${brand.id}">修改</a>
    <a href="${pageContext.request.contextPath}/deleteServlet?id=${brand.id}"
       onclick="return confirm('确定要删除【${brand.brandName}】吗?')">删除</a>
</td>

(5)测试。 点击"删除",浏览器弹出确认框,点"确定"后该行消失,列表重新加载。

onclick 里的 return confirm(...) 是这里的关键:confirm 会弹出"确定/取消"对话框并返回布尔值,onclick 返回 false 时浏览器会取消这次跳转。少了 return,不管用户点确定还是取消,链接都会跳转,这就是最典型的"确认框形同虚设"的 bug。

8.10.6 一次请求的完整链路

把"查询所有"这条链路完整走一遍,本章所有知识点都在里面:

浏览器点击"查询所有"
   │  GET /brand-demo/selectAllServlet          ← 第4章 HTTP
   ▼
Tomcat 按 @WebServlet("/selectAllServlet") 找到 Servlet   ← 第5章
   │
   ▼
SelectAllServlet.doGet()                        ← 表现层(Controller)
   │  ① service.selectAll()
   ▼
BrandService.selectAll()                        ← 业务逻辑层
   │  ② factory.openSession() → getMapper() → mapper.selectAll()
   ▼
BrandMapper.selectAll()  @Select("select * from tb_brand")  ← 数据访问层
   │  ③ JDBC 执行 SQL,resultMap 把 brand_name 映射到 brandName
   ▼
List<Brand> 沿原路返回
   │
   ▼
request.setAttribute("brands", brands)           ← 存进 request 域
   │
   ▼
request.getRequestDispatcher("/brand.jsp").forward(...)   ← 转发(第6章)
   │
   ▼
Tomcat 翻译/执行 brand_jsp.java 的 _jspService()   ← JSP 原理
   │  <c:forEach items="${brands}"> → EL 从 request 域取 brands
   ▼
out.write("<tr>...<td>三只松鼠</td>...")          ← 生成 HTML
   │
   ▼
浏览器收到 HTML,渲染出表格

对照这张图检查一下:哪一步属于 JSP?只有最后两步。 这就是 MVC 与三层架构落到代码里的样子。

【考点】

综合题常问"描述一次 JSP 页面的请求处理流程"。照上面这张图从 HTTP 请求讲到浏览器渲染,答出"Servlet 收请求 → Service → Mapper → 存 request 域 → 转发 JSP → JSP 翻译执行 → 输出 HTML"这七个环节,就能拿满分。

本章小结

本章的主线只有一条:把 HTML 从 Java 里解放出来(JSP),再把 Java 从 HTML 里赶出去(EL + JSTL),最后把两边的职责固定下来(MVC + 三层架构)。

主题必须记住的结论
JSP 的本质JSP 会被翻译成 xxx_jsp.java,继承 HttpJspBase,间接继承 HttpServlet,本质是 Servlet
执行流程翻译 → 编译 → 加载 → 执行,产物在 work\Tomcat\localhost\<项目>\org\apache\jsp
执行方法_jspService(),等价于 Servlet 的 service(),每次请求执行一次
五种组成指令 <%@ %>、脚本 <% %>、表达式 <%= %>、声明 <%! %>、注释 <%-- --%>
九大内置对象request、response、session、application、config、out、page、pageContext、exception
四大作用域page ⊂ request ⊂ session ⊂ application,取值顺序从小到大
EL${} 只能取值;取不到返回 null 并输出空串;属性名写错抛 PropertyNotFoundException
EL 11 个隐式对象pageContext、4 个 Scope、param/paramValues、header/headerValues、cookie、initParam
JSTL两个坐标 + <%@ taglib %>;<c:if> 无 else,多分支用 <c:choose>
<c:forEach>用法一 items/var/varStatus,用法二 begin/end/step/var;count 从 1、index 从 0
MVCModel / View / Controller,Servlet 当 C,JSP 当 V
三层架构表现层 / 业务逻辑层 / 数据访问层,web / service / mapper
两者关系C + V = 表现层,M = 业务逻辑层 + 数据访问层;MVC 是大概念

这章的知识在下章哪里被用到:

最后记住这条演进线,它是本章的"为什么":Servlet 拼字符串(第一阶段)→ JSP 里嵌 Java(第二阶段)→ Servlet 管逻辑、JSP 管展示(第三阶段,本章)→ HTML + Ajax(第四阶段)→ Thymeleaf / Vue 前后端分离(今天)。 每一步都是被上一步的痛推着走的。

自测

基础题

  1. 简述 JSP 的本质,并写出访问一个 JSP 时 Tomcat 内部经历的四个步骤。
  2. 写出 JSP 的九大内置对象。其中哪四个是域对象?它们的有效范围分别是什么?
  3. <% %>、<%= %>、<%! %> 三种脚本,翻译成 Java 后分别落在生成类的什么位置?
  4. 写出 EL 中与"域"有关的 5 个隐式对象,并说明 ${brands} 的查找顺序。
  5. page 指令的 contentType 和 pageEncoding 有什么区别?只写一个会有什么后果?
  6. 在 JSP 里使用 JSTL 需要哪两步?写出核心库的 taglib 指令。

进阶题

  1. 读代码写结果。Servlet 中执行 request.setAttribute("name","A"); session.setAttribute("name","B");,然后转发到 JSP,页面中写 ${name}、${sessionScope.name}、${requestScope.name},三个输出分别是什么?
  2. 找错并改正:
<c:if test="${brand.status == 1}">
    <td>启用</td>
</c:if>
<c:else>
    <td>禁用</td>
</c:else>
  1. 读代码写结果:
<c:forEach begin="1" end="5" step="2" var="i">${i}</c:forEach>
  1. 页面里有 <a href="/brand-demo/selectAllServlet">查询</a>,把项目部署到 /shop 之后链接 404 了。请说明原因并给出两种改法。
  2. 下面这段 JSP 有什么问题?会导致什么现象?
<%! int count = 0; %>
<% count++; %>
你是第 <%=count%> 位访问者

参考答案

  1. JSP 本质是 Servlet:Tomcat 把 .jsp 翻译成 xxx_jsp.java(继承 HttpJspBase,间接继承 HttpServlet),再编译成 .class 执行。四步:翻译 → 编译 → 加载 → 执行。
  2. request、response、session、application、config、out、page、pageContext、exception。域对象是 page(当前页面)、request(一次请求)、session(一次会话)、application(整个应用)。
  3. <% %> → _jspService() 方法体内;<%= %> → out.print(...) 的参数;<%! %> → 类体中,成为成员变量或成员方法。
  4. pageContext、pageScope、requestScope、sessionScope、applicationScope。顺序:page → request → session → application,找到即停。
  5. contentType 写给浏览器,决定响应体的 MIME 与编码;pageEncoding 写给容器,决定 Tomcat 用什么编码读 .jsp 源文件。只写 contentType 时容器会退而用它的 charset 读源文件;两个都不写则按 ISO-8859-1 读,源码中文会乱码。
  6. ① 引入 Maven 坐标 jstl:jstl:1.2 与 taglibs:standard:1.1.2(或把 jar 放入 WEB-INF/lib);② 页面顶部写 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。
  7. A、B、A。${name} 按 page→request→session→application 查找,request 里有所以取 A。
  8. <c:else> 不存在。改为两个 <c:if>(一个 ==1、一个 !=1),或用 <c:choose><c:when test="...">…</c:when><c:otherwise>…</c:otherwise></c:choose>。
  9. 135。begin/end 闭区间、step 为 2,输出 1、3、5。
  10. 原因:/ 开头是站点根路径,不是应用根路径,部署到 /shop 后请求打到了 http://localhost:8080/selectAllServlet。改法一:${pageContext.request.contextPath}/selectAllServlet;改法二:用相对路径 selectAllServlet(同目录下),或绝对 URL 写全 /shop/selectAllServlet(不推荐,路径写死)。
  11. count 是成员变量,所有请求共享,count++ 不是原子操作,并发下会少计数;且服务器重启后归零。应改用 application 域或数据库计数。