Bunele mele practici – Java
1. Pattern.compile() în loc de String.replaceAll() în buclă
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Pattern-ul trebuie de fiecare dată compilat mai întâi. Dacă același Pattern rulează des pe stringuri scurte, performanța este mult mai bună când refolosești Pattern-ul
deja compilat.
2. StringBuilder în loc de + în buclă
Mai performant este new StringBuilder() în loc de + în buclă.
3. Folosirea corectă a equals() și hashCode()
Uneori, în equals() sau hashCode() sunt incluse toate câmpurile. Este util pentru a verifica dacă o entitate a fost modificată, dar este greșit. Scopul lui
equals() nu este să verifice dacă o entitate a fost modificată, ci să stabilească dacă este vorba de aceeași entitate, chiar dacă instanța obiectului nu este
aceeași. Pe scurt: folosește doar cheia primară!
Acest lucru este necesar pentru orice fel de colecții. Când apelezi .contains() pe o listă, Java va folosi metoda equals. HashMap-urile au nevoie în plus de hashCode().
Cele două ar trebui suprascrise împreună.
O capcană frecventă sunt entitățile nesalvate, fără ID. Aici ai grijă și persistă-le din timp, ca să eviți problemele.
Pentru comparație:
Contractul acestor metode trebuie respectat întotdeauna:
4. Capcane cu DateTime când se amestecă sistemele de operare
Unele sisteme de operare au un ceas intern cu nanosecunde, altele cu milisecunde. Astfel, ZonedDateTime.now() nu funcționează la fel pe ambele platforme și strică
equals(). În schimb, ajustează ÎNTOTDEAUNA precizia!
/**
* 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 următoarele adnotări JPA se bazează pe ZonedDateTime.now() și n-ar trebui folosite niciodată așa, fără ajustări:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Nu declara tipuri de date complexe inline
Creează întotdeauna clase proprii pentru tipurile de date complexe, în loc să imbrici Pair, liste etc.
Așa nu:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Mai bine:
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 ...
}
Avantaje:
- Nume mai bune decât
getFirst()șigetSecond() - Modul de utilizare este clar definit prin getteri și setteri (este clasa imutabilă? Ce listă are voie să fie extinsă? Cum trebuie instanțiat obiectul?) → Încapsularea documentează de la sine modul de utilizare a codului
- Lizibilitate mult mai bună
Excepții:
Pentru relații simple, Pair e excelent! De exemplu:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Caching – dar corect (ghid de configurare minimă)
Cache-ul (pronunțat /kæʃ/, ca „cash”) este, în principiu, doar un map care păstrează intrări. Am renunțat la lucruri mai complexe, precum invalidarea în funcție de timp sau altele asemănătoare.
Nu e nevoie mereu de o bibliotecă, dacă un simplu map e suficient! Mulți dezvoltatori nu înțeleg asta și, de teamă, iau din principiu biblioteci. Cel mai important aspect la caching este ca invalidarea să aibă loc la momentul potrivit.
Capcana nr. 1: memory leaks
Memory leaks apar foarte ușor la caching. E suficient să uiți să suprascrii corect equals() și hashCode() și te alegi cu un HashMap care
pierde memorie. O metodă care e întotdeauna sigură este crearea manuală a cheilor din map. Ideal ar fi să incluzi exclusiv ID-ul entității; altfel, map-ul pierde memorie deja când schimbi un singur
câmp. Ceea ce e și corect: doar pentru că s-a schimbat prenumele unui utilizator, este tot același utilizator. Pentru comparație: 3. Folosirea corectă a equals() și
hashCode()
Capcana nr. 2: thread safety
Ideal, cache-ul folosește un ConcurrentHashMap, care oferă operații atomice, thread-safe. Map-ul este modificat doar printr-un singur apel – computeIfAbsent() –, astfel se obține 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());
}
Dacă codul ar fi mai lung și ar exista mai multe apeluri, întreaga metodă ar trebui să fie synchronized.
7. Serializarea – mai este necesară?
Întrebarea mea: Există această regulă Sonar numită Fields in a “Serializable” class should either be transient or serializable. Apare atunci când adaugi în clasă un câmp care nu este serializabil. Regula spune:
De exemplu, sub sarcină, majoritatea frameworkurilor de aplicații J2EE scriu obiectele pe disc, iar un obiect presupus Serializable, cu membri de date non-transient și non-serializabili, ar putea duce la prăbușirea programului și ar putea deschide calea atacatorilor.
Mie mi se pare depășit. În Effective Java 3rd Edition se spune că serializarea nu este relevantă când dezvolți software nou. De asemenea, eliminarea serializării din Java este un „obiectiv pe termen lung” la Oracle. Însuși arhitectul-șef Java, Mark Reinhold, a declarat:
„Serializarea a fost o greșeală îngrozitoare în 1997.”
Întrebarea dacă frameworkul Spring poate scrie obiecte pe disc a fost deja pusă aici:
Așa că întreb:
Există servere de aplicații moderne, frameworkuri sau chiar implementarea J2EE (în special componente Java care interacționează cu CDI, EJB, JPA) care scriu obiecte pe disc sub sarcină mare?
Sau este această afirmație depășită?
Răspuns:
TL;DR Dacă nu implementezi o aplicație web distribuibilă cu stare, nu activezi persistența sesiunilor și folosești doar cache-uri în memorie, s-ar putea să te descurci fără să ai nevoie de Serializable nicăieri.
Explicație:
Pentru replicarea sesiunilor, serializarea Java este încă obligatorie în cea mai recentă specificație Servlet: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Un caz de utilizare frecvent: ai mai multe servere în spatele unui load balancer, te autentifici pe un server și primești un cookie JSESSIONID. Dacă load balancerul te trimite la cererea următoare
pe alt server, atributele de autentificare și celelalte atribute din sesiune vor fi serializate și replicate pe noul server, astfel încât JSESSIONID-ul tău să rămână valid și acolo.
Un alt caz în care ai nevoie de serializare pentru atributele din sesiune este atunci când serverul de aplicații decide să mute sesiunile pe disc. De exemplu, în Tomcat 8 acest lucru poate fi configurat să se activeze când sunt prea multe sesiuni pentru a fi ținute în memorie sau când serverul este repornit și vrei să păstrezi sesiunile active. Vezi https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Orice server de aplicații compatibil cu Servlet 5.0 trebuie să suporte implicit obiecte Serializable în sesiunile HTTP pentru astfel de cazuri.
La unele servere, acest comportament poate fi ajustat sau înlocuit, de exemplu prin serializare JSON cu Jackson sau ceva asemănător. Vezi de exemplu https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Un alt caz frecvent este caching-ul. Acesta servește la păstrarea obiectelor accesibile cât mai rapid, ideal în memorie, dar eventual mutate temporar pe disc sau pe un server de cache extern.
Există un API standard pentru asta (JSR-107), care a ales explicit să nu impună obiecte Serializable, dar majoritatea implementărilor folosesc totuși implicit serializarea Java pentru partea de mutare pe disc.
Și acestea pot fi ajustate pentru a suporta alte mecanisme de serializare, de exemplu pentru ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: strategia de fetching
Ținând cont de EAGER fetching is a code smell, scris de un atotștiutor extrem de carismatic, care este contributor la
Hibernate, LAZY este de preferat ca FetchType. Chiar dacă unele tipuri de date nu au sens fără o cheie străină, aceste tipuri sunt totuși interogate
și separat (getCount(), tabele de date etc.).
Iată toate scenariile, ordonate după cât de satisfăcătoare este soluția, începând cu cea mai puțin satisfăcătoare:
1. Cel mai rău caz: LazyInitializationException
Nu e nevoie de cuvinte. Toți împărtășim durerea asta.
2. Soluția rapidă
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 direct în 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 direct în service (așa da!)
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 este integrat perfect cu Hibernate JPA. Observă când se schimbă entitățile și poate fi configurat simplu:
@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;
// ...
}
Local, căutarea este dezactivată:
// 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();
}
Căutare fuzzy & comportamentul căutării
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
Depanarea Lucene cu Luke
Versiunea noastră de Hibernate Search trebuie depanată cu Luke, versiunea 6.6.0. Funcționează doar cu această versiune!
- Descărcare: https://github.com/DmitryKey/luke/releases
- Cu el se pot deschide indexurile din folderul
target/. - Un punct bun pentru a vedea dacă entitățile sunt indexate corect.
- O căutare poate fi pornită și direct din Luke.
ElasticSearch
Hibernate Search suportă și ElasticSearch (care este folosit atunci în locul lui Lucene și are o căutare fuzzy mai bună / se simte mai mult ca Google).