Java中的Servlet你了解吗?

Java中的Servlet你了解吗?

Servlet详解

ServletServer + Applet的缩写,表示一个服务器应用。
其实,Servlet就是一套规范,我们按照这套规范写出来的代码就可以直接在Java服务器上运行。

Servlet3.1中的Servlet结构如下图所示:
在这里插入图片描述

1. Servlet接口

说到规范,那当然还是接口最重要了,我们看一下接口的定义:

package javax.servlet;

import java.io.IOException;

/**
* 定义所有servlet必须实现的方法。
* servlet是在Web服务器中运行的小Java程序。servlet通常通过HTTP(超文本传输协议)接收和响应来自Web客户机的请求。
* 要实现这个接口,您可以编写一个扩展javax.servlet.GenericServlet的通用servlet,或者编写一个扩展javax.servlet.http.HttpServlet的HTTP servlet。
* 该接口定义了初始化servlet、服务请求和从服务器中删除servlet的方法。这些被称为生命周期方法,按以下顺序调用:
* 先构造servlet,然后用init方法进行初始化。
* 从客户机到服务方法的任何调用都将被处理。
* servlet退出服务,然后使用destroy方法销毁,然后进行垃圾收集并最终完成。
* 除了生命周期方法之外,该接口还提供了getServletConfig方法和getServletInfo方法,
* servlet可以使用该方法获取任何启动信息,getServletInfo方法允许servlet返回关于自身的基本信息,例如作者、版本和版权。
*/
public interface Servlet {

/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* servlet容器在实例化servlet之后只调用init方法一次。init方法必须成功完成,servlet才能接收任何请求。
* 如果使用init方法,servlet容器不能将servlet放入服务中:
* 1.抛出ServletException
* 2.没有在Web服务器定义的时间段内返回
*/
public void init(ServletConfig config) throws ServletException;

/**
*
* 返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。
* 返回的ServletConfig对象是传递给init方法的对象。
* 这个接口的实现负责存储ServletConfig对象,以便这个方法可以返回它。实现这个接口的GenericServlet类已经做到了这一点。
*/
public ServletConfig getServletConfig();

/**
* 由servlet容器调用,以允许servlet响应请求。
* 此方法仅在servlet的init()方法成功完成后调用。
* 响应的状态码应该始终为抛出或发送错误的servlet设置。
* servlet通常运行在多线程servlet容器中,可以并发处理多个请求。
* 开发人员必须注意同步访问任何共享资源,如文件、网络连接以及servlet的类和实例变量。
*/
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

/**
* 返回有关servlet的信息,如作者、版本和版权。
* 此方法返回的字符串应该是纯文本,而不是任何类型的标记(如HTML、XML等)。
*/
public String getServletInfo();

/**
*
* 由servlet容器调用,以指示servlet正在退出服务。
* 此方法仅在servlet服务方法中的所有线程都退出或超时时间过后才调用。
* 在servlet容器调用此方法之后,它将不会在此servlet上再次调用该服务方法。
* 此方法为servlet提供了一个机会来清理任何被占用的资源(例如,内存、文件句柄、线程),
* 并确保任何持久状态都与servlet在内存中的当前状态同步。
*/
public void destroy();
}

简单介绍一下几个方法的作用:

  • **init**:init方法在容器启动时被容器调用(当load-on-startup设置位负数或者不设置时会在Servlet第一次用到时才会被调用),只会调用一次;
  • **getServletConfig**:用于获取ServletConfig,即Servlet的配置;
  • **service**:用于具体处理一个请求;
  • **getServletInfo**:获取一些Servlet相关的信息,如作者、版权等,默认返回空串;
  • **destroy**:用于在Servlet销毁时释放资源,也只会调用一次;

(1) ServletConfig

ServletConfig指的是Servlet的配置,也就是我们在web.xml中定义的Servlet时通过init-param标签配置的参数就是通过ServletConfig来保存的。

比如定义Spring MVCServlet时指定配置文件位置的contextConfigLocation参数就保存在ServletConfig中,如下:

<servlet>
<servlet-name>demoDispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>demo-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

下面我们看一下ServletConfig接口的定义:

package javax.servlet;

import java.util.Enumeration;

/**
* 一个servlet配置对象,由servlet容器使用,在初始化期间向servlet传递信息。
*/
public interface ServletConfig {

/**
* 返回此servlet实例的名称。该名称可以通过服务器管理提供,在web应用程序部署描述符中分配,
* 或者对于未注册(因此未命名)的servlet实例,它将是servlet的类名。
*/
public String getServletName();

/**
* 返回对调用者正在其中执行的ServletContext的引用。
*/
public ServletContext getServletContext();

/**
* 获取具有给定名称的初始化参数的值。
*/
public String getInitParameter(String name);

/**
* 返回servlet初始化参数的名称,作为String对象的枚举,如果servlet没有初始化参数,则返回空枚举。
*/
public Enumeration<String> getInitParameterNames();
}

