Servlet

概述

狭义地说,Servlet就是定义在JavaEE规范中的一个接口,javax.servlet.Servlet,参见JavaEE的API文档

package javax.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig config) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

Servlet接口定义的是一套处理网络请求的规范。一个实现了Servlet接口的类(下面简称Servlet类),表明它是一个可以处理网络请求的类。Servlet类需要实现Servlet接口中的5个方法,其中最主要的3个是:

  • init()
  • destroy()
  • service()

它们分别定义了,一个Servlet类,

  • 在初始化时需要干什么
  • 在销毁时需要干什么
  • 在每次接收到请求时,要作何处理

实现了Servlet接口的类,就能处理网络请求了吗?

显然没这么简单。

我们只知道,一个Servlet类在处理请求时,执行的是它的service()方法,但service()方法是由谁调用的,方法入参中的requestresponse对象又是怎么来的,这显然不是Servlet类自己做的,而且Servlet中也没有诸如监听网络端口等和网络请求有关的操作。

所以,Servlet类要处理网络请求,还需要依赖一个东西——Servlet容器。这个容器要负责监听网络端口,封装网络请求数据,管理众多的Servlet类,进行请求的调度分发,返回响应数据等。最典型的Servlet容器就是Tomcat。

Servlet容器和Servlet类是如何协作处理一个HTTP请求的?

1. Servlet容器监听网络端口,接收到HTTP请求
2. 根据请求的url, 查找对应的Servlet类
3. 若对应的Servlet类不在容器中,则检索并创建该Servlet实例,并初始化(调Servlet的init方法)
4. 容器构建该次请求的request和response对象, 调用对应Servlet的service方法,传入这2个参数
5. Servlet类执行service方法, 对请求进行处理,并设置响应数据到response对象中
6. 容器取出response对象中的响应数据,封装好HTTP响应报文,返回给客户端

我们可以看到,在HTTP请求的整个处理过程中,干活儿最多的,其实是Servlet容器。Servlet容器把处理请求的整个流程,框架,都搭好了,只是在中间留了很小的一部分空间,给Servlet类。所以,Servlet类负责的,其实只是整个流程中很小的一环。

不过,这也是因为,在处理网络请求时,诸如监听网络端口,封装请求数据,响应请求数据,这些操作,都是共性的。也就是说,所有的处理网络请求的程序,都必须有这些相同的步骤,而唯一不同的地方,只是在于对请求的业务处理。于是就把这些相同的地方抽取出来,做成了Servlet容器,不同的地方,交给各个Servlet类。这也体现了编程领域中,抽取共性,封装变化的设计理念。试想,若没有Servlet容器,则每开发一个网络程序,都要自己完整实现一套处理网络请求和响应的流程,那开发的门槛就太高了,开发量也很大,并且都是重复的工作。

广义地讲,Servlet是一套规范体系,不仅仅是javax.servlet.Servlet接口,而是javax.servlet包下定义的全部规范。主要包括了JavaWeb三大组件,Servlet,Filter,Listener,以及其他一系列相关的接口。

下面,对Servlet的规范体系进行非常简要的归纳总结。

基本使用

构建一个最基本的web程序,只需要有Servlet类(实现了javax.servlet.Servlet接口的类)即可

  1. 在IDEA中新建一个JavaWeb项目
  2. 导入servlet-api.jar依赖
  3. 创建一个实现javax.servlet.Servlet接口的类
  4. 对该Servlet类进行配置(主要配置该Servlet对应的url),有2种配置方法
    1. web.xml中配置
    2. 使用注解@WebServlet进行配置
  5. 配置并部署Tomcat

Servlet配置参数

  • urlPattern:该Servlet处理的url
  • initParams:name - value,键值对形式,可通过ServletConfig获取
  • loadOnStartup:值为一个整数。若是0或正数,容器启动时就立刻加载Servlet,数字越小越先加载。若是负数,容器可自由选择何时加载(一般会懒加载,即用到该Servlet时才加载)

Servlet生命周期

  • init():容器加载Servlet实例,进行初始化时,调用执行Servlet的init()
  • destroy():容器销毁Servlet实例时,调用执行Servlet的destroy()
  • service():容器将请求交由一个Servlet处理时,调用执行Servlet的service()

