Skrytý drahokam Springu

Dnes jsem koukal na rozhovor s Rodem Johnsonem, v kterém mluvil o novinkách ve Springu 3.0. Mezi řečí se zmínil o anotaci @Configurable, která je už ve Springu 2.5 a označil ji za skrytý drahokam. Protože jsem o něčem takovém slyšel poprvé, tak jsem se podíval do dokumentace a docela mě to zaujalo. (Teda, teď koukám, že jsem se o něčem podobném zmiňoval už před půl rokem zde, ach ta paměť). V zásadě jde o to, že obvykle funguje dependency injection jen u bean, které spravuje Spring. Takže například nejde použít u JPA entit a podobně. Což je docela škoda, protože pak musíme dělat umělou servisní vrstvu a objektový model tím docela trpí. Máme na jednu stranu třídy, které drží stav (entity) a na druhou stranu servisní třídy, v kterých je jen kód a stavu mají pomálu. Ale ve správném objektovém programování bychom měli mít třídy, které kombinují oboje. Takže například objekt User by měl mít metodu remove(), která by ho smazala z databáze. Jenže to by znamenalo, že by každá instance této třídy musela mít v sobě odkaz na EntityManager nebo ještě lépe DAO, což neumíme injektnout.

Možná si říkáte, že jsem se zbláznil, že to je ošklivé. Musím se přiznat, že mi je tento přístup taky cizí, ale asi je to jen otázka zvyku. Nejspíš jsem jen zvyklý na to procedurální programování, kde mám servisní procedury v singletonu a jim předhazuji hloupé datové objekty, které se o sebe neumějí postarat samy.

Anotace @Configurable je tu právě od toho, aby nám usnadnila injektování i do bean, které nejsou spravovány Springem. Předvedu na příkladu. Mám třídu Bean, které se dá podstrčit nějaký text.

@Configurable
public class Bean {
	private String text;
	public String getText() {
		return text;
	}
	public void setText(String text) {
		this.text = text;
	}
}

Mým cílem je zprovoznit tento test.

	public void testIt()
	{
		Bean bean = new Bean();
		assertEquals("Hallo",bean.getText());
	}

Všimněte si, že vytvářím novou instanci a koukám se, co je v ní nastaveno za text. Podle všeho by tam mělo být null. Ale pokud správně poladím Spring, tak tam může být cokoliv.

Prvním krokem je samozřejmě anotace @Configurable, která označí beanu jako kandidáta na injektování. V druhém kroku musím nastavit, co chci injektnout. Mohu například zvolit klasickou XML konfiguraci.

	<bean class="net.krecan.configurable.Bean" scope="prototype">
		<property name="text" value="Hallo"/>
	</bean>

	<context:spring-configured/>
	
	<context:load-time-weaver/

Zde si všimněme několika věcí. Například toho, že Spring pozná to kam má co injektnout automaticky, podle jména třídy. Nicméně dají se použít i vlastní jména. Pak si všimněme použití rozsahu „protoype“. To dává smysl, já mohu new zavolat kolikrát chci (i když tam to protoype nedáte, tak se to chová stejně, takhle je to jen hezčí). No a pak už nám zbývá jen magie. Pomocí prvního kouzelného slůvka <context:spring-configured/> zařídíme, aby se použil AspectJ aspekt, který to všechno zařídí. Ano, musí se použít AspectJ, zázraky ani Spring nedovede. Zatím.

Druhé kouzelné slůvko <context:load-time-weaver/> nastaví AspectJ, tak aby si podle potřeby při startu aplikace upravil třídy. Aby to totiž všechno fungovalo, tak je potřeba upravit bytecode. A to buď při kompilaci nebo při startu aplikace. Já jsem zvolil druhou variantu, která bohužel také vyžaduje, abyste to celé spouštěli s atributem virtuálního stroje -javaagent:spring-agent-2.5.6.jar. Pokud si kód budete zkoušet sami, tak na to prosím nezapomeňte.

A to je vše, testy projdou bez problému. Vidíme, že Spring umí injektovat i do tříd, nad kterými na první pohled nemá kontrolu. A to aniž bychom nějak zvlášť museli študovat AspectJ. Všechno už máme pěkně připraveno. Samozřejmě, uvedený příklad je umělý, ale pokud bychom našli dost odvahy, tak můžeme bez problémů injektnout EntityManager přímo do entit. A to může být docela užitečné.

Zdrojový kód je ke stažení zde, obzvláště doporučuji extra zvrhlou třídu MagicBean. No a pokud chcete umět Spring tak skvěle jako já, tak se mi ozvěte, můžu vás to naučit :-).

Tags: , , ,

