2017-10-25

Nástroje pro kontrolu platnosti parametrů v Java

Pro kontrolu platnosti parametrů se využívají validační třídy z někerý Java frameworků, rád bych připomněl (z mého pohledu) ty nejvýznamnější:
  1. Validate z projektu Apache Commons
  2. Preconditions z projektu Google Common
  3. Assert z projektu Spring Framework
  4. Objects z Java 8 (metoda requireNonNull(), doplněno 2019-09-12)
Třídy se liší počtem i názvem veřejných metod, všechny (výše zmíněné) podporují zprávy pro výjimku, ale jen první dvě umožňují vkládání parametrů pomocí šablon pro rychlejší běh aplikace (finální zprávu lze sestavit až před vyhozením výjimky, zpravidla IllegalArgumentException). Pokud však máte v oblibě framework pro logování událostí zvaný LogBack, vznikne v projektu nekonzistence zápisu parametrů v šabloně, protože LogBack označuje pozici parametru v šabloně dvojicí složených závorek "{}" (na rozdíl od validátorů používajích zpravidla výraz "%s"). Této rozdílnosti se nezbavíme ani použitím třídy Logger ze standardní Java knihovny, protože jeho šablona očekává číslovaný seznam parametrů podle vzoru "{0} {1}". Také se mi nelíbí nejednotné vyhazování výjimek, protože první dvě implementace (výše zmíněné) vyhazují při porušení pravidla not-null odlišnou výjimku (typu NullPointerException). Přesto, že uvedené výhrady nejsou zásadní, po zvážení jsem přistoupil ke vlastní implementaci validátoru s plným vědomím rizika, že to nezpůsobí revoluci ve světě IT :-). Jako vzor pro inspiraci jsem použil třídu Assert ze Spring frameworku, líbilo se mi, jak autoři obešli zápor v názvu metody pro testování parametru typu not-empty. Nový validátor využívají (podle očekávání) ostatní moduly frameworku Ujorm, třída zodpovědná za sestavování zpráv se využívá také pro interní proxy loggger.

Popis třídy Assert z frameworku Ujorm

Vlastnosti nové implementace:
  • podporuje parametry v šabloně (na rozdíl od svého vzoru)
  • pozice parametrů se označuje párem složených závorek "{}", formátování hodnot parametrů není podporováno šablonou
  • do zprávy se zapisují i parametry, které nemají vlastní značku v šabloně a jsou pak odděleny čárkou
  • parametry typu Throwable, které nemají vlastní značku v šabloně vypisují stacktrace
  • jsou podporované také argumenty typu Supplier (od verze 1.82+)
  • podmínku platnosti parametru lze popsat Lambda výrazem
  • při porušení platnosti se vyhazuje výhradně výjimka IllegalArgumentException
Uvádím vzorové použití, v komentáři najdete zprávu vyhozené výjimky:

   Integer value = 20;
   Assert.isTrue(value < 10, "Wrong number {}!", value); // "Wrong number 20!"
   Assert.isTrue(value < 10, "Wrong", value);            // "Wrong, 20"
   Assert.isTrue(value < 10,  value);                    // "20"
   Assert.isTrue(value < 10);                            //  null

   value = null;
   Assert.isTrue(value, (x)-> x<10, "Wrong number {}!", value); // "Wrong number null!"

   Supplier<Object> s = () -> value;
   Assert.isTrue(value < 10, "Wrong number {}!", s); // "Wrong number null!"
Všimněte si, že parametry postrádající svoji značku v šabloně se zapsaly na konec zprávy (například framework LogBack je zahazuje). Pokud je skutečný počet parametrů šablony naopak menší, nevyužité značky se zobrazí beze změny. Metody sice neřeší formátování parametrů, ale v případě nouze je možné parametry pro logování obalit vlastní třídou s překrytou metodou toString(), která to formátování řešit může. Pokud má parametr hodnotu null, tak nedochází k volání Lambda výrazu a není třeba to ošetřovat v kódu. Vzorové použití dalších metod uvádím pro zjednodušení bez kometářů, všechna tvrzení jsou pravdivá:

   Assert.isTrue(true);
   Assert.isTrue(30, (x)-> x>20);
   Assert.notNull("ABC");
   Assert.hasLength("ABC");
   Assert.hasLength(new char[]{'A', 'B', 'C'});
   Assert.hasLength(new StringBuilder().append("ABC"));
   Assert.hasLength(Arrays.asList("A", "B", "C"));

   Assert.isFalse(false);
   Assert.isFalse(30, (x)-> x<20);
   Assert.isNull (null);
   Assert.isEmpty("");
   Assert.isEmpty(new char[0]);
   Assert.isEmpty(new StringBuilder());
   Assert.isEmpty((List) null);
 
Performance testy vychází poměrně příznivě, pro zájemce přikládám odkaz na jUnit testy třídy Assert, případně na testy třídy MsgFormatter, která je zodpovědná za sestavování chybových zpráv. Pokud vás tento článek zaujal, knihovnu můžete připojit snadno pomocí Mavenu, velikost JAR je pouze 10 KB, modul přitom nemá žádné další závislosti.

   <dependency>
     <groupId>org.ujorm</groupId>
     <artifactId>ujo-tools</artifactId>
     <version>1.82</version>
   </dependency>

