August 14, 2022

Jersey and HK2 dependency injection (auto scanning)

This article shows how to use HK2 dependency injection framework in Jersey and enable auto-scanning auto-discovery of the declared @Contract and @Service components.

P.S. Tested with Jersey 3.0.2 and Java 1.8

1. Jersey and HK2 dependency injection

Jersey, by default, uses HK2 (Hundred-Kilobyte Kernel) for dependency injection. And the HK2 is an implementation of JSR-330(Dependency Injection for Java).

For Jersey and HK2 development, we only need to declare jersey-hk2.

pom.xml

  <dependency>
      <groupId>org.glassfish.jersey.inject</groupId>
      <artifactId>jersey-hk2</artifactId>
  </dependency>

2. @Contract, @Service, and @Inject

2.1 Review a simple dependency injection in Jersey.

@Contract
public interface MessageService {
  String getHello(String name);
}

@Service
public class MessageServiceImpl implements MessageService {

    @Override
    public String getHello() {
        return "Hello World Jersey and HK2"
    }

}

@Path("/hello")
public class MyResource {

    @Inject
    private MessageService msgService;

}

2.2 If we call the /hello endpoint, Jersey will throw the exception “no object available”?

Terminal

org.glassfish.hk2.api.UnsatisfiedDependencyException:
    There was no object available in __HK2_Generated_0 for injection at...

Note

For the Jersey and HK2 injection to work correctly, we must register the @Contract and @Service manually or enable the auto-scanning or auto-discovery feature. At this moment, I started to miss the Spring framework auto-scanning feature 🙂

3. HK2 manual register @Contract and @Service

In Jersey and HK2, we can use AbstractBinder to register the @Contract and @Service manually. This AbstractBinder manual registration is suitable for simple injection, and we, developers, control what to declare and inject.

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

  // Starts Grizzly HTTP server
  public static HttpServer startServer() {

        final ResourceConfig config = new ResourceConfig();
        config.register(MyResource.class);

        config.register(new AbstractBinder(){
            @Override
            protected void configure() {
                // map this service to this contract
                bind(MessageServiceImpl.class).to(MessageService.class);
            }
        });

        return GrizzlyHttpServerFactory
                  .createHttpServer(URI.create(BASE_URI), config);

    }

4. HK2 auto scanning @Contract and @Service

To enable Jersey and HK2 auto scanning, we need two things:

  1. HK2 metadata called inhabitant files.
  2. ServiceLocator to find the generated inhabitant files and loads the declared @Contract and @Service.

4.1 HK2 inhabitant files

Declares the hk2-metadata-generator dependency on the project classpath, and it will generate the inhabitant files during the compile process and place the output file in META-INF/hk2-locator/default.

pom.xml

  <dependency>
      <groupId>org.glassfish.hk2</groupId>
      <artifactId>hk2-metadata-generator</artifactId>
      <version>3.0.2</version>
  </dependency>

The below is the auto-generated inhabitant file.

META-INF/hk2-locator/default

#
# Generated by hk2-metadata-generator
#

[com.favtuts.service.MessageServiceImpl]S
contract={com.favtuts.service.MessageService}

Note

Read this HK2 Inhabitant Generators

4.2 ServiceLocator to read the inhabitant files

4.2.1 We can use ServiceLocatorUtilities to get the ServiceLocator and pass it to the Grizzly HTTP Server.

  public static HttpServer startServer() {

      // scan packages
      final ResourceConfig config = new ResourceConfig();
      config.register(MyResource.class);

      // Get a ServiceLocator
      ServiceLocator locator =
            ServiceLocatorUtilities.createAndPopulateServiceLocator();

      // pass the ServiceLocator to Grizzly Http Server
      final HttpServer httpServer =
              GrizzlyHttpServerFactory
                  .createHttpServer(URI.create(BASE_URI), config, locator);

      return httpServer;

  }

4.2.2 Alternatively, we can create a Feature to enable auto-scanning.

AutoScanFeature.java

package com.favtuts.config;

import jakarta.inject.Inject;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.hk2.api.Populator;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ClasspathDescriptorFileFinder;
import org.glassfish.hk2.utilities.DuplicatePostProcessor;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AutoScanFeature implements Feature {

    @Inject
    ServiceLocator serviceLocator;

    @Override
    public boolean configure(FeatureContext context) {

        DynamicConfigurationService dcs =
                serviceLocator.getService(DynamicConfigurationService.class);
        Populator populator = dcs.getPopulator();
        try {
            // Populator - populate HK2 service locators from inhabitants files
            // ClasspathDescriptorFileFinder - find files from META-INF/hk2-locator/default
            populator.populate(
                    new ClasspathDescriptorFileFinder(this.getClass().getClassLoader()),
                    new DuplicatePostProcessor());

        } catch (IOException | MultiException ex) {
            Logger.getLogger(AutoScanFeature.class.getName()).log(Level.SEVERE, null, ex);
        }
        return true;
    }

}

