Java: цепочка методов и внедрение зависимостей

Допустимо ли использовать цепочку методов при работе со службой, которая управляется инфраструктурой внедрения зависимостей (скажем, HK2)?

Я не уверен, разрешено ли «кешировать» экземпляр, даже если это только в рамках инъекции.

Пример сервиса, создающего пиццу:

@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
    }
}

Здесь служба внедряется в ресурс JAX-RS:

@Path('pizza')
public class PizzaResource {

    @Inject
    PizzaService pizzaService;

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

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

person Hank    schedule 09.05.2016    source источник


Ответы (3)


То, что вы делаете, имеет побочный эффект для всех остальных пользователей сервиса. Все они используют один и тот же экземпляр службы, поэтому, если вы вызовете withPeperoni, это изменит значение этого логического значения для всех тех, у кого есть ссылка на службу.

Кажется, вы хотите использовать Builder. Возможно, ваш сервис может создать новый конструктор, который будет нести ответственность за создание идеальной пиццы для вас. Так вы избежите всех возможных побочных эффектов:

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

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

И 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
    }
}

И PizzaService:

@Service
public class PizzaService {

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

Это решение не идеально, потому что сервис, который только создает экземпляр Builder, не очень полезен, однако он, по крайней мере, предотвращает побочные эффекты, с которыми вы столкнетесь при использовании вашего решения.

person zlandorf    schedule 09.05.2016
comment
В Builder экземпляр PizzaService не управляется HK2 и не имеет доступа к другим службам или локатору служб, верно? - person Hank; 09.05.2016
comment
Я не уверен, что понимаю ваш вопрос. Экземпляр PizzaService управляется любой используемой вами структурой внедрения зависимостей. Например, в случае Spring этот экземпляр уникален и может также ссылаться на другие службы. - person zlandorf; 09.05.2016
comment
Если вы говорите об экземпляре самого Builder, вы не можете внедрить в него сервисы с помощью @Inject. Однако вы можете передать ссылку на другую службу при вызове ее конструктора. PizzaService будет содержать ссылку на необходимые вам услуги, например: return new PizzaBuilder(otherService) - person zlandorf; 09.05.2016
comment
Да, извините, мой плохой - я имел в виду экземпляр PizzaBuilder, а не PizzaService, конечно... - person Hank; 09.05.2016

Это зависит от области ресурса JAX-RS и отсутствия состояния службы.

Обычно каждый экземпляр ресурса JAX-RS создается каждый раз, когда он запрашивается.

JSR339 3.1.1 Жизненный цикл и среда

По умолчанию новый экземпляр класса ресурсов создается для каждого запроса к этому ресурсу. Сначала вызывается конструктор (см. Раздел 3.1.2), затем внедряются любые запрошенные зависимости (см. Раздел 3.2), затем вызывается соответствующий метод (см. Раздел 3.3) и, наконец, объект становится доступным для сборки мусора.

Для следующего HTTP-запроса

GET /pizza HTTP/1.1

создается новый экземпляр PizzaResource, и в него вводится доступный экземпляр PizzaService.

Теперь ответ, который вы ищете, зависит от отсутствия состояния и жизненного цикла PizzaService, который может поддерживаться контейнером.

Надеюсь, я не могу найти спецификацию сейчас, но даже если PizzaService является @Stateless, контейнеры не будут совместно использовать экземпляр одновременно для разных сеансов.

Я бы поставил метод прослушивания жизненного цикла для сброса службы.

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

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

    @Inject
    private PizzaService pizzaService;
}

Где reset() будет делать

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

Обновлять

Я только что нашел хороший поток для @Service, который, кажется, является частью среды Spring. Как singleton Bean обслуживает параллельный запрос?

person Jin Kwon    schedule 09.05.2016
comment
Теперь, что произойдет, если придет 10 запросов... Есть еще один PizzaService. - person M. Deinum; 09.05.2016
comment
@ M.Deinum Вы имеете в виду, что PizzaService - это синглтон? - person Jin Kwon; 09.05.2016
comment
@M.Deinum M.Deinum Независимо от того, сколько запросов отправлено, я считаю, что стратегия внедрения и поддержка экземпляров сервисов полностью зависят от контейнера. - person Jin Kwon; 09.05.2016
comment
Да, так как это область по умолчанию. - person M. Deinum; 09.05.2016
comment
@M.Deinum M.Deinum Я думаю, что мне нужно сообщить, что я не ответил, основываясь на Spring. Я нашел очень хорошую тему. stackoverflow.com/q/25617962/330457 Спасибо. - person Jin Kwon; 09.05.2016
comment
Итак, если я объявлю PizzaService с помощью @PerLookup -scope, он должен работать с областью запроса ресурса JAX-RS, верно? Я посмотрю, смогу ли я создать тестовый пример, чтобы подтвердить это. - person Hank; 09.05.2016
comment
@Hank Если ваш @PerLookup это, он будет работать на вас, я надеюсь. - person Jin Kwon; 11.05.2016

Основываясь на ответе @JinKwon и его комментариях, это мое решение:

  • Служба помечена как @PerLookup, поскольку @Singleton – это По умолчанию. (Спасибо @M. Deinum)
  • В зависимости от жизненного цикла исходного класса я внедряю службу через поставщика . Это не такая большая проблема в ресурсах JAX-RS, так как они уже @RequestScoped по умолчанию. Но другой код (фоновые процессы, модульные тесты и т. д.) может иметь другую область действия, и здесь Provider имеет значение, каждый раз создавая отдельные новые экземпляры.

Следуя этому подходу, я могу использовать цепочку методов, возвращая this. Кроме того, и это важно для меня, экземпляр управляется DI-ядром и имеет доступ к самой инъекции зависимостей.

Услуга:

@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);
    }
}

Ресурс:

@Path('pizza')
public class PizzaResource {

    @Inject
    PizzaService pizzaService;

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

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

Unittest (пример внедрения через 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
    }

}
person Hank    schedule 11.05.2016
comment
На самом деле я считаю, что это не сработает, как вы ожидали. Если для одного и того же экземпляра PizzaResource вызывается одновременно (т. е. 2 потока вызывают getPizza()), они все равно испортят один и тот же экземпляр PizzaService. - person Adrian Shum; 12.05.2016
comment
@AdrianShum правда, отсюда и пример с провайдером. Однако ресурсы JAX-RS (root-) @RequestScoped. - person Hank; 12.05.2016