My Best Practices – Java
1. Pattern.compile() instead of String.replaceAll() in a loop
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
The Pattern always has to be compiled first. If the same Pattern runs often against short strings, performance is noticeably better when you reuse the compiled Pattern.
2. StringBuilder instead of + in a loop
new StringBuilder() performs better than + in a loop.
3. Using equals() and hashCode() correctly
Sometimes all fields get included in equals() or hashCode(). That’s handy for checking whether an entity has changed, but it’s wrong. The purpose of
equals() is not to check whether an entity was modified, but to determine whether it’s still the same entity, even if it’s not the same object instance.
In other words: Use only the primary key!
You need this for any kind of collection. Call .contains() on a List and Java will use the equals method. HashMaps also need hashCode().
The two should always be overridden together.
A common pitfall is unsaved entities without an ID. Be careful here and persist early to avoid problems.
See also:
The contract of these methods must always be honored:
4. DateTime pitfalls when mixing operating systems
Some operating systems have an internal clock with nanosecond precision, others with milliseconds. So ZonedDateTime.now() doesn’t behave the same on both platforms and breaks
equals(). Instead, ALWAYS adjust the precision!
/**
* 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);
}
The following JPA annotations also rely on ZonedDateTime.now() and should never be used plain:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Don’t declare complex data types inline
Always create dedicated classes for complex data types instead of nesting Pairs, Lists etc.
Bad:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Better:
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 ...
}
Advantages:
- Better names than
getFirst()andgetSecond() - Usage is clearly defined by getters and setters (Is the class immutable? Which list may be extended? How should the object be instantiated?) → Encapsulation documents how the code is meant to be used, all by itself
- Much better readability
Exceptions:
For simple relations, Pair is great! For example:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Caching – done right (minimal setup guide)
The cache (pronounced /kæʃ/, like “cash”) is basically just a map that holds entries. More complex things like time-based invalidation and the like were left out.
It doesn’t always have to be a library when a simple map will do! Many developers don’t get this and reach for libraries out of fear, as a matter of principle. The most important aspect of caching is invalidating at the right moment.
Pitfall #1: Memory Leaks
Memory leaks happen very easily when caching. Forget to override equals() and hashCode() properly, and before you know it you’ve got a HashMap that
leaks memory. One approach that’s always safe is building the map keys by hand. Ideally, include only the ID of an entity – otherwise the map already leaks as soon as you change
a single field. Which makes sense, too: just because a user’s first name changes, it’s still the same user. See: 3. Using equals() and hashCode()
correctly
Pitfall #2: Thread Safety
Ideally, the cache uses a ConcurrentHashMap, which offers atomic, thread-safe operations. The map is only ever modified through a single call – computeIfAbsent() – which is how thread safety is achieved:
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());
}
If the code were longer and there were several calls, the whole method would have to be synchronized.
7. Serialization – is it still needed?
My question: 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 strategy
In light of EAGER fetching is a code smell, written by an exceedingly charismatic smart-ass who happens to be a Hibernate
contributor, LAZY is the preferred FetchType. Even if some data types make no sense without a foreign key, these types very much do get
queried on their own (getCount(), data tables, etc.).
Here are all the scenarios, ranked by how satisfying the solution is, starting with the least satisfying:
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 integrates seamlessly with Hibernate JPA. It notices when entities change and is easy to configure:
@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;
// ...
}
Search is switched off locally:
// 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();
}
Fuzzy search & search behavior
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
Debugging Lucene with Luke
Our version of Hibernate Search has to be debugged with Luke version 6.6.0. It only works with this version!
- Download: https://github.com/DmitryKey/luke/releases
- You can use it to open the indexes from the
target/folder. - A good way to check whether the entities are being indexed correctly in the first place.
- You can also run a search from within Luke.
ElasticSearch
Hibernate Search also supports ElasticSearch (it then replaces Lucene and offers better fuzzy search / more of a Google feel).