niedziela, 26 września 2010

Za kulisami fabryki springa cz.2

O instancjonowaniu beanów.

Spring tworząc obiekty korzysta z refleksji i oczekuje istnienia konstruktora bezparametrowego. Widoczność nie znaczenia. Co w przypadku gdy nie mamy możliwości bądź nie chcemy takiego konstruktora udostępniać ? Istnieją inne metody.

Możemy utworzyć statyczną metodę fabrykującą. Spójrzmy na klasę i jej konfigurację :
public class ExampleService implements Service {
    private Repository repo;

    public ExampleService(Repository repository) {
        super();
        this.repo = repository;
    }

    private static ExampleService newInstance(Repository repository) {
        return new ExampleService(repository);
    }
}

oraz konfiguracja :
<bean id="service" class="pl.turo.spring.service.ExampleService" factory-method="newInstance">
    <constructor-arg ref="repository" />
</bean>

<bean id="repository" class="pl.turo.spring.repository.ExampleRepository" />  
Spring tworząc beana "service" nie będzie szukał konstruktora z parametrem zgodnym typem z Repository, ale na klasie ExampleService uruchomi metodę newInstance i jej w parametrze przekaże referencję do beana "repository". Jak widać metoda ta może być prywatna. Jeśli chcielibyśmy mieć taką metodę w innej klasie to nie ma problemu :
public class ServiceFactory {
    private Repository repository;

    public ExampleService newService() {
        return new ExampleService(repository);
    }

    public void setRepository(Repository repository) {
        this.repository = repository;
    }
}
oraz konfiguracja :
<bean id="serviceFactory" class="pl.turo.spring.service.ServiceFactory">
 <property name="repository" ref="repository" />
</bean>

<bean id="service" class="pl.turo.spring.service.ExampleService"
 factory-method="newService" factory-bean="serviceFactory" />

<bean id="repository" class="pl.turo.spring.repository.ExampleRepository" />
Tym razem spring tworząc beana "service" oddeleguje utworzenie obiektu do bena-fabryki ("serviceFactory") i na nim wywoła metodę newService.
Zauważmy, że przeciwnie do pierwszego przykładu - tutaj tworzymy beana będącego fabryką, więc metoda nie jest już statyczna.

Kolejną możliwością jest zaimplementowanie interefejsu FactoryBean. Beany takie są automatycznie wykrywane przez springa (BeanFactoryPostProcessor).
Przykład :
public class RepositoryFactory implements FactoryBean<Repository> {
 private boolean singleton;
 private Repository instance;
 
 public Repository getObject() throws Exception {
  if (!singleton || instance == null) {
   instance = new ExampleRepository();
  } 
  return instance;
 }

 public Class getObjectType() {
  return Repository.class;
 }

 public boolean isSingleton() {
  return singleton;
 }
 
 public void setSingleton(boolean singleton) {
  this.singleton = singleton;
 }
}
oraz konfiguracja :
<bean id="repositoryFactory" class="pl.turo.spring.repository.RepositoryFactory">
 <property name="singleton" value="false" />
</bean>

<bean id="service" class="pl.turo.spring.service.ExampleService">
 <constructor-arg ref="repositoryFactory" />
</bean>
Widzimy konfigurację fabryki. Bean tej fabryki jest użyty w miejscu, gdzie powinien pojawić się bean 'repository' i spring domyśli się, że należy uruchomić metodę getObject().

Ponieważ fabryka to najzwyklejszy w świecie bean, to można jej wstrzykiwać wartości/beany. Jedną z częściej wykorzystywanych fabryk w springu jest fabryka sesji hibernate :
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
 <property name="dataSource" ref="dataSource" />
 <property name="packagesToScan" value="pl.turo.spring" />
 <property name="schemaUpdate" value="true" />
 <property name="hibernateProperties">
  <props>
   <prop key="hibernate.format_sql">true</prop>
   <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
  </props>
 </property>
 <!-- dalsza konfiguracja -->
</bean>

To nie koniec możliwości. Spring 3 umożliwia tworzenie konfiguracji w kodzie javy - wystarczy dodać adnotację @Configuration do beana. Przykład :
@Configuration
public class RepositoryConfiguration {
 @Bean
 public Repository repository() throws Exception {
  RepositoryFactory repositoryFactory = new RepositoryFactory();
  repositoryFactory.setSingleton(true);
  return repositoryFactory.getObject();
 }
}
oraz :
@Configuration
@Import({ RepositoryConfiguration.class })
public class ServiceConfiguration {
 
 @Autowired
 private Repository repository;
 
 @Bean
 public Service service () throws Exception {
  return new ExampleService(repository);
 }
}
i konieczny xml :
<context:annotation-config />
<bean class="pl.turo.spring.configuration.RepositoryConfiguration" />
<bean class="pl.turo.spring.configuration.ServiceConfiguration" />
Pokazałem jak utworzyć więcej niż jeden bean konfiguracyjny i jak z jednego wyciągnąć beany aby wstrzyknąć drugiemu (repository wstrzykujemy do service).

W temacie instancjonowania obiektów należy jeszcze wspomnieć o metodach zwrotnych cyklu życia beana. Możemy chcieć aby spring uruchomił metodę beana zaraz po jego utworzeniu i wstrzyknięciu zależności. Możemy to osiągnąć na 3 sposoby :
  • adnotacją @PostConstruct z JSR-250 (metoda musi być bezparametrowa - to jedyne wymaganie)
  • atrybutem init-method w tagu bean (metoda musi być bezparametrowa - to jedyne wymaganie)
  • implementując interfejs InitalizingBean (metoda afterPropertiesSet())

Podobnie możemy chcieć wykonać jakąś akcję przed zniszczeniem beana (dokładniej tuż przed odpięciem beana od fabryki springa)
  • adnotacją @PreDestroy
  • atrybutem destroy-method w tagu bean
  • implementując interfejs DisposableBean (metod destroy())

To na tyle w tamacie instancjonowania obiektów.

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ś.

Hello

Witam na moim blogu. Pracuję jako programista java i postanowiłem usystematyzować i gdzieś przechować moją wiedzę o springu, dlatego powstawać będzie ów blog. Mam nadzieję, że przydadzą się komuś moje luźne notatki z nauki.

Pozdrawiam.