मेरी Best Practices – Java

1. Loop में String.replaceAll() की जगह Pattern.compile()

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

Pattern को हमेशा पहले compile करना पड़ता है। अगर वही Pattern छोटी strings पर बार-बार चलता है, तो compiled Pattern को दोबारा इस्तेमाल करने पर performance काफ़ी बेहतर होती है।

2. Loop में + की जगह StringBuilder

Loop में + की जगह new StringBuilder() ज़्यादा performant है।

3. equals() और hashCode() का सही इस्तेमाल

कभी-कभी equals() या hashCode() में सारे fields शामिल कर लिए जाते हैं। यह जाँचने के लिए तो उपयोगी है कि कोई entity बदली है या नहीं, लेकिन यह गलत है। equals() का मकसद यह जाँचना नहीं है कि entity modify हुई या नहीं, बल्कि यह तय करना है कि object instance अलग होने के बावजूद क्या यह वही entity है। यानी: सिर्फ़ primary key का इस्तेमाल करें!

इसकी ज़रूरत हर तरह के collections में पड़ती है। किसी List पर .contains() call करने पर Java equals method का इस्तेमाल करता है। HashMaps को इसके अलावा hashCode() भी चाहिए। इन दोनों को हमेशा साथ-साथ override करना चाहिए।

एक आम pitfall है बिना ID वाली, अभी save न हुई entities। यहाँ सावधान रहें और समस्याओं से बचने के लिए जल्दी persist करें।

इन्हें भी देखें:

इन methods के contract का हमेशा पालन होना चाहिए:

4. अलग-अलग operating systems के साथ DateTime के pitfalls

कुछ operating systems की internal clock nanoseconds में चलती है, तो कुछ की milliseconds में। इसलिए ZonedDateTime.now() दोनों platforms पर एक जैसा काम नहीं करता और equals() को तोड़ देता है। इसकी जगह हमेशा precision को adjust करें!

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

नीचे दिए गए JPA annotations भी ZonedDateTime.now() पर निर्भर हैं और इन्हें कभी भी ज्यों का त्यों (plain) इस्तेमाल नहीं करना चाहिए:

5. Complex data types को inline declare न करें

Complex data types के लिए Pair, Lists वगैरह को एक-दूसरे में nest करने के बजाय हमेशा अपनी अलग classes बनाएँ।

बुरा:

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

बेहतर:

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

फ़ायदे:

अपवाद:

सरल relations के लिए Pair बढ़िया है! उदाहरण के लिए:

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

6. Caching – पर सही तरीके से (Minimal Setup Guide)

Cache (उच्चारण /kæʃ/, “cash” की तरह) असल में बस एक Map है, जो entries रखता है। समय-आधारित invalidation जैसी ज़्यादा जटिल चीज़ें यहाँ जान-बूझकर छोड़ दी गई हैं।

जब एक साधारण Map काफ़ी हो, तो हर बार library की ज़रूरत नहीं होती! बहुत-से डेवलपर्स यह नहीं समझते और डर के मारे हर बार library ले आते हैं। Caching में सबसे अहम बात यह है कि invalidation सही समय पर हो।

पहला ख़तरा: Memory Leaks

Caching में memory leaks बहुत आसानी से हो जाते हैं। ज़रा-सा equals() और hashCode() को ठीक से override करना भूले नहीं कि HashMap memory leak करने लगता है। एक तरीका जो हमेशा safe है, वह है map keys को हाथ से बनाना। सबसे अच्छा यह होगा कि सिर्फ़ entity की ID शामिल की जाए, वरना किसी एक field के बदलते ही map leak करने लगता है। और यही सही भी है, क्योंकि किसी user का first name बदल जाने से वह कोई दूसरा user नहीं बन जाता। तुलना करें: 3. equals() और hashCode() का सही इस्तेमाल

दूसरा ख़तरा: Thread Safety

Cache के लिए सबसे अच्छा है ConcurrentHashMap, जो atomic, thread-safe operations देता है। Map को सिर्फ़ एक ही call – computeIfAbsent() – से modify किया जाता है, और इसी से thread safety हासिल होती है:

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

अगर कोड लंबा होता और उसमें कई calls होतीं, तो पूरे method को synchronized करना पड़ता।

7. Serialization – क्या अब भी इसकी ज़रूरत है?

मेरा सवाल: एक Sonar rule है, जिसका नाम है Fields in a “Serializable” class should either be transient or serializable (यानी “Serializable” class के fields या तो transient हों या serializable)। यह तब सामने आता है, जब आप अपनी class में कोई non-serializable field जोड़ते हैं। Rule कहता है:

उदाहरण के लिए, load के दौरान ज़्यादातर J2EE application frameworks objects को disk पर flush कर देते हैं, और non-transient, non-serializable data members वाला कोई कथित रूप से Serializable object program crash करा सकता है और attackers के लिए दरवाज़ा खोल सकता है।

