2

I have a simple REST API project using Jersey 2.x. I tried using Google Guice to inject my dependencies, but it doesn't seem to work. I get this error:

org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at SystemInjecteeImpl(requiredType=AccountService,parent=AccountsResource,qualifiers={},position=0,optional=false,self=false,unqualified=null,1658198405)

I have this simple resource class

@Path("/accounts")
@Produces(MediaType.APPLICATION_JSON)

public class AccountsResource {

    private final AccountService accountService;

    @Inject
    public AccountsResource(AccountService accountService) {
        this.accountService = accountService;
    }

  @GET
  @Path("test")
  public String test() {
    return this.accountService.test();
  }

I want to inject this service into my resource class

public class AccountService {

    public AccountService() {}

    public String test() {
        return "test";
    }
}

So, following Guice's guide, I created this module class

import com.google.inject.*;

public class AccountsResourceModule extends AbstractModule  {

@Override
protected void configure() {
    bind(AccountService.class);
}
}

Finally, I added the injector in my main method

public class TradingServer implements Runnable {
private static final int PORT = 8181;

public static void main(String[] args) {
    Injector injector = Guice.createInjector(new AccountsResourceModule());
    AccountsResource accountsResource = injector.getInstance(AccountsResource.class);
    new TradingServer().run();
}

public void run() {
    Server server = new Server(PORT);
    ServletContextHandler contextHandler = new ServletContextHandler(server, "/");
    ResourceConfig packageConfig = new ResourceConfig().packages("ca.ulaval.glo4002.trading");
    ServletContainer container = new ServletContainer(packageConfig);
    ServletHolder servletHolder = new ServletHolder(container);

    contextHandler.addServlet(servletHolder, "/*");

    try {
        server.start();
        server.join();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        server.destroy();
    }
}

}

When I call my server, I get the error mentioned above. It seems like the dependency injection didn't work. Please help

Steven
  • 166,672
  • 24
  • 332
  • 435
Wissam Goghrod
  • 327
  • 1
  • 5
  • 15
  • 1
    You need to use the guide-hkl2-bridge if you want to make this work. Jersey knows nothing about Guice or the Injector. You need to tie them together with the bridge. – Paul Samsotha Sep 28 '18 at 00:52
  • Does this require JavaEE? Because I'm using Java SE for this project – Wissam Goghrod Sep 28 '18 at 01:06
  • No. HK2 is the DI system that Jersey uses. If you want to use Guice, then you need to "bridge" Guice and HK2 by using the [Guice-HK2 bridge](https://javaee.github.io/hk2/guice-bridge.html). – Paul Samsotha Sep 28 '18 at 01:12
  • I see you are trying to get the `AccountResource` service from the Guice injector, but it is not bound in the AccountResourceModule. If you bind it then what you can do is register that instance with the ResourceConfig. with the register() method. You should remove the packages() call through, as this will cause Jersey to try to create the resource, which you don't want. – Paul Samsotha Sep 28 '18 at 03:57
  • @PaulSamsotha Thx for the help, but I couldn't make it work. I tried to do DI with HK2 following this simple tutorial https://riptutorial.com/jersey/example/23632/basic-dependency-injection-using-jersey-s-hk2 but it didnt work. I got the same error. I created a class that extends ResourceConfig and added the annotation @ApplicationPath("/api"), but I still get the same error. Do you have any idea what the problem could be with my app? – Wissam Goghrod Sep 28 '18 at 19:51
  • if you are using HK2, all you need to do is register the `AbstractBinder` with the `ResourceConfig` you have in your above code. Register the binder as an _instance_ though, not a class. – Paul Samsotha Sep 29 '18 at 04:07
  • Haha I just realized that the link you provided for the HK2 tutorial, I wrote that whole thing on SO. Someone plagiarized the whole thing. Not cool. Did you find this article via a Google search? – Paul Samsotha Sep 29 '18 at 20:51
  • Wow that's rude. Yeah I found it via Google search – Wissam Goghrod Sep 29 '18 at 21:46

1 Answers1

9

So Jersey knows nothing about Guice. It already uses it's own DI framework, HK2. There are a couple things you can do. You can either tie Guice together with HK2 so that HK2 can find services that are bound inside Guice, or another way is to just bind your resource classes inside Guice and and register instances of those resources with Jersey.

Tie Guice with HK2

To tie Guice with HK2, you need to use the Guice HK2 Bridge. First you need to add the following dependency

<dependency>
    <groupId>org.glassfish.hk2</groupId>
    <artifactId>guice-bridge</artifactId>
    <version>${hk2.version}</version>
</dependency>

To get the hk2.version look at your Jersey dependencies (you can run mvn dependency:tree and see what version of HK2 Jersey is pulling in). You want to make sure you are using the exact same version.

Next thing you need to do is to programmatically link the two systems. One way to do this is inside a Feature.

public class GuiceFeature implements Feature {

    @Override
    public boolean configure(FeatureContext context) {
        // This is the way in Jersey 2.26+ to get the ServiceLocator.
        // In earlier versions, use
        // ServiceLocatorProvider.getServiceLocator(context);
        ServiceLocator locator = InjectionManagerProvider.getInjectionManager(context)
                .getInstance(ServiceLocator.class);

        Injector injector = Guice.createInjector(new AccountResourceModule());
        GuiceBridge.getGuiceBridge().initializeGuiceBridge(locator);
        GuiceIntoHK2Bridge guiceBridge = locator.getService(GuiceIntoHK2Bridge.class);
        guiceBridge.bridgeGuiceInjector(injector);
        return true;
    }
}

Then just register the feature with Jersey.

ResourceConfig packageConfig = new ResourceConfig()
        .packages("ca.ulaval.glo4002.trading")
        .register(GuiceFeature.class);

And that's it. It should work, as I have tested.

Bind resources with Guice

With the above configuration, Jersey will be creating instances of your resource classes (@Path annotated classes). The reason we need the bridge is that Jersey is tightly coupled with HK2, so when we inject our resources classes, when creating the instance, Jersey will call HK2 to try to find all the dependencies for the resource.

In this case though, we will not rely on Jersey to create the instance of the resource. We will bind the resource to Guice and let Guice create the instance when we request it. We will use that instance to register with Jersey.

First bind the resource

public class AccountResourceModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AccountService.class);
        bind(AccountResource.class);
    }
}

