středa 13. února 2008

Spring IoC: syndrom míchání jablek a hrušek

Po dlouhé době, delší než by bylo zdrávo, jsem se rozhodl napsat trochu více prakticky orientovaný článek. Ten je věnován problematice míchání managovaných a nemanagovaných tříd při použití Spring frameworku, na které dříve nebo později možná narazíte. Předesílám, že vzhledem k pozdní hodině jeho vzniku, není cílem poskytnout definitivní rozkrytí dané funkcionality, ale ukázat cestu, kterou považuji za správnou.

Na začátek něco málo k ujasnění pojmů použitých ve zbytku článku. Managovaná třída je taková třída, u které je vznik instance a její životní cyklus řízen Spring IoC kontejnerem. Pokud chcete instanci managované třídy, vždy žádáte Spring IoC kontejnerem, který zajistí dependency injection atd. Nemanagovaná třída je taková třída, u které je vznik instance řízen voláním jejího konstruktoru mimo Spring IoC kontejner.

Valná většina aplikací psaných nad Springem je složena z managovaných tříd až po jedno centrální místo např. controller a nebo view helper u webových aplikací, kde se tyto třídy stýkají se světem nemanagovaných tříd. Pokud máte ovšem aplikaci, která byla například na Spring přepsána jenom z části, pak musíte vyřešit problém jakým způsobem získávat z nemanagovaných tříd ty managované.

Příklad: máme nemangovanou třídu MyObject a tato třída potřebuje pro svou činnost managovanou třídu A. Z třídy MyObject nemůže být z nějakého důvodu managovaná. Jak zajistíme, aby třída MyObject získala třídu A?

       
         public class MyObject {
             private A a;
         }
       
    

Pravděpodobně vložíme do konstruktoru a nebo inicializační metody objektu (pokud existuje) kód, který od Spring IoC kontejneru získá managovanou instanci A například přes SingletonBeanFactoryLocator.

       
         public class MyObject {
             private A a;
             
             public MyObject() {
                BeanFactoryLocator bfl = ContextSingletonBeanFactoryLocator.getInstance();
  BeanFactoryReference bf = bfl.useBeanFactory("foo");
         this.a = (A) bf.getFactory().getBean("a");
             }
         }
       
    

Výše uvedený kód bude fungovat, ale přináší dva problémy.

  • Vynucuje takovou inicializaci Spring IoC kontejneru, která zajistí, že bude vždy BeanFactory pod klíčem foo
  • Jakmile nám do hry vstoupí managovaná instance B, která bude používat nemanagovanou instanci MyObject (B-> MyObject -> A), tak můžeme mít seriozní problém. Pokud totiž bude managovaná instance B vytvořena Spring IoC kontejnerem dříve než managovaná instance A, skončí nám program v lepším případě na NullPointerException a nebo v horším inicializace vyhodí kryptickou výjimku, kterou budeme louskat další dvě hodiny. Důvod je na snadě, IoC kontejner nemá informace o tom, že by B záviselo na A, protože MyObject je nemangováná.

Já osobně tyto problémy zařazuji pod syndrom Míchání jablek a hrušek, tedy míchání inversion of control a service locator. No a nyní k jádru pudla, jak z toho ven?

Velice rychlá oprava spočívá v ustanovení explicitní závislosti mezi managovanou třídou B a A. To uděláme tak, že v XML deployment descriptoru přidáme atribut depends-on, který zaručí správné pořadí inicializace i pro managované třídy, které spolu nijak nesouvisí (na úrovni metadat).

       
    <bean id="a" class="A" />
    <bean id="b" class="B" depends-on="a"/>
       
    

To nám sice zafunguje, ale na správu to není nic moc. Druhým řešením je kombinace metadat a AOP. V podstatě metadaty (XML, anotace) popíšeme jaké závislosti má náš objekt MyObject a pomocí AOP zařídíme to, aby po zavolání konstruktoru byly všechny závislosti nastavené. Pak bude zaručené, že pokud kdokoliv zavolá new MyObject(), dostane instanci s korektně nastavenými závislostmi a navíc díky metadatům bude zřejmá závislost mezi B a A.

V krátkosti nastíním.

      
      @Configurable
      public class MyDomainObject {
        @Autowired
        private A a;

      }
      
    

Pomocí anotace org.springframework.beans.factory.annotation.Autowired vytvořím ony zmiňovaná metadata pro definici závislostí. Pomocí anotace org.springframework.beans.factory.annotation.Configurable jsem označil třídu k tomu, aby se na ní bylo možné zavěsit daným aspektem. Nyní stačí jenom Spring instruovat tak, aby vše do sebe zapadlo viz dokumentace Using AspectJ to dependency inject domain objects with Spring.

V této souvislosti stojí za pozornost článek New Improvements in Domain Object Dependency Injection Feature. Toto řešení má jednu zvláštnost, na kterou rádi vývojáři zapomínají a to, že je potřeba udělat takzvaný weaving. Tedy proces, když se nám "zapeče" na příslušná místa aspekt, který děla dependency injection magii.