Skip to content

Websocket and Servlet Guice Scopes automatically transferred between threads

License

Notifications You must be signed in to change notification settings

morgwai/servlet-scopes

Repository files navigation

Servlet and Websocket Guice Scopes

containerCallScope (either a HttpServletRequest or a websocket Endpoint event), websocketConnectionScope (javax.websocket.Session) and httpSessionScope for use in Servlet/websocket server containers and websocket client containers.
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0

latest release: 16.3
javax flavor (javadoc) - supports Servlet 4.0.1 and Websocket 1.1 APIs
jakarta flavor (javadoc) - supports Servlet 5.0.0 to at least 6.0.0 and Websocket 2.0.0 to at least 2.1.1 APIs

OVERVIEW

Provides the below Guice scopes:

Scopes bindings to either an HttpServletRequest or a websocket event (connection opened/closed, message received, error occurred).
Spans over a single container-initiated call to either one of servlet's doXXX(...) methods or to a websocket endpoint life-cycle method (annotated with one of the websocket annotations or overriding those of javax.websocket.Endpoint or of registered javax.websocket.MessageHandlers).
Having a common Scope for servlet requests and websocket events allows to inject scoped objects both in servlets and endpoints without a need for 2 separate bindings in user Modules. This Scope may be used both on a client and on a server side.

Scopes bindings to a websocket connection (javax.websocket.Session).
Spans over a lifetime of a given endpoint instance: all calls to life-cycle methods of a given endpoint instance (annotated with @OnOpen, @OnMessage, @OnError, @OnClose, or overriding those of javax.websocket.Endpoint together with methods of registered MessageHandlers) are executed within the same associated websocketConnectionScope. This Scope may be used both on a client and on a server side.

Scopes bindings to a given HttpSession. Available only on a server side both to servlets and websocket endpoints.

All the above scopes are built using guice-context-scopes lib, so they are automatically transferred to a new thread when dispatching using AsyncContext.dispatch() or ServletContextTrackingExecutor (see below).

MAIN USER CLASSES

Contains the above Scopes, related ContextTrackers and some helper methods.

Websocket ServerEndpoint Configurator that ensures that Endpoint instances have their dependencies injected and that their methods run within websocket contexts, so that the above Scopes work properly.

Base class for app ServletContextListeners. Creates and configures an app-wide Guice Injector instance, the above mentioned ServletModule and performs bookkeeping related to GuiceServerEndpointConfigurator and ServletContextTrackingExecutors. Also provides helper methods for creating and configuring programmatic Servlets, Filters and Endpoints.

Subclass of GuiceServerEndpointConfigurator that additionally automatically registers and deregisters created Endpoint instances to its associated WebsocketPingerService.

Subclass of GuiceServletContextListener that uses PingingEndpointConfigurator for programmatic Endpoints and configures app's WebsocketPingerService.

A ThreadPoolExecutor that upon dispatching a task, automatically transfers all the active Contexts to the thread running the task.

Binds tasks and callbacks (Runnables, Consumers, BiConsumers, Functions and BiFunctions) to contexts that were active at the time of binding. This can be used to transfer Contexts almost fully automatically when it's not possible to use GrpcContextTrackingExecutor when switching threads (for example when providing callbacks as arguments to async functions). See a usage sample below.

Context-aware proxy for client Endpoints. Executes lifecycle methods of its wrapped Endpoint and of its registered MessageHandlers within websocket Contexts.

Subclass of ClientEndpointProxy that additionally automatically registers and deregisters its wrapped Endpoint to its associated WebsocketPingerService.

USAGE

Adding Guice Modules and programmatic Servlets and Endpoints in ServletContextListener

@WebListener
public class ServletContextListener extends GuiceServletContextListener {
                          // ...or `extends PingingServletContextListener {`

    @Override
    protected LinkedList<Module> configureInjections() throws Exception {
        final var modules = new LinkedList<Module>();
        modules.add((binder) -> {
            binder.bind(MyService.class).in(containerCallScope);
                // @Inject Provider<MyService> myServiceProvider;
                // will now work both in servlets and endpoints
            // more bindings here...
        });
        return modules;
    }

    @Override
    protected void configureServletsFiltersEndpoints() throws ServletException, DeploymentException
    {
        addEnsureSessionFilter("/websocket/*");

        // MyServlet and MyProgrammaticEndpoint instances will have their dependencies injected
        addServlet("myServlet", MyServlet.class, "/myServlet");
        addEndpoint(MyProgrammaticEndpoint.class, "/websocket/myProgrammaticSocket");
        // more servlets / filters / unannotated endpoints here...
    }
}

