என் 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 சிறிய String-கள் மீது அடிக்கடி இயங்கினால், compile செய்த Pattern-ஐ மீண்டும் பயன்படுத்துவது performance-ஐக் கணிசமாக மேம்படுத்தும்.

2. Loop-இல் +-க்குப் பதிலாக StringBuilder

Loop-இல் +-ஐ விட new StringBuilder() அதிக performance தரும்.

3. equals(), hashCode()-ஐச் சரியாகப் பயன்படுத்துதல்

சில நேரங்களில் equals() அல்லது hashCode()-இல் எல்லா field-களும் சேர்க்கப்படுகின்றன. ஒரு entity மாற்றப்பட்டதா என்று சரிபார்க்க இது பயனுள்ளதுபோல் தோன்றும், ஆனால் இது தவறு. equals()-இன் நோக்கம் ஒரு entity மாற்றப்பட்டதா என்று சரிபார்ப்பது அல்ல; object instance வேறாக இருந்தாலும் அது அதே entity-தானா என்று கண்டறிவதுதான். அதாவது: Primary key-ஐ மட்டும் பயன்படுத்துங்கள்!

எல்லா வகை Collection-களுக்கும் இது தேவை. ஒரு List-இல் .contains()-ஐ அழைத்தால், Java equals method-ஐப் பயன்படுத்தும். HashMap-களுக்கு hashCode()-உம் தேவை. இரண்டையும் சேர்த்தே override செய்ய வேண்டும்.

ID இல்லாத, இன்னும் save செய்யப்படாத entity-கள் ஒரு பொதுவான பொறி. இங்கே கவனமாக இருங்கள்; சிக்கல்களைத் தவிர்க்க முன்கூட்டியே persist செய்யுங்கள்.

ஒப்பிட்டுப் பாருங்கள்:

இந்த method-களின் contract-ஐ எப்போதும் பின்பற்ற வேண்டும்:

4. வெவ்வேறு operating system-களைக் கலந்து பயன்படுத்தும்போது DateTime பொறிகள்

சில operating system-களின் உள் கடிகாரம் nanosecond அளவிலும், சிலவற்றினுடையது millisecond அளவிலும் இயங்குகிறது. இதனால் ZonedDateTime.now() இரண்டு platform-களிலும் ஒரே மாதிரி செயல்படாது; 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 method-களும் ZonedDateTime.now()-ஐ நம்பியுள்ளன; அவற்றை ஒருபோதும் அப்படியே (plain) பயன்படுத்தக் கூடாது:

5. சிக்கலான data type-களை inline-ஆக அறிவிக்காதீர்கள்

Pair, List போன்றவற்றை ஒன்றுக்குள் ஒன்றாக அடுக்குவதற்குப் பதிலாக, சிக்கலான data type-களுக்கு எப்போதும் தனி class-களை உருவாக்குங்கள்.

மோசம்:

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

6. Caching – ஆனால் சரியான முறையில் (குறைந்தபட்ச setup வழிகாட்டி)

Cache (உச்சரிப்பு /kæʃ/, ஆங்கில “cash” போல) அடிப்படையில் entry-களை வைத்திருக்கும் ஒரு Map மட்டுமே. நேர அடிப்படையிலான invalidation போன்ற சிக்கலான விஷயங்கள் இங்கே வேண்டுமென்றே தவிர்க்கப்பட்டுள்ளன.

ஒரு எளிய Map போதுமானதாக இருக்கும் இடத்தில் எப்போதும் library தேவையில்லை! பல developer-கள் இதைப் புரிந்துகொள்வதில்லை; பயத்தின் காரணமாக கொள்கையளவிலேயே library-களைப் பயன்படுத்துகிறார்கள். Caching-இல் மிக முக்கியமான அம்சம், சரியான நேரத்தில் invalidate செய்வதுதான்.

பொறி #1: Memory Leak

