Периодически приходится отвечать на вопросы про область видимости bean в Spring Framework. В данном примере разбирается область видимости(scope) prototype, который не часто встречается в коде. Статья содержит код, который можно найти на GitHub project.
Создадим два бина с разной областью видимости.
@Component
public class SingletonCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
Данный бин имеет область видимости singleton, так как по умолчанию, все бины создаются с данной областью видимости. Данный тип видимости можно задать явно, используя аннотацию ConfigurableBeanFactory.SCOPE_SINGLETON.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeCounter {
private final StringProcessor stringProcessor;
private int count = 0;
public PrototypeCounter(StringProcessor stringProcessor) {
this.stringProcessor = stringProcessor;
}
public void increment() {
count++;
}
public int getCount() {
return count;
}
public String process(String str) {
return stringProcessor.process(str);
}
}
Данный бин имеет область видимости prototype, которая задается соответствующей аннотацией ConfigurableBeanFactory.SCOPE_PROTOTYPE.
Теперь используем данные бины в классах сервисах, через внедрение с помощью конструктора:
@Service
public class PrototypeIncrementService {
private final PrototypeCounter prototypeCounter;
private final SingletonCounter singletonCounter;
public PrototypeIncrementService(PrototypeCounter prototypeCounter, SingletonCounter singletonCounter) {
this.prototypeCounter = prototypeCounter;
this.singletonCounter = singletonCounter;
}
public PrototypeCounter getPrototypeCounter() {
return prototypeCounter;
}
public SingletonCounter getSingletonCounter() {
return singletonCounter;
}
}
@Component
public class SingletonIncrementService {
private final PrototypeCounter prototypeCounter;
private final SingletonCounter singletonCounter;
public SingletonIncrementService(PrototypeCounter prototypeCounter, SingletonCounter singletonCounter) {
this.prototypeCounter = prototypeCounter;
this.singletonCounter = singletonCounter;
}
public PrototypeCounter getPrototypeCounter() {
return prototypeCounter;
}
public SingletonCounter getSingletonCounter() {
return singletonCounter;
}
}
Используем классы сервисы в основном приложении:
@SpringBootApplication
public class ScopeApplication implements ApplicationRunner {
private final PrototypeIncrementService prototypeIncrementService;
private final SingletonIncrementService singletonIncrementService;
public ScopeApplication(PrototypeIncrementService prototypeIncrementService, SingletonIncrementService singletonIncrementService) {
this.prototypeIncrementService = prototypeIncrementService;
this.singletonIncrementService = singletonIncrementService;
}
public static void main(String[] args) {
SpringApplication.run(ScopeApplication.class, args);
}
@Override
public void run(ApplicationArguments args) {
prototypeIncrementService.getPrototypeCounter().increment();
prototypeIncrementService.getSingletonCounter().increment();
singletonIncrementService.getPrototypeCounter().increment();
singletonIncrementService.getSingletonCounter().increment();
}
public PrototypeIncrementService getPrototypeIncrementService() {
return prototypeIncrementService;
}
public SingletonIncrementService getSingletonIncrementService() {
return singletonIncrementService;
}
}
В данным классе мы при запуске приложения инкрементируем наши счетчики.
Соответствующий класс с тестами:
@SpringBootTest
class ScopeApplicationTests {
@Autowired
private ScopeApplication scopeApplication;
@Test
void shouldCheckPrototypeAndSingletonScopeValues() {
assertThat(scopeApplication.getPrototypeIncrementService().getPrototypeCounter().getCount()).isEqualTo(1);
assertThat(scopeApplication.getSingletonIncrementService().getPrototypeCounter().getCount()).isEqualTo(1);
assertThat(scopeApplication.getPrototypeIncrementService().getSingletonCounter().getCount()).isEqualTo(2);
assertThat(scopeApplication.getSingletonIncrementService().getSingletonCounter().getCount()).isEqualTo(2);
assertThat(scopeApplication.getPrototypeIncrementService().getPrototypeCounter().process("String1")).isEqualTo("STRING1");
assertThat(scopeApplication.getSingletonIncrementService().getPrototypeCounter().process("String2")).isEqualTo("STRING2");
}
}
Из теста видно, что бины с областью видимости singleton создаются в единственном экземпляре, и если у них есть состояние, в данном случае поле private int count, при вызове метода для инкремента, будет инкрементироваться именно оно, где бы мы не использовали сервис с внедренным счетчиком.
Однако, если мы рассмотрим бины с областью видимости prototype, можно увидеть, что каждый раз, при внедрении данных бинов, создается новый экземпляр со своим состоянием. Похожего поведения, можно добиться если написать в программе явный вызов создания объекта через оператор new. Однако, в таком случае, мы потеряем возможность внедрять зависимости и друге бины в наши экземпляры, поскольку контейнер не управляет ими.
Выводы:
| Singleton bean | Prototype bean |
| Создается только один экземпляр. Он используется везде. | Каждый раз при обращению к бину, создается новый экземпляр. |
| Уничтожается spring при закрытии контекста. Вызывается метод destroy | Уничтожается сборщиком мусора. Spring не управляет удалением экземпляра и обработкой закрытия его ресурсов. |
| Используется, когда не нужно хранить состояние. | Используется, когда нужно хранить состояние. |