środa, 25 maja 2011

Spring singleton vs prototype scope.

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:
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@1a9d1b
Jak 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:
  1. metoda getStateBean jest chroniona i abstrakcyjna 
  2. klasa oczywiście musi zostać zadeklarowana jako abstrakcyjna.
Zmieniam też konfigurację w xml-u:
<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$$24de2c13
Widać że spring rzeczywiście skorzystał z CGLIB-a ($$EnhancerByCGLIB). Projekt na którym testowałem opisaną funkcjonalność można pobrać stąd.

3 komentarze:

  1. 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ń
  2. 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ń
  3. 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ń