containerCallScope
(either a HttpServletRequest
or a websocket Endpoint
event), websocketConnectionScope
(javax.websocket.Session
) and httpSessionScope
for use in servlet+websocket apps and standalone websocket apps (both client and server).
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0
latest release: 17.1
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
Provides the below Guice Scope
s:
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.MessageHandler
s).
Having a common Scope
for servlet requests and websocket events allows to inject scoped objects both in Servlet
s and Endpoint
s without a need for 2 separate bindings in user Module
s.
This Scope
may be used in all 3 container types.
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 MessageHandler
s) are executed within the same associated websocketConnectionScope
.
This Scope
may be used in websocket containers both on a client and on a server side.
Scopes bindings to a given HttpSession
. Available only in Servlet
containers to Servlet
s and
optionally server Endpoint
s.
All the above scopes are built using guice-context-scopes lib, so they are automatically transferred when dispatching using AsyncContext
, RequestDispatcher
or ContextTrackingExecutor
.
- BASE WEBSOCKET STUFF:
Obtains Endpoint
instances from Guice and ensures their methods run within websocket Context
s by wrapping them with context-aware proxies. May be used directly to obtain client Endpoint
instances.
Annotation for client Endpoint
s that should be injected using a GuiceEndpointConfigurator
.
Defines containerCallScope
and websocketConnectionScope
, configures GuiceEndpointConfigurator
. Necessary in all 3 container types.
ServerEndpointConfig.Configurator
(for use in @ServerEndpoint
annotations as configurator
argument) that obtains server Endpoint
instances from a GuiceEndpointConfigurator
.
Module
for standalone websocket server apps. Initializes GuiceServerEndpointConfigurator
.
- MIXED SERVLET-WEBSOCKET APPS:
Module
for mixed Servlet-websocket apps. Embeds a WebsocketModule
and defines httpSessionScope
.
Base class for app ServletContextListener
s, creates and configures the app-wide Injector
and ServletWebsocketModule
, initializes GuiceServerEndpointConfigurator
. Provides helper methods for programmatically adding Servlet
s, Filter
s and websocket Endpoint
s.
- MISC STUFF:
Binds closures (Runnable
s, Consumer
s, Callable
s etc) to Context
s that were active at the time of a given binding. This can be used to transfer Context
s semi-automatically when manually switching Thread
s, for example when passing callbacks to async functions.
Interface and decorator for an Executor
that automatically transfers active Contexts
using its associated ContextBinder
when executing tasks.
- INTEGRATION WITH WebsocketPingerService:
GuiceEndpointConfigurator
that additionally automatically registers and deregisters created Endpoint
s to its associated WebsocketPingerService
.
Annotation for client Endpoint
s that should be injected using a PingingEndpointConfigurator
.
Subclass of WebsocketModule
that allows to automatically register Endpoint
instances to a WebsocketPingerService
using PingingEndpointConfigurator
.
GuiceServerEndpointConfigurator
that uses PingingEndpointConfigurator
.
GuiceServletContextListener
that uses PingingWebsocketModule
and PingingEndpointConfigurator
. Creates and configures the app-wide WebsocketPingerService
.
@WebListener
public class MyServletContextListener extends GuiceServletContextListener {
// ...or `extends PingingServletContextListener {`
@Override
protected LinkedList<Module> configureInjections() {
final var modules = new LinkedList<Module>();
modules.add((binder) -> {
binder.bind(SomeService.class).to(MyService.class).in(containerCallScope);
// @Inject Provider<SomeService> myServiceProvider;
// will now work both in servlets and endpoints
// more bindings here...
});
return modules;
}
@Override
protected void addServletsFiltersEndpoints() 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 / endpoints here...
}
}
NOTE: If the servlet container being used uses mechanism other than the standard Java Serialization to persist/replicate HttpSession
s, 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)
).
@ServerEndpoint(
value = "/websocket/myAnnotatedSocket",
configurator = GuiceServerEndpointConfigurator.class // ...or PingingServerEndpointConfigurator
)
public class MyAnnotatedEndpoint {
@Inject Provider<SomeService> myServiceProvider; // will be injected automatically
// endpoint implementation here...
}
Note: in case of annotated Endpoints
, it is also necessary either for app's ServletContextListener
to extend GuiceServletContextListener
/ PingingServletContextListener
or to perform the setup manually as explained before.
public class MyWebsocketClientApp {
static final String SERVER_URL = "url";
static final String REQUEST = "request";
@Inject @Named(SERVER_URL) String serverUrl;
@Inject @Named(REQUEST) String request;
@Inject @GuiceClientEndpoint MyClientEndpoint endpoint;
@Inject WebSocketContainer container;
@ClientEndpoint
public static class MyClientEndpoint {
@Inject ResponseProcessor responseProcessor;
Session connection;
final CountDownLatch connectionClosed = new CountDownLatch(1);
@OnOpen public void onOpen(Session connection) {
this.connection = connection;
}
@OnMessage public void onMessage(String serverReply) {
try {
responseProcessor.process(serverReply);
} finally {
try {
connection.close();
} catch (IOException ignored) {}
}
}
@OnClose public void onClose() {
connectionClosed.countDown();
}
void awaitClosure(long timeout, TimeUnit unit) throws InterruptedException {
connectionClosed.await(timeout, unit);
}
}
void startAndAwait(long timeout, TimeUnit unit) throws Exception {
try(
final var connection = container.connectToServer(endpoint, URI.create(serverUrl));
) {
connection.getBasicRemote().sendText(request);
endpoint.awaitClosure(timeout, unit);
}
}
public static void main(String[] args) throws Exception {
final var modules = new ArrayList<Module>();
final var websocketModule = new WebsocketModule(false, MyClientEndpoint.class);
modules.add(websocketModule);
modules.add((binder) -> {
binder.bind(WebSocketContainer.class)
.toInstance(createClientWebsocketContainer());
binder.bind(String.class)
.annotatedWith(named(SERVER_URL))
.toInstance(args[0]);
binder.bind(String.class)
.annotatedWith(named(REQUEST))
.toInstance(args[1]);
binder.bind(ResponseProcessor.class); // has default or @inject constructor
binder.bind(SomeService.class)
.to(MyService.class)
.in(websocketModule.containerCallScope);
// more bindings here...
});
// more modules here...
final var injector = Guice.createInjector(modules);
final var myApp = injector.getInstance(MyWebsocketClientApp.class);
myApp.startAndAwait(10, SECONDS);
}
static WebSocketContainer createClientWebsocketContainer() {
// container specific code
}
}
public class MyWebsocketServer {
public static void main(String[] args) throws Exception {
final var port = Integer.parseInt(args[0]);
final var deploymentPath = args[1];
final var modules = new ArrayList<Module>();
final var websocketModule = new WebsocketModule(false);
final var serverModule = new StandaloneWebsocketServerModule(deploymentPath);
modules.add(websocketModule);
modules.add(serverModule);
modules.add((binder) -> {
binder.bind(SomeService.class)
.to(MyService.class)
.in(websocketModule.containerCallScope);
// more bindings here...
});
// more modules here...
final var injector = Guice.createInjector(modules);
final var server = createServer(port, deploymentPath, Config.class);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.stop();
GuiceServerEndpointConfigurator.deregisterInjector(injector);
}));
server.awaitTermination();
}
public static class Config implements ServerApplicationConfig {
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
return Set.of(MyAnnotatedEndpoint.class);
}
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> s) {
return Set.of();
}
}
Server createServer(int port, String deploymentPath, Class<?>... configs) {
// container specific code
}
}
Dependencies of this jar on guice is declared as optional, so that apps can use any version with compatible API.
Standalone websocket apps must include servlet-api
in their dependencies (javax or jakarta respectively).
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 asoptional
. This is useful for apps that also depend onbyte-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 thatservlet-scopes
depends on (in regard to features used byservlet-scopes
). If this is not the case, thenshadedbytebuddy
build should be used.
Tyrus connection proxy that provides unified, websocket API compliant access to clustered websocket connections and properties.
a trivial sample app built from the test code.
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 fakeHttpSevletRequest
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.