Classes for building Guice Scopes easily transferable when dispatching work to other Thread
s.
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0
latest release: 11.1
(javadoc)
Asynchronous servers (such as gRPC or asynchronous Servlet
s) often need to switch between various Thread
s. This requires extra care to not lose a given current Guice Scope
: it needs to be preserved as long as we are in the context of a given event/request/call/session, regardless of Thread
switching.
To ease this up, this lib formally introduces a notion of an InjectionContext that stores scoped Object
s and can be tracked using ContextTrackers when switching between Thread
s. Tracker
s are in turn used by ContextScopes to obtain the Context
that is current at a given moment and from which scoped Object
s 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 exampleServletRequestContext
in case of Java Servlet containers. - Defining a concrete subclass of ScopeModule with
public final ContextScope
fields corresponding to the aboveTrackableContext
subclasses, initialized with newContextScope(name, trackableCtxClass) calls. - Hooking creation of the above
TrackableContext
instances into a given existing framework: for example in case of Java Servlet containers, aFilter
may be created that for each new incomingServletRequest
will executechain.doFilter(request, response)
within a newly createdServletRequestContext
instance. - Optionally defining subclasses of
InjectionContext
forContext
types that are induced by someTrackableContext
subclass. For example entering into aServletRequestContext
may induce entering into the correspondingHttpSessionContext
. - Defining
public final InducedContextScope
fields in theScopeModule
subclass from the point 2, corresponding to the above inducedContext
types (if any) and initialized with newInducedContextScope(...) calls. - For app-level code development convenience, defining a
public final ContextBinder
field in theScopeModule
subclass 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 theirModule
s: see USAGE section.
App developers should then create a global instance of this ScopeModule
subclass defined in the point 2, pass its Scopes
to their other Module
s (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 Moudle
s.
When switching Thread
s 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 Context
s:
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 Context
s 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 ContextBinder
to automatically transfer active Context
s when executing tasks.
class MyOtherComponent {
ContextTrackingExecutor executor;
@Inject void setContextBinder(ContextBinder ctxBinder) {
executor = ContextTrackingExecutor.of(Executors.newFixedThreadPool(5), 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 Module
s or even worse using @RequestScoped
and @SessionScoped
annotations. This makes such Module
s (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 Scope
s in their corresponding Module
s.
Second, Module
s should not use static references to Scope
s, but instead accept Scope
s as their constructor params. In case of most technologies, usually 2 types of Scope
s make sense: a short-term one for storing stuff like EntityManager
s, pooled JDBC Connection
s or enclosing transactions and a long-term one for storing stuff like auth-tokens, credentials, client conversation state (like the immortal shopping cart) etc.
Therefore most Module
s 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 Module
s in several environments:
- When developing a Servlet app using the official Guice Servlet Scopes lib,
MyModule
may 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 websocketEndpoint
s andServlet
s and whether enforcing ofHttpSession
creation for websocket connections is acceptable. - For a gRPC app, it may be
new MyModule(grpcModule.listenerEventScope, grpcModule.rpcScope)
.