Java Web 开发详解

一、Web基础

1、Web 概述

Web 在英文中的含义是网状物、网络。在计算机领域,它通常指的是后者,即网络。

像 WWW 是由 3 个单词组成的,即World Wide Web,中文含义是万维网。

他们的出现都是为了让我们在网络的世界中获取资源,这些资源的存放之处,我们称之为网站。我们通过输入网站的地址(即网址),就可以访问网站中提供的资源。

在网上我们能访问到的内容全是资源(不区分局域网还是广域网)。只不过,不同类型的资源展示的效果不一样。资源可以分为静态资源和动态资源:

  • 静态资源指的是,网站中提供给人们展示的资源是一成不变的,也就是说不同人或者在不同时间,看到的内容都是一样的。例如:我们看到的新闻,网站的使用手册,网站功能说明文档等等。而作为开发者,我们编写的 html、css、js、图片、多媒体等,都可以称为静态资源。

  • 动态资源指的是,网站中提供给人们展示的资源是由程序产生的,在不同的时间或者用不同的人员由于身份的不同,所看到的内容是不一样的。例如:我们在12306上购买火车票,火车票的余票数由于时间的变化,会逐渐的减少,直到最后没有余票。还有,我们在 CSDN 上下载资料,只有登录成功后,且积分足够时才能下载。否则就不能下载,这就是访客身份和会员身份的区别。作为开发人员,我们编写的 JSP、servlet、php、ASP 等都是动态资源。

关于广域网和局域网的划分,广域网指的就是万维网,也就是我们说的互联网;局域网是指的是在一定范围之内可以访问的网络,出了这个范围,就不能再使用的网络。

2、系统结构

  • 根据基础结构划分:C/S 结构,B/S 结构两类。
  • 根据技术选型划分:Model1 模型,Model2 模型,MVC 模型、三层架构 + MVC 模型。
  • 根据部署方式划分:一体化架构,垂直拆分架构,分布式架构,流动计算架构,微服务架构。

1. C/S 结构

它指的是客户端——服务器的方式,其中 C 代表着 Client,S 代表着服务器。

C/S 结构的系统设计图如下:

2. B/S 结构

它指的是浏览器——服务器的方式,其中 B 代表着 Browser,S 代表着服务器。

B/S 结构的系统设计图如下:

3. 两种结构的区别及优略

两种结构的区别:

  1. 硬件环境不同:C/S 通常是建立在专用的网络或小范围的网络环境上(即局域网),且必须要安装客户端;而 B/S 是建立在广域网上的,适应范围强,通常有操作系统和浏览器就行。

  2. C/S 结构比 B/S 结构更安全,因为用户群相对固定,对信息的保护更强。

  3. B/S 结构维护升级比较简单,而 C/S 结构维护升级相对困难。

优势:

  1. C/S:是能充分发挥客户端PC的处理能力,很多工作可以在客户端处理后再提交给服务器。对应的优点就是客户端响应速度快。

  2. B/S:总体拥有成本低、维护方便、分布性强、开发简单,可以不用安装任何专门的软件就能 实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。

3、HTTP协议

1. HTTP协议简介

在Web应用中,浏览器请求一个URL,服务器就把生成的HTML网页发送给浏览器,而浏览器和服务器之间的传输协议是HTTP,所以:

  • HTML是一种用来定义网页的文本,会HTML,就可以编写网页;

  • HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。

HTTP协议是一个基于TCP协议之上的请求-响应协议,它非常简单,我们先使用Chrome浏览器查看新浪首页,然后选择View - Developer - Inspect Elements就可以看到HTML:

切换到Network,重新加载页面,可以看到浏览器发出的每一个请求和响应: 

使用Chrome浏览器可以方便地调试Web应用程序。

对于Browser来说,请求页面的流程如下:

  1. 与服务器建立TCP连接;
  2. 发送HTTP请求;
  3. 收取HTTP响应,然后把网页在浏览器中显示出来。

浏览器发送的HTTP请求如下:

GET / HTTP/1.1
Host: www.sina.com.cn
User-Agent: Mozilla/5.0 xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8

其中,第一行表示使用GET请求获取路径为/的资源,并使用HTTP/1.1协议,从第二行开始,每行都是以Header: Value形式表示的HTTP头,比较常用的HTTP Header包括:

  • Host: 表示请求的主机名,因为一个服务器上可能运行着多个网站,因此,Host表示浏览器正在请求的域名;
  • User-Agent: 标识客户端本身,例如Chrome浏览器的标识类似Mozilla/5.0 ... Chrome/79,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...) like Gecko
  • Accept:表示浏览器能接收的资源类型,如text/*image/*或者*/*表示所有;
  • Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
  • Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate, br

服务器的响应如下:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 21932
Content-Encoding: gzip
Cache-Control: max-age=300

<html>...网页数据...

服务器响应的第一行总是版本号+空格+数字+空格+文本,数字表示响应代码,其中2xx表示成功,3xx表示重定向,4xx表示客户端引发的错误,5xx表示服务器端引发的错误。数字是给程序识别,文本则是给开发者调试使用的。

常见的响应代码有:

  • 200 OK:表示成功;
  • 301 Moved Permanently:表示该URL已经永久重定向;
  • 302 Found:表示该URL需要临时重定向;
  • 304 Not Modified:表示该资源没有修改,客户端可以使用本地缓存的版本;
  • 400 Bad Request:表示客户端发送了一个错误的请求,例如参数无效;
  • 401 Unauthorized:表示客户端因为身份未验证而不允许访问该URL;
  • 403 Forbidden:表示服务器因为权限问题拒绝了客户端的请求;
  • 404 Not Found:表示客户端请求了一个不存在的资源;
  • 500 Internal Server Error:表示服务器处理时内部出错,例如因为无法连接数据库;
  • 503 Service Unavailable:表示服务器此刻暂时无法处理请求。

从第二行开始,服务器每一行均返回一个HTTP头。服务器经常返回的HTTP Header包括:

  • Content-Type:表示该响应内容的类型,例如text/htmlimage/jpeg
  • Content-Length:表示该响应内容的长度(字节数);
  • Content-Encoding:表示该响应压缩算法,例如gzip
  • Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒。

HTTP请求和响应都由HTTP Header和HTTP Body构成,其中HTTP Header每行都以\r\n结束。如果遇到两个连续的\r\n,那么后面就是HTTP Body。浏览器读取HTTP Body,并根据Header信息中指示的Content-Type、Content-Encoding等解压后显示网页、图像或其他内容。

通常浏览器获取的第一个资源是HTML网页,在网页中,如果嵌入了JavaScript、CSS、图片、视频等其他资源,浏览器会根据资源的URL再次向服务器请求对应的资源。

关于HTTP协议的详细内容,请参考HTTP权威指南一书,或者Mozilla开发者网站

这里我们以服务器的身份响应客户端请求,编写服务器程序来处理客户端请求通常就称之为Web开发。

4、编写HTTP Server

我们来看一下如何编写HTTP Server。一个HTTP Server本质上是一个TCP服务器,我们先用TCP编程的多线程实现的服务器端框架:

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8080); // 监听指定端口
        System.out.println("server is running...");
        for (;;) {
            Socket sock = ss.accept();
            System.out.println("connected from " + sock.getRemoteSocketAddress());
            Thread t = new Handler(sock);
            t.start();
        }
    }
}

class Handler extends Thread {
    Socket sock;

    public Handler(Socket sock) {
        this.sock = sock;
    }

    public void run() {
        try (InputStream input = this.sock.getInputStream()) {
            try (OutputStream output = this.sock.getOutputStream()) {
                handle(input, output);
            }
        } catch (Exception e) {
            try {
                this.sock.close();
            } catch (IOException ioe) {
            }
            System.out.println("client disconnected.");
        }
    }

    private void handle(InputStream input, OutputStream output) throws IOException {
        var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        // TODO: 处理HTTP请求
    }
}

只需要在handle()方法中,用Reader读取HTTP请求,用Writer发送HTTP响应,即可实现一个最简单的HTTP服务器。编写代码如下:

private void handle(InputStream input, OutputStream output) throws IOException {
    System.out.println("Process new http request...");
    var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
    var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
    // 读取HTTP请求:
    boolean requestOk = false;
    String first = reader.readLine();
    if (first.startsWith("GET / HTTP/1.")) {
        requestOk = true;
    }
    for (;;) {
        String header = reader.readLine();
        if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
            break;
        }
        System.out.println(header);
    }
    System.out.println(requestOk ? "Response OK" : "Response Error");
    if (!requestOk) {
        // 发送错误响应:
        writer.write("HTTP/1.0 404 Not Found\r\n");
        writer.write("Content-Length: 0\r\n");
        writer.write("\r\n");
        writer.flush();
    } else {
        // 发送成功响应:
        String data = "<html><body><h1>Hello, world!</h1></body></html>";
        int length = data.getBytes(StandardCharsets.UTF_8).length;
        writer.write("HTTP/1.0 200 OK\r\n");
        writer.write("Connection: close\r\n");
        writer.write("Content-Type: text/html\r\n");
        writer.write("Content-Length: " + length + "\r\n");
        writer.write("\r\n"); // 空行标识Header和Body的分隔
        writer.write(data);
        writer.flush();
    }
}

这里的核心代码是,先读取HTTP请求,这里我们只处理GET /的请求。当读取到空行时,表示已读到连续两个\r\n,说明请求结束,可以发送响应。发送响应的时候,首先发送响应代码HTTP/1.0 200 OK表示一个成功的200响应,使用HTTP/1.0协议,然后,依次发送Header,发送完Header后,再发送一个空行标识Header结束,紧接着发送HTTP Body,在浏览器输入http://localhost.com:8080/就可以看到响应页面:

HTTP目前有多个版本,1.0是早期版本,浏览器每次建立TCP连接后,只发送一个HTTP请求并接收一个HTTP响应,然后就关闭TCP连接。由于创建TCP连接本身就需要消耗一定的时间,因此,HTTP 1.1允许浏览器和服务器在同一个TCP连接上反复发送、接收多个HTTP请求和响应,这样就大大提高了传输效率。

我们注意到HTTP协议是一个请求-响应协议,它总是发送一个请求,然后接收一个响应。能不能一次性发送多个请求,然后再接收多个响应呢?HTTP 2.0可以支持浏览器同时发出多个请求,但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来。可见,HTTP 2.0进一步提高了传输效率,因为浏览器发出一个请求后,不必等待响应,就可以继续发下一个请求。

HTTP 3.0为了进一步提高速度,将抛弃TCP协议,改为使用无需创建连接的UDP协议,目前HTTP 3.0仍然处于实验阶段。

总结:

使用B/S架构时,总是通过HTTP协议实现通信,Web开发通常是指开发服务器端的Web应用程序。

二、JavaEE和Tomcat

1、JavaEE 规范

JavaEE规范是J2EE规范的新名称,早期被称为 J2EE 规范,其全称是 Java 2 Platform Enterprise Edition,是由 SUN 公司领导、各厂家共同制定并得到广泛认可的工业标准(JCP 组织成员)。

其中,JCP 组织(官网)的全称是 Java Community Process,是一个开放的国际组织,主要由 Java 开发者以及被授权者组成,职能是发展和更新,成立于 1998 年。

JavaEE 规范是众多 Java 开发技术的总称。这些技术规范都是沿用自 J2EE 的,一共包括了 13 个技术规范,如jsp/servlet、jndi、jaxp、jdbc、jni、jaxb、jmf、jta、jpa、EJB 等。

JavaEE 的版本是延续了 J2EE 的版本,但是没有继续采用其命名规则。J2EE 的版本从 1.0 开始到 1.4 结束,而 JavaEE 版本是从 JavaEE 5 版本开始的,详情请参考:JavaEE8 规范概览

2、Tomcat

1. Tomcat 简介

服务器的概念非常的广泛,它可以指代一台特殊的计算机(相比普通计算机运行更快、负载更高、价格更贵),也可以指代用于部署网站的应用。

以下说的服务器,其实是 Web 服务器,或者应用服务器,它本质就是一个软件,一个应用。作用就是发布我们的应用(工程),让用户可以通过浏览器访问我们的应用。

常见的应用服务器:

服务器名称说明
weblogic实现了 javaEE 规范,重量级服务器,又称为 javaEE 容器
websphereAS实现了 javaEE 规范,重量级服务器
JBOSSAS实现了 JavaEE 规范,重量级服务器,免费
Tomcat实现了 jsp/servlet 规范,是一个轻量级服务器,开源免费

2. Tomcat下载与安装

Tomcat 官网下载地址

Tomcat 各版本所需支持:

Tomcat 目录结构:

3. Tomcat 基础使用

Tomcat实际上也是一个Java程序,我们看看Tomcat的启动流程:

  1. 启动JVM并执行Tomcat的main()方法;
  2. 加载war并初始化Servlet;
  3. 正常服务。

启动Tomcat无非就是设置好classpath并执行Tomcat某个jar包的main()方法,我们完全可以把Tomcat的jar包全部引入进来,然后自己编写一个main()方法,先启动Tomcat,然后让它加载我们的webapp就行。

我们新建一个web-servlet-embedded工程,编写pom.xml如下:

<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>com.itranswarp.learnjava</groupId>
    <artifactId>web-servlet-embedded</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
        <tomcat.version>10.1.1</tomcat.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

其中,<packaging>类型仍然为war,引入依赖tomcat-embed-core和tomcat-embed-jasper,引入的Tomcat版本<tomcat.version>为10.1.1。

不必引入Servlet API,因为引入Tomcat依赖后自动引入了Servlet API。因此,我们可以正常编写Servlet如下:

@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        String name = req.getParameter("name");
        if (name == null) {
            name = "world";
        }
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, " + name + "!</h1>");
        pw.flush();
    }
}

然后,我们编写一个main()方法,启动Tomcat服务器:

public class Main {
    public static void main(String[] args) throws Exception {
        // 启动Tomcat:
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(Integer.getInteger("port", 8080));
        tomcat.getConnector();
        // 创建webapp:
        Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        resources.addPreResources(
                new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
        ctx.setResources(resources);
        tomcat.start();
        tomcat.getServer().await();
    }
}

这样,我们直接运行main()方法,即可启动嵌入式Tomcat服务器,然后,通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp,可直接在浏览器访问http://localhost:8080/:

通过main()方法启动Tomcat服务器并加载我们自己的webapp有如下好处:

  1. 启动简单,无需下载Tomcat或安装任何IDE插件;
  2. 调试方便,可在IDE中使用断点调试;
  3. 使用Maven创建war包后,也可以正常部署到独立的Tomcat服务器中。

对SpringBoot有所了解的童鞋可能知道,SpringBoot也支持在main()方法中一行代码直接启动Tomcat,并且还能方便地更换成Jetty等其他服务器。

推荐使用main()方法启动嵌入式Tomcat服务器并加载当前工程的webapp,便于开发调试,且不影响打包部署,能极大地提升开发效率。

Tomcat启动报错解决:

问题 1:启动一闪而过。

  • 原因:没有配置环境变量。

  • 解决办法:配置上 JAVA_HOME 环境变量。

问题 2:报错信息 Address already in use : JVM_Bind。

  • 原因:端口被占用。

  • 解决办法:找到占用该端口的应用。

    • 已占的进程不重要:使用 cmd 命令:netstat -a -o 查看 pid,在任务管理器中结束占用端口的进程。
    • 已占的进程很重要:修改 Tomcat 自己的端口号。在 Tomcat 目录下\conf\server.xml中修改配置:

问题 3:启动时很多异常,但能正常启动。

  • 原因:Tomcat 中部署着很多项目,每次启动这些项目都会启动。而这些项目中有启动报异常的。

  • 解决办法:

    • 能找到报异常的项目,就把它从发布目录中移除。
    • 不能确定报异常的项目,就重新部署一个新的 Tomcat。

其它问题:

  • 例如启动产生异常,但是不能正常启动。此时就需要部署一个新的 Tomcat 启动,来确定是系统问题,还是 Tomcat 的问题。

  • 此时就需要具体问题,具体分析,然后再对症解决。

4. IDEA 集成 Tomcat

5. Tomcat 配置虚拟目录

虚拟目录的配置,支持两种方式:第一种是通过在主配置文件中添加标签实现;第二种是通过写一个独立配置文件实现。

方式一:在server.xml的<Host>元素中加一个<Context path="" docBase=""/>元素。

  • path:访问资源 URI。URI 名称可以随便起,但是必须在前面加上一个“/”。
  • docBase:资源所在的磁盘物理地址。

方式二:是写一个独立的xml文件,该文件名可以随便起,但在文件内写一个<Context/>元素。

  • 该文件要放在 Tomcat 目录中的conf\Catalina\localhost\目录下。
  • 需要注意的是,在使用了独立的配置文件之后,访问资源 URI 就变成了/+文件的名称,而Contextpath属性就失效了。

6. Tomcat 配置虚拟主机(域名)

在<Engine>元素中添加一个<Host name="" appBase="" unparkWARs="" autoDeploy="" />,其中:

  • name:指定主机的名称。
  • appBase:当前主机的应用发布目录。
  • unparkWARs:启动时是否自动解压 war 包。
  • autoDeploy:是否自动发布。

配置示例如下:

<Host name="www.itcast.cn" appBase="D:\itcastapps" unpackWARs="true" autoDeploy="true"/>

<Host name="www.itheima.com" appBase="D:\itheimaapps" unpackWARs="true" autoDeploy="true"/>

7. Tomcat 默认项配置

1)配置默认端口

Tomcat 服务器的主配置文件中配置着访问端口,它在配置文件中写的值是:8080。配置方式如下:

<Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

2)配置默认应用

有两种方式可以配置默认应用。

方式一:把要作为默认应用的应用,名称改为ROOT,放到webapps目录中。

方式二:写一个独立的配置文件,文件名称为ROOT.xml

  • 注意:ROOT必须大写。当使用了独立的ROOT.xml文件时,webappsROOT应用就不再是默认应用。

3)配置默认主页

首先要明确的是,配置默认主页是针对应用所说的,是应用的默认主页。

在应用的 web.xml 中配置:

<welcome-file-list>
    <welcome-file>默认主页</welcome-file>
</welcome-file-list>

例如:

<welcome-file-list>
    <!-- 有多个默认页时,先找到的就显示,不再往下找 -->
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

3、Java Web 应用

1. Java Web 工程概述

