Tomcat8 WebSockets (JSR-356) with Guice 3.0

1.6k views Asked by At

I am trying to @Inject a Guice service into a @ServerEndpoint. I am using Tomcat 8.0.15 as the JSR-356 implementation. However, the dependency injection isn't working. Is there any additional configuration that needs to be done in order to enable Guice injection? Note that I am using all standard javax annotations only.

3

There are 3 answers

1
StanB123 On

Building upon Aritra's own answer:

To be honest, I don't know for sure if this works with Guice 3.0, but it does work for 4.0, which is the current stable release.

I think a somewhat cleaner approach is to change your CustomConfigurator into something like this:

public class CustomConfigurator extends Configurator {
    @Inject
    private static Injector injector;

    public <T> T getEndpointInstance(Class<T> endpointClass) {
        return injector.getInstance(endpointClass);
    }
}

And then from your extended ServletModule class' configureServlets method, call requestStaticInjection(CustomConfigurator.class)

That way you won't expose the injector to everyone. I don't know about you, but it gives me a nice and fuzzy feeling inside to know that no one will be able to mess with my injector :-).

1
Aritra On

I figured this out. The Websocket endpoint needs to have a custom configurator, which creates and returns instances using the Guice injector instance.

Example:

Custom Guice servlet context listener:

public class CustomServletContextListener extends GuiceServletContextListener { 
    public static Injector injector;

    @Override
    protected Injector getInjector() {
        injector = Guice.createInjector(...);
        return injector;
    }
}

Websockets custom configurator:

public class CustomConfigurator extends Configurator {
  @Override
  public <T> T getEndpointInstance(Class<T> clazz)
        throws InstantiationException {
    return CustomServletContextListener.injector.getInstance(clazz);
  }
}

And then in the Websocket endpoint:

@ServerEndpoint(value = "/ws/sample_endpoint", configurator = CustomConfigurator.class)
public class SampleEndpoint {
  private final SomeService service;

  @Inject
  public SampleEndpoint(SomeService service) {
    this.service = service;
  }
  ...
}
0
morgwai On

First, using annotations to do any "magic" behind the scene is a bad idea in general: it's much better to deploy Endpoints programmatically in ServletContextListener.contextInitialized(event) with ServerContainer.addEndpoint(config), so that you have the full control and can avoid storing injector on static vars.

Now regarding the injection, the solution is to define your custom ServerEndpointConfig.Configurator as stated in other answers already, however it is much safer to use field/setter injections in Endpoint classes and call super.getEndpointInstance(endpointClass) followed by injector.injectMembers(endpointInstance). That's because super (default Configurator impl of the given container) may return instances of container-specific dynamic subclasses or decorators wrapping the newly created instance of endpointClass. Furthermore, the spec requires Endpoint classes to have a paramless constructor, so some containers may refuse to deploy Endpoint classes that use constructor params for injections.

public class MyConfigurator extends ServerEndpointConfig.Configurator {

    public <EndpointT> EndpointT getEndpointInstance(Class<EndpointT> endpointClass)
            throws InstantiationException {
        EndpointT endpointInstance = super.getEndpointInstance(endpointClass);
        injector.injectMembers(endpointInstance);
        return endpointInstance;
    }
}

Now a SevletContextListener that adds Endpoints programmatically:

public class MyServletCtxListener implements SevletContextListener {

    ServerEndpointConfig.Configurator endpointConfigurator;
    ServerContainer endpointContainer;

    void addEndpoint(Class<?> endpointClass, String path) throws DeploymentException {
        endpointContainer.addEndpoint(
            ServerEndpointConfig.Builder
                .create(endpointClass, path)
                .configurator(endpointConfigurator)
                .build()
        );
    }

    public void contextInitialized(ServletContextEvent initialization) {
        final var ctx = initialization.getServletContext();
        endpointContainer = (ServerContainer)
                ctx.getAttribute("javax.websocket.server.ServerContainer");
        final var injector = Guice.createInjector(
            // put modules here...
        );
        endpointConfigurator = new MyConfigurator(injector); // NO STATIC :)

        try {
            addEndpoint(MyEndpoint.class, "/websocket/my");
            addEndpoint(MyOtherEndpoint.class, "/websocket/myOther");
            addEndpoint(MyYetAnotherEndpoint.class, "/websocket/myYetAnother");
            // ...
        } catch (DeploymentException e) {
            e.printStackTrace();
            System.exit(1);  // fail fast
        }
    }
}

Note that if you go for programmatic adding, your Endpoint classes should be extending javax.websocket.Endpoint / jakarta.websocket.Endpoint according to the spec (although AFAIR Tomcat specifically used to be relaxed regarding this requirement).

Some related self-promotion: for anyone combining guice with websockets, you may find useful my lib that provides custom Scopes for ws Endpoints: https://github.com/morgwai/servlet-scopes