Názvy argumentů metod v reflexi
Člověk neznalý věci by mohl nabýt dojmu, že přes reflexi v Javě půjdou získat všechny informace, které se v signaturách tříd a metod nacházejí. Reflexe v Javě je skutečně velmi mocná, nicméně k některým informacím se nedostává jednoduše (jak jsme si ukázali v minulém článku) a k některým se bohužel nedokážete dostat vůbec. Do té posledně jmenované kategorie právě patří názvy argumentů metod. A právě o nich se chci dnes rozepsat.
Ve většině scénářů vás názvy parametrů zajímat nebudou, ale pokud byste chtěli koketovat s dynamickým generováním logiky tříd podle signatur metod - podobně jako to dělají například Grails nebo iBatis (a jistě i další), brzy narazíte na to, že by se vám tato znalost velmi hodila. Schválně porovnejte následující ukázkové signatury metod:
Article createArticle(
String title, String description,
Author author, Date publishedDate
);
Article createArticle(
@Param("title") String title,
@Param("description")String description,
@Param("author")Author author,
@Param("publishedDate") Date publishedDate
);
Article createArticleWithTitleDescriptionAuthorPublishedDate(
String title, String description,
Author author, Date publishedDate
);
Jistě mi dáte za pravdu, že první varianta je nejpřehlednější a má dostatečně vypovídající hodnotu. Jenže zrovna s ní budete mít v Javě problémy. V reflexi se totiž dostanete pouze na typy těchto parametrů - tj. Class[] {String.class, String.class, Author.class, Date.class} a to vám k odvození toho, jakou informaci z parametru do jaké property POJO Article vložit, rozhodně nestačí. Že rozhodně nejste první člověk nepříjemně překvapený z tohoto faktu, zjistíte velmi jednoduše, když se zeptáte pana Gůgla.
Kromě řady blogů vám dá asi nejpodrobnější a nejsrozumitelnější přehled open source projekt s názvem Paranamer. V něm se dočtete, jaké důvody vedly autory Javy k tomu, aby tuto informaci v reflexi nezpřístupnili:
Sun had misgivings about the appropriateness of the this change to Java. It was felt that applications could end up depending on parameter names, and that they essentially became part of constructor/method signatures and could never be changed if you wanted to be backwards compatible.
Vzhledem k tlaku skriptovacích jazyků a knihoven, které by rády tuto funkcionalitu využívaly, Sun nějakou dobu zvažoval doplnění této informace do reflexní části Javy, nicméně v JDK 6 se tak nestalo a podobná funkcionalita není plánována ani do JDK 7. Zdá se tedy, že s tímto faktem budeme muset ještě několik let žít.
Jak z této šlamastyky ven? Naštěstí nejsme první, kdo to řeší - potřeba už dohnala řadu lidí k tvorbě nějakých řešení. Bohužel každé z nich má své mouchy:
Obohacování byte kódu
Toto je právě primární způsob řešení zmiňované knihovny Paranamer. V rámci build procesu přidá do kompilovaných class statické pole __PARANAMER_DATA, do kterého uloží informace o všech metodách třídy, jejich argumentech a především názvech argumentů. Při volání metody:
String[] parameterNames = paranamer.lookupParameterNames(method)
potom Paranamer konzultuje právě toto pole, ze kterého získá požadované informace pro danou metodu. Paranamer poskytuje pluginy pro Maven2 a Ant. Co je však poměrně zásadní nedostatek jsou chybějící pluginy do IDE. Pokud budete tedy chtít využívat Paranamer v kombinaci s JUnit testy spouštěnými nad kódem kompilovaným z IDE, budete mít problém. Jinak je tato cesta jedinou skutečně funkční a použitelnou.
Pro parsování Java zdrojového kódu používá Paranamer knihovnu QDox, která v mém případě měla v relativně aktuální verzi (nevím přesně - pravděpodobně to byla 1.9) problémy s některými generikami, což v kombinaci s Paranamer Maven2 pluginem způsobovalo selhávání buildu.
Nicméně vzhledem k současné situaci s JDK doufám, že Paranamer společně s QDoxem zmíněné problémy brzy vyřeší a půjde o obecně použitelný a uznávaný přístup k řešení této situace.
Čtení debug informací z byte kódu
Tento přístup je asi nejčastěji používaný - využívá toho, že když zkompilujete zdrojové kódy s debugovacími informacemi (javac -g) budou názvy argumentů metody přeloženy do těla metody jako lokální proměnné, jejichž název bude možné z byte kódu zjistitelný. Jedná se o alternativní zjišťovací metodu v Paranameru a hlavní zjišťovací metodu ve Spring Frameworku, který se se stejným problémem potýkal také při psaní AOP podpory.
Ve Springu tudíž najdeme rozhranní ParameterNameDiscoverer a jeho základní implementaci LocalVariableTableParameterNameDiscoverer, které nám právě umožňuje poptat se na názvy argumentů metod nebo konstruktorů. V jeho podání se názvy argumentů zjišťují takto:
//třída si cachuje výsledky a proto je poměrně výhodné držet ParameterNameDiscoverer jako singleton
ParameterNameDiscoverer pnd = new LocalVariableTableParameterNameDiscoverer();
String[] argNames = pnd.getParameterNames(myMethodOrConstructor);
Tento přístup je možné bez dodatečné konfigurace použít jak ze všech build nástrojů (Ant, Maven2 ...), tak i z IDE - má bohužel také svou nevýhodu a to poměrně zásadní. Informace jsou dostupné pouze pro metody, které mají své tělo (tj. implementaci). U metod interfacu, nebo abstraktních metod jsou tyto informace nedostupné (neexistuje tělo metody, neexistují lokální proměnné a tudíž není odkud tuto informaci vzít). Tj. pro případy, kdy existuje pouze definice interface a implementace má vzniknout dynamicky je tento přístup nepoužitelný (např. problém u iBatisu v. 3).
Čtení informací z JavaDocu
Toto je poslední z variant, kterou nabízí Paranamer - nicméně zde musíte ke kompilovanému kódu dodat referenci na Javadoc archív (url), což není ve většině produkčních nasazeních možné.
Závěrem
Současný stav je tedy poměrně tristní. Jediným inženýrským rozhodnutím od stolu se zadělalo na nehezký problém, který se těžko obchází. Je mi vcelku jasný argument, že umožněním čtení názvů parametrů by se tato informace defakto stala součástí API, kterou není možné jednoduše měnit, aniž bychom se obávali naboření zpětné kompatibility.
Na druhou stranu, je čím dál víc zcela legálních případů, kdy by využití názvů metod vyloženě pomohlo dělat úsporná a přehledná API, čemuž je v současnosti efektivně zamezeno.
Buď jak buď, osobně toto rozhodnutí vnímám jako něco, co bylo rozhodnuto z pozice síly bez ohledu na potřeby Java vývojářů. To že je řešení této potřeby vyjmuto i z JDK 7 můj názor jen potvrzuje. V praxi jsem viděl už řadu podobných rozhodnutí, které ve výsledku vedly jen k daleko horšímu stavu, než kdyby původní autor API nechal uživatelům-vývojářům volnou ruku.
Jsou mi jasné motivy defenzivního návrhu API - co se ovšem stane v případě, že danou věc, kterou vám původní autor blokuje opravdu nutně potřebujete? Sám si odpovím, protože jsem to už párkrát sám musel dělat :
- přistoupíte k privátním polím a metodám přes reflexi s přenastavením accessible flagu
- zkopírujete obsah původní třídy / metody do své vlastní
- forknete zdrojové kódy knihovny, provedete nutné vlastní úpravy a vytvoříte si vlastní build
- provedete úpravy byte kódu za běhu
- zahodíte celou knihovnu a její funkcionality, které by se vám sice hodily, ale pro svůj případ je prostě neohnete
Při psaní API podle si podle mého názoru musíme být vědomi toho, že nikdy nejsme z pozice autora schopni odhadnout všechny případy použití, na které bude knihovna ve výsledku použita. Také musíme uvědomit, že pokud naše skvěle defenzivně napsané API nebude konkrétní věc umožňovat, donutíme zoufalého vývojáře použít jakéhokoliv prostředku, aby naši ochranu probil a výsledek bude potom jenom horší. Vše je hnáno motivem zachování zpětné kompatibility, ale zkusme trochu více věřit ostatním vývojářům, zkusme si věřit sobě navzájem.
Skvělé API nemusí být přespříliš defenzivně psané - Spring Framework je toho skvělým příkladem. A pokud mám jmenovat knihovny na opačné straně spektra určitě budu nerad vzpomínat třeba na jCaptchu nebo i na Acegi Security.
Komentáře