浅析Servlet

(一) Setvlet 基本概述

(1) 什么是 Servlet?

Servlet(Server Applet)是 JavaServlet 的简称,称为小服务程序或服务连接器,用 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。

JavaWeb中,我们将会接触到三大组件(ServletFilterListener

Servlet 由服务器调用,处理服务器接收到的请求,即完成,接受请求数据 --> 处理请求 --> 完成响应,其本质就是一个实现了 Servlet 接口的 java 类

Servlet 类由我们来写,但对象由服务器来创建,并且由服务器来调用相应的方法

(2) Servlet 用来做什么?

网络中比较常见的一些功能,例如:登录,注册,文件下载上传等这些存在交互的功能,而 Servlet 就可以帮助我们处理这些请求,可以说 Servlet 是 JavaWeb 知识中重要的知识点之一

(二) 实现 Servlet 的方式

实现 Servlet 有三种方式:

  • 实现 javax.servlet.Servlet 接口;
  • 继承 javax.servlet.GenericServlet 类;
  • 继承 javax.servlet.http.HttpServlet 类;

实际开发中,我们通常会选择继承 HttpServlet 类来完成我们的 Servlet,但认识 Servlet 接口这种方式也是很重要的,是我们入门知识中不可或缺的部分。

(1) 创建我们的第一个 Servlet

我们创建一个 web 项目,选择对应的参数,我们所装的 jdk 为 1.8 版本,可以选择到 JavaEE8 的版本,对应 versions 也就是 4.0,不过我们在这里选择市面上用的还是比较多的 7 版本

img

创建一个 Demo 类实现 Servlet 接口,然后我们快速生成这个接口中未实现的方法,我们先暂时忽略 Servlet 中其他四个方法,只关心 service() 方法,因为它是用来处理请求的方法,我们在该方法内给出一条输出语句:

package cn.ideal.web.servlet;

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

public class ServeltDemo1 implements Servlet {
    //初始化方法
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
    }

    //Servlet配置方法
    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    //提供服务方法
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("理想三旬~");
    }

    //Servlet信息方法
    @Override
    public String getServletInfo() {
        return null;
    }

    //销毁方法
    @Override
    public void destroy() {
    }
}
复制代码

写完了一个最简单 Servlet 代码,但是如何在浏览器中可以访问到呢?我们就需要对 web/WEB-INF 下的 web.xml 进行配置,我们在 <web-app></web-app> 中加入以下代码(虽然后期有优化的方法,但是很推荐大家记忆下来)

<servlet>
    <!--给这个Servlet起一个名字,一般与类名相同-->
    <servlet-name>ServletDemo1</servlet-name>
    <!--全类名-->
    <servlet-class>cn.ideal.web.servlet.ServeltDemo1</servlet-class>
</servlet>

<!--配置映射路径-->
<servlet-mapping>
    <servlet-name>ServletDemo1</servlet-name>
    <!--外界访问的路径-->
    <url-pattern>/Demo1</url-pattern>
</servlet-mapping>
复制代码

现在我们根据我们在 url-pattern 中配置的路径来访问一下,在控制台中果然输出了。

(2) web.xml 的作用

趁热打铁,我们来简单分析一下这个 web.xml 的因由,其实在 web.xml 中配置 Servlet 的目的,就是把在浏览器中的 访问路径与对应 Servlet 绑到一起 ,上面的例子中就是把访问路径:/Demo1cn.ideal.web.servlet.ServeltDemo1 绑定到了一起

1、<servlet></servlet> :指定 ServletDemo1 这个 Servlet 的名字为 ServletDemo1,一般此处与对应类同名

2、<servlet-mapping></servlet-mapping> :设定访问的具体路径

而这两者又通过 <servlet-name></servlet-name> 关联在一起

执行过程:

1、当服务器中接受到了浏览器的请求,解析 URL 路径,获取到 Servlet 的资源路径

2、寻找 web.xml 文件,找到 <url-pattern> 标签,寻找对应的全类名 <servlet-class>

