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.