Довольно популярный вопрос на собеседовании - Можно ли внедрить Prototyte в Singleton и как это сделать?

Если просто добавить к определению бина аннотацию @Scope(SCOPE_PROTOTYPE), и использовать этот бин в синглтоне через аннотацию @Autowired – будет создан только один объект. Потому что синглтон создается только однажды, и обращение к прототипу случится тоже однажды при его создании (при внедрении зависимости).

На самом деле вариантов довольно много:

  • с помощью @Lookup

  • context.getBean(Prototype.class)

  • API -интерфейс javax.inject

  • Интерфейс ObjectFactory 

Но эти варианты мягко скажем "не очень".

В некоторых статьях пишут что можно сделать это с помощью параметра ProxyMode в аннотации @Scope, но я не смог найти реальных примеров внедрения, so напишу свой.

Для начала создадим наш будущий Prototype.

  • Каждый раз когда создается новый экземпляр будем выводить в консоль "new bank created".

  • Добавим @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,proxyMode = TARGET_CLASS)

    Выражение proxyMode=TARGET_CLASS oзначает, что класс создается с помощью наследования.

    Можно сделать proxyMode = INTERFACES, тогда proxy создастся c использованием интерфейсов, наш класс не реализует никаких интерфейсов(интерфейсы - маркеры не подойдут), поэтому используем TARGET_CLASS.

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;

import static org.springframework.context.annotation.ScopedProxyMode.TARGET_CLASS;

/**
 * Created in SoftMediaLab
 * by Mark Ponomarev
 * Date : 15.09.2023
 **/

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode=TARGET_CLASS)
public class Bank {

    private final String bankName;

    public Bank() {
        this.bankName = UUID.randomUUID().toString();
        System.out.println("new bank created");
    }

    public String getInput() {
        return bankName;
    }
}

Сейчас создадим Singleton.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Created in SoftMediaLab
 * by Mark Ponomarev
 * Date : 15.09.2023
 **/

@Service()
public class BankService {

    @Autowired
    Bank bank;

    public Bank getNewBank(){
        return bank;
    }
  
}

Тут все довольно просто, Мы инжектим наш Prototype c помощью aннотации @Autowired.

Для проверки напишем дополнительный метод.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;



@SpringBootTest
class PrimaryExampleApplicationTests {
    @Autowired
    BankService messageService;

    @Test
    public void testing(){
        Bank newBank1 = messageService.getNewBank();
        Bank newBank = messageService.getNewBank();
        System.out.println(newBank.getInput());
        System.out.println(newBank1.getInput());
    }
}

Можете самостоятельно проверить, что будет создано 2 экземпляра класса Bank.

Для полноты картины вы можете самостоятельно внедрить Prototype, используя динамический Proxy(proxyMode = INTERFACES). Не забудьте про то, что нужно реализовать интерфейс, иначе будет Exeption при поднятии контекста.