Register the above feature.

  public static HttpServer startServer() {

      final ResourceConfig config = new ResourceConfig();
      config.register(MyResource.class);
      // enable auto scan
      config.register(AutoScanFeature.class);

      return GrizzlyHttpServerFactory
          .createHttpServer(URI.create(BASE_URI), config);

  }

5. Jersey and HK2, @Service @Named complete example

The below is a complete Jersey and HK2 example to auto scan the @Service and @Contract and @Named (qualifier, multiple implementations for the same interface).

5.1 Project Directory Structure

C:.
│   pom.xml
│   README.md
├───src
│   └───main
│       └───java
│           └───com
│               └───favtuts
│                   │   MainApp.java
│                   │   MyResource.java
│                   │
│                   ├───config
│                   │       AutoScanFeature.java
│                   │
│                   └───service
│                           MessageService.java
│                           MessageServiceAwsImpl.java
│                           MessageServiceAzureImpl.java
│
└───target
    │   jersey-hk2-dependency-injection.jar
    │   │
    │   └───META-INF
    │       └───hk2-locator
    │               default

5.2 Project Dependencies

pom.xml

  <dependency>
      <groupId>org.glassfish.jersey.containers</groupId>
      <artifactId>jersey-container-grizzly2-http</artifactId>
  </dependency>

  <dependency>
      <groupId>org.glassfish.jersey.inject</groupId>
      <artifactId>jersey-hk2</artifactId>
  </dependency>

  <dependency>
      <groupId>org.glassfish.hk2</groupId>
      <artifactId>hk2-metadata-generator</artifactId>
      <version>3.0.2</version>
  </dependency>

5.3 @Contract @Service and @Named

We use the @Contract interface and two @Service implementations, and we use @Named to give each implementation a unique name.

MessageService.java

package com.favtuts.service;

import org.jvnet.hk2.annotations.Contract;

@Contract
public interface MessageService {
  String getHello();
}

MessageServiceAwsImpl.java

package com.favtuts.service;

import jakarta.inject.Named;
import org.jvnet.hk2.annotations.Service;

@Service @Named("aws")
public class MessageServiceAwsImpl implements MessageService {

  @Override
  public String getHello() {
      return "Hello from AWS";
  }

}

MessageServiceAzureImpl.java

package com.favtuts.service;

import jakarta.inject.Named;
import org.jvnet.hk2.annotations.Service;

@Service @Named("azure")
public class MessageServiceAzureImpl implements MessageService {

  @Override
  public String getHello() {
      return "Hello from Azure";
  }

}

5.4 Jersey Resources @Path and @Inject

MyResource.java

package com.favtuts;

import com.favtuts.service.MessageService;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class MyResource {

  @Inject
  @Named("aws")
  private MessageService awsService;

  @Inject
  @Named("azure")
  private MessageService azureService;

  @Path("/hk2/aws")
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String helloAws() {
      return awsService.getHello();
  }

  @Path("/hk2/azure")
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String helloAzure() {
      return azureService.getHello();
  }

}

5.5 Jersey application starter

Run the below MainApp.java, and it will start Grizzly HTTP server, auto scan the @Service, and publish the endpoints.

MainApp.java

package com.favtuts;

import com.favtuts.config.AutoScanFeature;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;

public class MainApp {

  public static final String BASE_URI = "http://localhost:8080/";

  // Starts Grizzly HTTP server
  public static HttpServer startServer() {

      final ResourceConfig config = new ResourceConfig();
      config.register(MyResource.class);

      // enable the auto scanning, see above 4.2.2
      config.register(AutoScanFeature.class);

      final HttpServer httpServer =
              GrizzlyHttpServerFactory
                  .createHttpServer(URI.create(BASE_URI), config);

      return httpServer;

  }

  public static void main(String[] args) {

      try {

          final HttpServer httpServer = startServer();

          // add jvm shutdown hook
          Runtime.getRuntime().addShutdownHook(new Thread(() -> {
              try {
                  System.out.println("Shutting down the application...");

                  httpServer.shutdownNow();

                  System.out.println("Done, exit.");
              } catch (Exception e) {
                  Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, e);
              }
          }));

          System.out.println(String.format("Application started.%nStop the application using CTRL+C"));

          // block and wait shut down signal, like CTRL+C
          Thread.currentThread().join();

      } catch (InterruptedException ex) {
          Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
      }

  }
}

5.6 Test it with cURL

We can use the cURL command to test the /hello endpoints.

Terminal

> curl http://localhost:8080/hello/hk2/azure
Hello from Azure

> curl http://localhost:8080/hello/hk2/aws
Hello from AWS

> curl -i http://localhost:8080/hello/hk2/aws
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 14

Hello from AWS

6. Download Source Code

$ git clone https://github.com/favtuts/java-jax-rs-tutorials.git

$ cd jersey-hk2-dependency-injection

$ mvn clean package

$ java -jar target/jersey-hk2-dependency-injection-1.0.jar

7. References

Leave a Reply

Your email address will not be published.