Le mie best practice – Java

1. Pattern.compile() invece di String.replaceAll() nei cicli

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

Il Pattern deve sempre essere compilato prima. Se lo stesso Pattern viene eseguito spesso su stringhe brevi, le prestazioni migliorano nettamente riutilizzando il Pattern già compilato.

2. StringBuilder invece di + nei cicli

Nei cicli new StringBuilder() è più performante di +.

3. Usare correttamente equals() e hashCode()

A volte in equals() o hashCode() vengono inclusi tutti i campi. È comodo per verificare se un’entity è stata modificata, ma è sbagliato. Lo scopo di equals() non è verificare se un’entity è stata modificata, bensì stabilire se si tratta comunque della stessa entity, anche quando l’istanza dell’oggetto non è la stessa. In breve: usa solo la chiave primaria!

Serve per qualsiasi tipo di collection. Se chiami .contains() su una List, Java userà il metodo equals. Le HashMap hanno bisogno anche di hashCode(). I due metodi vanno sempre sovrascritti insieme.

Una trappola frequente sono le entity non ancora salvate e quindi senza ID. Qui conviene fare attenzione e persisterle presto, per evitare problemi.

Per approfondire:

Il contratto di questi metodi va sempre rispettato:

4. Trappole con DateTime quando si mescolano sistemi operativi

Alcuni sistemi operativi hanno un orologio interno con precisione al nanosecondo, altri al millisecondo. Per questo ZonedDateTime.now() non si comporta allo stesso modo su entrambe le piattaforme e rompe equals(). Quindi adatta SEMPRE la precisione!

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

Anche le seguenti annotazioni JPA si basano su ZonedDateTime.now() e non andrebbero mai usate così come sono:

5. Non dichiarare tipi di dati complessi inline

Per i tipi di dati complessi crea sempre classi dedicate, invece di annidare Pair, List e simili.

Sbagliato:

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

Meglio:

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

Vantaggi:

Eccezioni:

Per relazioni semplici Pair va benissimo! Per esempio:

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

6. Caching, ma fatto bene (guida al setup minimo)

La cache (in inglese si pronuncia /kæʃ/, come «cash») in fondo è solo una Map che contiene delle voci. Cose più complesse, come l’invalidazione basata sul tempo, sono state tralasciate di proposito.

Non serve sempre una libreria, se basta una semplice Map! Molti sviluppatori non lo capiscono e, per paura, ricorrono per principio alle librerie. L’aspetto più importante del caching è invalidare al momento giusto.

Trappola n. 1: memory leak

Con il caching i memory leak capitano molto facilmente. Basta dimenticarsi di sovrascrivere correttamente equals() e hashCode() e ti ritrovi con una HashMap che perde memoria. Un metodo sempre sicuro è costruire a mano le chiavi della Map. L’ideale è includere esclusivamente l’ID dell’entity, altrimenti la Map perde memoria già quando cambi un solo campo. Ed è giusto così: se cambia il nome di un utente, si tratta pur sempre dello stesso utente. Vedi anche: 3. Usare correttamente equals() e hashCode()

Trappola n. 2: thread safety

Idealmente la cache usa una ConcurrentHashMap, che offre operazioni atomiche e thread-safe. La Map viene modificata solo tramite un’unica chiamata, computeIfAbsent(), e così la thread safety è garantita:

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

Se il codice fosse più lungo e ci fossero più chiamate, l’intero metodo dovrebbe essere synchronized.

7. Serializzazione: serve ancora?

La mia domanda: Esiste una regola di Sonar chiamata «I campi di una classe “Serializable” dovrebbero essere transient o serializzabili». Scatta quando aggiungi alla tua classe un campo non serializzabile. La regola afferma:

Per esempio, sotto carico la maggior parte dei framework applicativi J2EE scarica gli oggetti su disco, e un oggetto presumibilmente Serializable con membri dati non transient e non serializzabili potrebbe mandare in crash il programma e aprire la porta agli attaccanti.

