短版本
在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
:
对象 | 事件 | 侦听器接口和事件类|
---|---|---|
Web上下文 | 初始化和销毁 | |
Web上下文 | 添加、删除或替换的属性javax.servlet.ServletContextAttributeListener 和ServletContextAttributeEvent | |
会话 | ||
会话 | 添加、删除或替换的属性javax.servlet.http.HttpSessionAttributeListener 和HttpSessionBindingEvent | |
请求 | web组件已开始处理servlet请求 | |
请求 | 添加、删除或替换的属性javax.servlet.ServletRequestAttributeListener 和ServletRequestAttributeEvent |
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 |
---|---|
8 | 8 |
9 | 8 |
9.1 | 8&11(可能是17) |
10 |