javax.servlet.Servlet只是最原始的接口规范,javax.servlet包下还定义了一些基本的抽象子类,如javax.servlet.GenericServletjavax.servlet.http.HttpServlet,类的层次结构图如下

由于构建的web应用几乎都是基于HTTP协议的,我们创建一个Servlet时,通常只需要继承HttpServlet,由于最常用的HTTP方法是GET和POST,通常我们只需要重写doGetdoPost方法即可。本文下面的内容,默认基于HTTP协议。

两个重要对象

在Servlet处理请求时,有两个最重要的对象——requestresponse。前者封装了HTTP请求数据,后者封装了HTTP响应数据。这两个对象分别是HttpServletRequestHttpServletResponse类的实例。对于request,主要的操作是get,因为需要从其中获取请求数据,对于response,主要的操作是set,因为需要往里面设置响应数据

它们中的常用方法下面列举一二

HttpServletRequest

  • getRequestURL:获取请求URL
  • getMethod:获取请求方法
  • getParameter(String s):获取请求参数
  • getHeader(String s):获取请求头
  • getInputStream() / getReader():获取请求体

HttpServletResponse

  • addHeader(String k, String v):添加响应头
  • getOutputStream() / getWriter():获取输出流,以便往响应体里写入数据
  • setStatus(int sc):设置HTTP响应状态码
  • sendRedirect(String s):设置重定向

请求转发

当一个请求,在一个Servlet中不能完成处理,需要进行请求转发时,有2种转发方式

  • 服务端转发

    通过request.getRequestDispatcher("/xx").forward(request,response)

    转发到能处理/xx这种url的Servlet。发生在服务端内部,本质相当于方法调用,只能转发到服务端内部的资源。

  • 客户端转发

    通过设置HTTP状态码为3xx(一般设置为302),并在HTTP响应头添加Location指定重定向的url地址,浏览器收到3xx的HTTP响应,会自动重定向到Location头部指定的url地址。实际发生了2次HTTP请求,可跳转到任意url。可以调用response对象的sendRedirect方法完成。

会话技术

众所周知,HTTP是无状态的协议,即每组HTTP请求/响应,都是互相独立的,HTTP协议本身不具备记忆能力。但有的场景需要在多次HTTP请求之间维护一些状态信息,此时,就轮到会话技术登场了。根据状态信息是保存在服务端,还是客户端,会话技术分为了2种:Cookie和Session

Cookie

cookie是HTTP协议的扩展标准。本质就是一个简单的k-v键值对。当浏览器收到的响应报文中,包含了Set-Cookie头部时,会将该头部中的k-v键值对,保存在浏览器端(默认保存在浏览器内存,在浏览器关闭后失效),下次再发起请求时,会自动添加Cookie头部,携带先前保存下来的k-v键值对。

在Servlet中,通过response对象来向浏览器发送一个cookie,具体操作如下

Cookie cookie = new Cookie("userId","123");
response.addCookie(cookie);

HTTP响应报文如下(简化)

HTTP/1.1 200 OK
Set-Cookie: userId=123

下次请求时的HTTP请求报文如下(简化)

GET / HTTP/1.1
Host: www.baidu.com
Cookie: userId=123

在Servlet中,通过request对象获取请求中携带的cookie,具体操作如下

Cookie[] cookies = request.getCookies(); // 获取请求携带的全部Cookie
for(Cookie c : cookies) {
    String name = c.getName();
    String value = c.getValue();
}

k-v键值对,是cookie最基本的信息。除此之外,cookie还可以设置以下属性

  • domain:设置允许携带cookie的域
  • path:设置允许携带cookie的资源路径
  • maxAge:设置cookie最大存活时间,单位秒(若不设置,cookie默认在浏览器关闭时失效)
  • httpOnly:设为true,可避免JS脚本窃取cookie
  • secure:设为true,则只有使用HTTPS等安全的协议时,才携带Cookie

另:Cookie,翻译过来是小饼干的意思,这样的命名也意味着,它只能保存少量的信息(简单的k-v键值对),且浏览器一般对cookie的大小,数量等都有所限制,而由于将信息保存在了浏览器端,也就意味着信息容易遭到窃取或篡改,不够安全。

于是,将信息保存在服务端的Session技术,轮到它登场了。

Session

