En İyi Uygulamalarım – Java

1. Döngüde String.replaceAll() yerine Pattern.compile()

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

Pattern her seferinde önce derlenmek zorundadır. Aynı Pattern kısa string’ler üzerinde sık sık çalışıyorsa, derlenmiş Pattern yeniden kullanıldığında performans belirgin şekilde daha iyidir.

2. Döngüde + yerine StringBuilder

Döngüde + yerine new StringBuilder() kullanmak daha performanslıdır.

3. equals() ve hashCode() metotlarını doğru kullanmak

Bazen equals() veya hashCode() içine tüm alanlar dahil edilir. Bu, bir entity’nin değişip değişmediğini kontrol etmek için işe yarar, ama yanlıştır. equals() metodunun amacı bir entity’nin değiştirilip değiştirilmediğini kontrol etmek değil, nesne örnekleri farklı olsa bile söz konusu olanın aynı entity olup olmadığını belirlemektir. Kısacası: Yalnızca primary key’i kullanın!

Buna her türlü collection’da ihtiyaç duyulur. Bir List üzerinde .contains() çağrıldığında Java equals metodunu kullanır. HashMap’ler ayrıca hashCode() metoduna ihtiyaç duyar. Bu ikisi birlikte override edilmelidir.

Sık rastlanan bir tuzak, henüz kaydedilmemiş ve ID’si olmayan entity’lerdir. Burada dikkatli olun ve sorun yaşamamak için erkenden persist edin.

Ayrıca bakınız:

Bu metotların sözleşmesine (contract) her zaman uyulmalıdır:

4. Farklı işletim sistemlerinin bir arada kullanıldığı ortamlarda DateTime tuzakları

Bazı işletim sistemlerinin dahili saati nanosaniye, bazılarınınki milisaniye hassasiyetindedir. Bu yüzden ZonedDateTime.now() iki platformda aynı şekilde çalışmaz ve equals() metodunu bozar. Bunun yerine hassasiyeti HER ZAMAN ayarlayın!

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

Aşağıdaki JPA anotasyonları da ZonedDateTime.now() metoduna dayanır ve asla olduğu gibi kullanılmamalıdır:

5. Karmaşık veri tiplerini satır içinde tanımlamayın

Karmaşık veri tipleri için Pair, List vb. iç içe yapılar yerine her zaman ayrı sınıflar oluşturun.

Kötü:

// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();

Daha iyi:

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 ...
}

Avantajları:

İstisnalar:

Basit ilişkiler için Pair harikadır! Örneğin:

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

6. Önbellekleme – ama doğru şekilde (minimal kurulum rehberi)

Önbellek (cache; okunuşu /kæʃ/, “keş” gibi) temelde girdileri tutan bir Map’ten ibarettir. Zamana dayalı geçersiz kılma (invalidation) gibi daha karmaşık konulara burada girilmedi.

Basit bir Map yetiyorsa her zaman bir kütüphaneye gerek yok! Birçok geliştirici bunu anlamıyor ve korkudan her seferinde kütüphaneye başvuruyor. Önbelleklemede en önemli nokta, geçersiz kılmanın doğru zamanda yapılmasıdır.

Tuzak #1: Memory leak’ler

Önbellekleme sırasında memory leak (bellek sızıntısı) çok kolay oluşabilir. equals() ve hashCode() metotlarını düzgün override etmeyi unuttuğunuz anda, bellek sızdıran bir HashMap’iniz olur. Her zaman güvenli olan bir yöntem, Map anahtarlarını elle oluşturmaktır. En iyisi yalnızca entity’nin ID’sini dahil etmektir; aksi hâlde tek bir alan değiştiğinde bile Map sızdırmaya başlar. Bu da doğrudur, çünkü bir kullanıcının adı değişti diye o, başka bir kullanıcı olmaz. Ayrıca bakınız: 3. equals() ve hashCode() metotlarını doğru kullanmak

Tuzak #2: Thread safety

Önbellek ideal olarak atomik, thread-safe işlemler sunan bir ConcurrentHashMap kullanır. Map yalnızca tek bir çağrıyla – computeIfAbsent() – değiştirilir; böylece thread safety sağlanmış olur:

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

Kod daha uzun olsaydı ve birden fazla çağrı içerseydi, metodun tamamının synchronized yapılması gerekirdi.

7. Serileştirme – hâlâ gerekli mi?

Sorum: Fields in a “Serializable” class should either be transient or serializable adında bir Sonar kuralı var. Sınıfınıza serileştirilemeyen bir alan eklediğinizde devreye giriyor. Kural şöyle diyor:

Örneğin yük altında çoğu J2EE uygulama framework’ü nesneleri diske yazar (flush) ve transient olmayan, serileştirilemeyen veri üyelerine sahip, sözde Serializable bir nesne programın çökmesine neden olabilir ve saldırganlara kapı aralayabilir.

