আমার Best Practices – Java

১. লুপে String.replaceAll()-এর বদলে Pattern.compile()

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

Pattern-টি সবসময় আগে কম্পাইল করতে হয়। একই Pattern যদি ছোট ছোট String-এর ওপর বারবার চলে, তবে কম্পাইল করা Pattern পুনরায় ব্যবহার করলে পারফরম্যান্স অনেক ভালো হয়।

২. লুপে +-এর বদলে StringBuilder

লুপে +-এর বদলে new StringBuilder() বেশি পারফরম্যান্ট।

৩. equals() ও hashCode()-এর সঠিক ব্যবহার

কখনো কখনো equals() বা hashCode()-এ সব ফিল্ড অন্তর্ভুক্ত করা হয়। কোনো entity পরিবর্তিত হয়েছে কি না যাচাই করতে এটি কাজের মনে হয়, কিন্তু ভুল। equals()-এর উদ্দেশ্য কোনো entity পরিবর্তিত হয়েছে কি না যাচাই করা নয়, বরং অবজেক্ট ইনস্ট্যান্স আলাদা হলেও সেটি একই entity কি না তা নির্ধারণ করা। অর্থাৎ: শুধু primary key ব্যবহার করুন!

যেকোনো ধরনের Collection-এর জন্য এটি দরকার। কোনো List-এ .contains() কল করলে Java equals মেথড ব্যবহার করে। HashMap-এর জন্য আবার hashCode()-ও লাগে। দুটিকে একসাথে override করা উচিত।

একটি সাধারণ ফাঁদ হলো ID ছাড়া, এখনো সেভ না করা entity। এখানে সাবধান থাকুন এবং সমস্যা এড়াতে আগেভাগেই persist করুন।

তুলনা করুন:

এই মেথডগুলোর contract সবসময় মেনে চলতে হবে:

৪. বিভিন্ন অপারেটিং সিস্টেম মিশিয়ে চালালে DateTime-এর ফাঁদ

কিছু অপারেটিং সিস্টেমের অভ্যন্তরীণ ঘড়ি ন্যানোসেকেন্ডে চলে, কিছু মিলিসেকেন্ডে। ফলে ZonedDateTime.now() দুই প্ল্যাটফর্মে একইভাবে কাজ করে না এবং equals() ভেঙে দেয়। তাই সবসময় precision ঠিক করে নিন!

