Moje osvědčené postupy – Java

1. Pattern.compile() místo String.replaceAll() ve smyčce

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

Pattern se musí vždy nejprve zkompilovat. Pokud se stejný Pattern často spouští na krátkých řetězcích, je výkon výrazně lepší, když se zkompilovaný Pattern použije opakovaně.

2. StringBuilder místo + ve smyčce

Ve smyčce je new StringBuilder() výkonnější než +.

3. Správné použití equals() a hashCode()

Někdy se do equals() nebo hashCode() zahrnou všechna pole. To se hodí ke kontrole, zda se entita změnila, ale je to špatně. Účelem equals() není kontrolovat, zda byla entita upravena, ale zjistit, zda jde o tutéž entitu, i když instance objektu není stejná. Jinými slovy: používejte pouze primární klíč!

Potřebné je to pro jakékoli kolekce. Když se na List zavolá .contains(), použije Java metodu equals. HashMapy navíc potřebují hashCode(). Obě metody by se proto měly přepisovat společně.

Častou pastí jsou neuložené entity bez ID. Tady je třeba být opatrný a ukládat je co nejdříve, aby se předešlo problémům.

Pro srovnání:

Kontrakt těchto metod se musí vždy dodržet:

4. Pasti s DateTime při kombinaci operačních systémů

Některé operační systémy mají interní hodiny s přesností na nanosekundy, jiné na milisekundy. ZonedDateTime.now() proto nefunguje na obou platformách stejně a rozbíjí equals(). Místo toho přesnost VŽDY upravte!

/**
 * truncating the precision, because it relies on the internal clock of linux/macOs/windows.
 * windows and older macOS versions use only milliseconds, whereas linux and new macOS versions use microsecond precision.
 */
public static ZonedDateTime getNow() {
    return ZonedDateTime.now().truncatedTo(DateTimeUtil.PRECISION);
}

I následující anotace JPA spoléhají na ZonedDateTime.now() a nikdy by se neměly používat jen tak bez úprav:

5. Složité datové typy nedeklarujte inline

Pro složité datové typy vždy vytvářejte vlastní třídy místo vnořování Pair, List apod.

Špatně:

// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();

Lépe:

public class FTOChatMessageCollection {

    private List<FTOChatMessageCollectionPerDay> messagesPerDay;
    private Language language;
    private ChatRoom chatRoom;

    // constructor, getter & setter ...
}
public class FTOChatMessageCollectionPerDay {

    private List<FTOChatMessage> messages;
    private ZonedDateTime day;

    // constructor, getter & setter ...
}

Výhody:

Výjimky:

Pro jednoduché vztahy je Pair skvělý! Například:

Pair<User, Training> pair = this.addCurrentUserToTraining(training);

6. Cachování – ale pořádně (minimální návod k nastavení)

Cache (vyslovuje se /kæʃ/, jako anglické „cash“) je v podstatě jen Map, která uchovává záznamy. Složitější věci, jako je časová invalidace a podobně, jsme vynechali.

Nemusí to být vždy knihovna, když stačí obyčejná Map! Spousta vývojářů to nechápe a ze strachu automaticky sahá po knihovnách. Nejdůležitější na cachování je invalidovat ve správný okamžik.

Past č. 1: úniky paměti (memory leaks)

Úniky paměti při cachování vzniknou velmi snadno. Stačí zapomenout pořádně přepsat equals() a hashCode() a máte HashMapu, ze které uniká paměť. Vždy bezpečnou metodou je vytvářet klíče mapy ručně. Ideální je zahrnout výhradně ID entity, jinak mapa začne prosakovat, jakmile se změní byť jen jediné pole. Což je i správně: jen proto, že se uživateli změní křestní jméno, je to pořád tentýž uživatel. Pro srovnání: 3. Správné použití equals() a hashCode()

Past č. 2: vláknová bezpečnost (thread safety)

Cache v ideálním případě používá ConcurrentHashMap, která nabízí atomické, vláknově bezpečné operace. Mapa se mění jen jediným voláním – computeIfAbsent() – a tím je zajištěna vláknová bezpečnost:

