1

In a jetty/jersey2 self-hosted app api endpoints are generated programmatically inside ApiServiceConfig class ConfigurationProperties class reads and loads properties file into java.util.Properties class.

Jetty server instantiation is done in the following way.

     // Create and register resources
    final ResourceConfig resourceConfig = new ApiServiceConfig()
            .register(new DependencyInjectionBinder());

    ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);

    contextHandler.setContextPath("/mydomain/api");
    Server jettyServer = new Server(8585);
    jettyServer.setHandler(contextHandler);

    ServletHolder jerseyServlet = new ServletHolder(new ServletContainer(resourceConfig));
    contextHandler.addServlet(jerseyServlet, "/*");

    try {
        jettyServer.start();
        jettyServer.join();
    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } finally {
        jettyServer.destroy();
    }

public class ApiServiceConfig extends ResourceConfig {
    public ApiServiceConfig() {
       for(JsonNode jsonNode: nodeArray) {
           // JSON endpoint service description example.
           //{
           //    "service": "/item/{id}",
           //    "method": "GET",
           //    "process": {
           //        "@type": "com.mycompany.projectx.endpoint.services.GetController",
           //        "uri_param": "id",
           //        "type": "item",
           //        "fields": "uuid,name,content_area,title,grade,dok,bloom,item_banks,...,item_banks_titles"
           //    }
           //}
           // Json property "service" describes a URL pattern for a request (eg. "/item/{id}").
           final String path = jsonNode.get("service").asText();

           // Api RESTful verb ('GET', 'POST', etc.)
           final String method = jsonNode.get("method").asText();

           // Map a process description of a service to specific controller implementation class.
           // This is the instance creation where I want injection to happen.
           IController controller = this.objectMapper.convertValue(jsonNode.get("process"), AbstractBaseController.class);
           // Controller is added to a HashMap 
           ...
           final Resource.Builder resourceBuilder = Resource.builder();
           resourceBuilder.path(path);
           final ResourceMethod.Builder methodBuilder = resourceBuilder.addMethod(method);

           methodBuilder.produces(new MediaType("text", "plain"))
               handledBy((Inflector)(ctx) -> {
                   // Controller is retrieved from the HashMap 
                   controller.execute(new ProcessEvent());
                   ...
                   return responseResult;
               });
           final Resource resource = resourceBuilder.build();
           registerResources(resource);
       }
    }
}

GetController

public class GetController extends AbstractBaseController {

    @Config("data.cassandra")
    String connectionString; // == null, but should be a string injected.

    public GetController() {
    }

    @Override
    public ProcessEvent process(ProcessEvent event) throws Exception {

       String uri_param = this.uri_param;
       event.contentType = "application/json";

       event.object = ".Get method of Item endpoint got executed. Cassandra IP: " + getApplicationProperties().getProperty("data.cassandra");

       return event;
   }

Dependency resolver binder is registered in DependencyInjectionBinder class:

public class DependencyInjectionBinder extends AbstractBinder {