/**
 * 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 মেথডগুলোও ZonedDateTime.now()-এর ওপর নির্ভর করে, তাই এগুলো কখনো সরাসরি ব্যবহার করা উচিত নয়:

৫. জটিল ডেটা টাইপ inline ঘোষণা করবেন না

Pair, List ইত্যাদি একটার ভেতর আরেকটা না গুঁজে জটিল ডেটা টাইপের জন্য সবসময় আলাদা ক্লাস তৈরি করুন।

খারাপ:

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

সুবিধা:

ব্যতিক্রম:

সহজ সম্পর্কের জন্য Pair দারুণ! যেমন:

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

৬. ক্যাশিং – কিন্তু সঠিকভাবে (ন্যূনতম সেটআপ গাইড)

ক্যাশ (উচ্চারণ /kæʃ/, ইংরেজি “cash”-এর মতো) মূলত একটি Map ছাড়া কিছু নয়, যা এন্ট্রিগুলো ধরে রাখে। সময়ভিত্তিক invalidation-এর মতো জটিল বিষয় এখানে ইচ্ছে করেই বাদ রাখা হয়েছে।

যেখানে একটি সাধারণ Map যথেষ্ট, সেখানে সবসময় লাইব্রেরির দরকার নেই! অনেক ডেভেলপার এটা বোঝেন না এবং ভয় থেকে নীতিগতভাবেই লাইব্রেরি ব্যবহার করেন। ক্যাশিংয়ের সবচেয়ে গুরুত্বপূর্ণ দিক হলো সঠিক সময়ে invalidate করা।

ফাঁদ #১: Memory Leak

ক্যাশ করার সময় খুব সহজেই Memory Leak ঘটতে পারে। equals() ও hashCode() ঠিকমতো override করতে ভুলে গেলেই হলো – আপনার HashMap মেমরি লিক করতে শুরু করবে। একটি পদ্ধতি সবসময় নিরাপদ: Map-এর key নিজ হাতে তৈরি করা। শুধু entity-র ID অন্তর্ভুক্ত করাই সবচেয়ে ভালো, নইলে কোনো একটি ফিল্ড বদলালেই Map লিক করতে শুরু করে। আর সেটাই যুক্তিসঙ্গত, কারণ কোনো User-এর নাম বদলালেও সে একই User থাকে। তুলনা করুন: ৩. equals() ও hashCode()-এর সঠিক ব্যবহার

ফাঁদ #২: Thread Safety

ক্যাশে সবচেয়ে ভালো হয় একটি ConcurrentHashMap ব্যবহার করলে, যা অ্যাটমিক, thread-safe অপারেশন দেয়। Map শুধু একটিমাত্র কলের মাধ্যমে পরিবর্তিত হয় – computeIfAbsent() – আর এভাবেই 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());
}

কোড আরও লম্বা হলে এবং একাধিক কল থাকলে পুরো মেথডটিকে synchronized করতে হতো।

৭. সিরিয়ালাইজেশন – এখনো কি এর দরকার আছে?

আমার প্রশ্ন: একটি Sonar রুল আছে, যার নাম Fields in a “Serializable” class should either be transient or serializable। ক্লাসে কোনো non-serializable ফিল্ড যোগ করলে এটি দেখা দেয়। রুলটি বলে:

উদাহরণস্বরূপ, অধিক লোডে বেশিরভাগ J2EE অ্যাপ্লিকেশন ফ্রেমওয়ার্ক অবজেক্টগুলো ডিস্কে flush করে, আর non-transient, non-serializable ডেটা মেম্বারযুক্ত একটি তথাকথিত Serializable অবজেক্ট প্রোগ্রাম ক্র্যাশ ঘটাতে পারে এবং আক্রমণকারীদের জন্য দরজা খুলে দিতে পারে।

আমার কাছে এটা সেকেলে কথা বলে মনে হয়। Effective Java 3rd Edition-এ বলা হয়েছে, নতুন সফটওয়্যার তৈরির সময় সিরিয়ালাইজেশন প্রাসঙ্গিক নয়। তাছাড়া Java থেকে সিরিয়ালাইজেশন সরিয়ে ফেলা Oracle-এর একটি ‘দীর্ঘমেয়াদি লক্ষ্য’। Java-র প্রধান আর্কিটেক্ট Mark Reinhold নিজেই বলেছেন

“১৯৯৭ সালে সিরিয়ালাইজেশন ছিল এক ভয়াবহ ভুল।”

Spring ফ্রেমওয়ার্ক অবজেক্ট flush করতে পারে কি না, সেই প্রশ্ন এখানে আগেই করা হয়েছিল:

তাই আমার প্রশ্ন:

কোনো আধুনিক অ্যাপ্লিকেশন সার্ভার, ফ্রেমওয়ার্ক বা খোদ J2EE ইমপ্লিমেন্টেশন (বিশেষ করে CDI, EJB, JPA-র সাথে কাজ করে এমন Java কম্পোনেন্ট) কি অধিক লোডে অবজেক্ট ডিস্কে flush করে?

নাকি এই বক্তব্য সেকেলে হয়ে গেছে?

উত্তর:

TL;DR আপনি যদি stateful distributable ওয়েব অ্যাপ্লিকেশন তৈরি না করেন, session persistence চালু না করেন এবং কেবল in-memory ক্যাশ ব্যবহার করেন, তবে হয়তো কোথাও Serializable ছাড়াই দিব্যি চালিয়ে নিতে পারবেন।

ব্যাখ্যা:

Session replication-এর জন্য সর্বশেষ Servlet Specification-এও Java সিরিয়ালাইজেশন এখনো অপরিহার্য: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

একটি সাধারণ ব্যবহার: ধরুন একটি load balancer-এর পেছনে একাধিক সার্ভার আছে, আর আপনি একটি সার্ভারে লগইন করলে একটি JSESSIONID কুকি পান। load balancer যদি পরের রিকোয়েস্টে আপনাকে অন্য সার্ভারে পাঠায়, তবে আপনার লগইন অ্যাট্রিবিউট ও অন্যান্য session-scoped অ্যাট্রিবিউট সিরিয়ালাইজ হয়ে নতুন সার্ভারে রেপ্লিকেট হবে, ফলে সেখানেও আপনার JSESSIONID বৈধ থাকবে।

আরেকটি ক্ষেত্রে আপনার session-scoped অ্যাট্রিবিউটের জন্য সিরিয়ালাইজেশন দরকার হয়: যখন অ্যাপ্লিকেশন সার্ভার session-গুলো ডিস্কে swap করার সিদ্ধান্ত নেয়। যেমন Tomcat 8-এ এটি এমনভাবে কনফিগার করা যায় যে মেমরিতে রাখার তুলনায় session বেশি হয়ে গেলে, অথবা সার্ভার রিস্টার্টের পরও session সচল রাখতে চাইলে এটি চালু হয়। দেখুন https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

প্রতিটি Servlet-5.0-সামঞ্জস্যপূর্ণ অ্যাপ্লিকেশন সার্ভারকে এই ধরনের ব্যবহারের জন্য ডিফল্টভাবে Http Session-এ Serializable অবজেক্ট সাপোর্ট করতেই হবে।

কিছু সার্ভারে এটি বদলানো বা override করা যায়, যেমন Jackson-JSON সিরিয়ালাইজেশন বা এ ধরনের কিছু ব্যবহার করে। উদাহরণ হিসেবে দেখুন https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

আরেকটি সাধারণ ব্যবহার হলো ক্যাশিং। এর উদ্দেশ্য অবজেক্টগুলো যত দ্রুত সম্ভব অ্যাক্সেসযোগ্য রাখা – আদর্শভাবে মেমরিতে, তবে প্রয়োজনে সাময়িকভাবে ডিস্কে বা কোনো এক্সটার্নাল ক্যাশ সার্ভারে সরিয়ে রেখে।
এর জন্য একটি স্ট্যান্ডার্ড API (JSR-107) আছে, যা ইচ্ছে করেই Serializable অবজেক্ট বাধ্যতামূলক করেনি, কিন্তু বেশিরভাগ ইমপ্লিমেন্টেশন এখনো offloading অংশের জন্য ডিফল্ট হিসেবে Java সিরিয়ালাইজেশনই ব্যবহার করে।

এগুলোকেও অন্য সিরিয়ালাইজেশন পদ্ধতি সাপোর্ট করার জন্য বদলে নেওয়া যায়, যেমন ehcache-এর ক্ষেত্রে: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

৮. Hibernate: Fetching কৌশল

EAGER fetching is a code smell লেখাটি বিবেচনায় নিলে – যার লেখক এক দারুণ ক্যারিশম্যাটিক সবজান্তা এবং Hibernate-এর কন্ট্রিবিউটর – FetchType হিসেবে LAZY বেছে নেওয়াই শ্রেয়। কিছু ডেটা টাইপ ফরেন কি ছাড়া অর্থহীন হলেও, এই টাইপগুলোকে প্রায়ই আলাদাভাবেই query করা হয় (getCount(), Data-Table ইত্যাদি)।

নিচে সব পরিস্থিতি সমাধানটি কতটা সন্তোষজনক সেই অনুযায়ী সাজানো হলো, সবচেয়ে কম সন্তোষজনকটি দিয়ে শুরু:

১. সবচেয়ে খারাপ অবস্থা: LazyInitializationException

আর কিছু বলার দরকার নেই। এই যন্ত্রণা আমাদের সবার চেনা।

২. তাৎক্ষণিক সমাধান

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

৩. সরাসরি সার্ভিসেই 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 ???; 
}

৪. সরাসরি সার্ভিসেই 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);
        });
}

৯. Hibernate Search

Hibernate Search, Hibernate JPA-র সাথে নির্বিঘ্নে যুক্ত। entity পরিবর্তন হলে এটি টের পায় এবং সহজেই কনফিগার করা যায়:

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

    // ...

}

লোকাল পরিবেশে সার্চ বন্ধ থাকে:

// 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 সার্চ ও সার্চের আচরণ

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 ডিবাগ করা

আমাদের Hibernate Search ভার্সনটি Luke-এর 6.6.0 ভার্সন দিয়েই ডিবাগ করতে হবে। এটি শুধু এই ভার্সনেই কাজ করে!

ElasticSearch

Hibernate Search, ElasticSearch-ও সাপোর্ট করে (তখন এটি Lucene-এর বদলে ব্যবহৃত হয় এবং এর fuzzy সার্চ আরও ভালো / অনেকটা Google-এর মতো অনুভূতি দেয়)।