Ian Boyd
Ian Boyd

Reputation: 256951

How to hook BeginRequest and EndRequest in JavaEE?

Short Version

In JavaEE, i want to know when:

and be able to inspect the request and response objects.

Long Version

In the ASP.net world, if you want to know when a request starts and ends, you write an IHttpModule:

public class ExampleModuleForThisQuestion : IHttpModule
{
}

And then register your "module" in the web xml configuration file:

web.config:

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

Inside your module you can register callback handlers for:

The web-server infrastructure then calls you Init method. That is your opportunity to register that you want to receive notifications when a request starts, and when a request ends:

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
   }
}

And now we have our callbacks when a request starts:

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"]);
}

and we have our callback when a request ends:

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;
}

Now how to do it in JavaEE?

The question is: what is the moral equivalent of IHttpModule in the Java web-server world?

Some people say that it is an ServletRequestListener:

Interface for receiving notification events about requests coming into and going out of scope of a web application.

A ServletRequest is defined as coming into scope of a web application when it is about to enter the first servlet or filter of the web application, and as going out of scope as it exits the last servlet or the first filter in the chain.

Others insist that you want a "filter" and a "filter chain".

Some say that a "filter" gives you everything that ServletRequestListener does, but that a filter can also be configured to run for only certain URL.

This random page on docs.oracle.com says nothing about "filters", and instead i need a ServletContextListener, because that's the only way to receive a notification for each request. But then they have a table that seems to contradict that, and that i want a ServletRequestEvent:

Object Event Listener Interface and Event Class
Web context Initialization and destruction javax.servlet.ServletContextListener and ServletContextEvent
Web context Attribute added, removed, or replaced javax.servlet.ServletContextAttributeListener and ServletContextAttributeEvent
Session Creation, invalidation, activation, passivation, and timeout javax.servlet.http.HttpSessionListener, javax.servlet.http.HttpSessionActivationListener, and HttpSessionEvent
Session Attribute added, removed, or replaced javax.servlet.http.HttpSessionAttributeListener and HttpSessionBindingEvent
Request A servlet request has started being processed by web components javax.servlet.ServletRequestListener and ServletRequestEvent
Request Attribute added, removed, or replaced javax.servlet.ServletRequestAttributeListener and ServletRequestAttributeEvent

I don't know what any of these things are. I just want to be notified during a web-request:

Writing Java code blind

I can try to transcribe the ASP.net code into Java-esque code on the fly, and hopefully someone can just fix it.

First we'll create a class that implements...ServletRequestListener?:

public class ExampleListenerForThisQuestion 
      implements javax.servlet.ServletRequestListener {

}

And then we register our "module" in our web xml configuration file:

web.xml

<listener>
    <listener-class>ExampleListenerForThisQuestion</listener-class>
</listener>

Now we will implement the requestInitialized method to create our event listeners:

public class ExampleListenerForThisQuestion 
      implements javax.servlet.ServletRequestListener {

   public void requestInitialized(ServletRequestEvent sre) {
      //...now i'm stuck 
   }
}

That didn't get very var.

The closest question on stackoverflow to what i want is:

Event like .net's "Application_Start" and "Begin_Request" for java/tomcat/JSP?

The only problem is that the accepted answer doesn't answer how to do it. That makes sense: the question wasn't asking how to do it. The question was only asking if Java has an equivalent. Apparently it does - but we won't tell you how.

Research Effort

I know that trying to figure out how to do it in JavaEE will take me at least 4 days. I'm hoping to cut that down to 6 or 7 hours by asking Stackoverflow.

This question took me an hour-and-a-half to write up. I wrote the ASP.net code, from memory, in 25 seconds. It took me longer to ask the question about how to do it in Java than it would have just taken me to do it in ASP.net - which shows just how crushed and defeated i am right now.

Edit: I happen to be using Java 8. I assume that corresponds to JavaEE 8.

Upvotes: 2

Views: 244

Answers (2)

Ian Boyd
Ian Boyd

Reputation: 256951

The answer is: it is not possible.

Upvotes: -1

Basil Bourque
Basil Bourque

Reputation: 339482

