jersey + grizzly + hk2: Dependency injection, but

2019-02-17 19:03发布

问题:

Following up on Jersey + HK2 + Grizzly: Proper way to inject EntityManager?, I would like to understand how it is possible use dependency injection in classes which are not jersey resources.

As an example, I might have background tasks running in an ExecutorService, and they might need an EntityManager. If I attempt to @Inject the EntityManager into the class, nothing happens. Injecting it into a @Path-annotated jersey resource class, injecting works fine.

The application is running as a standalone JVM, not on a Java EE application server.

Update: I have created a test scenario to demonstrate what I mean. The code is running a standalone Grizzly server with a Jersey resource, as well as an ExecutorService. A Callable is submitted to the ExecutorService.

Injection of the EntityManager into the resource works, but not into the Callable. There the EntityManager remains null.

Please advise if the code is better kept here than on github.

回答1:

So to really understand how HK2 works, you should become familiar with its ServiceLocator. It is analogous to Spring ApplicationContext, which is the main container for the DI framework.

In a standalone app, you could bootstrap the DI container simply by doing

ServiceLocatorFactory locatorFactory = ServiceLocatorFactory.getInstance();
ServiceLocator serviceLocator = locatorFactory.create("TestLocator");
ServiceLocatorUtilities.bind(serviceLocator, new EntityManagerProvider());

Now your EntityManagerProvider is registered into the container. You can lookup the EntityManager simply by doing

EntityManager em = serviceLocator.getService(EntityManager.class);

Now in order to be able to take advantage of injection by the container, the service needs to be managed by the container. For example say you have this

public class BackgroundTask implements Callable<String> {

    @Inject
    EntityManager em;

    @Override
    public String call() throws Exception {
        ...
}

which you actually do. The problem is, the BackgroundTask is not managed by the container. So even in a standalone bootstrap (like the three lines of code above), instantiating the task

BackgroundTask task = new BackgroundTask();

does nothing, as far as injection, as the task class is not managed by the container, and you are creating it yourself. If you wanted it managed, there a few ways to register it to the container. You've discovered one already (use an AbstractBinder) and register the binder to the ServiceLocator. Then instead of instantiating the class yourself, you just request it, like the EntityManager example above.

Or you can simply explicitly inject the task, i.e

BackgroundTask task = new BackgroundTask(); 
serviceLocator.inject(task);

What that did was cause the locator to lookup the EntityManager and inject it into your task.

So how does this all fit in with Jersey? Jersey (partly) handles lookup of services and injection into resources during it's runtime. That's why it work's in your Jersey application. When the EntityManager is needed, it looks up the service an injects it into the resource instance.

So the next question is, if the tasks are being run outside the scope the Jersey application, how can you inject the task? For the most part, all the above is pretty much the gist of it. Jersey has it's own ServiceLocator, and it's not easy to try a obtain a reference to it. We could give Jersey our ServiceLocator, but Jersey ultimately still creates it's own locator and will populate it with our locator. So ultimately there would still be two locators. You can see an example of what I mean in the refactored code below, where it check the references in the ServiceLocatorFeature.

But if you do want to provide the ServiceLocator to Jersey, you can pass it to the Grizzly server factory method

server = GrizzlyHttpServerFactory.createHttpServer(
        URI.create(BASE_URI),
        config, 
        serviceLocator
);

Now you can still use your locator outside of Jersey. Honestly though, in this case, you could not involve Jersey at all and just keep your own locator, and just register the EntityManagerProvider with both Jersey and your ServiceLocator. I don't see it really making much difference, except for the extra line of code. Functionally, I don't see any change.

To learn more about HK2, I highly recommend thoroughly going through the user guide. You'll learn a lot about what goes on under the hood with Jersey, and also learn about features that you can incorporate into a Jersey application.

Below is the complete refactor of your test. I didn't really change much. Any changes I made are pretty much discussed above.

public class DependencyInjectionTest {

    private final ServiceLocatorFactory locatorFactory
            = ServiceLocatorFactory.getInstance();
    private ServiceLocator serviceLocator;

    private final static String BASE_URI = "http://localhost:8888/";
    private final static String OK = "OK";
    private HttpServer server;
    private ExecutorService backgroundService;

    public class EntityManagerProvider extends AbstractBinder
            implements Factory<EntityManager> {

        private final EntityManagerFactory emf;

        public EntityManagerProvider() {
            emf = Persistence.createEntityManagerFactory("derbypu");
        }

        @Override
        protected void configure() {
            bindFactory(this).to(EntityManager.class);
            System.out.println("EntityManager binding done");
        }

        @Override
        public EntityManager provide() {
            EntityManager em = emf.createEntityManager();
            System.out.println("New EntityManager created");
            return em;
        }

        @Override
        public void dispose(EntityManager em) {
            em.close();
        }
    }

    public class BackgroundTask implements Callable<String> {