private ConcurrentMap<String, T> cache = new ConcurrentHashMap<>();

protected T getOrCompute(final String key, final Supplier<T> computeFunction) {
    // thread safe, because `computeIfAbsent` is atomic, so no need for `synchronized` here.
    // if you extend this to more than 1 map operation, you should synchronize it
    return this.getCacheMap().computeIfAbsent(key, k -> computeFunction.get());
}

Kdyby byl kód delší a obsahoval více volání, musela by být celá metoda synchronized.

7. Serializace – je ještě potřeba?

Moje otázka: Existuje pravidlo Sonaru s názvem Fields in a “Serializable” class should either be transient or serializable. Objeví se, když do třídy přidáte neserializovatelné pole. Pravidlo uvádí:

Například pod zátěží většina aplikačních frameworků J2EE odkládá objekty na disk a údajně serializovatelný objekt s netransientními, neserializovatelnými datovými členy může způsobit pády programu a otevřít dveře útočníkům.

To mi připadá zastaralé. V knize Effective Java 3rd Edition se uvádí, že serializace při vývoji nového softwaru není relevantní. Navíc odstranění serializace z Javy je pro Oracle „dlouhodobým cílem“. Sám hlavní architekt Javy Mark Reinhold prohlásil:

„Serializace byla v roce 1997 strašlivá chyba.“

Otázka, zda framework Spring může odkládat objekty na disk, už tu padla:

Ptám se tedy:

Odkládají moderní aplikační servery, frameworky nebo samotná implementace J2EE (zejména komponenty Javy, které pracují s CDI, EJB, JPA) objekty pod velkou zátěží na disk?

Nebo je toto tvrzení zastaralé?

Odpověď:

TL;DR Pokud neimplementujete stavovou distribuovanou webovou aplikaci, nezapínáte perzistenci relací a používáte jen cache v paměti, možná se obejdete bez Serializable úplně.

Vysvětlení:

Pro replikaci relací je serializace Javy podle nejnovější specifikace Servletů stále nutností: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

Běžný případ: máte několik serverů za load balancerem, přihlásíte se na jednom serveru a dostanete cookie JSESSIONID. Pokud vás load balancer při dalším požadavku pošle na jiný server, vaše přihlašovací atributy a další atributy s rozsahem relace se serializují a zreplikují na nový server, takže tam váš JSESSIONID zůstane platný.

Další případ, kdy atributy s rozsahem relace potřebují serializaci, nastává, když se aplikační server rozhodne odložit relace na disk. Například v Tomcatu 8 lze nastavit, aby se to spustilo, když je relací příliš mnoho na to, aby zůstaly v paměti, nebo při restartu serveru, pokud chcete relace zachovat. Viz https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Každý aplikační server kompatibilní se Servletem 5.0 musí pro tyto případy ve výchozím stavu podporovat serializovatelné objekty v HTTP relacích.

U některých serverů to lze upravit nebo přepsat, například pomocí serializace Jackson-JSON nebo něčeho podobného. Viz například https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Dalším běžným případem je cachování. Slouží k tomu, aby byly objekty dostupné co nejrychleji, ideálně v paměti, ale případně dočasně odložené na disk nebo na externí cache server.
Existuje pro to standardní API (JSR-107), které serializovatelné objekty záměrně nevyžaduje, ale většina implementací pro odkládání přesto ve výchozím stavu používá serializaci Javy.

I ty lze upravit tak, aby podporovaly jiné mechanismy serializace, například u ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: strategie načítání (fetching)

Vzhledem k článku EAGER fetching is a code smell, který napsal nesmírně charismatický chytrolín a přispěvatel do Hibernate, je jako FetchType vhodnější LAZY. I když některé datové typy bez cizího klíče nedávají smysl, dotazují se tyto typy i samostatně (getCount(), datové tabulky apod.).

Zde jsou všechny scénáře seřazené podle toho, jak uspokojivé řešení nabízejí, od nejméně uspokojivého:

1. Nejhorší případ: LazyInitializationException

Slova jsou zbytečná. Tuhle bolest zná každý z nás.

