Moje dobre praktyki – Java
1. Pattern.compile() zamiast String.replaceAll() w pętli
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Pattern zawsze trzeba najpierw skompilować. Jeśli ten sam Pattern często działa na krótkich stringach, wydajność jest wyraźnie lepsza, gdy skompilowany Pattern
używasz ponownie.
2. StringBuilder zamiast + w pętli
W pętli new StringBuilder() jest wydajniejszy niż +.
3. Poprawne używanie equals() i hashCode()
Czasem w equals() albo hashCode() uwzględnia się wszystkie pola. Przydaje się to do sprawdzenia, czy encja została zmieniona, ale jest błędne. Celem
equals() nie jest sprawdzanie, czy encja została zmodyfikowana, lecz ustalenie, czy – nawet jeśli to nie ta sama instancja obiektu – chodzi o tę samą encję.
Krótko mówiąc: używaj wyłącznie klucza głównego!
Jest to potrzebne we wszelkiego rodzaju kolekcjach. Gdy wywołujesz .contains() na liście, Java użyje metody equals. HashMapy potrzebują dodatkowo hashCode().
Obie metody należy nadpisywać razem.
Częstą pułapką są niezapisane encje bez ID. Tu trzeba uważać i utrwalać je wcześnie, żeby uniknąć problemów.
Dla porównania:
Kontrakt tych metod musi być zawsze dotrzymany:
4. Pułapki z DateTime przy mieszaniu systemów operacyjnych
Niektóre systemy operacyjne mają zegar wewnętrzny z dokładnością do nanosekund, inne do milisekund. Przez to ZonedDateTime.now() nie działa tak samo na obu platformach i psuje
equals(). Dlatego ZAWSZE dostosowuj precyzję!
/**
* 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);
}
Również poniższe mechanizmy JPA opierają się na ZonedDateTime.now() i nie powinny być nigdy używane bez modyfikacji:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Nie deklaruj złożonych typów danych inline
Dla złożonych typów danych zawsze twórz własne klasy, zamiast zagnieżdżać Pair, listy itp.
Źle:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Lepiej:
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 ...
}
Zalety:
- Lepsze nazwy niż
getFirst()igetSecond() - Gettery i settery jasno określają sposób użycia (czy klasa jest niemutowalna? Którą listę wolno rozszerzać? Jak należy tworzyć obiekt?) → hermetyzacja sama dokumentuje, jak używać kodu
- Znacznie lepsza czytelność
Wyjątki:
Do prostych relacji Pair jest świetny! Na przykład:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Cache – ale porządnie (minimalny przewodnik)
Cache (wymowa /kæʃ/, jak „cash”) to w zasadzie zwykła mapa przechowująca wpisy. Bardziej złożone rzeczy, takie jak unieważnianie po czasie itp., zostały tu pominięte.
Nie zawsze potrzebna jest biblioteka, skoro wystarczy prosta mapa! Wielu programistów tego nie rozumie i ze strachu z zasady sięga po biblioteki. Najważniejsze przy cache’owaniu jest to, żeby unieważniać wpisy we właściwym momencie.
Pułapka nr 1: wycieki pamięci
Wycieki pamięci przy cache’owaniu zdarzają się bardzo łatwo. Wystarczy zapomnieć o porządnym nadpisaniu equals() i hashCode(), a już masz HashMapę, która
cieknie. Metodą zawsze bezpieczną jest ręczne tworzenie kluczy mapy. Najlepiej uwzględniać wyłącznie ID encji – w przeciwnym razie mapa cieknie już przy zmianie jednego
pola. I słusznie, bo jeśli zmienia się imię użytkownika, to nadal jest to ten sam użytkownik. Por. 3. Poprawne używanie equals()
i hashCode()
Pułapka nr 2: bezpieczeństwo wątków
Cache najlepiej opiera się na ConcurrentHashMap, która oferuje atomowe, bezpieczne wątkowo operacje. Mapa jest modyfikowana tylko jednym wywołaniem – computeIfAbsent() – i dzięki temu bezpieczeństwo wątków jest zapewnione:
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());
}
Gdyby kod był dłuższy i zawierał kilka wywołań, cała metoda musiałaby być synchronized.
7. Serializacja – czy jest jeszcze potrzebna?
Moje pytanie: Istnieje reguła Sonara o nazwie „Pola w klasie Serializable powinny być transient albo serializowalne”. Pojawia się, gdy dodasz do klasy pole, którego nie da się serializować. Reguła mówi:
Na przykład pod obciążeniem większość frameworków aplikacyjnych J2EE zrzuca obiekty na dysk, a rzekomo serializowalny obiekt z polami, które nie są ani transient, ani serializowalne, może powodować awarie programu i otwierać drogę atakującym.
Brzmi to dla mnie jak coś nieaktualnego. W Effective Java 3rd Edition czytamy, że przy tworzeniu nowego oprogramowania serializacja nie ma znaczenia. Poza tym Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. Sam główny architekt Javy, Mark Reinhold, stwierdził:
„Serializacja była w 1997 roku okropnym błędem”.
Pytanie, czy framework Spring może zrzucać obiekty na dysk, padło tu już wcześniej:
Dlatego pytam:
Czy jakiekolwiek nowoczesne serwery aplikacji, frameworki albo sama implementacja J2EE (zwłaszcza komponenty Javy współpracujące z CDI, EJB, JPA) zrzucają obiekty na dysk pod dużym obciążeniem?
A może to stwierdzenie jest już nieaktualne?
Odpowiedź:
W skrócie: jeśli nie implementujesz stanowej, rozproszonej aplikacji webowej, nie włączasz trwałości sesji i używasz wyłącznie cache’y w pamięci, możesz się obejść bez Serializable gdziekolwiek.
Wyjaśnienie:
Do replikacji sesji serializacja Javy jest nadal obowiązkowa także w najnowszej specyfikacji Servlet: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Typowy przypadek: masz kilka serwerów za load balancerem i logujesz się na jednym z nich – dostajesz ciasteczko JSESSIONID. Jeśli load balancer skieruje cię przy następnym żądaniu na
inny serwer, twoje atrybuty logowania i inne atrybuty o zasięgu sesji zostaną zserializowane i zreplikowane na nowym serwerze, więc twój JSESSIONID nadal będzie tam ważny.
Inny przypadek, w którym atrybuty o zasięgu sesji muszą być serializowalne, to sytuacja, gdy serwer aplikacji postanawia przenieść sesje na dysk. W np. Tomcacie 8 można to skonfigurować tak, by działo się, gdy sesji jest za dużo, by trzymać je w pamięci, albo przy restarcie serwera, jeśli chcesz zachować aktywne sesje. Zob. https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Każdy serwer aplikacji zgodny z Servlet 5.0 musi domyślnie obsługiwać obiekty Serializable w sesjach HTTP właśnie na potrzeby takich przypadków.
W niektórych serwerach można to dostroić albo zastąpić, np. serializacją JSON przez Jacksona lub czymś podobnym. Zob. np. https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Kolejny częsty przypadek to cache’owanie. Służy ono do tego, by obiekty były dostępne jak najszybciej, najlepiej w pamięci, ale czasem też tymczasowo przeniesione na dysk albo na zewnętrzny serwer cache.
Istnieje do tego standardowe API (JSR-107), które celowo nie wymaga obiektów Serializable, ale większość implementacji i tak domyślnie używa serializacji Javy przy przenoszeniu danych.
Również tutaj można je dostosować do innych mechanizmów serializacji, np. w ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: strategia pobierania
W świetle artykułu EAGER fetching is a code smell, napisanego przez niezwykle charyzmatycznego mądralę, który jest kontrybutorem
Hibernate, należy preferować LAZY jako FetchType. Nawet jeśli niektóre typy danych bez klucza obcego nie mają sensu, to i tak bywają odpytywane
samodzielnie (getCount(), tabele danych itp.).
Oto wszystkie scenariusze uporządkowane według tego, jak zadowalające jest rozwiązanie – od najmniej zadowalającego:
1. Najgorszy przypadek: LazyInitializationException
Bez komentarza. Wszyscy znamy ten ból.
2. Szybka poprawka
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 bezpośrednio w serwisie
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 bezpośrednio w serwisie (tak rób!)
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 jest płynnie zintegrowany z Hibernate JPA. Wychwytuje zmiany encji i daje się łatwo skonfigurować:
@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;
// ...
}
Lokalnie wyszukiwanie jest wyłączone:
// 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();
}
Wyszukiwanie rozmyte i zachowanie wyszukiwania
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
Debugowanie Lucene za pomocą Luke
Naszą wersję Hibernate Search trzeba debugować za pomocą Luke w wersji 6.6.0. Działa tylko z tą wersją!
- Pobieranie: https://github.com/DmitryKey/luke/releases
- Można w nim otwierać indeksy z katalogu
target/. - Dobry sposób, żeby sprawdzić, czy encje w ogóle są poprawnie indeksowane.
- Wyszukiwanie można też uruchomić bezpośrednio z Luke.
ElasticSearch
Hibernate Search obsługuje też ElasticSearch (używany wtedy zamiast Lucene, oferuje lepsze wyszukiwanie rozmyte i bardziej „googlowe” wrażenia).