13

I am having trouble getting a very basic implementation of a singleton class off the ground with Jersey 2 (2.7) and only Jersey's built-in HK2 dependency injection. I am running this on Tomcat.

My goal is to create a singleton instance of a support class that will be used by various web service methods. I don't have a strong preference between constructor injection, method injection, and annotating a class member (as I do below).

Here is my to-be-singleton class:

package singletest;

import javax.inject.Singleton;

@Singleton
public class JustOne {
    private int secretNumber = 0;

    public void hitMe(int input) {
        secretNumber += input;
    }

    @Override
    public String toString() {
        return String.format("{ \"secretNumber\": %s }", secretNumber);
    }
}

Here is my application class:

package singletest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;

@ApplicationPath("/*")
public class MainApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        classes.add(TestResource.class);
        return classes;
    }

    @Override
    public Set<Object> getSingletons() {
        Set<Object> singletons = new HashSet<>();
        singletons.add(new JustOneProvider());
        return singletons;
    }
}

Here is my Provider/ContextResolver class:

package singletest;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

@Provider
public class JustOneProvider implements ContextResolver<JustOne> {
    private static JustOne justOne = new JustOne();

    @Override
    public JustOne getContext(Class<?> type) {
        return justOne;
    }
}

web.xml:

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <filter>
        <filter-name>singletest.MainApplication</filter-name>
        <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>singletest.MainApplication</param-value>
        </init-param>
        <!-- pass to next filter if Jersey/App returns 404 -->
        <init-param>
            <param-name>jersey.config.servlet.filter.forwardOn404</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>singletest.MainApplication</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

The resource where I intend to inject the singleton instance of my JustOne class:

package singletest;

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/test")
public class TestResource {

    @Inject
    private JustOne justOne;

    @GET
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/func1/{input}")
    public Response func1(@PathParam("input") int input) {
        justOne.hitMe(input);
        String responseData = justOne.toString();
        return Response.ok(responseData).build();
    }
}

This warning is raised when the war is deployed/initialized in Jersey:

Apr 28, 2014 11:48:25 AM org.glassfish.jersey.server.ApplicationHandler initialize
INFO: Initiating Jersey application, version Jersey: 2.7 ${buildNumber}...
Apr 28, 2014 11:48:25 AM org.glassfish.jersey.internal.inject.Providers checkProviderRuntime
WARNING: A provider java.lang.Class registered in SERVER runtime does not implement any provider interfaces applicable in the SERVER runtime. Due to constraint configuration problems the provider java.lang.Class will be ignored.

And the error I get when I call this web service:

type Exception report

message A MultiException has 3 exceptions. They are:

description The server encountered an internal error that prevented it from fulfilling this request.

exception

javax.servlet.ServletException: A MultiException has 3 exceptions.  They are:
1. org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at Injectee(requiredType=JustOne,parent=TestResource,qualifiers={}),position=-1,optional=false,self=false,unqualified=null,58952407)
2. java.lang.IllegalArgumentException: While attempting to resolve the dependencies of singletest.TestResource errors were found
3. java.lang.IllegalStateException: Unable to perform operation: resolve on singletest.TestResource

    org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:333)
    org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:372)
    org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:335)
    org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:218)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)

So, I get it: hk2 isn't getting enough information to create/bind a JustOne instance and/or find/use my JustOneProvider. I'm sure I'm missing something basic. Any guidance would be appreciated.

ricb
  • 1,197
  • 2
  • 12
  • 23

2 Answers2

19
bind(JustOne.class).to(JustOne.class).in(Singleton.class);     

is the most important part. Without "in(Singleton.class)" multiple instances of JustOne will be created.

