Servlet详细解析

首先先说一下为什么要学Servlet。起因是在学习Spring MVC的时候,了解到Servlet是其核心,对其原理不了解将会对学习Spring MVC造成障碍,因此决定书写这篇文章用来学习。

1、什么是Servlet

Servlet(Server Applet),全称Java Servlet,未有中文译文。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

1.1 工作模式

  • 客户端发送请求至服务器
  • 服务器启动并调用Servlet,Servlet根据客户端请求生成响应内容并将其传给服务器
  • 服务器将响应返回客户端

1.2 Servlet API预览

Servlet API 包含以下4个Java包:

1.javax.servlet 其中包含定义servlet和servlet容器之间契约的类和接口。

2.javax.servlet.http 其中包含定义HTTP Servlet 和Servlet容器之间的关系。

3.javax.servlet.annotation 其中包含标注servlet,Filter,Listener的标注。它还为被标注元件定义元数据。

4.javax.servlet.descriptor,其中包含提供程序化登录Web应用程序的配置信息的类型。

2、Servlet接口

2.1 Servlet接口定义的方法

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;
    
	ServletConfig getServletConfig();
 
	void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
 
	String getServletInfo();
 
	void destroy();
}

另外还有

String getServletInfo();//这个方法会返回Servlet的一段描述,可以返回一段字符串。
ServletConfig getServletConfig();//这个方法会返回由Servlet容器传给init( )方法的ServletConfig对象。

2.2 Servlet的生命周期

其中,**init( ),service( ),destroy( )**是Servlet生命周期的方法。代表了Servlet从“出生”到“工作”再到“死亡 ”的过程。Servlet容器(例如TomCat)会根据下面的规则来调用这三个方法:

  1. init( ),当Servlet第一次被请求时,Servlet容器就会开始调用这个方法来初始化一个Servlet对象出来,但是这个方法在后续请求中不会在被Servlet容器调用,就像人只能“出生”一次一样。我们可以利用init( )方法来执行相应的初始化工作。调用这个方法时,Servlet容器会传入一个ServletConfig对象进来从而对Servlet对象进行初始化。

  2. service( )方法,每当请求Servlet时,Servlet容器就会调用这个方法。就像人一样,需要不停的接受老板的指令并且“工作”。第一次请求时,Servlet容器会先调用init( )方法初始化一个Servlet对象出来,然后会调用它的service( )方法进行工作,但在后续的请求中,Servlet容器只会调用service方法了。

  3. destory,当要销毁Servlet时,Servlet容器就会调用这个方法,就如人一样,到时期了就得死亡。在卸载应用程序或者关闭Servlet容器时,就会发生这种情况,一般在这个方法中会写一些清除代码。

2.3 ServletRequest 接口

Servlet容器对于接受到的每一个Http请求,都会创建一个ServletRequest对象,并把这个对象传递给Servlet的Sevice( )方法。

让我们来看一看ServletRequest接口的部分内容:

public interface ServletRequest {
    int getContentLength();//返回请求主体的字节数

    String getContentType();//返回主体的MIME类型

    String getParameter(String var1);//返回请求参数的值
}

其中,getParameter是在ServletRequest中最常用的方法,可用于获取查询字符串的值。

2.4 ServletResponse接口

javax.servlet.ServletResponse接口表示一个Servlet响应,在调用Servlet的Service( )方法前,Servlet容器会先创建一个ServletResponse对象,并把它作为第二个参数传给Service( )方法。ServletResponse隐藏了向浏览器发送响应的复杂过程。

public interface ServletResponse {
    String getCharacterEncoding();

    String getContentType();

    ServletOutputStream getOutputStream() throws IOException;

    PrintWriter getWriter() throws IOException;

    void setCharacterEncoding(String var1);

    void setContentLength(int var1);

    void setContentType(String var1);

    void setBufferSize(int var1);

    int getBufferSize();

    void flushBuffer() throws IOException;

    void resetBuffer();

    boolean isCommitted();

