4

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

Raffaele
  • 461
  • 1
  • 7
  • 20
  • What does the constructor look like? Where do you expect those constructor argumemts to be coming from? – Paul Samsotha Jan 01 '17 at 02:11
  • I would like to know how can I fully integrate jersey with hk2: I mean that hk2 can build object for me and then inject those object into jersey resources (and of course into other objects where needed). At the moment I don't know how to tell hk2 to create 2 singletons "MyServiceBean" and "MyAlternativeServiceBean". I'm thinking the spring way where basically the framework build objects for me and keep those objects into the AppContext. – Raffaele Jan 01 '17 at 14:45
  • 2
    Not sure what you're getting at. The error is telling you that it can't find a suitable constructor because you added constructor arguments that aren't suitable injection points, i.e. either you have no services to inject to the constructor or you haven't marked the constructor as an injection point with `@Inject`. This is not different from how Spring would work, If you try to have constructor arguments and it is not marked as an injection point with `@Autowired` or there are no beans to inject, you will get similar error. It is no different. – Paul Samsotha Jan 01 '17 at 14:54
  • This is reason for my comment – Paul Samsotha Jan 01 '17 at 14:54
  • I edited my question. Of course all works as expected for MyServiceBean because this one needs no properties/dependencies so it has the 0args constructor. – Raffaele Jan 01 '17 at 16:03
  • Check [this out](https://psamsotha.github.io/jersey/2015/12/27/jersey-configuration-properties.html) – Paul Samsotha Jan 01 '17 at 16:12
  • Thanks @peeskillet. As far as I understand this article explain how to inject properties/constants inside jersey Resources. This is of course useful to me but in this specific scenario I'm working on a common bean not a rest resource. My problem is that I don't know how to tell hk2 to inject constants into MyAlternativeServiceBean creating a singleton that will be injected where required (in this case inside a resources) – Raffaele Jan 01 '17 at 18:05
  • It should still work for services. Have you at least tried it? – Paul Samsotha Jan 02 '17 at 02:06
  • I tried but it's not working. I used the "Custom Injection of Properties" approach. Anyway I think it's not working because MyAlternativeServiceBean is not a jersey resource so the ConfigInjectionResolver is never called.. – Raffaele Jan 02 '17 at 10:38
  • 1
    [Here](https://gist.github.com/psamsotha/bc732e897e65916422245295a9369737) is an updated version. (In the InjectionResolver) You need to get the annotation from the constructor parameter. Otherwise the `injectee.getParent()` is the actual constructor, not the parameter, which cause the null. Also the constructor parameter indicator should also have returned true. – Paul Samsotha Jan 02 '17 at 11:22
  • Can you post the code from the last comment as an answer since it seems correct to me? – jwells131313 Jan 02 '17 at 21:51

1 Answers1

8

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));
    }
}
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720