NOTE: If the servlet container being used uses mechanism other than the standard Java Serialization to persist/replicate HttpSessions, then a deployment init-param named pl.morgwai.base.servlet.guice.scopes.HttpSessionContext.customSerialization must be set to true either in web.xml or programmatically before any request is served (for example in ServletContextListener.contextInitialized(event)).

Note: in cases where it is not possible to extend GuiceServletContextListener, all the setup required to use ServletModule (with all its Scopes etc) and GuiceServerEndpointConfigurator / PingingEndpointConfigurator, can be done manually: see an example in ManualServletContextListener.

Using annotated server Endpoints

@ServerEndpoint(
    value = "/websocket/myAnnotatedSocket",
    configurator = GuiceServerEndpointConfigurator.class  // ...or PingingEndpointConfigurator
)
public class MyAnnotatedEndpoint {

    @Inject Provider<MyService> myServiceProvider;  // will be injected automatically

    // endpoint implementation here...
}

Note: in case of annotated Endpoints, it is still necessary either for app's ServletContextListener to extend GuiceServletContextListener / PingingServletContextListener or to perform the setup manually as explained before.

Client websocket app sample

public class MyWebsocketClientApp {

    public static void main(String[] args) throws Exception {
        final var modules = new ArrayList<Module>();
        final var servletModule = new ServletModule();
        modules.add(servletModule);
        modules.add((binder) -> {
            binder.bind(MyClientEndpointDependency.class).in(servletModule.containerCallScope);
            // more bindings here...
        });
        // more modules here...
        final var injector = Guice.createInjector(modules);
        final WebSocketContainer clientWebsocketContainer = createClientWebsocketContainer();
        final var myClientEndpoint = injector.getInstance(MyClientEndpoint.class);
        clientWebsocketContainer.connectToServer(
            new ClientEndpointProxy(myClientEndpoint, servletModule.containerCallContextTracker),
            null,
            URI.create("wss://someapp.example.com/websocket/someservice")
        );
        myClientEndpoint.awaitClosure(10, SECONDS);
    }

    static WebSocketContainer createClientWebsocketContainer() {/* ... */}
}

Transferring contexts to callbacks using ContextBinder

class MyComponent {

    @Inject ContextBinder ctxBinder;

    void methodThatCallsSomeAsyncMethod(/* ... */) {
        // other code here...
        someAsyncMethod(arg1, /* ... */ argN, ctxBinder.bindToContext((callbackParam) -> {
            // callback code here...
        }));
    }
}

NOTE: when dispatching work to servlet container threads using any of AsyncContext.dispatch() methods, the context is transferred automatically.

Dependency management

Dependencies of this jar on guice is declared as optional, so that apps can use any version with compatible API.

There are 2 builds available:

  • build with shadedbytebuddy classifier includes relocated dependency on byte-buddy. Most apps should use this build. To do so, add <classifier>shadedbytebuddy</classifier> to your dependency declaration.
  • "default" build does not include any shaded dependencies and dependency on byte-buddy is marked as optional. This is useful for apps that also depend on byte-buddy and need to save space (byte-buddy is over 3MB in size). Note that the version provided by the app needs to be compatible with the version that servlet-scopes depends on (in regard to features used by servlet-scopes). If this is not the case, then shadedbytebuddy build should be used.

EXTENSIONS

Tyrus connection proxy that provides unified, websocket API compliant access to clustered websocket connections and properties.

EXAMPLES

a trivial sample app built from the test code.

FAQ

Why isn't this built on top of official servlet scopes lib?

  • the official Guice-servlet has some serious issues
  • in order to extend the official Guice-servlet lib to support websockets, the code would need to pretend that everything is an HttpServletRequest (websocket events and websocket connections would need to be wrapped in some fake HttpSevletRequest wrappers), which seems awkward.
  • guice-context-scopes allows to remove objects from scopes.

Why do I have to install myself a filter that creates HTTP session for websocket requests? Can't addEnsureSessionFilter("/*") be called automatically?

Always enforcing a session creation is not acceptable in many cases, so this would limit applicability of this lib. Reasons may be technical (cookies disabled, non-browser clients that don't even follow redirections), legal (user explicitly refusing any data storage) and probably others. It's a sad trade-off between applicability and API safety.