[JAVA] Java Servlet 基础简介


本文转译自 Guide to Java Servlets,有部分增删和修改。有兴趣的可以直接阅读原文。

Java Servlet 是一些遵从 Java Servlet API 的 Java 类,这些 Java 类可以响应请求。尽管 Servlet 可以响应任意类型的请求,但是它们使用最广泛的是响应 Web 请求。 Java Servlet 也必须部署在 Servlet 容器才能使用。 Servlet 容器是一个运行在 Web 服务器上的组件,它负责管理 Servlets 的生命周期,接收客户端请求,调用相应的 Servlet 方法,并将响应发送回客户端。常见的 Servlet 容器有 TomcatJettyGlassFish 等。

虽然很多开发者都使用 Java Server Pages(JSP)Java Server Faces(JSF)等 Servlet 框架,但是这些技术都要在幕后通过 Servlet 容器把页面编译为 Java Servlet。所以说了解 Servlet 对于理解 Web 开发中的基本概念和原理是很有帮助的。

The First Servlet

我们的第一个 Servlet 只有简单的几行代码,目的是为了让你了解如何使用它:

public class MyFirstServlet extends HttpServlet {
 
  private static final long serialVersionUID = -1915463532411657451L;
 
  @Override
  protected void doGet(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
        
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();
    try {
      // Write some content
      out.println("<html>");
      out.println("<head>");
      out.println("<title>MyFirstServlet</title>");
      out.println("</head>");
      out.println("<body>");
      out.println("<h2>Hello World.</h2>");
      out.println("</body>");
      out.println("</html>");
    } finally {
      out.close();
    }
  }
   
  @Override
  protected void doPost(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
    //Do some other work
  }
 
  @Override
  public String getServletInfo() {
    return "MyFirstServlet";
  }
}

为了在 Web 容器里注册上面的 Servlet,还要创建一个 web.xml 文件作为入口。

<?xml version="1.0"?>
<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://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">

...
   
  <servlet>
    <servlet-name>MyFirstServlet</servlet-name>
    <servlet-class>org.demo.MyFirstServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>MyFirstServlet</servlet-name>
    <url-pattern>/MyFirstServlet</url-pattern>
  </servlet-mapping>

...
   
</web-app>

上面这个 Servlet 有以下几点需要了解和注意:

Servlet Life Cycle

当你在加载并使用一个 Servlet 时,从初始化到销毁这个 Servlet 期间会发生一系列的事件。这些事件叫做 Servlet 的生命周期事件(或方法)。

Servlet 生命周期的三个核心方法分别是 init()service()destroy()。每个 Servlet 都会实现这些方法,并且在特定的时间调用它们。

init():在 Servlet 生命周期的初始化阶段,Web 容器通过调用 init() 方法来初始化 Servlet 实例,并且可以传递一个实现 javax.servlet.ServletConfig 接口的对象给它。这个配置对象使 Servlet 能够读取在 Web 应用的 web.xml 文件里定义的 name-value 初始参数。这个方法在整个实例的生命周期里只调用一次。

init() 方法大概长这样:

public void init() throws ServletException {
	//custom initialization code
}

service():初始化后,Servlet 实例就可以处理客户端请求了。Web 容器调用 Servlet 的 service() 方法来处理每一个请求。service() 方法定义了能够处理的请求类型并且调用适当方法来处理这些请求。编写 Servlet 的开发者必须为这些方法提供实现。如果收到了一个 Servlet 没实现的请求,那么父类的方法就会被调用并且通常会给请求方返回一个错误信息。

一般情况下不用重写这个方法:

protected void service(HttpServletRequest req, HttpServletResponse resp)  throws ServletException, IOException { 
    ... 
}

destroy():最后,Web 容器调用 destroy() 方法来结束 Servlet。如果你想在 Servlet 的生命周期内关闭或者销毁一些文件系统或者网络资源,你可以调用这个方法来实现。destroy() 方法和 init() 方法一样,在整个生命周期里只能调用一次。

