Amalan Terbaik Saya – Java

1. Pattern.compile() dan bukannya String.replaceAll() dalam gelung

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

Pattern sentiasa perlu dikompil terlebih dahulu. Jika Pattern yang sama dijalankan berkali-kali pada rentetan pendek, prestasinya jauh lebih baik apabila Pattern yang sudah dikompil itu digunakan semula.

2. StringBuilder dan bukannya + dalam gelung

Menggunakan new StringBuilder() lebih pantas berbanding + dalam gelung.

3. Gunakan equals() dan hashCode() dengan betul

Kadangkala semua medan dimasukkan ke dalam equals() atau hashCode(). Ini berguna untuk menyemak sama ada sesebuah entiti telah berubah, tetapi ia salah. Tujuan equals() bukanlah untuk menyemak sama ada entiti telah diubah suai, tetapi untuk menentukan sama ada ia tetap entiti yang sama walaupun instans objeknya berlainan. Dengan kata lain: Gunakan kunci primer sahaja!

Ini diperlukan oleh semua jenis koleksi. Apabila anda memanggil .contains() pada sebuah List, Java akan menggunakan kaedah equals. HashMap pula turut memerlukan hashCode(). Kedua-dua kaedah ini patut di-override bersama-sama.

Perangkap yang biasa ialah entiti yang belum disimpan dan belum mempunyai ID. Berhati-hatilah di sini dan simpan (persist) entiti seawal mungkin untuk mengelakkan masalah.

Bacaan lanjut:

Kontrak kaedah-kaedah ini mesti sentiasa dipatuhi:

4. Perangkap DateTime apabila sistem pengendalian bercampur

Sesetengah sistem pengendalian mempunyai jam dalaman dengan ketepatan nanosaat, yang lain pula milisaat. Oleh itu ZonedDateTime.now() tidak berkelakuan sama pada kedua-dua platform dan merosakkan equals(). Sebaliknya, SENTIASA laraskan ketepatannya!