मुझे यह पुराना लगता है। Effective Java 3rd Edition में कहा गया है कि नया software बनाते समय Serialization प्रासंगिक नहीं है। साथ ही, Java से Serialization को हटाना Oracle का एक ‘long-term goal’ है। खुद Java के chief architect, Mark Reinhold ने कहा था:

“Serialization 1997 में की गई एक भयानक गलती थी।”

Spring framework objects को flush कर सकता है या नहीं, यह सवाल यहाँ पहले ही पूछा जा चुका है:

इसलिए मेरा सवाल है:

क्या कोई भी आधुनिक application server, framework या खुद J2EE implementation (ख़ासकर CDI, EJB, JPA के साथ काम करने वाले Java components) भारी load में objects को disk पर flush करता है?

या यह बात अब पुरानी हो चुकी है?

जवाब:

TL;DR अगर आप कोई stateful distributable web application नहीं बना रहे, session persistence enable नहीं करते और सिर्फ़ in-memory caches इस्तेमाल करते हैं, तो मुमकिन है कि आपको कहीं भी Serializable की ज़रूरत न पड़े।

व्याख्या:

Session replication के लिए नवीनतम Servlet Specification में भी Java Serialization अब भी अनिवार्य है: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

एक आम use case: अगर load balancer के पीछे कई servers हैं और आप एक server पर login करते हैं, तो आपको एक JSESSIONID cookie मिलती है। अगर load balancer अगली request के लिए आपको किसी दूसरे server पर भेजता है, तो आपके login attributes और बाकी session-scoped attributes serialize होकर नए server पर replicate हो जाते हैं, ताकि आपका JSESSIONID वहाँ भी valid रहे।

एक और use case, जिसमें session-scoped attributes के लिए Serialization चाहिए, तब आता है जब application server sessions को disk पर swap करने का फ़ैसला करता है। उदाहरण के लिए, Tomcat 8 में इसे ऐसे configure किया जा सकता है कि यह तब activate हो, जब memory में रखने के लिए sessions बहुत ज़्यादा हों, या जब server restart हो और आप sessions को बनाए रखना चाहें। देखें https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

हर Servlet-5.0 compatible application server को ऐसे use cases के लिए HTTP sessions में Serializable objects को default रूप से support करना ज़रूरी है।

कुछ servers पर इसे बदला या override किया जा सकता है, जैसे Jackson-JSON serialization या किसी मिलते-जुलते तरीके से। उदाहरण के लिए देखें https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

एक और आम use case है caching। इसका इस्तेमाल objects को जितना हो सके उतनी तेज़ी से उपलब्ध रखने के लिए होता है – आदर्श रूप से memory में, लेकिन ज़रूरत पड़ने पर अस्थायी रूप से disk या किसी external cache server पर offload करके।
इसके लिए एक standard API (JSR-107) है, जिसने जान-बूझकर Serializable objects को ज़रूरी नहीं बनाया, लेकिन ज़्यादातर implementations offloading वाले हिस्से के लिए default रूप से अब भी Java Serialization ही इस्तेमाल करते हैं।

इन्हें भी दूसरे serialization mechanisms को support करने के लिए बदला जा सकता है, जैसे ehcache के लिए: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: Fetching strategy

EAGER fetching is a code smell को देखते हुए – जिसे एक बेहद करिश्माई होशियारचंद ने लिखा है, जो Hibernate contributor भी है – FetchType के रूप में LAZY को प्राथमिकता देनी चाहिए। भले ही कुछ data types foreign key के बिना कोई मतलब न रखते हों, फिर भी इन types को अकेले query किया ही जाता है (getCount(), data tables वगैरह)।

यहाँ सारे scenarios इस क्रम में हैं कि समाधान कितना संतोषजनक है – सबसे कम संतोषजनक से शुरू करके:

1. सबसे बुरी स्थिति: LazyInitializationException

कुछ कहने की ज़रूरत नहीं। यह दर्द हम सब जानते हैं।

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. Service में सीधे 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. Service में सीधे fetch करें (यही करें!)

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 से पूरी तरह जुड़ा हुआ है। Entities बदलने पर इसे पता चल जाता है, और इसे आसानी से 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;

    // ...

}

Local environment में search बंद रहता है:

// 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 का व्यवहार

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

Luke के साथ Lucene को debug करना

हमारे Hibernate Search version को Luke version 6.6.0 के साथ debug करना पड़ता है। यह सिर्फ़ इसी version के साथ काम करता है!

ElasticSearch

Hibernate Search, ElasticSearch को भी support करता है (तब यह Lucene की जगह इस्तेमाल होता है और इसमें बेहतर fuzzy search / ज़्यादा Google जैसा एहसास मिलता है)।