2. Rychlá oprava

public void updateChatRoom(ChatMessage message) {
    // reload here or LazyInitializationException would be thrown
    message = this.chatMessageService.reload(message); // not optimal: second roundtrip to the database - could easily take 100ms!
    // we missed the chance to unproxy without a performance hit.

    ChatRoom chatRoom = message.getChatRoom().getUpdatedAt(DateTimeUtil.getNow());
}

3. Unproxy přímo v servisní vrstvě

public ChatMessage getChatMessage(final Long id, boolean withRoom) {
    // assume this is the same service + the same transaction.
    // so we are in the same persistence context + everything will be performed together at the transaction flush.
    // perfect: no additional roundtrip like in example 2.!

    ChatMessage message = this.chatMessageService.getById(id);

    if (withRoom) {
        ChatRoom chatRoom = this.chatMessageService.unproxy(message.getChatRoom());
    }
   
    // we should not call message.setChatRoom() -> this getter should not even exist as Hibernate would 
    // map this relation for us! setting it again would perform an update on our foreign key!!
    // so we have no option to set the ChatRoom back to the message and have to pass it everywhere in a Tuple or something similar
    return ???; 
}

4. Fetch přímo v servisní vrstvě (takhle to dělejte!)

public ChatMessage getChatMessage(final Long id, boolean withRoom) {
    // all aforementioned problems solved
    return this.executeSingleResultQuery((query, cb, root) -> {
            if (withRoom) {
                root.fetch(ChatRoomEntity_.partition).fetch(ChatPartitionEntity_.chatModule);
            }

            return cb.equal(root.get(ChatRoomEntity_.id), id);
        });
}

9. Hibernate Search

Hibernate Search je hladce propojený s Hibernate JPA. Pozná, když se entity změní, a snadno se konfiguruje:

@Entity
@Indexed(interceptor = SearchableChatMessageInterceptor.class) // hier können Einträge von der Indexierung ausgeschlossen werden
@FullTextFilterDef(name = "SearchableChatMessageFilter", impl = SearchableChatMessageFilter.class) // hier wird der Key festgelegt
public class ChatMessageEntity implements Searchable { // das Searchable dient als Marker-Interface

    @Column
    @Field  // dieses Feld wird indexiert
    private String message;

    @ManyToOne(targetEntity = ChatRoomEntity.class, fetch = FetchType.LAZY, optional = false)
    @IndexedEmbedded(includeEmbeddedObjectId = true, targetElement = ChatRoomEntity.class)   // indexiert den Fremschlüssel mit.
                                                                                             // Über dieses Entity wird auch die Message gesucht,
                                                                                             // wie in @FullTextFilterDef -> SearchableChatMessageFilter definiert
    private ChatRoom chatRoom;

    // ...

}

Lokálně je vyhledávání vypnuté:

// DefaultInstanceInitializer.java

// we don't need the global search everywhere, givin an option to override this setting here
protected boolean isBuildSearchIndexes() {
    return !Constants.Mode.isDebug();
}

Neostré vyhledávání a chování vyhledávání

if ((needle.split(" ").length > 1 || needle.contains("\"")) && !searchExact) {
  /*
   * Simple Query String. Provides basic functionality like:
   *
   *     boolean (AND using “+”, OR using “|”, NOT using “-“)
   *     prefix (prefix*)
   *     phrase (“some phrase”)
   *     precedence (using parentheses)
   *     fuzzy (fuzy~2)
   *     near operator for phrase queries (“some phrase”~3)
   */
  query = qb.simpleQueryString()
            .onField(field)
            .andFields(moreFields)
            .withAndAsDefaultOperator()
            .matching(needle)
            .createQuery();
} // ...

Lucene

Ladění Lucene pomocí Luke

Naši verzi Hibernate Search je nutné ladit pomocí Luke ve verzi 6.6.0. Funguje to jen s touto verzí!

ElasticSearch

Hibernate Search podporuje také ElasticSearch (ten se pak použije místo Lucene a nabízí lepší neostré vyhledávání – víc jako Google).