/**
 * 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);
}

Anotasi JPA berikut juga bergantung pada ZonedDateTime.now() dan tidak patut digunakan begitu sahaja:

5. Jangan isytiharkan jenis data kompleks secara sebaris

Sentiasa cipta kelas tersendiri untuk jenis data kompleks dan bukannya menyarangkan 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 ...
}

Kelebihan:

Pengecualian:

Untuk hubungan yang ringkas, Pair memang hebat! Contohnya:

Pair<User, Training> pair = this.addCurrentUserToTraining(training);

6. Caching – dengan cara yang betul (panduan persediaan minimum)

Cache (sebutannya /kæʃ/, seperti "cash") pada dasarnya hanyalah sebuah Map yang menyimpan entri. Perkara yang lebih kompleks seperti pembatalan berasaskan masa dan seumpamanya sengaja ditinggalkan.

Tidak semestinya perlu menggunakan pustaka jika sebuah Map yang ringkas sudah memadai! Ramai pembangun tidak memahami perkara ini dan, kerana takut, sentiasa memilih pustaka. Aspek paling penting dalam caching ialah membatalkan (invalidate) cache pada masa yang tepat.

Perangkap #1: kebocoran memori

Kebocoran memori sangat mudah berlaku semasa caching. Sebaik sahaja anda terlupa meng-override equals() dan hashCode() dengan betul, HashMap anda sudah pun membocorkan memori. Cara yang sentiasa selamat ialah membina kunci Map secara manual. Sebaik-baiknya hanya ID entiti yang digunakan; jika tidak, Map itu sudah bocor sebaik sahaja satu medan berubah. Ini memang wajar, kerana walaupun nama pertama seorang pengguna berubah, dia tetap pengguna yang sama. Rujuk: 3. Gunakan equals() dan hashCode() dengan betul

Perangkap #2: thread safety

Sebaik-baiknya cache menggunakan ConcurrentHashMap, yang menyediakan operasi atomik dan selamat-thread. Map itu hanya diubah melalui satu panggilan sahaja – computeIfAbsent() – dan dengan itu 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 kod itu lebih panjang dan terdapat beberapa panggilan, keseluruhan kaedah itu perlu dijadikan synchronized.

7. Serialisasi – masih diperlukan?

Soalan saya: Terdapat sebuah peraturan Sonar bernama Fields in a “Serializable” class should either be transient or serializable (medan dalam kelas “Serializable” mestilah sama ada transient atau boleh diserialisasi). Ia muncul apabila anda menambah medan yang tidak boleh diserialisasi pada kelas anda. Peraturan itu menyatakan:

Sebagai contoh, apabila beban tinggi, kebanyakan rangka kerja aplikasi J2EE menyimpan objek ke cakera, dan objek yang kononnya Serializable tetapi mempunyai ahli data yang bukan transient dan tidak boleh diserialisasi boleh menyebabkan program ranap serta membuka pintu kepada penyerang.

Bagi saya, ini kedengaran lapuk. Dalam Effective Java 3rd Edition dinyatakan bahawa serialisasi tidak relevan apabila membangunkan perisian baharu. Selain itu, membuang serialisasi daripada Java ialah “matlamat jangka panjang” Oracle. Ketua arkitek Java sendiri, Mark Reinhold, pernah berkata:

“Serialisasi ialah satu kesilapan yang dahsyat pada tahun 1997.”

Soalan sama ada rangka kerja Spring mungkin menyimpan objek ke cakera sudah pun ditanya di sini:

Jadi saya bertanya:

Adakah mana-mana pelayan aplikasi moden, rangka kerja atau pelaksanaan J2EE itu sendiri (terutamanya komponen Java yang berinteraksi dengan CDI, EJB dan JPA) menyimpan objek ke cakera apabila beban sangat tinggi?

Atau adakah kenyataan ini sudah lapuk?

Jawapan:

Ringkasnya: Jika anda tidak membina aplikasi web teragih yang stateful, tidak mengaktifkan penyimpanan sesi yang kekal (session persistence) dan hanya menggunakan cache dalam memori, anda mungkin langsung tidak memerlukan Serializable.

Penjelasan:

Untuk replikasi sesi, serialisasi Java masih wajib menurut spesifikasi Servlet terkini: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

Kes penggunaan yang biasa: anda mempunyai beberapa pelayan di belakang pengimbang beban (load balancer), dan apabila anda log masuk pada satu pelayan, anda menerima kuki JSESSIONID. Jika pengimbang beban menghantar anda ke pelayan lain untuk permintaan seterusnya, atribut log masuk anda dan atribut lain berskop sesi akan diserialisasi dan direplikasi ke pelayan baharu itu, supaya JSESSIONID anda masih sah di sana.

Satu lagi kes penggunaan yang memerlukan serialisasi untuk atribut berskop sesi ialah apabila pelayan aplikasi memutuskan untuk memindahkan sesi ke cakera. Contohnya dalam Tomcat 8, ini boleh dikonfigurasikan supaya berlaku apabila terdapat terlalu banyak sesi untuk disimpan dalam memori, atau apabila pelayan dimulakan semula dan anda mahu sesi kekal hidup. Lihat https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Setiap pelayan aplikasi yang serasi dengan Servlet 5.0 mesti menyokong objek Serializable dalam sesi HTTP secara lalai untuk kes penggunaan seperti ini.

Bagi sesetengah pelayan, tingkah laku ini boleh dilaraskan atau diganti, contohnya dengan serialisasi Jackson-JSON atau yang seumpamanya. Lihat contohnya https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Satu lagi kes penggunaan yang biasa ialah caching. Ia digunakan supaya objek boleh dicapai sepantas mungkin, sebaik-baiknya dalam memori, tetapi mungkin dipindahkan buat sementara ke cakera atau ke pelayan cache luaran.
Terdapat API standard untuknya (JSR-107) yang sengaja tidak mewajibkan objek Serializable, tetapi kebanyakan pelaksanaan masih menggunakan serialisasi Java secara lalai untuk bahagian pemindahan itu.

Sekali lagi, anda boleh melaraskannya supaya menyokong mekanisme serialisasi lain, contohnya untuk ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: strategi fetching

Memandangkan artikel EAGER fetching is a code smell, yang ditulis oleh seorang “si serba tahu” yang sangat berkarisma dan juga penyumbang Hibernate, LAZY lebih diutamakan sebagai FetchType. Walaupun sesetengah jenis data tidak bermakna tanpa kunci asingnya, jenis-jenis ini tetap di-query secara bersendirian (getCount(), jadual data dan sebagainya).

Berikut ialah semua senario, disusun mengikut tahap kepuasan penyelesaiannya, bermula dengan yang paling kurang memuaskan:

1. Senario terburuk: LazyInitializationException

Tak perlu cakap apa-apa. Kita semua pernah merasai keperitan ini.

2. Penyelesaian pantas

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 terus di dalam 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 terus di dalam service (buat begini!)

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 bersepadu dengan lancar bersama Hibernate JPA. Ia mengesan perubahan pada entiti dan mudah dikonfigurasikan:

@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;

    // ...

}

Dalam persekitaran setempat, carian dimatikan:

// 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();
}

Carian kabur & tingkah laku carian

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

Menyahpepijat Lucene dengan Luke

Versi Hibernate Search kami mesti dinyahpepijat dengan Luke versi 6.6.0. Ia hanya berfungsi dengan versi ini!

ElasticSearch

Hibernate Search juga menyokong ElasticSearch (yang kemudiannya digunakan menggantikan Lucene dan menawarkan carian kabur yang lebih baik – lebih terasa seperti Google).