Java
Java Web基础

Servlet的使用

简介:Servlet是Sun提供的一种动态Web资源开发技术,本质上就是一段Java小程序,可以将Servlet加入到Servlet容器中运行。Servlet 通常通过 HTTP(超文本传输协议)接收和响应来自 Web 客户端的请求。

1. Servlet简介

Servlet是Sun提供的一种动态Web资源开发技术,本质上就是一段Java小程序,可以将Servlet加入到Servlet容器中运行。Servlet 通常通过 HTTP(超文本传输协议)接收和响应来自 Web 客户端的请求。

  • Servlet容器:能够运行Servlet的环境就叫做Servlet容器。常见的即Tomcat。
  • Web容器:能够运行Web应用的环境就叫做Web容器。常见的即Tomcat。

Servlet没有main()方法。它们受控于另一个Java应用(例如Tomcat),这个Java应用称为Sevlet容器,Tomcat就是这样一个容器。通常我们把Tomcat也叫做Servlet容器。Servlet默认是以多线程模式执行的。

注:JSP是为了方便写HTML代码和Java代码,它的本身其实还是Servlet。

2. Servet简单示例

这里编写一个简单的Servlet程序作为演示。首先编写一个TestServlet类,实现Servlet接口,代码如下:

Java
  • package com.coderap.servlet;
  • import javax.servlet.*;
  • import java.io.IOException;
  • public class TestServlet implements Servlet {
  • private ServletConfig servletConfig;
  • @Override
  • public void init(ServletConfig servletConfig) throws ServletException {
  • this.servletConfig = servletConfig;
  • System.out.println("TestServlet init invoked");
  • }
  • @Override
  • public ServletConfig getServletConfig() {
  • return this.servletConfig;
  • }
  • @Override
  • public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
  • System.out.println("TestServlet service invoked + " + servletRequest + " + " + servletResponse);
  • servletResponse.getOutputStream().print("<p>Hello</p>");
  • }
  • @Override
  • public String getServletInfo() {
  • return null;
  • }
  • @Override
  • public void destroy() {
  • System.out.println("TestServlet destroy invoked");
  • }
  • }

在上面的TestServlet类中,重写了Servlet接口中的一些方法,这里最重要的是init、service和destroy三个方法,分别代表了Servlet的生命周期阶段,后面将详细介绍。

