liquidform
Metamodellek
Sunday, January 31st, 2010 | Uncategorized | 1 Comment
Néha hasznos lenne, hogy ha egy objektum struktúra felépítésére tudnék típusosan hivatkozni.
Nem találtam jobb nevet erre, mint a JPA 2.0 zsargonjából kölcsönzött Metamodel kifejezést, de talán egy példán keresztül világosabb lesz, mire gondolok. Tegyük fel hogy szeretnék egy Java Bean to Java Bean mapper-t, hasonlót a dozer-hez, csak egyrészt programmatikusan szeretném konfigurálni (nem XML-ből), másrészt én akarom részletesen megmondani, hogy mit másoljon, és csak azt másolja, amit szeretnék.
Tegyük fel, hogy van egy managed JPA entitás struktúránk, amiből szeretnék egy detached változatot, de úgy, hogy én mondom meg, hogy az egyes objektum kapcsolatokból melyik legyen kifejtve és melyik ne. Ezt a vázolt mapper alkalmazással fogom elérni, belemondom, hogy melyik atribútuomkat másolja, és egy adott entitásból egy ugyanolyan detached állapotú osztályhierarchiát másol.
Hogy mit másol, azt megadhatom mondjuk Expression Language szerű kifejezéssekkel. Pl.:
1 2 3 4 5 6 | Mapper m = MapperFactory.create(Partner.class); m.config("partner.nev"); m.config("partner.cim.varos"); Partner managed = .... Partner detached = new Parner(); m.copy(managed,detached); |
És itt szeretném elérni azt, hogy a 2. és a 3. sorban típusosan adhassam meg a másolási szabályokat.
Jelenleg két ötletem van erre:
APT
Az első az, amit a JPA2.0 is alkalmaz. Generálunk meta osztályokat mondjuk PartnerMeta, CimMeta, stb.néven, és azokba begeneráljuk a típusok leírását. Pl.
1 2 3 | Mapper m = MapperFactory.create(Partner.class); m.config(PartnerMeta.nev); m.config(PartnerMeta.cim.varos); |
A JPA2 mondjuk Partner_-nek hívná metamodel osztályt, és nem lenne rekurzív, tehát a Partner_.cim az nem egy Cim_ a JPA2-ben, ezért a fenti definíciót nem is tudnánk használni. Persze nem egy ördöngősség saját metamodelt kitalálni és az APT eszközzel viszonylag elegánsan lehet generálni ezeket az osztályokat (már ha beszélhetünk eleganciáról bármilyen generált kód esetében): Elég egy jól irányzott jar-t elhelyezni a fordítás idejű classpathba és már generálódnak is az osztályaink (lásd még SPI).
A JPA2 API-ját még nem használtam, de azért idemásolom az íze kedvéért egy cikkből, hogy hogy megy ez:
1 2 | Root team = cq2.from(Team.class); cq2.select(team.get(Team_.name)).where(cb.like(team.get(Team_.name), "Longfellow%")); |
cglib
A másik megoldás a futás idejű byte-code generáláshoz vezet. Ezt csinálja pl. a liquidform:
Person p = LiquidForm.use(Person.class, "p"); List people = em.createQuery( select(p).from(Person.class).as(p).where(like(p.getSurname(), "Smith%")).toString()) .getResultList();
és a Mockito:
Partner partner = mock(Partner.class); when(partner.getNev()).thenReturn("valami"); assertEquals("valami",partner.getNev());
Látható, hogy egyes függvények paraméter gyanánt egy függvényhívást kapnak (pl. partner.getNev()) és nem a függvény hívás visszatérési értéke az érdekes, hanem maga a hívás, mintha egy metamodel egy elemét adnánk meg.
De hogy lehetséges ez? Nem tudom megállni, hogy ne idézzem a Mockito szerőjét Szczepan Faber-t:
…I was asked if Mockito is able to mock concrete classes? The answer is yes, Mockito doesn’t care if you mock an interface or a class. Mockito can do it thanks to primordial voodoo magic only ancient shamans understand these days (you guessed right – it’s the cglib library).
A cglib egy futási idejű bytecode generátor, ami az ASM nevű bytecode manipulátort használja. Gyakorlatilag proxy szerű objektumokat tudunk vele létrehozni, amire mi mondhatjuk meg, hogy az egyes metódusok mit csináljanak. Mindkét megoldás úgy kezdődött, hogy csináltunk egy saját Partner osztályt a Mockito.mock vagy Liquidbase.use factory-val. Na ez az osztály már nem egy rendes Partner, hanem a memóriában összerakott saját bytecode-ból áll.
A mockito ebbe a bytecode-ba azt írja bele, hogy egy ThreadLocal polcra tedd el az adott hívás tényét (pl. partner.getNev meghívodott). Aztán a when metódus már meg se nézi milyen paramétert kapott (partner.getNev() visszatérési értékét), hanem erről a polcról megnézi, hogy épp mi futott le, és ezért már tudja, hogy amikor a mockolt osztályon legközelebb meghívódik a getNev(), akkor mit kell visszaadni. (A when metódus természetesen később hívódik meg, mint a partner.getNev(), mert előszőr a when argumentumait kell kiértékelni).
A liquidform ennél kicsit egyszerűbbet utat választott. A partner.getNev() meghívásakor ő is feljegyzi, hogy milyen hívás történik éppen, és visszaad egy visszatérési típusnak megfelelő objektumot, de előszőr egy globális Map-ben a kettőt összerendelni. A like(p.getNev(), “Smith%”) metódust ezért érdeklik a visszatérési értékek, mert ez alapján megkeresi, hogy a Map-ben van-e rá bejegyzés, és ha van, akkor onnan kimatekolja, hogy a p.nev értéket kell berakni majd String szinten a készülő Querybe.
A liquidform megoldásának azonban van egy nagy hátránya: nem tud primitív típusokat kezelni. Mivel a Map-ben létező objektum referenciákra tudja megmondani az elérési utat, ezért egy visszaadott long értékkel nem tud mit kezelni. Persze meg lehetne a liquidformot is okosítani a Mockito módszerével, de akkor egy másik problémába ütközünk. A like(p.getNev(), “Smith%”) híváskor a polcon az szerepel, hogy egy pszeudó hívás érkezett a bytecode lekvárunkba, aki ezt gondosan feljegyezte. Majd ez után kapunk két String paramétert. Hogy melyik String paramétert kell feloldani a polcunkon lévő hívási információból, azt csak akkor tudnánk megmondani, hogy ha egy kitűntetett String értéket lefoglalnánk annak a jelzésére, hogy az a visszatérési érték nem számít, ehelyett használd a polcon lévő hívási információt. Másik lehetőség, hogy ha a paraméterek minden esetben felcserélhetők, akkor azt mondjuk, hogy a metamodeles hívás mindig az első kell, hogy legyen.
Hát itt tartok éppen a gondolataimban.
Archive
- September 2010
- July 2010
- June 2010
- April 2010
- February 2010
- January 2010
- December 2009
- November 2009
- September 2009
- May 2009
- April 2009
- March 2009
- January 2009
- December 2008
- November 2008
- October 2008
- September 2008
- August 2008
- July 2008
- June 2008
- May 2008
- April 2008
- March 2008
- February 2008
- January 2008
- December 2007
- November 2007
- October 2007
- September 2007
- August 2007
- July 2007
- June 2007
- May 2007
- April 2007
- March 2007
- February 2007
- January 2007
- December 2006
- November 2006
- October 2006
- September 2006
- August 2006
- July 2006
- June 2006