    void reset();

    void setLocale(Locale var1);

    Locale getLocale();
}

其中的getWriter方法,它返回了一个可以向客户端发送文本的的Java.io.PrintWriter对象。默认情况下,**PrintWriter对象使用ISO-8859-1编码(该编码在输入中文时会发生乱码)。**在向客户端发送响应时,大多数都是使用该对象向客户端发送HTML。

还有一个方法也可以用来向浏览器发送数据,它就是getOutputStream,从名字就可以看出这是一个二进制流对象,因此这个方法是用来发送二进制数据的。

在发送任何HTML之前,应该先调用setContentType()方法,设置响应的内容类型,并将“text/html”作为一个参数传入,这是在告诉浏览器响应的内容类型为HTML,需要以HTML的方法解释响应内容而不是普通的文本,或者也可以加上“charset=UTF-8”改变响应的编码方式以防止发生中文乱码现象。

如何解决中文乱码现象,是一个非常重要的问题,这里后面会另作解释。

2.5 ServletConfig接口

当Servlet容器初始化Servlet时,Servlet容器会给Servlet的init( )方式传入一个ServletConfig对象。

其中几个方法如下:

public interface ServletConfig {
    String getServletName();

    ServletContext getServletContext();

    String getInitParameter(String var1);

    Enumeration getInitParameterNames();
}

2.6 ServletContext对象

ServletContext对象表示Servlet应用程序。ServletContext官方叫servlet上下文。服务器会为每一个工程创建一个对象,这个对象就是ServletContext对象。这个对象全局唯一,而且工程内部的所有servlet都共享这个对象。所以叫全局应用程序共享对象。

每个Web应用程序都只有一个ServletContext对象。在将一个应用程序同时部署到多个容器的分布式环境中,每台Java虚拟机上的Web应用都会有一个ServletContext对象。

通过在ServletConfig中调用getServletContext方法,也可以获得ServletContext对象。

那么为什么要存在一个ServletContext对象呢?存在肯定是有它的道理,**因为有了ServletContext对象,就可以共享从应用程序中的所有资料处访问到的信息,并且可以动态注册Web对象。前者将对象保存在ServletContext中的一个内部Map中。**保存在ServletContext中的对象被称作属性。

ServletContext中的下列方法负责处理属性:

Object getAttribute(String var1);

Enumeration<String> getAttributeNames();

void setAttribute(String var1, Object var2);

void removeAttribute(String var1);

总结可以得出以下几点:

  1. 是一个域对象(域对象是服务器在内存上创建的存储空间,用于在不同动态资源(servlet)之间传递与共享数据)
  2. 可以读取全局配置参数
  3. 可以搜索当前工程目录下面的资源文件
  4. 可以获取当前工程名字(了解)

3、GenericServlet抽象类

使用Servlet接口需要手动定义接口的所有方法,并且还要手动维护ServletConfig这个对象的引用。而GenericServlet抽象类的出现简化了这个过程。

GenericServlet实现了Servlet和ServletConfig接口,下面是GenericServlet抽象类的具体代码:

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
    private transient ServletConfig config;

    public GenericServlet() {
    }

    public void destroy() {
    }

    public String getInitParameter(String name) {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameter(name);
        }
    }

    public Enumeration getInitParameterNames() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameterNames();
        }
    }

    public ServletConfig getServletConfig() {
        return this.config;
    }

    public ServletContext getServletContext() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletContext();
        }
    }

    public String getServletInfo() {
        return "";
    }

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

    public void init() throws ServletException {
    }

    public void log(String msg) {
        this.getServletContext().log(this.getServletName() + ": " + msg);
    }

    public void log(String message, Throwable t) {
        this.getServletContext().log(this.getServletName() + ": " + message, t);
    }

    public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    public String getServletName() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletName();
        }
    }
}

