深入理解 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 方法另外,前面提到 Servlet 创建时是单例的,而单例则分为两种情况:懒汉式(预加载)和饿汉式(延迟加载)。在 @WebServlet 注解中,我们可以通过 loadOnStartup 来控制加载行为(缺省值为 -1),负数表示采用饿汉式加载,否则采用懒汉式加载,并且值越小,优先级越高,在启动 Tomcat 服务器时越先被创建,即启动顺序是
$0, 1, 2, \cdots, N$
。
@WebServlet(
urlPatterns="/test",
loadOnStartup = 0) // 懒汉式
init 和 destroy
在 IndexBlog 中,除了 service 方法,还有 init 和 destroy 两个方法,这两个方法分别用来初始化和销毁一些自定义的数据。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 则相当于对应的析构函数(析构自定义的数据)。
getServletConfig 与 getServletInfo
前面提到,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 实例内的自定义数据销毁。");
}
}