如何在JavaEE中挂接BeginRequest和EndRequest



短版本

在JavaEE中,我想知道什么时候:

  • 请求启动时
  • 以及当请求结束时

并且能够检查请求和响应对象。

长版本

在ASP.net世界中,如果您想知道请求何时开始和结束,可以编写IHttpModule:

public class ExampleModuleForThisQuestion : IHttpModule
{
}

然后注册您的"模块">在web xml配置文件中:

web.config

<system.webServer>
<modules>
<add name="DoesntMatter" type="ExampleModuleForThisQuestion "/>
</modules>
</system.webServer> 

在您的模块中,您可以注册回调处理程序:

  • BeginRequest事件
  • 结束请求事件

然后web服务器基础结构会调用Init方法。这是您注册的机会,您希望在请求开始和结束时接收通知:

public class ExampleModuleForThisQuestion : IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(beginRequest); //register the "BeginRequet" event
application.EndRequest += new EventHandler(endRequest); //register the "EndRequest" event
}
}

现在,当请求开始时,我们有了回调:

private void beginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
//Record the time the request started
application.Context.Items["RequestStartTime"] = DateTime.Now;
//We can even access the Request and Response objects
application.ContenxtLog(application.Context.Request.Headers["User-Agent"]);
}

请求结束时,我们有回调:

private void endRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
//We can even access the Request and Response objects
//Get the response status code (e.g. 418 I'm a teapot)
int statusCode = application.Context.Response.StatusCode;
//Get the request method (e.g. GET, POST, BREW)
String method = application.context.Request.RequestType;
//Get the path from the request (e.g. /ViewCustomer)
String path = application.context.Request.AppRelativeCurrentExecutionFilePath'

//Get when the request started - that we recorded during "Begin Request"
DateTime requestStartTime = (DateTime)application.Context.Items["RequestStartTime"];
//And we can modify the response
if ((DateTime.Now - requestStartTime).TotalSeconds = 17)
application.Context.Response.StatusCode = 451;
}

现在如何在JavaEE中做到这一点

问题是:在Java web服务器世界中,IHttpModule在道德上的等价物是什么?

有人说这是一个ServletRequestListener:

用于接收进入和离开web应用程序范围的请求的通知事件的接口。

servlet请求被定义为当它将要进入web应用程序的第一个servlet或筛选器时进入该应用程序的范围,以及当它退出链中的最后一个servlet或第一个筛选器时超出范围。

其他人坚持认为你想要一个"过滤器">";过滤链">

有人说;过滤器"提供了ServletRequestListener所做的一切,但过滤器也可以配置为仅针对特定URL运行。

docs.oracle.com上的这个随机页面没有提到"过滤器">,而我需要一个ServletContextListener,因为这是接收每个请求的通知的唯一方法。但他们有一个表似乎与此相矛盾,我想要一个ServletRequestEvent:

侦听器接口和事件类javax.servlet.ServletContextListenerServletContextEvent添加、删除或替换的属性javax.servlet.http.HttpSessionListenerjavax.servlet.http.HttpSessionActivationListenerHttpSessionEvent的创建、无效、激活、钝化和超时添加、删除或替换的属性javax.servlet.ServletRequestListenerServletRequestEvent添加、删除或替换的属性
对象事件
Web上下文初始化和销毁
Web上下文javax.servlet.ServletContextAttributeListenerServletContextAttributeEvent
会话
会话javax.servlet.http.HttpSessionAttributeListenerHttpSessionBindingEvent
请求web组件已开始处理servlet请求
请求javax.servlet.ServletRequestAttributeListenerServletRequestAttributeEvent

ServletRequestListener

我想知道什么时候:请求何时开始,请求何时结束

按照问题中的建议使用ServletRequestListener。自从Servlet规范版本2.4以来,这是Servlet API中一个长期存在的特性。

  • requestInitialized方法是您了解何时开始处理请求的挂钩
  • requestDestroyed方法是处理请求结束时的挂钩

示例代码

这是我写的一个小演示。

package work.basil.example.demo;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Instant;
@WebListener
public class RequestAnnouncer implements ServletRequestListener
{
@Override
public void requestInitialized ( ServletRequestEvent sre )
{
ServletRequestListener.super.requestInitialized( sre );
ServletRequest sr = sre.getServletRequest();
HttpServletRequest request = ( HttpServletRequest ) sr;
String url = request.getRequestURL() + ( request.getQueryString() == null ? "" : "?" + request.getQueryString() );
System.out.println( "INFO - Request initialized in thread " + Thread.currentThread().getId() + " at " + Instant.now() + " | " + url );
}
@Override
public void requestDestroyed ( ServletRequestEvent sre )
{
ServletRequestListener.super.requestDestroyed( sre );
ServletRequest sr = sre.getServletRequest();
HttpServletRequest request = ( HttpServletRequest ) sr;
String url = request.getRequestURL() + ( request.getQueryString() == null ? "" : "?" + request.getQueryString() );
System.out.println( "INFO - Request destroyed in thread " + Thread.currentThread().getId() + " at " + Instant.now() + " | " + url );
}
}

@WebListener

请注意注释@WebListener。使用此注释,您可以跳过编辑web XML配置文件。@WebListener在运行时充当一个标志,因此Servlet容器将在web应用程序生命周期的适当时刻自动检测、实例化和执行此类。这种现代化的便利性被添加到Servlet规范3.0中。

jakarta.*包命名

还要注意import语句中的jakarta.*包命名。Java EE现在被称为Jakarta EE,几年前Oracle公司将责任移交给Eclipse基金会。Jakarta Servlet规范版本4和5(此处使用)是等效的,只是包名称从javax.*更改为Jakarta.*.