JavaWeb应用是一个全新的应用种类,这类应用程序指供浏览器访问的程序,通常也简称为 Web 应用。

一个 Web 应用由多个静态 Web 资源和动态 Web 资源组成,例如有 html、css、js 文件,jsp 文件、java 程序、支持的 jar 包、工程配置文件、图片、音视频等等。

Web 应用开发好后,若想供外界访问,需要把 Web 应用的所在目录,交给 Web 服务器管理(Tomcat 就是 Web 服务器之一),这个过程称之为虚似目录的映射。

2. Java Web 应用目录结构

myapp -------- 应用名称
    demo.html
    css/demo.css
    js/demo.js
	WEB-INF -------- 如果有 web.xml 或者 .class 文件时,该目录必须存在,且严格区分大小写。
	        -------- 该目录下的资源,客户端是无法直接访问的。
                -------- 目录中内容如下:
        classes 目录 -------- web 应用的 class 文件(加载顺序:我们的 class,lib 目录中的 jar 包,tomcat 的 lib 目录中的 jar 包。其优先级依次降低)
        lib 目录 -------- web 应用所需的 jar 包(tomcat 的 lib 目录下 jar 为所有应用共享)
        web.xml -------- web 应用的主配置文件

3. IDEA 创建 Java Web 应用工程

4. Java Web 应用部署

1)IDEA 部署

2)war 包发布

步骤一:使用命令jar -cvf war [需要打包的目录路径]打包 war 包。

步骤二:把打好的 war 包拷贝到 tomcat 的 webapps 目录中。 

步骤三:Tomcat 在启动时,会自动解压 war 包。 

三、Servlet 控制器

1、Java Web 设计模式

上面的例子,我们看到,编写HTTP服务器其实是非常简单的,只需要先编写基于多线程的TCP服务,然后在一个TCP连接中读取HTTP请求,发送HTTP响应即可。 

但是,要编写一个完善的HTTP服务器,以HTTP/1.1为例,需要考虑的包括:

  • 识别正确和错误的HTTP请求;
  • 识别正确和错误的HTTP头;
  • 复用TCP连接;
  • 复用线程;
  • IO异常处理;
  • ...

这些基础工作需要耗费大量的时间,并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的HTML页面,就不得不编写上千行底层代码,那就根本无法做到高效而可靠地开发。

因此,在JavaEE平台上,处理TCP连接,解析HTTP协议这些底层工作统统扔给现成的Web服务器去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet API,我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层功能:

一个完整的Web应用程序的开发流程如下:

  1. 编写Servlet;
  2. 打包为war文件;
  3. 复制到Tomcat的webapps目录下;
  4. 启动Tomcat。

我们来实现一个最简单的Servlet:

// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 设置响应类型:
        resp.setContentType("text/html");
        // 获取输出流:
        PrintWriter pw = resp.getWriter();
        // 写入响应:
        pw.write("<h1>Hello, world!</h1>");
        // 最后不要忘记flush强制输出:
        pw.flush();
    }
}

一个Servlet总是继承自HttpServlet,然后覆写doGet()或doPost()方法。注意到doGet()方法传入了HttpServletRequest和HttpServletResponse两个对象,分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter,写入响应即可。

现在问题来了:Servlet API是谁提供?

Servlet API是一个jar包,我们需要通过Maven来引入它,才能正常编译。编写pom.xml文件如下:

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>web-servlet-hello</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>hello</finalName>
    </build>
</project>

注意到这个pom.xml与前面我们讲到的普通Java程序有个区别,打包类型不是jar,而是war,表示Java Web Application Archive:

<packaging>war</packaging>

引入的Servlet API如下:

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
</dependency>

注意到<scope>指定为provided,表示编译时使用,但不会打包到.war文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。

整个工程结构如下:

目录webapp目前为空,如果我们需要存放一些资源文件,则需要放入该目录。有的同学可能会问,webapp目录下是否需要一个/WEB-INF/web.xml配置文件?这个配置文件是低版本Servlet必须的,但是高版本Servlet已不再需要,所以无需该配置文件。

运行Maven命令mvn clean package,在target目录下得到一个hello.war文件,这个文件就是我们编译打包后的Web应用程序。

如果执行package命令遇到Execution default-war of goal org.apache.maven.plugins:maven-war-plugin:2.2:war failed错误时,可手动指定maven-war-plugin最新版本3.3.2,参考练习工程的pom.xml。

现在问题又来了:我们应该如何运行这个war文件?

普通的Java程序是通过启动JVM,然后执行main()方法开始运行。但是Web应用程序有所不同,我们无法直接运行war文件,必须先启动Web服务器,再由Web服务器加载我们编写的HelloServlet,这样就可以让HelloServlet处理浏览器发送的请求。

因此,我们首先要找一个支持Servlet API的Web服务器。常用的服务器有:

  • Tomcat:由Apache开发的开源免费服务器;
  • Jetty:由Eclipse开发的开源免费服务器;
  • GlassFish:一个开源的全功能JavaEE服务器。

还有一些收费的商用服务器,如Oracle的WebLogic,IBM的WebSphere

无论使用哪个服务器,只要它支持Servlet API 5.0(因为我们引入的Servlet版本是5.0),我们的war包都可以在上面运行。这里我们选择使用最广泛的开源免费的Tomcat服务器。

要运行我们的hello.war,首先要下载Tomcat服务器,解压后,把hello.war复制到Tomcat的webapps目录下,然后切换到bin目录,执行startup.sh或startup.bat启动Tomcat服务器:

$ ./startup.sh 
Using CATALINA_BASE:   .../apache-tomcat-10.1.x
Using CATALINA_HOME:   .../apache-tomcat-10.1.x
Using CATALINA_TMPDIR: .../apache-tomcat-10.1.x/temp
Using JRE_HOME:        .../jdk-17.jdk/Contents/Home
Using CLASSPATH:       .../apache-tomcat-10.1.x/bin/bootstrap.jar:...
Tomcat started.

在浏览器输入http://localhost:8080/hello/即可看到HelloServlet的输出:

细心的童鞋可能会问,为啥路径是/hello/而不是/?因为一个Web服务器允许同时运行多个Web App,而我们的Web App叫hello,因此,第一级目录/hello表示Web App的名字,后面的/才是我们在HelloServlet中映射的路径。

那能不能直接使用/而不是/hello/?毕竟/比较简洁。

答案是肯定的。先关闭Tomcat(执行shutdown.sh或shutdown.bat),然后删除Tomcat的webapps目录下的所有文件夹和文件,最后把我们的hello.war复制过来,改名为ROOT.war,文件名为ROOT的应用程序将作为默认应用,启动后直接访问http://localhost:8080/即可。

实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()方法,然后由Tomcat负责加载我们的.war文件,并创建一个HelloServlet实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/(假定部署文件为ROOT.war),就转发到HelloServlet并传入HttpServletRequest和HttpServletResponse两个对象。

因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。

在Servlet容器中运行的Servlet具有如下特点:

  • 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
  • Servlet容器只会给每个Servlet类创建唯一实例;
  • Servlet容器会使用多线程执行doGet()或doPost()方法;
  • 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
  • HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
  • 在doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用;

总结:

  • 编写Web应用程序就是编写Servlet处理HTTP请求;
  • Servlet API提供了HttpServletRequest和HttpServletResponse两个高级接口来封装HTTP请求和响应;
  • Web应用程序必须按固定结构组织并打包为.war文件;
  • 需要启动Web服务器来加载我们的war包来运行Servlet。

2、Servlet 简介

Servlet 是 SUN 公司提供的一套规范,名称就叫 Servlet 规范,它也是 JavaEE 规范之一。我们可以通过访问官方 API 学习和查阅里面的内容。

打开官方 API 网址,在左上部分找到 javax.servlet 包,在左下部分找到 Servlet,如下图显示:

通过阅读 API,我们可以得到如下信息:

  1. Servlet 是一个运行在 Web 服务端的 Java 小程序。
  2. 它可以用于接收和响应客户端的请求,主要功能在于交互式地浏览和修改数据,生成动态的 Web 内容。
  3. 要想实现 Servlet 功能,可以实现 Servlet 接口、继承 GenericServlet 或者 HttpServlet。
  4. 每次请求都会执行 service 方法。
  5. Servlet 还支持配置。

Servlet 主要执行以下工作:

使用 Servlet,可以收集来自网页表单的用户输入,可以呈现来自数据库或者其他来源的记录,还可以动态地创建网页(JSP)。

  1. 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 Applet 或自定义的 HTTP 客户端程序的表单。
  2. 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 Cookies、媒体类型和浏览器能理解的压缩格式等。
  3. 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,认用 Web 服务,或者直接计算得出对应的响应。
  4. 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样自包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
  5. 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返国的文档类型(例如 HTML)、设置 Cookie 和缓存参数,以及其他类似的任务。

Java Servlet 通常情况下与使用 CGI(common gateway interface,公共网关接口)实现的程序可以达到异曲同工的效果。

但是相比于 CGI,Servlet 有以下几点优势:

  1. 性能明显更好。
  2. 在 Web 服务器的地址空间内执行。这样它就没有必要再创建一个单独的进程来处理每个客户端请求。
  3. Servlet 是独立于平台的,因为它们是用 Java 编写的。
  4. 服务器上的 Java 安全管理器具有一定的限制,以保护服务器计算机上的资源。因此 Servlet 是可信的。
  5. Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 Socket 和 RMI 机制与 Applet、数据库或其他软件进行交互。

3、Servlet 基础使用

1. Servlet 编写步骤

1)编码

  1. 前期准备-创建 Java Web 工程;

  2. 编写一个普通类继承 GenericServlet 并重写 service 方法;

  3. 在 web.xml 配置 Servlet;

2)测试

  1. 在 Tomcat 中部署项目;

  2. 在浏览器访问 Servlet。

2. Servlet 执行过程 

  1. 浏览器使用 Socket(IP+端口)与服务器建立连接。
  2. 浏览器将请求数据按照 HTTP 打成一个数据包(请求数据包)发送给服务器。
  3. 服务器解析请求数据包并创建请求对象(request)和响应对象(response)。
    • 请求对象是 HttpServletRquest 接口的一个实现。
    • 响应对象是 HttpServletResponse 接口的一个实现,响应对象用于存放 Servlet 处理自的结果。
  4. 服务器将解析之后的数据存放到请求对象(request)里面。
  5. 服务器依据请求资源路径找到相应的 Servlet 配置,通过反射创建 Servlet 实例。
  6. 服务器调用其 service() 方法,在在调用 serviceO方法时,会将事先创建好的请求对象(request)和响应对象(response)作为参数进行传递。
  7. 在 Servlet 内部,可以通过 reques st 获得请求数据,或者通过 response 设置响应数据
  8. 服务器从 response 中获取数据 按照 HTTP 打成一个数据包(响应数据包),发送给浏览器。
  9. 浏览器解析响应数据包,取出相应的数据,生成相应的界面。

3. Servlet 类视图

  • 在 Servlet 的 API 介绍中,除了继承 GenericServlet 外还可以继承 HttpServlet。
  • 通过查阅 servlet 的类视图,我们看到 GenericServlet 还有一个子类 HttpServlet。
  • 同时,在 service 方法中还有参数 ServletRequest 和 ServletResponse。

它们的关系如下图所示:

4. Servlet 编写方式

我们在实现 Servlet 功能时,可以选择以下三种方式:

第一种:实现 Servlet 接口,接口中的方法必须全部实现。

  • 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。

第二种:继承 GenericServlet,service 方法必须重写,其他方可根据需求,选择性重写。

  • 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发 Servlet 变得简单。但是,此种方式是和 HTTP 协议无关的。

第三种:继承 HttpServlet。

  • 它是 javax.servlet.http 包下的一个抽象类,是 GenericServlet 的子类。
  • 如果我们选择继承 HttpServlet 时,只需要重写 doGet 和 doPost 方法,不需要覆盖 service 方法。
  • 使用此种方式,表示我们的请求和响应需要和 HTTP 协议相关。也就是说,我们是通过 HTTP 协议来访问的。那么每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式(HTTP 协议支持 7 种请求方式:GET、POST、PUT、DELETE、TRACE、OPTIONS、HEAD)。
  • 为了实现代码的可重用性,通常我们只需要在 doGet 或者 doPost 方法任意一个中提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。

5. Servlet 生命周期

对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向于开发的官方说法,就是对象从被创建到销毁的过程。

Servlet 的生命周期主要有初始化阶段、处理客户端请求阶段和终止阶段:

  1. 初始化阶段

    • Servlet 容器加载 Servlet,加载完成后,Servlet 容器会创建一个 Servlet 实例并调用 init() 方法,init() 方法只会调用一次。
    • Servlet 容器会在以下几种情况加载 Servlet:
      1. Servlet 容器启动时自动加载某些 Servlet,这样需要在 web.xml 文件中添加。
      2. 在 Servlet 容器启动后,客户首次向 Servlet 发送请求。
      3. Servlet 类文件被更新后,重新加载。
  2. 处理客户端请求阶段

    • 每收到一个客户端请求,服务器就会产生一个新的线程去处理。对于用户的 Servlet 请求,Servlet 容器会创建一个特定于请求的 ServletRequest 和 ServletResponse。
    • 对于 Tomcat 来说,它会将传递来的参数放入一个哈希表中,这是一个 String->String[]的键值映射。
  3. 终止阶段

    • 当 Web 应用被终止,或者 Servlet 容器终止运行,又或者 Servlet 重新加载 Servlet 新实例时,Servlet 容器会调用 Servlet 的 destroy() 方法。

通过分析 Servlet 的生命周期可以发现,它的实例化和初始化只会在请求第一次到达 Servlet 时执行,而销毁只会在 Tomcat 服务器停止时执行。

由此我们得出一个结论,Servlet 对象只会创建一次,销毁一次。所以,每一个 Servlet 只有一个实例对象。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式。

如下是一个典型的 Servlet 生命周期方案: 

  1. 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
  2. Servlet 容器在调用 service() 方法之前加载 Servlet。
  3. Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。

6. Servlet 执行时一般要实现的方法

Servlet 类要继承的 GenericServlet 与 HttpServlet 类说明:

  1. GenericServlet 类是一个实现了 Servlet 的基本特征和功能的基类,其完整名称为 javax.Servlet.GenericServlet,它实现了 Servlet 和 ServletConfig 接口。
  2. HtpServlet 类是 GenericServlet 的子类,其完整名称为javax.Servlet.HttpServlet,它提供了处理 HTTP 的基本构架。如果一个 Servlet 类要充分使用 HTTP 的功能,就应该继 HttpServlet。在 HttpServlet 类及其子类中,除可以调用 HttpServlet 类内部新定义的方法外,可以调用包括 Servlet、ServletConfig 接口和 GenericServlet 类中的一些方法。

Servlet 若继承上述类,执行时一般要实现的方法:

