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:

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:

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!

ElasticSearch

Hibernate Search juga mendukung ElasticSearch (dipakai sebagai pengganti Lucene dan memiliki pencarian fuzzy yang lebih baik / terasa lebih seperti Google).