wtorek, 21 września 2010

Za kulisami fabryki springa cz.1

Cykl życia springa dzieli się na 3 etapy :

1. initialization
2. use
3. destruction

Ad 1.
W tej fazie spring :

1. parsuje wszystkie xml-e
2. ładuje definicje beanów do fabryki (każdy bean pod unikalnym id)
3. uruchamiane są BeanFactoryPostProcessor-y - mogą one zmieniać definicję beanów.

Przykładem takiego postprocessora jest PropertyPlaceholderConfigurer, którym zastępowane są wyrażenia ${nazwa_wartości} wartościami (np zaczytanymi z plików .properties lub wstrzykniętymi). Uruchamiane są zanim zostaną utworzone obiekty. Można napisać własny BeanPostProcessor.
Przykład użycia PropertyPlaceholderConfigurer-a :
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 <property name="driverClassName" value="${driver}" />
 <property name="url" value="${url}" />
 <property name="username" value="${username}" />
 <property name="password" value="${password}" />
</bean>

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="locations">
  <util:list>
   <value>/datasource.properties</value>
   <value>/user.properties</value>
  </util:list>
 </property>
</bean>
datasource.properties :
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:BAZA
oraz user.properties :
username=slawek
password=tajne

Zdefiniowałem dwa bean-y. Bean-owi 'dataSource' w fazie inizjalicacji zostaną ustawione wartości wczytane z plików .properties. Spring czytając definicje bean-ów automatycznie wykrywa beana implementującego interfejs BeanFactoryPostProcessor i go uruchomi, zatem wystarczy że pojawi się definicja PropertyPlaceholderConfigurer-a. Czy można mieć kilka definicji PropertyPlaceholderConfigurer-a ? Pewnie, że można, tylko będąc świadomym jak BeanFactoryPostProcessor-y działają musimy wiedzieć, że taka konfiguracja poprawna nie będzie :
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 <property name="driverClassName" value="${driver}" />
 <property name="url" value="${url}" />
 <property name="username" value="${username}" />
 <property name="password" value="${password}" />
</bean>

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="location" value="/user.properties" />
</bean>

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="location" value="/datasource.properties" />
</bean>
a dlaczego ? Otóż pierwszy PropertyPlaceholderConfigurer nie ma wczytanych wszystkich wartości (podaliśmy mu tylko to, co jest w pliku /user.properties). Zatem widząc taką wartość "${driver}" w definicji beana "dataSource" rozpoznaje jako ciąg, który należy podmienić i szuka w swojej mapie wartości o takim kluczu. Nie znajduje jej (ponieważ nie ma takiej wartości w /user.properties, jest natomiast w dasasource.properties) i skutkuje to wyjątkiem :
Caused by: org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name 'dataSource' defined in class path resource [META-INF/spring/app-context.xml]: Could not resolve placeholder 'driver'.
Rozwiązaniem problemu jest atrybut ignoreUnresolvablePlaceholders. W kolejnym poście szerzej opisałem działanie PropertyPlaceholderConfigurer'a.

Korzystając ze springa 3 możemy użyć namepsace 'context' i nieco skrócić konfigurację. Zamiast :

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="location" value="/datasource.properties" />
</bean>
możemy mieć :
<context:property-placeholder location="/datasource.properties"/>
te zapisy są sobie równoważne.
Spring 3 daje nam jeszcze inne możliwości. Pokazałem jak można korzystać z BeanFactoryPostProcessor-ów w xml-u, spójrzmy jak to można zrobić adnotacjami :
@Service
public class PropertyBean {
 @Value(value = "#{prop.username}")
 private String user;

 @Value(value = "#{prop.password}")
 private String password;

 public String getUser() {
  return user;
 }

 public String getPassword() {
  return password;
 }
}
zakładając, że w xml - u pojawi się bean o id 'prop' który będzie typu java.util.Properties i będzie posiadał wartość o kluczu 'username' :
<util:properties id="prop" location="/user.properties">
 <prop key="password">nowe_hasło</prop>
</util:properties>
oraz powiemy springowi że mamy adnotacyjnie skonfigurowane bean-y :
<context:component-scan base-package="pl.turo.spring" />
O adnotacjach jeszcze wspomnę.
Co ciekawe password będzie miał wartość 'tajne' - jak widać nowe_hasło nie nadpisuje nam wartości zaczytanej z pliku.
Kiedy spring uruchomi już wszystkie post processory - wtedy dopiero będzie instancjonował beany. Domyślnie każdy bean jest instancjonoawny zachłannie i jest singletonem. Oba zachowania można przekonfigurować.
Instancjonowanie można zmienić atrybutem lazy-init taga bean podając wartość true. Spring używa refleksji do tworzenia obiektów, więc dla singletonów wydaje się dobrym pomysłem instancjonowanie zachłanne.
Jeśli potrzebujemy innego zasięgu, to mamy do dyspozycji :

* prototype - spring utworzy zawsze nowy obiekt przy każdym wstrzykiwaniu tego beana. Innymi słowy jeśli w kilku miejscach będziemy wstrzykiwać beana to każdorazowo otrzymamy inną instancję
* request - chodzi o HTTP request.
* session - chodzi o HTTP session.
* można również zdefiniować swój własny scope.

Tag bean posiada strybut scope, gdzie podajemy zasięg, analogicznie jest adnotacja @Scope. Załóżmy, że przedefiniowaliśmy beana PropertyBean tak, aby miał zasięg sesji. Jeśli wstrzykiwalibyśmy go do singletonowego beana pojawi się nam problem : otóż spring w momencie instancjonowania obiektów nie wie nic o sesji http i nie będzie umiał takiego PropertyBean-a nigdzie wstrzyknąć. Istnieje tutaj możliwość skorzystania z proxy - zamiast starać się utworzyć obiekt sesyjny, utwórzmy do niego proxy. Możemy to zrobić anotacyjnie bądź xml-owo. Spójrzmy :
@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS, value="session")
możemy również użyć ScopedProxyMode.INTERFACES - różnica między jednym a drugim jest istotna. Pierwszy korzysta z CGLIB tworząc proxy, drugi używa dynamic proxy z JDK.
W xml-u wygląda to tak :
<bean id="repository" class="pl.turo.spring.repository.ExampleRepository" scope="session">
 <aop:scoped-proxy />
</bean>
i użyty tu jest CGLIB. Proxy to będzie odpowiedzialne za dostęp do odpowiedniego obiektu pobranego z sesji http. Czyli nasz singletonowy bean posiada referencję do proxy, które to potrafi delegować wywołania metod do odpowiedniego beana. To naprawdę działa.
To tyle na dziś.

Brak komentarzy:

Prześlij komentarz