session是依赖于cookie的。它的大概原理是:一次请求到来,服务器端在处理请求时,生成一个session对象(保存在服务端的内存中),以及一个对应的sessionId。当服务端处理完毕,返回响应时,在HTTP响应报文添加这样的头部Set-Cookie:JSESSIONID=123。浏览器端保存下这个name为JSESSIONID的cookie,在下次请求时,携带这个cookie。服务端根据JSESSIONID,找到自己内存中对应的session对象,这样,即可在多个HTTP请求之间共享数据。在tomcat的配置中,session的默认有效时间为30分钟。session对象中也可以添加k-v键值对,但不仅仅是字符串,还可以是任意的Object对象,这就比cookie能携带的信息要大得多了。且session对象的大小,理论上只会受到服务器内存大小的限制。且由于存在服务端,安全性相对就高了许多。

在Servlet中,一个session对象是一个HttpSession类的实例,通过request对象来生成或获取一个session对象,如下

// 1. 若此次请求中没有携带name为JSESSIONID的cookie, 则getSession会新创建一个session对象
//    并把对应的id添加到响应头
// 2. 若此次请求中有携带name为JSESSIONID的cookie, 则getSession会根据这个id的值
//    从内存中找到该id对应的session对象
HttpSession session = request.getSession();

session对象中常用的方法:

  • setAttribute(String s, Object o):往session对象中添加一组键值对数据
  • getAttribue(String s)
  • setMaxInactiveInterval(int i):设置session对象最大有效时长,单位秒。(负数表示永不失效)
  • invalidate():使该session对象失效

另:

  1. 由于session对象是保存在服务端内存,当服务器关闭时,内存中的数据就没了。所以session还有钝化和活化处理。这种处理在tomcat中是默认开启的。当session未失效且服务器正常关闭时,内存中的session对象,会被序列化,存储到磁盘,文件名为SESSIONS.ser(这叫钝化)。下次服务器启动时,会自动读取并删除这个SESSIONS.ser,将session对象的数据从磁盘加载到内存(这叫活化)。由于session对象中可以保存任意Object对象,若要成功支持session钝化活化,则保存在session中的数据,都需要可序列化(实现Serializable接口)
  2. 由于name为JSESSIONID的cookie,是自动设置到响应中的,所以这个cookie遵循默认的配置,在浏览器关闭后就会失效。若需要在浏览器关闭后不失效,则手动设置一个name为JSESSIONID的cookie,并设置其maxAge

对比小结

CookieSession
数据存储位置客户端(浏览器端)服务端
限制少量数据,简单的字符串键值对可存很多数据,任意Object类型
有效性默认在浏览器关闭后失效默认有效时长30分钟(tomcat中)
安全性

其它相关对象

上面的会话技术,是在同一用户的多次请求之间进行数据共享。若要在整个web应用中共享数据,则可以通过ServletContext对象实现。

ServletContext

一个Web应用,对应一个ServletContext对象,根据命名也能看出,这是整个web应用的上下文环境。这个对象中的常用方法,下面列举一二

  • setAttribue(String s, Object o):添加一组键值对数据
  • getAttribe(String s)
  • getInitParameter(String s):获取web应用的全局初始化参数(web.xml中的<context-param>标签)

小应用:统计网站访问量。每收到一个请求,就对ServletContext中的一个属性进行累加操作

ServletConfig

ServletConfig对象用于向Servlet传递一些参数,以便在Servlet初始化时使用。它主要包含了如下方法

  • getServletContext():获取web应用全局上下文
  • getInitParameter(String s):获取Servlet初始化参数(web.xml中的<init-param>标签,也可通过@WebInitParam注解进行设置)

过滤器

概述

Servlet,Filter,Listener并称JavaWeb三大组件。

其中的Filter,指的就是javax.servlet.Filter接口。它可以在请求被处理(请求的资源可以是Servlet,也可以是HTML等静态资源)的之前之后,进行一些额外的操作。

它的工作流程如下图所示

过滤器可以配置不止一个,当有多个过滤器时,它们就形成了一个,如下图

与Servlet的生命周期类似,Filter也有如下3个方法

  • init():过滤器被创建时,该方法被调用
  • destroy():过滤器被销毁时,该方法被调用
  • doFilter():过滤器对请求/响应进行过滤处理时,该方法被调用

