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:

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:

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!

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).