Mina best practices – Java
1. Pattern.compile() i stället för String.replaceAll() i loopar
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Ett Pattern måste alltid kompileras först. Körs samma Pattern ofta mot korta strängar blir prestandan betydligt bättre om man återanvänder det kompilerade Pattern.
2. StringBuilder i stället för + i loopar
new StringBuilder() ger bättre prestanda än + i en loop.
3. Använd equals() och hashCode() rätt
Ibland tas alla fält med i equals() eller hashCode(). Det är praktiskt för att se om en entitet har ändrats, men fel. Syftet med
equals() är inte att kontrollera om en entitet har ändrats, utan att avgöra om det, även när objektinstansen inte är densamma, ändå rör sig om samma entitet.
Alltså: Använd bara primärnyckeln!
Det här behövs för alla slags collections. Anropar man .contains() på en lista använder Java metoden equals. HashMaps behöver dessutom hashCode().
De två bör alltid överskuggas tillsammans.
En vanlig fallgrop är osparade entiteter utan ID. Var försiktig här och persistera tidigt för att undvika problem.
Jämför:
Kontraktet för dessa metoder måste alltid uppfyllas:
4. Fallgropar med DateTime när operativsystem blandas
Vissa operativsystem har en intern klocka med nanosekundsupplösning, andra med millisekunder. ZonedDateTime.now() beter sig därför inte likadant på alla plattformar och förstör
equals(). Anpassa i stället ALLTID precisionen!
/**
* 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);
}
Även följande JPA-annoteringar förlitar sig på ZonedDateTime.now() och bör aldrig användas rakt av:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Deklarera inte komplexa datatyper inline
Skapa alltid egna klasser för komplexa datatyper i stället för att nästla Pair, listor osv.
Dåligt:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Bättre:
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 ...
}
Fördelar:
- Bättre namn än
getFirst()ochgetSecond() - Användningen definieras tydligt av getters och setters (är klassen immutable? Vilken lista får utökas? Hur ska objektet instansieras?) → Inkapsling dokumenterar av sig själv hur koden ska användas
- Betydligt bättre läsbarhet
Undantag:
För enkla relationer är Pair utmärkt! Till exempel:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Cachning – men rätt (minimal installationsguide)
Cachen (uttalas /kæʃ/, som ”cash”) är i princip bara en map som håller poster. Mer avancerade saker som tidsbaserad invalidering o.d. har vi avstått från.
Det behöver inte alltid vara ett bibliotek när en enkel map räcker! Många utvecklare förstår inte det och tar av ren rädsla alltid till bibliotek. Det viktigaste med cachning är att invalidera vid rätt tidpunkt.
Fallgrop #1: minnesläckor
Minnesläckor uppstår väldigt lätt vid cachning. Glömmer man att överskugga equals() och hashCode() ordentligt har man genast en HashMap som
läcker minne. En metod som alltid är säker är att bygga map-nycklarna för hand. Helst bör bara entitetens ID ingå, annars läcker mappen redan när man ändrar ett enda
fält. Och det är också rätt: bara för att en användares förnamn ändras är det fortfarande samma användare. Jämför: 3. Använd equals() och hashCode()
rätt
Fallgrop #2: trådsäkerhet
Cachen använder helst en ConcurrentHashMap, som erbjuder atomära, trådsäkra operationer. Mappen ändras bara genom ett enda anrop – computeIfAbsent() – och därmed uppnås trådsäkerhet:
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());
}
Om koden vore längre och innehöll flera anrop skulle hela metoden behöva vara synchronized.
7. Serialisering – behövs den fortfarande?
Min fråga: Det finns en Sonar-regel som heter Fields in a “Serializable” class should either be transient or serializable. Den slår till när man lägger till ett icke serialiserbart fält i sin klass. Regeln säger:
Under hög belastning skriver till exempel de flesta J2EE-applikationsramverk objekt till disk, och ett förment Serializable-objekt med icke-transienta, icke serialiserbara datamedlemmar kan orsaka programkrascher och öppna dörren för angripare.
Det här låter föråldrat i mina öron. I Effective Java 3rd Edition står det att serialisering inte är relevant när man utvecklar ny mjukvara. Dessutom: Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. Javas chefsarkitekt själv, Mark Reinhold, har sagt:
”Serialisering var ett fruktansvärt misstag 1997.”
Frågan om Spring-ramverket kan skriva objekt till disk har redan ställts här:
Så jag ställer frågan:
Skriver några moderna applikationsservrar, ramverk eller själva J2EE-implementationen (särskilt Java-komponenter som samverkar med CDI, EJB, JPA) objekt till disk under hög belastning?
Eller är det påståendet föråldrat?
Svar:
TL;DR Om du inte implementerar en tillståndsbaserad, distribuerbar webbapplikation, inte aktiverar sessionspersistens och bara använder cachar i minnet, klarar du dig troligen utan Serializable överallt.
Förklaring:
För sessionsreplikering är Java-serialisering fortfarande ett måste enligt den senaste Servlet-specifikationen: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Ett vanligt användningsfall: Du har flera servrar bakom en lastbalanserare, loggar in på en av dem och får en JSESSIONID-cookie. Om lastbalanseraren skickar dig till en annan
server vid nästa förfrågan serialiseras dina inloggningsattribut och andra sessionsattribut och replikeras till den nya servern, så att din JSESSIONID fortfarande är giltig där.
Ett annat användningsfall där dina sessionsattribut behöver serialisering är när applikationsservern bestämmer sig för att växla ut sessioner till disk. I till exempel Tomcat 8 kan detta konfigureras att aktiveras när det finns för många sessioner för att hålla i minnet, eller när servern startas om och du vill behålla sessionerna. Se https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Varje Servlet 5.0-kompatibel applikationsserver måste som standard stödja Serializable-objekt i HTTP-sessioner för den här typen av användningsfall.
På vissa servrar kan detta justeras eller ersättas, till exempel med Jackson-JSON-serialisering eller något liknande. Se t.ex. https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Ett annat vanligt användningsfall är cachning. Den används för att hålla objekt så snabbt åtkomliga som möjligt, helst i minnet, men ibland tillfälligt utflyttade till disk eller till en extern cacheserver.
Det finns ett standard-API för detta (JSR-107) som uttryckligen valde att inte kräva Serializable-objekt, men de flesta implementationer använder ändå Java-serialisering som standard för utflyttningen.
Även här går det att justera så att andra serialiseringsmekanismer stöds, t.ex. för ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: fetch-strategi
Med tanke på EAGER fetching is a code smell, skriven av en ytterst karismatisk besserwisser som är Hibernate-contributor,
är LAZY att föredra som FetchType. Även om vissa datatyper inte är meningsfulla utan sin främmande nyckel, så hämtas de här typerna mycket väl även fristående
(getCount(), datatabeller osv.).
Här är alla scenarier sorterade efter hur tillfredsställande lösningen är, med början i den minst tillfredsställande:
1. Värsta fallet: LazyInitializationException
Inga ord behövs. Vi delar alla den här smärtan.
2. Snabbfix
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. Packa upp proxyn direkt i servicen
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. Använd fetch direkt i servicen (gör så här!)
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 är sömlöst integrerat med Hibernate JPA. Det märker när entiteter ändras och är enkelt att konfigurera:
@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;
// ...
}
Lokalt är sökningen avstängd:
// 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();
}
Oskarp sökning & sökbeteende
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
Felsöka Lucene med Luke
Vår version av Hibernate Search måste felsökas med Luke version 6.6.0. Det fungerar bara med den versionen!
- Nedladdning: https://github.com/DmitryKey/luke/releases
- Med Luke kan du öppna indexen i mappen
target/. - Ett bra sätt att se om entiteterna över huvud taget indexeras korrekt.
- Du kan också starta en sökning direkt från Luke.
ElasticSearch
Hibernate Search stöder även ElasticSearch (som då används i stället för Lucene och har bättre oskarp sökning / mer Google-känsla).