Servlet高级之监听器

Servlet高级之监听器

1.Listener

除了Servlet和Filter外,JavaEE的Servlet规范还提供了第三种组件:Listener。

Listener顾名思义就是监听器,有好几种Listener,其中最常用的是ServletContextListener,我们编写一个实现了ServletContextListener接口的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
@WebListener
public class AppListener implements ServletContextListener {
// 在此初始化WebApp,例如打开数据库连接池等:
public void contextInitialized(ServletContextEvent sce) {
System.out.println("WebApp initialized.");
}

// 在此清理WebApp,例如关闭数据库连接池等:
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("WebApp destroyed.");
}
}

任何标注为@WebListener,且实现了特定接口的类会被Web服务器自动初始化。上述AppListener实现了ServletContextListener接口,它会在整个Web应用程序初始化完成后,以及Web应用程序关闭后获得回调通知。我们可以把初始化数据库连接池等工作放到contextInitialized()回调方法中,把清理资源的工作放到contextDestroyed()回调方法中,因为Web服务器保证在contextInitialized()执行后,才会接受用户的HTTP请求。

很多第三方Web框架都会通过一个ServletContextListener接口初始化自己。

除了ServletContextListener外,还有几种Listener:

  • HttpSessionListener:监听HttpSession的创建和销毁事件;
  • ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
  • ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);

一个Web服务器可以运行一个或多个WebApp,对于每个WebApp,Web服务器都会为其创建一个全局唯一的ServletContext实例,我们在AppListener里面编写的两个回调方法实际上对应的就是ServletContext实例的创建和销毁:

1
2
3
public void contextInitialized(ServletContextEvent sce) {
System.out.println("WebApp initialized: ServletContext = " + sce.getServletContext());
}

ServletRequestHttpSession等很多对象也提供getServletContext()方法获取到同一个ServletContext实例。ServletContext实例最大的作用就是设置和共享全局信息。

此外,ServletContext还提供了动态添加Servlet、Filter、Listener等功能,它允许应用程序在运行期间动态添加一个组件,虽然这个功能不是很常用。

2.资源部署

对一个Web应用程序来说,除了Servlet、Filter这些逻辑组件,还需要JSP这样的视图文件,外加一堆静态资源文件,如CSS、JS等。

合理组织文件结构非常重要。我们以一个具体的Web应用程序为例:

image-20231129164038048

我们把所有的静态资源文件放入/static/目录,在开发阶段,有些Web服务器会自动为我们加一个专门负责处理静态文件的Servlet,但如果IndexServlet映射路径为/,会屏蔽掉处理静态文件的Servlet映射。因此,我们需要自己编写一个处理静态文件的FileServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@WebServlet(urlPatterns = "/static/*")
public class FileServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext ctx = req.getServletContext();
// RequestURI包含ContextPath,需要去掉:
String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
// 获取真实文件路径:
String filepath = ctx.getRealPath(urlPath);
if (filepath == null) {
// 无法获取到路径:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path path = Paths.get(filepath);
if (!path.toFile().isFile()) {
// 文件不存在:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 根据文件名猜测Content-Type:
String mime = Files.probeContentType(path);
if (mime == null) {
mime = "application/octet-stream";
}
resp.setContentType(mime);
// 读取文件并写入Response:
OutputStream output = resp.getOutputStream();
try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
input.transferTo(output);
}
output.flush();
}
}

这样一来,在开发阶段,我们就可以方便地高效开发。

类似Tomcat这样的Web服务器,运行的Web应用程序通常都是业务系统,因此,这类服务器也被称为应用服务器。==应用服务器并不擅长处理静态文件,也不适合直接暴露给用户。==通常,我们在生产环境部署时,总是使用类似Nginx这样的服务器充当反向代理静态服务器,只有动态请求才会放行给应用服务器,所以,部署架构如下:

image-20231129164151037

实现上述功能的Nginx配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
listen 80;

server_name www.local.liaoxuefeng.com;

# 静态文件根目录:
root /path/to/src/main/webapp;

access_log /var/log/nginx/webapp_access_log;
error_log /var/log/nginx/webapp_error_log;

# 处理静态文件请求:
location /static {
}

# 处理静态文件请求:
location /favicon.ico {
}

# 不允许请求/WEB-INF:
location /WEB-INF {
return 404;
}

# 其他请求转发给Tomcat:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

使用Nginx配合Tomcat服务器,可以充分发挥Nginx作为网关的优势,既可以高效处理静态文件,也可以把https、防火墙、限速、反爬虫等功能放到Nginx中,使得我们自己的WebApp能专注于业务逻辑。