1

I'm refactoring a legacy Java codebase to provide Guice-powered dependency injection to Jersey resource classes.

Here is a stripped down application that uses the legacy Jetty/Jersey setup (see Main & Application) along with my attempts to wire up Guice using their wiki article on servlets:

build.gradle

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.projectlombok:lombok:1.16.18'
    compile 'com.google.inject:guice:4.1.0'
    compile 'com.google.inject.extensions:guice-servlet:4.1.0'
    compile 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.9.3'
    compile 'org.eclipse.jetty:jetty-server:9.4.8.v20171121'
    compile 'org.eclipse.jetty:jetty-servlet:9.4.8.v20171121'
    compile 'org.glassfish.jersey.media:jersey-media-sse:2.26'
    compile 'com.sun.jersey:jersey-servlet:1.19.4'
}

Main.java

package org.arabellan.sandbox;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.ServletModule;

import java.util.ArrayList;
import java.util.List;

public class Main {

    static Injector injector;

    public static void main(String[] args) throws Exception {
        List<AbstractModule> modules = new ArrayList<>();
        modules.add(new ExistingModule());
        modules.add(new ServletModule());
        injector = Guice.createInjector(modules);
        injector.getInstance(Application.class).run();
    }

}

Application.java

package org.arabellan.sandbox;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.google.inject.servlet.GuiceFilter;
import com.sun.jersey.spi.container.servlet.ServletContainer;
import org.glassfish.jersey.message.DeflateEncoder;
import org.glassfish.jersey.message.GZipEncoder;
import org.glassfish.jersey.server.ResourceConfig;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.filter.EncodingFilter;

class Application {

    void run() throws Exception {
        Server jettyServer = new Server(8080);
        ServletContextHandler httpContext = new ServletContextHandler(jettyServer, "/");
        httpContext.addEventListener(new GuiceServletConfig());
        httpContext.addFilter(GuiceFilter.class, "/*", null);
        httpContext.addServlet(new ServletHolder(new ServletContainer(buildResourceConfig())), "/*");
        jettyServer.setHandler(httpContext);
        jettyServer.start();
    }

    private ResourceConfig buildResourceConfig() {
        ResourceConfig config = new ResourceConfig();
        config.register(JacksonJsonProvider.class);
        config.registerClasses(EncodingFilter.class, GZipEncoder.class, DeflateEncoder.class);
        config.packages("org.arabellan.sandbox");
        return config;
    }

}

ExistingModule.java

package org.arabellan.sandbox;

import com.google.inject.AbstractModule;

public class ExistingModule extends AbstractModule {

    protected void configure() {
        bind(FooDao.class).to(DynamoDBFooDao.class);
    }

}

GuiceServletConfig.java

package org.arabellan.sandbox;

import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;

public class GuiceServletConfig extends GuiceServletContextListener {

    @Override
    protected Injector getInjector() {
        return Main.injector;
    }

}

FooResource.java

package org.arabellan.sandbox;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;

@Path("/foo")
public class FooResource {

    private final FooDao dao;

    @Inject
    public FooResource(FooDao dao) {
        this.dao = dao;
    }

    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") String id) {
        return Response.ok(dao.getById(id)).build();
    }

}

DynamoDBFooDao.java

package org.arabellan.sandbox;

import javax.inject.Singleton;

@Singleton
public class DynamoDBFooDao implements FooDao {

    public String getById(String id) {
        return id;
    }

}

FooDao.java

package org.arabellan.sandbox;

interface FooDao {

    String getById(String id);

}

I'm failing to understand the various components and how they work together. As such I keep getting the following error:

SEVERE: The following errors and warnings have been detected with resource and/or provider classes:
  SEVERE: Missing dependency for constructor public org.arabellan.sandbox.FooResource(org.arabellan.sandbox.FooDao) at parameter index 0

If I access the Guice injector directly in FooResource's constructor then it works. This tells me the Jetty/Jersey stuff is setup properly to serve the resource and Guice is able to build it's dependency tree correctly. I believe this means the problem lies in getting Jersey to use Guice when constructing the resource.

Exide
  • 859
  • 8
  • 24
  • Mistake number one is trying to mix Jersey 1.x and Jersey 2.x. You need to first figure out which one you're gonna use. Then work from there. The way you integrate Guice will be completely different from Jersey 1.x. and Jersey 2.x. – Paul Samsotha Nov 02 '18 at 03:48
  • That makes sense. I'll work on refactoring the code to use one or the other before introducing Guice to Jersey. – Exide Nov 02 '18 at 04:55
  • Jetty 9.4.8 has several CVEs associated with it, consider upgrading. - https://www.eclipse.org/lists/jetty-announce/msg00123.html – Joakim Erdfelt Nov 02 '18 at 10:20

2 Answers2

1

