Search code examples
javarestdependency-injectionjersey-2.0hk2

Jersey HK2 Dependency Injection


I'm writing a simple microservices that exposes REST API. So I started working with Jersey and of course I need to Inject my object into jersey resources. Basically I have 2 classes that defines a set of resources and some of them need to use another service.

So basically I have:

public interface MyService {

String getServiceName();

void doService();

}

2 implementations of this interface (MyServiceBean and MyAlternativeServiceBean)

and, as far as I understood reading jersey docs, I defined an hk2 Binder:

public class MyBinder implements Binder{

@Override
public void bind(DynamicConfiguration config) {

    DescriptorImpl descriptor = BuilderHelper.link(MyServiceBean.class).named("MyServiceBean").to(MyService.class).build();
    config.bind(descriptor);


    config.bind(BuilderHelper.link(MyAlternativeServiceBean.class).named("MyAlternativeServiceBean").to(MyService.class).build());

}

I registered this binder to the ApplicationConfig class

public class ApplicationConfig extends ResourceConfig{

public ApplicationConfig(){
    property("property.value", "MyAlternativeServiceImplementation");
    registerInstances(new MyBinder());
}

}

And annotated properly into the resources

@Path("first")
    public class First {

        @Inject @Named(value = "MyServiceBean")
        private MyService myService;
    //...
    }

    @Path("second")
    public class Second {

        @Inject @Named(value = "MyAlternativeServiceBean")
        private MyService myService;
    //...
    }

All works until MyService implementation have no args constructor. But in 1 case I need to provide a dependency also to MyAlternativeServiceBean.

Here is the constructor

@Inject @Named("property.value")
    public MyAlternativeServiceBean(String property){
        this.property = property;
    }

But I get an exception:

javax.servlet.ServletException: A MultiException has 5 exceptions.  They are:|1. org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at Injectee(requiredType=String,parent=MyAlternativeServiceBean,qualifiers={}),position=0,optional=false,self=false,unqualified=null,2080509613)|2. java.lang.IllegalArgumentException: While attempting to resolve the dependencies of com.hpe.services.MyAlternativeServiceBean errors were found|3. java.lang.IllegalStateException: Unable to perform operation: resolve on com.hpe.services.MyAlternativeServiceBean|4. java.lang.IllegalArgumentException: While attempting to resolve the dependencies of com.hpe.tests.SecondEntryPoint errors were found|5. java.lang.IllegalStateException: Unable to perform operation: resolve on com.hpe.tests.SecondEntryPoint|
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:392)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:381)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:344)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:219)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:684)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:501)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:229)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1086)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:427)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1020)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116)
    at org.eclipse.jetty.server.Server.handle(Server.java:370)
    at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:494)
    at org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:973)
    at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:1035)
    at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:641)
    at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:231)
    at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:696)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:53)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543)
    at java.lang.Thread.run(Thread.java:745)

Basically I don't konw how to inject properties/constants (that I can read from a configuration file for instance) in hk2

Thanks

Regards


Solution

  • What you can do is create a custom annotation

    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Config {
        String value();
    }
    

    Then create an InjectionResolver for it (which allows for injection using custom annotations)

    public static class ConfigInjectionResolver implements InjectionResolver<Config> {
    
        private static final Map<String, String> properties = new HashMap<>();
    
        public ConfigInjectionResolver() {
            properties.put("greeting.message", "Hello World");
        }
    
        @Override
        public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
            if (String.class == injectee.getRequiredType()) {
                AnnotatedElement elem = injectee.getParent();
                if (elem instanceof Constructor) {
                    Constructor ctor = (Constructor) elem;
                    Config config = (Config) ctor.getParameterAnnotations()[injectee.getPosition()][0];
                    return properties.get(config.value());
                } else {
                    Config config = elem.getAnnotation(Config.class);
                    return properties.get(config.value());
                }
            }
            return null;
        }
    
        @Override
        public boolean isConstructorParameterIndicator() { return true; }
    
        @Override
        public boolean isMethodParameterIndicator() { return false; }
    }
    

    This example just uses a Map, but I'm sure you can figure out how to make it use Properties. Once you register the InjectionResolver, you can now just do

    public SomeService(@Config("some.property") String property) {}
    

    Here is a complete test case

    import org.glassfish.hk2.api.Injectee;
    import org.glassfish.hk2.api.InjectionResolver;
    import org.glassfish.hk2.api.ServiceHandle;
    import org.glassfish.hk2.api.TypeLiteral;
    import org.glassfish.hk2.utilities.binding.AbstractBinder;
    import org.glassfish.jersey.filter.LoggingFilter;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.glassfish.jersey.test.JerseyTest;
    import org.junit.Test;
    
    import javax.inject.Inject;
    import javax.inject.Singleton;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Response;
    import java.lang.annotation.*;
    import java.lang.reflect.AnnotatedElement;
    import java.lang.reflect.Constructor;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.logging.Logger;
    
    import static org.junit.Assert.*;
    
    /**
     * Run like any other JUnit Test. Only one required dependency
     *
     * <dependency>
     *   <groupId>org.glassfish.jersey.test-framework.providers</groupId>
     *   <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
     *   <version>${jersey2.version}</version>
     * </dependency>
     *
     * @author Paul Samsotha
     */
    public class ConfigExample extends JerseyTest {
    
        @Target({ElementType.FIELD, ElementType.PARAMETER})
        @Retention(RetentionPolicy.RUNTIME)
        public static @interface Config {
            String value();
        }
    
        public static class ConfigInjectionResolver implements InjectionResolver<Config> {
    
            private static final Map<String, String> properties = new HashMap<>();
    
            public ConfigInjectionResolver() {
                properties.put("greeting.message", "Hello World");
            }
    
            @Override
            public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
                if (String.class == injectee.getRequiredType()) {
                    AnnotatedElement elem = injectee.getParent();
                    if (elem instanceof Constructor) {
                        Constructor ctor = (Constructor) elem;
                        Config config = (Config) ctor.getParameterAnnotations()[injectee.getPosition()][0];
                        return properties.get(config.value());
                    } else {
                        Config config = elem.getAnnotation(Config.class);
                        return properties.get(config.value());
                    }
                }
                return null;
            }
    
            @Override
            public boolean isConstructorParameterIndicator() { return true; }
    
            @Override
            public boolean isMethodParameterIndicator() { return false; }
        }
    
    
        private static interface GreetingService {
            String getGreeting();
        }
    
        private static class ConfiguredGreetingService implements GreetingService {
            private String message;
    
            public ConfiguredGreetingService(@Config("greeting.message") String message) {
                this.message = message;
            }
    
            @Override
            public String getGreeting() {
                return this.message;
            }
        }
    
        @Path("greeting")
        public static class GreetingResource {
    
            @Inject
            private GreetingService greetingService;
    
            @GET
            public String getConfigProp() {
                return greetingService.getGreeting();
            }
        }
    
        @Override
        public ResourceConfig configure() {
            ResourceConfig config = new ResourceConfig(GreetingResource.class);
            config.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
            config.register(new AbstractBinder(){
                @Override
                protected void configure() {
                    bind(ConfiguredGreetingService.class).to(GreetingService.class).in(Singleton.class);
                    bind(ConfigInjectionResolver.class)
                            .to(new TypeLiteral<InjectionResolver<Config>>(){})
                            .in(Singleton.class);
                }
            });
            return config;
        }
    
        @Test
        public void should_get_configured_greeting() {
            final Response response = target("greeting")
                    .request().get();
            assertEquals("Hello World", response.readEntity(String.class));
        }
    }