A me sembra un’affermazione superata. In Effective Java 3rd Edition si dice che la serializzazione non è rilevante quando si sviluppa software nuovo. Inoltre: Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. Lo stesso chief architect di Java, Mark Reinhold, ha dichiarato:

«La serializzazione è stata un errore orribile nel 1997.»

La domanda se il framework Spring possa scaricare oggetti su disco era già stata posta qui:

Quindi la mia domanda è:

Esistono application server o framework moderni, o la stessa implementazione J2EE (in particolare i componenti Java che interagiscono con CDI, EJB, JPA), che scaricano oggetti su disco sotto carico elevato?

Oppure questa affermazione è superata?

Risposta:

In breve: se non stai implementando un’applicazione web stateful e distribuibile, non abiliti la persistenza delle sessioni e usi solo cache in memoria, potresti cavartela senza dover usare Serializable da nessuna parte.

Spiegazione:

Per la replica delle sessioni la serializzazione Java è ancora obbligatoria anche nella specifica Servlet più recente: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

Un caso d’uso comune: hai più server dietro un load balancer e, quando fai il login su un server, ricevi un cookie JSESSIONID. Se alla richiesta successiva il load balancer ti manda su un altro server, i tuoi attributi di login e gli altri attributi con scope di sessione vengono serializzati e replicati sul nuovo server, così il tuo JSESSIONID resta valido anche lì.

Un altro caso d’uso in cui serve la serializzazione per gli attributi con scope di sessione è quando l’application server decide di spostare le sessioni su disco. In Tomcat 8, per esempio, lo si può configurare perché scatti quando ci sono troppe sessioni da tenere in memoria, oppure quando il server viene riavviato e vuoi mantenere attive le sessioni. Vedi https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Ogni application server compatibile con Servlet 5.0 deve supportare per impostazione predefinita gli oggetti Serializable nelle sessioni HTTP, proprio per questi casi d’uso.

Su alcuni server questo comportamento si può modificare o sostituire, per esempio con la serializzazione JSON di Jackson o qualcosa di simile. Vedi per esempio https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Un altro caso d’uso comune è il caching. Serve a mantenere gli oggetti accessibili il più rapidamente possibile, idealmente in memoria, ma eventualmente scaricati temporaneamente su disco o su un server di cache esterno.
Esiste un’API standard (JSR-107) che ha scelto esplicitamente di non richiedere oggetti Serializable, ma la maggior parte delle implementazioni usa comunque di default la serializzazione Java per la parte di offloading.

Anche qui puoi configurarle per supportare altri meccanismi di serializzazione, per esempio con ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: strategia di fetching

Considerando EAGER fetching is a code smell, scritto da un saputello oltremodo carismatico che è contributor di Hibernate, come FetchType è preferibile LAZY. Anche se alcuni tipi di dati non hanno senso senza una chiave esterna, questi tipi vengono comunque interrogati da soli (getCount(), data table ecc.).

Ecco tutti gli scenari, ordinati in base a quanto la soluzione è soddisfacente, a partire dalla meno soddisfacente:

1. Caso peggiore: LazyInitializationException

Non servono parole. È un dolore che conosciamo tutti.

2. Soluzione rapida

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 direttamente nel service

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 direttamente nel service (fai così!)

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 è integrato perfettamente con Hibernate JPA. Si accorge quando le entity cambiano e si configura facilmente:

@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;

    // ...

}

In locale la ricerca è disattivata:

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

Ricerca fuzzy & comportamento della ricerca

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

Debug di Lucene con Luke

La nostra versione di Hibernate Search va sottoposta a debug con Luke versione 6.6.0. Funziona solo con questa versione!

ElasticSearch

Hibernate Search supporta anche ElasticSearch (che in quel caso sostituisce Lucene e offre una ricerca fuzzy migliore, con una sensazione più «alla Google»).