publle void init(servletconfig config)
public void service(servletRequest request, servletResponse response) public void destroy()
public Servletconfig getservletConfig() 
publle string getservletInfo()
  1. init() 方法在 Servlet 的生命周期中仅执行一次,在 Servlet 引擎创建 Servlet 对象后执行。 Servlet 在调用 init() 方法时,会传递一个包含 Servlet 的配置和运行环境信息的 ServletConfig 对象。如果初始化代码中要使用到 ServletConfig 对象,则初始化代码就只能在 Servlet 的 init() 方法中编写,而不能在构造方法中编写。默认的 init(方法通常是符合要求的,不过也可以根据需要进行覆盖,比如管理服务器端资源、初始化数据库连接等,默认的 inti()方法设置了 Servlet的初始化参数,并用它的 ServeltConfig 对象参数来启动配置,所以覆盖 init() 方法时,应调用 super.init() 以确保仍然执行这些任务。

  2. service() 方法是 Servlet 的核心,用于响应对 Servlet 的访问请求。对于 HttpServlet,每当客户请求一个 HttpServlet 对象时,该对象的 serviceO方法就要被调用,HttpServlet 默认的 serviceo方法的服务功能就是调用与 HTTP 请求的方法相应的 do 功能:doPostO和 doGet0,所以对于 HttpServlet,一般都是重写 doPostO和 doGet()方法。

  3. destroy() 方法在 Servlet 的生命周期中也仅执行一次,即在服务器停止卸载 Servlet 之前被调用,把 Servlet 作为服务器进程的一部分关闭。默认的 destroy() 方法通常是符合要求的,但也可以覆盖,来完成与 init() 方法相反的功能。比如在卸载 Servlet 时将统计数字保存在文件中,或是关闭数据库连接或 I/O 流。

  4. getServletConfig() 方法返回一个 ServletConfig 对象,该对象用来返回初始化参数和 ServletContext。ServletContext 接口提供有关 Servlet 的环境信息。

  5. getServletInfo() 方法提供有关 Servlet 的描述信息,如作者、版本、版权。可以对它进行覆盖。

  6. doXxx() 方法客户端可以用 HTTP 中规定的各种请求方式来访问 Servlet,Servlet 采取不同的访问方式进行处理。不管用哪种请求方式访问 Servlet,Servlet 引擎都会调用 Servlet 的 service() 方法,service() 方法是所有请求方式的入口。

    • doGet() 用于处理 GET 请求;
    • doPost() 用于处理 POST 请求;
    • doHead() 用于处理 HEAD 请求;
    • doPut() 用于处理 PUT 请求;
    • doDelete() 用于处理 DELETE 请求;
    • doTrace() 用于处理 TRACE 请求;
    • doOptions() 用于处理 OPTIONS 请求。

7. Servlet 线程安全

由于 Servlet 运用了单例模式,即在整个应用中,每一个 Servlet 类只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。

接下来,我们来看下面的的示例:

public class ServletDemo extends HttpServlet {
    //1.定义用户名成员变量
    //private String username = null;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = null;
        //synchronized (this) {
            //2.获取用户名
            username = req.getParameter("username");

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //3.获取输出流对象
            PrintWriter pw = resp.getWriter();

            //4.响应给客户端浏览器
            pw.print("welcome:" + username);

            //5.关流
            pw.close();
        //}
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题:

通过上面的测试我们发现,在 Servlet 中定义了类成员后,多个浏览器都会共享类成员的数据。每一个浏览器端就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享 Servlet 类成员中的数据。那么,其中任何一个线程修改了数据,都会影响其他线程。因此,我们可以认为 Servlet 不是线程安全的。

分析产生这个问题的根本原因,其实就是因为 Servlet 是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都可能会改变,而不是重新初始化。

要解决这个线程安全问题,需要在 Servlet 中定义类成员时慎重。

  • 如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题的。
  • 但如果类成员并非共用,或者每次使用都有可能对其赋值(如上图示例),那么就要考虑线程安全问题了,解决方案是把它定义到 doGet 或者 doPost 方法中。

8. Servlet 映射配置

Servlet 支持三种映射方式,以达到灵活配置的目的。

首先编写一个Servlet,代码如下:

public class ServletDemo extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("ServletDemo5接收到了请求");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

方式一:精确映射。

此种方式,只有和映射配置一模一样时,Servlet 才会接收和响应来自客户端的请求。

方式二:/开头+通配符。

此种方式,只要符合目录结构即可,不用考虑结尾是什么。

例如:映射为:/servlet/*

方式三:通配符+固定格式结尾。

此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确)

例如:映射为:*.do

优先级:

通过测试我们发现,Servlet 支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的 Servlet 映射都符合请求 URL 时,由谁来响应呢?

注意:HTTP 协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,映射规则的优先级如下:

  1. 精确匹配
  2. /开头+通配符
  3. 通配符+固定格式结尾

9. 多路径映射 Servlet

这其实是给一个 Servlet 配置多个访问映射,从而可以根据不同请求 URL 实现不同的功能。

示例 Servlet:

public class ServletDemo extends HttpServlet {

    /**
     * 根据不同的请求URL,做不同的处理规则
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1. 获取当前请求的 URI
        String uri = req.getRequestURI();
        uri = uri.substring(uri.lastIndexOf("/"), uri.length());
        //2. 判断是1号请求还是2号请求
        if("/servletDemo7".equals(uri)){
            System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示");
        }else if("/demo7".equals(uri)){
            System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示");
        }else {
            System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

web.xml 配置 Servlet:

<servlet>
    <servlet-name>servletDemo7</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class>
</servlet>
<!--映射路径1-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/demo7</url-pattern>
</servlet-mapping>
<!--映射路径2-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/servletDemo7</url-pattern>
</servlet-mapping>
<!--映射路径3-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/servlet/*</url-pattern>
</servlet-mapping>

启动服务,测试运行结果:

10. 启动时即创建 Servlet

Servlet 的创建默认情况下是请求第一次到达 Servlet 时创建的。但是我们知道,Servlet 是单例的,也就是说在应用中只有唯一的一个实例,所以在 Tomcat 启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?

  • 第一种:应用加载时创建 Servlet。

    • 它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。
    • 它的弊端也同样明显,因为在应用加载时就创建了 Servlet 对象,因此,有可能导致内存中充斥着大量用不上的 Servlet 对象,造成了内存的浪费。
  • 第二种:请求第一次访问是创建 Servlet。

    • 它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的 Servlet 对象就不会被创建,同时也提高了服务器的启动时间。
    • 而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,那么它就没法完成,从而要考虑其他技术实现。

通过上面的分析可得出,当需要在应用加载就要完成一些工作时,就需要选择第一种方式;当有很多 Servlet 且其使用时机并不确定时,就选择第二种方式。

在 web.xml 中是支持对 Servlet 的创建时机进行配置的,配置的方式如下:

<servlet>
    <servlet-name>servletDemo3</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>
    <!-- 配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建
        配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高。
    -->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo3</servlet-name>
    <url-pattern>/servletDemo3</url-pattern>
</servlet-mapping>

11. 默认 Servlet

默认 Servlet 是由 Web 服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。

如下图所示:

它的映射路径是<url-pattern>/<url-pattern>。在我们发送请求时,首先会在我们应用中的 web.xml 中查找映射配置,找到就执行。当找不到对应的 Servlet 路径时,就会去找默认的 Servlet,由默认 Servlet 处理。所以,一切都是 Servlet。

12. Servlet 关系总图

4、ServletConfig

1. ServletConfig 简介

概念:

  • ServletConfig 是 Servlet 的配置参数对象。
  • 在 Servlet 规范中,允许为每个 Servlet 都提供一些初始化配置。所以,每个 Servlet 都一个自己的 ServletConfig。
  • 它的作用是在 Servlet 初始化期间,把一些配置信息传递给 Servlet。

生命周期:

  • 由于 ServletConfig 是在初始化阶段读取了 web.xml 中为 Servlet 准备的初始化配置,并把配置信息传递给 Servlet,所以生命周期与 Servlet 相同。
  • 这里需要注意的是,如果 Servlet 配置了<load-on-startup>1</load-on-startup>,那么 ServletConfig 也会在应用加载时创建。

2. ServletConfig 使用

获取:

ServletConfig 可以为每个 Servlet 都提供初始化参数,所以肯定可以在每个 Servlet 中都配置。

public class ServletDemo8 extends HttpServlet {

    // 定义 Servlet 配置对象 ServletConfig
    private ServletConfig servletConfig;

    /**
     * 在初始化时为 ServletConfig 赋值
     * @param config
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.servletConfig = config;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 输出ServletConfig
        System.out.println(servletConfig);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

web.xml:

<servlet>
    <servlet-name>servletDemo8</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo8</servlet-name>
    <url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>

上面我们已经准备好了 Servlet,同时也获取到了它的 ServletConfig 对象,而如何配置初始化参数,则需要使用<servlet>标签中的<init-param>标签来配置。

即 Servlet 的初始化参数都是配置在 Servlet 的声明部分的,并且每个 Servlet 都支持有多个初始化参数,并且初始化参数都是以键值对的形式存在的。

配置示例:

<servlet>
    <servlet-name>servletDemo8</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
    <!--配置初始化参数-->
    <init-param>
        <!--用于获取初始化参数的key-->
        <param-name>encoding</param-name>
        <!--初始化参数的值-->
        <param-value>UTF-8</param-value>
    </init-param>
    <!--每个初始化参数都需要用到init-param标签-->
    <init-param>
        <param-name>servletInfo</param-name>
        <param-value>This is Demo8</param-value>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>servletDemo8</servlet-name>
    <url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>

常用方法:

示例: 

/**
 * 演示Servlet的初始化参数对象
 * @author 黑马程序员
 * @Company http://www.itheima.com
 */
public class ServletDemo8 extends HttpServlet {

    // 定义 Servlet 配置对象 ServletConfig
    private ServletConfig servletConfig;

    /**
     * 在初始化时为 ServletConfig 赋值
     * @param config
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.servletConfig = config;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 输出ServletConfig
        System.out.println(servletConfig);
        // 2. 获取Servlet的名称
        String servletName= servletConfig.getServletName();
        System.out.println(servletName);
        // 3. 获取字符集编码
        String encoding = servletConfig.getInitParameter("encoding");
        System.out.println(encoding);
        // 4. 获取所有初始化参数名称的枚举
        Enumeration<String> names = servletConfig.getInitParameterNames();
        //遍历names
        while(names.hasMoreElements()){
            //取出每个name(key)
            String name = names.nextElement();
            //根据key获取value
            String value = servletConfig.getInitParameter(name);
            System.out.println("name:"+name+",value:"+value);
        }
        // 5. 获取ServletContext对象
        ServletContext servletContext = servletConfig.getServletContext();
        System.out.println(servletContext);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

5、ServletContext

1. ServletContext 简介

ServletContext 对象是应用上下文对象

每一个应用有且只有一个 ServletContext 对象,它可以实现让应用中所有 Servlet 间的数据共享。

生命周期:

  1. 出生: 应用一加载,该对象就被创建出来了。一个应用只有一个实例对象(Servlet 和 ServletContext 都是单例的)。

  2. 活着:只要应用一直提供服务,该对象就一直存在。

  3. 死亡:应用停止(或者服务器挂了),该对象消亡。

域对象概念:

  • 域对象指的是对象有作用域,即有作用范围

  • 域对象的作用,域对象可以实现数据共享。不同作用范围的域对象,共享数据的能力不一样。

  • 在 Servlet 规范中,一共有 4 个域对象,ServletContext 就是其中一个。

  • ServletContext 是 web 应用中最大的作用域,叫application 域。每个应用只有一个 application 域,它可以实现整个应用间的数据共享功能。

2. ServletContext 使用

获取:

只需要调用 ServletConfig 对象的getServletContext()方法就可以了。

public class ServletDemo9 extends HttpServlet {

    // 定义 Servlet 配置对象 ServletConfig
    private ServletConfig servletConfig;

    /**
     * 在初始化时为 ServletConfig 赋值
     * @param config
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.servletConfig = config;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取 ServletContext 对象
        ServletContext servletContext = servletConfig.getServletContext();
        System.out.println(servletContext);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

web.xml:

<servlet>
	<servlet-name>servletDemo9</servlet-name>
	<servlet-class>com.itheima.web.servlet.ServletDemo9</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>servletDemo9</servlet-name>
	<url-pattern>/servletDemo9</url-pattern>
</servlet-mapping>

更简洁的获取方法:

在实际开发中,如果每个 Servlet 对 ServletContext 都使用频繁的话,那么每个 Servlet 里定义 ServletConfig,再获取 ServletContext 的代码将非常多,造成大量的重复代码。

而 Servlet 规范的定义中也为我们想到了这一点,所以它在 GenericServlet 中,已经为我们声明好了 ServletContext 获取的方法。

示例 Servlet 都是继承自 HttpServlet,而 HttpServlet 又是 GenericServlet 的子类,所以我们在获取 ServletContext 时,如果当前 Servlet 没有用到它自己的初始化参数时,就可以不用再定义初始化参数了,而是直接改成下图所示的代码即可: 

ServletContext 既然被称之为应用上下文对象,那么它的配置就是针对整个应用的配置,而非某个特定 Servlet 的配置。它的配置被称为应用的初始化参数配置。

配置的方式,需要在<web-app>标签中使用<context-param>来配置初始化参数。

具体代码如下:

<!--配置应用初始化参数-->
<context-param>
    <!--用于获取初始化参数的 key-->
    <param-name>servletContextInfo</param-name>
    <!--初始化参数的值-->
    <param-value>This is application scope</param-value>
</context-param>
<!--每个应用初始化参数都需要用到 context-param 标签-->
<context-param>
    <param-name>globalEncoding</param-name>
    <param-value>UTF-8</param-value>
</context-param>

6、Servlet 注解开发

1. Servlet 3.0 规范

在大概十多年前,那会还是 Servlet 2.5 的版本的天下,它最明显的特征就是 Servlet 的配置要求配在 web.xml 中。

从 2007 年开始到 2009 年底的这个时间段中,软件开发开始逐步的演变,基于注解的配置理念开始逐渐出现,大量注解配置思想开始用于各种框架的设计中,例如:Spring 3.0 版本的 Java Based Configuration、JPA 规范、Apache 旗下的 struts2 和 mybatis 的注解配置开发等等。

JavaEE6 规范也是在这个期间设计并推出的,与之对应就是它里面包含了新的 Servlet 规范:Servlet 3.0 版本。

2. 使用示例

配置步骤:

步骤一:创建 Java Web 工程,并移除 web.xml。

步骤二:编写 Servlet。

public class ServletDemo1 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("Servlet Demo1 Annotation");
    }
}

步骤三:使用注解配置 Servlet。

步骤四:测试。 

注解源码分析: 

/**
 * WebServlet注解
 * @since Servlet 3.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {

    /**
     * 指定Servlet的名称。
     * 相当于xml配置中<servlet>标签下的<servlet-name>
     */
    String name() default "";

    /**
     * 用于映射Servlet访问的url映射
     * 相当于xml配置时的<url-pattern>
     */
    String[] value() default {};

    /**
     * 相当于xml配置时的<url-pattern>
     */
    String[] urlPatterns() default {};

    /**
     * 用于配置Servlet的启动时机
     * 相当于xml配置的<load-on-startup>
     */
    int loadOnStartup() default -1;

    /**
     * 用于配置Servlet的初始化参数
     * 相当于xml配置的<init-param>
     */
    WebInitParam[] initParams() default {};

    /**
     * 用于配置Servlet是否支持异步
     * 相当于xml配置的<async-supported>
     */
    boolean asyncSupported() default false;

    /**
     * 用于指定Servlet的小图标
     */
    String smallIcon() default "";

    /**
     * 用于指定Servlet的大图标
     */
    String largeIcon() default "";

    /**
     * 用于指定Servlet的描述信息
     */
    String description() default "";

    /**
     * 用于指定Servlet的显示名称
     */
    String displayName() default "";
}

7、请求对象

1. 请求对象介绍

请求,顾名思义,就是客户端希望从服务器端索取一些资源,因此向服务器发出的询问。在 B/S 架构中,就是客户浏览器向服务器发出询问。在 JavaEE 工程中,客户浏览器发出询问,要遵循 HTTP 协议所规定的。

请求对象,就是在 JavaEE 工程中,用于发送请求的对象。

常用请求对象:

常用的请求对象是ServletRequest和HttpServletRequest,它们的区别就是是否和 HTTP 协议有关。

常用方法:

2. 获取各种路径

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class requestServlet {
    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //本机地址:服务器地址
        String localAddr = request.getLocalAddr();
        //本机名称:服务器名称
        String localName = request.getLocalName();
        //本机端口:服务器端口
        int localPort = request.getLocalPort();
        //来访者ip
        String remoteAddr = request.getRemoteAddr();
        //来访者主机
        String remoteHost = request.getRemoteHost();
        //来访者端口
        int remotePort = request.getRemotePort();
        //统一资源标识符
        String URI = request.getRequestURI();
        //统一资源定位符
        String URL = request.getRequestURL().toString();
        //获取查询字符串
        String queryString = request.getQueryString();
        //获取Servlet映射路径
        String servletPath = request.getServletPath();

        //输出内容
        System.out.println("getLocalAddr() is :"+localAddr);
        System.out.println("getLocalName() is :"+localName);
        System.out.println("getLocalPort() is :"+localPort);
        System.out.println("getRemoteAddr() is :"+remoteAddr);
        System.out.println("getRemoteHost() is :"+remoteHost);
        System.out.println("getRemotePort() is :"+remotePort);
        System.out.println("getRequestURI() is :"+URI);
        System.out.println("getRequestURL() is :"+URL);
        System.out.println("getQueryString() is :"+queryString);
        System.out.println("getServletPath() is :"+servletPath);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
    
}

3. 获取请求头信息

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

public class requestServlet {
    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //1.根据名称获取头的值	一个消息头一个值
        String value = request.getHeader("Accept-Encoding");
        System.out.println("getHeader():"+value);

        //2.根据名称获取头的值	一个头多个值
        Enumeration<String> values = request.getHeaders("Accept");
        while(values.hasMoreElements()){
            System.out.println("getHeaders():"+values.nextElement());
        }

        //3.获取请求消息头的名称的枚举
        Enumeration<String> names = request.getHeaderNames();
        while(names.hasMoreElements()){
            String name = names.nextElement();
            String value1 = request.getHeader(name);
            System.out.println(name+":"+value1);
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

4. 获取请求参数

1)获取请求参数

准备一个表单页面:

<form action="/requestServlet" method="post">
    用户名:<input type="text" name="username" /><br/>
    密码:<input type="password" name="password" /><br/>
    确认密码:<input type="password" name="password" /><br/>
    性别:<input type="radio" name="gender" value="1" checked>男
    <input type="radio" name="gender" value="0">女
    <br/>
    <input type="submit" value="注册" />
</form>

方法示例:

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;

@WebServlet("/requestServlet")
public class requestServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 方式一
        String username = request.getParameter("username");
        String[] password = request.getParameterValues("password");  // 当表单中有多个名称是一样时,得到是一个字符串数组
        String gender = request.getParameter("gender");
        System.out.println(username+","+ Arrays.toString(password)+","+gender);  // user,[123, 123],1

        // 方式二
        // 1.获取请求正文名称的枚举
        Enumeration<String> names = request.getParameterNames();
        // 2.遍历正文名称的枚举
        while(names.hasMoreElements()){
            String name = names.nextElement();
            String value = request.getParameter(name);
            System.out.println(name+":"+value);
            /*
            username:user
            password:123
            gender:1
             */
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

2)封装请求参数到实体类中

通过上面的示例方法可以获取到请求参数,但是如果参数过多,在进行传递时,方法的形参定义将会变得非常难看。此时我们应该用一个对象来描述这些参数,它就是实体类。

实体类示例:

import java.util.Arrays;

public class Student {

    // 成员变量名要与表单name值一致
    private String username;
    private String password;
    private String[] hobby;

    public Student() {
    }

    public Student(String username, String password, String[] hobby) {
        this.username = username;
        this.password = password;
        this.hobby = hobby;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String[] getHobby() {
        return hobby;
    }

    public void setHobby(String[] hobby) {
        this.hobby = hobby;
    }

    @Override
    public String toString() {
        return "Student{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", hobby=" + Arrays.toString(hobby) +
                '}';
    }
}

我们现在要做的就是把表单中提交过来的数据填充到实体类中。

使用 apache 的 commons-beanutils 实现封装:

private void test(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    Users user = new Users();
    System.out.println("封装前:"+user.toString());
    try{
        BeanUtils.populate(user, request.getParameterMap());  // 就一句代码
    }catch(Exception e){
        e.printStackTrace();
    }
    System.out.println("封装后:"+user.toString());
}

5. 以流的方式读取请求信息

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
    流对象获取数据
 */
@WebServlet("/servletDemo")
public class ServletDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 字符流(必须是post方式)
        /*BufferedReader br = req.getReader();
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }*/
        // br.close();  // 由request获取的流对象无需手动关闭,由服务器自动关闭即可

        // 字节流
        ServletInputStream is = req.getInputStream();
        byte[] arr = new byte[1024];
        int len;
        while((len = is.read(arr)) != -1) {
            System.out.println(new String(arr, 0, len));
        }
        // is.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

6. 请求的中文乱码问题

1)POST 请求

public class RequestDemo extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //1.获取请求正文
		/*POST方式:
		 * 问题:取的时候会不会有乱码
		 * 答案:会。因为是在获取的时候就已经乱码
		 * 解决办法:
		 * 	 是request对象的编码出问题了,因此设置request对象的字符集
		 *   request.setCharacterEncoding("GBK"); 它只能解决POST的请求方式,GET方式解决不了
		 * 结论:
		 * 	 请求正文的字符集和响应正文的字符集没有关系。各是各的
		 */
		request.setCharacterEncoding("UTF-8");
		String username = request.getParameter("username");
        // 输出到控制台
		System.out.println(username);
        // 输出到浏览器:注意响应的乱码问题已经解决了
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(username);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

2)GET 请求

GET 方式请求的正文是在地址栏中,在 Tomcat8.5 版本及以后,Tomcat 服务器已经帮我们解决了,所以不会有乱码问题。

而如果我们使用的不是 Tomcat 服务器,或者 Tomcat 版本是 8.5 以前,那么 GET 方式仍然会有乱码问题。

解决方式如下:

public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        /*
         * GET方式:正文在地址栏
         * username=%D5%C5%C8%FD
         * %D5%C5%C8%FD是已经被编过一次码了
         *
         * 解决办法:
         * 	 使用正确的码表对已经编过码的数据进行解码。
         * 		就是把取出的内容转成一个字节数组,但是要使用正确的码表。(ISO-8859-1)
         * 	 再使用正确的码表进行编码
         * 		把字节数组再转成一个字符串,需要使用正确的码表,是看浏览器当时用的是什么码表
         */
        String username = request.getParameter("username");
        byte[] by = username.getBytes("ISO-8859-1");
        username = new String(by, "GBK");

        //输出到浏览器:注意响应的乱码问题已经解决了
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(username);
}

public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    doGet(request, response);
}

