Bevált gyakorlataim – Java

1. Pattern.compile() a String.replaceAll() helyett ciklusban

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

A Pattern-t mindig előbb le kell fordítani. Ha ugyanaz a Pattern sokszor fut rövid stringeken, a teljesítmény jóval jobb, ha újrahasznosítod a már lefordított Pattern-t.

2. StringBuilder a + helyett ciklusban

Ciklusban a new StringBuilder() hatékonyabb, mint a +.

3. Az equals() és a hashCode() helyes használata

Néha az equals() vagy a hashCode() minden mezőt figyelembe vesz. Ez hasznos annak ellenőrzésére, hogy egy entitás módosult-e, de helytelen. Az equals() célja nem az, hogy ellenőrizze, módosult-e egy entitás, hanem annak megállapítása, hogy ugyanarról az entitásról van-e szó, még ha az objektumpéldány nem is ugyanaz. Vagyis: csak az elsődleges kulcsot használd!

Erre mindenféle collectionnél szükség van. Ha egy listán meghívod a .contains() metódust, a Java az equals metódust fogja használni. A HashMapeknek ezenfelül a hashCode() is kell. A kettőt együtt kell felülírni.

Gyakori csapda a még el nem mentett, ID nélküli entitás. Itt légy óvatos, és perzisztáld őket korán, hogy elkerüld a problémákat.

Lásd még:

Ezeknek a metódusoknak a szerződését mindig be kell tartani:

4. DateTime-csapdák vegyes operációs rendszereknél

Egyes operációs rendszerek belső órája nanoszekundumos, másoké milliszekundumos pontosságú. Így a ZonedDateTime.now() nem ugyanúgy működik a két platformon, és elrontja az equals()-t. Ehelyett a pontosságot MINDIG igazítsd!