user1739372
  • 193
  • 1
  • 5
  • Well, no, that's not true. In this context you don't need "in(Singleton.class)". I have multiple binder/feature pairs in use, and they all use "bind(new JustOne()).to(JustOne.class)" (for various classes other than the JustOne I used for my example here). None of these pairs use "in(Singleton.class)". Still, if I set a breakpoint in the constructor of a singleton created in this way, it only gets called once, even if a web service that has one of these singleton classes injected is called dozens of times. – ricb Nov 13 '14 at 01:39
  • he/she brings up a good point tho. How does the HK2 know to register as singleton if the lifecycle is never specified. Is Singleton the default lifecycle? – leojh Jan 11 '16 at 19:09
  • user1739372's code is "bind(JustOne.class).to...". My code is "bind(new JustOne()).to...". Perhaps the fact that I am creating the instance to use is what drives it being treated as a singleton. The other way may work as well; I have not tried it that way. – ricb Feb 25 '16 at 18:09
  • 4
    `bind(new JustOne()).to...` is the syntax for binding a _constant_, not a singleton. It is true that only one instance of `JustOne` will be created, but it will be created by you, manually, not by HK2. The difference between the two forms will probably only be important if your `JustOne` singleton also requires dependencies to be injected. – Logan Pickup May 30 '16 at 04:28
  • 2
    I can confirm that this is exactly what you would need to do when you want a singleton instance that will be injected by HK2 itself, instead of providing a class instance manually. – Joeri Sykora Jun 02 '16 at 13:26
15

Okay, after spending a very healthy chunk of time on this, I got it working.

I used an HK2 AbstractBinder and JAX-RS Feature (javax.ws.rs.core.Feature).

It turned out that no Provider/ContextResolver was required.

Application:

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;

@ApplicationPath("/*")
public class MainApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        classes.add(TestResource.class);
        classes.add(JustOneFeature.class);
        return classes;
    }

}

To-be-singleton class:

public class JustOne {
    private int secretNumber = 0;

    public int getSecretNumber() {
        return secretNumber;
    }

    public void bumpSecretNumber() {
        secretNumber += 1;
    }
}

Binder:

import org.glassfish.hk2.utilities.binding.AbstractBinder;

public class JustOneBinder extends AbstractBinder {
    @Override
    protected void configure() {
        bind(new JustOne()).to(JustOne.class);
    }
}

Feature:

import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;

public class JustOneFeature implements Feature {

    @Override
    public boolean configure(final FeatureContext context) {
        context.register(new JustOneBinder());
        return true;
    }
}

Resource that gets singleton injected:

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/test")
public class TestResource {

    @Inject
    private JustOne justOne;

    @GET
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/func1/{input}")
    public Response func1(@PathParam("input") int input) {
        justOne.bumpSecretNumber();
        String responseData = String.format("{ \"result\": %s }", input + justOne.getSecretNumber());
        return Response.ok(responseData).build();
    }
}
ricb
  • 1,197
  • 2
  • 12
  • 23
  • 1
    I think it's even simpler than this. Can't we get rid of the Feature completely, and instead just bind to the Singleton? In your configure method in the Binder, just add `bindAsContract(JustOne.class).in(Singleton.class);` before your bind and then switch your Application to add the Binder class instead of the feature. – Brian Schrameck Feb 10 '16 at 20:26
  • Hello ricb, i am trying to inject JustOne object in non resource class and it is throwing NullPointerException, here is my code with exception.... import javax.inject.Inject; public class JustOneService { @Inject private JustOne justOne; public int getInteger(){ justOne.bumpSecretNumber(); return justOne.getSecretNumber(); } } my question is how to inject singleton in non resource class – Hassan Feb 25 '16 at 13:33
  • 2
    Well, for DI to work, the classes that have Inject declarations must themselves be created by the DI framework. If you create a class instance (non-resource, or resource for that matter) by using "new MyClass()," the DI framework won't know anything about it and the Inject won't work. HK2 is built into Jersey, so it is involved in creating Jersey class instances, such as resource classes. That's why the Inject works for those. If you want to harness HK2 DI for non-Jersey-created classes, you will have to figure out how to have HK2 instantiate the classes that contain your Inject declarations. – ricb Feb 25 '16 at 18:25