如果想要让上面编写的Servlet处理浏览器的请求,还需要修改web.xml文件,给servlet配置可访问的URI地址进行映射:

  • <?xml version="1.0" encoding="UTF-8"?>
  • <web-app version="2.5"
  • xmlns="http://java.sun.com/xml/ns/javaee"
  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  • xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  • http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  • <servlet>
  • <servlet-name>TestServlet</servlet-name>
  • <servlet-class>com.coderap.servlet.TestServlet</servlet-class>
  • </servlet>
  • <servlet-mapping>
  • <servlet-name>TestServlet</servlet-name>
  • <url-pattern>/*</url-pattern>
  • </servlet-mapping>
  • </web-app>

在上面的XML代码中,定义了一个名为TestServlet的servlet结点,同时将其与com.coderap.servlet.TestServlet类进行绑定,然后将所有的请求都映射到这个Servlet上。编写完后,将代码部署到Tomcat(默认为本地8080端口)上运行,并请求地址http://localhost:8080/WebTest/,可以得到下面的打印:

  • TestServlet init invoked
  • TestServlet service invoked + org.apache.catalina.connector.RequestFacade@63a3793c + org.apache.catalina.connector.ResponseFacade@4c159cc3

可以看到这里调用了TestServlet的两个方法。当关闭Tomcat容器程序时,会打印TestServlet destroy invoked。这些是Servlet的生命周期方法,接下来将详细说明。

3. Servlet生命周期

Servlet的生命周期定义了一个Servlet如何被加载、初始化,以及它怎样接收请求、响应请求、提供服务。生命周期主要阶段如下:

  1. 通常情况下,服务器会在Servlet第一次被调用时创建该Servlet类的实例对象(Servlet出生),创建出对象后立即调用init(ServletConfig servletConfig)方法做初始化操作;
  2. 一旦被创建出来,该Servlet实例就会驻留在内存中,为后续对这个Servlet的请求做服务,每次对这个Servlet的访问都会导致Servlet中service(ServletRequest servletRequest, ServletResponse servletResponse)方法执行;
  3. 当web应用被移除容器或者关闭服务器时,随着web应用的销毁,Servlet也会被销毁(Servlet死亡)。在销毁之前服务器会调用Servlet的destroy()方法做一些善后的工作。

有三个方法代表了Servlet的生命周期:

  1. init(ServletConfig servletConfig)方法,负责初始化Servlet对象。
  2. service(ServletRequest servletRequest, ServletResponse servletResponse)方法,负责响应客户的请求(如HTTPServlet中负责调用doGet或doPost等方法)。
  3. destroy()方法,当Servlet对象退出生命周期时,负责释放占用的资源。

注:在Servlet的整个生命周期内,Servlet的init(ServletConfig servletConfig)方法只有在Servlet被创建时被调用一次,每次对这个Servlet的访问都会导致Servlet中service(ServletRequest servletRequest, ServletResponse servletResponse)方法执行。假如现在浏览器连续访问Servlet 10次,内存中只有一个Sevlet对象。Servlet对象由服务器创建(创建一次),request和response由Servlet容器创建10次。因此Servlet其实存在线程安全的问题,为了避免线程之间的数据污染,在所有当前只提供给当前线程使用的上下文中,不要写全局变量,而要写局部变量。

如果想让相应的Servlet在服务器启动时就创建,只需要在web.xml文件中给相应的Servlet配置load-on-startup的值大于0即可:

  • <servlet>
  • <servlet-name>TestServlet</servlet-name>
  • <servlet-class>com.coderap.servlet.TestServlet</servlet-class>
  • <load-on-startup>1</load-on-startup>
  • </servlet>

考虑到生命周期的各个阶段,Servlet的运行时序图如下:

1.Servlet运行时序图.png

4. Servlet的三种创建方式

Servlet-api.jar包提供了三个基本接口或类用于创建Servlet:

  1. 实现javax.servlet.Servlet接口。
  2. 继承javax.servet.GenericServlet类(适配器模式)。
  3. 继承javax.servlet.http.HttpServlet类(模板方法设计模式),开发中常用的方式。

其中,javax.servlet.Servlet接口是顶层接口,javax.servet.GenericServlet类是一个抽象类,实现了javax.servlet.Servlet接口,而javax.servlet.http.HttpServlet类也是一个抽象类,它继承了javax.servet.GenericServlet类,关系如下:

  • javax.servlet.Servlet
  • javax.servet.GenericServlet
  • javax.servlet.http.HttpServlet

在之前的例子中,是通过实现javax.servlet.Servlet接口来创建我们自己的Servlet的。我们也可以通过继承javax.servet.GenericServlet类和javax.servlet.http.HttpServlet类来创建自定义的Servlet:

  • 继承javax.servet.GenericServlet类:
Java
  • package com.coderap.servlet;
  • import javax.servlet.*;
  • import java.io.IOException;
  • public class TestServlet extends GenericServlet {
  • @Override
  • public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
  • System.out.println("TestServlet service invoked + " + servletRequest + " + " + servletResponse);
  • servletResponse.getOutputStream().print("<p>Hello</p>");
  • }
  • }

注:在重写GenericServlet类的init(ServletConfig servletConfig)方法时,一定要调用super的相关方法,否则在使用GenericServlet的某些方法获取与servletConfig相关的属性时会报NullPointerException异常。

GenericServlet类已经实现了Servlet中的大部分方法,我们只需要实现service(ServletRequest servletRequest, ServletResponse servletResponse)方法即可。如果某些地方会用到ServletContext、ServletConfig等属性,GenericServlet类提供了相应的getter方法方便开发者获取:

Java
  • // 获取ServletConfig
  • public ServletConfig getServletConfig() {
  • return config;
  • }
  • // 获取ServletContext
  • public ServletContext getServletContext() {
  • ServletConfig sc = getServletConfig();
  • if (sc == null) {
  • throw new IllegalStateException(
  • lStrings.getString("err.servlet_config_not_initialized"));
  • }
  • return sc.getServletContext();
  • }
  • // 获取ServletInfo
  • public String getServletInfo() {
  • return "";
  • }
  • // 获取ServletName
  • public String getServletName() {
  • ServletConfig sc = getServletConfig();
  • if (sc == null) {
  • throw new IllegalStateException(
  • lStrings.getString("err.servlet_config_not_initialized"));
  • }
  • return sc.getServletName();
  • }
  • 继承javax.servlet.http.HttpServlet类:
Java
  • package com.coderap.servlet;
  • import javax.servlet.ServletException;
  • import javax.servlet.http.HttpServlet;
  • import javax.servlet.http.HttpServletRequest;
  • import javax.servlet.http.HttpServletResponse;
  • import java.io.IOException;
  • public class TestServlet extends HttpServlet {
  • @Override
  • protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
  • System.out.println("TestServlet service invoked + " + httpServletRequest + " + " + httpServletResponse);
  • httpServletResponse.getOutputStream().print("<p>Hello</p>");
  • }
  • @Override
  • protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
  • this.doGet(httpServletRequest, httpServletResponse);
  • }
  • }

注1:在重写HttpServlet类的init(ServletConfig servletConfig)方法时,一定要调用super的相关方法,否则在使用GenericServlet的某些方法获取与servletConfig相关的属性时会报NullPointerException异常。
注2:需要注意的是,如果覆盖doPost或者doGet等方法的时候要去掉调用super的方法,否则请求会被拦截。

继承HttpServlet来实现自定义的Servlet是最常用的方式,这是因为HttpServlet在通用Servlet的基础上基于HTTP协议进行了进一步的强化。在大部分的HTTP请求的处理中,我们可能只需要关注Get和Post请求即可,因此可以选择只重写相应的方法来处理相应的请求(如果需要处理其他类型的请求,重写相应的方法进行处理即可)。观察以下HttpServlet源码中对请求的转发处理:

Java
  • private static final String METHOD_DELETE = "DELETE";
  • private static final String METHOD_HEAD = "HEAD";
  • private static final String METHOD_GET = "GET";
  • private static final String METHOD_OPTIONS = "OPTIONS";
  • private static final String METHOD_POST = "POST";
  • private static final String METHOD_PUT = "PUT";
  • private static final String METHOD_TRACE = "TRACE";
  • ...
  • 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 doesn't support if-modified-since, no reason
  • // to go through further expensive logic
  • doGet(req, resp);
  • } else {
  • long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
  • if (ifModifiedSince < (lastModified / 1000 * 1000)) {
  • // If the servlet mod time is later, call doGet()
  • // Round down to the nearest second for a proper compare
  • // A ifModifiedSince of -1 will always be less
  • maybeSetLastModified(resp, lastModified);
  • doGet(req, resp);
  • } else {
  • resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
  • }
  • }
  • } else if (method.equals(METHOD_HEAD)) {
  • long lastModified = getLastModified(req);
  • maybeSetLastModified(resp, lastModified);
  • 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 {
  • //
  • // Note that this means NO servlet supports whatever
  • // method was requested, anywhere on this server.
  • //
  • String errMsg = lStrings.getString("http.method_not_implemented");
  • Object[] errArgs = new Object[1];
  • errArgs[0] = method;
  • errMsg = MessageFormat.format(errMsg, errArgs);
  • resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
  • }
  • }

5. ServletConfig的使用

ServletConfig可以通过两种方式来获取:

  • 方式1,通过记录init方法传入的ServletConfig来使用:
  • private ServletConfig servletConfig;
  • @Override
  • public void init(ServletConfig servletConfig) throws ServletException {
  • this.servletConfig = servletConfig;
  • }
  • 方式2,通过父类提供的getServletConfig()方法获取ServletConfig。

5.1. 获取Servlet配置信息

通过ServletConfig可以获取Servlet配置参数信息,如this.getServletConfig().getInitParameter("encoding");可以获取web.xml中定义Servlet传入的init参数,如编写下面的TestServlet配置,上面的代码将在控制台输出utf-8

  • <servlet>
  • <servlet-name>TestServlet</servlet-name>
  • <servlet-class>com.coderap.servlet.TestServlet</servlet-class>
  • <init-param>
  • <param-name>encoding</param-name>
  • <param-value>utf-8</param-value>
  • </init-param>
  • <load-on-startup>1</load-on-startup>
  • </servlet>

5.2. 获取ServletContext对象

通过this.servletConfig.getServletContext()的方式可以获取ServletContext对象。ServletContext代表的是整个应用,一个应用只有一个ServletContext对象,它是一个单实例对象。它的常用用处有以下几类:

  1. 获取全局配置信息。

全局配置信息指的是在web.xml文件中用以下标签定义的键值对配置:

  • <context-param>
  • <param-name>encoding</param-name>
  • <param-value>utf-8</param-value>
  • </context-param>

我们可以通过ServletContext来获取:

  • this.servletConfig.getServletContext().getInitParameter("encoding");
  1. 用于获取资源路径。如this.getServletContext().getRealPath("/WEB-INF/web.xml"),以这种方式可以获取到项目部署后的相关目录,记得一定要以/开头,表示从项目部署根路径开始计算。

  2. 可以用于让一定范围内(当前应用)的多个Servlet共享数据。通过下面的方法可以管理ServletContext中的数据,这些数据在多个Servlet中是共享的:

  • void setAttribute(String name, object value); // 向ServletContext对象的Map中添加数据
  • Object getAttribute(String name); // 从ServletContext对象的Map中取数据
  • void rmoveAttribute(String name); // 根据name去移除数据
  1. 实现Servlet的转发

通过ServletContext可以实现从当前Servlet将请求转发到其他的Servlet,如下面的代码:

  • RequestDispatcher requestDispatcher = this.getServletContext().getRequestDispatcher("/TestServletForward");
  • requestDispatcher.forward(httpServletRequest, httpServletResponse);

上面的代码会将当前的请求转发到/TestServletForward路径请求下。