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.

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


  1. Rockway
    25.12.2023 05:43

    Добрый день. К сожалению после фразы «Под капотом Spring сделает так:» какого либо примера я не увидел. Хотелось бы увидеть какой-то пример как делает под капотом Spring.


    1. bae_prosto Автор
      25.12.2023 05:43

      Спасибо за внимательность! Вернул потерявшийся пример.


  1. ultrinfaern
    25.12.2023 05:43

    Еще самый красивый вариант:

    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)


  1. DmitryKozlov1
    25.12.2023 05:43

    А ещё можно поставить скоуп request и вообще не париться. Из коробки будет новый Бин на каждый реквест.


    1. vic_1
      25.12.2023 05:43

      Именно так, скоуп реквест был специально сделан для веб приложений