Meine Best Practices für Java- & Webentwicklung
- Die Paradigmen
- Meine Best Practices – Allgemein
- Meine Best Practices – Java
- Meine Best Practices – JavaScript
- Meine Best Practices – CSS
- Best Practices aus "Effective Java (3rd Edition)"
Meine Best Practices – Java
1. Pattern.compile() statt String.replaceAll() im Loop
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Das Pattern muss immer zuerst kompiliert werden. Rennt das gleiche Pattern oft mit kurzen Strings, ist die Performance deutlich besser, wenn man das kompilierte Pattern
wiederverwendet.
2. StringBuilder statt + im Loop
Performanter ist new StringBuilder() statt + im Loop.
3. equals() und hashCode() richtig verwenden
Manchmal werden alle Felder beim equals() oder hashCode() einbezogen. Dies ist nützlich für die Überprüfung, ob ein Entity geändert wurde, aber falsch. Der Zweck von
equals() ist nicht zu überprüfen, ob ein Entity modifiziert wurde, sondern festzustellen, ob es sich, auch wenn die Objektinstanz nicht die gleiche ist, trotzdem um das gleiche Entity
handelt. Sprich: Nur den primary Key verwenden!
Gebraucht wird dies für jegliche Art von Collections. Ruft man .contains() auf einer List auf, so wird Java die equals-Methode verwenden. HashMaps brauchen zudem hashCode().
Sie sollten zusammen überschrieben werden.
Ein gängiger Pitfall sind ungespeicherte Entities ohne ID. Hier vorsichtig sein und früh persistieren, um Probleme zu vermeiden.
Vergleich:
Der Contract dieser Methoden muss stets eingehalten werden:
4. Pitfalls mit DateTime bei Mischung von Betriebssystemen
Manche Betriebssysteme haben eine interne Uhr mit Nanosekunden, andere mit Milisekunden. ZonedDateTime.now() funktioniert damit nicht auf beiden Platformen gleich und macht
equals() kaputt. Stattdessen IMMER die Präzision anpassen!
/**
* 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);
}
Auch nachfolgende JPA-Methoden verlassen sich auf ZonedDateTime.now() und sollten nie plain verwendet werden:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Komplexe Datentypen nicht inline deklarieren
Immer eigene Klassen erstellen für komplexe Datentypen statt Verschachtelung von Pair, Lists etc.
Bad:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Besser:
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 ...
}
Vorteile:
- Bessere Namen als
getFirst()undgetSecond() - Verwendung ist durch Getter und Setter klar definiert (ist die Klasse immutable? Welche Liste darf erweitert werden? Wie ist das Objekt zu instanziieren?) → Encapsulation dokumentiert die Verwendung des Codes von alleine
- Deutlich bessere Lesbarkeit
Ausnahmen:
Für einfache Relationen ist Pair super! Zum Beispiel:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Caching – aber richtig (Minimal Setup Guide)
Der Cache (Aussprache /kæʃ/, wie "cash") ist im Prinzip nur eine Map, die Einträge hält. Auf komplexere Dinge wie zeitbasierte Invalidierung o.Ä. wurde verzichtet.
Es muss nicht immer eine Library sein, wenn eine einfache Map ausreicht! Viele Entwickler verstehen das nicht und nehmen aus Angst prinzipiell Libraries. Der wichtigste Aspekt beim Cachen ist, dass zum richtigen Zeitpunkt invalidiert wird.
Pitfall #1: Memory Leaks
Memory Leaks können sehr leicht passieren beim Cachen. Kaum vergisst man equals() und hashCode() sauber zu überschreiben, schon hat man eine HashMap die
Memory leakt. Eine Methode die immer safe ist, ist das händische Erstellen der Map Keys. Gut wäre, ausschließlich die ID eines Entities einzubeziehen, ansonsten leakt die Map bereits, wenn man nur
ein Feld ändert. Was auch richtig ist, da nur weil sich der Vorname eines Users ändert, so ist es immer noch der gleiche User. Vergleich: 3. equals() und hashCode()
richtig verwenden
Pitfall #2: Thread Safety
Der Cache verwendet bestenfalls eine ConcurrentHashMap, die atomare, thread-safe Operationen anbietet. Modifiziert wird die Map nur über einen einzigen Call – computeIfAbsent() - dadurch ist Thread-Safety erreicht:
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());
}
Wäre der Code länger und gäbe es mehrere Aufrufe, müsste die ganze Methode synchronized werden.
7. Serialisierung – Wird sie noch benötigt?
Meine Frage: There is this Sonar rule called Fields in a “Serializable” class should either be transient or serializable. It comes up when you add a non-serializable field to your class. The rule states:
For instance, under load, most J2EE application frameworks flush objects to disk, and an allegedly Serializable object with non-transient, non-serializable data members could cause program crashes, and open the door to attackers.
This sounds outdated to me. In Effective Java 3rd Edition it is stated that Serialization is not relevant, when developing new software. Also, Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. The Java chief architect himself, Mark Reinhold, stated
„Serialization was a horrible mistake in 1997.“
The question, if the Spring framework may flush objects, was already asked on here:
So I ask the question:
Do any modern application servers, frameworks or the J2EE implementation itself (especially Java components that interact with CDI, EJB, JPA) flush objects to disk under heavy load?
Or is this statement outdated?
Answer:
TL;DR If you're not implementing a stateful distributable web application, you do not enable session persistence, and you only use in-memory caches, you might get away without needing Serializable anywhere.
Explanation:
For session replication, Java Serialization is still a must for the latest Servlet Specification: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
A common use case is if you have multiple servers behind a load balancer, and you log in on one server, you get a JSESSIONID cookie. If the load balancer sends you to a different
server for the next request, your login attributes and other session-scoped attributes will be serialized and replicated on the new server, so your JSESSIONID is still valid there.
Another use case where you need Serialization for your session-scoped attributes, is when the application server decides to swap sessions to disk. For e.g. Tomcat 8, this can be configured to activate when there are too many sessions to keep in memory, or when the server is restarted, and you want to keep the sessions alive. See https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Every Servlet-5.0 compatible application server must support Serializable objects in Http Sessions by default for these kinds of use cases.
For some servers, this can be tweaked or overridden by using e.g. Jackson-JSON serialization or something similar. See e.g. https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Another common use case is caching. This is used to keep objects accessible as fast as possible, ideally in memory, but possibly temporarily offloaded to disk or to an external cache server.
There is a standard API for it (JSR-107) that explicitly chose not to require Serializable objects, but most implementations still use Java Serialization for the offloading part as a default.
You can again tweak these to support other serialization mechanisms, e.g. for ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: Fetching-Strategie
In Anbetracht von EAGER fetching is a code smell, geschrieben von einem überaus charismatischen Klugscheisser, der Hibernate
Contributor ist, ist LAZYals FetchType vorzuziehen. Auch wenn einige Datentypen ohne einen Fremdschlüssel keinen Sinn machen, so werden diese Typen sehr wohl alleine
gequeried (getCount(), Data-Tables, etc.).
Hier alle Szenarien sortiert nach Grad der Zufriedenstellung der Lösung, beginnend mit dem am wenigsten zufriedenstellenden:
1. Worst Case: LazyInitializationException
No words needed. We all share this pain.
2. Quick fix
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 directly in the 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 directly in the service (do this!)
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 ist nahtlos mit Hibernate JPA verbunden. Es bekommt mit, wenn sich Entities ändern und kann einfach konfiguriert werden:
@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;
// ...
}
Lokal ist die Suche ausgeschaltet:
// 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();
}
Unscharfe Suche & Suchverhalten
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
Debuggen von Lucene mit Luke
Unsere Version von Hibernate Search muss mit Luke Version 6.6.0 debuggt werden. Es funktioniert nur mit dieser Version!
- Download: https://github.com/DmitryKey/luke/releases
- Es können damit die Indexes aus dem Ordner
target/geöffnet werden. - Ein guter Punkt um zu sehen, ob die Entities überhaupt richtig indexiert werden.
- Eine Suche kann auch aus Luke heraus gestartet werden.
ElasticSearch
Hibernate Search unterstützt auch ElasticSearch (wird dann anstelle von Lucene eingesetzt und hat eine bessere unscharfe Suche / mehr Google-Feeling).