顺便说一句,Jakarta Servlet规范版本6作为Jakarta EE 10的一部分于今年发布,这是多年来的第一次重大更改。请参阅文章,关于Jakarta Servlet 6.0 API版本的五大须知

运行示例

当在Microsoft Edge浏览器版本103.01264.62(官方版本)(arm64)中运行时,适用于Apache Tomcat 2022.2 Beta上的macOS Monterey 12.4,在MacBook Pro(16英寸,2021)Apple M1 Pro上的Adoptium Temurin for Java 18.0.1+10,URLhttp://localhost:8080/demo_war_exploded/hello-servlet会产生以下输出。

INFO - Request initialized in thread 23 at 2022-07-15T20:55:21.060303Z | http://localhost:8080/demo_war_exploded/hello-servlet
INFO - Request destroyed in thread 23 at 2022-07-15T20:55:21.066361Z | http://localhost:8080/demo_war_exploded/hello-servlet
INFO - Request initialized in thread 26 at 2022-07-15T20:55:21.151810Z | http://localhost:8080/demo_war_exploded/hello-servlet
INFO - Request destroyed in thread 26 at 2022-07-15T20:55:21.152086Z | http://localhost:8080/demo_war_exploded/hello-servlet

为什么对于一个请求,我们会得到四行而不是两行?好吧,查看访问日志显示,该浏览器在IPv4和IPv6上都发送了请求。浏览器只向/发送了一个请求,但我不知道为什么。

127.0.0.1 - - [15/Jul/2022:13:55:21 -0700] "GET /demo_war_exploded/hello-servlet HTTP/1.1" 200 306
127.0.0.1 - - [15/Jul/2022:13:55:21 -0700] "GET / HTTP/1.1" 404 683
0:0:0:0:0:0:0:1 - - [15/Jul/2022:13:55:21 -0700] "GET /demo_war_exploded/hello-servlet HTTP/1.1" 200 306

过滤器

何时使用ServletRequestListener,何时使用过滤器?过滤器用于链中处理请求和响应。侦听器在每个请求的开始和结束时都会触发一次。相反,对于单个请求,可能会触发多个筛选器。引用Javadoc:

用于接收进入和离开web应用程序范围的请求的通知事件的接口。

servlet请求被定义为当它将要进入web应用程序的第一个servlet或筛选器时进入该应用程序的范围,以及当它退出链中的最后一个servlet或第一个筛选器时超出范围。

因此,我们有一个开始、中间和结束的级数,其中侦听器为开始和结束而激发,而零、一个或多个筛选器可能在"中间"激发。

ServletRequestListener#requestInitialized>[…筛选器…]>ServletRequestListener#requestDestroyed

有趣的是,我注意到过滤器API是在Servlet规范版本2.3中添加的,ServletRequestListenerAPI是在2.4中添加的。因此,我想,在推出过滤器功能后不久,开发人员就发现了对钩子的需求,这些钩子可以充当临时的书尾,将一系列过滤器括起来。由于任何数量的过滤器都可能启动,而其中一个过滤器不知道其他过滤器,因此您可能需要在开始和结束时使用钩子。


Servlet示例代码

如果好奇的话,我为上面的例子写的Servlet:

package work.basil.example.demo;
import java.io.*;
import java.time.Instant;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet ( name = "helloServlet", value = "/hello-servlet" )
public class HelloServlet extends HttpServlet
{
private String message;
public void init ( )
{
message = "What time is it?";
}
public void doGet ( HttpServletRequest request , HttpServletResponse response ) throws IOException
{
response.setContentType( "text/html" );
// Hello
PrintWriter out = response.getWriter();
out.println( "<!DOCTYPE html>" );
out.println(
"""
<head>
<meta charset="utf-8">
<title>Servlet Request Listener Example</title>
<!-- <link rel="stylesheet" href="style.css">  -->
<!-- <script src="script.js"></script> -->
</head>
"""
);
out.println( "<html lang='en'>" );
out.println( "<body>" );
out.println( "<h1>" + message + "</h1>" );
out.println( "<p>" + Instant.now() + "</p>" );
out.println( "</body>" );
out.println( "</html>" );
}
public void destroy ( )
{
}
}

你说:

我碰巧在使用Java 8。我认为这对应于JavaEE8。

如果您的意思是版本号8和8之间的对应关系,则不是

Java EE(现在的Jakarta EE)这个名称,意思是"企业版",有点用词不当。Jakarta EE只是一组软件的规范,这些软件可以从Java虚拟机中运行的Java代码中调用。所以"Java EE";从来都不是真正的";Java";,它是一个由规范和库组成的框架,用于帮助开发人员构建面向企业的应用程序。

Java EE 8,重新发布为Jakarta EE 8,需要Java 8。(Java 9到11发生了重大变化,迁移时可能会出现问题。)

Jakarta EE 9本质上与Jakarta EE 8相同,只是包名称从javax.*更改为jakarta.*。因此它也需要Java 8。

雅加达EE 9.1是雅加达EE 8&9、去掉一些旧的贬低,增加更多的贬低,并做其他工作为未来的创新奠定基础。其中的变化是要求实现除了支持Java 8(两个LTS版本)之外还支持Java 11。虽然不是明确的要求,但所有实现都应该能够在Java17(当前的LTS版本)中运行。

雅加达EE 10带来了期待已久的第一波创新。

Jakarta EE需要Java
88
98
9.18&11(可能是17)
10