Cache செய்யும்போது Memory Leak மிக எளிதாக நேரலாம். equals(), hashCode()-ஐ ஒழுங்காக override செய்ய மறந்தாலே போதும் – உங்கள் HashMap memory-ஐ leak செய்யத் தொடங்கிவிடும். எப்போதும் பாதுகாப்பான ஒரு வழி: Map key-களைக் கைமுறையாக உருவாக்குவது. ஒரு entity-இன் ID-ஐ மட்டுமே சேர்ப்பது சிறந்தது; இல்லையென்றால் ஒரு field மாறினாலே Map leak ஆகிவிடும். அதுவும் சரியானதே, ஏனெனில் ஒரு User-இன் முதல் பெயர் மாறினாலும் அவர் அதே User-தான். ஒப்பிடுக: 3. equals(), hashCode()-ஐச் சரியாகப் பயன்படுத்துதல்

பொறி #2: Thread Safety

Cache-க்குச் சிறந்தது ஒரு ConcurrentHashMap; அது atomic, thread-safe operation-களை வழங்குகிறது. Map ஒரே ஒரு call மூலம் மட்டுமே மாற்றப்படுகிறது – 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());
}

Code இன்னும் நீளமாகவும் பல call-கள் கொண்டதாகவும் இருந்தால், method முழுவதையும் synchronized ஆக்க வேண்டியிருக்கும்.

7. Serialization – இன்னும் தேவையா?

என் கேள்வி: ஒரு Sonar விதி உள்ளது, அதன் பெயர் Fields in a “Serializable” class should either be transient or serializable. உங்கள் class-இல் serializable அல்லாத ஒரு field-ஐச் சேர்க்கும்போது இது தோன்றும். அந்த விதி இப்படிச் சொல்கிறது:

எடுத்துக்காட்டாக, அதிக load இருக்கும்போது பெரும்பாலான J2EE application framework-கள் object-களை disk-க்கு flush செய்கின்றன; transient அல்லாத, serializable அல்லாத data member-களைக் கொண்ட, Serializable எனப்படும் ஒரு object program crash-களை உண்டாக்கலாம், தாக்குதல் நடத்துபவர்களுக்குக் கதவைத் திறந்துவிடலாம்.

இது எனக்குக் காலாவதியான கருத்தாகத் தோன்றுகிறது. புதிய மென்பொருளை உருவாக்கும்போது Serialization பொருத்தமற்றது என்று Effective Java 3rd Edition கூறுகிறது. மேலும், Java-விலிருந்து Serialization-ஐ நீக்குவது Oracle-இன் ‘நீண்டகால இலக்கு’. Java-வின் தலைமைக் கட்டமைப்பாளர் Mark Reinhold-ஏ இப்படிக் கூறியுள்ளார்:

“1997-இல் Serialization ஒரு பயங்கரமான தவறு.”

Spring framework object-களை flush செய்யுமா என்ற கேள்வி இங்கே ஏற்கெனவே கேட்கப்பட்டுள்ளது:

எனவே என் கேள்வி:

நவீன application server-களோ, framework-களோ, அல்லது J2EE implementation-ஏ (குறிப்பாக CDI, EJB, JPA-வுடன் இணைந்து செயல்படும் Java component-கள்) அதிக load இருக்கும்போது object-களை disk-க்கு flush செய்கின்றனவா?

அல்லது இந்தக் கூற்று காலாவதியாகிவிட்டதா?

பதில்:

TL;DR நீங்கள் stateful distributable web application-ஐ உருவாக்கவில்லை, session persistence-ஐ இயக்கவில்லை, in-memory cache-களை மட்டுமே பயன்படுத்துகிறீர்கள் என்றால், எங்குமே Serializable இல்லாமலேயே சமாளித்துவிடலாம்.

விளக்கம்:

Session replication-க்கு, சமீபத்திய Servlet Specification-இலும் Java Serialization இன்னும் கட்டாயம்: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

ஒரு பொதுவான பயன்பாடு: ஒரு load balancer-க்குப் பின்னால் பல server-கள் இருக்கின்றன; நீங்கள் ஒரு server-இல் login செய்தால் ஒரு JSESSIONID cookie கிடைக்கும். அடுத்த request-இல் load balancer உங்களை வேறொரு server-க்கு அனுப்பினால், உங்கள் login attribute-களும் பிற session-scoped attribute-களும் serialize செய்யப்பட்டு புதிய server-இல் replicate செய்யப்படும்; இதனால் அங்கும் உங்கள் JSESSIONID செல்லுபடியாகும்.