相较于直接实现Servlet接口,使用GenericServlet抽象类有以下好处:

  1. 为Servlet接口中的所有方法提供了默认的实现,则程序员需要什么就直接改什么,不再需要把所有的方法都自己实现了。
  2. 提供方法,包围ServletConfig对象中的方法。
  3. 将init( )方法中的ServletConfig参数赋给了一个内部的ServletConfig引用从而来保存ServletConfig对象,不需要程序员自己去维护ServletConfig了。

3.1 为什么有两个init()方法

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

public void init() throws ServletException {
}

可以看到,GenericServlet抽象类中,存在两个init()方法。设计者的初衷到底是什么呢?先说结论,只重写init()方法,可以保存GenericServlet对象中的servletConfig。

我们知道,抽象类是无法直接产生实例的,需要另一个类去继承这个抽象类,那么就会发生方法覆盖的问题,如果在类中覆盖了GenericServlet抽象类的init()方法,那么程序员就必须手动的去维护ServletConfig对象了,还得调用super.init(servletConfig)方法去调用父类GenericServlet的初始化方法来保存ServletConfig对象,这样会给程序员带来很大的麻烦。GenericServlet提供的第二个不带参数的init( )方法,就是为了解决上述问题的。

这个不带参数的init()方法,是在ServletConfig对象被赋给ServletConfig引用后,由第一个带参数的init(ServletConfig servletconfig)方法调用的,那么这意味着,当程序员如果需要覆盖这个GenericServlet的初始化方法,则只需要覆盖那个不带参数的init( )方法就好了,此时,GenericServlet对象仍然有servletConfig保存着。

4、更强大的HttpServlet

继承了GenericServlet抽象类的HttpServlet,为什么说HttpServlet更加强大?有两个主要原因:

  1. HttpServlet是由GenericServlet抽象类扩展而来的

    public abstract class HttpServlet extends GenericServlet implements Serializable 
    
  2. 现在大部分的应用程序都要与HTTP结合起来使用,这意味着我们可以利用HTTP的特性完成更多更强大的任务。

4.1 Service方法

HttpServlet重写了service方法。我们先来看一看GenericServlet抽象类的service方法:

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

可以看到,这是一个抽象方法,也就是HttpServlet要自己去实现这个service方法,我们在看看HttpServlet是怎么覆盖这个service方法的:

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方法把接收到的ServletRequsest类型的对象转换成了HttpServletRequest类型的对象,把ServletResponse类型的对象转换成了HttpServletResponse类型的对象。之所以能够这样强制的转换,**是因为在调用Servlet的Service方法时,Servlet容器总会传入一个HttpServletRequest对象和HttpServletResponse对象,预备使用HTTP。**因此,转换类型当然不会出错了。

完成转换之后,service方法把两个转换后的对象传入了另一个service方法中,该方法是完成任务的核心:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    long lastModified;
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader("If-Modified-Since");
            if (ifModifiedSince < lastModified / 1000L * 1000L) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
    } else if (method.equals("HEAD")) {
        lastModified = this.getLastModified(req);
        this.maybeSetLastModified(resp, lastModified);
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
        this.doTrace(req, resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }

}

我们发现,这个service方法的参数是HttpServletRequest对象和HttpServletResponse对象,刚好接收了上一个service方法传过来的两个对象。

接下来我们再看看service方法是如何工作的,我们会发现在service方法中还是没有任何的服务逻辑,但是却在解析HttpServletRequest中的方法参数,并调用以下方法之一:**doGet,doPost,doHead,doPut,doTrace,doOptions和doDelete。**这7种方法中,每一种方法都表示一个Http方法。**doGet和doPost是最常用的。**所以,如果我们需要实现具体的服务逻辑,不再需要覆盖service方法了,只需要覆盖doGet或者doPost就好了。

总结一下较之GenericServlet的特点:

  1. 不用覆盖service方法,而是覆盖doGet或者doPost方法。在少数情况,还会覆盖其他的5个方法。
  2. 使用的是HttpServletRequest和HttpServletResponse对象。

4.2 HttpServletRequest接口