As pointed out in the comments, I needed to settle on either version 1 or 2 of Jersey before trying to hook up Guice. I went with Jersey 2.

My original assumption however was correct, the linkage between Guice and Jersey (or rather HK2) needed to be setup. I facilitated this with the GuiceToHK2 class. I didn't want to define DI bindings in two places so this solution loops through all of the Guice bindings, filters them to a specific package (optional), and then binds them within HK2.

build.gradle

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.projectlombok:lombok:1.16.18'
    compile 'com.google.inject:guice:4.1.0'
    compile 'com.google.inject.extensions:guice-servlet:4.1.0'
    compile 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.9.3'
    compile 'org.eclipse.jetty:jetty-server:9.4.8.v20171121'
    compile 'org.eclipse.jetty:jetty-servlet:9.4.8.v20171121'
    compile 'org.glassfish.jersey.containers:jersey-container-jetty-servlet:2.26'
    compile 'org.glassfish.jersey.media:jersey-media-sse:2.26'
    compile 'org.glassfish.jersey.inject:jersey-hk2:2.26'
}

Application.java

package org.arabellan.sandbox;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.message.DeflateEncoder;
import org.glassfish.jersey.message.GZipEncoder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.EncodingFilter;
import org.glassfish.jersey.servlet.ServletContainer;

class Application {

    void run() throws Exception {
        ServletContextHandler httpContext = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        ServletContainer container = new ServletContainer(buildResourceConfig());
        ServletHolder holder = new ServletHolder(container);
        httpContext.setContextPath("/");
        httpContext.addServlet(holder, "/*");

        Server jettyServer = new Server(8080);
        jettyServer.setHandler(httpContext);
        jettyServer.start();
    }

    private ResourceConfig buildResourceConfig() {
        ResourceConfig config = new ResourceConfig();
        config.register(new GuiceToHK2(Main.injector));
        config.register(JacksonJsonProvider.class);
        config.registerClasses(EncodingFilter.class, GZipEncoder.class, DeflateEncoder.class);
        config.packages("org.arabellan.sandbox");
        return config;
    }

}

GuiceToHK2.java

package com.flightstats.hub.app;

import com.google.inject.Injector;
import com.google.inject.Key;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.binding.AbstractBinder;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Slf4j
class GuiceToHK2 extends AbstractBinder {

    private final Injector injector;

    GuiceToHK2(Injector injector) {
        this.injector = injector;
    }

    @Override
    protected void configure() {
        injector.getBindings().forEach((key, value) -> {
            if (isNamedBinding(key)) {
                bindNamedClass(key);
            } else {
                bindClass(key);
            }
        });
    }

    private boolean isNamedBinding(Key<?> key) {
        return key.getAnnotationType() != null && key.getAnnotationType().getSimpleName().equals("Named");
    }

    private void bindClass(Key<?> key) {
        try {
            String typeName = key.getTypeLiteral().getType().getTypeName();
            log.info("mapping guice to hk2: {}", typeName);
            Class boundClass = Class.forName(typeName);
            bindFactory(new ServiceFactory<>(boundClass)).to(boundClass);
        } catch (ClassNotFoundException e) {
            log.warn("unable to bind {}", key);
        }
    }

    private void bindNamedClass(Key<?> key) {
        try {
            String typeName = key.getTypeLiteral().getType().getTypeName();
            Method value = key.getAnnotationType().getDeclaredMethod("value");
            String name = (String) value.invoke(key.getAnnotation());
            log.info("mapping guice to hk2: {} (named: {})", typeName, name);
            Class boundClass = Class.forName(typeName);
            bindFactory(new ServiceFactory<>(boundClass)).to(boundClass).named(name);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            log.warn("unable to bind {}", key);
        }
    }

    private class ServiceFactory<T> implements Factory<T> {

        private final Class<T> serviceClass;

        ServiceFactory(Class<T> serviceClass) {
            this.serviceClass = serviceClass;
        }

        public T provide() {
            return injector.getInstance(serviceClass);
        }

        public void dispose(T versionResource) {
            // do nothing
        }
    }

}

It's not a bulletproof solution but it solved my issue. It assumes that everything that needs to be injected into my resources is in the org.arabellan.sandbox package and isn't @Named.

UPDATE: Made the solution more generic by removing assumptions.

Exide
  • 859
  • 8
  • 24
0

hmmn for me it looks like you execute one of the following URLs:

so that the string-parameter "id" of this function: "public Response getById(@PathParam("id") String id)" is null. which results in your error.

It's just an assumption. Could you check it if i'm right, please

user3606183
  • 157
  • 9
  • The resource works just fine if I don't rely on Jersey to provide dependencies to the constructor. The error that I am getting refers to the constructor of the resource class, not the `getById` method call. – Exide Nov 02 '18 at 00:57