Комментарии (6)


  1. Krusho
    15.09.2023 15:20
    +1

    В Вашем реализации, в синглтон внедряется не реальный объект класса, а прокси. И при каждом вызове его методов, прокси решает, создавать новый экземпляр класса для вызова или нет. То есть, в Вашем случае, при каждом вызове метода getInput(), прокси решил создавать новый объект. Вызовите метод getInput() на одном и том же прокси 100 раз, думаю, там будет создано гораздо больше экземпляров, чем 1.

    Простой пример, получен один бин из Вашей реализации, и на одном и том же бине вызван 500 раз один и тот же метод, вывод по спойлером под кодом.

            final Bank bank = ctx.getBean(Bank.class);
            IntStream.range(0, 500)
                    .forEach(i -> System.out.println(bank.getInput()));
    Вывод кода выше

    new bank created
    356f5044-9117-4d2a-b67a-e7d1690cc84b
    new bank created
    53950529-5dfb-4c85-b95f-0e84f25b2108
    new bank created
    bcd131c0-09d8-49e7-ad2b-4331aac0cabf
    new bank created
    f8fa5bbe-ee93-42cb-ab16-e153a0a013ed
    new bank created
    5707d254-78e9-4bf5-85d6-4b842da36403
    new bank created
    648d3719-fb8b-4c39-b3af-ca1e9736a05d
    new bank created
    102b8341-5309-4dfc-9cb3-2d3e3717971d
    new bank created
    9e9e1189-7eb5-43cd-b9c8-b2438c0db6c9
    new bank created
    3b762b69-2746-4ef4-9a8e-36238129617b
    new bank created
    704b9894-47fa-420b-9c7f-b86e019bcebe
    new bank created
    9d1a519f-d841-4cb4-ace2-2fbcd0cb983d
    new bank created
    0e55e9a6-662e-4fed-b568-dd44abe5829d
    new bank created
    43162b6d-ed89-451a-9fce-cec798862d96
    new bank created
    427bcda1-6d2d-4cf0-8b1e-17aaaec1ebee

    Всё же, внедрять Prototype лучше так, как описано в документации, например, имплементировать интерфейс ApplicationContextAware и вызывать context.getBean(Prototype.class).https://docs.spring.io/spring-framework/docs/3.0.0.M4/reference/html/ch03s04.html#beans-factory-method-injection


    1. m1llark Автор
      15.09.2023 15:20

      Вызов ApplicationContext.getBean() это не Инверсия Контроля! Несмотря на то, что по-прежнему легко изменить реализацию, настроенную для заданного имени компонента, класс теперь напрямую полагается на Spring для предоставления этой зависимости и не может получить ее каким-либо другим способом. Вы не можете просто сделать свою собственную имитацию реализации в тестовом классе и передать ее ему самостоятельно. Это, по сути, противоречит цели Spring как контейнера для внедрения зависимостей.

      Если кратко то ваш вариант это костыль.


      1. Krusho
        15.09.2023 15:20
        +2

        Видимо, я не смог донести до Вас свою мысль.

        В Ваш синглтон всегда инжектится один, и только один экземпляр Bank. Только в случае использования proxyMode=TARGET_CLASS, инжектится не объект класса, а прокси. Вы всегда получаете один и тот же объект, когда вызываете метод синглтона getNewBank(), можете это проверить.

        Пример ниже, в котором получены два экземпляра Bank через BankService, убеждаемся, что получили один и тот же экземпляр и вызываем дважды метод getInput() для обоих Bank. Было создано 4 реальных экземпляра Bank, по одному на каждый вызов метод. Остаётся вопрос, действительно ли разработчик хотел бы вызвать метод getInput() на 4 разных объектах, или же только на 2, как это видно в коде.

        final BankService bankService = ctx.getBean(BankService.class);
        final Bank bank1 = bankService.getBank();
        final Bank bank2 = bankService.getBank();
        if(Objects.equals(bank1, bank2)) {
            System.out.println("There is the same bank");
            IntStream.range(0, 2)
                    .forEach(i -> {
                        System.out.println(bank1.getInput());
                        System.out.println(bank2.getInput());
                    });
        }
        Результат выполнения

        There is the same bank
        Created new bank.
        3dafc23c-ad22-4a05-96af-ec155cc9cd46
        Created new bank.
        e328e94f-3a62-44df-837f-df3c8f6fd440
        Created new bank.
        39381838-c273-4131-9a54-0e4736cf3155
        Created new bank.
        3f7dca89-15ef-49e8-b71f-4289790d51e3


      1. Bonce
        15.09.2023 15:20
        -1

        Я бы не утверждал, что это не инверсия контроля. Это скорее dependency lookup


  1. coregabe
    15.09.2023 15:20

    на java как долго import org.springframework.beans.factory.annotation.Autowired; \


  1. coregabe
    15.09.2023 15:20

    на go две строчки

    Да?