5

Is it acceptable to use method-chaining, when working with a service that is managed by a dependency injection framework (say HK2)?

I'm unsure if it is allowed to "cache" the instance, even if its only within the scope of the injection.

Example Service that creates a pizza:

@Service
public class PizzaService {

    private boolean peperoni = false;
    private boolean cheese = false;
    private boolean bacon = false;

    public PizzaService withPeperoni() {
        peperoni = true;
        return this;
    }

    public PizzaService withCheese() {
        cheese = true;
        return this;
    }

    public PizzaService withBacon() {
        bacon = true;
        return this;
    }

    public Pizza bake() {
        // create the instance and return it
    }
}

Here the service is injected into a JAX-RS resource:

@Path('pizza')
public class PizzaResource {

    @Inject
    PizzaService pizzaService;

    @GET
    public Response getPizza() {
        Pizza pizza = pizzaService
            .withPeperoni()
            .withCheese()
            .bake();

        return Response.ok(pizza).build();
    }
}
Hank
  • 4,597
  • 5
  • 42
  • 84

3 Answers3

1

What you are doing has a side effect for all other users of the service. They all share the same instance of the service, so if you call withPeperoni it will change the value of that boolean for all those who have a reference to the service.

What you seem to want is to use a Builder. Perhaps your service can instantiate a new builder that will have the responsibility to build the perfect pizza for you. That way you avoid all possible side effects :

@GET
public Response getPizza() {
    Pizza pizza = pizzaService.newPizzaBuilder()
        .withPeperoni()
        .withCheese()
        .bake();

    return Response.ok(pizza).build();
}

And PizzaBuilder :

public class PizzaBuilder {

    private boolean peperoni = false;
    private boolean cheese = false;
    private boolean bacon = false;

    public PizzaBuilder withPeperoni() {
        peperoni = true;
        return this;
    }

    public PizzaBuilder withCheese() {
        cheese = true;
        return this;
    }

    public PizzaBuilder withBacon() {
        bacon = true;
        return this;
    }

    public Pizza bake() {
        // create the instance and return it
    }
}

And PizzaService :

@Service
public class PizzaService {

    public PizzaBuilder newPizzaBuilder() {
        return new PizzaBuilder();
    }
}

This solution is not perfect, because there is not much use of a service that only instantiates a Builder, however it at least prevents the side effects you'll encounter with your solution.

zlandorf
  • 1,100
  • 11
  • 15
  • With the Builder, the instance of `PizzaService` is not managed by HK2 and has no access to other services or the service locator, right? – Hank May 09 '16 at 11:06
  • 1
    I'm not sure I understand your question. The PizzaService instance is managed by whatever dependency injection framework you're using. In the case of spring, for instance, that instance is unique and can reference other services too. – zlandorf May 09 '16 at 14:39
  • 1
    If you're talking about the instance of the Builder itself, you cannot inject services in it by using @Inject. However, you can pass a reference to another service when calling it's constructor. The PizzaService will hold the reference to the services you need, eg : `return new PizzaBuilder(otherService)` – zlandorf May 09 '16 at 14:43
  • Yes, sorry, my bad - I meant the instance of the `PizzaBuilder`, not the `PizzaService` of course... – Hank May 09 '16 at 17:15
1

It depends on the scope of the JAX-RS resource and the stateless-ness of the service.

Normally, each JAX-RS resource instance is created every time when it's requested.

JSR339 3.1.1 Lifecycle and Environment

By default a new resource class instance is created for each request to that resource. First the constructor (see Section 3.1.2) is called, then any requested dependencies are injected (see Section 3.2), then the appropriate method (see Section 3.3) is invoked and finally the object is made available for garbage collection.

For following HTTP request,

GET /pizza HTTP/1.1

a new instance of PizzaResource created and an available instance of PizzaService is injected in it.

Now the answer you're looking for depends on the statelessness and the lifecycle of the PizzaService which might be maintained by the container.

Hopefully, I can't find the spec now but, even if the PizzaService is a @Stateless, containers won't share the instance at the same time for different session.

I would put a lifecycle listener method for resetting the service.

@Path("/pizza")
public class PizzaResource {

    @PostConstruct
    private void resetPizzaService() { // invoked after the injection
        pizzaService.reset();
    }

    @Inject
    private PizzaService pizzaService;
}

Where the reset() will do

public void reset() {
    peperoni = false;
    cheese = false;
    bacon = false;
}

Update

I just found a good thread for @Service which seems to be part of Spring framework. How does the singleton Bean serve the concurrent request?

Community
  • 1
  • 1