2017-10-06

Sestavování textových zpráv pomocí šablon nejen v Ujorm

Ujorm verze 1.74 obsahuje nový, samostatný modul, který nabízí třídy pro sestavování textových zpráv pomocí šablony a parametrů. Pro srovnání přikládám vzorové použití standardní Java knihovny. Všechny třídy se liší především způsobem zápisu parametrů v šabloně. Použité ukázky kódu jsem zkopíroval z jUnit testů projektu Ujorm.

Třída MsgFormatter

Metoda MsgFormatter.format() z knihovny Ujorm je určena pro rychlé použití šablony, která označuje vkládané parametry dvojicí složených uvozovek "{}", pořadí parametrů vkládaných do šablony je dáno pořadím v metodě. Vzorová ukázka použití je tady:

    assertEquals("TEST"    , MsgFormatter.format("TE{}T", "S"));
    assertEquals("TE, S, T", MsgFormatter.format("TE", "S", "T"));
    assertEquals("TES{}"   , MsgFormatter.format("TE{}{}", "S"));
 
Výhodou je absence výjimek způsobených chybným počtem parametrů, protože přebytečné parametry se zapisují na konec zprávy oddělené čárkou s mezerou a ty chybějící se pouze nenahradí za značku. Pokud má výsledná zpráva obsahovat sekvenci "{}", je nutné ji vložit do šablony jako parametr. Třída nepodporuje formátování hodnot parametrů, v případě takové potřeby použijte raději následující třídu ze stejné knihovny.

Třída MessageService

Tato třída je také z knihovny Ujorm. Parametry metody MessageService.format() v šabloně se označují výrazem typu "${PRICE}", kde PRICE je název parametru, který může být volitelně doplněný formátem podle vzoru "${PRICE,%.2f}". Popis formátu je shdoný je shodný s metodou String.format(). Parametry šablony se předávají v objektu typu  Map<String, Object>, ukázka použití následuje:

    String expected = "On 2017-01-15, we spent 254.00 EUR.";
    String template = "On ${DAY,%tF}, we spent ${PRICE,%.2f} EUR.";
    MessageService instance = new MessageService();
    Map<String,Object> params = instance.map
          ( "DAY", LocalDateTime.of(2017, Month.JANUARY, 15, 12, 30)
          , "PRICE", new BigDecimal("254"));
    String result = new MessageService().format(template, params);
    assertEquals(expected, result);

Další ukázky použití najdete v jUnit testu. Pro srovnání uvádím dále ještě dvě metody ze standardní Java knihovny.

Třída String

Statická metoda String.format() vytváří interně instanci třídy Formatter. Pozice parametrů se označují výrazem začínajícím znakem procenta (například "%s") a volitelné formátování hodnoty parametru. Příklad použití:

    String expected = "On 2017-01-15, we spent 254.00 EUR.";
    String template = "On %tF, we spent %.2f EUR.";
    LocalDateTime day = LocalDateTime.of(2017, Month.JANUARY, 15, 12, 30);
    String result = String.format(ENGLISH, template, day, new BigDecimal("254"));
    assertEquals(expected, result);

Pokud je parametrů méně, než značek, metoda vyhazuje výjimku MissingFormatArgumentException, pokud je jich naopak více, tak jsou ignrorovány.

Třída MessageFormat

Poslední metoda MessageFormat.format() je také statická metoda ze standardní Java knihovny a také podporuje formátování parametrů. Místo určené pro vkládání pametrů se označuje výrazy číslovanými od nuly podle vzoru "{0}, "{1}" a konkrétní parametry se vkládájí pomocí pole. Příklad použití:

    String expected = "On 2017-01-15, we spent 254.00 EUR.";
    String template = "On {0,date,yyyy-MM-dd}, we spent {1,number,#.00} EUR.";
    Date day = Date.from(LocalDateTime.of(2017, Month.JANUARY, 15, 12, 30)
              .atZone(ZoneId.systemDefault()).toInstant());
    Object[] params = { day, new BigDecimal("254")};
    String result = new MessageFormat(template, ENGLISH).format(params);
    assertEquals(expected, result);

Výchozí Locale se bere z operačního systému, argument typu java.time.LocalDateTime není bohužel podporovaný a vyhazuje výjimku IllegalArgumentException.

Performance

Přikládám přibližnou výkonnost jednotlivých tříd, tolerance naměřených hodnot může být až 10%. Pro každý formatter proběhlo 5_000_000 iterací ve kterých se do jednoduché šablony (bez formátování) dosadily tři krátké argumenty.
Třída Čas [ms]
Výkon
(větší je lepší)
MsgFormatter 1158 100.00%
MessageService 2268 51.06%
String 12243 9.46%
MessageFormat 5023 23.05%

Další podrobnosti o testu najdete tady, Na závěr přikládám popis Maven závislosti:

   <dependency>
     <groupId>org.ujorm</groupId>
     <artifactId>ujo-tools</artifactId>
     <version>1.74</version>
   </dependency>