7. 请求转发

1)请求域

  • 请求(Request)域:可以在一次请求范围内进行数据共享。一般用于请求转发的多个资源中共享数据。
  • 作用范围:当前请求(一次请求,和当前请求的转发之中。

请求对象操作共享数据的方法: 

返回值方法名说明
voidsetAttribute(String name, Object value)向请求域对象中存储数据
ObjectgetAttribute(String name)通过名称获取请求域对象中的数据
voidremoveAttribute(String name)通过名称移除请求域对象中的数据

2)请求转发

请求转发:客户端的一次请求到达后,发现需要借助其他 Servlet 来实现功能。

特点:

  • 浏览器地址不变
  • 域对象中的数据不丢失
  • 负责转发的 Servlet 的响应正文会丢失
  • 由转发的目的地(Servlet)来响应客户端

请求转发代码示例:

  • 中转 Servlet
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 1.拿到请求调度对象
        RequestDispatcher rd = request.getRequestDispatcher("/RequestDemo7");  // 如果是给浏览器看的,/可写可不写。如果是给服务器看的,一般情况下,/都是必须的。
        // 2.放入数据到请求域中
        request.setAttribute("CityCode", "bj-010");
        // 3.实现真正的转发操作
        rd.forward(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
  • 目标 Servlet
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 获取请求域中的数据
        String value = (String)request.getAttribute("CityCode");
        response.getWriter().write("welcome to request demo:"+value);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

3)请求转发与重定向的区别

  • 当使用请求转发时,Servlet 容器将使用一个内部的方法来调用目标页面,新的页面继续处理同一个请求,而浏览器将不会知道这个过程(即服务器行为)。与之相反,重定向的含义是第一个页面通知浏览器发送一个新的页面请求。因为当使用重定向时,浏览器中所显示的 URL 会变成新页面的 URL(浏览器行为)。而当使用转发时,该 URL 会保持不变。

  • 重定向的速度比转发慢,因为浏览器还得发出一个新的请求。

  • 同时,由于重定向产生了一个新的请求,所以经过一次重定向后,请求内的对象将无法使用。

总结:

  • 重定向:两次请求,浏览器行为,地址栏改变,请求域中的数据会丢失。
  • 请求转发:一次请求,服务器行为,地址栏不变,请求域中的数据不丢失。

怎么选择是重定向还是转发呢?

  • 通常情况下转发更快,而且能保持请求内的对象,所以它是第一选择。但是由于在转发之后,浏览器中 URL 仍然指向开始页面,此时如果重载当前页面,开始页面将会被重新调用。如果不想看到这样的情况,则选择重定向。

  • 不要仅仅为了把变量传到下一个页面而使用 session 作用域,那会无故增大变量的作用域,转发也许可以帮助解决这个问题。

    • 重定向:以前的请求中存放的变量全部失效,并进入一个新的请求作用域。
    • 转发:以前的请求中存放的变量不会失效,就像把两个页面拼到了一起。

4)请求包含

我们都知道 HTTP 协议的特点是一请求,一响应的方式,所以绝对不可能出现有多个 Servlet 同时响应的方式。那么我们就需要用到“请求包含”,把多个 Servlet 的响应内容合并输出。

请求包含:可以合并其他 Servlet 中的功能,一起响应给客户端。

特点:

  • 浏览器地址不变
  • 域对象中的数据不丢失
  • 被包含的 Servlet 响应头会丢失
  • 这种包含是“动态包含”:各编译各的,只是最后合并输出

代码示例:

  • 被包含 Servlet
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().write("include request demo1");
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}
  • 最终 Servlet
public class RequestDemo2 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().write("include request demo2");
        // 1.拿到请求调度对象
        RequestDispatcher rd = request.getRequestDispatcher("/RequestDemo1");
        // 2.实现包含的操作
        rd.include(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

浏览器响应结果:

include request demo2
include request demo1

8、响应对象

1. 响应对象简介

什么是响应:

响应,它表示了服务器端收到请求,同时也已经处理完成,把处理的结果告知用户。简单来说,指的就是服务器把请求的处理结果告知客户端。在 B/S 架构中,响应就是把结果带回浏览器。

响应对象,顾名思义就是用于在 JavaWeb 工程中实现上述功能的对象。

常用响应对象:

响应对象也是 Servlet 规范中定义的,它包括了协议无关的和协议相关的。

  • 协议无关的对象标准是:ServletResponse 接口

  • 协议相关的对象标准是:HttpServletResponse 接口

类结构图如下:

常用方法:

注意:

  • response 获取的流无需手动关闭(close),由服务器关闭即可。
  • response 得到的字符流和字节流互斥,只能选其一。

2. 字节流响应对象及中文乱码问题

常用方法:

返回值方法名说明
ServletOutputStreamgetOutputStream()获取响应字节输出流对象
voidsetContentType("text/html;charset=UTF-8")设置响应内容类型,解决中文乱码问题

中文乱码问题:

  • 问题:IDEA 编写的 String str = "字节流中文乱码问题",使用字节流输出,会不会产生中文乱码?
  • 答案:会产生乱码。
  • 原因:String str = "字节流中文乱码问题"; 在保存时用的是 IDEA 创建文件使用的字符集 UTF-8。在到浏览器上显示,Chrome 浏览器和 IE 浏览器默认的字符集是 GB2312(GBK),存和取用的不是同一个码表,就会产生乱码。
  • 引申:如果产生了乱码,就是存和取用的不是同一个码表
  • 解决方案:把存和取的码表统一。

解决方法详解:

  1. 解决方法一:修改浏览器的编码,使用右键——编码——改成UTF-8。IE 和火狐浏览器可以直接右键设置字符集。而 chrome 需要安装插件,很麻烦。(不建议使用,尽量不要求用户做什么事情)
  2. 解决方法二:向页面上输出一个 meta 标签:<meta http-equiv="content-type" content="text/html;charset=UTF-8">,其实它就是指挥了浏览器,使用哪个编码进行显示。(不建议使用,因为不好记)
  3. 解决方法三:设置响应消息头,告知浏览器响应正文的MIME类型和字符集:response.setHeader("Content-Type","text/html;charset=UTF-8");
  4. 解决方法四:(推荐使用)本质就是设置了一个响应消息头:response.setContentType("text/html;charset=UTF-8");

示例代码:

public class ResponseDemo extends HttpServlet {

    /**
     * 演示字节流输出的乱码问题
     */
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String str = "字节流输出中文的乱码问题";  // UTF-8的字符集。  解决方法一:浏览器显示也需要使用UTF-8的字符
        // 1.拿到字节流输出对象
        ServletOutputStream sos = response.getOutputStream();

        // 解决方法二:sos.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'>".getBytes());
        // 解决方法三:response.setHeader("Content-Type","text/html;charset=UTF-8");
		
        // 解决方法四:
        response.setContentType("text/html;charset=UTF-8");
		
        // 2.把str转换成字节数组之后输出到浏览器
        sos.write(str.getBytes("UTF-8")); 
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

3. 字符流响应对象及中文乱码问题

public class ResponseDemo extends HttpServlet {

    /**
     * 演示:字符流输出中文乱码
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
				
        String str = "字符流输出中文乱码";

        // 设置响应正文的MIME类型和字符集
        response.setContentType("text/html;charset=UTF-8");
		
        // 1.获取字符输出流
        PrintWriter out = response.getWriter();
        // 2.使用字符流输出中文
		out.write(str);
		
        /**
         * 问题:out.write(str); 直接输出,会不会产生乱码?
		 *
         * 答案:会产生乱码
		 *
         * 原因:
         *   UTF-8(存)————>PrintWriter ISO-8859-1(取)		乱
         *   PrintWirter ISO-8859-1(存)————>浏览器 GBK(取)	乱
         *
         * 解决办法:
         * 	 改变PrintWriter的字符集,PrintWriter是从response对象中获取的,因此设置response的字符集。
         *   注意:设置response的字符集,需要在拿流之前。
         *  response.setCharacterEncoding("UTF-8");
         *
         * response.setContentType("text/html;charset=UTF-8");
         * 此方法,其实是做了两件事:
         * 		1. 设置响应对象的字符集(包括响应对象取出的字符输出流)
         * 		2. 告知浏览器响应正文的MIME类型和字符集
         */
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

4. 响应消息头:设置缓存时间

使用缓存的一般都是静态资源,动态资源一般不能缓存。

public class ResponseDemo extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        String str = "设置缓存时间";
        
        /*
         * 设置缓存时间,其实就是设置响应消息头:Expires,其值是一个毫秒数。
         * 使用的是:response.setDateHeader();
         *
         * 缓存1小时,是在当前时间的毫秒数上加上1小时之后的毫秒值       
        */
        
        response.setDateHeader("Expires",System.currentTimeMillis()+1*60*60*1000);
        response.setContentType("text/html;charset=UTF-8");
        response.getOutputStream().write(str.getBytes());
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

5. 响应消息头:定时刷新

public class ResponseDemo extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String str = "用户名和密码不匹配,2秒后转向登录页面...";
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(str);
        // 定时刷新,其实就是设置一个响应消息头
        response.setHeader("Refresh", "2;URL=/login.html");  // Refresh设置的时间单位是秒,如果刷新到其他地址,需要在时间后面拼接上地址
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

6. 请求重定向

  • 原始 Servlet
public class ResponseDemo6 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 1.设置响应状态码
//		response.setStatus(302);
        // 2.定向到哪里去: 其实就是设置响应消息头,Location
//		response.setHeader("Location", "ResponseDemo7");

        //使用重定向方法
        response.sendRedirect("ResponseDemo7");  // 此行做了什么事,请看上面
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}
  • 目标 Servlet
public class ResponseDemo7 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().write("welcome to ResponseDemo7");
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

7. 文件下载

首先在工程的 web 目录下新建一个目录 uploads,并且拷贝一张图片到目录中,如下图所示:

文件下载的 Servlet: 

public class ResponseDemo8 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        /*
         * 文件下载的思路:
         * 		1.获取文件路径
         * 		2.把文件读到字节输入流中
         * 		3.告知浏览器,以下载的方式打开(告知浏览器下载文件的MIME类型)
         * 		4.使用响应对象的字节输出流输出到浏览器上
         */
        // 1.获取文件路径(绝对路径)
        ServletContext context = this.getServletContext();
        String filePath = context.getRealPath("/uploads/6.jpg");//通过文件的虚拟路径,获取文件的绝对路径
        // 2.通过文件路径构建一个字节输入流
        InputStream in  = new FileInputStream(filePath);
        // 3.设置响应消息头
        response.setHeader("Content-Type", "application/octet-stream");  // 注意下载的时候,设置响应正文的MIME类型,用application/octet-stream
        response.setHeader("Content-Disposition", "attachment;filename=1.jpg");  // 告知浏览器以下载的方式打开
        // 4.使用响应对象的字节输出流输出
        OutputStream out = response.getOutputStream();
        int len = 0;
        byte[] by = new byte[1024];
        while((len = in.read(by)) != -1){
            out.write(by, 0, len);
        }
        in.close();
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

9、会话管理

1. 会话管理简介

什么是会话:

这里的会话,指的是 Web 开发中的一次通话过程,当打开浏览器,访问网站地址后,会话开始,当关闭浏览器(或者到了过期时间),会话结束。

会话管理的作用:

什么时候会用到会话管理呢?最常见的就是购物车,当我们登录成功后,把商品加入到购物车之中,此时我们无论再浏览什么商品,当点击购物车时,那些加入的商品都仍在购物车中。

在我们的实际开发中,还有很多地方都离不开会话管理技术。比如,我们在论坛发帖,没有登录的游客身份是不允许发帖的。所以当我们登录成功后,无论我们进入哪个版块发帖,只要权限允许的情况下,服务器都会认识我们,从而让我们发帖,因为登录成功的信息一直保留在服务器端的会话中。

通过上面的两个例子,我们可以看出,它是为我们共享数据用的,并且是在不同请求间实现数据共享。也就是说,如果我们需要在多次请求间实现数据共享,就可以考虑使用会话管理技术了。

会话管理分类:

在 JavaEE 的项目中,会话管理分为两类,分别是:客户端会话管理技术和服务端会话管理技术。

  • 客户端会话管理技术:它是把要共享的数据保存到了客户端(也就是浏览器端)。每次请求时,把会话信息带到服务器,从而实现多次请求的数据共享。

  • 服务端会话管理技术:它本质仍是采用客户端会话管理技术,只不过保存到客户端的是一个特殊的标识,并且把要共享的数据保存到了服务端的内存对象中。每次请求时,把这个标识带到服务器端,然后使用这个标识,找到对应的内存空间,从而实现数据共享。

2. 客户端会话管理技术:Cookie

它是客户端浏览器的缓存文件,里面记录了客户浏览器访问网站的一些内容。同时,也是 HTTP 请求和响应消息头的一部分。

作用:

Cookie 可以保存客户端浏览器访问网站的相关内容(需要客户端不禁用 Cookie),从而在每次访问需要同一个内容时,先从本地缓存获取,使资源共享,提高效率。

属性名称属性作用是否重要
namecookie 的名称必要属性
valuecookie 的值(不能是中文)必要属性
pathcookie 的路径重要
domaincookie 的域名重要
maxAgecookie 的生存时间重要
versioncookie 的版本号不重要
commentcookie 的说明不重要

详解:

  • Cookie 有大小和个数限制:

    • 每个网站最多只能存 20 个cookie,且大小不能超过 4kb。
    • 同时,所有网站的 cookie 总数不超过 300 个。
  • maxAge 值:

  • 当要删除 Cookie 时,可以设置 maxAge 值为 0。

  • 当不设置 maxAge 时,使用的是浏览器的内存。当关闭浏览器之后,Cookie 将丢失。

  • 设置了此值,就会保存成缓存文件(值必须是大于 0 的,以秒为单位)。

创建 Cookie:

/**
 * 通过指定的名称和值构造一个Cookie
 *
 * Cookie的名称必须遵循RFC 2109规范。这就意味着,它只能包含ASCII字母数字字符,
 * 不能包含逗号、分号或空格或以$字符开头。
 * 创建后无法更改cookie的名称。
 *
 * 该值可以是服务器选择发送的任何内容。
 * 它的价值可能只有服务器才感兴趣。
 * 创建之后,可以使用setValue方法更改cookie的值。
 */
public Cookie(String name, String value) {
	validation.validate(name);
	this.name = name;
	this.value = value;
}

向浏览器添加 Cookie: 

/**
 * 添加Cookie到响应中。此方法可以多次调用,用以添加多个Cookie。
 */
public void addCookie(Cookie cookie);

获取客户端 Cookie:

/**
 * 这是HttpServletRequest中的方法。
 * 它返回一个Cookie的数组,包含客户端随此请求发送的所有Cookie对象。
 * 如果没有符合规则的cookie,则此方法返回null。
 */
 public Cookie[] getCookies();

4)Cookie 的 Path :客户浏览器何时带 cookie 到服务器端,何时不带

需求说明:

创建一个 Cookie,设置 Cookie 的 path,通过不同的路径访问,从而查看请求携带 Cookie 的情况。

案例目的:

通过此案例的讲解,可以清晰的描述出,客户浏览器何时带 cookie 到服务器端,何时不带。

案例步骤:

第一步:编写 Servlet。

  1. 在 demo1 中写一个 cookie 到客户端
  2. 在 demo2 和 demo3 中分别去获取 cookie
    • demo1 的 Servlet 映射是 /servlet/PathQuestionDemo1
    • demo2 的 Servlet 映射是 /servlet/PathQuestionDemo2
    • demo3 的 Servlet 映射是 /PathQuestionDemo3
/**
 * 写一个 cookie 到客户端
 */
public class PathQuestionDemo1 extends HttpServlet {

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// 1.创建一个Cookie
		Cookie cookie = new Cookie("pathquestion", "CookiePathQuestion");
		// 2.设置cookie的最大存活时间
		cookie.setMaxAge(Integer.MAX_VALUE);
		// 3.把cookie发送到客户端
		response.addCookie(cookie);  // setHeader("Set-Cookie", "cookie的值")
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}
/**
 * 获取Cookie,名称是pathquestion
 */
public class PathQuestionDemo2 extends HttpServlet {

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// 1.获取所有的cookie
		Cookie[] cs = request.getCookies();
		// 2.遍历cookie的数组
		for(int i=0; cs!=null && i<cs.length; i++){
			if("pathquestion".equals(cs[i].getName())){
				// 找到了我们想要的cookie,输出cookie的值
				response.getWriter().write(cs[i].getValue());
				return;
			}
		}
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}
/**
 * 获取Cookie,名称是pathquestion
 */
public class PathQuestionDemo3 extends HttpServlet {

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// 1.获取所有的cookie
		Cookie[] cs = request.getCookies();
		// 2.遍历cookie的数组
		for(int i=0;cs!=null && i<cs.length;i++){
			if("pathquestion".equals(cs[i].getName())){
				// 找到了我们想要的cookie,输出cookie的值
				response.getWriter().write(cs[i].getValue());
				return;
			}
		}
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}

第二步:配置 Servlet。

<!-- Demo1:设置Cookie -->
<servlet>
    <servlet-name>PathQuestionDemo1</servlet-name>
    <servlet-class>com.web.servlet.pathquestion.PathQuestionDemo1</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PathQuestionDemo1</servlet-name>
    <url-pattern>/servlet/PathQuestionDemo1</url-pattern>
</servlet-mapping>

<!-- Demo2:获取Cookie -->
<servlet>
    <servlet-name>PathQuestionDemo2</servlet-name>
    <servlet-class>com.web.servlet.pathquestion.PathQuestionDemo2</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PathQuestionDemo2</servlet-name>
    <url-pattern>/servlet/PathQuestionDemo2</url-pattern>
</servlet-mapping>

<!-- Demo3:获取Cookie -->
<servlet>
    <servlet-name>PathQuestionDemo3</servlet-name>
    <servlet-class>com.web.servlet.pathquestion.PathQuestionDemo3</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PathQuestionDemo3</servlet-name>
    <url-pattern>/PathQuestionDemo3</url-pattern>
</servlet-mapping>

测试结果:

通过分别运行 PathQuestionDemo1,2 和 3 这三个 Servlet,我们发现由 demo1 写的 Cookie,在 demo2 中可以取到,但是到了 demo3 中就无法获取了。

路径问题的分析及总结:

问题:demo2 和 demo3 谁能取到 Cookie?

答案:demo2 能取到,demo3 取不到。

分析:

  1. 首先,我们要知道如何确定一个 cookie ?那就是使用 cookie 的三个属性组合:domain + path + name
  2. 这里面,同一个应用的domain是一样的,在我们的案例中都是 localhost。并且,我们取的都是同一个 cookie,所以 name 也是一样的,都是 pathquestion。
  3. 那么,不一样的只能是 path 了。但是示例中没有设置过 cookie 的 path 属性,这就表明 path 是有默认值的。
  4. 接下来,我们打开这个 cookie 来看一看,在 IE 浏览器访问一次 PathQuestionDemo1 这个 Servlet:

我们是通过 demo1 写的 cookie,demo1 的访问路径是 http://localhost:9090/servlet/PathQuestionDemo1 。通过比较两个路径:请求资源地址和 cookie 的 path,可以看出:cookie 的 path 默认值是 URI 中去掉资源的部分

在上述案例中:

访问 URLURI 部分Cookie 的 Path是否携带 Cookie能否取到 Cookie
http://localhost:9090/servlet/PathQuestionDemo2/servlet/PathQuestionDemo2/servlet/能取到
http://localhost:9090/PathQuestionDemo3/PathQuestionDemo3/servlet/不带不能取到

总结:客户端什么时候带 cookie 到服务器,什么时候不带?

  • 就是看 URI 和 cookie 的 path 比较。
  • URI.startWith(cookie 的 path):如果返回的是 true 就带,如果返回的是 false 就不带。

3. 服务端会话管理技术:Session

1)HttpSession 对象概述