public void destroy() {
	//
}

在大多数情况下,都不需要在你的 Servlet 里重写这些方法。

@WebServlet Annotation

如果你不喜欢使用 xml 配置而喜欢注解的话,Servlets API 同样提供了一些注解接口给你。你可以像下面的例子一样使用 @WebServlet 注解并且不需要在 web.xml 里为 Servlet 注册任何信息,容器会将你的 Servlet 自动注册到运行环境。

@WebServlet(name = "MyFirstServlet", urlPatterns = {"/MyFirstServlet"})
public class MyFirstServlet extends HttpServlet {
  private static final long serialVersionUID = -1915463532411657451L;
 
  @Override
  protected void doGet(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
    //Do some work
  }
   
  @Override
  protected void doPost(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
    //Do some other work
  }
}

Packaging and Deploying Servlet into Tomcat Server

如果你在使用 IDE(比如 IntelliJ IDEA),可以创建一个 webapp + Maven 项目,导入依赖(主要就是 javax.servlet-api)并内嵌 Tomcat ,然后创建一个运行配置(如下图),就可以一键运行调试。


如果你没在使用 IDE,那么你需要做一些额外的工作。比如,使用命令行工具编译应用,使用 ANT 去生成 war 文件等等。但我相信,现在的开发者都在使用 IDE 来开发,所以我就不在这方面浪费时间了。

运行上面的 Servlet ,在浏览器输入”http://localhost:8080/MyFirstServlet”:


Writing to a Servlet Response

Java Servlet 如此有用的原因之一是 Servlet 能动态显示网页内容。这些内容可以从服务器本身、另外一个网站、或者许多其他网络可以访问的资源里获取。Servlet 不是静态网页,它们是动态的。可以说这是它们最大的优势。

还是这个 Servlet 例子,这个 Servlet 会显示当前日期时间和一些自定义的信息:

@WebServlet(name = "CalendarServlet", urlPatterns = {"/CalendarServlet"})
public class CalendarServlet extends HttpServlet {
  private static final long serialVersionUID = -1915463532411657451L;
 