    @Override
    protected void configure() {

        bind(ConfigInjectionResolver.class)
            .to(new TypeLiteral<InjectionResolver<Config>>() {})
            .in(Singleton.class);
    }
}

ConfigInjectionResolver implements InjectionResolver and resolve some logic.

ApiServiceConfig in a loop goes through descriptions and creates endpoints. For each endpoint Resource builder is created, populated and resource is registered. During creation of an endpoint resource a class is instantiated with the help of jackson-databind:

IController controller = this.objectMapper.convertValue(jsonNode.get("process"), AbstractBaseController.class);

This class is supposed to get another class injected. Resolver DependencyInjectionBinder is not kicking in when the controller instance is created. If I move DependencyInjectionBinder instantiation into ApiServiceConfiguration constructor as a first operation, injection of a property into the controller instance doesn't happen anyway.

However when I register a class defined endpoint:

resourceConfig.register(AnEndpointClass.class);

DI resolver kicks in and adds dependency.

How to make Dependency resolver work for instantiated classes while programmatically create and register endpoints?

Maxim
  • 4,152
  • 8
  • 50
  • 77
  • _"ApiServiceConfig in a loop goes through descriptions and creates endpoints. For each endpoint Resource builder is created, populated and resource is registered. During creation of an endpoint resource a class is instantiated with the help of jackson-databind:"_ - Can you actually show this in code. – Paul Samsotha Apr 05 '16 at 02:33
  • Basically, you have not provided enough information to reproduce the problem. There are a lot of things missing. I cannot guess what you are doing. – Paul Samsotha Apr 05 '16 at 02:35
  • @peeskillet updated. Thank you for your attention! – Maxim Apr 05 '16 at 14:21
  • I don't seen anything being registered. Please show a full example with a complete resource builder and you register it and the controller. And how the controller fits with the resource builder – Paul Samsotha Apr 05 '16 at 14:28
  • I don't get exactly what you are trying to do, but if your only problem is injecting the controller, you can explicitly inject it. See [this example](http://stackoverflow.com/a/35953104/2587435) – Paul Samsotha Apr 05 '16 at 14:31
  • I still don't see how the controller fits in anywhere. I don't see it being used anywhere. So I don't know what exactly you are trying to do. But if all you want to do is inject it, see the link in my previous comment – Paul Samsotha Apr 05 '16 at 14:33
  • I want to inject @Config into controller instance that is instantiated by jackson. However, I believe that Dependency resolver is not in play yet. and doesn't kick in or Dependency resolver not gonna work at all because of jackson instantiation lifecycle. – Maxim Apr 05 '16 at 14:34

1 Answers1

1

To explicitly inject the objects, you will need to get a hold of the ServiceLocator, and call locator.inject(controller). You can get the ServiceLocator inside a Feature, as mentioned in this post.

And since you need to also register the resources using the controller, you will also need a way to register the resources inside the Feature. For that you can use a ModelProcessor. You can read more about it in the Jersey documentation. It allows you alter Jersey's resource model. Here we can just register all the resources we build programmatically.

Below is a complete example using Jersey Test Framework. You can run it like any other JUnit test.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.logging.Logger;

import javax.annotation.Priority;
import javax.inject.Singleton;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.core.Response;

import org.glassfish.hk2.api.Injectee;
import org.glassfish.hk2.api.InjectionResolver;
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.ServiceLocatorProvider;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.test.JerseyTest;

import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
 * Stack Overflow https://stackoverflow.com/q/36410420/2587435
 * 
 * Run this like any other JUnit test. Only one required dependency
 * 
 *  <dependency>
 *      <groupId>org.glassfish.jersey.test-framework.providers</groupId>
 *      <artifactId>jersey-test-framework-provider-inmemory</artifactId>
 *      <version>${jersey2.version}</version>
 *      <scope>test</scope>
 *  </dependency>
 * 
 * @author Paul Samsotha
 */
public class PropsInjectionTest extends JerseyTest {

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

    @Singleton
    public static class ConfigInjectionResolver implements InjectionResolver<Config> {

        @Override
        public Object resolve(Injectee injectee, ServiceHandle<?> root) {
            if (String.class == injectee.getRequiredType()) {
                Config anno = injectee.getParent().getAnnotation(Config.class);
                if (anno != null) {
                    String key = anno.value();
                    return key + "Value";
                }
            }
            return null;
        }

        @Override
        public boolean isConstructorParameterIndicator() { return false; }

        @Override
        public boolean isMethodParameterIndicator() { return false; }  
    }


    public static class Controller {
        @Config("Key")
        private String prop;

        public String getProp() {
            return prop;
        }
    }

    public static class ResourceFeature implements Feature {

        @Override
        public boolean configure(FeatureContext ctx) {
            final ServiceLocator locator = ServiceLocatorProvider.getServiceLocator(ctx);
            final Controller controller = new Controller();
            locator.inject(controller);

            final Resource.Builder builder = Resource.builder().path("test");
            final ResourceMethod.Builder methodBuilder = builder.addMethod("GET");
            methodBuilder.handledBy(new Inflector<ContainerRequestContext, String>(){
                @Override
                public String apply(ContainerRequestContext data) {
                    return controller.getProp();
                }
            });
            final Resource resource = builder.build();
            ctx.register(new MyModelProcessor(resource));
            return true;      
        }

        @Priority(100)
        static class MyModelProcessor implements ModelProcessor {
            private final Resource[] resources;

            public MyModelProcessor(Resource... resources) {
                this.resources = resources;
            }

            @Override
            public ResourceModel processResourceModel(ResourceModel rm, Configuration c) {
                final ResourceModel.Builder builder = new ResourceModel.Builder(false);
                // add any other resources not added in this feature. If there are none,
                // you can skip this loop
                for (Resource resource: rm.getResources()) {
                    builder.addResource(resource);
                }

                for (Resource resource: this.resources) {
                    builder.addResource(resource);
                }

                return builder.build();
            }

            @Override
            public ResourceModel processSubResource(ResourceModel rm, Configuration c) {
                return rm;
            }
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig()
                .register(new ResourceFeature())           
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true))
                .register(new AbstractBinder() {
                    @Override
                    protected void configure() {
                        bind(ConfigInjectionResolver.class)
                                .to(new TypeLiteral<InjectionResolver<Config>>(){})
                                .in(Singleton.class);
                    }
                });
    }

    @Test
    public void allShouldBeGood() {
        final Response response = target("test").request().get();
        assertThat(response.readEntity(String.class), is("KeyValue"));
    }
}
Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720