Jin Kwon
  • 20,295
  • 14
  • 115
  • 184
  • Now what would happen if 10 requests come in... There is still a single `PizzaService`. – M. Deinum May 09 '16 at 12:18
  • @M.Deinum You mean the `PizzaService` is a singleton? – Jin Kwon May 09 '16 at 12:20
  • @M.Deinum No matter how many requests push, the injection strategy and the maintaining the instances of the services are totally up to the container, I believe. – Jin Kwon May 09 '16 at 12:22
  • Yes, as that is the default scope. – M. Deinum May 09 '16 at 12:25
  • @M.Deinum I think I have to admin that I didn't answered based on Spring. I found a very good thread. http://stackoverflow.com/q/25617962/330457 Thank you. – Jin Kwon May 09 '16 at 12:34
  • So, if I declare the `PizzaService` with [@PerLookup](https://hk2.java.net/apidocs/org/glassfish/hk2/api/PerLookup.html)-scope, it should work with the per-request-scope of the JAX-RS resource, right? I'll see if I can create a test-case to confirm this. – Hank May 09 '16 at 17:30
  • @Hank If your `@PerLookup` is [this](https://hk2.java.net/apidocs/org/glassfish/hk2/api/PerLookup.html), it will work for you, I hope. – Jin Kwon May 11 '16 at 02:58
0

Building on top of @JinKwon's answer and its comments, this is my solution:

  • The service is marked @PerLookup, since @Singleton is the default. (Thanks @M. Deinum)
  • Depending on the lifecycle of the origin class, I'm injecting the service via a Provider. This is not so much a problem in JAX-RS resources, since they are already @RequestScoped by default. But other code (background processes, unit tests, etc) may have different scope, and there the Provider makes the difference, creating separate fresh instances every time.

Following this approach, I can use method-chaining, returning this. Also, and this is important to me, the instance is managed by the DI-kernel and has access to depenency injection itself.

Service:

@Service
@PerLookup
public class PizzaService {

    Pizza pizza = new Pizza(); // naked pizza by default

    @Inject
    OvenService    oven; // just to show that I can use @Inject here

    public PizzaService withPeperoni() {
        pizza.peperoni = true;
        return this;
    }

    public PizzaService withCheese() {
        pizza.cheese = true;
        return this;
    }

    public PizzaService withBacon() {
        pizza.bacon = true;
        return this;
    }

    public Pizza bake() {
        return oven.bake(pizza);
    }
}

Resource:

@Path('pizza')
public class PizzaResource {

    @Inject
    PizzaService pizzaService;

    @GET
    public Response getPizza() {
        Pizza pizza = pizzaService
            .withPeperoni()
            .withCheese()
            .bake();

        return Response.ok(pizza).build();
    }
}

Unittest (an example of injecting via a javax.inject.Provider):

@HK2
@Test
public class PizzaServiceNGTest {

    @Inject
    PizzaService pizzaService;

    @Inject
    Provider<PizzaService> pizzaServiceProvider;

    public void testProviderInjection() {
        Pizza pizza;

        // pizza with the works
        pizza = pizzaServiceProvider.get()
            .withPeperoni()
            .withBacon()
            .withCheese()
            .bake();

        assertTrue(pizza.peperoni);
        assertTrue(pizza.bacon);
        assertTrue(pizza.cheese);

        // naked pizza
        pizza = pizzaServiceProvider.get()
            .bake();

        assertFalse(pizza.peperoni);
        assertFalse(pizza.bacon);
        assertFalse(pizza.cheese);
    }

    public void testDirectInjection() {
        Pizza pizza;

        // pizza with the works
        pizza = pizzaService
            .withPeperoni()
            .withBacon()
            .withCheese()
            .bake();

        assertTrue(pizza.peperoni);
        assertTrue(pizza.bacon);
        assertTrue(pizza.cheese);

        // naked pizza
        pizza = pizzaService
            .bake();

        // this is where it goes wrong: the pizzaService hasn't been reset and
        // is messing up the order!
        assertFalse(pizza.peperoni);    // will fail
        assertFalse(pizza.bacon);   // will fail
        assertFalse(pizza.cheese);  // will fail
    }

}
Community
  • 1
  • 1
Hank
  • 4,597
  • 5
  • 42
  • 84
  • Actually I believe it wont work as you expected. If, for the *same* instance of `PizzaResource` is called concurrently (i.e. 2 threads are calling `getPizza()`), they will still corrupt the same instance of `PizzaService`. – Adrian Shum May 12 '16 at 01:38
  • @AdrianShum true, hence the example with the Provider. JAX-RS (root-)resources are `@RequestScoped` though. – Hank May 12 '16 at 06:55