Filter相关的一共有3个类

  • javax.servlet.Filter:核心接口
  • javax.servlet.FilterConfig:过滤器配置接口,可以通过该接口给过滤器传递一些初始化参数
  • javax.servlet.FilterChain:过滤器链接口

使用

创建一个过滤器的步骤如下

  1. 新建一个类,实现javax.servlet.Filter接口

  2. 重写init()destroy()doFilter()方法

  3. 配置过滤器

    • xml方式:web.xml

      	<filter>
              <filter-name>myFilter</filter-name>
              <filter-class>filter.LogFilter</filter-class>
          </filter>
          <filter-mapping>
              <filter-name>myFilter</filter-name>
              <url-pattern>/*</url-pattern>
          </filter-mapping>
      
    • 注解方式:@WebFilter

      @WebFilter(servletNames = {"ElServlet"})
      public class LogFilter implements Filter {
      	// 代码略
      }
      

配置参数

只需要配置过滤器在何时起作用即可,可以指定其urlPattern,对满足某一格式的url进行拦截;也可以指定servletNames,对指定的Servlet进行拦截(二者选其一即可)

  • urlPattern

  • servletNames

如下

@WebFilter(servletNames = {"ElServlet"})
public class LogFilter implements Filter {
	// 代码略
}
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
	// 代码略
}

注意

由于拦截某一请求的过滤器,可能有多个,这就形成一条过滤器链,当一个过滤器处理完毕后,应该调用链上的下一个过滤器,或者直接进入到资源处理(当该过滤器是链中的最后一个时)。

所以,在某个过滤器的doFilter()方法中,完成了处理后,应该调用FilterChaindoFilter()方法,将请求/响应在过滤器链上传递下去(若不调用,则请求不会到达最终的web资源)。在调用FilterChaindoFilter()方法之前,请求还未被处理,在其后,请求已完成处理。这就对应了先前说的,Filter可以在请求被处理之前之后,进行一些额外的操作。

@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
	public void destroy() {
	}

	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
		System.out.println("before servlet");
		chain.doFilter(req, resp);
		System.out.println("after servlet");
	}

	public void init(FilterConfig config) throws ServletException {
	}
}

应用场景

Filter的常见应用场景列举如下

  • 进行日志记录(记录被调用的接口,请求参数,以便进行问题排查,数据统计等工作)

  • 统一设置编码格式(如处理中文乱码问题等)

  • 过滤敏感词汇

  • 数据压缩

  • 数据加密

  • 身份认证

监听器

JavaWeb三大组件的最后一个,Listener。

用于监听一些重要事件的发生。以便在事件发生时,能够做一些额外操作。

Servlet API中针对如下对象,提供了对应的监听器接口

  • ServletContext对象

    ServletContextListenerServletContextAttributeListener

  • HttpSession对象

    HttpSessionListenerHttpSessionAttributeListenerHttpSessionIdListener

  • ServletRequest对象

    ServletRequestListenerServletRequestAttributeListener

比如,可以监听HttpSession对象的创建和销毁,来实现一个网站在线人数统计的功能(若用户请求的是一个JSP页面,JSP页面在默认情况下,会为每一个新的请求创建一个session对象,这是下面进行人数统计的基础,这个默认行为,可以通过JSP指令<%@ page session="false" %>来取消)

import javax.servlet.ServletContext;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.time.LocalDateTime;

/**
 * 统计网线在线人数
 * Session创建, 就全局人数+1, Session销毁, 就全局人数-1
 * **/
@WebListener // 加上注解后,就不必在web.xml中进行配置
public class OnLineCounter implements HttpSessionListener {

	private final String COUNT = "count";

	@Override
	public void sessionCreated(HttpSessionEvent httpSessionEvent) {
		HttpSession session = httpSessionEvent.getSession();
		System.out.println(LocalDateTime.now() + ", 新来了一个人, id = " + session.getId());
		ServletContext servletContext = session.getServletContext();
		Object o = servletContext.getAttribute(COUNT);
		int count = o == null ? 0 : (int) o;
		servletContext.setAttribute(COUNT, ++count);
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
		HttpSession session = httpSessionEvent.getSession();
		System.out.println(LocalDateTime.now() + ", 走了一个人, id = " + session.getId());
		ServletContext servletContext = session.getServletContext();
		int count = (int) servletContext.getAttribute(COUNT);
		servletContext.setAttribute(COUNT, --count);
	}
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