Also make sure that the @Inject annotation in the resource class is com.google.inject.Inject.

Get instance of resource and register it

Injector injector = Guice.createInjector(new AccountResourceModule());
AccountResource accountResource = injector.getInstance(AccountResource.class);

ResourceConfig config = new ResourceConfig()
        .register(accountResource);

You probably have to figure out a cleaner way to do this as you don't want to have to do this for every resource you have. But this is the gist if what you need to do.

Update

So here's a quick implementation to clean up the second solution. What we can do is scan a package recursively to get all the @Path annotated classes and then bind them in Guice and register them with Jersey.

From this SO post, we can use the Reflections library to easily get all the classes. Just add the following dependency

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>

Then make a little helper classes

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.Path;
import org.reflections.Reflections;

public class ResourceClassHelper {

    private static Set<Class<?>> resourceClasses;

    public static Set<Class<?>> getResourceClasses() {
        if (resourceClasses != null) {
            return resourceClasses;
        }

        // the package to scan for @Path classes "com.example"
        Reflections reflections = new Reflections("com.example");

        resourceClasses = reflections.getTypesAnnotatedWith(Path.class);
        resourceClasses = Collections.unmodifiableSet(resourceClasses);
        return resourceClasses;
    }
}

Then in your Guice module

public class AccountResourceModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AccountService.class);

        ResourceClassHelper.getResourceClasses().forEach(this::bind);
    }
}

And your resource registration

Injector injector = Guice.createInjector(new AccountResourceModule());

ResourceConfig config = new ResourceConfig();
ResourceClassHelper.getResourceClasses()
            .forEach(cls -> config.register(injector.getInstance(cls)));
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • "You probably have to figure out a cleaner way to do this as you don't want to have to do this for every resource you have." What about annotate each resource with something like @JerseyResource and then get (by reflection) all the classes with this annotation and bind && register them ? – bnsd55 Sep 29 '18 at 13:11
  • It actually worked! Thank you so much @PaulSamsotha for your time, I really appreciate it – Wissam Goghrod Sep 29 '18 at 17:54
  • @bnsd55 I think something like that would work. But like I said in our discussion from your other Q&A, this solution is limited in what can be achieved. For one, you cannot access any Jersey services that are normally injectable. For instance, if you need some request information, like headers, you won't be able to get this information in your services. There are a lot of times where you will want your service to be request scoped. But this is not possible with this solution. For simple use cases though, this will work just fine. – Paul Samsotha Sep 29 '18 at 19:18
  • @PaulSamsotha weird because i can use HttpHeaders and HttpServletRequest... – bnsd55 Sep 29 '18 at 22:02
  • @bnsd55 I mean into your services. You can inject em into the resources because Jersey will do that for you when you register the resource. But you will not be able to inject them in your non-resource services. You will just need to pass things down if you need em in your services. – Paul Samsotha Sep 29 '18 at 22:44
  • @PaulSamsotha I was under the impression that resource objects should be created on per-request basis. But here a single accountResource instance will be used by all requests. Is it incorrect or am I missing something? – user2473992 Oct 15 '18 at 20:46
  • 1
    @user2473992 You can make you resource classes request scoped _or_ singletons. If they are singletons, just make sure they are thread safe. By default, when you register a resource class with Jersey, it will be request scoped, and a new one will be created for each request. If you register the resource as an instance, then it will be a singleton. You can also register it as a class and annotate it with `@Singleton` and Jersey will create it and keep only one instance for the life of the application. – Paul Samsotha Oct 16 '18 at 05:15
  • 1
    @user2473992 If you use the Guice-HK2 bridge, then you can get the request scoped resource, but if you do with the second solution, then the only option is to use the singleton resource. These are some of the limitations going with the second option. – Paul Samsotha Oct 16 '18 at 05:16