3、Tomcat 将字节码文件加载进内存,并且创建对象,调用其中的方法

所以我们需要知道:Servlet 中的大多数方法均不由我们来创建和调用,均由 Tomcat 完成

(三) Servlet 接口(Ⅰ)

(1) 生命周期简单概述

我将生命周期简单理解为这样几个过程:

生前—>出生—>服务—>死亡—>埋葬

1、生前: 当 Tomcat 第一次访问 Servlet,Tomcat 会创建 Servlet 的实例

2、出生: Tomcat 会调用 init() 方法初始化这个对象

3、服务: 客户端访问 Servlet 的时候,service() 方法会被调用

4、死亡: 当 Tomcat 被关闭或者 Servlet 长时间不被使用,destroy() 方法会被调用

5、埋葬: destroy() 方法被调用后,Servlet 就等待垃圾回收(不轻易),有需要则用 init() 方法重新初始化

(2) 生命周期详解

1、生前

服务器会在 Servlet 第一次被访问时,或者是在服务器启动时创建 Servlet。如果服务器启动时就创建 Servlet,那么还需要在 web.xml 文件中进行配置,也就是说默认情况下,Servlet 是在第一次访问时由服务器创建的

一个 Servlet 类型,服务器只创建一个实例对象:例如我们第一次访问 <http://localhost:8080/Demo1>,服务器通过 /Demo1 就找到了 cn.ideal.web.servlet.ServeltDemo1 ,服务器就会判断这个类型的 Servlet 是否创建过,若没有才通过反射来创建 ServletDmoe1 的实例,否则就直接使用已经存在的实例

2、出生

在 Servlet 被创建后,服务器会立即调用 Servlet 的 void init(ServletConfig) 方法,而且一个 Servlet 的一生,这个方法只会被调用一次,我们可以把一些对 Servlet 的初始化工作放到 init() 方法中!

3、服务

当服务器每次接收到请求时,都会去调用 Servlet 的 service() 方法来处理请求。service() 方法是会被调用多次的,服务器接收到一次请求,就会调用 service() 方法一次,也正因为如此,所以我们才需要把处理请求的代码写到 service() 方法中!

4、死亡及埋葬

当服务器关闭时 Servlet 也需要被销毁了,但是销毁之前,服务器会先调用 Servlet 中的 destroy() 方法,我们可以把一些释放资源的代码放到此处。

(3) Servlet 接口的三个类型

在这五个方法中,我们可以在参数中看到三个我们没有接触过的类型

public void init(ServletConfig servletConfig) throws ServletException {}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {}
复制代码

也就是这个三个类型:ServletConfigServletRequestServletResponse

A:ServletConfig

ServletConfig 是服务器创建的一个对象,然后传递到 Servlet 的 init() 方法中

下述方法中我们简单使用一下第一个 getServletName() 就可以了,后面的方法等我们学写了 Context 以及其他知识才能更好的理解

//获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称
String getServletName()

//用来获取ServletContext对象
ServletContext getServletContext()

//用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值
String getInitParameter(String name)

//用来获取在web.xml中配置的所有初始化参数名称
Enumeration getInitParameterNames()
复制代码

B:ServletRequest & ServletResponse

这两个类型出现在 Servlet 的 service() 方法中,分别代表着请求响应对象,并且两者的实例也都是由服务器创建的

但是我们想要做一个 web 应用,归根结底要和 HTTP 相挂钩,如果我们希望在 service() 方法中使用 HTTP 相关的功能,可以把 ServletRequestServletResponse 强转成 HttpServletRequestHttpServletResponse

HttpServletRequest 方法:

//获取指定请求参数的值
String getParameter(String paramName)

//获取请求方法,例如 GET 或 POST
String getMethod()

//获取指定请求头的值
String getHeader(String name)

//设置请求体的编码!
/*
    GET 没有请求体,所以这个方法只对 POST 请求有效当调用
    这个方法必须在调用 getParameter() 方法之前调用!
    使用 request.setCharacterEncoding(“utf-8”) 之后,再通过 getParameter() 方法获取参数时,
    参数值都已经通过了转码,即转换成了 UTF-8 编码
*/
void setCharacterEncoding(String encoding)
复制代码