HttpSession 是 Servlet 规范中提供的一个接口。该接口的实现由 Servlet 规范的实现提供商提供。

由于 Tomcat 服务器对 Servlet 规范进行了实现,所以 HttpSession 接口的实现由 Tomcat 提供。该对象用于提供一种通过多个页面请求或访问网站,来标识用户并存储有关该用户的信息的方法。简单说它就是一个服务端的会话对象,用于存储用户的会话数据。

同时,它也是 Servlet 规范中四大域对象之一的会话域对象。并且它也是用于实现数据共享的,但它与前面介绍的应用域和请求域是有区别的。

域对象作用范围使用场景
ServletContext整个应用范围当前项目中需要数据共享时,可以使用此域对象。
ServletRequest当前请求范围在请求或者当前请求转发时需要数据共享可以使用此域对象。
HttpSession会话返回在当前会话范围中实现数据共享;可以在多次请求中实现数据共享。

2)HttpSession 对象的获取

HttpSession 的获取是通过 HttpServletRequest 接口中的两个方法获取的,如下图所示:

两个方法的区别: 

3)HttpSession 常用方法

4)HttpSession 入门案例

需求说明:

在请求 HttpSessionDemo1 这个 Servlet 时,携带用户名信息,并且把信息保存到会话域中,然后从 HttpSessionDemo2 这个 Servlet 中获取登录信息。

案例目的:

通过本案例认识到会话域的作用,即多次请求间的数据共享。因为是两次请求,请求域肯定不一样了,所以不能用请求域实现。

最终掌握 HttpSession 对象的获取和使用。

原理分析:

HttpSession 虽然是服务端会话管理技术的对象,但它本质仍是一个 Cookie,是一个由服务器自动创建的特殊的 Cookie,Cookie 的名称是 JSESSIONID,其值是服务器分配的一个唯一的标识。

当我们使用 HttpSession 时,浏览器在没有禁用 Cookie 的情况下,都会把这个 Cookie 带到服务器端,然后根据唯一标识去查找对应的 HttpSession 对象,找到了,我们就可以直接使用了。

下图就是入门案例中,HttpSession 分配的唯一标识,可以看到两次请求的 JSESSIONID 的值是一样的:

5)HttpSession 的钝化和活化

什么是持久态?

  • 把长时间不用,但还不到过期时间的 HttpSession 进行序列化,写到磁盘上。

  • 我们把 HttpSession 持久态也叫做钝化(与钝化相反的,我们叫活化)。

什么时候使用持久化?

  • 第一种情况:当访问量很大时,服务器会根据 getLastAccessTime 来进行排序,对长时间不用,但是还没到过期时间的 HttpSession 进行持久化。

  • 第二种情况:当服务器进行重启的时候,为了保持客户 HttpSession 中的数据,也要对 HttpSession 进行持久化。

注意:

  • HttpSession 的持久化由服务器来负责管理,我们不用关心。

  • 只有实现了序列化接口的类才能被序列化,否则不行。

四、JSP、EL 表达式、JSTL 

1、JSP概述

1. JSP 简介

JSP 全称是 Java Server Page,它和 Servlet 一样,也是 Sun 公司推出的一套开发动态 web 资源的技术,称为 JSP/Servlet 规范。

JSP 的本质其实就是一个 Servlet。

2. JSP 与 Servlet 的区别

  1. JSP 经编译后就变成了 Servlet(JSP 的本质就是 Servlet,JVM 只能识别 Java 的类,不能识别 JSP 的代码,于是 Web 容器将 JSP 的代码编译成 JVM 能够识别的 Java类)。

  2. Servlet 和 JSP 最主要的不同点在于:Servlet 的应用逻辑在 Java 文件中,并且完全从表示层中的 HTML 里分离开来。而对于 JSP,Java 和 HTML 可以组合成一个扩展名为 .jsp 的文件。

  3. JSP 是 Servlet 的一种简化,使用 JSP 只需要完成输出到客户端的内容(JSP 中的 Java 脚本如何镶嵌到一个类中,由 JSP 容器完成);而 Servlet 则是个完整的 Java 类,这个类的 service() 方法用于生成对客户端的响应。

  4. JSP 更擅长于页面显示(视图),Servlet 更擅长于逻辑控制。

  5. Servlet 中没有内置对象,JSP 中的内置对象都必须通过 HttpServletRequest 对象、 HttpServletResponse 对象以及 HttpServlet 对象得到。

3. JSP、HTML、Servlet 适用场景

类别适用场景
HTML只能开发静态资源,不能包含 java 代码,无法添加动态数据。
Servlet写 java 代码,可以输出页面内容,但是不够方便,开发效率低。
JSP包括了 HTML 的展示技术,同时具备 Servlet 输出动态资源的能力;但是不适合作为控制器来用。

4. JSP 简单入门

创建 JavaWeb 工程:

在 index.jsp 中编写内容: 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>JSP的入门</title>
  </head>
  <body>
      这是第一个JSP页面
  </body>
</html>

测试运行:

5. JSP 执行详解

JSP 就是一个特殊的 Servlet,其执行过程如下:

  1. 客户端提交请求;
  2. Tomcat 服务器解析请求地址;
  3. 找到 JSP 页面;
  4. Tomcat 将 JSP 页面翻译成 Servlet 的 java 文件;
  5. 将翻译好的 .java 文件编译成 .class 文件;
  6. 返回到客户端浏览器上。

JSP 的 .java 文件内容分析:

当我们打开 index.jsp 翻译的 java 文件看到的就是public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase类的声明,接着我们在 Tomcat 的源码中找到该类的声明,如下图:

该图表明我们写的 JSP 的本质就是一个 HttpServlet:

同时,我们在 index_jsp.java 文件中找到了输出页面的代码,并且在浏览器端查看源文件,看到的内容是一样的。这也就是说明,我们的浏览器上的内容,在通过 JSP 展示时,本质都是用 out.write() 输出出来的。

至此,我们应该清楚地认识到,JSP 是一个特殊的 Servlet,主要是用于展示动态数据。它展示的方式是用流把数据输出。而我们在使用 JSP 时,涉及的 HTML 部分,都与 HTML 的用法一致,这部分称为 JSP 中的模板元素,在开发过程中,先写好这些模板元素,因为它们决定了页面的外观。

2、JSP语法

1. Java 代码块

在 JSP 中,可以使用 java 脚本代码,形式为:<% 此处写java代码 %>

但是在实际开发中,极少使用此种形式编写 java 代码。同时需要注意的是:

<%
	在里面写java程序脚本,需要注意:这里面的内容由tomcat负责翻译,翻译之后是service方法的成员变量
%>

示例:

<!--Java代码块-->
<% out.println("这是Java代码块");%>
<hr/>

2. JSP 表达式

在 JSP 中,可以使用特定表达式语法,形式为:<%=表达式%>

JSP 在翻译完后是out.print(表达式内容);,所以<%out.print("当前时间);%><%="当前时间"%>是等价的。

在实际开发中,这种表达式语法用的也很少。

示例:

<!--JSP表达式-->
<%="这是JSP表达式"%><br/>
就相当于<br/>
<%out.println("这是没有JSP表达式输出的");%>

3. JSP 声明

在 JSP 中也可以声明一些变量、方法、静态方法,形式为:<%! 声明的内容 %>

使用 JSP 声明时需要注意:

<%! 
	需要注意的是:写在里面的内容将会被tomcat翻译成全局的属性或者类方法
%>       

示例:

<!--JSP声明-->
<%! String str = "声明语法格式";%>
<%=str%>

4. JSP 注释

在使用 JSP 时,它也有自己的注释,形式为:<%--注释--%>

需要注意的是:

  • 在 JSP 中可以使用 HTML 的注释,但是只能注释 HTML 元素,不能注释 Java 程序片段和表达式。同时,被 HTML 注释部分会参与翻译,并且会在浏览器上显示。

  • JSP 的注释不仅可以注释 Java 程序片段,也可以注释 HTML 元素,并且被 JSP 注释的部分不会参与翻译成 .java 文件,也不会在浏览器上显示。

示例:

<%--JSP注释--%>
<!--HTML注释-->

5. 语法示例

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>JSP语法</title>
</head>
<body>

<!--Java代码块-->
<% out.println("这是Java代码块");%>
<hr/>

<!--JSP表达式-->
<%="这是JSP表达式"%><br/>
就相当于<br/>
<%out.println("这是没有JSP表达式输出的");%>

<hr/>
<!--JSP声明-->
<%! String str = "声明语法格式";%>
<%=str%>

<hr/>

<%--JSP注释--%>
<!--HTML注释-->

</body>
</html>

JSP 语法运行结果:

3、JSP指令

1. page 指令

language:告知引擎,脚本使用的是 Java。默认是 Java,不写也行。

extends:告知引擎,JSP 对应的 Servlet 的父类是哪个,不需要写,也不需要改。

import:告知引擎,导入哪些包(类)。

注意:引擎会自动导入:java.lang.*, javax.servlet.*, javax.servlet.http.*, javax.servlet.jsp.*

导入的语法:

<%@pageimport=”java.util.Date, java.util.UUID”%>

或者:

<%@pageimport=”java.util.Date”%>
<%@pageimport=”java.util.UUID”%>

Eclipse 自动导入:Alt + /

session:告知引擎是否产生 HttpSession 对象,即是否在代码中调用 request.getSession()。默认是 true。

buffer:JspWriter 用于输出 JSP 内容到页面上。告知引擎,设定他的缓存大小。默认 8kb。

errorPage:告知引擎,当前页面出现异常后,应该转发到哪个页面上(路径写法:/代表当前应用)。

  • 当在 errorPage 上使用了 isErrorPage=true 之后,IE8 有时候不能正常显示。
  • 配置全局错误页面:web.xml。
  • 当使用了全局错误页面,就无须再写 errorPage 来实现转到错误页面,而是由服务器负责跳转到错误页面。
<error-page>    
    <exception-type>java.lang.Exception</exception-type>    			
    <location>/error.jsp</location>
</error-page>

<error-page>
    <error-code>404</error-code>
    <location>/404.html</location>
</error-page>   

isErrorPage:告知引擎,是否抓住异常。如果该属性为 true,页面中就可以使用 exception 对象,打印异常的详细信息。默认值是 false。

contentType:告知引擎,响应正文的 MIME 类型。

  • contentType="text/html;charset=UTF-8" 相当于 response.setContentType("text/html;charset=UTF-8");

pageEncoding:告知引擎,翻译 JSP 时(从磁盘上读取 JSP 文件)所用的码表。

  • pageEncoding="UTF-8" 相当于告知引擎用 UTF-8 读取 JSP 。

isELIgnored:告知引擎,是否忽略 EL 表达式,默认值是 false,不忽略。

2. include 指令

语法:<%@include file="" %>,该指令表示包含外部页面。

  • file 属性以/开头,表示当前应用。

使用示例:

静态包含 .java 文件内容: 

3. aglib 指令

语法:<%taglib uri="" prefix=""%>

作用:该指令用于引入外部标签库(html 标签和 jsp 标签无需引入)。

属性:

  • uri:外部标签的 URI 地址。
  • prefix:使用标签时的前缀。

4、JSP对象

1. 九大隐式对象

什么是隐式对象呢?它指的是在 JSP 中,可以不声明就直接使用的对象。它只存在于 JSP 中,因为 Java 类中的变量必须要先声明再使用。

其实 JSP 中的隐式对象也并非是未声明,只是它是在翻译成 .java 文件时声明的,所以我们可以在 JSP 中直接使用。

隐式对象名称类型备注
requestjavax.servlet.http.HttpServletRequest
responsejavax.servlet.http.HttpServletResponse
sessionjavax.servlet.http.HttpSessionpage 指令可以控制开关
applicationjavax.servlet.ServletContext
pageJava.lang.Object当前 JSP 对应的 servlet 引用实例
configjavax.servlet.ServletConfig
exceptionjava.lang.Throwablepage 指令可以控制开关
outjavax.servlet.jsp.JspWriter字符输出流,相当于 printwriter
pageContextjavax.servlet.jsp.PageContext重要

2. ageContext 对象

它是 JSP 独有的对象,Servlet 中没有这个对象。

ageContext 本身也是一个域(作用范围)对象,但是它可以操作其他 3 个域对象中的属性,而且还可以获取其他 8 个隐式对象。

生命周期:

它是一个局部变量,所以它的生命周期随着 JSP 的创建而开始,随着 JSP 的结束而消失。每个 JSP 页面都有一个独立的 PageContext。

常用方法:

3. 四大域对象

域对象名称范围级别备注
PageContext页面范围最小,只能在当前页面用因范围太小,开发使用较少
ServletRequest请求范围一次请求或当前请求转发用当请求转发之后,再次转发时请求域丢失。开发使用较多
HttpSession会话范围多次请求数据共享时使用多次请求共享数据,但不同的客户端不能共享。。如存放用户的登录信息、购物车功能。开发使用较多
ServletContext应用范围最大,整个应用都可以使用因范围太大,尽量少用。如果对数据有修改则需要做同步处理

5、MVC模型

MVC 模型:

  • M(model,模型):通常用于封装数据模型(实体类)。

  • V(view,视图):通常用于展示数据。动态展示用 JSP 页面,静态数据展示用 HTML。

    • JSP 擅长显示界面,不擅长处理程序逻辑。在 Web 开发中多用于展示动态界面。
  • C(controller,控制器):通常用于处理请求和响应。一般指的是 Servlet。

    • Servlet 擅长处理业务逻辑,不擅长输出显示界面。在 Web 开发中多用于控制程序逻辑(流程)。

6、JSP综合案例

1. 登录功能

index.jsp:主页。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>首页</title>
</head>
<body>
    <%--
        获取会话域的数据
        如果获取到了,则显示添加和查询功能
        如果获取不到,则显示登录功能
    --%>
    <% Object username = session.getAttribute("username");
            if(username == null || "".equals(username)){
    %>
            <a href="/web_demo/login.jsp">登录<a/>
    <%}else { %>
            <a href="/web_demo/add.jsp">添加<a/>
            <a href="/web_demo/queryServlet">查询<a/>
    <% } %>
</body>
</html>

login.jsp:登录页。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录页</title>
</head>
<body>
    <form action="/web_demo/loginServlet" method="get" autocomplete="off">
        姓名:<input type="text" name="username"><br/>
        密码:<input type="password" name="password"><br/>
        <button type="submit">登录</button>
    </form>
</body>
</html>

LoginServlet:获取登录页的用户名和密码。

package com.demo;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取用户名和密码
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        // 校验用户名密码
        // 用户名为空
        if("".equals(username) || username == null){
            // 重定向到登录页
            resp.sendRedirect("/web_demo/login.jsp");
        } else {
            // 用户名不为空,存入会话域数据
            req.getSession().setAttribute("username", username);
            // 重定向到首页
            resp.sendRedirect("/web_demo/index.jsp");
        }
    }

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

2. 添加功能

add.jsp:实现添加学生信息的表单项。

<%--
  Created by IntelliJ IDEA.
  User: juno
  Date: 2021/10/7
  Time: 21:18
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>添加页</title>
</head>
<body>
    <form action="/web_demo/addServlet" method="get" autocomplete="off">
        学生姓名:<input type="text" name="username"><br/>
        学生年龄:<input type="number" name="age"><br/>
        学生成绩:<input type="number" name="score"><br/>
        <button type="submit">保存</button>
    </form>
</body>
</html>

AddServlet:获取学生信息并保存到文件中。

