Witam.
Dziś zaprezentuję jak pracować z różnymi (singleton, prototype) zasięgami beanów w springu. Jak wiemy domyślnie wszystko w springu jest singletonem. Wynika z tego rzecz oczywista - beany nie mogą być stanowe. A co jeśli potrzebujemy stanowego beana ? Mamy do dyspozycji scope=prototype.
Rozważmy przykład z jednym beanem stanowym, drugim singletonowym:
Rozważmy bardziej wyrafinowany przypadek. Co jeśli chcemy w singletonowym beanie w jakiejś metodzie (u nas SingletonBean i metoda process) mieć za każdym jej wywołaniem świerzy obiekt StateBean? Powstaje też pytanie dlaczego sami nie utworzymy instancji StateBean? Załóżmy, że nie chcemy tego z jakiś powodów (np. StateBean otrzymuje referencje do innych benów i chcemy móc na nim odpalić metody cyklu życia beana springa, albo cokolwiek innego).
Pokażę dwa rozwiązania. Pierwsze :
Jest to jeden ze sposobów otrzymania referencji do fabryki springa wewnątrz beana (moża też zaimplementować BeanFactoryAware. Jest różnica między nimi, ale w naszym przykładzie jest nieistotna; można również zwyczajnie wstrzyknąć sobie ApplicationContext przez @Autowired). Jeśli mamy w ręku fabrykę springa to możemy z niej pobrać beana StateBean (// 1).
Do tego potrzeba jeszcze kawałek xml-a:
W metodzie testowej uruchamiam dwa razy metodę process i oczekuję że otrzymam za każdym razem inny obiekt. Test przechodzi i dodatkowo loger (z wnętrza mojej metody process) pokazuje na konsoli:
A teraz nieco zaczaruję i pokażę inny, mniej inwazyjny sposób:
Innymi słowy: spring oczywiście nie utworzy nam beana typu SingletonBeanImpl - gdyż nie da się utworzyć jego instancji (klasa jest abstrakcyjna), ale można w czasie wykonania po niej podziedziczyć i nadpisać metodę getStateBean.
W konfiguracji podaliśmy że chcemy aby ta metoda zwracała beana stateBean. Czyli spring robi sam za nas to co pokazałem w pierwszym podejściu.
Zmodyfikowałem test aby przekonać się jaki jest typ beana SingletonBeanImpl:
Dziś zaprezentuję jak pracować z różnymi (singleton, prototype) zasięgami beanów w springu. Jak wiemy domyślnie wszystko w springu jest singletonem. Wynika z tego rzecz oczywista - beany nie mogą być stanowe. A co jeśli potrzebujemy stanowego beana ? Mamy do dyspozycji scope=prototype.
Rozważmy przykład z jednym beanem stanowym, drugim singletonowym:
public interface StateBean { /** * jest to bean stanowy więc można mu coś ustawić * @param number jakiś tam parametr modyfikujący stan beana */ void setNumber(Integer number); } public interface SingletonBean { /** * Metoda korzysta w środku ze stanowego beana i zwraca do niego referencję. */ StateBean process(); }Do tego prosta implementacja:
public class StateBeanImpl implements StateBean { private Integer number; public void setNumber(Integer number) { this.number = number; } }Scope prototype działa tak, że za każdym razem jak zostanie odpytana fabryka springa o beana typu StateBean to zwróci ona nowy obiekt. Zatem jeśli będziemy mieli kilka singletonowych beanów korzystających z naszego stanowego beana - każdy otrzyma inny obiekt stanowy.
Rozważmy bardziej wyrafinowany przypadek. Co jeśli chcemy w singletonowym beanie w jakiejś metodzie (u nas SingletonBean i metoda process) mieć za każdym jej wywołaniem świerzy obiekt StateBean? Powstaje też pytanie dlaczego sami nie utworzymy instancji StateBean? Załóżmy, że nie chcemy tego z jakiś powodów (np. StateBean otrzymuje referencje do innych benów i chcemy móc na nim odpalić metody cyklu życia beana springa, albo cokolwiek innego).
Pokażę dwa rozwiązania. Pierwsze :
public class SingletonBeanImpl implements SingletonBean, ApplicationContextAware { private static final Logger LOGGER = Logger.getLogger(SingletonBeanImpl.class); private ApplicationContext context; public StateBean process() { StateBean stateBean = getStateBean(); LOGGER.info(format("stateBean = {0}", stateBean)); return stateBean; } private StateBean getStateBean () { return context.getBean(StateBean.class); // 1 } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } }Nasz singletonowy bean implementuje interfejs ApplicationContextAware i implementuje metodę z tego interfejsu - setApplicationContext.
Jest to jeden ze sposobów otrzymania referencji do fabryki springa wewnątrz beana (moża też zaimplementować BeanFactoryAware. Jest różnica między nimi, ale w naszym przykładzie jest nieistotna; można również zwyczajnie wstrzyknąć sobie ApplicationContext przez @Autowired). Jeśli mamy w ręku fabrykę springa to możemy z niej pobrać beana StateBean (// 1).
Do tego potrzeba jeszcze kawałek xml-a:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="singletonBean" class="pl.turo.spring.impl.SingletonBeanImpl" /> <bean id="stateBean" class="pl.turo.spring.impl.StateBeanImpl" scope="prototype" /> </beans>Jak wiemy za każdym razem powinniśmy otrzymać nowy obiekt. Sprawdzę to pisząc test:
@ContextConfiguration(locations = {"classpath:META-INF/appConfig.xml"}) @RunWith(SpringJUnit4ClassRunner.class) public class AppContextTest { @Autowired private SingletonBean singletonBean; @Test public void testConfig() { // za każdym razem oczekuję nowego obiektu StateBean firstStateBean = singletonBean.process(); StateBean secondStateBean = singletonBean.process(); assertNotSame(firstStateBean, secondStateBean); } }Jak widać uruchamiam test podając ścieżkę do mojej konfiguracji springa i wstrzykuję sobie beana SingletonBean.
W metodzie testowej uruchamiam dwa razy metodę process i oczekuję że otrzymam za każdym razem inny obiekt. Test przechodzi i dodatkowo loger (z wnętrza mojej metody process) pokazuje na konsoli:
INFO (SingletonBeanImpl.java:18) - stateBean = pl.turo.spring.impl.StateBeanImpl@15e2075 INFO (SingletonBeanImpl.java:18) - stateBean = pl.turo.spring.impl.StateBeanImpl@1a9d1bJak widać są to dwa różne obiekty w jvm.
A teraz nieco zaczaruję i pokażę inny, mniej inwazyjny sposób:
public abstract class SingletonBeanImpl implements SingletonBean { private static final Logger LOGGER = Logger.getLogger(SingletonBeanImpl.class); public StateBean process() { StateBean stateBean = getStateBean(); LOGGER.info(format("stateBean = {0}", stateBean)); return stateBean; } protected abstract StateBean getStateBean(); }Zauważmy dwie rzeczy:
- metoda getStateBean jest chroniona i abstrakcyjna
- klasa oczywiście musi zostać zadeklarowana jako abstrakcyjna.
<bean id="singletonBean" class="pl.turo.spring.impl.SingletonBeanImpl" > <lookup-method bean="stateBean" name="getStateBean" /> </bean>Cóż tutaj się dzieje ? Jest to przykład czegoś co spring nazywa 'Method Injection'. Widzimy beana SingletonBeanImpl z metodą abstrakcyjną. Spring dostarczy nam jej implementacji dzięki potędze CGLIB-a.
Innymi słowy: spring oczywiście nie utworzy nam beana typu SingletonBeanImpl - gdyż nie da się utworzyć jego instancji (klasa jest abstrakcyjna), ale można w czasie wykonania po niej podziedziczyć i nadpisać metodę getStateBean.
W konfiguracji podaliśmy że chcemy aby ta metoda zwracała beana stateBean. Czyli spring robi sam za nas to co pokazałem w pierwszym podejściu.
Zmodyfikowałem test aby przekonać się jaki jest typ beana SingletonBeanImpl:
@Test public void testConfig() { // za każdym razem oczekuję nowego obiektu StateBean firstStateBean = singletonBean.process(); StateBean secondStateBean = singletonBean.process(); assertNotSame(firstStateBean, secondStateBean); LOGGER.info("singletonBean.getClass() = " + singletonBean.getClass()); }Po uruchomieniu widzę :
INFO (AppContextTest.java:26) - singletonBean.getClass() = class pl.turo.spring.impl.SingletonBeanImpl$$EnhancerByCGLIB$$24de2c13Widać że spring rzeczywiście skorzystał z CGLIB-a ($$EnhancerByCGLIB). Projekt na którym testowałem opisaną funkcjonalność można pobrać stąd.
Wpis ciekawy, ale nie wyjaśnia jednego scenariusza. Mianowicie, co jeżeli konstruktor StateBean'a, wymaga jakichś danych, które wyliczane są w metodzie SingletonBean.process()? Dla ułatwienia konstruktora StateBean'a nie można zmienić. Spring udostępnia jakieś mechanizmy ułatwiające radzenie sobie w takich sytuacjach?
OdpowiedzUsuńObiekt jest tworzony przez springa zatem nie widzę takiej możliwości (żeby być uczciwym wydaje mi się, że jest ale jeszcze przeze mnie nierozpoznana - kiedyś na blogu się pojawi). Proponuję pobranie sobie referencji do stanowego beana i wykorzystanie seterów . Jest to przecież stanowy obiekt.
OdpowiedzUsuńBardzo ciekawy artykuł, żałuje, że znajomość tego mechanizmu jest tak nikła wśród programistów. Jeśli chodzi o pytanie mojego imiennika, niemal równo rok temu napisałem artykuł opisujący ten problem. Niestety jego rozwiązanie wymaga drobnej modyfikacji w samym Springu. Mój patch czeka: SPR-7431.
OdpowiedzUsuń