உங்கள் session-scoped attribute-களுக்கு Serialization தேவைப்படும் இன்னொரு சூழல்: application server, session-களை disk-க்கு swap செய்ய முடிவெடுக்கும்போது. எடுத்துக்காட்டாக Tomcat 8-இல், memory-இல் வைத்திருக்க முடியாத அளவுக்கு session-கள் அதிகமாகும்போதோ, server restart ஆன பிறகும் session-களை உயிர்ப்புடன் வைத்திருக்க விரும்பும்போதோ இது இயங்கும்படி அமைக்கலாம். பார்க்க: https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

இத்தகைய பயன்பாடுகளுக்காக, Servlet-5.0 உடன் இணக்கமான ஒவ்வொரு application server-உம் இயல்பாகவே Http Session-களில் Serializable object-களை ஆதரிக்க வேண்டும்.

சில server-களில் இதை மாற்றியமைக்கலாம் அல்லது override செய்யலாம் – எடுத்துக்காட்டாக Jackson-JSON serialization அல்லது அதுபோன்ற ஒன்றைப் பயன்படுத்தி. பார்க்க: https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

இன்னொரு பொதுவான பயன்பாடு caching. Object-களை முடிந்தவரை வேகமாக அணுகக்கூடியதாக வைத்திருக்க இது பயன்படுகிறது – இயன்றால் memory-இல், ஆனால் தேவைப்பட்டால் தற்காலிகமாக disk-க்கோ ஒரு வெளி cache server-க்கோ மாற்றி.
இதற்கு ஒரு standard API (JSR-107) உள்ளது; அது வேண்டுமென்றே Serializable object-களைக் கட்டாயமாக்கவில்லை, ஆனாலும் பெரும்பாலான implementation-கள் offloading பகுதிக்கு இயல்பாக Java Serialization-ஐயே பயன்படுத்துகின்றன.

இவற்றையும் வேறு serialization முறைகளை ஆதரிக்கும்படி மாற்றியமைக்கலாம், எடுத்துக்காட்டாக ehcache-க்கு: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: Fetching உத்தி

EAGER fetching is a code smell என்ற கட்டுரையைக் கருத்தில் கொண்டால் – அதை எழுதியவர் அபாரமான கவர்ச்சி கொண்ட, எல்லாம் தெரிந்தவர்போல் பேசும் ஒருவர்; Hibernate contributor-உம் கூட – FetchType-ஆக LAZY-ஐத் தேர்வுசெய்வதே நல்லது. சில data type-கள் foreign key இல்லாமல் அர்த்தமற்றவையாக இருந்தாலும், அந்த type-கள் தனியாகவும் query செய்யப்படுகின்றன (getCount(), Data-Table-கள் போன்றவை).

தீர்வு எந்த அளவுக்குத் திருப்திகரமானது என்ற வரிசையில் எல்லாச் சூழல்களும் கீழே உள்ளன – மிகக் குறைவாகத் திருப்தி தருவதிலிருந்து தொடங்கி:

1. மிக மோசமான நிலை: LazyInitializationException

வேறு எதுவும் சொல்லத் தேவையில்லை. இந்த வலி நம் எல்லோருக்கும் தெரிந்ததே.

2. அவசரத் தீர்வு

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-வுடன் தடையின்றி இணைந்துள்ளது. Entity-கள் மாறும்போது அது அறிந்துகொள்கிறது; எளிதாக 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 சூழலில் 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 & தேடல் நடத்தை

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 பதிப்பை Luke 6.6.0 பதிப்பைக் கொண்டுதான் debug செய்ய வேண்டும். இந்தப் பதிப்பில் மட்டுமே அது வேலை செய்கிறது!

ElasticSearch

Hibernate Search, ElasticSearch-ஐயும் ஆதரிக்கிறது (அப்போது அது Lucene-க்குப் பதிலாகப் பயன்படுத்தப்படும்; அதன் fuzzy search இன்னும் சிறப்பானது / Google போன்ற அனுபவம் அதிகம்).