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.

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ý.

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:

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řidání jcl-over-slf4j je pak bez problémů.

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
Specifika:

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...