ServletRequestListener

i want to know when: when a request starts and when a request ends

Use a ServletRequestListener as suggested in your Question. This is a long-standing feature in the Servlet API, since Servlet spec version 2.4.

Example code

Here is a little demo I wrote.

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

Notice the annotation @WebListener. With this annotation, you can skip editing the web XML configuration file. The @WebListener acts as a flag at runtime, so the Servlet container will automatically detect, instantiate, and execute this class at the appropriate point in the lifecycle of your web app. This modern convenience was added in Servlet spec 3.0.

jakarta.* package naming

Also notice the jakarta.* package naming in the import statements. Java EE is now known as Jakarta EE after Oracle Corp handed responsibility to the Eclipse Foundation a few years ago. Jakarta Servlet specifications versions 4 and 5 (used here) are equivalent except for a change in package names from javax.* to jakarta.*.

By the way, Jakarta Servlet spec version 6 was released this year as part of Jakarta EE 10, with the first major changes in many years. See article, Top 5 things to know about the Jakarta Servlet 6.0 API release.

Run example

When run in the Microsoft Edge browser Version 103.0.1264.62 (Official build) (arm64) for macOS Monterey 12.4 on Apache Tomcat 2022.2 Beta with Adoptium Temurin for Java 18.0.1+10 on a MacBook Pro (16-inch, 2021) Apple M1 Pro, for URL http://localhost:8080/demo_war_exploded/hello-servlet results in the following output.

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

Why do we get a four lines instead of two for a single request? Well, looking at the access logs shows that this browser sent a request on both IPv4 and IPv6. And the browser sent a request to just /, but I have no idea why.

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

Filters

When to use ServletRequestListener and when to use filters? Filters are used in a chain to process the request and response. The listener fires once per request, at the very beginning and the very end. In contrast, multiple filters may fire for a single request. To quote the Javadoc:

Interface for receiving notification events about requests coming into and going out of scope of a web application.

A ServletRequest is defined as coming into scope of a web application when it is about to enter the first servlet or filter of the web application, and as going out of scope as it exits the last servlet or the first filter in the chain.

So we have a progression of beginning, middle, and end where the listener fires for beginning and end while zero, one , or more filters may fire in the middle.

ServletRequestListener#requestInitialized > [ … filters … ] > ServletRequestListener#requestDestroyed

Interestingly, I noticed the Filter API was added in Servlet spec version 2.3, and the ServletRequestListener API was added in 2.4. So I would guess that soon after rolling out the filter feature, developers found a need for hooks that act as temporal bookends, bracketing outside a series of filters. Since any number of filters may fire, without one filter knowing about the others, you might want a hook for the very beginning and the very end.


Servlet example code

If curious, the Servlet I wrote for the above example:

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 ( )
    {
    }
}

You said:

I happen to be using Java 8. I assume that corresponds to JavaEE 8.

If you mean a correspondence between version numbers 8 and 8, no.

The name Java EE (now Jakarta EE), meaning “Enterprise Edition”, is a bit of a misnomer. Jakarta EE is just a bunch of specifications for a bunch of software that can be called from Java code running in a Java Virtual Machine. So "Java EE" was never really "Java", it was a framework of specs and libraries for helping developers build enterprise-oriented applications.

Java EE 8, re-released as Jakarta EE 8, requires Java 8. (Major changes came to Java 9 through 11 that may cause problems when migrating.)

Jakarta EE 9 is essentially the same as Jakarta EE 8, but for the change in package naming from javax.* to jakarta.*. So it too requires Java 8.

Jakarta EE 9.1 is a revamp and cleanup of Jakarta EE 8 & 9, dropping some old deprecations, adding more deprecations, and doing other work to lay the foundation for future innovations. Among the changes is requiring implementations to support Java 11 in addition to Java 8 (both LTS versions). And while not an explicit requirement, all implementations are expected to be able to run in Java 17 (the current LTS version) as well.

Jakarta EE 10 brings the first wave of those long-awaited innovations.

Jakarta EE Requires Java
8 8
9 8
9.1 8 & 11 (and likely 17)
10 ??

Upvotes: 1

Related Questions