/**
 * 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);
}

A következő JPA-annotációk is a ZonedDateTime.now()-ra támaszkodnak, ezért soha ne használd őket csupaszon:

5. Ne deklarálj összetett adattípusokat inline

Összetett adattípusokhoz mindig hozz létre saját osztályokat, ahelyett hogy Pair-eket, listákat stb. ágyaznál egymásba.

Rossz:

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

Jobb:

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

Előnyök:

Kivételek:

Egyszerű kapcsolatokra a Pair remek! Például:

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

6. Cache-elés – de helyesen (minimális beállítási útmutató)

A cache (kiejtése /kæʃ/, mint a „cash”) lényegében csak egy map, amely bejegyzéseket tárol. Az összetettebb dolgokat, például az időalapú invalidálást és hasonlókat, kihagytuk.

Nem kell mindig könyvtár, ha egy egyszerű map is elég! Sok fejlesztő ezt nem érti, és félelemből elvből könyvtárakhoz nyúl. A cache-elés legfontosabb szempontja, hogy a megfelelő pillanatban történjen az invalidálás.

1. csapda: memory leakek

Memory leak cache-elésnél nagyon könnyen előfordul. Elég elfelejteni rendesen felülírni az equals()-t és a hashCode()-ot, és máris van egy memóriát szivárogtató HashMaped. Egy mindig biztonságos módszer a map kulcsainak kézi létrehozása. Az lenne a jó, ha kizárólag az entitás ID-ját vennénk bele, különben a map már akkor szivárog, ha csak egyetlen mezőt módosítasz. Ami helyes is, hiszen attól, hogy egy felhasználó keresztneve megváltozik, még ugyanaz a felhasználó. Lásd: 3. Az equals() és a hashCode() helyes használata

2. csapda: thread safety

A cache ideális esetben egy ConcurrentHashMap-et használ, amely atomi, thread-safe műveleteket kínál. A mapet csak egyetlen hívás módosítja – a computeIfAbsent() –, így teljesül a thread safety:

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());
}

Ha a kód hosszabb lenne, és több hívás is lenne benne, az egész metódust synchronizeddá kellene tenni.

7. Szerializáció – szükség van még rá?

A kérdésem: Van egy Sonar-szabály, a neve Fields in a “Serializable” class should either be transient or serializable. Akkor jelenik meg, amikor egy nem szerializálható mezőt adsz az osztályodhoz. A szabály így szól:

Például terhelés alatt a legtöbb J2EE-alkalmazás-keretrendszer lemezre írja az objektumokat, és egy állítólag Serializable objektum, amelynek nem transient, nem szerializálható adattagjai vannak, a program összeomlását okozhatja, és utat nyithat a támadóknak.

Ez nekem elavultnak tűnik. Az Effective Java 3rd Edition azt írja, hogy új szoftver fejlesztésekor a szerializáció nem releváns. Ráadásul a szerializáció eltávolítása a Javából „hosszú távú cél” az Oracle-nél. Maga a Java főarchitektusa, Mark Reinhold is kijelentette:

„A szerializáció szörnyű hiba volt 1997-ben.”

Azt a kérdést, hogy a Spring keretrendszer lemezre írhat-e objektumokat, itt már feltették:

Így hát felteszem a kérdést:

Ír-e lemezre objektumokat nagy terhelés alatt bármely modern alkalmazásszerver, keretrendszer vagy maga a J2EE-implementáció (különösen a CDI-vel, EJB-vel, JPA-val együttműködő Java-komponensek)?

Vagy ez az állítás elavult?

Válasz:

TL;DR Ha nem állapotot tartó, elosztható webalkalmazást implementálsz, nem kapcsolod be a sessionök perzisztálását, és csak memóriabeli cache-eket használsz, akkor talán sehol sem lesz szükséged a Serializable-re.

Magyarázat:

A sessionök replikálásához a Java-szerializáció a legújabb Servlet-specifikációban is kötelező: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

Gyakori eset, hogy több szerver áll egy load balancer mögött; bejelentkezel az egyikre, és kapsz egy JSESSIONID sütit. Ha a load balancer a következő kérésnél egy másik szerverre küld, a bejelentkezési és egyéb session-szintű attribútumaidat szerializálja és replikálja az új szerverre, így a JSESSIONID-d ott is érvényes marad.

Egy másik eset, amikor a session-szintű attribútumaidhoz szerializációra van szükség: ha az alkalmazásszerver úgy dönt, hogy a sessionöket lemezre írja. Ez például a Tomcat 8-ban beállítható úgy, hogy akkor lépjen életbe, amikor túl sok session van ahhoz, hogy a memóriában maradjon, vagy amikor a szerver újraindul, és meg akarod tartani a sessionöket. Lásd: https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Minden Servlet 5.0-kompatibilis alkalmazásszervernek alapértelmezés szerint támogatnia kell a Serializable objektumokat a HTTP-sessionökben az ilyen esetekhez.

Egyes szervereknél ez finomhangolható vagy felülírható, például Jackson-alapú JSON-szerializációval vagy valami hasonlóval. Lásd például: https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Egy másik gyakori eset a cache-elés. Ennek célja, hogy az objektumok a lehető leggyorsabban elérhetők legyenek, ideális esetben a memóriában, de esetleg ideiglenesen lemezre vagy egy külső cache-szerverre kiszervezve.
Létezik erre egy szabványos API (JSR-107), amely kifejezetten úgy döntött, hogy nem követeli meg a Serializable objektumokat, de a legtöbb implementáció a kiszervezéshez alapértelmezésként mégis Java-szerializációt használ.

Ezek is finomhangolhatók más szerializációs mechanizmusok támogatására, például az ehcache esetében: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: fetching-stratégia

Tekintettel az EAGER fetching is a code smell című cikkre, amelyet egy rendkívül karizmatikus okostojás, a Hibernate egyik contributora írt, a LAZY az előnyben részesítendő FetchType. Még ha egyes adattípusoknak idegen kulcs nélkül nincs is értelmük, ezeket a típusokat igenis lekérdezik önmagukban is (getCount(), adattáblák stb.).

Íme az összes forgatókönyv aszerint rendezve, mennyire kielégítő a megoldás, a legkevésbé kielégítővel kezdve:

1. A legrosszabb eset: LazyInitializationException

Nem kell hozzá szó. Mindannyian ismerjük ezt a fájdalmat.

2. Gyors javítás

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 közvetlenül a service-ben

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 közvetlenül a service-ben (így csináld!)

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

A Hibernate Search zökkenőmentesen összekapcsolódik a Hibernate JPA-val. Észreveszi, ha az entitások megváltoznak, és egyszerűen konfigurálható:

@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álisan a keresés ki van kapcsolva:

// 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();
}

Fuzzy keresés & keresési viselkedés

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

A Lucene debugolása a Luke-kal

A mi Hibernate Search-verziónkat a Luke 6.6.0-s verziójával kell debugolni. Csak ezzel a verzióval működik!

ElasticSearch

A Hibernate Search az ElasticSearch-öt is támogatja (ilyenkor a Lucene helyett ez fut, és jobb a fuzzy keresése / jobban hasonlít a Google-re).