深入理解 Servlet

在环境准备一节中给出了一个丐版的 Servlet 示例 IndexBlog,这一节我们仔细审视一下 IndexBlog 这个类。

Servlet 实例的创建细节

在写好 IndexBlog 这个类后,我们直接把项目部署到 Tomcat 服务器中,然后每当我们向 http://localhosyt:8080 发起 HTTP 请求,service 都会响应 HTTP 请求。而在示例代码中我们却完全没有创建 IndexBlog 这个类的实例,并且 service 也不是一个静态的方法,这说明 IndexBlog 这个类的实例化是由 Tomcat 完成的。Tomcat 中内有一个重要的组件,称作 Servlet,Servlet 对象的创建和销毁由该组件负责。并且,Servlet 作为响应用户请求的对象,其实例是单例的,即有且仅有一个 IndexBlog 实例对象被创建,并用来响应来自 /blog/test 的请求。

另外,我们并未在 IndexBlog 中提供构造函数,也就是说,在构造 IndexBlog 时,使用的是默认的构造方法,我们也可以像下面这样显式地提供一个构造方法:

public IndexBlog() {
    System.out.println("创建了 IndexBlog 实例。");
}

这里有一点必须明确,Tomcat 在创建 IndexBlog 实例时,并不是通过常规的调用构造函数的方式创建其对象的。我们设计的 Servlet,最终是编译成字节码提供给 Servlet 容器的,因而 Tomcat 容器要实例化该 Servlet,只能通过反射的方式完成,即调用 Class.newInstance 来实例化 Servlet,这也就解释了为何我们在为 Servlet 指定 URL 路径时采用的是注解的方式(当然,也可以在 web.xml 中进行配置,但并不是现在的主流做法)。因此,为 Servlet 提供含参构造函数是错误的做法,当然,提供无参构造函数还是被允许的。

Class.newInstance 方法
Class.newInstance 方法

另外,前面提到 Servlet 创建时是单例的,而单例则分为两种情况:懒汉式(预加载)和饿汉式(延迟加载)。@WebServlet 注解中,我们可以通过 loadOnStartup 来控制加载行为(缺省值为 -1),负数表示采用饿汉式加载,否则采用懒汉式加载,并且值越小,优先级越高,在启动 Tomcat 服务器时越先被创建,即启动顺序是 $0, 1, 2, \cdots, N$

@WebServlet(
    urlPatterns="/test",
    loadOnStartup = 0) // 懒汉式

initdestroy

IndexBlog 中,除了 service 方法,还有 initdestroy 两个方法,这两个方法分别用来初始化和销毁一些自定义的数据。init 方法传入了 ServletConfig 参数,用于告知我们 Servlet 创建时的配置,即 web.xml注解参数中提供的配置。

可以通过如下方式在注解中提供 ServletConfig 参数,参数以键值对的形式呈现:

@WebServlet(
    urlPatterns="/test",
    loadOnStartup = 0,
    initParams = {
        @WebInitParam(name="FOO", value="bar")
    }
)

web.xml 和注解参数中提供了相同的参数时,注解参数拥有更高的优先级,即注解参数会覆盖 web.xml 中的参数。

然后,可以在 init 中获取该参数:

@Override
public void init(ServletConfig config) throws ServletException {
    System.out.printf("参数 FOO = %s", config.getInitParameter("FOO"));
    System.out.println("IndexBlog 实例内的自定义数据初始化。");
}

init 方法主要用于初始化自定义的数据(我们当然可以把某些数据的初始化放到无参构造函数里,但这样做显得十分怪异),而 destroy 则相当于对应的析构函数(析构自定义的数据)。

getServletConfiggetServletInfo

前面提到,Servlet 在调用 init 初始化时会传入 ServletConfig 参数,对应地,有这样一个在运行时获取 Servlet 配置的方法 getServletConfig。我们可以在 init 中将传入的 ServletConfig 对象保存下来,在 getServletConfig 中返回。

private ServletConfig servletConfig;
public void init(ServletConfig config) throws ServletException {
    this.servletConfig = config;
    // ...
}
@Override
public ServletConfig getServletConfig() {
    return servletConfig;
}

getServletInfo 所做的事比较鸡肋,主要是返回与该 Servlet 相关的一些相关信息,没什么特别的用处。

service 中的线程安全问题

创建完单例的 Servlet 后,客户端对 /blog/test 请求都将转发给该 Servlet,由 service 受理后响应请求。Tomcat 服务器在响应对 /blog/test 的请求时是多线程的,但负责处理用户请求的是同一个 Servlet 对象,这就有可能导致线程安全问题,因此,当需要访问共享资源时,需要以同步方式进行。

private long count;
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    res.setContentType("text/html; charset=UTF-8");
    synchronized (this) { // 确保线程安全
        count++;
    }
    try (PrintWriter writer = res.getWriter()) {
        writer.printf("<html><body>您是第 %d 位访问者。</body></html>", count);
        writer.close();
    }
    System.out.printf("IndexBlog(地址:%s)响应用户请求。\n", this);
}

完整代码及运行效果

package fun.macrohard;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;

@WebServlet(
    urlPatterns="/test",
    loadOnStartup = 0,
    initParams = {
        @WebInitParam(name="FOO", value="bar")
    }
)
public class IndexBlog implements Servlet {
    private long count;
    private ServletConfig servletConfig;
    public IndexBlog() {
        System.out.println("创建 IndexBlog 实例。");
    }
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.count = 0;
        this.servletConfig = config;
        System.out.printf("参数 FOO = %s", config.getInitParameter("FOO"));
        System.out.println("IndexBlog 实例内的自定义数据初始化。");
    }
    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }
    @Override
    public String getServletInfo() {
        return "https://blog.macrohard.fun";
    }
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html; charset=UTF-8");
        synchronized (this) { // 确保线程安全
            count++;
        }
        try (PrintWriter writer = res.getWriter()) {
            writer.printf("<html><body>您是第 %d 位访问者。</body></html>", count);
            writer.close();
        }
        System.out.printf("IndexBlog(地址:%s)响应用户请求。\n", this);
    }
    @Override
    public void destroy() {
        System.out.println("IndexBlog 实例内的自定义数据销毁。");
    }
}
运行效果
运行效果