package com.demo;

import com.demo.bean.Student;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取表单数据
        String username = req.getParameter("username");
        String age = req.getParameter("age");
        String score = req.getParameter("score");

        // 创建学生对象并赋值
        Student student = new Student();
        student.setAge(age);
        student.setScore(score);
        student.setUsername(username);

        // 将新增数据写入文件中
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("d://student.txt", true));
        bufferedWriter.write(username+","+age+","+score);
        bufferedWriter.newLine();
        bufferedWriter.close();

        // 通过定时刷新功能响应给浏览器
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().write("添加成功!2秒后自动跳转到首页~");
        resp.setHeader("Refresh", "2;URL=/web_demo/index.jsp");
    }

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

3. 查看功能

QueryServlet:读取文件中的学生信息到集合中。

package com.demo;

import com.demo.bean.Student;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

@WebServlet("/queryServlet")
public class QueryServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建字符输入流对象,关联读取的文件
        BufferedReader bufferedReader = new BufferedReader(new FileReader("d://student.txt"));

        // 创建集合对象,用于保存student对象
        ArrayList<Student> arrayList = new ArrayList<>();

        // 循环读取文件中的数据,将数据封装到Student对象中。再把多个学生对象添加到集合中
        String line;
        while((line = bufferedReader.readLine()) != null){
            Student student = new Student();
            String[] attribute = line.split(",");
            student.setUsername(attribute[0]);
            student.setAge(attribute[1]);
            student.setScore(attribute[2]);
            arrayList.add(student);
        }

        // 将集合对象存入会话域中
        req.getSession().setAttribute("students", arrayList);

        // 重定向到学生列表页面
        resp.sendRedirect("/web_demo/query.jsp");
    }

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

query.jsp:展示学生信息列表。

<%--
  Created by IntelliJ IDEA.
  User: juno
  Date: 2021/10/7
  Time: 23:41
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="com.demo.bean.Student" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>学生列表页</title>
</head>
<body>
  <table width="600px" border="1px">
    <tr>
      <th>学生姓名</th>
      <th>学生年龄</th>
      <th>学生成绩</th>
    </tr>
      <% ArrayList<Student> students = (ArrayList<Student>) session.getAttribute("students");
          for(Student student: students){
      %>
          <tr align="center">
            <td><%= student.getUsername() %></td>
            <td><%= student.getAge() %></td>
            <td><%= student.getScore() %></td>
          <tr/>
      <%  } %>
  </table>
</body>
</html>

页面效果如下:

7、EL表达式

1. EL 表达式介绍

基本概念:

EL 表达式,全称是 Expression Language,意为表达式语言。它是 Servlet 规范中的一部分,是 JSP2.0 规范加入的内容。

其作用是用于在 JSP 页面中获取数据,从而让我们的 JSP 脱离 Java 代码块和 JSP 表达式。

1)基本语法

EL 表达式的语法格式非常简单:${表达式内容}。

假定我们在请求域中存入了一个名称为 "message" 的数据,此时在 JSP 中获取的方式如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>EL表达式入门案例</title>
  </head>
  <body>
    <%--使用java代码在请求域中存入一个名称为message的数据--%>
    <% request.setAttribute("message","Expression Language"); %>

    Java代码块获取:<% out.print(request.getAttribute("message")); %>
    <br/>
    JSP表达式获取:<%= request.getAttribute("message") %>
    <br/>
    EL表达式获取:${message}
  </body>
</html>

可以看出,有多种方式可以从请求域中获取数据,但是 EL 表达式写起来是最简单的方式。这也是以后我们在实际开发中,当使用 JSP 作为视图时,绝大多数都会采用的方式。

2)EL 表达式注意事项

EL 表达式没有空指针异常、没有数组下标越界、没有字符串拼接。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>EL表达式的注意事项</title>
  </head>
  <body>
    <%-- EL表达式的三个没有 --%>
    第一个:没有空指针异常<br/>
    <% String str = null;
       request.setAttribute("testNull",str);
    %>
    ${testNull}
    <hr/>
    第二个:没有数组下标越界<br/>
    <% String[] strs = new String[]{"a","b","c"};
       request.setAttribute("strs",strs);
    %>
    取第一个元素:${strs[0]}<br/>
    取第六个元素:${strs[5]}
    <hr/>
    第三个:没有字符串拼接<br/>
    <%--${strs[0]+strs[1]}--%>
    ${strs[0]}+${strs[1]}
  </body>
</html>

运行效果:

3)EL 表达式的数据来源

EL 表达式只能从四大域中获取数据,调用的就是findAttribute(name,value);方法,通过名称根据范围由小到大逐个域中查找,找到就返回,找不到就什么都不显示。

它可以获取对象,可以是对象中关联的其他对象,可以是一个 List 集合,也可以是一个 Map 集合。

使用:创建两个实体类(User 和 Address)并使用 EL 表达式获取输出

  • User 实体类
public class User implements Serializable{

    private String name = "黑马";
    private int age = 18;
    private Address address = new Address();
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address address) {
        this.address = address;
    }    
}
  • Address 实体类
public class Address implements Serializable {

    private String province = "北京";
    private String city = "昌平区";

    public String getProvince() {
        return province;
    }
    public void setProvince(String province) {
        this.province = province;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
}
  • JSP
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ page import="com.itheima.domain.User" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>EL入门</title>
    </head>
    <body>
        <%--
            EL表达式的数据获取:
                它只能在四大域对象中获取数据,不在四大域对象中的数据它取不到。
                它的获取方式就是findAttribute(String name)
         --%>
         
         <br/>-----------获取对象数据---------------------<br/>
         <% //1.把用户信息存入域中
             User user = new User();
             pageContext.setAttribute("u", user);
          %>
          ${u}===============输出的是内存地址 <%--相当于调用 <%=pageContext.findAttribute("u")%> --%><br/>
          ${u.name} <%--相当于调用 <% User user = (User) pageContext.findAttribute("u");out.print(user.getName());%> --%><br/>
          ${u.age}
         
         <br/>-----------获取关联对象数据------------------<br/>
         ${u.address}==========输出的address对象的地址<br/>
         ${u.address.province}${u.address.city}<br/>
         ${u["address"]['province']}
         
         <br/>-----------获取数组数据---------------------<br/>
         <% String[] strs = new String[]{"He", "llo", "Expression", "Language"}; 
             pageContext.setAttribute("strs", strs);
         %>
         ${strs[0]}==========取的数组中下标为0的元素<br/>
         ${strs[3]}
         ${strs[5]}===========如果超过了数组的下标,则什么都不显示<br/>
         ${strs["2"]}=========会自动为我们转换成下标<br/>
         ${strs['1']}
         
         <br/>-----------获取List集合数据-----------------<br/>
         <% List<String> list = new ArrayList<String>();
             list.add("AAA");
             list.add("BBB");
             list.add("CCC");
             list.add("DDD");
             pageContext.setAttribute("list", list);
          %>
         ${list}<br/>
         ${list[0]}<br/>
         ${list[3]}<br/>
         
         <br/>-----------获取Map集合数据------------------<br/>
         <% Map<String,User> map = new HashMap<String,User>();
             map.put("aaa", new User());
             pageContext.setAttribute("map", map);
          %>
          ${map}<br/>
          ${map.aaa} <%--获取map的value,是通过get(Key) --%> <br/>
          ${map.aaa.name}${map.aaa.age}<br/>
          ${map["aaa"].name}
    </body>
</html>

运行效果:

4)EL 表达式运算符

有两个特殊的运算符,使用方式如下: 

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ page import="com.itheima.domain.User" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>EL两个特殊的运算符</title>
    </head>
    <body>
        <%-- empty运算符:
             它会判断:对象是否为null,字符串是否为空字符串,集合中元素是否是0个
        --%>
        <% String str = null;
          String str1 = "";
          List<String> slist = new ArrayList<String>();
          pageContext.setAttribute("str", str);
          pageContext.setAttribute("str1", str1);
          pageContext.setAttribute("slist", slist);
        %>
        ${empty str}============当对象为null返回true<br/>
        ${empty str1}==========当字符串为空字符串是返回true(注意:它不会调用trim()方法)<br>
        ${empty slist}==========当集合中的元素是0个时,是true
        <hr/>
			
        <%-- 三元运算符: 
             条件?真:假  
        --%>
        <% request.setAttribute("gender", "female"); %>
        <input type="radio" name="gender" value="male" ${gender eq "male"?"checked":""} >男
        <input type="radio" name="gender" value="female" ${gender eq "female"?"checked":""}>女
    </body>
</html>

运行效果:

5)EL 表达式的 11 个隐式对象

EL表达式除了能在四大域中获取数据,同时也为我们提供了隐式对象,可以让我们无需声明而直接使用,并且可以访问对象有返回值的方法。

11 个对象见下表(需要注意的是,它和 JSP 的隐式对象不是一回事):

EL 中的隐式对象类型对应的 JSP 隐式对象备注
PageContextJavax.serlvet.jsp.PageContextPageContext完全一样
ApplicationScopeJava.util.Map没有应用域范围的对象数据
SessionScopeJava.util.Map没有会话域范围的对象数据
RequestScopeJava.util.Map没有请求域范围的对象数据
PageScopeJava.util.Map没有页面域范围的对象数据
HeaderJava.util.Map没有请求消息头 key,值是 value(一个)
HeaderValuesJava.util.Map没有请求消息头 key,值是数组(一个头多个值)
ParamJava.util.Map没有请求参数 key,值是 value(一个)
ParamValuesJava.util.Map没有请求参数 key,值是数组(一个名称多个值)
InitParamJava.util.Map没有全局参数,key 是参数名称,value 是参数值
CookieJava.util.Map没有Key 是 cookie 的名称,value 是 cookie 对象

示例:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>EL表达式11个隐式对象</title>
</head>
<body>

    <%-- applicationScope sessionScope requestScope pageScope 操作四大域对象中的数据 --%>
    <%--获取四大域对象中的数据--%>
    <%
       pageContext.setAttribute("username", "zhangsan");
       request.setAttribute("username", "zhangsan");
       session.setAttribute("username", "zhangsan");
       application.setAttribute("username", "zhangsan");
    %>
    ${username} <br>
    ${requestScope.username} <br>

    <%-- pageContext对象 可以获取其他三个域对象和JSP中八个隐式对象 --%>
    <%-- 获取虚拟目录名称 --%>
    ${pageContext.request.contextPath} <%-- 等价于 <%= request.getContextPath() %> --%>

    <%-- header headerValues  获取请求头数据 --%>
    ${header["connection"]} <br>
    ${headerValues["connection"][0]} <br>

    <%-- param paramValues 获取请求参数数据 --%>
    ${param.username} <br>
    ${paramValues.hobby[0]} <br>
    ${paramValues.hobby[1]} <br>

    <%-- initParam 获取全局配置参数 --%>
    ${initParam["pname"]}  <br>

    <%-- cookie 获取cookie信息 --%>
    ${cookie}  <br>  <%-- 获取Map集合 --%>
    ${cookie.JSESSIONID}  <br> <%-- 获取map集合中第二个元素 --%>
    ${cookie.JSESSIONID.name}  <br> <%-- 获取cookie对象的名称 --%>
    ${cookie.JSESSIONID.value} <%-- 获取cookie对象的值 --%>

</body>
</html>

8、JSTL

1. JSTL 简介

JSTL 的全称是 JSP Standard Tag Libary,是 JSP 中标准的标签库。

JSTP 是由 Apache 实现的,由以下 5 个部分组成:

组成作用说明
Core核心标签库通用逻辑处理
Fmt国际化相关需要不同地域显示不同语言时使用
FunctionsEL 函数EL 表达式可以使用的方法
SQL操作数据库很少用
XML操作XML很少用

1)使用要求

要想使用 JSTL 标签库,需要先在 javaweb 工程中需要导入坐标。首先是要在工程的 WEB-INF 目录中创建一个 lib 目录,然后把 JSTL 的 jar 拷贝到 lib 目录中,再在 jar 包上点击右键,然后选择【Add as Libary】添加。

如下图所示:

2)核心标签库

在实际开发中,用到的 JSTL 标签库主要以核心标签库为准,偶尔会用到国际化标签库的标签。

下表中把经常可能用到的标签列在此处,其余标签库可自行查阅:

标签名称功能分类分类作用
<c:if>流程控制核心标签库用于条件判断
<c:choose>
<c:when>
<c:otherwise>
流程控制核心标签库用于多个条件判断
<c:foreache>迭代操作核心标签库用于循环遍历

3)JSTL 使用

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%-- 导入jstl标签库 --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>JSTL的常用标签</title>
  </head>
  <body>
  
    <%-- 演示 c:if --%>
    <% pageContext.setAttribute("score", "F"); %>
    <c:if test="${pageScope.score eq 'A'}">
        优秀
    </c:if>
    <c:if    test="${pageScope.score eq 'C'}">
        一般
    </c:if>
    <hr/>
    
    <%-- 演示 c:choose c:when c:otherwise --%>
    <c:choose>
        <c:when test="${pageScope.score eq 'A'}">
            AAA
        </c:when>
        <c:when test="${pageScope.score eq 'B'}">BBB
        </c:when>
        <c:when test="${pageScope.score eq 'C'}">CCC
        </c:when>
        <c:when test="${pageScope.score eq 'D'}">DDD
        </c:when>
        <c:otherwise>其他</c:otherwise>
    </c:choose>
    
    <%-- 演示 c:forEach 它是用来遍历集合的
         属性:
             items:要遍历的集合,它可以是EL表达式取出来的
             var:把当前遍历的元素放入指定的page域中。 var的取值就是key,当前遍历的元素就是value
                 注意:它不能支持EL表达式,只能是字符串常量
             begin:开始遍历的索引
             end:结束遍历的索引
             step:步长。i+=step
             varStatus:它是一个计数器对象。里面有两个属性,一个是用于记录索引。一个是用于计数。
                        索引是从0开始。计数是从1开始
    --%>
    <hr/>
    
    <% List<String> list = new ArrayList<String>();
       list.add("AAA");
       list.add("BBB");
       list.add("CCC");
       list.add("DDD");
       list.add("EEE");
       list.add("FFF");
       list.add("GGG");
       list.add("HHH");
       list.add("III");
       list.add("JJJ");
       list.add("KKK");
       list.add("LLL");
       pageContext.setAttribute("list",list);
     %>
     
    <c:forEach items="${list}" var="s" begin="1" end="7" step="2">
        ${s}<br/>
    </c:forEach>
    
    <hr/>
    
    <c:forEach begin="1" end="9" var="num">
        <a href="#">${num}</a>
    </c:forEach>
    <hr/>
    <table>
        <tr>
            <td>索引</td>
            <td>序号</td>
            <td>信息</td>
        </tr>

    <c:forEach items="${list}" var="s" varStatus="vs">
        <tr>
            <td>${vs.index}</td>
            <td>${vs.count}</td>
            <td>${s}</td>
        </tr>
    </c:forEach>

    </table>
  </body>
</html>

五、Filter 过滤器

1、Filter 简介

1. 过滤器基本概念

Servlet 过滤器从字面可理解为经过一层层的过滤处理才达到使用的要求,而其实 Servlet 过滤器就是服务器与客户端请求与响应的中间层组件。

在实际项目开发中 Servlet 过滤器主要用于对浏览器的请求进行过滤处理,将过滤后的请求再转给下一个资源。

过滤器是以一种组件的形式绑定到 Web 应用程序当中的,与其他的 Web 应用程序组件不同的是,过滤器是采用了“链”的方式进行处理的,如下所示:

2. Filter

Filter(过滤器)是 JavaWeb 三大组件之一(另外两个是 Servlet 和 Listener),是在 2000 年发布的 Servlet2.3 规范中加入的一个接口,是 Servlet 规范中非常实用的技术。

当需要限制用户访问某些资源或者在处理请求时提前处理某些资源的时候,就可以使用过滤器(Filter)完成。

  • 当一个请求访问服务器资源时,服务器首先判断会是否有过滤器与请求资源相关联,如果有,过滤器会先将请求拦截下来,完成一些特定的功能,再由过滤器决定是否继续交给请求资源进行处理。
  • 响应也是类似的。

Filter 应用场景:

  • URL 级别的权限控制
  • 过滤敏感词汇
  • 中文乱码问题
  • ...

Servlet 的 Filter 特点:

  1. 声明式的
    通过在 web.xml 配置文件中声明,允许添加、删除过滤器,而无须改动任何应用程序代码或 JSP 页面。

  2. 灵活的
    过滤器可用于客户端的直接调用执行预处理和后期的处理工作,通过过滤链可以实现一些灵活的功能。

  3. 可移植的
    由于现今各个 Web 容器都是以 Servlet 的规范进行设计的,因此 Servlet 过滤器同样是跨容器的。

  4. 可重用的
    基于其可移植性和声明式的配置方式,Filter 是可重用的。

总的来说,Servlet 的过滤器是通过一个配置文件来灵活的声明的模块化可重用组件。过滤器动态的截获传入的请求和传出的响应,在不修改程序代码的情况下,透明的添加或删除他们。其独立于任何平台和 Web 容器。

2、Filter API

1. Filter核心方法

Filter 是一个接口。如果想实现过滤器的功能,则必须实现该接口。

核心方法:

返回值方法名作用
voidinit(FilterConfig config)初始化方法
voiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain)对请求资源和响应资源进行拦截
voiddestroy()销毁方法

配置方式:

  • 方式一:使用注解 @WebFilter("拦截路径")

  • 方式二:web.xml 配置

2. FilterChain

  • FilterChain 是一个接口,代表过滤器链对象,由 Servlet 容器提供实现类对象,我们直接使用即可。

  • 过滤器可以定义多个,就会组成过滤器链。

核心方法:

返回值方法名作用
voiddoFilter(ServletRequest request, ServletResponse response)放行方法
  • 如果有多个过滤器,则会在第一个过滤器中再调用下一个过滤器,依次类推,直到到达最终访问资源。
  • 如果只有一个过滤器,放行时,就会直接到达最终访问资源。

3. FilterConfig

FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数。

核心方法:

返回值方法名作用
StringgetFilterName()获取过滤器对象名称
StringgetInitParameter(String key)根据 key 获取 value
Enumeration<String>getInitParameterNames()获取所有参数的 key
ServletContextgetServletContext()获取应用上下文对象

