7. února 2017

throw new RuntimeException

Známe to všichni – píšu kód, termín hoří, chci co nejdříve mít něco, co se alespoň dá spustit, napíšu další řádek kódu a najednou mě IDE začne upozorňovat, že mám ošetřit nějakou výjimku. No dobře, nechám IDE, ať mi pomůže, a ono se mi vygeneruje toto:
try {
    return serviceClass.getConstructor(URL.class).newInstance(serviceUrl);
} catch (IllegalArgumentException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();

} catch (InstantiationException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalAccessException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (InvocationTargetException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
Co s tím? Výjimky nechci programově ošetřovat. Když nastanou, je problém v nějaké konfiguraci, aplikace si s tím stejně neporadí. Potřebuji se o nich jen nějak dozvědět.

Jak to neudělat dobře

Nechat to tak, jak je. A smazat TODO komentáře, ať mi to v IDE nesvítí. Chyba se mi někam vypíše, snad to bude někam, kde to půjde přečíst. Kód ale bude pokračovat, jako by k chybě nedošlo, takže se objeví nová chyba, která už bude pravou příčinu problému trochu mlžit.
Nebo zrušit odchytávání výjimek a deklarovat u metody propagaci výjimek o úroveň výše. Stejný problém se ale bude muset řešit v místě, kde se metoda volá. Pokud se to i tam vyřeší stejně, bude v aplikaci přibývat metod s velkým množstvím deklarovaných výjimek, které nebude chtít nikdo řešit, ale bude muset.

Jak to udělat předpisově

Odchytit výjimky a vyhodit nové, které se ale nemusí ošetřit (Runtime exception). Do těchto výjimek ještě nejlépe přidat informace o kontextu, v jakém k chybě došlo – v ukázkovém případě například informace o objektu serviceClass a parametru serviceUrl.
Vyhazovat pro každý typ odchytávané výjimku „obalovací“ Runtime výjimku vždy stejného typu? Pokud chceme být předpisový, tak ne. Každá výjimka může nést jiné informace o kontextu (např. u NoSuchMethodException by na rozdíl od ostatních měla být informace o metodě). Dále by se neměla komplikovat situace volající metodě, kdyby chtěla výjimky řešit. Hledat příčinu chyby procházením hierarchie výjimek není nejlepší řešení (už jen proto, že místo v hierarchii, kde pravou příčinu hledat, s největší pravděpodobností nebude nikde popsáno a už vůbec nebude deklarováno, že se to v budoucnu nezmění).
Vyřešit situaci předpisově je poměrně časově náročná záležitost. Otázka je, zda vynaložené úsilí bude odpovídat přínosu.

Jak to udělat normálně

Odchytit výjimky a vyhodit nové, které se ale nemusí ošetřit (Runtime exception). Chceme si ale co nejméně komplikovat život s vytvářením nových výjimek. A to i za cenu, že další práce s výjimkou bude komplikovanější. Můžeme si to dovolit? Tuto otázku bychom si měli pokaždé položit a podle odpovědi se přizpůsobit.
Pokud nastane NoSuchMethodException, co mi stačí, abych situaci vyřešil? Pravděpodobně bude nějaký nepořádek v knihovnách. Bude mi stačit v logu výpis chyby včetně stacktrace, ve kterém bude název třídy a metody. Nic víc nepotřebuji.
Pokud chceme být poctiví, použijeme jako „zabalovací“ výjimku nějakou již existující Runtime výjimku se stejným významem. Na to to chce trochu zkušeností. Kdo dokáže najít během pár minut Runtime ekvivalent k NoSuchMethodException?
Jestliže jsme Runtime ekvivalent nenašli, vyrobíme si ho. Všichni na projektu by měli mít přehled o těchto výjimkách – ať je ve stejných situacích taky používají.
Častěji kolem sebe ale vidím, že v těchto situacích vznikají vlastní Runtime výjimky, které svým názvem popisují příslušnost do nějakého modulu našeho projektu – např. pro náš příklad ServiceCreatorException. Ano, urychlí to vyřešení ošetření výjimky. Ne, nezlepší to řešení situace, až výjimka opravdu nastane.

Jak to hacknout

Zajímavou možností je transformace výjimky na Runtime s tím, že výjimka zůstane stále stejného typu. V tomto článku je popis tohoto triku. Lze pak napsat:
public static void main(final String[] args) {
  Unchecked.rethrow(new IOException());
}
Výstupem je:
Exception in thread "main" java.io.IOException
       at cz.cca.Unchecked.main(Unchecked.java:19)
Toto je velice elegantní řešení – výjimka není ničím obalená, prostě jako by byla Runtime.
Pozor, článek se jmenuje „Hack of the day“. Ne každý bude nakloněn tomuto řešení. Já osobně nechápu, proč toto ještě není v Java standardu.

Jak na to po Googlovsku

Google si je evidentně vědom četnosti řešení tohoto problému a proto má ve svém projektu guava třídu Throwables, která toto řeší. Jednoduše se řekne, co za výjimky nechcete řešit a to se propaguje dál obalené do java.lang.RuntimeException.
try {
  someMethodThatCouldThrowAnything
();
} catch (IKnowWhatToDoWithThisException e) {
  handle
(e);
} catch (Throwable t) {
  
Throwables.propagateIfInstanceOf(t, IOException.class);
  
Throwables.propagateIfInstanceOf(t, SQLException.class);
  
throw Throwables.propagate(t);
}
A tady už to začíná být na hraně. Je správně, když se výjimky zabalí do obecné java.lang.RuntimeException?

throw new RuntimeException?

A co cesta nejmenšího odporu – zabalit výjimku do java.lang.RuntimeException? Jde asi o nejrychlejší způsob, jak situaci vyřešit. Skoro bych to zařadil do kapitoly „Jak to udělat normálně“ až na to, že pokud bude třeba výjimku programově řešit, bude se muset odchytávat java.lang.RuntimeException. Zpracovat výjimku java.lang.RuntimeException je zas o fous složitější než zpracovat vlastní výjimku typu ServiceCreatorException. Přeci jenom ServiceCreatorException nese alespoň informaci o tom, že k chybě došlo v našem modulu ServiceCreator.
Asi každý nástroj pro kontrolu kódu má pravidlo kontrolující, že nikde v kódu není vyhození obecné výjimky java.lang.RuntimeException. Důvodem je, že by se nemělo RuntimeException odchytávat a i když se odchytne, špatně se zjišťuje pravá příčina výjimky.
Co když jsem si ale jistý, že výjimku, kterou obaluji do RuntimeException, nebude nikdy nikdo potřebovat odchytávat? V tom případě by všechny argumenty proč nevyhazovat java.lang.RuntimeException padly. Nikdy si nemůžeš být jistý, že nikdy nikdo nebude potřebovat výjimku odchytávat.

Jaká je praxe?

Kromě Google guava lze vyhazování java.lang.RuntimeException vysledovat na mnoha velkých projektech. Stačí si stáhnout jejich zdrojové kódy a prohledat je na výskyt „throw new RuntimeException“. Namátkou v Apache CXF je vyhození RuntimeException na 347 místech.
Proč je tato praktika tak rozšířená, když ji velké akademické autority odsuzují?
Podle mého jednoduše proto, že si lidé řeknou: „Tohle opravdu nebude nikdy nikdo odchytávat. A když náhodou jednou ano, tak to pak vylepším“.

Autor článku: Jan Herma, CCA Group a.s.

1 komentář:

  1. Skoda, ze neni uveden autor.. Pote bych se rozhodl zda precist ;)

    OdpovědětVymazat