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:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
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:
- Nomi migliori di
getFirst()egetSecond() - L’uso è definito chiaramente da getter e setter (la classe è immutabile? Quale lista si può estendere? Come va istanziato l’oggetto?) → l’incapsulamento documenta da solo come usare il codice
- Leggibilità nettamente migliore
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!
- Download: https://github.com/DmitryKey/luke/releases
- Con Luke puoi aprire gli indici dalla cartella
target/. - È un buon punto di partenza per vedere se le entity vengono indicizzate correttamente.
- Puoi anche avviare una ricerca direttamente da Luke.
ElasticSearch
Hibernate Search supporta anche ElasticSearch (che in quel caso sostituisce Lucene e offre una ricerca fuzzy migliore, con una sensazione più «alla Google»).