3、Filter 工作原理

1. Filter 的体系结构

如其名字所暗示的一样,Servlet 过滤器用于拦截传入的请求和传出的响应,并监视、修改处理 Web 工程中的数据流。过滤器是一个可插入的自由组件。Web 资源可以不配置过滤器、也可以配置单个过滤器,也可以配置多个过滤器,形成一个过滤器链。Filter 接受用户的请求,并决定将请求转发给链中的下一个组件,或者终止请求直接向客户端返回一个响应。如果请求被转发了,它将被传递给链中的下一个过滤器(以 web.xml 过滤器的配置顺序为标准)。这个请求在通过过滤链并被服务器处理之后,一个响应将以相反的顺序通过该链发送回去。这样,请求和响应都得到了处理。 

Filter 可以应用在客户端和 Servlet 之间、Servlet 和 Servlet 或 JSP 之间,并且可以通过配置言息,灵活的使用那个过滤器。

基于上述体系结构的描述,Filter 工作原理如下图所示:

  1. 客户端浏览器在访问 Web 服务器的某个具体资源的时候,经过过滤器 1 中 code l 代码块的相关处理之后,将请求传递给过滤链中的下一个过滤器 2(过滤链的顺序以配置文件中的顺序为基准)
  2. 过滤器 2 处理完之后,请求就根据传递的 Servlet 完成相应的逻辑。
  3. 返回响应的过程类似,只是过滤链的顺序相反。

4、Filter 生命周期

生命周期:

  1. 出生:当应用加载时,执行初始化方法。
  2. 活着:只要应用一直提供服务,对象就一直存在。
  3. 死亡:当应用卸载时(执行销毁方法)或服务器宕机时,对象消亡。

Filter 的实例对象在内存中也只有一份,所以 Filter 也是单例的。

import javax.servlet.*;
import java.io.IOException;

/*
    过滤器生命周期
 */
//@WebFilter("/*")
public class FilterDemo03 implements Filter{

    /*
        初始化方法
     */
    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("对象初始化成功了...");
    }

    /*
        提供服务方法
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("filterDemo03执行了...");

        //处理乱码
        servletResponse.setContentType("text/html;charset=UTF-8");

        //放行
        filterChain.doFilter(servletRequest,servletResponse);
    }

    /*
        对象销毁
     */
    @Override
    public void destroy() {
        System.out.println("对象销毁了...");
    }
}

5、Filter 使用案例

Filter 的创建过程:

要编写一个过滤器必须实现 Filter 接口,实现其接口规定的方法。

  1. 实现 javax.Servlet.Filter 接口;
  2. 实现 init() 方法,读取过滤器的初始化参数;
  3. 实现 doFilter()方法,完成对请求或响应的过滤;
  4. 调用 FilterChain 接口对象的 doFilter() 方法,向后续的过滤器传递请求或响应。

编写接收和处理请求的 Servlet:

public class ServletDemo1 extends HttpServlet {

    /**
     * 处理请求的方法
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("ServletDemo1接收到了请求");
        req.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       doGet(req,resp);
    }
}

配置 Servlet:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1"
         metadata-complete="true">
    
    <!--配置Servlet-->
    <servlet>
        <servlet-name>ServletDemo1</servlet-name>
        <servlet-class>com.itheima.web.servlet.ServletDemo1</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletDemo1</servlet-name>
        <url-pattern>/ServletDemo1</url-pattern>
    </servlet-mapping>
</web-app>

编写 index.jsp:

<%-- Created by IntelliJ IDEA. --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>主页面</title>
  </head>
  <body>
    <a href="${pageContext.request.contextPath}/ServletDemo1">访问ServletDemo1</a>
  </body>
</html>

编写 success.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>成功页面</title>
</head>
<body>
<% System.out.println("success.jsp执行了"); %>
执行成功!
</body>
</html>

编写 Filter:

public class FilterDemo1 implements Filter {

    /**
     * 过滤器的核心方法
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /**
         * 如果不写此段代码,控制台会输出两次:FilterDemo1拦截到了请求。
         */
        HttpServletRequest req = (HttpServletRequest) request;
        String requestURI = req.getRequestURI();
        if (requestURI.contains("favicon.ico")) {
            return;
        }

        System.out.println("FilterDemo1拦截到了请求");
    }
}

配置 Filter:

<!--配置过滤器-->
<filter>
    <filter-name>FilterDemo1</filter-name>
    <filter-class>com.itheima.web.filter.FilterDemo1</filter-class>
</filter>
<filter-mapping>
    <filter-name>FilterDemo1</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

运行结果分析:

当我们启动服务并在地址栏输入访问地址后,发现浏览器任何内容都没有,控制台却输出了【FilterDemo1拦截到了请求】,也就是说在访问任何资源的时候,都先经过了过滤器。

这是因为,我们在配置过滤器的拦截规则时,使用了/*,表示访问当前应用下的任何资源,此过滤器都会起作用。

除了这种全部过滤的规则之外,它还支持特定类型的过滤配置。我们可以稍作调整,修改的方式如下:

新的问题是,我们拦截下来了,但点击链接发送请求,运行结果是: 

对此,需要对过滤器执行放行操作,才能让它继续执行,那么如何放行的?

我们需要使用FilterChain中的doFilter方法放行:

继续修改:在FilterDemo1的doFilter方法后添加一行代码。

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /**
         * 如果不写此段代码,控制台会输出两次:FilterDemo1拦截到了请求。

        HttpServletRequest req = (HttpServletRequest) request;
        String requestURI = req.getRequestURI();
        if (requestURI.contains("favicon.ico")) {
            return;
        }*/
        System.out.println("FilterDemo1拦截到了请求");
        // 过滤器放行
        chain.doFilter(request,response);
        // 新增一行代码
        System.out.println("FilterDemo1放行之后,又回到了doFilter方法");
    }

 运行结果如下,我们发现过滤器放行之后执行完目标资源,最后仍会回到过滤器中。

6、FilterConfig

1. 过滤器配置对象

新增过滤器 FilterDemo2 :

public class FilterDemo2 implements Filter {

    private FilterConfig filterConfig;

    /**
     * 初始化方法
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("FilterDemo2的初始化方法执行了");
        // 给过滤器配置对象赋值
        this.filterConfig = filterConfig;
    }

    /**
     * 过滤器的核心方法
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        System.out.println("FilterDemo2拦截到了请求");

        // 根据名称获取过滤器的初始化参数
        String paramValue = filterConfig.getInitParameter("filterInitParamName");
        System.out.println(paramValue);

        // 获取过滤器初始化参数名称的枚举
        Enumeration<String> initNames = filterConfig.getInitParameterNames();
        while(initNames.hasMoreElements()){
            String initName = initNames.nextElement();
            String initValue = filterConfig.getInitParameter(initName);
            System.out.println(initName+","+initValue);
        }

        // 获取ServletContext对象
        ServletContext servletContext = filterConfig.getServletContext();
        System.out.println(servletContext);

        // 获取过滤器名称
        String filterName = filterConfig.getFilterName();
        System.out.println(filterName);

        // 过滤器放行
        chain.doFilter(request, response);
    }
    
    /**
     * 销毁方法
     */
    @Override
    public void destroy() {
        System.out.println("FilterDemo2的销毁方法执行了");
    }
}

配置 FilterDemo2 :

<filter>
    <filter-name>FilterDemo2</filter-name>
    <filter-class>com.itheima.web.filter.FilterDemo2</filter-class>
    <!--配置过滤器的初始化参数-->
    <init-param>
        <param-name>filterInitParamName</param-name>
        <param-value>filterInitParamValue</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>FilterDemo2</filter-name>
    <url-pattern>/ServletDemo1</url-pattern>
</filter-mapping>

运行效果:

7、Filter 五种拦截行为

    <filter>
        <filter-name>filterDemo05</filter-name>
        <filter-class>com.itheima.filter.FilterDemo05</filter-class>
        <!-- 配置开启异步支持,当dispatcher配置ASYNC时,需要配置此行 -->
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>filterDemo05</filter-name>
        <!-- <url-pattern>/error.jsp</url-pattern> -->
        <url-pattern>/index.jsp</url-pattern>
        <!-- 过滤请求(默认值)-->
        <dispatcher>REQUEST</dispatcher>
        <!-- 过滤全局错误页面:当由服务器调用全局错误页面时,过滤器工作 -->
        <dispatcher>ERROR</dispatcher>
        <!-- 过滤请求转发:当请求转发时,过滤器工作 -->
        <dispatcher>FORWARD</dispatcher>
        <!-- 过滤请求包含:当请求包含时,过滤器工作。它只能过滤动态包含,jsp的include指令是静态包含,过滤器不会起作用 -->
        <dispatcher>INCLUDE</dispatcher>
        <!-- 过滤异步类型,它要求我们在filter标签中配置开启异步支持 -->
        <dispatcher>ASYNC</dispatcher>
    </filter-mapping>

    <!-- 配置全局错误页面 -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/error.jsp</location>
    </error-page>
    <error-page>
        <error-code>404</error-code>
        <location>/error.jsp</location>
    </error-page>
</web-app>

六、Listener 监听器

1、Listener 简介

1. 观察者设计模式

在介绍 Listener(监听器)之前,需要先了解观察者设计模式,因为所有的监听器都是观察者设计模式的体现。

那么什么是观察者设计模式呢?

它是事件驱动的一种体现形式。就好比在做什么事情的时候被人盯着,当做了某件事时,就会触发事件。

观察者模式通常由以下三部分组成:

  1. 事件源:触发事件的对象。

  2. 事件:触发的动作,里面封装了事件源。

  3. 监听器:当事件源触发事件时,要做的事情。一般是一个接口,由使用者来实现。(此处还涉及一种设计模式的思想:策略模式)

下图描述了观察者设计模式组成:

2. Listener 介绍

在程序当中我们可以对以下情况进行监听:对象的创建销毁、域对象中属性的变化、会话相关内容。

Servlet 规范中共计 8 个监听器,监听器都是以接口形式提供的,具体功能需要我们自己来完成。

2、Listener 配置方式

Listender 有两种配置方法:

  1. 注解方式 @WebListener

  2. web.xml 配置方式

    <!-- 配置监听器 -->
    <listener>
        <listener-class>com.listener.ServletContextListenerDemo</listener-class>
    </listener>

    <listener>
        <listener-class>com.listener.ServletContextAttributeListenerDemo</listener-class>
    </listener>

3、Servlet 规范中的 8 个监听器

1. 8个监听器

  • 监听对象的

    1. ServletContextListener
    2. HttpSessionListener
    3. ServletRequestListener
  • 监听域中属性变化的

    1. ServletContextAttributeListener
    2. HttpSessionAttributeListener
    3. ServletRequestAttributeListener
  • 会话相关的感知型

    1. HttpSessionBindingListener
    2. HttpSessionActivationListener

2. 监听对象的监听器

1)ServletContextListener

用于监听 ServletContext 对象的创建和销毁。

核心方法:

返回值方法名作用
voidcontextlnitialized(ServletContextEvent sce)对象创建时执行该方法
voidcontextDestroyed(ServletContextEvent sce)对象销毁时执行该方法

ServletContextEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 ServletContext
  • 直正的事件指的是创建或销毁 ServletContext 对象的操作

2)HttpSessionListener

用于监听 HttpSession 对象的创建和销毁核心方法。

核心方法:

返回值方法名作用
voidsessionCreated(HttpSessionEventse)对象创建时执行该方法
voidsessionDestroyed(HttpSessionEvent se对象销毁时执行该方法

HttpSessionEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 HttpSession
  • 真正的事件指的是创建或销毁 HttpSession 对象的操作

3)ServletRequestListener

用于监听 ServletRequest 对象的创建和销毁核心方法。

核心方法:

返回值方法名作用
voidrequestinitialized(ServletRequestEvent sre)对象创建时执行该方法
voidrequestDestroyed(ServletRequestEvent sre)对象销毁时执行该方法

ServletRequest5vent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 ServletRequest
  • 真正的事件指的是创建或销毁 ServletRequest 对象的操作

3. 监听域中属性变化的监听器

1)ServletContextAttributeListener

用于监听 ServletContext 应用域中属性的变化核心方法。

核心方法:

返回值方法名作用
voidattributeAdded(ServletContextAttributeEvent scae)域中添加属性时执行该方法
voidattributeRemoved(ServletContextAttributeEvent scae)域中移除属性时执行该方法
voidattributeReplaced(ServletContextAttributeEvent scae)域中替换属性时执行该方法

ServletContextAttributeEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 ServletContext
  • 直正的事件指的是添加、移除、替换应用域中属性的操作

2)HttpSessionAttributeListener

用于监听 HttpSession 会话域中属性的变化。

核心方法:

返回值方法名作用
voidattributeAdded(HttpSessionBindingEvent se)域中添加属性时执行该方法
voidattributeRemoved(HttpSessionBindingEvent se)域中移除属性时执行该方法
voidattributeReplaced(HttpSessionBindingEvent se)域中替换属性时执行该方法

HttpSessionBindingEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 HttpSession
  • 真正的事件指的是添加、移除、替换会话域中属性的操作

3)ServletRequestAttributeListener

用于监听 ServletRequest 请求域中属性的变化。

核心方法:

返回值方法名作用
voidattributeAdded(ServletRequestAttributeEvent srae)域中添加属性时执行该方法
voidattributeRemoved(ServletRequestAttributeEvent srae)域中移除属性时执行该方法
voidattributeReplaced(ServletRequestAttributeEvent srae)域中替换属性时执行该方法

ServletRequestAttributeEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 ServletRequest
  • 真正的事件指的是添加、移除、替换请求域中属性的操作

4. 监听会话相关的感知型监听器

注意:监听会话相关的感知型监听器,只要定义了即可使用,无需进行配置。

1)HttpSessionBindingListener

用于感知对象和会话域绑定的监听器。

核心方法:

返回值方法名作用
voidvalueBound(HttpSessionBindingEvent event)数据添加到会话域中(绑定)时执行该方法
voidvalueUnbound(HttpSessionBindingEvent event)数据从会话域中移除(解绑)时执行该方法

HttpSessionBindingEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 HttpSession
  • 直正的事件指的是添加、移除会话域中数据的操作

2)HttpSessionActivationListener

用于感知会话域中对象钝化(序列化)和活化(反序列化)的监听器。

核心方法:

返回值方法名作用
voidsessionWillPassivate(HttpSessionEvent se)会话域中数据钝化时执行该方法
voidsessionDidActivate(HttpSessionEvent se)会话域中数据活化时执行该方法

HttpSessionEvent 参数:代表事件对象

  • 事件对象中封装了事件源,也就是 HttpSession
  • 直正的事件指的是会话域中数据钝化、活化的操作

4、Listener 使用示例

1. ServletContextListener 使用示例

1)编写监听器

/**
 * 用于监听ServletContext对象创建和销毁的监听器
 */
@WebListener
public class ServletContextListenerDemo implements ServletContextListener {

    /**
     * 对象创建时,执行此方法
     * @param sce
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("监听到了对象的创建");
        // 获取事件源对象
        ServletContext servletContext = sce.getServletContext();
        System.out.println(servletContext);
    }

    /**
     * 对象销毁时,执行此方法
     * @param sce
     */
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("监听到了对象的销毁");
    }
}

2)启动并停止 web 服务

2. ServletContextAttributeListener 使用示例

1)编写监听器

/**
 * 监听域中属性发生变化的监听器
 */
public class ServletContextAttributeListenerDemo implements ServletContextAttributeListener {

    /**
     * 域中添加了数据
     * @param scae
     */
    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        System.out.println("监听到域中加入了属性");
        /**
         * 由于除了我们往域中添加了数据外,应用在加载时还会自动往域中添加一些属性。
         * 我们可以获取域中所有名称的枚举,从而看到域中都有哪些属性
         */
        
        //1.获取事件源对象ServletContext
        ServletContext servletContext = scae.getServletContext();
        //2.获取域中所有名称的枚举
        Enumeration<String> names = servletContext.getAttributeNames();
        //3.遍历名称的枚举
        while(names.hasMoreElements()){
            //4.获取每个名称
            String name = names.nextElement();
            //5.获取值
            Object value = servletContext.getAttribute(name);
            //6.输出名称和值
            System.out.println("name is "+name+" and value is "+value);
        }
    }

    /**
     * 域中移除了数据
     * @param scae
     */
    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        System.out.println("监听到域中移除了属性");
    }

    /**
     * 域中属性发生了替换
     * @param scae
     */
    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        System.out.println("监听到域中属性发生了替换");
    }
}

同时,我们还需要借助上个示例的 ServletContextListenerDemo 监听器,往域中存入数据、替换域中的数据以及从域中移除数据,代码如下:

/**
 * 用于监听ServletContext对象创建和销毁的监听器
 */
public class ServletContextListenerDemo implements ServletContextListener {

    /**
     * 对象创建时,执行此方法
     * @param sce
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("监听到了对象的创建");
        //1.获取事件源对象
        ServletContext servletContext = sce.getServletContext();
        //2.往域中加入属性
        servletContext.setAttribute("servletContext","test");
    }

    /**
     * 对象销毁时,执行此方法
     * @param sce
     */
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        //1.取出事件源对象
        ServletContext servletContext = sce.getServletContext();
        //2.往域中加入属性,但是名称仍采用servletContext,此时就是替换
        servletContext.setAttribute("servletContext","demo");
        System.out.println("监听到了对象的销毁");
        //3.移除属性
        servletContext.removeAttribute("servletContext");
    }
}

2)在 web.xml 中配置监听器

<!--配置监听器-->
<listener>
    <listener-class>com.listener.ServletContextListenerDemo</listener-class>
</listener>

<!--配置监听器-->
<listener>
    <listener-class>com.listener.ServletContextAttributeListenerDemo</listener-class>
</listener>

3)启动 web 服务

5、Listener 综合案例

对前面的JSP综合案例进行优化。

优化需求:

  1. 解决乱码:使用过滤器统一实现请求和响应乱码问题的解决。
  2. 检查登录:使用过滤器统一实现身份认证。
  3. 优化 JSP 页面:使用 EL 表达式和 JSTL 。

1)乱码问题过滤器

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 解决全局乱码问题
@WebFilter("/*")
public class EncodingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 将请求和响应对象转换为和HTTP协议相关
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // 设置编码格式
        httpServletRequest.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("text/html;charset=UTF-8");

        // 放行
        chain.doFilter(httpServletRequest, httpServletResponse);

    }
}

