Spring prototype при помощи Lookup

1. Введение

У бинов в Spring бывают разные области действия. Стандартной областью является Singleton.

Singleton — это область действия, при котором в контейнере Spring создает единственный экземпляр нашего бина. Все последующие взаимодействия происходят именно с этим экземпляром.

В этой статье разберем бины со скоупом prototype. Рассмотрим пример использования аннотации @Lookup. Статья поможет новичкам увидеть наглядный пример создания прототайп бина при помощи использования аннотации @Lookup.

Создадим Spring Boot приложение. Система сборки Maven, версия Spring Boot 2.7.18, Java 11.

8749eb9bf1d664e59fb1a7515eb351ff.png

Добавим следующие зависимости:

Spring Web — для написания REST контроллера

Lombok— для избавления от шаблонного кода.

849a7e2fc22b2330d2e6e971731e0ac4.png

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 getWeather() throws InterruptedException {
       List 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

9a4abb89440f2f39c7a5a51da0a60d9d.png

Несмотря на то, что 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();
   }
}

Тогда, перезапустив приложение и сделав запрос к нашему сервису, получаем ожидаемый результат. В разные промежутки времени мы получаем разные данные:

4b458e949091902d1336c8b690b7c5d2.png

Такой подход не рекомендуется, так как он нарушает ключевой принцип фреймворка 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 objectFactory;

   public String getWeather() {
       return objectFactory.getObject().getCurrentTemperature();
   }
}

Перезапускаем приложение, делаем запрос:

1405a0a5aa03e5eae46df38841c31053.png

Получаем корректный результат, но при таком подходе объект создается сразу при запуске приложения и занимает память.

5. Использование

Решение будет следующее. Вместо того, чтобы инжектить сервис погоды, создаем метод с возвращаемым типом 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 сделает так:

6. Вывод

В этой небольшой статье мы рассмотрели что такое прототайп в рамках скоупа спринга и как его создавать с использование аннотации @Lookup.

© Habrahabr.ru