HttpServletResponse 方法:

//获取字符响应流,使用该流可以向客户端输出响应信息
PrintWriter getWriter()
Eg:response.getWriter().print(“<h1>Just for test</h1>”)//获取字节响应流,例如可实现向客户端响应一张图片
ServletOutputStream getOutputStream()

//用来设置字符响应流的编码
void setCharacterEncoding(String encoding)

//向客户端添加响应头信息
void setHeader(String name, String value)
Eg:setHeader(“Refresh”, “3;url=http://www.xxx.com”) 表示三秒后自动刷新到该网址

//该方法是 setHeader(“content-type”, “xxx”) 的简便方法,即用来添加名为 content-type 响应头的方法
/*
    content-type 响应头用来设置响应数据的 MIME 类型,例如要向客户端响应 jpg 的图片,那么
    可以 setContentType(“image/jepg”),如果响应数据为文本类型,那么还要台同时设置编
    码,例如 setContentType(“text/html;chartset=utf-8”) 表示响应数据类型为文本类型
    中的 html 类型,并且该方法会调用 setCharacterEncoding(“utf-8”) 方法;
*/
void setContentType(String contentType)

//向客户端发送状态码,以及错误消息
void sendError(int code, String errorMsg)
复制代码

(四) GenericServlet 类(Ⅱ)

A:通过查看这个类的源码可以知道,该类中只有

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
复制代码

一个方法需要实现,其他的方法已经均在源码中有了定义

B:GenericServlet 的 init() 方法

还需要提一提的两个方法就是:

public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
}

public void init() throws ServletException {
}
复制代码

GenericServlet 类实现了 Servlet 的 init(ServletConfig) 方法,把参数 config 赋给了本类的成员 config,然后再调用本类自己的无参的 init() 方法

这个方法是 GenericServlet 自己的方法,而不是从 Servlet 继承下来的。当我们自定义 Servlet 时,如果想完成初始化作用就不要再重复 init(ServletConfig) 方法了,而是应该去重写 init() 方法。因为在 GenericServlet中的 init(ServletConfig) 方法中保存了 ServletConfig 对象,如果覆盖了保存 ServletConfig 对象的代码,那么就不能再使用 ServletConfig 的对象了

C:实现了 ServletConfig 接口

GenericServlet 还实现了 ServletConfig 接口,所以可以直接调用 getInitParameter()getServletContext() 等 ServletConfig 的方法

但是这个类我们仍然不是我们要讲的重点,我们接着看下一个类

(五) HttpServlet 类(Ⅲ)

(1) 概述

在上面我们实现 Servlet 接口,需要实现 5 个方法,十分麻烦,而 HttpServlet 类已经实现了 Servlet 接口的所有方法,编写 Servlet 时,只需要继承 HttpServlet,重写你需要的方法即可,并且它提供了对 HTTP 请求的特殊支持,更加强大!

(2) service() 方法

在 HttpServlet 的 service(ServletRequest,ServletResponse) 方法中会把 ServletRequestServletResponse 强转成 HttpServletRequestHttpServletResponse

//HttpServlet 源码节选
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    try {
        request = (HttpServletRequest)req;
        response = (HttpServletResponse)res;
    } catch (ClassCastException var6) {
        throw new ServletException("non-HTTP request or response");
    }

    this.service(request, response);
}
复制代码

强转过后,然后调用 HttpServlet 类中提供的 service(HttpServletRequest,HttpServletResponse) 方法,这是这个类本身的方法,而不是继承而来的,这说明我们在使用的时候,只需要覆盖 service(HttpServletRequest,HttpServletResponse) 就可以了,不需要再进行强转这个两个对象了~

注意:其实还有更一步简化的步骤,也不必使用 service(HttpServletRequest,HttpServletResponse)

(3) doGet() 和 doPost()