HttpServletRequest表示Http环境中的Servlet请求。它扩展于javax.servlet.ServletRequest接口,并添加了几个方法。

String getContextPath();//返回请求上下文的请求URI部分

Cookie[] getCookies();//返回一个cookie对象数组

String getHeader(String var1);//返回指定HTTP标题的值

String getMethod();//返回生成这个请求HTTP的方法名称

String getQueryString();//返回请求URL中的查询字符串

HttpSession getSession();//返回与这个请求相关的会话对象

因为Request代表请求,所以我们可以通过该对象分别获得HTTP请求的请求行,请求头和请求体。

Servlet详细解析

4.2.1 获得请求行的方法

假设查询字符串为:username=zhangsan&password=123

获得客户端的请求方式:String getMethod()

获得请求的资源:

String getRequestURI()

StringBuffer getRequestURL()

String getContextPath()//web应用的名称

String getQueryString()//get提交url地址后的参数字符串

4.2.2 获得请求头的方法

long getDateHeader(String name)

String getHeader(String name)

Enumeration getHeaderNames()

Enumeration getHeaders(String name)

int getIntHeader(String name)

//referer头的作用:执行该此访问的的来源,做防盗链

4.2.3 获得请求体的方法

请求体中的内容是通过post(get)提交的请求参数,格式是key-value:

username=zhangsan&password=123&hobby=football&hobby=basketball

通过下面的方法可以获得请求参数:

String getParameter(String name)

String[] getParameterValues(String name)

Enumeration getParameterNames()

Map<String,String[]> getParameterMap()

4.3 HttpServletResponse接口

在Service API中,定义了一个HttpServletResponse接口,它继承自ServletResponse接口,专门用来封装HTTP响应消息。

由于HTTP请求消息分为状态行,响应消息头,响应消息体三部分,因此,在HttpServletResponse接口中定义了向客户端发送响应状态码,响应消息头,响应消息体的方法。

Servlet详细解析

以下是接口中的所有方法,其中已经删去了@deprecated即淘汰的方法:

void addCookie(Cookie var1);//给这个响应添加一个cookie

boolean containsHeader(String var1);

String encodeURL(String var1);

String encodeRedirectURL(String var1);

void sendError(int var1, String var2) throws IOException;

void sendError(int var1) throws IOException;

void sendRedirect(String var1) throws IOException;//发送一条响应码,讲浏览器跳转到指定的位置

void setDateHeader(String var1, long var2);

void addDateHeader(String var1, long var2);

void setHeader(String var1, String var2);

void addHeader(String var1, String var2);//给这个请求添加一个响应头

void setIntHeader(String var1, int var2);

void addIntHeader(String var1, int var2);

void setStatus(int var1);//设置响应行的状态码

4.3.1 字符流和字节流获取

PrintWriter getWriter() throws IOException;

获得字符流,通过字符流的write(String s)方法可以将字符串设置到response 缓冲区中,随后Tomcat会将response缓冲区中的内容组装成Http响应返回给浏览器端。

ServletOutputStream getOutputStream() throws IOException;

获得字节流,通过该字节流的write(byte[] bytes)可以向response缓冲区中写入字节,再由Tomcat服务器将字节内容组成Http响应返回给浏览器。想要输出二进制格式的相应正文,就需要使用 getOutputStream()方法。

注意:两个方法不能同时使用,否则报错。

4.3.2 Response乱码问题

由于HttpServletResponse默认使用ISO8859-1码表,而浏览器端使用的则是GB2312码表,二者不统一会导致出现乱码问题,因此需要告知发送端服务端和浏览器端都使用UTF-8编码去解码,这样才能解决乱码问题。

response.setContentType("text/html;charset=UTF-8")

4.3.3 Response工作流程

Servlet详细解析

5、Servlet工作流程

Servlet详细解析

6、ServletContextListener(全局监听器)

首先说明的是,ServletContextListener是Servlet容器中的一个接口,而实现了该接口的类负责监听ServletContext,下面先看看接口内容:

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);//初始化

    void contextDestroyed(ServletContextEvent var1);//销毁
}