        @Inject
        EntityManager em;

        @Override
        public String call() throws Exception {
            System.out.println("Background task started");
            Assert.assertNotNull(em);   // will throw exception

            System.out.println("EntityManager is not null");
            return OK;
        }
    }

    public class ServiceLocatorFeature implements Feature {

        @Override
        public boolean configure(FeatureContext context) {
            ServiceLocator jerseyLocator
                    = org.glassfish.jersey.ServiceLocatorProvider
                            .getServiceLocator(context);

            System.out.println("ServiceLocators are the same: "
                    + (jerseyLocator == serviceLocator));

            return true;
        }
    }

    @Path("/test")
    public static class JerseyResource {

        @Inject
        EntityManager em;

        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public Response doGet() {
            System.out.println("GET request received");
            Assert.assertNotNull(em);

            System.out.println("EntityManager is not null");
            return Response.ok()
                    .entity(OK)
                    .build();
        }
    }

    @Before
    public void setUp() {
        serviceLocator = locatorFactory.create("TestLocator");
        ServiceLocatorUtilities.bind(serviceLocator, new EntityManagerProvider());

        System.out.println("Setting up");
        ResourceConfig config = new ResourceConfig();
        config.register(new ServiceLocatorFeature());
        //config.register(new EntityManagerProvider());
        config.register(JerseyResource.class);
        // can't find a better way to register the resource
        //config.registerInstances(JerseyResource.class);   

        server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create(BASE_URI),
                config, serviceLocator
        );

        backgroundService = Executors.newSingleThreadScheduledExecutor();
    }

    @After
    public void tearDown() {
        System.out.println("Shutting down");
        server.shutdownNow();
        backgroundService.shutdownNow();
    }

    @Test
    public void testScheduledBackgroundTask() throws Exception {
        Assert.assertTrue(server.isStarted());

        BackgroundTask task = new BackgroundTask();
        serviceLocator.inject(task);
        Future<String> f = backgroundService.submit(task);
        System.out.println("Background task submitted");

        try {
            Assert.assertEquals(OK, f.get());   // forces Exception
        } catch (ExecutionException | InterruptedException ex) {
            System.out.println("Caught exception " + ex.getMessage());
            ex.printStackTrace();

            Assert.fail();
        }
    }

    @Test
    public void testBackgroundTask() throws Exception {
        Assert.assertTrue(server.isStarted());

        BackgroundTask task = new BackgroundTask();
        serviceLocator.inject(task);
        System.out.println("Background task instantiated");

        Assert.assertEquals(OK, task.call());
    }

    @Test
    public void testResource() {
        Assert.assertTrue(server.isStarted());

        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(BASE_URI);

        Response r = target.path("test")
                .request()
                .get();
        Assert.assertEquals(200, r.getStatus());
        Assert.assertEquals(OK, r.readEntity(String.class));
    }
}

Another thing I might mention is that you should need only one EntityManagerFactory for the application. It's expensive to create, and creating one every time the EntityManager is needed is not a good idea. See one solution here.



回答2:

Statement: Implementation of Dependency Injection using Grizzly and Jersey

Please follow the below steps to do the same –

  • List item Create a class called Hk2Feature which implements Feature -

    package com.sample.di;
    import javax.ws.rs.core.Feature;
    import javax.ws.rs.core.FeatureContext;
    import javax.ws.rs.ext.Provider;
    @Provider
    public class Hk2Feature implements Feature {
      public boolean configure(FeatureContext context) {
        context.register(new MyAppBinder());
        return true;
      }
    }
    
  • List item Create a class called MyAppBinder which extends AbstractBinder and you need to register all the services here like below –

    package com.sample.di;
    import org.glassfish.hk2.utilities.binding.AbstractBinder;
    public class MyAppBinder extends AbstractBinder {
      @Override
      protected void configure() {
        bind(MainService.class).to(MainService.class);
      }
    }
    
  • List item Now, it’s time to write your own services and inject all the required services in your appropriate controllers like below code – package com.sample.di;

    public class MainService {
      public String testService(String name) {
        return “Hi” + name + “..Testing Dependency Injection using Grizlly Jersey “;
      }
    }
    package com.sample.di;
    import javax.inject.Inject;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.QueryParam;
    import javax.ws.rs.core.MediaType;
    @Path(“/main”)
    public class MainController {
        @Inject
        public MainService mainService;
        @GET
        public String get(@QueryParam(“name”) String name) {
            return mainService.testService(name);
        }
        @GET
        @Path(“/test”)
        @Produces(MediaType.APPLICATION_JSON)
        public String ping() {
            return “OK”;
        }
    }
    

Now hit the url http://localhost:8080/main?name=Tanuj and you will get your result. This is how you can achieve dependency injection in Grizzly Jersey application. Find the detailed implementation of the above skeleton in my repo. Happy Coding