在 HttpServlet的 service(HttpServletRequest,HttpServletResponse) 方法会去判断这个请求是 GET 还是 POST,如果是 GET 请求,就去调用类中的 doGet() 方法,如果是 POST 请求,就去调用 doPost() 方法,这说明我们在子类中去覆盖 doGet()doPost() 方法就可以了

(六) Servlet 细节

(1) 线程安全问题

Servlet 只会被服务器创建一个实例对象,很多情况下,一个 Servlet 需要处理多个请求,显然,Servlet 虽然效率高,但也不是线程安全的

所以我们不应该在 Servlet 中轻易创建成员变量,因为可能会存在多个线程同时对这个成员变量进行不同的操作

结论:不要在Servlet中创建成员!创建局部变量即可,可以创建无状态成员量,或者状态只为可读的成员

(2) 服务器启动时就创建 Servlet

之前我们将生命周期的时候有说过,Servlet 是在第一次访问时由服务器创建的,但我们可以通过在 web.xml 中对 Servlet 进行配置,使服务器启动时就创建 Servlet!

<servlet>
    <servlet-name>ServletDemo1</servlet-name>
    <servlet-class>cn.ideal.web.ServletDemo1</servlet-class>
    <!-- 在<servlet>中配置<load-on-startup>,其中给出一个非负整数!-->
    <load-on-startup>0</load-on-startup>
</servlet>
复制代码

它的作用是确定服务器启动时创建 Servlet 的顺序

(3) 一个 Servlet 可以绑定多个 URL

<servlet-mapping>
    <servlet-name>Servlet</servlet-name>
    <url-pattern>/AServlet</url-pattern>
    <url-pattern>/BServlet</url-pattern>
</servlet-mapping>
复制代码

这样配置后无论访问 /AServlet 还是 /BServlet,访问的都是 AServlet

(4) 通配符匹配问题

<url-pattern> 中可以使用通配符,也就是 “ * ” ,它可以匹配任何前缀或者后缀

