Best Practices Saya – Java
1. Pattern.compile() alih-alih String.replaceAll() di dalam loop
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Pattern selalu harus dikompilasi terlebih dahulu. Jika Pattern yang sama sering dijalankan dengan string pendek, performanya jauh lebih baik bila Pattern yang sudah dikompilasi
digunakan ulang.
2. StringBuilder alih-alih + di dalam loop
Di dalam loop, new StringBuilder() lebih cepat daripada +.
3. Menggunakan equals() dan hashCode() dengan benar
Terkadang semua field dilibatkan dalam equals() atau hashCode(). Ini berguna untuk memeriksa apakah sebuah entity telah berubah, tetapi salah. Tujuan
equals() bukanlah memeriksa apakah sebuah entity telah dimodifikasi, melainkan menentukan apakah, meskipun instance objeknya berbeda, keduanya tetap entity yang sama.
Singkatnya: Gunakan primary key saja!
Hal ini dibutuhkan oleh semua jenis collection. Jika .contains() dipanggil pada sebuah List, Java akan menggunakan method equals. HashMap juga membutuhkan hashCode().
Keduanya sebaiknya di-override bersamaan.
Jebakan yang umum adalah entity yang belum disimpan dan belum memiliki ID. Berhati-hatilah di sini dan lakukan persist sedini mungkin untuk menghindari masalah.
Bandingkan:
Kontrak method-method ini harus selalu dipatuhi:
4. Jebakan DateTime saat sistem operasi bercampur
Sebagian sistem operasi memiliki jam internal dengan presisi nanodetik, sebagian lagi milidetik. Akibatnya ZonedDateTime.now() tidak bekerja sama di kedua platform dan merusak
equals(). Karena itu, SELALU sesuaikan presisinya!
/**
* 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);
}
Method JPA berikut juga mengandalkan ZonedDateTime.now() dan tidak boleh dipakai begitu saja:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Jangan mendeklarasikan tipe data kompleks secara inline
Selalu buat class sendiri untuk tipe data kompleks, alih-alih menumpuk Pair, List, dan sebagainya.
Buruk:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Lebih baik:
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 ...
}
Keuntungan:
- Nama yang lebih baik daripada
getFirst()dangetSecond() - Cara penggunaan terdefinisi jelas lewat getter dan setter (apakah class-nya immutable? List mana yang boleh ditambah? Bagaimana objek ini harus diinstansiasi?) → Enkapsulasi mendokumentasikan cara pakai kode dengan sendirinya
- Keterbacaan jauh lebih baik
Pengecualian:
Untuk relasi sederhana, Pair sangat bagus! Contohnya:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Caching – tapi dengan benar (panduan setup minimal)
Cache (dilafalkan /kæʃ/, seperti “cash”) pada dasarnya hanyalah sebuah Map yang menyimpan entri. Hal-hal yang lebih rumit seperti invalidasi berbasis waktu dan sejenisnya sengaja tidak dipakai.
Tidak selalu harus memakai library jika Map sederhana sudah cukup! Banyak developer tidak memahami hal ini dan karena takut selalu memakai library. Aspek terpenting dalam caching adalah melakukan invalidasi pada saat yang tepat.
Jebakan #1: Memory Leak
Memory Leak sangat mudah terjadi saat caching. Begitu lupa meng-override equals() dan hashCode() dengan benar, Anda langsung punya HashMap
yang bocor memori. Cara yang selalu aman adalah membuat key Map secara manual. Sebaiknya hanya ID entity yang dilibatkan; jika tidak, Map sudah bocor begitu
satu field saja diubah. Dan itu memang benar, karena hanya karena nama depan seorang user berubah, ia tetaplah user yang sama. Bandingkan: 3. Menggunakan equals() dan hashCode()
dengan benar
Jebakan #2: Thread Safety
Idealnya cache menggunakan ConcurrentHashMap, yang menyediakan operasi atomik dan thread-safe. Map hanya dimodifikasi lewat satu panggilan – computeIfAbsent() – sehingga thread safety tercapai:
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());
}
Jika kodenya lebih panjang dan ada beberapa panggilan, seluruh method harus dibuat synchronized.
7. Serialisasi – masih diperlukan?
Pertanyaan saya: Ada aturan Sonar bernama Fields in a “Serializable” class should either be transient or serializable (field dalam class “Serializable” harus transient atau serializable). Aturan ini muncul ketika Anda menambahkan field yang tidak serializable ke class Anda. Aturan tersebut menyatakan:
Misalnya, saat beban tinggi, sebagian besar framework aplikasi J2EE menulis (flush) objek ke disk, dan objek yang katanya Serializable tetapi memiliki anggota data non-transient yang tidak serializable dapat membuat program crash, serta membuka pintu bagi penyerang.
Bagi saya ini terdengar usang. Dalam Effective Java 3rd Edition dinyatakan bahwa serialisasi tidak relevan saat mengembangkan perangkat lunak baru. Selain itu, Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. Kepala arsitek Java sendiri, Mark Reinhold, mengatakan
“Serialisasi adalah kesalahan mengerikan pada tahun 1997.”
Pertanyaan apakah framework Spring mungkin melakukan flush objek sudah pernah ditanyakan di sini:
Jadi saya bertanya:
Apakah ada application server, framework modern, atau implementasi J2EE itu sendiri (terutama komponen Java yang berinteraksi dengan CDI, EJB, JPA) yang melakukan flush objek ke disk saat beban berat?
Atau pernyataan ini sudah usang?
Jawaban:
TL;DR Jika Anda tidak membangun aplikasi web stateful yang dapat didistribusikan, tidak mengaktifkan persistensi session, dan hanya memakai cache di memori, kemungkinan besar Anda bisa lolos tanpa membutuhkan Serializable di mana pun.
Penjelasan:
Untuk replikasi session, Java Serialization masih wajib menurut Servlet Specification terbaru: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Kasus penggunaan yang umum: Anda punya beberapa server di belakang load balancer, lalu Anda login di salah satu server dan mendapat cookie JSESSIONID. Jika load balancer mengarahkan Anda ke server
lain pada request berikutnya, atribut login dan atribut lain yang berlingkup session akan diserialisasi dan direplikasi ke server baru, sehingga JSESSIONID Anda tetap valid di sana.
Kasus lain yang membutuhkan serialisasi untuk atribut berlingkup session adalah ketika application server memutuskan untuk memindahkan session ke disk. Misalnya pada Tomcat 8, hal ini dapat dikonfigurasi agar aktif ketika ada terlalu banyak session untuk disimpan di memori, atau ketika server di-restart dan Anda ingin session tetap hidup. Lihat https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Setiap application server yang kompatibel dengan Servlet 5.0 secara default wajib mendukung objek Serializable di HTTP session untuk kasus-kasus seperti ini.
Pada sebagian server, hal ini dapat disesuaikan atau diganti, misalnya dengan serialisasi JSON Jackson atau yang serupa. Lihat misalnya https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Kasus umum lainnya adalah caching. Caching dipakai agar objek dapat diakses secepat mungkin, idealnya di memori, tetapi bisa juga untuk sementara dipindahkan ke disk atau ke server cache eksternal.
Ada API standar untuk ini (JSR-107) yang secara eksplisit memilih untuk tidak mewajibkan objek Serializable, tetapi sebagian besar implementasi tetap memakai Java Serialization secara default untuk bagian pemindahan tersebut.
Anda juga bisa menyesuaikannya agar mendukung mekanisme serialisasi lain, misalnya untuk ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: Strategi fetching
Mengingat EAGER fetching is a code smell, yang ditulis oleh seorang sok tahu yang luar biasa karismatik dan kebetulan contributor Hibernate,
LAZY lebih disarankan sebagai FetchType. Meskipun beberapa tipe data tidak masuk akal tanpa foreign key, tipe-tipe ini tetap saja di-query
secara terpisah (getCount(), data table, dll.).
Berikut semua skenario, diurutkan menurut seberapa memuaskan solusinya, dimulai dari yang paling tidak memuaskan:
1. Kasus terburuk: LazyInitializationException
Tak perlu banyak kata. Kita semua pernah merasakan sakitnya.
2. Perbaikan cepat
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 langsung di 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 langsung di service (lakukan ini!)
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 terhubung mulus dengan Hibernate JPA. Ia tahu saat entity berubah dan mudah dikonfigurasi:
@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;
// ...
}
Secara lokal, pencarian dinonaktifkan:
// 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();
}
Pencarian fuzzy & perilaku pencarian
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 dengan Luke
Versi Hibernate Search kami harus di-debug dengan Luke versi 6.6.0. Hanya versi ini yang berfungsi!
- Unduh: https://github.com/DmitryKey/luke/releases
- Dengan Luke, index dari folder
target/dapat dibuka. - Cara yang bagus untuk melihat apakah entity memang sudah diindeks dengan benar.
- Pencarian juga bisa dijalankan langsung dari Luke.
ElasticSearch
Hibernate Search juga mendukung ElasticSearch (dipakai sebagai pengganti Lucene dan memiliki pencarian fuzzy yang lebih baik / terasa lebih seperti Google).