Spring Boot & Hazelcast: Distributed Caching සහ Clustering SC Guide - Sri Lanka

කොහොමද යාලුවනේ! ❤️
අද අපි කතා කරන්න යන්නේ ඔයාලා හැමෝටම ගොඩක් වැදගත් වෙන, ඒ වගේම අපේ Spring Boot Applications වල Performance එකයි, Scalability එකයි දියුණු කරගන්න පුළුවන් සුපිරි Topic එකක් ගැන – ඒ තමයි Distributed Caching.
දැන් ඔයාලා හිතනවා ඇති, "Caching කියන්නේ මොකක්ද කියලා අපි දන්නවනේ, ඕක මොකක්ද අලුතෙන් Distributed වෙලා?" කියලා. ඔව්, Caching කියන්නේ Application එකක Data නැවත නැවතත් Database එකට යන්නැතුව, ඉක්මනින්ම Access කරගන්න පුළුවන් විදියට Memory එකේ තියාගන්න එකනේ. ඒත්, අපේ Application එක Single instance එකකින් එහාට ගිහින්, Microservices Architecture එකකට එහෙම යනකොට, Cluster එකක් විදියට Run වෙනකොට මොකද කරන්නේ? එතනදී තමයි Distributed Caching අපිට ලොකු හයියක් වෙන්නේ.
විශේෂයෙන්ම, මේ වැඩේට අපිට ලොකු සපෝට් එකක් දෙන, In-Memory Data Grid (IMDG) Solution එකක් වෙන Hazelcast කොහොමද Spring Boot එක්ක වැඩ කරන්නේ කියලා අපි අද බලමු. සරලවම කිව්වොත්, Hazelcast කියන්නේ අපේ Data distributed විදියට ගබඩා කරලා තියාගන්න, ඒ වගේම Cluster එකක තියෙන හැම Node එකකටම එකම Data Set එක Access කරන්න පුල්වන් කරන Platform එකක්.
ඉතින්, ඔයාලා Ready ද? එහෙනම්, අපි පටන් ගමු!
Distributed Caching කියන්නේ මොකක්ද? (What is Distributed Caching?)
අපි මුලින්ම බලමු මේ Distributed Caching කියන concept එකේ තියෙන වැදගත්කම මොකක්ද කියලා.
සාමාන්යයෙන් අපේ Application එකක Data අවශ්ය වුණාම, අපි Database එකට Request එකක් යවනවා. මේක හැම Request එකකටම වුණොත්, Database එකට ලොකු Load එකක් එනවා. ඒ වගේම, Data Retrieve කරන්න යන Time එකත් වැඩියි. මේකට විසඳුමක් විදියට තමයි අපි Caching භාවිතා කරන්නේ. Caching කරාම, නිතර නිතර අවශ්ය වෙන Data, Application Server එකේ Memory එකේ තියාගෙන, Database එකට නොගිහින්ම ඉක්මනින් ලබා දෙන්න පුළුවන් වෙනවා.
හැබැයි, මේක Single Server Environment එකකට හොඳයි. ඒත්, ඔයාලගේ Application එක Load Balancer එකක් පස්සේ, Servers කිහිපයක (Cluster එකක් විදියට) Run වෙනවා නම් මොකද කරන්නේ? හිතන්න, User Data එකක් Server 1 එක Cache කරගත්තා කියලා. ඒත් ඊලඟ Request එක Server 2 එකට ගියාම, ඒකේ මේ Data එක Cache වෙලා නෑ. ඒ නිසා ආපහු Database එකට යන්න වෙනවා. මේක තමයි අපිට තියෙන ලොකුම ගැටළුවක් වෙන්නේ.
මෙන්න මේ වෙලාවේදී තමයි Distributed Caching වල වටිනාකම තේරෙන්නේ. Distributed Cache එකක් කියන්නේ, Cache එක Servers කිහිපයක් අතරේ බෙදිලා තියෙන එකක්. මේකෙන් වෙන්නේ, එක Server එකක Cache වුණ Data එක, Cluster එකේ ඉන්න අනිත් Servers වලටත් Access කරන්න පුළුවන් වෙන එක. ඒ කියන්නේ, Server 1 එක Cache කරපු Data එක, Server 2 එකටත්, Server 3 එකටත් තේරෙනවා. මේකෙන්:
- Performance වැඩි වෙනවා: Database එකට යන Requests ගාන අඩු වෙන නිසා, Data Retrieve කරන වේගය වැඩි වෙනවා.
- Scalability වැඩි වෙනවා: අවශ්ය වුණොත් තවත් Servers Add කරලා, Load එක බෙදාගන්න පුළුවන්. Cache එකත් ඒ අනුව Expand වෙනවා.
- Fault Tolerance: එක Server එකක් Down වුණත්, Cache එකේ Data නැති වෙන්නේ නෑ. මොකද Data එක Cluster එකේ තියෙන අනිත් Servers වලත් තියෙන නිසා.
- Consistency: Cluster එකේ හැම Node එකකටම එකම Cache එකේ Data Set එක පේන නිසා, Data Inconsistency ගැටළු අඩු වෙනවා.
ඉතින්, මේවා තමයි Distributed Caching වල ප්රධානම වාසි.
Hazelcast Introduction - අපේ Solution එක (Our Solution)
Distributed Caching වලට විවිධ Solutions තියෙනවා. ඒ අතරින්, Hazelcast කියන්නේ Open-Source, In-Memory Data Grid (IMDG) එකක්. මේක Java වලින් ලියලා තියෙන්නේ, ඒ නිසා Java Applications වලට ගොඩක් හොඳින් ගැලපෙනවා. Hazelcast වල තියෙන විශේෂත්වය තමයි, මේකේ Data memory එකේ තියාගෙන distributed විදියට manage කරන එක. ඒ කියන්නේ, අපේ Application එක Run වෙන Server වලම memory එක භාවිතා කරලා, Cluster එකක් හදාගෙන Data ගබඩා කරගන්න පුළුවන්.
Hazelcast වල ප්රධාන Feature කිහිපයක් මෙන්න:
- Distributed Data Structures:
IMap
,IQueue
,ISet
,IList
,ICountDownLatch
,ISemaphore
වගේ Standard Java Data Structures වල Distributed Versions Hazelcast වල තියෙනවා. මේවා Cluster එකේ හැම Node එකකටම Access කරන්න පුළුවන්. - Distributed Caching: Caching වලට මූලිකවම
IMap
භාවිතා කරන්න පුළුවන්. Spring Cache Abstraction එකත් එක්ක ලේසියෙන්ම integrate කරන්න පුළුවන්. - Clustering: Hazelcast Nodes Auto-Discovery Features වලින් Cluster එකක් හදාගන්නවා. Multicast හෝ TCP/IP Configuration භාවිතා කරන්න පුළුවන්.
- High Performance & Scalability: In-memory operations නිසා ඉතාම වේගවත්. අවශ්ය විදියට Nodes Add කරලා Scalability වැඩි කරගන්න පුළුවන්.
- Fault Tolerance & High Availability: Data backup Features නිසා, එක Node එකක් Fail වුණත් Data නැති වෙන්නේ නෑ.
දැන් ඔයාලට තේරෙනවා ඇතිනේ Hazelcast කියන්නේ මොන වගේ කෙනෙක්ද කියලා. Spring Boot එක්ක වැඩ කරනකොට, මේක ඇත්තටම කෝකටත් තෛලයක් වගේ.
Spring Boot එක්ක Hazelcast ගලපමු! (Integrating Hazelcast with Spring Boot)
හරි, දැන් අපි බලමු කොහොමද අපේ Spring Boot Project එකකට Hazelcast integrate කරලා Distributed Caching implement කරන්නේ කියලා. මේක හිතනවට වඩා ගොඩක් ලේසියි.
1. Dependencies Add කරගමු (Add Dependencies)
මුලින්ම ඔයාලගේ Maven (pom.xml) හෝ Gradle (build.gradle) Project එකට අවශ්ය Dependencies Add කරගන්න ඕනේ.
Maven:
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.3.6</version> <!-- Latest stable version -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Gradle:
implementation 'com.hazelcast:hazelcast-spring:5.3.6' // Latest stable version
implementation 'org.springframework.boot:spring-boot-starter-cache'
මෙහිදී, hazelcast-spring
එකෙන් Spring Framework එකත් එක්ක Hazelcast Integration එකට අවශ්ය සියලු දේ ලබා දෙනවා. spring-boot-starter-cache
එකෙන් Spring's Cache Abstraction එකට Support එක දෙනවා.
2. Caching Enable කරමු (Enable Caching)
ඔයාලගේ Main Application Class එකට @EnableCaching
Annotation එක Add කරන්න ඕනේ. මේකෙන් තමයි Spring Cache Abstraction එක Activate කරන්නේ.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class HazelcastCachingDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HazelcastCachingDemoApplication.class, args);
}
}
3. Hazelcast Configuration (Hazelcast Configuration)
Hazelcast Instance එකක් Spring Boot Project එකට Configure කරන්න ක්රම කිහිපයක් තියෙනවා. සරලම ක්රමය තමයි application.yml
හෝ application.properties
භාවිතා කරන එක. තවත් ක්රමයක් තමයි Java Configuration Class එකක් භාවිතා කරන එක.
Method 1: application.yml/properties හරහා
මෙන්න සරල application.yml
Configuration එකක්:
spring:
cache:
type: hazelcast
hazelcast:
config:
network:
join:
multicast:
enabled: true # Clustering via multicast, suitable for local development/LAN
tcp-ip:
enabled: false
members: # If multicast is false, specify members here (e.g., 192.168.1.10:5701, 192.168.1.11:5701)
- 127.0.0.1:5701 # For local testing, you can run multiple instances on different ports
map:
my-user-cache: # Define a cache named 'my-user-cache'
backup-count: 1 # Number of backups for resilience
time-to-live-seconds: 300 # Cache entries expire after 300 seconds (5 minutes)
eviction-policy: LRU # Least Recently Used eviction policy
max-size:
value: 1000 # Max 1000 entries
policy: PER_NODE
මෙහිදී, අපි spring.cache.type
එක hazelcast
විදියට සෙට් කරනවා. ඊට පස්සේ spring.hazelcast.config
යටතේ Hazelcast Cluster එකට අදාළ Network Configuration එක සහ Caching කරන්න යන Maps වල Configuration එක දෙනවා. multicast
කියන්නේ Nodes Automatic-Discovery කරගන්න ලේසිම ක්රමය. ඒත් Production වලට වගේ නම් tcp-ip
භාවිතා කරන එක වඩා ආරක්ෂිතයි.
Method 2: Java Configuration Class එකක් හරහා
වැඩි Control එකක් අවශ්ය නම්, Java Configuration Class එකක් භාවිතා කරන්න පුළුවන්:
import com.hazelcast.config.Config;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MaxSizeConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HazelcastConfig {
@Bean
public Config hazelcastConfiguration() {
Config config = new Config();
config.setInstanceName("hazelcast-instance"); // Instance name
// Network Configuration
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(5701).setPortAutoIncrement(true); // Default port and auto increment if busy
networkConfig.getJoin().getMulticastConfig().setEnabled(true); // Enable multicast for discovery
// Alternatively, use TCP/IP for specific member list:
// networkConfig.getJoin().getMulticastConfig().setEnabled(false);
// networkConfig.getJoin().getTcpIpConfig().setEnabled(true)
// .addMember("127.0.0.1:5701")
// .addMember("127.0.0.1:5702"); // Add more members if needed
// Define Cache Map configurations
MapConfig userCacheMapConfig = new MapConfig("my-user-cache"); // Cache name
userCacheMapConfig.setTimeToLiveSeconds(300); // 5 minutes
userCacheMapConfig.setEvictionPolicy(EvictionPolicy.LRU); // Least Recently Used
userCacheMapConfig.setMaxSizeConfig(new MaxSizeConfig(1000, MaxSizeConfig.MaxSizePolicy.PER_NODE)); // Max 1000 entries per node
userCacheMapConfig.setBackupCount(1); // 1 backup copy for each partition
config.getMapConfigs().put("my-user-cache", userCacheMapConfig);
return config;
}
// This bean isn't strictly necessary if Spring Boot auto-configures based on the Config bean,
// but useful if you need direct access to the HazelcastInstance.
// @Bean
// public HazelcastInstance hazelcastInstance(Config hazelcastConfiguration) {
// return Hazelcast.newHazelcastInstance(hazelcastConfiguration);
// }
}
මේ Config Class එකෙන් අපිට Hazelcast Instance එකක් අපේ අවශ්යතා අනුව customize කරන්න පුළුවන්. my-user-cache
කියන Map එකේ Settings අපි මෙතනදී Define කරනවා. මේ Map එක තමයි අපි අපේ Data Cache කරන්න භාවිතා කරන්නේ.
ප්රායෝගික උදාහරණයක් - Distributed User Cache (Practical Example - Distributed User Cache)
හරි, දැන් අපි මේ Caching Implement කරන්නේ කොහොමද කියලා සරල උදාහරණයක් එක්ක බලමු. අපි User Management Service එකක් හදමු.
1. User Model එක හදමු (Create User Model)
import java.io.Serializable; // Important for distributed objects
public class User implements Serializable { // Implement Serializable for network transfer
private Long id;
private String name;
private String email;
// Constructors
public User() {
}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
වැදගත් පොයින්ට් එකක්: Distributed Caching වලට භාවිතා කරන Objects අනිවාර්යයෙන්ම Serializable
වෙන්න ඕනේ. මොකද මේවා Network එක හරහා Nodes අතරේ Share වෙන නිසා.
2. User Service එක හදමු (Create User Service)
මෙතනදී තමයි අපි Spring Cache Annotations භාවිතා කරන්නේ.
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class UserService {
// Simulating a database
private Map<Long, User> users = new HashMap<>();
private AtomicLong idCounter = new AtomicLong();
@PostConstruct
public void init() {
// Populate with some initial data
saveUser(new User(idCounter.incrementAndGet(), "Kasun Perera", "[email protected]"));
saveUser(new User(idCounter.incrementAndGet(), "Amali Fernando", "[email protected]"));
saveUser(new User(idCounter.incrementAndGet(), "Nimal Bandara", "[email protected]"));
}
// @Cacheable: Caches the result of a method call. If the data is in the cache, it's returned immediately.
// Otherwise, the method is executed, and its result is put into the cache.
@Cacheable(value = "my-user-cache", key = "#id")
public User getUserById(Long id) {
System.out.println("Fetching user from 'database' (simulated) for ID: " + id);
try {
Thread.sleep(1000); // Simulate network delay/DB call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return users.get(id);
}
// @CachePut: Updates the cache with the result of the method execution.
// The method is always executed, unlike @Cacheable. Useful for update operations.
@CachePut(value = "my-user-cache", key = "#user.id")
public User saveUser(User user) {
if (user.getId() == null) {
user.setId(idCounter.incrementAndGet());
}
System.out.println("Saving user to 'database' (simulated): " + user.getName());
users.put(user.getId(), user);
return user;
}
// @CacheEvict: Removes one or all entries from the cache. Useful for delete operations.
@CacheEvict(value = "my-user-cache", key = "#id")
public void deleteUser(Long id) {
System.out.println("Deleting user from 'database' (simulated) and evicting from cache for ID: " + id);
users.remove(id);
}
}
මෙහිදී, my-user-cache
කියන්නේ අපි කලින් Hazelcast Configuration එකේදී define කරපු Cache Map එකේ නම.
@Cacheable
: මේ Method එකෙන් Data retrieve කරනකොට, Data Cache එකේ තියෙනවනම් එතනින්ම දෙනවා. නැත්නම් Method එක Execute කරලා, Result එක Cache කරනවා.@CachePut
: මේ Method එක Execute වුණාම, Result එක Cache එකට දානවා (Update කරනවා). මේකCacheable
වගේ Data Cache එකේ තිබ්බට Method එක Execute වීම නවත්තන්නේ නෑ.@CacheEvict
: මේ Method එක Execute වුණාම, Cache එකෙන් අදාළ Entry එක (හෝ හැම Entry එකම) අයින් කරනවා.
3. REST Controller එකක් හදමු (Create REST Controller)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.saveUser(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id); // Ensure ID is set for update
return userService.saveUser(user);
}
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return "User with ID " + id + " deleted and cache evicted.";
}
}
4. Project එක Run කරලා බලමු (Run the Project and Test)
දැන් ඔයාලට පුළුවන් මේ Project එක Run කරන්න. ඒ සඳහා:
HazelcastCachingDemoApplication.java
එක Run කරන්න.- Console එක බලන්න. මුලින්ම Hazelcast Instance එක Start වෙනවා කියලා පෙන්නයි. Multicast enabled නම්,
Members [1]
වගේ දෙයක් පෙන්නයි. - ඔයාලගේ Browser එකේ හෝ Postman වගේ Tool එකක් භාවිතා කරලා
http://localhost:8080/users/1
වගේ Request එකක් යවන්න. - මුල් වතාවට Request එක යවනකොට, Console එකේ "Fetching user from 'database' (simulated) for ID: 1" කියලා පින්ට් වෙනවා පෙනෙයි. මොකද මේ Data Cache එකේ නෑනේ.
- ආපහු ඒ Request එකම යවන්න. දැන් Console එකේ අර "Fetching user from 'database'" කියලා පින්ට් වෙන්නේ නෑ. ඒ කියන්නේ, Data එක Cache එකෙන් ආවා කියන එකයි! වැඩේ ගොඩ!
Distributed Cache එකක් විදියට Test කරන්නේ කොහොමද?
දැන් මේක තමයි වැදගත්ම දේ. අපිට Distributed Cache එකක Power එක තේරෙන්නේ Servers කිහිපයක් Run කරලා බලනකොටනේ.
- මුලින් Run කරපු Application එකේ Process එක නවත්තන්න එපා.
- ඔයාලගේ IDE එකේ Run Configurations වලට ගිහින්, "Program arguments" යටතේ
--server.port=8081
වගේ දෙයක් Add කරලා, මේ Project එකේම තවත් Instance එකක් අලුත් Port එකකින් (උදා: 8081) Run කරන්න. - දැන් ඔයාලට Hazelcast Console එකේ පෙන්නයි
Members [2]
කියලා. ඒ කියන්නේ Nodes දෙකක් Cluster වෙලා කියලා. - මුලින්
http://localhost:8080/users/1
කියන එකට Request එකක් දාලා Cache කරන්න. Console එක බලන්න. - ඊට පස්සේ
http://localhost:8081/users/1
කියන එකට Request එකක් යවන්න. මේක දෙවෙනි Instance එක. පුදුමේ කියන්නේ, මේකෙන් "Fetching user from 'database'" කියලා පින්ට් වෙන්නේ නෑ! ඒ කියන්නේ 8080 Instance එකෙන් Cache කරපු Data එක 8081 Instance එකටත් Access කරන්න පුළුවන් වෙලා! ඔන්න ඔහොමයි Distributed Caching වැඩ කරන්නේ! - User එකක් Update කරන්න හෝ Delete කරන්න (
POST
,PUT
,DELETE
requests) ඒවත් Cache එකට බලපාන හැටි බලන්න.
Hazelcast Cluster එකක් හදමු! (Let's Create a Hazelcast Cluster!)
උඩින් අපි බැලුවා multicast
හරහා Hazelcast Nodes කොහොමද Automatic-Discovery වෙන්නේ කියලා. මේක Local Development වලට හොඳයි. හැබැයි Production Environment වලදී, Security සහ Reliability Issues නිසා TCP/IP
Discovery භාවිතා කරන එක වඩාත් සුදුසුයි.
TCP/IP
Configuration එකේදී ඔයාලට Cluster එකේ ඉන්න Nodes වල IP Addresses සහ Port අතින්ම Specify කරන්න වෙනවා. උදාහරණයක් විදියට, application.yml
එකේ මේ වගේ වෙනස්කමක් කරන්න පුළුවන්:
spring:
cache:
type: hazelcast
hazelcast:
config:
network:
join:
multicast:
enabled: false # Disable multicast
tcp-ip:
enabled: true # Enable TCP/IP
members:
- 192.168.1.10:5701 # IP and Port of the first node
- 192.168.1.11:5701 # IP and Port of the second node
- 192.168.1.12:5701 # And so on...
නැත්නම්, Java Config එකේදී:
import com.hazelcast.config.Config;
import com.hazelcast.config.NetworkConfig;
// ... other imports
@Configuration
public class HazelcastConfig {
@Bean
public Config hazelcastConfiguration() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(5701).setPortAutoIncrement(true);
networkConfig.getJoin().getMulticastConfig().setEnabled(false); // Disable multicast
networkConfig.getJoin().getTcpIpConfig().setEnabled(true)
.addMember("192.168.1.10:5701")
.addMember("192.168.1.11:5701")
.addMember("192.168.1.12:5701");
// ... rest of your map configs
return config;
}
}
මේ විදියට Configure කරාම, Hazelcast Nodes අදාළ IP Address සහ Ports වලට ගිහින් Cluster එකට Join වෙනවා. මේක Production Environments වලට ගොඩක් ආරක්ෂිත සහ predictable වෙනවා.
අවසන් වශයෙන් (Finally)
ඉතින් යාලුවනේ, අද අපි Spring Boot Application එකකට Hazelcast භාවිතා කරලා Distributed Caching implement කරන්නේ කොහොමද කියලා විස්තරාත්මකව බැලුවා. මේක අපේ Applications වල Performance එක, Scalability එක සහ Fault Tolerance එක වැඩි කරගන්න පුළුවන් සුපිරිම Solution එකක්. විශේෂයෙන්ම Microservices වගේ Architectures වලදී Distributed Caching කියන්නේ අනිවාර්යයෙන්ම අවශ්ය වෙන Feature එකක්.
ඔයාලට දැන් පුළුවන් මේ උදාහරණය ඔයාලගේ Project වලට අරගෙන, තවත් Experiment කරලා බලන්න. Caching Policies, Eviction Strategies, Backup Counts වගේ දේවල් එක්ක සෙල්ලම් කරලා තව ඉගෙන ගන්න පුළුවන්. මතක තියාගන්න, හැම Application එකකටම එකම Caching Strategy එක ගැලපෙන්නේ නෑ. ඒ නිසා ඔයාලගේ Specific Requirements වලට අනුව Configure කරගන්න එක වැදගත්.
මේ Article එක ඔයාලට ප්රයෝජනවත් වෙන්න ඇති කියලා හිතනවා. මොනවා හරි ප්රශ්න තියෙනවා නම්, පහළින් Comment කරන්න. ඔයාලගේ අදහස් දැනගන්නත් මම ආසයි. තවත් මේ වගේ වැදගත් Article එකකින් හම්බෙමු! Happy Coding! 🚀