<!--路径匹配-->
<!-- /servlet/a、/servlet/b,都匹配/servlet/* -->
<url-pattern>/servlet/*<url-patter>

<!--扩展名匹配-->
<!-- /abc/de.xx、/a.xx,都匹配*.xx -->
<url-pattern>*.xx</url-pattern><!-- 匹配所有的URL -->
<url-pattern>/*<url-pattern>
复制代码

通配符要么为前缀,要么为后缀,不能出现在 URL 中间位置,并且一个 URL 中最多只能出现一个通配符,如果存在更具体的地址,会优先访问具体的地址

(七) ServletContext

(1) 概述

服务器会为每个 web 应用创建一个 ServletContext 对象,可以说它就代表着这个 web 站点,并且这个对象,在 Tomcat 启动时就创建,在 Tomcat 关闭时才会销毁

(2) 功能

所有 Servlet 都共享着一个 ServletContext 对象,所以 ServletContext 对象的作用是在整个 Web 应用的动态资源之间共享数据,也就是说不同的 Servlet 之间可以通过 ServletContext 进行通讯,从而共享数据

(3) 获取 ServletContext 对象

GenericServlet 类有 getServletContext() 方法,所以可以直接使用 this.getServletContext() 来获取

public class MyServlet implements Servlet {
    public void init(ServletConfig config) {
        ServletContext context = config.getServletContext();
    }
}

public class MyServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    	ServletContext context = this.getServletContext();
    }
}
复制代码

(4) 域对象的功能

所有域对象都有存取数据的功能,可以将这种存储数据的方式看做 ==> Map 的方式

我们来看几个常见的用来操作数据的方法

存储

//用来存储一个对象,也可以称之为存储一个域属性
void setAttribute(String name, Object value)
    
Eg:servletContext.setAttribute(“xxx”, “XXX”)
//在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX
复制代码

获取

//用来获取 ServletContext 中的数据
Object getAttribute(String name)
//获取名为xx的域属性
Eg:String value = (String)servletContext.getAttribute(“xxx”);

//获取所有域属性的名称;
Enumeration getAttributeNames()
复制代码

移除

//用来移除 ServletContext 中的域属性
void removeAttribute(String name)
复制代码

访问量统计的小案例

package cn.ideal.web.servlet;

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

public class ServletDemo2 extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        //获取ServletContext对象
        ServletContext servletContext = this.getServletContext();
        //获取ServletContext对象中的count属性
        Integer count = (Integer) servletContext.getAttribute("count");
        if (count == null) {
            //如果在ServletContext中不存在count属性,name设置为count的值为1,表示第一次访问
            count = 1;
        } else {
            //如果在Servlet中存在count属性,说明以前被访问过,name让count在原来的基础上加1
            count++;
        }
        servletResponse.setContentType("text/html;charset=UTF-8");
        //向客户端响应本页面被访问的次数
        servletResponse.getWriter().print("<h1>本页面一共访问" + count + "次</h1>");
        //保存count的值到ServletContext对象中
        servletContext.setAttribute("count", count);
    }
}
复制代码

(八) 获取资源相关方法

(1) 获取路径

使用 ServletContext 对象可以用来获取 Web 应用下的资源,例如在一个 web 应用的根目录下创建 aaa.txt 文件,WEB-INF 目录下创建 bbb.txt 文件,如果我们想要通过 Servlet 获取这两者的路径就可以这样来写

//获取aaa.txt的路径
String realPath = servletContext.getRealPath(“/aaa.txt”)

//获取bbb.txt的路径
String realPath = servletContext.getRealPath(“/WEB-INF/b.txt”)
复制代码

获取单个文件路径是这样,我们还有一种方式,可以获取到指定目录下所有的资源路径,例如获取 /WEB-INF 下的所有资源路径

Set set = context.getResourcePaths("/WEB-INF");
System.out.println(set);
复制代码

(2) 获取资源流

不仅我们可以使用 ServletContext 获取路径,我们还可以获取资源流,以上面假设的两个文件为例

//获取aaa.txt
InputStream in = servletContext.getResourceAsStream(“/aaa.txt”);

//获取bbb.txt
InputStream in = servletContext.getResourceAsStream(“/WEB-INF/b.txt”);
复制代码

(3) 获取类路径下资源

InputStream in = this.getClass().getClassLoader().getResourceAsStream("xxx.txt");
System.out.println(IOUtils.toString(in));
复制代码

(九) 使用注解,不再配置 web.xml

每创建一个 Servlet 我们就需要在 web.xml 中配置,但是如果我们的 Servlet 版本在 3.0 以上,就可以选择不创建 web.xml,而使用注解来解决,十分简单方便

例如我们创建一个 Servlet,配置 web.xml 如下

<servlet>
    <servlet-name>ServletDemo2</servlet-name>
    <servlet-class>cn.ideal.web.servlet.ServletDemo2</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ServletDemo2</servlet-name>
    <url-pattern>/Demo2</url-pattern>
</servlet-mapping>

<!-- 在类名的上方写入这样一句代码,引号内为外部访问路径 -->
@WebServlet("/Demo2")
复制代码

是不是很简单方便,我们看一下其中的原理:

//WebServlet 源码节选
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    String name() default "";

    String[] value() default {};

    String[] urlPatterns() default {};

    int loadOnStartup() default -1;
}
复制代码

这个注解可以看到,@Target({ElementType.TYPE}) 作用范围为类上, @Retention(RetentionPolicy.RUNTIME) 保留在运行期,name() 方法反而在这里没有那么重要,因为在 web.xml 中,name 主要起一个关联的作用,其中我们最重要的就是这个 String[] urlPatterns() default {}; 配置一个地址,它的定义为一个数组,当然配置一个也是可以的,即 urlPatterns = "/Demo2" 而其中 value 所代表的最重要的值,其实也就代表这个地址,所以可以写为 Value = "/Demo2" ,而 Value又可以省略,所以可以写成 "/Demo2"

☠本文系转载!原文地址:juejin.cn/post/684490…