Tentokrát to bude stručné a prosté kódu i úvah nad programováním. Vyflushnutí "blogovacího bufferu" za tento víkend není náhoda, k 1.6.2015 opouštím po necelých 6.5 letech Intelis a nastupuji do TopMonks.
V Intelisu jsem byl rád a vzájemně jsme si hodně dali. Vážím si toho, že odcházím v dobrém a nemám problém Intelis doporučit.
Táhne mne to však dál. Tento blogpost tedy stavím taky jako předěl mezi blogposty inspirovanými prací ve starém a novém zaměstnání. Nevím zatím přesně, co mě tam čeká, ale každopádně se moc těším :-).
1. 6. 2015
31. 5. 2015
K čemu je dobrý generický vzor Type literal
Type literal je trik s Java generikami, jehož cílem je obejít v určitých případech type erasure a dosáhnout, aby celý typ (tj. včetně generického argumentu) byl viditelný za běhu. Typy s touto vlastností se v JLS, různých tutoriálech a jiné podobné beletrii nazývají reifiable.
Trik je založen na tom, že u potomka generické třídy parametrizované pro určitý typový parametr je tento parametr čitelný pomocí reflection API, konkrétně použitím přetypování na správný interface v hierarchii pod java.lang.reflect.Type a použitím metody getActualTypeArguments. V Guavě je tento princip ztělesněn ve třídě TypeToken, ale není na knihovně nijak závislý a používám ji tedy pouze pro demonstraci. V tomto blogpostu uvádím 2 příklady z vlastní praxe, kdy použití tohoto typu vedlo – v porovnání s alternativními způsoby – k robustnějším, typově bezpečným řešením.
Pokud je takové zatěžování klienta nežádoucí a identifikace klíčů řetězcem je nedostatečná, je možno použít robustnější řešení: jako klíč mapy použít speciální třídu popisující klíč včetně jeho typu a obalit mapu tak, aby propustila tam i zpět jen hodnoty konzistentní s typem klíče:
Nyní co když přibyde další konfigurační položka představující třeba seznam parametrů? Zápis:
je ilegální. Řešení? Implementovat Key.type jako TypeToken a použít zápis
Při tomto způsobu docílíme typové bezpečnosti a zároveň je ihned čitelné, jakého typu klíč je.
TypeToken tedy dává spolehlivější výsledky a navíc není tak ukecaný, proč ho nepoužít?
Type literals vypadají na první pohled trochu akademicky a kdo zrovna nepracuje na nějakém metasystému (struktuře pro evidenci podoby jiné struktury), pak asi všechny jejich featury nevyužije. Přesto je patrné, že mají uplatnění i pro běžné úlohy.
Trik je založen na tom, že u potomka generické třídy parametrizované pro určitý typový parametr je tento parametr čitelný pomocí reflection API, konkrétně použitím přetypování na správný interface v hierarchii pod java.lang.reflect.Type a použitím metody getActualTypeArguments. V Guavě je tento princip ztělesněn ve třídě TypeToken, ale není na knihovně nijak závislý a používám ji tedy pouze pro demonstraci. V tomto blogpostu uvádím 2 příklady z vlastní praxe, kdy použití tohoto typu vedlo – v porovnání s alternativními způsoby – k robustnějším, typově bezpečným řešením.
Příklad 1: heterogenní mapy
Heterogenní mapa je mapa, kde každá hodnota je jiného typu a tento typ zpravidla souvisí s konkrétním klíčem, se kterým je hodnota spojena. Je možné si ji představit jako typově bezpečné properties. Pokud např. konfiguraci HTTP klienta volajícího vzdálený systém, kde chceme nastavit URL a timeout, implementujeme pomocí Map<String,Object>, pak klient musí vědět, jaký typ hodnot je uložený pod jakým klíčem:Map<String,Object> cfg = new HashMap<>(); ... cfg.put("url",new URL("http://example.com")); cfg.put("timeout",60); ... URL url = (URL)cfg.get("url"); Integer timeout = (Integer)cfg.get("timeout");
Pokud je takové zatěžování klienta nežádoucí a identifikace klíčů řetězcem je nedostatečná, je možno použít robustnější řešení: jako klíč mapy použít speciální třídu popisující klíč včetně jeho typu a obalit mapu tak, aby propustila tam i zpět jen hodnoty konzistentní s typem klíče:
class Key<T> { final String name; final Class<T> type; Key(String name, Class<T> type) {...} } ... class HeterogenousMap { Map<Key<?>,Object> entries; <T> T get(Key<T> key) { return (T)entries.get(key); } <T> void put(Key<T> key, T value) { return entries.put(key,value); } ... final Key<URL> URL_KEY = new Key<URL>("url",URL.class); final Key<Integer> TIMEOUT_KEY = new Key<Integer>("timeout",Integer.class); ... HeterogenousMap cfg = new HeterogenousMap(); cfg.put(URL_KEY,new URL("http://example.com")); cfg.put(TIMEOUT_KEY,60); ... URL url = cfg.get(URL_KEY); Integer timeout = cfg.get(TIMEOUT_KEY);
Nyní co když přibyde další konfigurační položka představující třeba seznam parametrů? Zápis:
final Key<List<String>> PARAMS_KEY = new Key<List<String>>("params",List<String>.class);
je ilegální. Řešení? Implementovat Key.type jako TypeToken a použít zápis
final Key<List<String>> PARAMS_KEY = new Key<List<String>>("params",new TypeToken<List<String>>(){});
Při tomto způsobu docílíme typové bezpečnosti a zároveň je ihned čitelné, jakého typu klíč je.
Příklad 2: odvození správného typu
Máme-li košatou hierarchii, TypeToken nám ulehčuje orientovat se v ní a napojit se vždy na správný typový parametr. Uvažme, že trik s vrácením správného typu je založen na výběru nultého typového argumentu, což je při doporučeném způsobu použití (zápis s pomocnou anonymní třídou) splněno. Pokud ale pravidlo nultého typového argumentu aplikujeme slepě přímo na hierarchii, obdržíme chybné výsledky. Např. pokud potomek nezachová původní typový parametr jako první. Nebo pokud se parametr hlouběji v hierarchii zkonkretizuje a ještě hlouběji se další potomek stane generickým pro parametr, který už má zcela odlišný význam. Obě situace ukazuje následující příklad:import java.lang.reflect.ParameterizedType; import java.math.BigDecimal; import org.junit.Test; import com.google.common.reflect.TypeToken; public class TypeTokenExample { static class Parent<T> { TypeToken<T> entityToken = new TypeToken<T>(getClass()) {}; // Spravne: odchytneme typ tokenem. String wrongType() { // Chyba: zkoumame aktualni tridu. return ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0].toString(); } } static class Child extends Parent<String> {} // konkretizuje predka static class Grandchild<U> extends Child {} // opet genericky argument, ale s jinym vyznamem static class Grandgrandchild extends Grandchild<BigDecimal> {} // konkretizuje pozdejsi genericky argument static class Child2<U,T> extends Parent<T> {} // puvodne prvni parametr odsouva na druhe misto static class Grandchild2 extends Child2<Byte,String> {} // konkretizuje oba parametry static void test(Parent<?> tested) { System.out.println("\nTESTING " + tested.getClass().getName()); System.out.print("right = "); try { System.out.println(tested.entityToken.toString()); } catch (Exception e) { System.out.println(e.getMessage()); } System.out.print("wrong = "); try { System.out.println(tested.wrongType()); } catch (Exception e) { System.out.println(e.getMessage()); } } @Test public void testAll() { test(new Parent<Integer>()); // right: T, wrong: CCE test(new Child()); // right: String, wrong: String (zde je to nahodou dobre) test(new Grandchild<Boolean>()); // right: String, wrong: CCE test(new Grandgrandchild()); // right: String, wrong: BigDecimal test(new Grandchild2()); // right: String, wrong: Byte } }
TypeToken tedy dává spolehlivější výsledky a navíc není tak ukecaný, proč ho nepoužít?
Závěr
Type literals vypadají na první pohled trochu akademicky a kdo zrovna nepracuje na nějakém metasystému (struktuře pro evidenci podoby jiné struktury), pak asi všechny jejich featury nevyužije. Přesto je patrné, že mají uplatnění i pro běžné úlohy.
Refactoring antipatternu "god method"
Ve staticky typovaných jazycích, mezi něž patří i Java, je ve srovnání s dynamicky typovanými jazyky mnohem snazší refactoring. V tomto příspěvku popíšu způsob, jak jednoduše a přitom typově bezpečně vyřešit situaci, kdy potřebujeme na mnoha místech opravit jeden způsob volání metody na jiný.
a která byla volána na stovkách míst pro desítky různých tříd a kombinací varargs. Ze zpočátku užitečné metody se postupem času vyklubal antipattern nejčastěji označovaný jako God object, akorát aplikovaný na metodu. Udělám pro vás všechno, naházejte mi to sem, já si přeberu vstup a postarám se o instanciaci třídy. Další požadavky (typicky specifikování jiného způsobu transformace properties nebo řízení zapnutí/vypnutí transformace) by nutily přidávat další parametry a tím antipattern ještě více zabetonovaly. Proto bylo vytvořeno nové řešení založené na Guava Function:
Nicméně, jak bezbolestně přejít od starého řešení k novému, aniž bychom šli cestou "vše zahodíme a napíšeme to znovu"? V podmínkách projektu by nebylo praktické překlopit volání jedním commitem. Jak zajistit migraci pouze pro malou část projektu, např. pro jednu třídu FooDto?
Přímočarý způsob se nabízí: vyhledat volání textově pomocí regulárního výrazu např. copyProperties\(.*?, *FooDto\.class. To je časté u dynamických jazyků a i v Javě v některých případech nezbývá než to dělat takto. Výhodou je jednoduchost a to, že si procvičíme regulární výrazy. Nevýhodou je, že se tak nemusí najít 1:1 všechny výskyty (pro každý regex lze zkonstruovat situaci vedoucí k nenalezení nebo naopak falešně pozitivnímu nálezu), i když uznávám, že v praxi by pravděpodobně tyto situace byly okrajové a postižitelné jinak. Především je ale nevýhodou to, že se vzdáváme zacházení s kódem na úrovni struktury. Máme k dispozici pouze text, takže i pro další transformaci se s ním musí tak zacházet (typicky vymyslet replacement string, což neřeší importy atd.). Jak se udržet v blízkosti kódu?
Právě využitím statické typové kontroly v jazyce, konkrétně přetížení. Připomeňme si, že přetížená metoda se vybírá již při překladu. Pokud přidáme ještě následující přetížení metody copyProperties
bude pro volání copyProperties(fooEntity,FooDto.class,"ignored","properties") vítězit tato metoda. V prvním kroku nová metoda pouze deleguje volání na původní metodu, ale už tu je otevřený prostor pro další úpravy: změnu implementace a nakonec pravděpodobně inlinování tohoto přetížení do volajícího kódu. Volání pro ostatní třídy přistanou na původní metodě, změna je tedy bezpečná a přitom ovlivní pouze tu část projektu, která je potřeba.
Základní princip pak může být obměňován:
Někdy se copyProperties používá pro kopírování z DTO objektu do DTO. Je však žádoucí vybrat pouze kopírování z entit do DTO. Pokud všechny entity dědí z abstraktního předka, pomůže opět přetížení se specifičtější mezí generického parametru.
Pokud je parametr příliš obecný, rozliší se volání konkrétního typu zavedením přetížení pro konkrétní typ nebo všechny konkrétní typy.
Příklad:
Pokud chceme vysledovat volání a upravit logiku pouze pro jablka, použijeme refactoring:
Pokud chceme vysledovat volání pro všechny konkrétní typy a zajistit, že se obecná metoda nevolá, použijeme refactoring:
U obecné metody nejprve minimalizujeme viditelnost (tím vyskáčou volání z jiných tříd jako chyby a v téže třídě už se výskyt snadno ohlídá) a později ji úplně odstraníme.
Aby to fungovalo, předpokládá se, že skutečný parametr je deklarován s konkrétním typem, tj. Apple apple = ...; doWith(apple);, nikoli Fruit apple = ...; doWith(apple);. To může být někdy problém, ale pokud se držíme smyslu, proč refactoring provádíme, zpravidla se vždy nějak rozhodne.
Naneštěstí, na rozdíl od základní varianty je signatura
po type erasure úplně stejná a v této podobě tedy překladač hlásí chybu. Protože pro její odstranění stačí jen to, aby metoda vrátila specifičtějšího potomka, nabízí se následující workaround:
Ztratí se tím sice možnost metodu volat (cast by selhal na ClassCastException), ale struktura aspoň zůstane zachována pro refactoring a call hierarchy.
Dalším příkladem přetížení je likvidace varargs parametrů, které nejdou inlinovat. (Samotný inline v Eclipse nefunguje, protože tělo metody je příliš složité.)
Metodu:
rozdělíme na dvě:
Každé z dosavadních volání pak spadne do jednoho z nově vzniklých přetížení. První přetížení bez varargs už jde bez problémů inlinovat. Pro druhé přetížení (s varargs) zjistíme Call Hierarchy.
Pokud se nikde nevolá, odstraníme ho. Pokud se volá, odřízneme z varargs další argument (vznikne metoda monster(Foo parameter1, Foo parameter2, Foo... rest) a postup opakujeme (případně, pokud zbývá jediné volání, můžeme signaturu rovnou přizpůsobit tomuto volání).
Refactoring s využitím přetížení se osvědčil jako užitečná technika umožňující přesně zacílit rozsah změn. Současně je zachována typová bezpečnost a bezpečnost transformací kódu během jednotlivých kroků. Nevýhodou je - stejně jako u každého refactoringu - omezení na živý projekt v IDE, takže do modulů, které na sobě závisejí pouze na úrovni jaru, samozřejmě nedosáhne. Myšlenku přetížení je možné omezeně použít i v případech, kdy sice nelze zachovat bezpečnost za překladu, ale kdy se pořád vyplatí do přetížené metody dát alespoň vyhození UnsupportedOperationException, rozlišení dle volající metody (v Javě ze stacktracu) nebo jinou reakci, která se projeví až za běhu.
Pro úspěšné použití refactoringu není bezpodmínečně nutné umět zpaměti odrecitovat pravidla výběru přetížené metody nebo být kovaný v rozdílech mezi přetěžování a překrýváním v souvislosti s generikami. Sám to taky do takových podrobností neznám. Na druhou stranu znalost těchto věcí jistě napomáhá v situaci, kdy je třeba se z nějakých špaget vymotat. Vždy mne těší, když nějaký jazykový rys, který je popsán hromadou paragrafů a běžně ho považujeme za dobrý tak maximálně pro složení certifikační zkoušky, prokáže využití v praxi. A to je myslím právě tento případ.
Problém
V projektu existovala helper metoda pro kopírování fieldů z entity do DTO, jejíž rozhraní a použití vypadala takto:public static <I,O> O copyProperties(I input, Class<O> output, String... ignored) {...} ... FooDto dto = Helper.copyProperties(fooEntity, FooDto.class, "hoo");
a která byla volána na stovkách míst pro desítky různých tříd a kombinací varargs. Ze zpočátku užitečné metody se postupem času vyklubal antipattern nejčastěji označovaný jako God object, akorát aplikovaný na metodu. Udělám pro vás všechno, naházejte mi to sem, já si přeberu vstup a postarám se o instanciaci třídy. Další požadavky (typicky specifikování jiného způsobu transformace properties nebo řízení zapnutí/vypnutí transformace) by nutily přidávat další parametry a tím antipattern ještě více zabetonovaly. Proto bylo vytvořeno nové řešení založené na Guava Function:
FooDto dto = new FooDtoFunction().apply(fooEntity);
Nicméně, jak bezbolestně přejít od starého řešení k novému, aniž bychom šli cestou "vše zahodíme a napíšeme to znovu"? V podmínkách projektu by nebylo praktické překlopit volání jedním commitem. Jak zajistit migraci pouze pro malou část projektu, např. pro jednu třídu FooDto?
Řešení
Přímočarý způsob se nabízí: vyhledat volání textově pomocí regulárního výrazu např. copyProperties\(.*?, *FooDto\.class. To je časté u dynamických jazyků a i v Javě v některých případech nezbývá než to dělat takto. Výhodou je jednoduchost a to, že si procvičíme regulární výrazy. Nevýhodou je, že se tak nemusí najít 1:1 všechny výskyty (pro každý regex lze zkonstruovat situaci vedoucí k nenalezení nebo naopak falešně pozitivnímu nálezu), i když uznávám, že v praxi by pravděpodobně tyto situace byly okrajové a postižitelné jinak. Především je ale nevýhodou to, že se vzdáváme zacházení s kódem na úrovni struktury. Máme k dispozici pouze text, takže i pro další transformaci se s ním musí tak zacházet (typicky vymyslet replacement string, což neřeší importy atd.). Jak se udržet v blízkosti kódu?
Právě využitím statické typové kontroly v jazyce, konkrétně přetížení. Připomeňme si, že přetížená metoda se vybírá již při překladu. Pokud přidáme ještě následující přetížení metody copyProperties
public static <I> FooDto copyProperties(I input, Class<FooDto> outputClass, String... ignored) { return Helper.<I,FooDto>copyProperties(input, FooDto.class, ignored); }
bude pro volání copyProperties(fooEntity,FooDto.class,"ignored","properties") vítězit tato metoda. V prvním kroku nová metoda pouze deleguje volání na původní metodu, ale už tu je otevřený prostor pro další úpravy: změnu implementace a nakonec pravděpodobně inlinování tohoto přetížení do volajícího kódu. Volání pro ostatní třídy přistanou na původní metodě, změna je tedy bezpečná a přitom ovlivní pouze tu část projektu, která je potřeba.
Základní princip pak může být obměňován:
Varianta: omezení generického typu
Někdy se copyProperties používá pro kopírování z DTO objektu do DTO. Je však žádoucí vybrat pouze kopírování z entit do DTO. Pokud všechny entity dědí z abstraktního předka, pomůže opět přetížení se specifičtější mezí generického parametru.
public static <I extends AbstractEntity> FooDto copyProperties(I input, Class<FooDto> outputClass, String... ignored) { return Helper.<I,FooDto>copyProperties(input, FooDto.class, ignored); }
Varianta: přetížení pro konkrétní typy
Pokud je parametr příliš obecný, rozliší se volání konkrétního typu zavedením přetížení pro konkrétní typ nebo všechny konkrétní typy.
Příklad:
public void doWith(Fruit fruit) {...}
Pokud chceme vysledovat volání a upravit logiku pouze pro jablka, použijeme refactoring:
public void doWith(Apple apple) {...} public void doWith(Fruit fruit) {...}
Pokud chceme vysledovat volání pro všechny konkrétní typy a zajistit, že se obecná metoda nevolá, použijeme refactoring:
public void doWith(Apple apple) {...} public void doWith(Pear pear) {...} public void doWith(Orange orange) {...} public void doWith(Banana banana) {...} private void doWith(Fruit fruit) {...}
U obecné metody nejprve minimalizujeme viditelnost (tím vyskáčou volání z jiných tříd jako chyby a v téže třídě už se výskyt snadno ohlídá) a později ji úplně odstraníme.
Aby to fungovalo, předpokládá se, že skutečný parametr je deklarován s konkrétním typem, tj. Apple apple = ...; doWith(apple);, nikoli Fruit apple = ...; doWith(apple);. To může být někdy problém, ale pokud se držíme smyslu, proč refactoring provádíme, zpravidla se vždy nějak rozhodne.
Varianta: migrace pro Guava funkci
K původní metodě copyProperties existovala ještě další za účelem použití jako Guava funkce při transformaci kolekcí (podtržítko na začátku je projektová konvence pro usnadnění budoucího přechodu na Java8 operátor ::):public static <I,O> Function<I,O> _copyProperties(Class<O> output, String... ignored) {...} ... List<FooDto> dtoList = Lists.transform(entityList,_copyProperties(FooDto.class,"hoo");
Naneštěstí, na rozdíl od základní varianty je signatura
public static <I> Function<I,FooDto> _copyProperties(Class<FooDto> outputClass, String... ignored) {...}
po type erasure úplně stejná a v této podobě tedy překladač hlásí chybu. Protože pro její odstranění stačí jen to, aby metoda vrátila specifičtějšího potomka, nabízí se následující workaround:
public interface Function2<I,O> extends Function<I,O> {} public static <I> Function2<I,FooDto> _copyProperties(Class<FooDto> outputClass, String... ignored) { return (Function2<I,FooDto>)Helper.<I,FooDto>_copyProperties(FooDto.class, ignored); }
Ztratí se tím sice možnost metodu volat (cast by selhal na ClassCastException), ale struktura aspoň zůstane zachována pro refactoring a call hierarchy.
Varianta: rozklad varargs
Dalším příkladem přetížení je likvidace varargs parametrů, které nejdou inlinovat. (Samotný inline v Eclipse nefunguje, protože tělo metody je příliš složité.)
Metodu:
public static void monster(Foo... parameters) { for (Foo parameter : parameters) { doWith(parameter) } }
rozdělíme na dvě:
public static void monster() {} public static void monster(Foo parameter1, Foo... rest) { doWith(parameter1); for (Foo parameter : rest) { doWith(parameter) } }
Každé z dosavadních volání pak spadne do jednoho z nově vzniklých přetížení. První přetížení bez varargs už jde bez problémů inlinovat. Pro druhé přetížení (s varargs) zjistíme Call Hierarchy.
Pokud se nikde nevolá, odstraníme ho. Pokud se volá, odřízneme z varargs další argument (vznikne metoda monster(Foo parameter1, Foo parameter2, Foo... rest) a postup opakujeme (případně, pokud zbývá jediné volání, můžeme signaturu rovnou přizpůsobit tomuto volání).
Závěr
Refactoring s využitím přetížení se osvědčil jako užitečná technika umožňující přesně zacílit rozsah změn. Současně je zachována typová bezpečnost a bezpečnost transformací kódu během jednotlivých kroků. Nevýhodou je - stejně jako u každého refactoringu - omezení na živý projekt v IDE, takže do modulů, které na sobě závisejí pouze na úrovni jaru, samozřejmě nedosáhne. Myšlenku přetížení je možné omezeně použít i v případech, kdy sice nelze zachovat bezpečnost za překladu, ale kdy se pořád vyplatí do přetížené metody dát alespoň vyhození UnsupportedOperationException, rozlišení dle volající metody (v Javě ze stacktracu) nebo jinou reakci, která se projeví až za běhu.
Pro úspěšné použití refactoringu není bezpodmínečně nutné umět zpaměti odrecitovat pravidla výběru přetížené metody nebo být kovaný v rozdílech mezi přetěžování a překrýváním v souvislosti s generikami. Sám to taky do takových podrobností neznám. Na druhou stranu znalost těchto věcí jistě napomáhá v situaci, kdy je třeba se z nějakých špaget vymotat. Vždy mne těší, když nějaký jazykový rys, který je popsán hromadou paragrafů a běžně ho považujeme za dobrý tak maximálně pro složení certifikační zkoušky, prokáže využití v praxi. A to je myslím právě tento případ.
12. 5. 2015
Poznámky z migrace logovacího frameworku
Tento příspěvek píšu ve stavu doznívajícího pocitu vzteku na tu overengineerovanou tvář světa Javy, která mne minulý týden doprovázela při migraci z log4j 1.2 na log4j 2.2.
Problémy s deadlocky v major verzi 1 jsou známy a trápily i nás. K tomu byla v projektu zavlečena i závislost na apachí fasádě commons-logging, jež může přinášet problémy při redeployi, a samozřejmě SLF4J, kterou potřebují zas jiné knihovny. Neméně rozlezlá byla i přímá volání logovacího frameworku. Připočtěme oslavné články na novou technologii asynchronního logování v log4j2 na principu LMAX Disruptor a není divu, že jsem řekl: Je čas oddlužit a posunout projekt dál. Zde jsou poznámky z akce:
Po skončení činnosti nástroje je nezbytné sjet rozdíly vizuálně např. v patch souboru. Nástroj např. změnil typy fieldů i jejich inicializaci, ale už ne typy parametru jejich setteru. Také samozřejmě nezohlední rozdíly v API: Apache má debug(Object), zatímco SLF4J debug(String), takže v situacích, kdy se Object nepřevede na String, vyskákají kompilační chyby. Všechny tyto situace ale byly v našem případě pouze minoritní, takže celkově nástroj práci značně ušetřil.
Další úskalí vyvstane při dědění ze Spring tříd, které logují přes commons-logging. Pokud je zděděný logger z Apache a má se použít v místě, kde se s projektem již přešlo na SLF4J, může vzniknout nekompatibilita API. Řešením je buď "zavrtat" reflexí v adaptéru jcl-over-slf4j (nedoporučuji) nebo si vytáhnout potřebný logger podle jména přímo ze SLF4J.
Také bylo nutné zmigrovat konfiguraci ve Springu. Pro commons-logging existuje podpora v podobě CommonsLogFactoryBean, kde stačí nadefinovat:
Ekvivalentní zápis pro SLF4J je (zdroj):
V tomto smyslu očekávám zlepšení s příchodem JDK9 modulů, kde koncept classpath projde revolucí umožňující definovat viditelnost mezi moduly s jemnější granularitou.
Po zvážení výhod a nevýhod jsme se nakonec přiklonili k setrvání u fasády a migraci přímých volání na fasádu (včetně migrace použití třídy MDC). Umím si ale představit, že v některých případech může být rozumné použít framework napřímo. Argumentace á la tento článek už neplatí - placeholdery jsou "nativně" i v log4j2 - a navíc mi smrdí overengineeringem typu "co kdyby se měnilo něco, co se měnit nebude".
(Pro úplnost: log4j2 nabízí i možnost obrácené adaptace na SLF4J, ale to se samozřejmě vylučuje s použitím SLF4J jako fasády, protože by došlo k zacyklení volání.)
Překlápění souborů po splnění určité podmínky je v log4j2 řešeno také nově. Nechci posuzovat, zda byl náš usecase tak specifický nebo knihovna tak nepřipravená, nicméně starý framework s naším usecasem problém neměl, nový ano. Za vše mluví můj dotaz na SO. Nakonec jsem se ošklivému hacku nevyhnul, řešení je tamtéž. Opět se mi framework jevil jako přehlídka všech možných OO patternů a principů, ale běda když chci open, co je myšleno jako closed. Typickým příkladem je tvorba potomků FileManager pomocí factory, privátní field s instancí privátní classy, takže nejde podědit ani neexistuje mechanismus přenastavení zvenčí.
Zajímavé odkazy z hledání příčiny, o podobných problémech: 1, 2, 3, 4, 5, 6, příbuzné issues v log4j2 JIRA.
Ve webové aplikaci musí být log4j2 inicializován hned ze začátku. Toho se dosáhne uvedením org.apache.logging.log4j.web.Log4jServletContextListeneru jako prvního listeneru, nicméně to ještě není postačující podmínka - pokud jiné listenery mají logger jako statický field, je tento field inicializován při nahrání classy, tj. ještě předtím, než se volá samotná lifecycle metoda contextInitialized. (V prostředí servletů 2.5 jsem toto chování zaznamenal a byl nucen to obejít, doufám, že s migrací na verzi 3 se tyto problémy vyřeší.) Při příliš pozdní inicializaci pak např. není správně inicializován WebLookup (identický problém, podobné problémy).
Do starého log4j.xml bylo možné napsat např. <level value="${log4j.root.level}" /> a pomocí Springu nastavit framework na čtení z dodaných properties. To je v novém frameworku dosaženo pomocí mechanismu lookupů. V našem případě je pro tyto properties potřeba znát ServletContext. Konkrétně lze použít buď WebLookup, který je založen na thread-local znalosti servlet contextu, nebo vytvořit vlastní na stejném principu. Placeholder se pak musí upravit na <level value="${config:log4j.root.level}">, kde config je prefix definovaný anotací (lookup opět není nic jiného než log4j2 plugin). Funguje to dobře, za předpokladu, že je log4j2 inicializován ze servlet contextu a ne předčasně (viz předchozí odstavec).
Na druhou stranu nemohu se ubránit hořkému dojmu, že tak jednoduchá věc jako logování nabobtnala do tak odpudivého shluku návrhových vzorů. Pamatuji si dosud na ten naivní pocit naděje, kdy jsem jako začátečník v Javě 1.4 objevil package java.util.logging a těšil se, že brzy už žádné externí logovací knihovny potřeba nebudou. Kdybych věděl, že o 12 let později budu tweetovat tohle...
Problémy s deadlocky v major verzi 1 jsou známy a trápily i nás. K tomu byla v projektu zavlečena i závislost na apachí fasádě commons-logging, jež může přinášet problémy při redeployi, a samozřejmě SLF4J, kterou potřebují zas jiné knihovny. Neméně rozlezlá byla i přímá volání logovacího frameworku. Připočtěme oslavné články na novou technologii asynchronního logování v log4j2 na principu LMAX Disruptor a není divu, že jsem řekl: Je čas oddlužit a posunout projekt dál. Zde jsou poznámky z akce:
Odstranění Apache commons-logging
Lze snadno provést pomocí návodu ve známém článku Michala France, který jsem po redesignu na původním webu blog.fg.cz již nenašel, ale naštěstí se dochoval na blogu Honzy Novotného. Díky kluci. Doplňuji jen pár připomínek:- V případě multimodule projektu se nastavení musí provést u každého modulu, tj. pokud jsou moduly A, B a závislosti A -> B -> commons-logging, pouhé nastavení scope=provided u B nezabrání zavlečení knihovny do A.war.
- Nastavení scope=provided nezabrání zavlečení závislostí artefaktu, později při vlastní migraci je proto nutno doplnit exclusion artefaktu log4j:log4j.
- Pokud se v pom.xml pracuje s maven-dependency-pluginem (náš případ, kdy sestavujeme aplikaci pro Java Web Start), musí se knihovna excludnout i v konfiguraci pluginu (nejjistěji pomocí <excludeArtifactIds>commons-logging</excludeArtifactIds>, excludeScope opět nezabere na tranzitivní závislosti).
Přechod na SLF4J z Apache commons-logging
Když už nemáme apache-commons fyzicky v projektu, nemá smysl si na jejich použití hrát ani ve vlastním kódu. V SLF4J existuje utilita Migrator, kde stačí zaškrtnout příslušný převod a poštvat ji na projekt. Výsledkem je náhrada Apache za SLF4J. Jednoduchý a docela (viz další odstavec) účinný nástroj. Doporučené zálohování za nás řeší git, tak proč to nezkusit?Po skončení činnosti nástroje je nezbytné sjet rozdíly vizuálně např. v patch souboru. Nástroj např. změnil typy fieldů i jejich inicializaci, ale už ne typy parametru jejich setteru. Také samozřejmě nezohlední rozdíly v API: Apache má debug(Object), zatímco SLF4J debug(String), takže v situacích, kdy se Object nepřevede na String, vyskákají kompilační chyby. Všechny tyto situace ale byly v našem případě pouze minoritní, takže celkově nástroj práci značně ušetřil.
Další úskalí vyvstane při dědění ze Spring tříd, které logují přes commons-logging. Pokud je zděděný logger z Apache a má se použít v místě, kde se s projektem již přešlo na SLF4J, může vzniknout nekompatibilita API. Řešením je buď "zavrtat" reflexí v adaptéru jcl-over-slf4j (nedoporučuji) nebo si vytáhnout potřebný logger podle jména přímo ze SLF4J.
Také bylo nutné zmigrovat konfiguraci ve Springu. Pro commons-logging existuje podpora v podobě CommonsLogFactoryBean, kde stačí nadefinovat:
<bean id="logger" class="org.springframework.beans.factory.config.CommonsLogFactoryBean"> <property name="logName" value="myLoggerName" /> ...
Ekvivalentní zápis pro SLF4J je (zdroj):
<bean id="logger" class="org.slf4j.LoggerFactory" factory-method="getLogger"> <constructor-arg value="myLoggerName" /> ... </bean>
Přechod na SLF4J z přímého volání logovacího frameworku
Celý koncept logovacích fasád považuji za krásnou ukázku prosakující abstrakce. Je mi jasné, jak se historicky k tomu stavu dospělo. Nicméně mi vadí, že jakmile přidám do projektu (rozuměj na classpath) fasádu i implementaci, není už v Javě prostředek, jak implementaci zneviditelnit a přinutit používat pouze fasádu (nebo naopak, nechat fasádu pouze pro externí knihovny a v projektu ji zakázat). Důsledkem je, že programátor, který napíše Logger, Ctrl+mezera, pak dostane napovězen logger z fasády i implementace. Praxe pak potvrzuje, že bez dodatečné metodiky (a že se mi řízení programátorů dokumenty příčí) pak výběr záleží na příslovečném vkusu každého soudruha.V tomto smyslu očekávám zlepšení s příchodem JDK9 modulů, kde koncept classpath projde revolucí umožňující definovat viditelnost mezi moduly s jemnější granularitou.
Po zvážení výhod a nevýhod jsme se nakonec přiklonili k setrvání u fasády a migraci přímých volání na fasádu (včetně migrace použití třídy MDC). Umím si ale představit, že v některých případech může být rozumné použít framework napřímo. Argumentace á la tento článek už neplatí - placeholdery jsou "nativně" i v log4j2 - a navíc mi smrdí overengineeringem typu "co kdyby se měnilo něco, co se měnit nebude".
(Pro úplnost: log4j2 nabízí i možnost obrácené adaptace na SLF4J, ale to se samozřejmě vylučuje s použitím SLF4J jako fasády, protože by došlo k zacyklení volání.)
Vlastní upgrade na log4j2
- v pom.xml změnit závislosti z org.slf4j:slf4j-log4j12 na org.apache.logging.log4j:log4j-slf4j-impl a přidat org.apache.logging.log4j:log4j-core
- u webové aplikace navíc přidat org.apache.logging.log4j:log4j-web a com.lmax:disruptor:3.3.2
- vlastní framework má poměrně pěkně napsaný manuál (pokud pominu ten frustrující pocit, že tak jednoduchá věc jako logování vůbec potřebuje takový manuál), kde je vše vysvětleno, doporučuji předtím přečíst, zejména kapitolu o celkové architektuře a o migraci
- migrace XML konfigurace (pokud už u ní chceme zůstat; jinak se nabízí i JSON nebo YAML) je přímočará, trochu se divím, že po vzoru SLF4J Migratoru ještě nikoho nenapadlo napsat na to XSLT šablonu
Asynchronní logování
V log4j2 kromě dosavadního konceptu asychronních appenderů postavených na ArrayBlockingQueue přibyl ještě koncept asynchronních loggerů postavený na knihovně LMAX Disruptor. Podrobnější info je v manuálu. Funkcionalita se nastavuje buď pomocí systémové property, pokud chceme mít všechny loggery asynchronní, nebo pomocí speciálních XML elementů v konfiguraci, pokud chceme asynchronní jen některé. V prostředí webové aplikace považuji nastavení systémové property za trochu kontroverzní způsob, protože tuto konfiguraci považuji za cosi, co by se mělo dodat s warem, nikoli za konfiguraci Tomcatu, kde nakonec systémová property skončí. Programové nastavení pomocí System.setProperty je nejisté, vyžaduje další úsilí spojené s ohlídáním, že bude zavoláno před inicializací logovacího frameworku. Jiná řešení založená na vlomení se do log4j2 nejsou pěkná kvůli nutnosti použít ošklivé reflexní hacky. Proto jsem upřednostnil způsob pomocí XML elementů, i když z výkonového hlediska představuje režii jednoho threadu navíc.Rollover
Překlápění souborů po splnění určité podmínky je v log4j2 řešeno také nově. Nechci posuzovat, zda byl náš usecase tak specifický nebo knihovna tak nepřipravená, nicméně starý framework s naším usecasem problém neměl, nový ano. Za vše mluví můj dotaz na SO. Nakonec jsem se ošklivému hacku nevyhnul, řešení je tamtéž. Opět se mi framework jevil jako přehlídka všech možných OO patternů a principů, ale běda když chci open, co je myšleno jako closed. Typickým příkladem je tvorba potomků FileManager pomocí factory, privátní field s instancí privátní classy, takže nejde podědit ani neexistuje mechanismus přenastavení zvenčí.
Zajímavé odkazy z hledání příčiny, o podobných problémech: 1, 2, 3, 4, 5, 6, příbuzné issues v log4j2 JIRA.
Vlastní appender
V aplikaci byl appender z log4j verze 1 pro ukládání logovacích zpráv do Swing komponenty. Změna major verze přináší změnu API a s tím spojenou nutnost třídu přepsat: náhradu předka AppenderSkeleton za AbstractAppender, změněný vztah k layoutům, zavedení konceptu filtrů, zavírání.AsyncAppender final
Klientská aplikace posílá události s úrovní ERROR na server. To se provádí asynchronním appenderem, který deleguje na appender provádějící vlastní odeslání zprávy. Aby se celý mechanismus asynchronního appenderu nevolal při každé události, vytvořil jsem v původní verzi vlastního potomka AsyncAppenderu, který volal super.append(event) pouze v případě úrovně ERROR. V nové verzi už je AsyncAppender final. Proto bylo nutné použít mechanismus filtrů, který je pro tento případ určen, konkrétně ThresholdFilter.Dynamické změny konfigurace
Programové nastavení konfigurace (appenderů, levelu) je poměrně bez problémů, stačí pochopit architektonické změny ve verzi 2 (rozlišování objektu loggeru a jeho konfigurace). Samozřejmě se tím odřezáváme od fasády, ale pro administrátorské zásahy se stále funkcionalita hodí. Za ocenění stojí mechanismus pluginů: stačí vlastní třídu označit příslušnou anotací a specifikovat způsob vyhledání pluginů (kterých je opět spousta, log4j2 je prostě tank) - v našem případě hledání v zadaném packagi. O zbytek se postará framework. Anotace dokážou popsat i podobu konfiguračních XML elementů.Nutnost inicializovat log4j2 jako první
Ve webové aplikaci musí být log4j2 inicializován hned ze začátku. Toho se dosáhne uvedením org.apache.logging.log4j.web.Log4jServletContextListeneru jako prvního listeneru, nicméně to ještě není postačující podmínka - pokud jiné listenery mají logger jako statický field, je tento field inicializován při nahrání classy, tj. ještě předtím, než se volá samotná lifecycle metoda contextInitialized. (V prostředí servletů 2.5 jsem toto chování zaznamenal a byl nucen to obejít, doufám, že s migrací na verzi 3 se tyto problémy vyřeší.) Při příliš pozdní inicializaci pak např. není správně inicializován WebLookup (identický problém, podobné problémy).
Konfigurace logování z externích properties
Do starého log4j.xml bylo možné napsat např. <level value="${log4j.root.level}" /> a pomocí Springu nastavit framework na čtení z dodaných properties. To je v novém frameworku dosaženo pomocí mechanismu lookupů. V našem případě je pro tyto properties potřeba znát ServletContext. Konkrétně lze použít buď WebLookup, který je založen na thread-local znalosti servlet contextu, nebo vytvořit vlastní na stejném principu. Placeholder se pak musí upravit na <level value="${config:log4j.root.level}">, kde config je prefix definovaný anotací (lookup opět není nic jiného než log4j2 plugin). Funguje to dobře, za předpokladu, že je log4j2 inicializován ze servlet contextu a ne předčasně (viz předchozí odstavec).
Závěr
Log4j2 celkově hodnotím jako pěknou knihovnu, kterou má smysl zkusit. Rozhodně převažují pozitivní změny, které si autoři mohli se změnou major verze dovolit: zásahy do architektury, koncept filtrů, pluginů, failover, sloučení MDC a NDC do ThreadContext.Na druhou stranu nemohu se ubránit hořkému dojmu, že tak jednoduchá věc jako logování nabobtnala do tak odpudivého shluku návrhových vzorů. Pamatuji si dosud na ten naivní pocit naděje, kdy jsem jako začátečník v Javě 1.4 objevil package java.util.logging a těšil se, že brzy už žádné externí logovací knihovny potřeba nebudou. Kdybych věděl, že o 12 let později budu tweetovat tohle...
7. 4. 2015
Tři chytáky k typovým anotacím v Javě 8
Typové anotace patří mezi syntaktické změny přidané v Javě 8. Umožňují přidat anotace nejen k deklaraci typu jako dosud, ale i na mnoho dalších míst, kde může být použit. Oracle dokumentace v úvodním odkazu je poměrně skoupá na informace, ale pěknou představu o možnostech je možné si udělat z tohoto článku.
V porovnání s lambda funkcemi a streamy se mi tato featura zdá poměrně málo zmiňovaná. A když už zmíněna je, je často spojována jen s usecasem, který tvoří její jakousi vlajkovou loď: typová kontrola a podpora sémantické analýzy, okřídlený příklad s @Nullable nebo @NotNull. Nicméně odmyslíme-li si riziko vzniku "annotation hell", k němuž může koneckonců dojít i před verzí 8, přijde mi tato featura mnohem všestrannější. Na svém hobbyprojektu, o němž napíšu třeba někdy v budoucnu, mne napadlo využití, kvůli kterému jsem se o typové anotace začal více zajímat. Některé chování – ač je už teď považuji za logické – jsem však napoprvé nečekal a o tom bude tento článek.
Máme vyjádřit deklaraci seznamu řetězců, samotný seznam může být null. Zápis je zřejmý:
Teď co když místo seznamu má být pole? Zdálo by se, že stačí nahradit příslušný typ:
Omyl! Právě jsme deklarovali pole řetězců, o kterých říkáme, že jsou nullable, ale samotný typ pole je bez anotace. Tj. tomuto chybnému zápisu odpovídá ve světě kolekcí zápis:
Správný zápis je:
Poučení: na první pohled se zdá, že díky syntaxi zápisu polí v Javě se anotace u kolekcí píšou obráceně než u polí. Abych vytěsnil tento dojem nekonzistence, pomáhám si představou, že typová anotace se uvádí těsně před to, co tvoří podstatu příslušného typu (název třídy u kolekce, hranaté závorky u pole).
Co když máme vnořené kolekce nebo pole? (Pro rozlišení, co je čím anotováno, zde použiju víc různých anotací rozlišených čísly.) U kolekcí asi zápis
intuitivně pochopíme opět jako referenci na seznam, která sama může být null (1), prvky seznamu jsou opět seznamy, které mohou být null (2) a jejich prvky jsou řetězce, které také mohou být null (3).
Tipnete si, jak oanotovat pole polí řetězců? Poučeni předchozím bodem už víme, že anotace k polím máme psát před [], ale jak poznat, které závorky představují vnější a které vnitřní pole? V případě kolekcí fungovalo, že zápis deklarace anotovaného typu (např. @Nullable2 List<@Nullable3 String>) bylo možné bez roztržení použít jako typový argument v kolekci vyšší úrovně. Potřeboval jsem generovat zdroják a roztržení by generátor komplikovalo. Proto jsem se nejprve nechal svést k představě, že když např. pole řetězců dostaneme přidáním závorek za String, tj. String[] (barevně je zvýrazněn typ prvku pole), pak pole polí řetězců obdržíme přidáním závorek za String[], tj. String[][]. Dle této úvahy by zápis vypadal:
To je ovšem také omyl. K jeho vyvrácení nepotřebujeme ani Javu 8, stačí si představit pole s deklarovanou velikostí. Např. pole 5x10 je pole prvků String[10] délky 5 a zapíše se jako String[5][10], což potvrzuje, že roztrhnutí se nevyhneme. Správné umístění anotací je tedy:
Poučení: Vztah mezi hranatými závorkami a úrovněmi pole nám může být lhostejný, pouze pokud nepoužíváme anotace ani explicitní vyjádření délky pole. V případě použití alespoň jedné z uvedených featur je ale znalost této oblasti potřeba. Je dobře, že typové anotace byly navrženy konzistentně s dosavadní praxí.
V bodu 1 je návod, jak se pozná anotace typu. Jak ale Java rozezná, kdy anotace patří k parametru a kdy k jeho typu? Není to totéž?
Není. Pravidlo je zde jednoduché: za anotace na parametru se považují všechny zleva až po první syntaktický konstrukt, který není anotace. V případě uvedené metody má anotaci pouze první a čtvrtý parametr. Nicméně takto použité anotace jsou viditelné i od typu, pouze je uvidíme pokaždé jinde: v případě prvního parametru u typu kontejneru, v případě čtvrtého parametru u typu elementu.
Poučení: Toto uspořádání je celkem intuitivní a není na něm co řešit. Podtrhuje důraz na použití správného API na daný problém: jestliže má anotace povahu vlastnosti typu, je třeba se snažit ji získat přes typ, nikoli ji číst z parametru (a doufat, že typem nebude pole nebo se divit, že pro pole to nefunguje). A jestliže má anotace povahu vlastnosti parametru, přistupovat k ní přes anotace parametru. Pro úplnost dodávám, že tento bod se týká nejen parametrů metod, ale i fieldů tříd a samotných metod (jejichž anotace může takto proniknout i do typu fieldu resp. typu návratové hodnoty metody).
Pro podrobnější seznámení odkazuji na specifikaci (upřímně, na bod 2 jsem si po vzoru strýce Františka přišel ještě před jejím objevením). Ta se zabývá i dalšími konstrukcemi jako anotací vnitřních tříd, výjimek apod. Je důležité si uvědomit, že v reflexi je s typovými anotacemi možné zacházet pouze v případech, kde to bylo dosud možné s typy. Tj. anotace na parametrech, metodách, třídách apod. jsou (za předpokladu runtime retention) přístupné přes reflexi, zatímco anotace v případech jako new @Foo Hoo() nebo String s = (@NotEmpty String)object nikoli, tyto případy spadají výlučně do kompetence nástrojů pro zpracování kódu.
Nakonec bych zde rád zmínil pěkný nástroj pro generování Java zdrojáku JavaPoet. Pouze jeho nepřipravenost na syntaktické konstrukty Javy 8 mě přiměla k zájmu o tuto oblast a napsání vlastního řešení. Pro nižší verze Javy bych se nebál ho doporučit a těším se, až po releasu verze kompatibilní s Javou 8 vlastní řešení opustím.
V porovnání s lambda funkcemi a streamy se mi tato featura zdá poměrně málo zmiňovaná. A když už zmíněna je, je často spojována jen s usecasem, který tvoří její jakousi vlajkovou loď: typová kontrola a podpora sémantické analýzy, okřídlený příklad s @Nullable nebo @NotNull. Nicméně odmyslíme-li si riziko vzniku "annotation hell", k němuž může koneckonců dojít i před verzí 8, přijde mi tato featura mnohem všestrannější. Na svém hobbyprojektu, o němž napíšu třeba někdy v budoucnu, mne napadlo využití, kvůli kterému jsem se o typové anotace začal více zajímat. Některé chování – ač je už teď považuji za logické – jsem však napoprvé nečekal a o tom bude tento článek.
Prerekvizity
- omezím se pouze na použití v deklaraci typu
- pro získání představy o tom, co jsme uvedenými ukázkami spáchali, je dobré vědět i o změnách v reflection API, především o hierarchii pod novým interfacem AnnotatedType.
- anotace použité v ukázkách mají retention RUNTIME, což je podmínka viditelnosti v reflection
- anotace použité v ukázkách mají target odpovídající příslušnému konstruktu jazyka (v opačném případě je sice anotaci možné napsat, ale také není v runtime viditelná)
- anotace @Nullable je vybrána pouze pro srozumitelnost, nejde tu o samotnou kontrolu na null
Chyták #1: pole se anotují před hranatými závorkami
Máme vyjádřit deklaraci seznamu řetězců, samotný seznam může být null. Zápis je zřejmý:
private @Nullable List<String> field;
Teď co když místo seznamu má být pole? Zdálo by se, že stačí nahradit příslušný typ:
private @Nullable String[] field;
Omyl! Právě jsme deklarovali pole řetězců, o kterých říkáme, že jsou nullable, ale samotný typ pole je bez anotace. Tj. tomuto chybnému zápisu odpovídá ve světě kolekcí zápis:
private List<@Nullable String> field;
Správný zápis je:
private String @Nullable [] field;
Poučení: na první pohled se zdá, že díky syntaxi zápisu polí v Javě se anotace u kolekcí píšou obráceně než u polí. Abych vytěsnil tento dojem nekonzistence, pomáhám si představou, že typová anotace se uvádí těsně před to, co tvoří podstatu příslušného typu (název třídy u kolekce, hranaté závorky u pole).
Chyták #2: vícerozměrná pole se anotují zleva doprava
Co když máme vnořené kolekce nebo pole? (Pro rozlišení, co je čím anotováno, zde použiju víc různých anotací rozlišených čísly.) U kolekcí asi zápis
private @Nullable1 List<@Nullable2 List<@Nullable3 String>> field;
intuitivně pochopíme opět jako referenci na seznam, která sama může být null (1), prvky seznamu jsou opět seznamy, které mohou být null (2) a jejich prvky jsou řetězce, které také mohou být null (3).
Tipnete si, jak oanotovat pole polí řetězců? Poučeni předchozím bodem už víme, že anotace k polím máme psát před [], ale jak poznat, které závorky představují vnější a které vnitřní pole? V případě kolekcí fungovalo, že zápis deklarace anotovaného typu (např. @Nullable2 List<@Nullable3 String>) bylo možné bez roztržení použít jako typový argument v kolekci vyšší úrovně. Potřeboval jsem generovat zdroják a roztržení by generátor komplikovalo. Proto jsem se nejprve nechal svést k představě, že když např. pole řetězců dostaneme přidáním závorek za String, tj. String[] (barevně je zvýrazněn typ prvku pole), pak pole polí řetězců obdržíme přidáním závorek za String[], tj. String[][]. Dle této úvahy by zápis vypadal:
private @Nullable3 String @Nullable2 [] @Nullable1 [] field;
To je ovšem také omyl. K jeho vyvrácení nepotřebujeme ani Javu 8, stačí si představit pole s deklarovanou velikostí. Např. pole 5x10 je pole prvků String[10] délky 5 a zapíše se jako String[5][10], což potvrzuje, že roztrhnutí se nevyhneme. Správné umístění anotací je tedy:
private @Nullable3 String @Nullable1 [] @Nullable2 [] field;
Poučení: Vztah mezi hranatými závorkami a úrovněmi pole nám může být lhostejný, pouze pokud nepoužíváme anotace ani explicitní vyjádření délky pole. V případě použití alespoň jedné z uvedených featur je ale znalost této oblasti potřeba. Je dobře, že typové anotace byly navrženy konzistentně s dosavadní praxí.
Chyták #3: anotace parametru nesouvisí s anotací typu
Pozornému pohledu na reflection API neunikne, že v Javě 8 přibyla třída Parameter zprostředkující vlastnosti parametru metody, mezi něž patří i anotovaný typ. Současně však může mít anotaci samotný parametr. Uvažme metodu:private void foo( @Nullable List<String> nls, List<@Nullable String> lns, String @Nullable [] nas, @Nullable String [] ans );
V bodu 1 je návod, jak se pozná anotace typu. Jak ale Java rozezná, kdy anotace patří k parametru a kdy k jeho typu? Není to totéž?
Není. Pravidlo je zde jednoduché: za anotace na parametru se považují všechny zleva až po první syntaktický konstrukt, který není anotace. V případě uvedené metody má anotaci pouze první a čtvrtý parametr. Nicméně takto použité anotace jsou viditelné i od typu, pouze je uvidíme pokaždé jinde: v případě prvního parametru u typu kontejneru, v případě čtvrtého parametru u typu elementu.
Poučení: Toto uspořádání je celkem intuitivní a není na něm co řešit. Podtrhuje důraz na použití správného API na daný problém: jestliže má anotace povahu vlastnosti typu, je třeba se snažit ji získat přes typ, nikoli ji číst z parametru (a doufat, že typem nebude pole nebo se divit, že pro pole to nefunguje). A jestliže má anotace povahu vlastnosti parametru, přistupovat k ní přes anotace parametru. Pro úplnost dodávám, že tento bod se týká nejen parametrů metod, ale i fieldů tříd a samotných metod (jejichž anotace může takto proniknout i do typu fieldu resp. typu návratové hodnoty metody).
Závěr
Uvedené vlastnosti je možno si vyzkoušet na příkladu. (Pozor, v Javě 8u11 selže na ClassCastException – na řádku 120 je typ vyhodnocen jako AnnotatedTypeBaseImpl místo správného AnnotatedParameterizedType. V Javě 8u20 i 8u40 je to již v pořádku.)Pro podrobnější seznámení odkazuji na specifikaci (upřímně, na bod 2 jsem si po vzoru strýce Františka přišel ještě před jejím objevením). Ta se zabývá i dalšími konstrukcemi jako anotací vnitřních tříd, výjimek apod. Je důležité si uvědomit, že v reflexi je s typovými anotacemi možné zacházet pouze v případech, kde to bylo dosud možné s typy. Tj. anotace na parametrech, metodách, třídách apod. jsou (za předpokladu runtime retention) přístupné přes reflexi, zatímco anotace v případech jako new @Foo Hoo() nebo String s = (@NotEmpty String)object nikoli, tyto případy spadají výlučně do kompetence nástrojů pro zpracování kódu.
Nakonec bych zde rád zmínil pěkný nástroj pro generování Java zdrojáku JavaPoet. Pouze jeho nepřipravenost na syntaktické konstrukty Javy 8 mě přiměla k zájmu o tuto oblast a napsání vlastního řešení. Pro nižší verze Javy bych se nebál ho doporučit a těším se, až po releasu verze kompatibilní s Javou 8 vlastní řešení opustím.
6. 3. 2015
Kam až může vést konflikt maven dependencí
Poznámky z čerstvé zkušenosti.
Jak mohou dvě volání obyčejného getteru nad stejnou referencí dávat pokaždé jiný výsledek? Referencovaný objekt je javassist proxy představující Hibernate lazy inicializovanou entitu. První volání volá invocation handler, který provolá reálnou metodu, druhé volání se ale na invocation handler nedostane a vrací hodnotu fieldu (která se u umělé třídy nevyužívá, proto je null). Zkoumání ukázalo, že kořenovou příčinou je konflikt artefaktů, nicméně za chováním je shoda více okolností:
Zde je vidět chování pro jednotlivé verze JRE a javassistu:
Poučením z uvedené situace je hlídat si pořádek v mvn dependencích a rozumně aktualizovat na vyšší verze. Problém se sice stal ve fázi vývoje a neohrozil provoz, nicméně jeho diagnostika nějaký čas zabrala. Trávit další čas hrabáním v bytekódu, proč se druhé volání nezaproxuje, už samozřejmě nehodlám, stačí mi potvrzení, že příčina je v nesprávném artefaktu, obzvlášť když bug je v javassistu již fixnut. (Po prolítnutí changelogu jen tipuji, že se jednalo o tuto chybu, ale blíže už to nezkoumám.) Vedlejší zkušeností pak bylo poznat, jak se Maven s konfliktem vypořádává.
Problém
Na projektu máme závislost na artefaktu javassist-3.6.0. Kolega nedávno přidal do projektu dvě knihovny pro podporu REST API: Jersey a výbornou knihovnu Swagger pro dokumentaci. Obě ale zatáhly jiné verze artefaktů: 3.16.1 a 3.18.1. Než jsme si toho všimli, asi týden vše fungovalo, pak jsem však narazil na příklad, kde mi nad chováním Javy zůstal rozum stát. Po anonymizaci, minimalizaci a převodu na test vypadal takto:Animal animal = ... Dog dog = (Dog)animal; assertSame(dog,animal); assertEquals(BigDecimal.ONE,animal.getId()); assertEquals(BigDecimal.ONE,dog.getId()); // zde test spadne, getId() vrací null
Jak mohou dvě volání obyčejného getteru nad stejnou referencí dávat pokaždé jiný výsledek? Referencovaný objekt je javassist proxy představující Hibernate lazy inicializovanou entitu. První volání volá invocation handler, který provolá reálnou metodu, druhé volání se ale na invocation handler nedostane a vrací hodnotu fieldu (která se u umělé třídy nevyužívá, proto je null). Zkoumání ukázalo, že kořenovou příčinou je konflikt artefaktů, nicméně za chováním je shoda více okolností:
- Animal (ve skutečnosti předek všech Hibernate entit) je generická třída parametrizovaná typem svého id, Dog je potomek konkretizující typový parametr pro typ BigDecimal
- Artefakt javassist má groupId ve verzích 3.16.1 a 3.18.1 org.javassist, ve verzi 3.6.0 pouze javassist. Při kopírování do adresáře target se konflikt názvů sice vyřešil přidáním groupId – ve výsledku byly v adresáři nakonec 3 soubory javassist.jar, javassist-javassist.jar a org.javassist-javassist.jar, všechny soubory však měly stejný obsah odpovídající verzi 3.16.1.
- Ve verzi 3.16.1 je bug projevující se u generik.
- Projekt se na lokále spouštěl pod JRE 6.0.37 32bit (to jsem musel nedávno nainstalovat kvůli jiné aplikaci). Pod JRE 7u65 64bit, které běží na produkci, se chyba opět nezreprodukuje.
Kompletní ukázka
import java.lang.reflect.Method; import java.math.BigDecimal; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; import javassist.util.proxy.ProxyObject; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; public class JavassistTest { static abstract class Animal<I> { abstract I getId(); } static class Dog extends Animal<BigDecimal> { private BigDecimal id; BigDecimal getId() { return id; } void setId(BigDecimal poziceId) { this.id = poziceId; } } @Test public void test() throws Exception { ProxyFactory factory = new ProxyFactory(); factory.setSuperclass(Dog.class); ProxyObject proxy = (ProxyObject)factory.createClass().newInstance(); proxy.setHandler(new MethodHandler() { public Object invoke(Object arg0, Method arg1, Method arg2, Object[] arg3) throws Throwable { return BigDecimal.ONE; // return something nonnull - just proof of handler call } }); Animal animal = (Animal<?>)proxy; Dog dog = (Dog)animal; assertTrue(proxy instanceof Animal && animal instanceof Animal && dog instanceof Animal); assertSame(dog,animal); // references are same assertEquals(BigDecimal.ONE,animal.getId()); assertEquals(BigDecimal.ONE,dog.getId()); // here, proxy is not called in JDK 6.0.37 and javassist 3.16.1 } }
Zde je vidět chování pro jednotlivé verze JRE a javassistu:
Závěr
Poučením z uvedené situace je hlídat si pořádek v mvn dependencích a rozumně aktualizovat na vyšší verze. Problém se sice stal ve fázi vývoje a neohrozil provoz, nicméně jeho diagnostika nějaký čas zabrala. Trávit další čas hrabáním v bytekódu, proč se druhé volání nezaproxuje, už samozřejmě nehodlám, stačí mi potvrzení, že příčina je v nesprávném artefaktu, obzvlášť když bug je v javassistu již fixnut. (Po prolítnutí changelogu jen tipuji, že se jednalo o tuto chybu, ale blíže už to nezkoumám.) Vedlejší zkušeností pak bylo poznat, jak se Maven s konfliktem vypořádává.
28. 2. 2015
Ohlédnutí za marketingem Javy 7
Původně jsem chtěl napsat jen komentář pod článek "To nejlepší z Javy 7" na Et netera dev blogu, ale brzy mi rozsah komentáře přestal stačit. Jsem taky rád za změny, které byly v jazyce provedeny, a rozhodně nejsem příznivcem konzervativního přístupu za každou cenu (byl jsem od začátku pro i v případě lambda funkcí i modulů). Pár skutečností ale moje nadšení poněkud tlumí. Možná budete následující zamyšlení považovat za "hejt", ale pokusím se v něm tyto skutečnosti dát do souvislosti se situací, do které se Java 7 narodila.
Důraz na project Coin bylo něco, co mne na propagaci Javy 7 od začátku spíš zaráželo. Měl jsem dojem, že drtivá většina článků, blogů a nejrůznějších odborných komentářů prakticky ztotožňuje (nebo alespoň velmi protežuje) veškerou inovaci, kterou Java 7 přináší, s tímto projektem. Jsem si vědom, že od začátku byly změny zastřešené tímto projektem zamýšleny jako malé, které nemohou mít ambici být srovnávány s revolucemi typu generiky nebo lambda funkce. Nicméně mám dojem, že se míjejí účinkem v něčem, co je hlubší povahy. A když přemýšlím, co to je, docházím ke třem kořenům:
Každý, kdo přecházel z imperativního paradigmatu na objektové, si musel prožít ten aha-efekt, kdy kód s mnoha rozkopírovanými if/else
refaktoroval na jediný if/else, který je potřeba pouze v tovární metodě, neboť vše ostatní se promění na metody vzniklých tříd:
Přidání Stringů do switch je tedy rozpačité z pohledu programovacího stylu, protože u tohoto typu případů se jeho relativní přínos lépe projeví při aplikaci imperativního, nikoli objektového paradigmatu. Kromě toho může podporovat použití Stringů tam, kde by byl vhodnější enum. A nemožnost switchovat přes null bohužel trvá i zde.
Důležitější než rozplývat se nad tím, že místo 1000000 mohu psát 1_000_000, je podle mne přemýšlet: proč ten literál má hodnotu zrovna 1000000? Jak pomůžu tomu, kdo kód po mně bude číst, tento důvod co nejrychleji pochopit? Je to časový údaj? Porovnejte:
A pro jiné případy si udělat pomocnou třídu. Takže podtržítka zčitelňují, ale k přemýšlení nad programováním zas tolik nenapomáhají. Oceňuji přidání binárních literálů (0b0000_0101_0101_1111), nicméně to zas naráží na problém úrovně abstrakce (využití bitových operací ve specializovaných oblastech) i načasování (spousta je toho již dnes napsána, kdo by opravoval na podtržítka např. algoritmy v java.lang.Math?).
O něco lepší situace se zdá u multicatche. Pro něj lze sice také najít příklad v duchu předchozího odstavce – jmenovitě reflection (uvažme, kolik existuje utilit pro pohodlné použití reflexe, které se i postarají o výjimky), nicméně v Javě 6 pro multicatch neexistuje srovnatelný konstrukt, nepočítám-li catchnutí Exception a následné rozhodování pomocí instanceof. (Je-li v takovém případě společnou obsluhou všech výjimek vyhození výš, nabízí Guava praktickou metodu propagateIfInstanceOf.)
Za nejlepší změnu v oblasti týkající se výjimek považuji more precise rethrow – ukázkový příklad, jak překladač může ušetřit přemýšlení programátorovi a zároveň ho při postupu hierarchií volání směrem výš nenutit ke zobecňování výjimek až k Exception. Malou výtku bych měl opět pouze k oblasti načasování, a to vzhledem k současnému trendu používat spíš runtime výjimky, u nichž se tento konstrukt tolik nevyužije.
Když Oracle po akvizici Sunu zjistil, že lambdy a moduly zkrátka nezvládne v jednom releasu, došlo na rozhodnutí vydat "osekanou" Javu. K tomu se přidaly nově zjištěné bezpečnostní problémy, takže Java 7 spatřila světlo světa se značným zpožděním a po porodních bolestech (vzpomeňme na oznámení Marka Reinholda, které ve své době oblétlo Java svět). S přihlédnutím k těmto skutečnostem vnímám jako moudré rozhodnutí vydat release, který tvoří podhoubí budoucích revolučních změn.
Podíváme-li se na Javu 7 z tohoto pohledu, nevede si vůbec špatně. Rád bych zde odkázal na výborný článek Bena Evanse, který na toto téma vyšel v říjnu 2013. I když rozsahem není nijak dlouhý, přišel mi inspirativní, protože odhaluje souvislosti mezi změnami zavedenými v Javě 7 a změnami (tehdy) očekávanými do Javy 8. Dozvíme se tam například, proč není jedno, zda se diamond operátor používá na pravé či levé straně přiřazení a proč se inference nedělá zprava doleva po vzoru scalovského val. Method handles jako chytřejší a rychlejší java.lang.reflect.Method byly přichystány pro reprezentaci lambda funkcí, které jsou volány díky invokedynamic. Tato bytekódová instrukce si také v běžné propagandě získala pověst čehosi, co bylo přidané kvůli dynamickým jazykům, ale faktem je, že Java ji v případě lambd aplikuje i sama na sebe, pěkná prezentace na toto téma je zde.
(Upozornění: Prezentované názory nepostihují celou šíři změn, které release Javy 7 přinesl. Jsem také jistě zatížen pohledem na svoji oblast činnosti, což může pohled zkreslit. Budu proto u tohoto příspěvku obzvláště rád za nesouhlasné reakce, protože důvodně očekávám, že mohou být velmi obohacující. Díky.)
Project Coin – vnější tvář Javy 7
Důraz na project Coin bylo něco, co mne na propagaci Javy 7 od začátku spíš zaráželo. Měl jsem dojem, že drtivá většina článků, blogů a nejrůznějších odborných komentářů prakticky ztotožňuje (nebo alespoň velmi protežuje) veškerou inovaci, kterou Java 7 přináší, s tímto projektem. Jsem si vědom, že od začátku byly změny zastřešené tímto projektem zamýšleny jako malé, které nemohou mít ambici být srovnávány s revolucemi typu generiky nebo lambda funkce. Nicméně mám dojem, že se míjejí účinkem v něčem, co je hlubší povahy. A když přemýšlím, co to je, docházím ke třem kořenům:
- programovací styl. Neschválení původního záměru přidat do Javy Elvis operátor chápu jako snahu Javy podporovat čistý styl programování (i když osobně jsem pro jeho přidání). Nicméně pokud považuji Javu za objektový jazyk, podobnou podporu v některých nových rysech jazyka nevidím.
- úroveň abstrakce. Nové rysy jazyka cílí na nižší úroveň abstrakce, než na jaké píšu kód při své běžné práci.
- načasování. Nové rysy jazyka řeší problémy, které si už programátoři vyřešili pomocí externích (nebo vlastních) knihoven, a proto přicházejí poněkud pozdě.
Stringy ve switch
Každý, kdo přecházel z imperativního paradigmatu na objektové, si musel prožít ten aha-efekt, kdy kód s mnoha rozkopírovanými if/else
if ("dog".equals(animal)) System.out.println("woof"); else if ("cat".equals(animal)) System.out.println("miaow"); ... if ("dog".equals(animal)) eat(cat); else if ("cat".equals(animal)) eat(mouse);
refaktoroval na jediný if/else, který je potřeba pouze v tovární metodě, neboť vše ostatní se promění na metody vzniklých tříd:
if ("dog".equals(animal)) {return new Dog();} else if ("cat".equals(animal)) {return new Cat();}
Přidání Stringů do switch je tedy rozpačité z pohledu programovacího stylu, protože u tohoto typu případů se jeho relativní přínos lépe projeví při aplikaci imperativního, nikoli objektového paradigmatu. Kromě toho může podporovat použití Stringů tam, kde by byl vhodnější enum. A nemožnost switchovat přes null bohužel trvá i zde.
Podtržítka v literálech
Důležitější než rozplývat se nad tím, že místo 1000000 mohu psát 1_000_000, je podle mne přemýšlet: proč ten literál má hodnotu zrovna 1000000? Jak pomůžu tomu, kdo kód po mně bude číst, tento důvod co nejrychleji pochopit? Je to časový údaj? Porovnejte:
long timeInMillis = 300_000; long timeInMillis = TimeUnit.MINUTES.toMillis(5);Je to velikost v bajtech? Porovnejte:
long twoMB = 2_097_152; long twoMB = 2 * 1024 * 1024;
A pro jiné případy si udělat pomocnou třídu. Takže podtržítka zčitelňují, ale k přemýšlení nad programováním zas tolik nenapomáhají. Oceňuji přidání binárních literálů (0b0000_0101_0101_1111), nicméně to zas naráží na problém úrovně abstrakce (využití bitových operací ve specializovaných oblastech) i načasování (spousta je toho již dnes napsána, kdo by opravoval na podtržítka např. algoritmy v java.lang.Math?).
Diamond operátor
"Diamond operátorem chudých" je obyčejná typová inference u generických metod. Tu využili mj. i autoři Guavy, kterým jistě také vadilo zdvojené psaní typových argumentů, jež bylo motivací vzniku diamond operátoru. Takže mám-li Guavu po ruce, pořád je mi blízké použít např. Lists.newArrayList(). I když samotní autoři Guavy už tento způsob de facto považují za deprecated, stále diamondu dobře konkuruje – minimálně z hlediska čitelnosti (ve vizuálním dojmu není tolik nepísmenkových znaků), programovacího stylu (použití factory metody) i načasování (je tu už od Javy 5). Samozřejmě má i nevýhody: typová inference není vždy překladači jasná (typicky prázdný seznam, to je zlepšeno až v Javě 8). Na diamond si postupně zvykám a určitě zvyknu, jen je třeba ho slepě neaplikovat všude. Např. nemožnost použít ho u anonymních tříd byla pro mne zklamáním.Výjimky: AutoCloseable a multicatch
Podobná situace nastává i u featur okolo try/catch. Try-with-resources je skvělá věc, ale při řešení běžných úloh a použití prověřených knihoven se zpravidla dostaneme na vyšší úroveň abstrakce a konstrukt nakonec nepoužijeme. Paradoxně je nám pak jedno, jestli to implementují pomocí try-with-resources nebo postaru, hlavně, že to mají dobře. Nejde pouze o moji oblíbenou Guavu (konkrétně com.google.common.io) nebo Apache commons IO, AutoCloseable je např. i trojice ResultSet, Statement, Connection z JDBC. Pokud takovou logiku už dnes každý volá přes nějakou template, pak je marginální zisk z této jazykové featury jen velmi malý. (Možná by jejímu rozšíření pomohla i větší otevřenost směrem k usecasům, které se netýkají přímo zavírání, ale pracují také s finally, např. lock/unlock, konkrétně tento usecase byl ovšem shozen ze stolu.)O něco lepší situace se zdá u multicatche. Pro něj lze sice také najít příklad v duchu předchozího odstavce – jmenovitě reflection (uvažme, kolik existuje utilit pro pohodlné použití reflexe, které se i postarají o výjimky), nicméně v Javě 6 pro multicatch neexistuje srovnatelný konstrukt, nepočítám-li catchnutí Exception a následné rozhodování pomocí instanceof. (Je-li v takovém případě společnou obsluhou všech výjimek vyhození výš, nabízí Guava praktickou metodu propagateIfInstanceOf.)
Za nejlepší změnu v oblasti týkající se výjimek považuji more precise rethrow – ukázkový příklad, jak překladač může ušetřit přemýšlení programátorovi a zároveň ho při postupu hierarchií volání směrem výš nenutit ke zobecňování výjimek až k Exception. Malou výtku bych měl opět pouze k oblasti načasování, a to vzhledem k současnému trendu používat spíš runtime výjimky, u nichž se tento konstrukt tolik nevyužije.
Skryté inovace Javy 7
Když Oracle po akvizici Sunu zjistil, že lambdy a moduly zkrátka nezvládne v jednom releasu, došlo na rozhodnutí vydat "osekanou" Javu. K tomu se přidaly nově zjištěné bezpečnostní problémy, takže Java 7 spatřila světlo světa se značným zpožděním a po porodních bolestech (vzpomeňme na oznámení Marka Reinholda, které ve své době oblétlo Java svět). S přihlédnutím k těmto skutečnostem vnímám jako moudré rozhodnutí vydat release, který tvoří podhoubí budoucích revolučních změn.
Podíváme-li se na Javu 7 z tohoto pohledu, nevede si vůbec špatně. Rád bych zde odkázal na výborný článek Bena Evanse, který na toto téma vyšel v říjnu 2013. I když rozsahem není nijak dlouhý, přišel mi inspirativní, protože odhaluje souvislosti mezi změnami zavedenými v Javě 7 a změnami (tehdy) očekávanými do Javy 8. Dozvíme se tam například, proč není jedno, zda se diamond operátor používá na pravé či levé straně přiřazení a proč se inference nedělá zprava doleva po vzoru scalovského val. Method handles jako chytřejší a rychlejší java.lang.reflect.Method byly přichystány pro reprezentaci lambda funkcí, které jsou volány díky invokedynamic. Tato bytekódová instrukce si také v běžné propagandě získala pověst čehosi, co bylo přidané kvůli dynamickým jazykům, ale faktem je, že Java ji v případě lambd aplikuje i sama na sebe, pěkná prezentace na toto téma je zde.
Shrnutí
Javu 7 považuji za potřebný release, v němž se většina užitečných změn děje spíš "pod povrchem" a v duchu přípravy na revoluční Javu 8. Na druhou stranu nikdo, kdo má všech pět pohromadě, neřekne: "tady máte nový software, sice vypadá stejně jako předchozí, ale věřte nám, že uvnitř jsme ho vytunili!". Změny syntaxe a obecně změny, o kterých se dobře bloguje, posloužily marketingové stránce a to se taky počítá! O to cennější pak je, když se tyto dva pohledy podaří propojit.(Upozornění: Prezentované názory nepostihují celou šíři změn, které release Javy 7 přinesl. Jsem také jistě zatížen pohledem na svoji oblast činnosti, což může pohled zkreslit. Budu proto u tohoto příspěvku obzvláště rád za nesouhlasné reakce, protože důvodně očekávám, že mohou být velmi obohacující. Díky.)
Přihlásit se k odběru:
Příspěvky (Atom)