简单介绍一下几个方法的作用:

  • getServletName:用于获取Servlet的名字,也就是我们在web.xml中定义的servlet-name
  • getServletContext:获取ServletContext,表示我们这个应用本身;
  • getInitParameter:获取init-param配置的参数;
  • getInitParameterNames:获取配置的所有init-param的名字集合;

(2) ServletContext

ServletContext非常重要,代表的其实就是我们应用本身。

那么自然,ServletContext中设置的参数可以被我们当前应用的所有Servlet共享,也就是我们在写功能时,把一些参数保存在Application中,其实就是保存在了ServletContext中了。

这里只是简单了解一下ServletContext

(3) 使用

ServletConfigServletContext最常见的使用之一是传递初始化参数

<!-- web.xml -->
<?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">
<display-name>initParam Demo</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>application-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>DemoServlet</servlet-name>
<servlet-class>com.excelib.DemoServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>demo-servlet.xml</param-value>
</init-param>
</servlet>
...
</web-app>
  • 通过context-param配置的contextConfigLocation配置到了servletContext
  • servlet下面的init-param配置的contextConfigLocation配置到了ServletConfig

在Servlet中可以分别通过它们的getInitParameter方法进行获取,比如:

String contextLocation = getServletConfig().getServletContext().getInitParameter("contextConfigLocation");
String servletLocation = getServletConfig().getInitParameter("contextConfigLocation");

另外**ServletContext中非常常用的用法就是保存Application级别的属性**,比如:

getServletContext().setAttribute("contextConfigLocation", "new path");

需要注意的是,这里设置的同名的Attribute并不会覆盖initParameter中的参数值,它们是两套数据,互不干扰。

ServletConfig不可以设置属性

2.GenericServlet

GenericServletServlet的默认实现,主要做了三件事情:

  • 实现了ServletConfig接口,我们可以直接调用该接口里面的方法;
  • 提供了无参的init方法;
  • 提供了log方法;

实现了ServletConfig接口

这样我们在需要调用ServletConfig中方法的时候可以直接调用,而不需要先获取ServletConfig了。其实是在底层帮我们内部调用了,源码如下:

/**
* 返回对servlet在其中运行的ServletContext的引用。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取上下文。
*/
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig(); // 先获取ServletConfig
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}

return sc.getServletContext();
}

提供了无参的init方法

GenericServlet实现了Servlet的init(ServletConfig config)方法,在里面将config设置给了内部变量config,然后调用无参的init方法,该方法是一个模板方法,在子类中可以通过覆盖它来完成自己的初始化工作,如下:

/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* 这个实现存储了它从servlet容器接收到的ServletConfig对象,以供以后使用。
* 当重写这种形式的方法时,调用super.init(config)。
*/
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

/**
*一个方便的方法,可以被重写,这样就不需要调用super.init(config)。
* 而不是重写init(ServletConfig),只需重写这个方法,它将被GenericServlet调用。
* init (ServletConfig配置)。ServletConfig对象仍然可以通过getServletConfig来检索。
*/
public void init() throws ServletException {

}

这样做有三个好处:

  • 将参数config设置给内部属性config,这样就可以在ServletConfig的接口方法中直接调用config的相应方法来执行;
  • 之后写Servlet时只需要处理自己的初始逻辑,不需要再关注config
  • 重写init方法时也不需要再调用super.init(config)

提供了log方法

  • 记录日志的log
  • 记录异常的log

具体实现是通过传给ServletContext的日志实现的。

/**
* 将指定的消息写入servlet日志文件,并以servlet的名称作为前置。
*/
public void log(String msg) {
getServletContext().log(getServletName() + ": "+ msg);
}


/**
* 将给定Throwable异常的解释性消息和堆栈跟踪写入servlet日志文件,并以servlet名称为前缀。
*/
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}

完整源码如下:

package javax.servlet;

import java.io.IOException;
import java.util.Enumeration;
import java.util.ResourceBundle;