2)检查登录过滤器

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 检查登录态
@WebFilter(value={"/add.jsp", "/queryServlet"})
public class LoginFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 将请求和响应对象转换为和HTTP协议相关
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // 判断会话域对象中的身份数据
        Object username = httpServletRequest.getSession().getAttribute("username");
        if ("".equals(username) || username == null) {
            // 重定向到登录页
            httpServletResponse.sendRedirect(httpServletRequest.getContextPath()+"/login.jsp");
            return;
        }

        // 放行
        chain.doFilter(httpServletRequest, httpServletResponse);
    }
}

3)优化 JSP:使用 EL 表达式和 JSTL

修改 add.jsp 的虚拟访问路径:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>添加</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/addServlet" method="post" autocomplete="off">
        学生姓名:<input type="text" name="username"><br/>
        学生年龄:<input type="number" name="age"><br/>
        学生成绩:<input type="number" name="score"><br/>
        <button type="submit">保存</button>
    </form>
</body>
</html>

修改 index.jsp:

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

<html>
<head>
    <title>学生管理系统首页</title>
</head>
<body>
    <%--
        获取会话域的数据
        如果获取到了,则显示添加和查询功能
        如果获取不到,则显示登录功能
    --%>
    <c:if test="${sessionScope.username eq null}">
        <a href="${pageContext.request.contextPath}/login.jsp">登录<a/>
    </c:if>

    <c:if test="${sessionScope.username ne null}">
        <a href="${pageContext.request.contextPath}/add.jsp">添加<a/>
        <a href="${pageContext.request.contextPath}/queryServlet">查询<a/>
    </c:if>

</body>
</html>

修改 query.jsp :

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

<html>
<head>
    <title>学生列表页面</title>
</head>
<body>
  <table width="600px" border="1px">
    <tr>
      <th>学生姓名</th>
      <th>学生年龄</th>
      <th>学生成绩</th>
    </tr>
      <c:forEach items="${students}" var="student">
          <tr align="center">
              <td>${student.username}</td>
              <td>${student.age}</td>
              <td>${student.score}</td>
          <tr/>
      </c:forEach>

  </table>
</body>
</html>

修改 login.jsp :

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录页面</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/loginServlet" method="get" autocomplete="off">
        姓名:<input type="text" name="username"><br/>
        密码:<input type="password" name="password"><br/>
        <button type="submit">登录</button>
    </form>
</body>
</html>

七、MVC开发部署

1、MVC开发

通过结合Servlet和JSP的MVC模式,我们可以发挥二者各自的优点:

  • Servlet实现业务逻辑;
  • JSP实现展示逻辑。

假设我们已经编写了几个JavaBean:

public class User {
    public long id;
    public String name;
    public School school;
}

public class School {
    public String name;
    public String address;
}

在UserServlet中,我们可以从数据库读取User、School等信息,然后,把读取到的JavaBean先放到HttpServletRequest中,再通过forward()传给user.jsp处理:

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 假装从数据库读取:
        School school = new School("No.1 Middle School", "101 South Street");
        User user = new User(123, "Bob", school);
        // 放入Request中:
        req.setAttribute("user", user);
        // forward给user.jsp:
        req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
    }
}

在user.jsp中,我们只负责展示相关JavaBean的信息,不需要编写访问数据库等复杂逻辑:

<%@ page import="com.itranswarp.learnjava.bean.*"%>
<%
    User user = (User) request.getAttribute("user");
%>
<html>
<head>
    <title>Hello World - JSP</title>
</head>
<body>
    <h1>Hello <%= user.name %>!</h1>
    <p>School Name:
    <span style="color:red">
        <%= user.school.name %>
    </span>
    </p>
    <p>School Address:
    <span style="color:red">
        <%= user.school.address %>
    </span>
    </p>
</body>
</html>

请注意几点:

  • 需要展示的User被放入HttpServletRequest中以便传递给JSP,因为一个请求对应一个HttpServletRequest,我们也无需清理它,处理完该请求后HttpServletRequest实例将被丢弃;
  • user.jsp放到/WEB-INF/目录下,是因为WEB-INF是一个特殊目录,Web Server会阻止浏览器对WEB-INF目录下任何资源的访问,这样就防止用户通过/user.jsp路径直接访问到JSP页面;
  • JSP页面首先从request变量获取User实例,然后在页面中直接输出,此处未考虑HTML的转义问题,有潜在安全风险。

我们在浏览器访问http://localhost:8080/user,请求首先由UserServlet处理,然后交给user.jsp渲染:

我们把UserServlet看作业务逻辑处理,把User看作模型,把user.jsp看作渲染,这种设计模式通常被称为MVC:Model-View-Controller,即UserServlet作为控制器(Controller),User作为模型(Model),user.jsp作为视图(View),整个MVC架构如下: 

使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。

MVC模式广泛地应用在Web页面和传统的桌面程序中,我们在这里通过Servlet和JSP实现了一个简单的MVC模型,但它还不够简洁和灵活,后续我们会介绍更简单的Spring MVC开发。

直接把MVC搭在Servlet和JSP之上还是不太好,原因如下:

  • Servlet提供的接口仍然偏底层,需要实现Servlet调用相关接口;
  • JSP对页面开发不友好,更好的替代品是模板引擎;
  • 业务逻辑最好由纯粹的Java类实现,而不是强迫继承自Servlet。

能不能通过普通的Java类实现MVC的Controller?类似下面的代码:

public class UserController {
    @GetMapping("/signin")
    public ModelAndView signin() {
        ...
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(SignInBean bean) {
        ...
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        ...
    }
}

上面的这个Java类每个方法都对应一个GET或POST请求,方法返回值是ModelAndView,它包含一个View的路径以及一个Model,这样,再由MVC框架处理后返回给浏览器。

如果是GET请求,我们希望MVC框架能直接把URL参数按方法参数对应起来然后传入:

@GetMapping("/hello")
public ModelAndView hello(String name) {
    ...
}

如果是POST请求,我们希望MVC框架能直接把Post参数变成一个JavaBean后通过方法参数传入:

@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
    ...
}

为了增加灵活性,如果Controller的方法在处理请求时需要访问HttpServletRequest、HttpServletResponse、HttpSession这些实例时,只要方法参数有定义,就可以自动传入:

@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
    ...
}

以上就是我们在设计MVC框架时,上层代码所需要的一切信息。

2、MVC框架

如何设计一个MVC框架?

在前面,我们已经定义了上层代码编写Controller的一切接口信息,并且并不要求实现特定接口,只需返回ModelAndView对象,该对象包含一个View和一个Model。实际上View就是模板的路径,而Model可以用一个Map<String, Object>表示,因此,ModelAndView定义非常简单:

public class ModelAndView {
    Map<String, Object> model;
    String view;
}

比较复杂的是我们需要在MVC框架中创建一个接收所有请求的Servlet,通常我们把它命名为DispatcherServlet,它总是映射到/,然后,根据不同的Controller的方法定义的@Get或@Post的Path决定调用哪个方法,最后,获得方法返回的ModelAndView后,渲染模板,写入HttpServletResponse,即完成了整个MVC的处理。

这个MVC的架构如下:

其中,DispatcherServlet以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller。

我们来看看如何编写最复杂的DispatcherServlet。首先,我们需要存储请求路径到某个具体方法的映射:

@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
    private Map<String, GetDispatcher> getMappings = new HashMap<>();
    private Map<String, PostDispatcher> postMappings = new HashMap<>();
}

处理一个GET请求是通过GetDispatcher对象完成的,它需要如下信息:

class GetDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    String[] parameterNames; // 方法参数名称
    Class<?>[] parameterClasses; // 方法参数类型
}

有了以上信息,就可以定义invoke()来处理真正的请求:

class GetDispatcher {
    ...
    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            String parameterName = parameterNames[i];
            Class<?> parameterClass = parameterClasses[i];
            if (parameterClass == HttpServletRequest.class) {
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) {
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) {
                arguments[i] = request.getSession();
            } else if (parameterClass == int.class) {
                arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == long.class) {
                arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == boolean.class) {
                arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
            } else if (parameterClass == String.class) {
                arguments[i] = getOrDefault(request, parameterName, "");
            } else {
                throw new RuntimeException("Missing handler for type: " + parameterClass);
            }
        }
        return (ModelAndView) this.method.invoke(this.instance, arguments);
    }

    private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
        String s = request.getParameter(name);
        return s == null ? defaultValue : s;
    }
}

上述代码比较繁琐,但逻辑非常简单,即通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。

类似的,PostDispatcher需要如下信息:

class PostDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    Class<?>[] parameterClasses; // 方法参数类型
    ObjectMapper objectMapper; // JSON映射
}

和GET请求不同,POST请求严格地来说不能有URL参数,所有数据都应当从Post Body中读取。这里我们为了简化处理,只支持JSON格式的POST请求,这样,把Post数据转化为JavaBean就非常容易。

class PostDispatcher {
    ...
    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            Class<?> parameterClass = parameterClasses[i];
            if (parameterClass == HttpServletRequest.class) {
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) {
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) {
                arguments[i] = request.getSession();
            } else {
                // 读取JSON并解析为JavaBean:
                BufferedReader reader = request.getReader();
                arguments[i] = this.objectMapper.readValue(reader, parameterClass);
            }
        }
        return (ModelAndView) this.method.invoke(instance, arguments);
    }
}

最后,我们来实现整个DispatcherServlet的处理流程,以doGet()为例:

public class DispatcherServlet extends HttpServlet {
    ...
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        String path = req.getRequestURI().substring(req.getContextPath().length());
        // 根据路径查找GetDispatcher:
        GetDispatcher dispatcher = this.getMappings.get(path);
        if (dispatcher == null) {
            // 未找到返回404:
            resp.sendError(404);
            return;
        }
        // 调用Controller方法获得返回值:
        ModelAndView mv = dispatcher.invoke(req, resp);
        // 允许返回null:
        if (mv == null) {
            return;
        }
        // 允许返回`redirect:`开头的view表示重定向:
        if (mv.view.startsWith("redirect:")) {
            resp.sendRedirect(mv.view.substring(9));
            return;
        }
        // 将模板引擎渲染的内容写入响应:
        PrintWriter pw = resp.getWriter();
        this.viewEngine.render(mv, pw);
        pw.flush();
    }
}

这里有几个小改进:

  • 允许Controller方法返回null,表示内部已自行处理完毕;
  • 允许Controller方法返回以redirect:开头的view名称,表示一个重定向。

这样使得上层代码编写更灵活。例如,一个显示用户资料的请求可以这样写:

@GetMapping("/user/profile")
public ModelAndView profile(HttpServletResponse response, HttpSession session) {
    User user = (User) session.getAttribute("user");
    if (user == null) {
        // 未登录,跳转到登录页:
        return new ModelAndView("redirect:/signin");
    }
    if (!user.isManager()) {
        // 权限不够,返回403:
        response.sendError(403);
        return null;
    }
    return new ModelAndView("/profile.html", Map.of("user", user));
}

最后一步是在DispatcherServlet的init()方法中初始化所有Get和Post的映射,以及用于渲染的模板引擎:

public class DispatcherServlet extends HttpServlet {
    private Map<String, GetDispatcher> getMappings = new HashMap<>();
    private Map<String, PostDispatcher> postMappings = new HashMap<>();
    private ViewEngine viewEngine;

    @Override
    public void init() throws ServletException {
        this.getMappings = scanGetInControllers();
        this.postMappings = scanPostInControllers();
        this.viewEngine = new ViewEngine(getServletContext());
    }
    ...
}

如何扫描所有Controller以获取所有标记有@GetMapping和@PostMapping的方法?当然是使用反射了。虽然代码比较繁琐,但我们相信各位童鞋可以轻松实现。

这样,整个MVC框架就搭建完毕。

3、实现渲染

有的童鞋对如何使用模板引擎进行渲染有疑问,即如何实现上述的ViewEngine?其实ViewEngine非常简单,只需要实现一个简单的render()方法:

public class ViewEngine {
    public void render(ModelAndView mv, Writer writer) throws IOException {
        String view = mv.view;
        Map<String, Object> model = mv.model;
        // 根据view找到模板文件:
        Template template = getTemplateByPath(view);
        // 渲染并写入Writer:
        template.write(writer, model);
    }
}

Java有很多开源的模板引擎,常用的有:

他们的用法都大同小异。这里我们推荐一个使用Jinja语法的模板引擎Pebble,它的特点是语法简单,支持模板继承,编写出来的模板类似:

<html>
<body>
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  {% endfor %}
  </ul>
</body>
</html>

即变量用{{ xxx }}表示,控制语句用{% xxx %}表示。

使用Pebble渲染只需要如下几行代码:

public class ViewEngine {
    private final PebbleEngine engine;

    public ViewEngine(ServletContext servletContext) {
        // 定义一个ServletLoader用于加载模板:
        ServletLoader loader = new ServletLoader(servletContext);
        // 模板编码:
        loader.setCharset("UTF-8");
        // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录:
        loader.setPrefix("/WEB-INF/templates");
        // 模板后缀:
        loader.setSuffix("");
        // 创建Pebble实例:
        this.engine = new PebbleEngine.Builder()
            .autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
            .cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
            .loader(loader).build();
    }

    public void render(ModelAndView mv, Writer writer) throws IOException {
        // 查找模板:
        PebbleTemplate template = this.engine.getTemplate(mv.view);
        // 渲染:
        template.evaluate(writer, mv.model);
    }
}

最后我们来看看整个工程的结构:

其中,framework包是MVC的框架,完全可以单独编译后作为一个Maven依赖引入,controller包才是我们需要编写的业务逻辑。

我们还硬性规定模板必须放在webapp/WEB-INF/templates目录下,静态文件必须放在webapp/static目录下,因此,为了便于开发,我们还顺带实现一个FileServlet来处理静态文件:

@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
public class FileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 读取当前请求路径:
        ServletContext ctx = req.getServletContext();
        // RequestURI包含ContextPath,需要去掉:
        String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
        // 获取真实文件路径:
        String filepath = ctx.getRealPath(urlPath);
        if (filepath == null) {
            // 无法获取到路径:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Path path = Paths.get(filepath);
        if (!path.toFile().isFile()) {
            // 文件不存在:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 根据文件名猜测Content-Type:
        String mime = Files.probeContentType(path);
        if (mime == null) {
            mime = "application/octet-stream";
        }
        resp.setContentType(mime);
        // 读取文件并写入Response:
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
            input.transferTo(output);
        }
        output.flush();
    }
}

行代码,在浏览器中输入URLhttp://localhost:8080/hello?name=Bob可以看到如下页面:

为了把方法参数的名称编译到class文件中,以便处理@GetMapping时使用,我们需要打开编译器的一个参数,在Eclipse中勾选Preferences-Java-Compiler-Store information about method parameters (usable via reflection);在Idea中选择Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters,填入-parameters;在Maven的pom.xml添加一段配置如下: 

<project ...>
    <modelVersion>4.0.0</modelVersion>
    ...
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

有些用过Spring MVC的童鞋会发现,本节实现的这个MVC框架,上层代码使用的公共类如GetMapping、PostMapping和ModelAndView都和Spring MVC非常类似。实际上,我们这个MVC框架主要参考就是Spring MVC,通过实现一个“简化版”MVC,可以掌握Java Web MVC开发的核心思想与原理,对将来直接使用Spring MVC是非常有帮助的。

一个MVC框架是基于Servlet基础抽象出更高级的接口,使得上层基于MVC框架的开发可以不涉及Servlet相关的HttpServletRequest等接口,处理多个请求更加灵活,并且可以使用任意模板引擎,不必使用JSP。

4、部署项目

对一个Web应用程序来说,除了Servlet、Filter这些逻辑组件,还需要JSP这样的视图文件,外加一堆静态资源文件,如CSS、JS等。

合理组织文件结构非常重要。我们以一个具体的Web应用程序为例:

我们把所有的静态资源文件放入/static/目录,在开发阶段,有些Web服务器会自动为我们加一个专门负责处理静态文件的Servlet,但如果IndexServlet映射路径为/,会屏蔽掉处理静态文件的Servlet映射。因此,我们需要自己编写一个处理静态文件的FileServlet: 

@WebServlet(urlPatterns = "/static/*")
public class FileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext ctx = req.getServletContext();
        // RequestURI包含ContextPath,需要去掉:
        String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
        // 获取真实文件路径:
        String filepath = ctx.getRealPath(urlPath);
        if (filepath == null) {
            // 无法获取到路径:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Path path = Paths.get(filepath);
        if (!path.toFile().isFile()) {
            // 文件不存在:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 根据文件名猜测Content-Type:
        String mime = Files.probeContentType(path);
        if (mime == null) {
            mime = "application/octet-stream";
        }
        resp.setContentType(mime);
        // 读取文件并写入Response:
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
            input.transferTo(output);
        }
        output.flush();
    }
}

这样一来,在开发阶段,我们就可以方便地高效开发。

类似Tomcat这样的Web服务器,运行的Web应用程序通常都是业务系统,因此,这类服务器也被称为应用服务器。应用服务器并不擅长处理静态文件,也不适合直接暴露给用户。通常,我们在生产环境部署时,总是使用类似Nginx这样的服务器充当反向代理和静态服务器,只有动态请求才会放行给应用服务器,所以,部署架构如下:

实现上述功能的Nginx配置文件如下: 

server {
    listen 80;

    server_name www.local.yyds.com;

    # 静态文件根目录:
    root /path/to/src/main/webapp;

    access_log /var/log/nginx/webapp_access_log;
    error_log  /var/log/nginx/webapp_error_log;

    # 处理静态文件请求:
    location /static {
    }

    # 处理静态文件请求:
    location /favicon.ico {
    }

    # 不允许请求/WEB-INF:
    location /WEB-INF {
        return 404;
    }

    # 其他请求转发给Tomcat:
    location / {
        proxy_pass       http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

使用Nginx配合Tomcat服务器,可以充分发挥Nginx作为网关的优势,既可以高效处理静态文件,也可以把https、防火墙、限速、反爬虫等功能放到Nginx中,使得我们自己的WebApp能专注于业务逻辑。

注意:部署Web应用程序时,要设计合理的目录结构,同时考虑开发模式需要便捷性,生产模式需要高性能。

  • 26
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wespten

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值