1. Введение
У бинов в Spring бывают разные области действия. Стандартной областью является Singleton.
Singleton — это область действия, при котором в контейнере Spring создает единственный экземпляр нашего бина. Все последующие взаимодействия происходят именно с этим экземпляром.
В этой статье разберем бины со скоупом prototype. Рассмотрим пример использования аннотации @Lookup. Статья поможет новичкам увидеть наглядный пример создания прототайп бина при помощи использования аннотации @Lookup.
Создадим Spring Boot приложение. Система сборки Maven, версия Spring Boot 2.7.18, Java 11.

Добавим следующие зависимости:
Spring Web — для написания REST контроллера
Lombok— для избавления от шаблонного кода.

2. Вызов прототипа в синглтоне
Открываем приложение в среде разработки.
Наш главный класс выглядит следующим образом:
package ru.programstore.prostolookup; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ProstoLookupApplication { public static void main(String[] args) { SpringApplication.run(ProstoLookupApplication.class, args); } }
Для примера напишем сервис погоды. Указав скоуп prototype, мы хотим чтобы этот сервис при каждом запросе возвращал текущее время и новое значение температуры воздуха. Значение температуры генерируется объектом класса Random.
package ru.programstore.prostolookup.service; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import java.time.LocalTime; import java.util.Random; @Service @Scope("prototype") public class WeatherService { final private LocalTime time = LocalTime.now(); final private int temperature = new Random().nextInt(60); public String getCurrentTemperature() { return time + " -> " + temperature; } }
Предположим, потребителем сервиса погоды является туристический сервис. Напишем сервис и для него:
package ru.programstore.prostolookup.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class TouristService { private final WeatherService weatherService; public String getWeather() { return weatherService.getCurrentTemperature(); } }
Напишем RestController, в котором будем вызывать наш сервис:
package ru.programstore.prostolookup.controller; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.programstore.prostolookup.service.TouristService; import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/weather") @RequiredArgsConstructor public class WeatherController { private final TouristService touristService; @GetMapping public List<String> getWeather() throws InterruptedException { List<String> result = new ArrayList<>(); result.add(touristService.getWeather()); Thread.sleep(1000); result.add(touristService.getWeather()); Thread.sleep(1000); result.add(touristService.getWeather()); System.out.println(result); return result; } }
Здесь endpoint "/weather", в методе делаются три запроса к нашему weather API с интервалом в одну секунду. Такой короткий интервал выбран для наглядности примера. Обычно данные о погоде мы запрашиваем с бОльшими интервалами. Однако вместо сервиса погоды у нас может быть, например, сервис фондового рынка, из которого мы получаем котировки ценных бумаг.
Запускаем приложение и делаем запрос http://localhost:8080/weather

Несмотря на то, что WeatherService имеет скоуп prototype, когда мы инжектим его в singleton бин TouristService, он работает как singleton. Погода штука изменчивая, и мы ожидаем в разные интервалы времени получать разные данные о погоде. Здесь же получаем на разные запросы один и тот же объект с одними и теми же значениями времени и температуры.
3. Использование ApplicationContext
Можно занжектить вместо сервиса погоды ApplicationContext, вызвав getBean получить этот самый сервис погоды и вызвать метод getCurrentTemperature():
package ru.programstore.prostolookup.service; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class TouristService { private final ApplicationContext context; public String getWeather() { return context.getBean(WeatherService.class).getCurrentTemperature(); } }
Тогда, перезапустив приложение и сделав запрос к нашему сервису, получаем ожидаемый результат. В разные промежутки времени мы получаем разные данные:

Такой подход не рекомендуется, так как он нарушает ключевой принцип фреймворка Spring — Inversion of Control.
4. Использование ObjectFactory
Можно использовать ObjectFactory:
package ru.programstore.prostolookup.service; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectFactory; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class TouristService { private final ObjectFactory<WeatherService> objectFactory; public String getWeather() { return objectFactory.getObject().getCurrentTemperature(); } }
Перезапускаем приложение, делаем запрос:

Получаем корректный результат, но при таком подходе объект создается сразу при запуске приложения и занимает память.
5. Использование @Lookup
Решение будет следующее. Вместо того, чтобы инжектить сервис погоды, создаем метод с возвращаемым типом WeatherService с заглушкой в виде null (спринг за нас переопределяет этот метод), вешаем на него аннотацию @Lookup. Далее вызываем этот метод и через него так нужный нам getCurrentTemperature():
package ru.programstore.prostolookup.service; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Lookup; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class TouristService { @Lookup public WeatherService getWeatherServiceBean() { return null; } public String getWeather() { return getWeatherServiceBean().getCurrentTemperature(); } }
Таким образом мы получаем наш prototype бин в виде нового объекта при каждом запросе, а не один и тот же singleton. Следует отметить, что WeatherService бин должен быть public и не final. Метод getWeatherServiceBean() не должен быть private, static или final.
Под капотом Spring сделает так:
@Lookup public WeatherService getWeatherServiceBean(){ return applicationbContext.getBean(WeatherService.class); }
6. Вывод
В этой небольшой статье мы рассмотрели что такое прототайп в рамках скоупа спринга и как его создавать с использование аннотации @Lookup.
