Moje osvědčené postupy – Java
1. Pattern.compile() místo String.replaceAll() ve smyčce
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Pattern se musí vždy nejprve zkompilovat. Pokud se stejný Pattern často spouští na krátkých řetězcích, je výkon výrazně lepší, když se zkompilovaný Pattern
použije opakovaně.
2. StringBuilder místo + ve smyčce
Ve smyčce je new StringBuilder() výkonnější než +.
3. Správné použití equals() a hashCode()
Někdy se do equals() nebo hashCode() zahrnou všechna pole. To se hodí ke kontrole, zda se entita změnila, ale je to špatně. Účelem
equals() není kontrolovat, zda byla entita upravena, ale zjistit, zda jde o tutéž entitu, i když instance objektu není
stejná. Jinými slovy: používejte pouze primární klíč!
Potřebné je to pro jakékoli kolekce. Když se na List zavolá .contains(), použije Java metodu equals. HashMapy navíc potřebují hashCode().
Obě metody by se proto měly přepisovat společně.
Častou pastí jsou neuložené entity bez ID. Tady je třeba být opatrný a ukládat je co nejdříve, aby se předešlo problémům.
Pro srovnání:
Kontrakt těchto metod se musí vždy dodržet:
4. Pasti s DateTime při kombinaci operačních systémů
Některé operační systémy mají interní hodiny s přesností na nanosekundy, jiné na milisekundy. ZonedDateTime.now() proto nefunguje na obou platformách stejně a rozbíjí
equals(). Místo toho přesnost VŽDY upravte!
/**
* 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 následující anotace JPA spoléhají na ZonedDateTime.now() a nikdy by se neměly používat jen tak bez úprav:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Složité datové typy nedeklarujte inline
Pro složité datové typy vždy vytvářejte vlastní třídy místo vnořování Pair, List apod.
Špatně:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Lépe:
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 ...
}
Výhody:
- Lepší názvy než
getFirst()agetSecond() - Gettery a settery jasně určují, jak třídu používat (je třída immutable? Který seznam se smí rozšiřovat? Jak se má objekt instancovat?) → zapouzdření samo dokumentuje, jak kód používat
- Výrazně lepší čitelnost
Výjimky:
Pro jednoduché vztahy je Pair skvělý! Například:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Cachování – ale pořádně (minimální návod k nastavení)
Cache (vyslovuje se /kæʃ/, jako anglické „cash“) je v podstatě jen Map, která uchovává záznamy. Složitější věci, jako je časová invalidace a podobně, jsme vynechali.
Nemusí to být vždy knihovna, když stačí obyčejná Map! Spousta vývojářů to nechápe a ze strachu automaticky sahá po knihovnách. Nejdůležitější na cachování je invalidovat ve správný okamžik.
Past č. 1: úniky paměti (memory leaks)
Úniky paměti při cachování vzniknou velmi snadno. Stačí zapomenout pořádně přepsat equals() a hashCode() a máte HashMapu, ze které
uniká paměť. Vždy bezpečnou metodou je vytvářet klíče mapy ručně. Ideální je zahrnout výhradně ID entity, jinak mapa začne prosakovat, jakmile se změní byť jen
jediné pole. Což je i správně: jen proto, že se uživateli změní křestní jméno, je to pořád tentýž uživatel. Pro srovnání: 3. Správné použití equals() a
hashCode()
Past č. 2: vláknová bezpečnost (thread safety)
Cache v ideálním případě používá ConcurrentHashMap, která nabízí atomické, vláknově bezpečné operace. Mapa se mění jen jediným voláním – computeIfAbsent() – a tím je zajištěna vláknová bezpečnost:
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());
}
Kdyby byl kód delší a obsahoval více volání, musela by být celá metoda synchronized.
7. Serializace – je ještě potřeba?
Moje otázka: Existuje pravidlo Sonaru s názvem Fields in a “Serializable” class should either be transient or serializable. Objeví se, když do třídy přidáte neserializovatelné pole. Pravidlo uvádí:
Například pod zátěží většina aplikačních frameworků J2EE odkládá objekty na disk a údajně serializovatelný objekt s netransientními, neserializovatelnými datovými členy může způsobit pády programu a otevřít dveře útočníkům.
To mi připadá zastaralé. V knize Effective Java 3rd Edition se uvádí, že serializace při vývoji nového softwaru není relevantní. Navíc odstranění serializace z Javy je pro Oracle „dlouhodobým cílem“. Sám hlavní architekt Javy Mark Reinhold prohlásil:
„Serializace byla v roce 1997 strašlivá chyba.“
Otázka, zda framework Spring může odkládat objekty na disk, už tu padla:
Ptám se tedy:
Odkládají moderní aplikační servery, frameworky nebo samotná implementace J2EE (zejména komponenty Javy, které pracují s CDI, EJB, JPA) objekty pod velkou zátěží na disk?
Nebo je toto tvrzení zastaralé?
Odpověď:
TL;DR Pokud neimplementujete stavovou distribuovanou webovou aplikaci, nezapínáte perzistenci relací a používáte jen cache v paměti, možná se obejdete bez Serializable úplně.
Vysvětlení:
Pro replikaci relací je serializace Javy podle nejnovější specifikace Servletů stále nutností: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Běžný případ: máte několik serverů za load balancerem, přihlásíte se na jednom serveru a dostanete cookie JSESSIONID. Pokud vás load balancer při dalším požadavku pošle na jiný
server, vaše přihlašovací atributy a další atributy s rozsahem relace se serializují a zreplikují na nový server, takže tam váš JSESSIONID zůstane platný.
Další případ, kdy atributy s rozsahem relace potřebují serializaci, nastává, když se aplikační server rozhodne odložit relace na disk. Například v Tomcatu 8 lze nastavit, aby se to spustilo, když je relací příliš mnoho na to, aby zůstaly v paměti, nebo při restartu serveru, pokud chcete relace zachovat. Viz https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Každý aplikační server kompatibilní se Servletem 5.0 musí pro tyto případy ve výchozím stavu podporovat serializovatelné objekty v HTTP relacích.
U některých serverů to lze upravit nebo přepsat, například pomocí serializace Jackson-JSON nebo něčeho podobného. Viz například https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Dalším běžným případem je cachování. Slouží k tomu, aby byly objekty dostupné co nejrychleji, ideálně v paměti, ale případně dočasně odložené na disk nebo na externí cache server.
Existuje pro to standardní API (JSR-107), které serializovatelné objekty záměrně nevyžaduje, ale většina implementací pro odkládání přesto ve výchozím stavu používá serializaci Javy.
I ty lze upravit tak, aby podporovaly jiné mechanismy serializace, například u ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: strategie načítání (fetching)
Vzhledem k článku EAGER fetching is a code smell, který napsal nesmírně charismatický chytrolín a přispěvatel do Hibernate,
je jako FetchType vhodnější LAZY. I když některé datové typy bez cizího klíče nedávají smysl, dotazují se tyto typy
i samostatně (getCount(), datové tabulky apod.).
Zde jsou všechny scénáře seřazené podle toho, jak uspokojivé řešení nabízejí, od nejméně uspokojivého:
1. Nejhorší případ: LazyInitializationException
Slova jsou zbytečná. Tuhle bolest zná každý z nás.
2. Rychlá oprava
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 přímo v servisní vrstvě
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 přímo v servisní vrstvě (takhle to dělejte!)
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 je hladce propojený s Hibernate JPA. Pozná, když se entity změní, a snadno se konfiguruje:
@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álně je vyhledávání vypnuté:
// 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();
}
Neostré vyhledávání a chování vyhledávání
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
Ladění Lucene pomocí Luke
Naši verzi Hibernate Search je nutné ladit pomocí Luke ve verzi 6.6.0. Funguje to jen s touto verzí!
- Stažení: https://github.com/DmitryKey/luke/releases
- Lze v něm otevřít indexy ze složky
target/. - Dobrý způsob, jak zjistit, zda se entity vůbec správně indexují.
- Vyhledávání lze spustit i přímo z Luke.
ElasticSearch
Hibernate Search podporuje také ElasticSearch (ten se pak použije místo Lucene a nabízí lepší neostré vyhledávání – víc jako Google).