Classes for building Guice Scopes easily transferable when dispatching work to other Threads.
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0
latest release: 12.0
(javadoc)
See CHANGES for the summary of changes between releases. If the major version of a subsequent release remains unchanged, it is supposed to be backwards compatible in terms of API and behaviour with previous ones with the same major version (meaning that it should be safe to just blindly update in dependent projects and things should not break under normal circumstances).
Asynchronous apps (such as gRPC, websockets, async Servlets etc) often need to switch between various Threads. This requires an extra care not to lose the Guice Scope associated with a given event/request/call/session: it needs to be preserved as long as we are in the context of such an event/request/call/session, regardless of Thread switching.
To ease this up, this lib formally introduces a notion of an InjectionContext that stores scoped Objects and can be tracked using ContextTrackers when switching between Threads. Trackers are in turn used by ContextScopes to obtain the Context that is current at a given moment and from which scoped Objects will be obtained.
Creation of a (set of) custom Scope(s) boils down to the below things:
- Defining at least one concrete subclass of TrackableContext (subclass of
InjectionContext). For exampleServletRequestContextin case of Java Servlet containers. - Defining a concrete subclass of ScopeModule with
public final ContextScopefields corresponding to the aboveTrackableContextsubclasses, initialized with newContextScope(name, trackableCtxClass) calls. - Hooking creation of the above
TrackableContextinstances into a given existing framework: for example in case of Java Servlet containers, aFiltermay be created that for each new incomingServletRequestwill executechain.doFilter(request, response)within a newly createdServletRequestContextinstance. - Optionally defining subclasses of
InjectionContextforContexttypes that are induced by someTrackableContextsubclass. For example entering into aServletRequestContextmay induce entering into the correspondingHttpSessionContext. - Defining
public final InducedContextScopefields in theScopeModulesubclass from the point 2, corresponding to the above inducedContexttypes (if any) and initialized with newInducedContextScope(...) calls. - For app-level code development convenience, defining a
public final ContextBinderfield in theScopeModulesubclass from the point 2, initialized with a newContextBinder() call. This may be useful for app developers when creating their global ContextTrackingExecutor instances bound for injection withtoInstance(myGlobalCtxTrackingExecutor)calls in theirModules: see USAGE section.
App developers should then create an app-wide instance of this ScopeModule subclass defined in the point 2, pass its Scopes to their other Modules (as needed for scoping of their app components, see PORTABLE MODULES section) and finally pass this ScopeModule instance to their Guice.createInjector(...) call(s) along with their other Moudles.
When switching Threads in a low level library code, static helper methods getActiveContexts(List<ContextTracker<?>>) and executeWithinAll(List<TrackableContext>, Runnable) can be used to manually transfer all active Contexts:
class MyComponent {
@Inject List<ContextTracker<?>> allTrackers;
void methodThatCallsSomeAsyncMethod(/* ... */) {
// other code here...
final var activeCtxs = ContextTracker.getActiveContexts(allTrackers);
someAsyncMethod(
arg1,
// ...
argN,
(callbackParamIfNeeded) -> TrackableContext.executeWithinAll(
activeCtxs,
() -> {
// callback code here will run within the same Contexts
// as methodThatDispatchesToExecutor(...)
}
)
);
}
}For higher level abstraction and app-level code, ContextBinder class was introduced that allows to bind closures defined as common functional interfaces (Runnable, Callable, Consumer, BiConsumer, Function, BiFunction) to Contexts that were active at the time of a given binding:
class MyComponent { // compare with the "low-level" version above
@Inject ContextBinder ctxBinder;
void methodThatCallsSomeAsyncMethod(/* ... */) {
// other code here...
someAsyncMethod(
arg1,
// ...
argN,
ctxBinder.bindToContext((callbackParamIfNeeded) -> {
// callback code here will run within the same Contexts
// as methodThatDispatchesToExecutor(...)
})
);
}
}For app development convenience, ContextTrackingExecutor interface and decorator was provided that uses ContextBinderto automatically transfer active Contexts when executing tasks.
class MyOtherComponent {
ContextTrackingExecutor executor;
@Inject void setExecutorAndBinder(ExecutorService executor, ContextBinder ctxBinder) {
this.executor = ContextTrackingExecutor.of(executor, ctxBinder);
}
void methodThatDispatchesToExecutor(/* ... */) {
// other code here...
executor.execute(() -> {
// task code here will run within the same Contexts
// as methodThatDispatchesToExecutor(...)
});
}
}gRPC Guice Scopes
Servlet and Websocket Guice Scopes
As the official Guice Servlet Scopes lib stores its Scope instances as static vars (ServletScopes.REQUEST and ServletScopes.SESSION), developers tended to scope their components using these static references directly in their Modules or even worse using @RequestScoped and @SessionScoped annotations. This makes such Modules (or even whole components in case of annotations) tightly tied to Java Servlet framework and if there's a need to use them with gRPC or websockets, they must be rewritten.
To avoid this problem, first, scoping annotations should never be used in components that are meant to be portable, so that they are not tied to any given framework. Instead they should be explicitly bound in appropriate Scopes in their corresponding Modules.
Second, Modules should not use static references to Scopes, but instead accept Scopes as their constructor params. In case of most technologies, usually 2 types of Scopes make sense:
- a short-term one for storing stuff like
EntityManagers, pooled JDBCConnections or enclosing transactions; - a long-term one for storing stuff like auth-tokens, credentials, client conversation state (like the immortal shopping cart) etc;
Therefore most Modules should have a constructor that accepts such 2 Scope references (public MyModule(Scope shortTermScope, Scope longTermScope) {...}) and then use these to bind components. This allows to reuse such Modules in several environments:
- When developing a Servlet app using the official Guice Servlet Scopes lib,
MyModulemay be created withnew MyModule(ServletScopes.REQUEST, ServletScopes.SESSION). - In case of a websocket client app or a standalone websocket server, it may be created with
new MyModule(websocketModule.containerCallScope, websocketModule.websocketConnectionScope). - For a websocket server app embedded in a Servlet Container it may be either
new MyModule(websocketModule.containerCallScope, websocketModule.websocketConnectionScope)ornew MyModule(servletModule.containerCallScope, servletModule.httpSessionScope)depending whether it is desired to share state between websocketEndpoints andServlets and whether enforcing ofHttpSessioncreation for websocket connections is acceptable. - For a gRPC app, it may be
new MyModule(grpcModule.listenerEventScope, grpcModule.rpcScope).