  @Override
  protected void doGet(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {

    Map<String,String> data = getData();
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();
    try {
      // Write some content
      out.println("<html>");
      out.println("<head>");
      out.println("<title>CalendarServlet</title>");
      out.println("</head>");
      out.println("<body>");
      out.println("<h2>hello " + data.get("username") + ", " + data.get("message") + "</h2>");
      out.println("<h2>The time right now is : " + new Date() + "</h2>");
      out.println("</body>");
      out.println("</html>");
    } finally {
      out.close();
    }
  }
   
  //This method will access some external system as database to get user name, and his personalized message
  private Map<String, String> getData() {
    Map<String, String> data = new HashMap<String, String>();
    data.put("username", "yven");
    data.put("message",  "Welcome to my first servlet!");
    return data;
  }
}

调试运行,并在浏览器输入”http://localhost:8080/CalendarServlet”:


Handling Servlet Requests and Responses

Servlet 可以轻松创建一个基于请求和响应生命周期的 web 应用。它们能够提供 HTTP 响应并且可以使用同一段代码来处理业务逻辑。处理业务逻辑的能力使 Servlet 比标准的 HTML 代码更强大。

实际应用中,一个 HTML 网页表单包含了要发送给 Servlet 的参数。Servlet 会以某种方式来处理这些参数并且返回一个客户端能够识别的响应。在对象是 HttpServlet 的情况下,客户端是 web 浏览器,响应是 web 页面。<form> 标签中的 action 属性指定了使用哪个 Servlet 来处理表单里的参数值。

获取请求参数,可以调用 HttpServletRequest 对象的 getParameter() 方法,方法参数是你要获取的参数值的 id。

String value1 = httpServletRequest.getParameter("param1");
String value1 = httpServletRequest.getParameter("param2");

获取到的参数值会在需要的时候处理。对客户端的响应和我们上面部分讨论的一样。我们使用 HttpServletResponse 对象给客户端发送响应。

一个基本的请求和响应:

@Override
protected void doGet(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException {
   
  response.setContentType("text/html;charset=UTF-8");
  PrintWriter out = response.getWriter();
  String username = request.getParameter("username");
  String password = request.getParameter("password");
  boolean success = validateUser(username, password);
   
  try {
    // Write some content
    out.println("<html>");
    out.println("<head>");
    out.println("<title>LoginServlet</title>");
    out.println("</head>");
    out.println("<body>");
 
    if(success) {
      out.println("<h2>Welcome Friend</h2>");
    } else {
      out.println("<h2>Validate your self again.</h2>");
    }

    out.println("</body>");
    out.println("</html>");
  } finally {
    out.close();
  }
}

通常使用 HttpServletResponse 里的 PrintWriter 对象给客户端发送内容。写入这个对象的内容是通过输出流返回给客户端。

Listening for Servlet Container Events

有时候,知道容器里某些事件发生的时间是很有用的。这个概念适用于很多情况,但最常见的是用于在启动时初始化应用程序或在关闭应用程序后的清理工作。可以在应用里注册一个监听器来指示应用什么时候开启或者关闭。因此,通过监听这些事件,Servlet 可以在一些事件发生时执行相应的动作。

创建基于容器事件执行动作的监听器,就必须创建一个实现 ServletContextListener 接口的类。这个类必须实现的方法有 contextInitialized()contextDestroyed()。这两个方法都需要 ServletContextEvent 作为参数,在每次初始化或者关闭容器的时候都会被自动调用。

可以通过以下几种方式注册监听器:

值得注意的是,ServletContextListener 不是 Servlet API 里唯一的监听器。这里还有一些其他的监听器,比如:

根据需要来选择实现你的监视器。比如,每当创建或销毁一个用户 session 时,HttpSessionListener 会发出通知。

Passing Parameters to Servlet Initialization

现在的大多数应用都需要设置一些在应用或控制器启动时可以传递的配置参数。Servlet 也可以接受初始化参数,并在处理请求之前使用它们来构建配置参数。

你也可以在 Servlet 里对配置值进行硬编码。但是这样做的话,在每次 Servlet 发生改动时都需要再次重新编译整个应用。

<web-app>
    <servlet>
        <servlet-name>SimpleServlet</servlet-name>
        <servlet-class>org.demo.SimpleServlet</servlet-class>
     
        <!-- Servlet init param -->
        <init-param>
            <param-name>name</param-name>
            <param-value>value</param-value>
        </init-param>
 
    </servlet>
 
</web-app>

设置后,你就可以调用 getServletConfig.getInitParameter() 并传递参数名给该方法来使用参数,就像这样:

String value = getServletConfig().getInitParameter("name");

Servlet Filters

Web 过滤器用于预处理请求和在访问给定 URL 时调用某些功能。与直接调用存在于指定 URL 的 Servlet 不同,任何包含相同 URL 模式的过滤器都会在 Servlet 之前被调用。这在许多情况下都很有帮助,也许最有用的是执行日志记录、身份验证或其他在后台无需用户交互的服务。

过滤器必须要实现 javax.servlet.Filter 接口。这个接口包含了 init()descriptor()doFilter() 这些方法。init()destroy() 方法会被容器调用。 doFilter() 方法用来在过滤器类里实现业务逻辑。如果你想把过滤器组成过滤链或者给定的 URL 模式存在多个过滤器,它们就会根据 web.xml 里的配置顺序被调用。

在 web.xml 里配置过滤器,使用 <filter><filter-mapping> 标签以及相关的子元素标签。

<filter>
    <filter-name>LoggingFilter</filter-name>
    <filter-class>LoggingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LogingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

也可以使用 @WebFilter 注解来为特定的 Servlet 配置过滤器。

Downloading a Binary File using Servlet

下载文件的功能在 Web 应用中很常见。为了下载一个文件,Servlet 必须提供一个和下载文件类型匹配的响应类型。同样,必须在响应头里指出该响应包含附件。

String mimeType = context.getMimeType( fileToDownload );
response.setContentType( mimeType != null ? mimeType : "text/plain" );
response.setHeader( "Content-Disposition", "attachment; filename=\"" + fileToDownload + "\"" );

通过调用 ServletContext.getResourceAsStream() 方法并传递文件路径来获取要下载的文件的引用。这个方法会返回一个输入流对象,我们可以用这个对象来读取文件内容。当读取文件时,我们创建一个字节缓存区,从文件里获取数据块。最后读取文件内容并且把它们复制到输出流,使用 while 循环读取 InputStream 中的数据,直到处理完所有内容就会退出循环。之后 ServletOutputStream 对象的 flush() 方法就会被调用清空内容和释放资源:

private void downloadFile(HttpServletRequest request, 
      HttpServletResponse response, String fileToDownload) throws IOException {

    final int BYTES = 1024;
    int length = 0;
     
    ServletOutputStream outStream = response.getOutputStream();
    ServletContext context = getServletConfig().getServletContext();
 
    String mimeType = context.getMimeType(fileToDownload);
    response.setContentType(mimeType != null ? mimeType : "text/plain");
    response.setHeader("Content-Disposition", "attachment; filename=\"" + fileToDownload + "\"");
    InputStream in = context.getResourceAsStream("/" + fileToDownload);
     
    byte[] bbuf = new byte[BYTES];
 
    while((in != null) && ((length = in.read(bbuf)) != -1)) {
      outStream.write(bbuf, 0, length);
    }
 
    outStream.flush();
    outStream.close();
  }

Forward Request to Another Servlet

有时候,你的应用需要把一个 Servlet 要处理的请求转让给另一个 Servlet 来处理。而且,转让请求时不能发生重定向。即浏览器地址栏上的 URL 不会改变。

ServletContext 里已经内置了实现上面需求的方法。当你获取了 ServletContext 的引用,你就可以调用 getRequestDispatcher() 方法去获取用来转发请求的 RequestDispatcher 对象。

当调用 getRequestDispatcher() 方法时,需要传递包含 Servlet 名的字符串,这个 Servlet 就是你用来处理转让请求的 Servlet。获取 RequestDispatcher 对象后,通过传递 HttpServletRequestHttpServletResponse 对象给它来调用转发方法。然后对请求进行转发。

RequestDispatcher rd = servletContext.getRequestDispatcher("/NextServlet");
rd.forward(request, response);

Redirect Request to Another Servlet

有些情况下,我们又希望访问一些特定的 URL 时发生重定向,即浏览器地址栏的 URL 变成另一个。

这时候就可以调用 HttpServletResponse 对象的 sendRedirect() 方法。

httpServletResponse.sendRedirect("/anotherURL");

这个简单的重定向,与 servlet chaining 相反,不需要传递目标地址的 HttpRequest 对象。

Read/Write Cookies using Servlets

很多应用都想在客户端机器里保存用户当前的浏览历史。目的是当用户再次访问时,能够从上次离开的地方继续。通常使用 Cookies 来实现。你可以把它看作是保存在客户端里的键值对基本数据。当使用浏览器打开应用时,就可以对这些数据进行读写。

创建 cookie,需要实例化一个 javax.servlet.http.Cookie 对象并且为它分配键值。实例化后,可以设置属性来配置 cookie。在这个例子里,我们使用 setMaxAge()setHttpOnly() 方法来设置 cookie 的生命周期和防范客户端脚本。

Cookie cookie = new Cookie("sessionId","123456");
cookie.setHttpOnly(true);
cookie.setMaxAge(-30);
response.addCookie(cookie);

这里的 response 是传递给 doXXX() 方法的 HttpServletResponse 实例。

读取服务端的 cookie 信息:

Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
    //cookie.getName();
    //cookie.getValue()
}


Happy Learning !!