可以看到,接口内部只有两个函数,功能为初始化和销毁。凭此,我们可以大概猜测ServletContextListener的工作机制:

  1. 当应用启动时,ServletContext进行初始化,然后Servlet容器会自动调用正在监听ServletContext的ServletContextListener的void contextInitialized(ServletContextEvent var1)方法,并向其传入一个ServletContextEvent对象。
  2. 当应用停止时,ServletContext被销毁,此时Servlet容器也会自动地调用正在监听ServletContext的ServletContextListener的void contextDestroyed(ServletContextEvent var1)方法。

根据以上的两个方法,我们可以做到一些功能。比如,在实际应用中,往往需要统计自Web 应用被发布后网页被客户端访问的次数,这就要求当Web 应用被终止时,计数器的数值被永久存储在一个文件中或者数据库中,等到Web 应用重新启动时,先从文件或数据库中读取计数器的初始值,然后在此基础上继续计数。这个功能可以通过实现ServletContextListener接口进行实现。

6.1 在Spring中的应用

首先需要回顾ServletContext的概念。其实ServletContext就是一个“域对象”,它存在于整个应用中,并在在整个应用中有且仅有1份,它表示了当前整个应用的“状态”,你也可以理解为某个时刻的ServletContext代表了这个应用在某个时刻的“一张快照”,这张“快照”里面包含了有关应用的许多信息,应用的所有组件都可以从ServletContext获取当前应用的状态信息。

ServletContext随着程序的启动而创建,随着程序的停止而销毁。通俗点说,我们可以往这个ServletContext域对象中“存东西”,然后也可以在别的地方中“取出来”。

在web.xml中注册Spring IOC容器:

<listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>

        classpath:applicationContext.xml
    </param-value>

</context-param>

其中的监听器类org.springframework.web.context.ContextLoaderListener实现了ServletContextListener接口,能够监听ServletContext的生命周期中的“初始化”和“销毁”。其中,org.springframework.web.context.ContextLoaderListener为Spring团队写的监听器类。

ContextLoaderListener的内部情况又是怎么样的呢?下面我们来看一下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

可以看到,ContextLoaderListener实现了ServletContextListener接口中的两个方法。其中,当ServletContext初始化后,contextInitialized(ServletContextEvent event)方法被调用,并开始执行initWebApplicationContext(event.getServletContext()方法。由于这个方法并没有在这个类中进行声明,因此我们追根溯源看一下其代码:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException("Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!");
    } else {
        servletContext.log("Initializing Spring root WebApplicationContext");
        Log logger = LogFactory.getLog(ContextLoader.class);
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }

        long startTime = System.currentTimeMillis();

        try {
            if (this.context == null) {
                this.context = this.createWebApplicationContext(servletContext);
            }

            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        ApplicationContext parent = this.loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }

                    this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }

            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            } else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }

            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
            }

            return this.context;
        } catch (Error | RuntimeException var8) {
            logger.error("Context initialization failed", var8);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
            throw var8;
        }
    }
}

很长的代码,大概就是一个创建IoC容器的过程。

至此,我们发现Spring容器在这个方法中被实例化了。接下来,我们总结一下整体思路:

  1. 当Servlet容器启动时,ServletContext对象被初始化
  2. 然后Servlet容器调用web.xml中注册的监听器的 public void contextInitialized(ServletContextEvent event)方法,而在监听器中,调用了this.initWebApplicationContext(event.getServletContext())方法,在这个方法中实例化了Spring IOC容器。即ApplicationContext对象。
  3. 因此,当ServletContext创建时我们可以创建applicationContext对象,当ServletContext销毁时,我们可以销毁applicationContext对象。这样applicationContext就和ServletContext“共生死了”。

7、全文总结

Servlet详细解析
Servlet详细解析

上一篇:springmvc 注解启动servlet


下一篇:ServletContext对象