Bu bana güncelliğini yitirmiş gibi geliyor. Effective Java 3rd Edition kitabında, yeni yazılım geliştirirken serileştirmenin önemli olmadığı belirtiliyor. Ayrıca: Serileştirmeyi Java’dan kaldırmak, Oracle’ın “uzun vadeli hedefi”. Java’nın baş mimarı Mark Reinhold bizzat şöyle demişti:

“Serileştirme, 1997’de yapılmış korkunç bir hataydı.”

Spring framework’ünün nesneleri diske yazıp yazmayabileceği sorusu burada daha önce sorulmuştu:

Bu yüzden şu soruyu soruyorum:

Modern uygulama sunucuları, framework’ler ya da J2EE implementasyonunun kendisi (özellikle CDI, EJB ve JPA ile etkileşen Java bileşenleri) yoğun yük altında nesneleri diske yazıyor mu?

Yoksa bu ifade artık geçerliliğini yitirdi mi?

Yanıt:

Kısaca: Stateful, dağıtılabilir (distributable) bir web uygulaması geliştirmiyorsanız, session persistence’ı etkinleştirmiyorsanız ve yalnızca bellek içi önbellekler kullanıyorsanız, büyük ihtimalle hiçbir yerde Serializable’a ihtiyaç duymadan idare edebilirsiniz.

Açıklama:

Session replikasyonu için Java serileştirmesi, en güncel Servlet spesifikasyonunda bile hâlâ zorunludur: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

Yaygın bir kullanım senaryosu şudur: Bir load balancer’ın arkasında birden fazla sunucunuz var ve sunuculardan birinde oturum açtığınızda bir JSESSIONID çerezi alıyorsunuz. Load balancer sizi bir sonraki istekte başka bir sunucuya yönlendirirse, oturum bilgileriniz ve session kapsamındaki diğer öznitelikler serileştirilip yeni sunucuya kopyalanır; böylece JSESSIONID orada da geçerli kalır.

Session kapsamındaki öznitelikler için serileştirmeye ihtiyaç duyduğunuz bir başka senaryo da uygulama sunucusunun session’ları diske aktarmaya karar vermesidir. Örneğin Tomcat 8’de bu, bellekte tutulamayacak kadar çok session olduğunda ya da sunucu yeniden başlatıldığında session’ların canlı kalmasını istediğinizde devreye girecek şekilde yapılandırılabilir. Bkz. https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Servlet 5.0 uyumlu her uygulama sunucusu, bu tür senaryolar için HTTP session’larında Serializable nesneleri varsayılan olarak desteklemek zorundadır.

Bazı sunucularda bu davranış, örneğin Jackson-JSON serileştirmesi ya da benzeri bir yöntemle ayarlanabilir veya değiştirilebilir. Örneğin bkz. https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Bir diğer yaygın senaryo önbelleklemedir. Amaç, nesneleri olabildiğince hızlı erişilebilir tutmaktır; ideal olarak bellekte, ama gerekirse geçici olarak diske ya da harici bir önbellek sunucusuna aktarılarak.
Bunun için standart bir API (JSR-107) var ve bu API bilinçli olarak Serializable nesneleri zorunlu kılmıyor; yine de çoğu implementasyon, aktarma kısmı için varsayılan olarak Java serileştirmesini kullanıyor.

Bunları da başka serileştirme mekanizmalarını destekleyecek şekilde ayarlayabilirsiniz, örneğin ehcache için: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: Fetching stratejisi

Hibernate katkıcısı olan son derece karizmatik bir ukalanın kaleme aldığı EAGER fetching is a code smell yazısı göz önüne alındığında, FetchType olarak LAZY tercih edilmelidir. Bazı veri tipleri bir yabancı anahtar (foreign key) olmadan anlam taşımasa da, bu tipler pekâlâ tek başlarına sorgulanır (getCount(), data table’lar vb.).

Aşağıda tüm senaryolar, çözümün ne kadar tatmin edici olduğuna göre, en az tatmin edici olandan başlayarak sıralanmıştır:

1. En kötü durum: LazyInitializationException

Söze gerek yok. Bu acıyı hepimiz biliyoruz.

2. Hızlı çözüm

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. Doğrudan serviste unproxy

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. Doğrudan serviste fetch (bunu yapın!)

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, Hibernate JPA ile kusursuz biçimde entegredir. Entity’lerin ne zaman değiştiğini algılar ve kolayca yapılandırılabilir:

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

    // ...

}

Yerel ortamda arama kapalıdır:

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

Bulanık arama ve arama davranışı

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

Lucene’de Luke ile hata ayıklama

Kullandığımız Hibernate Search sürümünde hata ayıklama, Luke’un 6.6.0 sürümüyle yapılmalıdır. Yalnızca bu sürümle çalışır!

ElasticSearch

Hibernate Search, ElasticSearch’ü de destekler (bu durumda Lucene yerine kullanılır; bulanık araması daha iyidir ve daha çok “Google hissi” verir).