/**
* 定义一个通用的、独立于协议的servlet。要编写用于Web的HTTP servlet,请扩展javax.servlet.http.HttpServlet。
* GenericServlet实现Servlet和ServletConfig接口。GenericServlet可以由servlet直接扩展,
* 尽管更常见的是扩展特定于协议的子类,如HttpServlet。
* GenericServlet使编写servlet更容易。它提供了生命周期方法init和destroy的简单版本,以及ServletConfig接口中的方法。
* GenericServlet还实现了在ServletContext接口中声明的日志方法。
* 要编写通用servlet,只需要重写抽象服务方法。
*/
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static ResourceBundle lStrings =
ResourceBundle.getBundle(LSTRING_FILE);

private transient ServletConfig config;


/**
*
* 什么也不做。所有servlet初始化都是由其中一个init方法完成的。
*
*/
public GenericServlet() { }


/**
* 由servlet容器调用,以指示servlet正在退出服务。
*/
public void destroy() {
}


/**
* 返回一个包含命名初始化参数值的字符串,如果参数不存在则返回null。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取指定参数的值。
*/
public String getInitParameter(String name) {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}

return sc.getInitParameter(name);
}


/**
* 以String对象的枚举形式返回servlet初始化参数的名称,如果servlet没有初始化参数,则返回空枚举。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取参数名。
*/
public Enumeration<String> getInitParameterNames() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}

return sc.getInitParameterNames();
}


/**
* 返回servlet的ServletConfig对象。
*/
public ServletConfig getServletConfig() {
return config;
}


/**
* 返回对servlet在其中运行的ServletContext的引用。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取上下文。
*/
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}

return sc.getServletContext();
}


/**
* 返回有关servlet的信息,如作者、版本和版权。默认情况下,此方法返回一个空字符串。重写此方法以使其返回一个有意义的值。
*/
public String getServletInfo() {
return "";
}


/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* 这个实现存储了它从servlet容器接收到的ServletConfig对象,以供以后使用。
* 当重写这种形式的方法时,调用super.init(config)。
*/
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}


/**
*一个方便的方法,可以被重写,这样就不需要调用super.init(config)。
* 而不是重写init(ServletConfig),只需重写这个方法,它将被GenericServlet调用。
* init (ServletConfig配置)。ServletConfig对象仍然可以通过getServletConfig来检索。
*/
public void init() throws ServletException {

}


/**
* 将指定的消息写入servlet日志文件,并以servlet的名称作为前置。
*/
public void log(String msg) {
getServletContext().log(getServletName() + ": "+ msg);
}


/**
* 将给定Throwable异常的解释性消息和堆栈跟踪写入servlet日志文件,并以servlet名称为前缀。
*/
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}


/**
* 由servlet容器调用,以允许servlet响应请求。
* 这个方法被声明为抽象的,所以子类,比如HttpServlet,必须覆盖它。
*/

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


/**
* 返回此servlet实例的名称。
*/
public String getServletName() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}

return sc.getServletName();
}
}

3. HttpServlet

HttpServlet是用HTTP协议实现的Servlet的基类,写Servlet时直接继承它就可以了,不需要再从头实现Servlet接口。

HttpServlet是跟协议有关的,所以我们就关注一下如何处理请求。

HttpServlet的重点主要在service中,在service方法中首先将ServletRequestServletResponse转换为HttpServletRequestHttpServletResponse,然后根据Http请求的类型不同将请求路由到了不同的处理方法

   @Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
// 如果请求类型不相符,则抛出异常
if (!(req instanceof HttpServletRequest &&
res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
// 转换request和response类型
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
// 调用http的处理方法
service(request, response);
}

/**
* 从公共服务方法接收标准HTTP请求,并将其分派给该类中定义的doXXX方法。
* 该方法是Servlet的http特定版本Servlet. service方法。不需要重写这个方法。
*/
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
// 获取请求类型
String method = req.getMethod();
// 将不同的请求类型路由到不同的处理方法
if (method.equals(METHOD_GET)) {
// 获取最后修改时间
long lastModified = getLastModified(req);
if (lastModified == -1) {
// 如果servlet不支持if-modified-since,则无需进一步执行昂贵的逻辑
doGet(req, resp);
} else {
// 获取请求头中的if-modified-since时间
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// 如果servlet修改时间较晚,则调用doGet()
// 向下舍入最近的秒,以进行正确比较
// ifModifiedSince为-1将始终小于lastModified
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
// 返回状态码为304(未修改)
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}

} else if (method.equals(METHOD_HEAD)) {
// 获取最后修改时间
long lastModified = getLastModified(req);
// 设置响应头中的最后修改时间
maybeSetLastModified(resp, lastModified);
// 调用doHead()
doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);

} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);

} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);

} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);

} else {
// 注意,这意味着没有servlet支持该服务器上任何地方请求的任何方法。
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
// 返回状态码为501(未实现)
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}