12 Responses to “Skrytý drahokam Springu”

  1. jira Says:

    Přiznám se, že takovej fanda springu nejsem, ale co mě zaujala je přednáška Roda, kterou měl nějakou dobu zoět, tuším, že se jmenovala "Are we there yet?", která právě hovořila o tom, že neprogramuje objektově, ale procedurální, spíš strukturálně. To je samé DAO (žádný stav), samé transfer (value) objekty (žádný kód), ale skutečné objekty a zapouzdření nikde. Takže si myslím, že to je cesta správným směrem, tj. naučit model (business objekty) funkcionalitu, kterou mají umět, tj. např. persistovat je. A ktomu vám (mě ne, protože Spring nepoužívám) tato funkcionality pomůže. Takže do toho!

  2. palo Says:

    Zas tak skryté to není, např:
    http://blog.springsource.com/2008/01/23/new-improvements-in-domain-object-dependnecy-injection-feature/

    Problém je, že když jsem se to snažil použít na reálné webové aplikaci, tak jsem musel Tomcatu přidat paměti a i tak Tomcat startoval 2x pomaleji (místo 20s to bylo 40s). To je daň za aplikování aspectů při classloadingu.

    Vyřešil jsem to zveřejněním jedné beany (která má nainjektované ostatní potřebné beany) přes statický field a domain objekty pak k beanům přistupují přes něj.
    Mít něco "statického" se v dnešní době nepovažuje za "in", ale když se podíváte, jak Spring řeší @Configurable, zjistíte, že to dělá stejně :o)

    Pokud máte někdo jiné zkušenosti, rád se přiučím..

  3. Vlasta Says:

    Naucit objekty fukcionalitu napr persistovat je?

    S tim nemohu souhlasit. To si pak kazdy muze volatu User.save() kdy a kde se mu to libi. Vedlo by to k rozdrobeni business logiky. Kdo nam zaruci, ze kdyz ma User ve svem ifacu metodu save(), ze bude vzdy spravne nasetovane DAO (nebo EntityManager).

    Kdyz se tento pristup dovede ad absurdum tak by zadne servisni metody nebyly potreba...Fuj 🙂

  4. palo Says:

    @vlasta:
    zkus si precist Domain Driven Design od Erica Evanse (je opravdu vyborna - http://domaindrivendesign.org/books/index.html#DDD) a/nebo dalsi zdroje na webu, napr. http://debasishg.blogspot.com/search/label/ddd a potom mozna zmenis nazor a nebude to fuj :o)

  5. Lukáš Křečan Says:

    Co se týče rychlosti při startu, předpokládám, že by to mohlo vyřešit přesunutí úprav bytecode do kompilace, popřípadě nějaký filtr, který omezí množství prohledávaných tříd.

  6. Lukáš Křečan Says:

    Ještě jsem o tom přemýšlel a napadly mě dvě věci.
    1) @palo Máš pravdu, ono to ani jinak než staticky vyřešit nejde. Prostě je tam natvrdo vazba mezi třídou a konfigurací, ne mezi instancí a konfigurací. Varianta s AspectJ je jen mnohem lépe testovatelná než přímé volání statických metod.
    2) Asi jsem špatně zvolil příklad, místo User.remove() tam mělo být User.leave() nebo User.die(), prostě něco co má smysl z obchodního hlediska, ne databázového.

  7. Honza Novotný Says:

    Zdá se mi, že tahle technika asi nebude použitelná v případě, že v aplikaci běží víc aplikačních kontextů. Protože z ukázek není patrné, jak by bylo možné zvolit ten správný kontext, který mě například zajímá pro autowiring. Nebo je tu něco co jsem nepostřehl?

    Na první pohled se mi tohle zdá jako fakt už velká magie. Kdybych studoval cizí kód který by něco takového používal a já o této možnosti nevěděl, tak jsem asi v koncích.

  8. Honza Novotný Says:

    O jej, kdybych jen četl dokumentaci do konce .... 🙂 :

    "6.8.1.2. Working with multiple application contexts

    The AnnotationBeanConfigurerAspect used to implement the @Configurable support is an AspectJ singleton aspect. The scope of a singleton aspect is the same as the scope of static members, that is to say there is one aspect instance per classloader that defines the type. This means that if you define multiple application contexts within the same classloader hierarchy you need to consider where to define the bean and where to place spring-aspects.jar on the classpath.

    Consider a typical Spring web-app configuration with a shared parent application context defining common business services and everything needed to support them, and one child application context per servlet containing definitions particular to that servlet. All of these contexts will co-exist within the same classloader hierarchy, and so the AnnotationBeanConfigurerAspect can only hold a reference to one of them. In this case we recommend defining the bean in the shared (parent) application context: this defines the services that you are likely to want to inject into domain objects. A consequence is that you cannot configure domain objects with references to beans defined in the child (servlet-specific) contexts using the @Configurable mechanism (probably not something you want to do anyway!).

    When deploying multiple web-apps within the same container, ensure that each web-application loads the types in spring-aspects.jar using its own classloader (for example, by placing spring-aspects.jar in 'WEB-INF/lib'). If spring-aspects.jar is only added to the container wide classpath (and hence loaded by the shared parent classloader), all web applications will share the same aspect instance which is probably not what you want."

  9. Dagi Says:

    Bohuzel ten drahokam je tak skryty, ze z toho muze jednoho bolet hlava :-). Problem je v tom, ze pokud tam ten weaving neni, tak se uzivatel hrozne divi, ze se DI neprovede. Mozna vam to neprijde podstatne, ale nam se stalo, pri pouziti tehle techniky a lidi celkem matlo. Pustili si v IDE testy a hrozne se divili, ze to nefakci. No samozrejme by to fakcilo, kdyby vedeli, ze pro tu magii je potreba. Nerikam ze je to chyba technologie, ale...

  10. Lukas Says:

    Dagi, presne. Par dni jsem si s tim hral v prostredi, kde se pouziva maven.

    Compile time weaving bylo nemozne nastavit v Eclipse rucne. Natoz donutit maven, aby pro kazde IDE generoval spravny konfigurak, ktery to zapne.

    I Runtime weaving se musi nastavovat v IDE rucne, ale zaroven ho neni mozne mit v produkci, takze se musi delat 2 ruzne buldy.

  11. Lukáš Křečan Says:

    Dík za zkušenosti z praxe.

  12. Petr Juza Says:

    Kromě @Configurable je ještě možné k podobným účelům použít ProxyFactory, ale to není vůbec tak hezké a hlavně to neřeší inicializaci objektů, kterou nemáme pod kontrolou, např. vytváření objektů Hibernatem.

    Něco málo jsem o tom napsal zde.