Spring Boot Protobuf Integration SC Guide - කාර්යක්ෂම Data Serialization

Spring Boot Protobuf Integration SC Guide - කාර්යක්ෂම Data Serialization

කොහොමද යාලුවනේ! Tech ගමනේ ඉන්න ඔයාලා හැමෝටම සුභ දවසක්! අද අපි කතා කරන්න යන්නේ ටිකක් වෙනස්, හැබැයි software development වලදී මාර විදියට වැදගත් වෙන මාතෘකාවක් ගැන. අපි හැමෝම API හදනකොට JSON පාවිච්චි කරනවානේ නේද? ඒක ගොඩක් වෙලාවට පහසුයි, කියවන්නත් ලේසියි. ඒත් හැම වෙලාවෙම JSON හොඳම විකල්පයද? සමහර වෙලාවට data size එක, performance එක වගේ දේවල් ගැන හිතනකොට, JSON අපිට හිතන තරම් උදව්වක් වෙන්නේ නෑ. විශේෂයෙන්ම microservices environment එකකදී, high-performance distributed systems වලදී, data transfer efficiency එක ගොඩක් වැදගත් වෙනවා. අන්න ඒ වගේ තැන් වලට තමයි අද අපි කතා කරන මේ Protocol Buffers (Protobuf) කියන කෙනා පිහිටට එන්නේ. මේක Google එකෙන් හදපු, සරල, කාර්යක්ෂම, programming language-neutral, extensible mechanism එකක් data serialize කරන්න. අපි බලමු Spring Boot එක්ක මේ Protobuf එක කොහොමද ලස්සනට පාවිච්චි කරන්නේ කියලා. අපි මේකේ JSON වලට වඩා තියෙන වාසි, කොහොමද Spring Boot project එකකට මේක set කරගන්නේ, API එකක් හදන්නේ කොහොමද වගේ හැමදේම අද මේ post එකෙන් ඉගෙන ගමු. කතාව නවත්තලා කෙලින්ම වැඩේට බහිමුද?

Protobuf කියන්නේ මොකක්ද? (Protocol Buffers - Simple Explanation)

හරි, මුලින්ම බලමු මේ Protobuf කියන්නේ මොකක්ද කියලා. සරලවම කිව්වොත්, මේක Google එකෙන් හදපු binary serialization format එකක්. JSON, XML වගේ formats වලට වඩා ගොඩක් පොඩි size එකකින් data transfer කරන්න මේකට පුළුවන්. ඒ වගේම data serialize කරන එකයි, deserialize කරන එකයි ගොඩක් වේගවත්. හිතන්නකෝ, ඔයාගේ app එකේ backend එකෙන් frontend එකට ලොකු data set එකක් යවනවා කියලා. JSON වලින් යවනකොට ඒකේ size එක වැඩි වෙන්න පුළුවන්. ඒත් Protobuf වලින් යවනකොට ඒකේ size එක සැලකිය යුතු විදියට අඩු වෙනවා. මේක නිසා network bandwidth එක save වෙනවා, ඒ වගේම request-response time එකත් අඩු වෙනවා. නියමයි නේද?

Protobuf වලදි අපි data structure එක define කරන්නේ .proto කියන file එකක් ඇතුලේ. මේක අපි schema එකක් විදියට පාවිච්චි කරනවා. ඊට පස්සේ Protobuf compiler එක (protoc) පාවිච්චි කරලා, ඒ .proto file එකෙන් අපිට ඕනෑම programming language එකකට (Java, Python, C++, Go, JavaScript වගේ) code generate කරගන්න පුළුවන්. මේ generate වෙන classes වලින් තමයි අපි data object handle කරන්නේ. මේකේ ලොකුම වාසිය තමයි, එකම schema එකෙන් විවිධ programming languages වලට code generate කරලා, ඒවා අතර data share කරන්න පුළුවන් වීම. මේක microservices වගේ තැන්වලදී මාරම පහසුකමක්. හිතන්න, ඔයාගේ microservice එක Java වලින් හදලා, client app එක JavaScript වලින් හදලා. දෙන්නම එකම Protobuf schema එකෙන් data exchange කරනවා. ගැම්මක් නේද!

Protobuf වල ප්‍රධාන වාසි (Key Advantages of Protobuf)

  • කාර්යක්ෂමතාව (Efficiency): JSON වලට වඩා 3-10 ගුණයකින් කුඩා data size එකක්. Serialize/deserialize වේගය 5-10 ගුණයකින් වැඩියි.
  • භාෂා-අනුවර්තනය (Language Neutral): ඕනෑම programming language එකකට code generate කරගන්න පුළුවන්.
  • පහසු නඩත්තුව (Easy Maintenance): Schema changes handle කරන එක පහසුයි. Fields add කරන එක, remove කරන එක වගේ දේවල් backwards-compatible විදියට කරන්න පුළුවන්.
  • Structured Data: Data structure එක පැහැදිලිව define කරන්න පුළුවන්.

ඇයි Spring Boot එක්ක Protobuf? (Why Spring Boot with Protobuf?)

Spring Boot කියන්නේ Java development වලදී දැන් අංක එකේ Framework එකනේ. Microservices, REST APIs, Web applications වගේ ගොඩක් දේවල් හදන්න Spring Boot මාර විදියට පහසුකම් සපයනවා. ඉතින්, අපි Spring Boot API එකකදී JSON වෙනුවට Protobuf පාවිච්චි කරන්නේ ඇයි කියලා බලමු.

ප්‍රධානම හේතුව තමයි Performance එක. අපි හැමෝම දන්නවා production applications වලදී response time එකයි, resource usage එකයි කියන්නේ මොන තරම් වැදගත්ද කියලා. Protobuf පාවිච්චි කරනකොට, network traffic එක අඩු වෙනවා, CPU usage එක අඩු වෙනවා, ඒ වගේම API response time එකත් වේගවත් වෙනවා. විශේෂයෙන්ම high-volume APIs හදනකොට, මේ වෙනස ඔයාට පැහැදිලිවම දැනේවි. උදාහරණයක් විදියට, 50MB JSON payload එකක් වෙනුවට 5MB Protobuf payload එකක් යවනවා නම්, network delay එක කොච්චර අඩු වෙනවද, server එකේ CPU usage එක කොච්චර අඩු වෙනවද කියලා හිතන්න. මේකෙන් server එකට වැඩි requests ප්‍රමාණයක් handle කරන්න පුළුවන් වෙනවා.

ඒ වගේම, Spring Boot කියන්නේ extensible framework එකක්. ඒ කියන්නේ අපිට ඕනෑම තෙවන පාර්ශවීය library එකක් අපේ project එකට ලේසියෙන්ම add කරගන්න පුළුවන්. Spring Boot මේ Protobuf support එකත් automatic configure කරගන්න පුළුවන් විදියට ProtobufHttpMessageConverter වගේ classes සපයනවා. මේක නිසා අපිට JSON වලින් Protobuf වලට migrate වෙන එක ලොකු කරදරයක් වෙන්නේ නෑ. අපි බලමු මේක step-by-step කොහොමද කරන්නේ කියලා.

Spring Boot වල Protobuf සෙට් කරගමු (Setting up Protobuf in Spring Boot)

හරි, දැන් අපි කෙලින්ම වැඩේට බහිමු. මුලින්ම අපිට Spring Boot project එකක් අවශ්‍යයි. ඒක හදාගන්න Spring Initializr (start.spring.io) පාවිච්චි කරන්න පුළුවන්. Dependencies විදියට Spring Web තෝරගන්න. ඊට පස්සේ, අපි Protobuf පාවිච්චි කරන්න අවශ්‍ය කරන Maven (හෝ Gradle) dependencies එකතු කරගමු. මෙතනදී අපි Protobuf compiler එක use කරනවා වගේම, Spring Boot වලට Protobuf messages handle කරන්න පුළුවන් වෙන්න protobuf-java සහ protobuf-java-util වගේ dependencies එකතු කරනවා.

Maven Dependencies (pom.xml)

ඔයාගේ pom.xml file එකට මේ dependencies එකතු කරන්න:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Protobuf Java runtime -->
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.25.1</version> <!-- නවතම version එක බලලා දාගන්න -->
    </dependency>
    <!-- Protobuf utilities (optional, but useful) -->
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java-util</artifactId>
        <version>3.25.1</version> <!-- නවතම version එක බලලා දාගන්න -->
    </dependency>
</dependencies>

ඒ වගේම, .proto files වලින් Java classes generate කරන්න අපිට Maven plugin එකක් අවශ්‍ය වෙනවා. pom.xml file එකේ <build> section එකට මේ plugin එක එකතු කරන්න:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <!-- Protobuf Maven Plugin -->
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version> <!-- නවතම version එක බලලා දාගන්න -->
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.25.1:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.62.2:exe:${os.detected.classifier}</pluginArtifact>
                <!-- The plugin generates classes into target/generated-sources/protobuf/java and target/generated-sources/protobuf/grpc-java -->
                <outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>
                <clearOutputDirectory>false</clearOutputDirectory>
                <checkStaleness>true</checkStaleness>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <!-- <goal>compile-custom</goal> if using gRPC -->
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

මතක තියාගන්න, මෙතන protocArtifact එකේ තියෙන version එක protobuf-java එකට ගැලපෙන්න ඕනේ. ඒ වගේම ${os.detected.classifier} කියන එක os-maven-plugin එකෙන් detect වෙනවා. ඒකත් pom.xml එකේ <properties> වලට add කරන්න.

<properties>
    <java.version>17</java.version>
    <!-- Add this for os.detected.classifier -->
    <os.detected.classifier>windows-x86_64</os.detected.classifier> <!-- Example, will be detected dynamically -->
</properties>

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.1</version>
        </extension>
    </extensions>
    ...
</build>

.proto File එක හදමු (Creating the .proto File)

දැන් අපි data structure එක define කරමු. src/main/proto කියන folder එකක් හදලා, ඒක ඇතුලේ user.proto වගේ file එකක් හදන්න. මේකේ තමයි අපේ message definition එක තියෙන්නේ.

syntax = "proto3"; // Protobuf 3 syntax

package com.example.protobuf; // Java package name for generated classes

option java_multiple_files = true; // Generate separate files for each message
option java_package = "com.example.protobuf.user"; // Java package specific to user messages
option java_outer_classname = "UserProto"; // Outer class name for generated classes

// Define a User message
message User {
  int32 id = 1; // Unique identifier for the user
  string firstName = 2; // User's first name
  string lastName = 3;  // User's last name
  string email = 4;     // User's email address
  repeated string roles = 5; // List of roles for the user (e.g., "ADMIN", "USER")
}

// Define a UserList message to send multiple users
message UserList {
  repeated User users = 1; // List of User messages
}

.proto file එක save කරලා, දැන් ඔයාගේ project එක mvn clean install (හෝ mvn compile) විධානයෙන් build කරන්න. එතකොට Protobuf Maven Plugin එකෙන් src/main/proto/user.proto file එක compile කරලා, target/generated-sources/protobuf/java කියන folder එක ඇතුලට UserProto.java වගේ classes generate කරයි. මේ generated classes තමයි අපේ Spring Boot application එකේ data objects විදියට අපි පාවිච්චි කරන්නේ.

Protobuf API එකක් හදමු (Implementing a Protobuf API in Spring Boot)

දැන් අපි Spring Boot Controller එකක් හදලා, මේ Protobuf messages පාවිච්චි කරලා API endpoints expose කරමු. Spring Boot වලදී, ProtobufHttpMessageConverter එක automatically configure වෙන නිසා, අපිට විශේෂ configuration එකක් කරන්න අවශ්‍ය වෙන්නේ නෑ. අපිට කරන්න තියෙන්නේ Controller එකේ request body එකට හෝ response body එකට Protobuf generated classes (e.g., UserProto.User) පාවිච්චි කරන එක විතරයි. Spring Boot ඒක තේරුම් අරන් Protobuf serialization/deserialization ටික කරගන්නවා.

User Service එකක් හදමු (Creating a UserService)

මුලින්ම, user data handle කරන්න service class එකක් හදමු.

package com.example.protobuf.service;

import com.example.protobuf.user.UserProto.User;
import com.example.protobuf.user.UserProto.UserList;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class UserService {

    private final List<User> users = new ArrayList<>();
    private final AtomicInteger idCounter = new AtomicInteger();

    public UserService() {
        // Sample data
        users.add(User.newBuilder()
                .setId(idCounter.incrementAndGet())
                .setFirstName("Kasun")
                .setLastName("Silva")
                .setEmail("[email protected]")
                .addRoles("ADMIN")
                .addRoles("USER")
                .build());
        users.add(User.newBuilder()
                .setId(idCounter.incrementAndGet())
                .setFirstName("Nimal")
                .setLastName("Perera")
                .setEmail("[email protected]")
                .addRoles("USER")
                .build());
    }

    public User createUser(User user) {
        User newUser = User.newBuilder(user) // Copy existing user data
                .setId(idCounter.incrementAndGet()) // Assign a new ID
                .build();
        users.add(newUser);
        return newUser;
    }

    public User getUserById(int id) {
        return users.stream()
                .filter(user -> user.getId() == id)
                .findFirst()
                .orElse(null);
    }

    public UserList getAllUsers() {
        return UserList.newBuilder().addAllUsers(users).build();
    }
}

User Controller එකක් හදමු (Creating a UserController)

දැන් අපේ Controller එක හදමු. මෙතනදී අපි @RequestBody සහ @ResponseBody Annotation පාවිච්චි කරනවා. Spring Boot auto-configuration නිසා, request header එකේ Content-Type: application/x-protobuf සහ Accept: application/x-protobuf තියෙනවා නම්, Spring Boot automatically protobuf format එකට convert කරගනීවි.

package com.example.protobuf.controller;

import com.example.protobuf.service.UserService;
import com.example.protobuf.user.UserProto.User;
import com.example.protobuf.user.UserProto.UserList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping(
            consumes = "application/x-protobuf",
            produces = "application/x-protobuf"
    )
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User createdUser = userService.createUser(user);
        return ResponseEntity.ok(createdUser);
    }

    @GetMapping(
            value = "/{id}",
            produces = "application/x-protobuf"
    )
    public ResponseEntity<User> getUserById(@PathVariable int id) {
        User user = userService.getUserById(id);
        if (user != null) {
            return ResponseEntity.ok(user);
        }
        return ResponseEntity.notFound().build();
    }

    @GetMapping(
            produces = "application/x-protobuf"
    )
    public ResponseEntity<UserList> getAllUsers() {
        UserList users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
}

ඔබට පේනවා ඇති, අපේ controller එකේ කිසිම විශේෂ protobuf related code එකක් නෑ. ඒක සාමාන්‍ය Spring Boot REST Controller එකක් වගේමයි. consumes සහ produces attribute වලට "application/x-protobuf" set කරපු නිසා, Spring Boot ඒ MediaType එකට අදාළ HttpMessageConverter එක පාවිච්චි කරනවා.

ටෙස්ට් කරලා හොඳම විදිහට පාවිච්චි කරමු (Testing and Best Practices)

දැන් අපි අපේ Spring Boot application එක start කරමු. (mvn spring-boot:run). Postman, Insomnia වගේ tools වලින් මේ API එක test කරන්න පුළුවන්. හැබැයි, Postman වගේ tools වලින් කෙලින්ම Protobuf requests යවන්න support එකක් නෑ. ඒකට අපිට Protobuf binary data හදලා යවන්න ඕනේ. ඒක ටිකක් සංකීර්ණයි.

වඩා හොඳම ක්‍රමය තමයි Java client එකක් (හෝ වෙනත් language client එකක්) හදලා test කරන එක. ඒ client එකේදී UserProto.User object එක හදලා, ඒක serialize කරලා request body එකට දාලා යවන්න පුළුවන්. Response එක ආවම deserialize කරලා බලන්නත් පුළුවන්.

Protobuf Client එකක් හදමු (Creating a Protobuf Client)

මෙන්න සරල Java client එකක උදාහරණයක්. මේක වෙනම Spring Boot project එකක් විදියට හෝ test class එකක් විදියට හදන්න පුළුවන්.

package com.example.protobuf.client;

import com.example.protobuf.user.UserProto.User;
import com.example.protobuf.user.UserProto.UserList;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

public class ProtobufClient {

    private static final String BASE_URL = "http://localhost:8080/api/users";
    private final RestTemplate restTemplate = new RestTemplate();

    public static void main(String[] args) {
        ProtobufClient client = new ProtobufClient();

        // 1. Create a User
        User newUser = User.newBuilder()
                .setFirstName("Chamari")
                .setLastName("Fernando")
                .setEmail("[email protected]")
                .addRoles("GUEST")
                .build();
        System.out.println("Creating user...");
        User createdUser = client.createUser(newUser);
        System.out.println("Created User: " + createdUser.getId() + " - " + createdUser.getFirstName() + " " + createdUser.getLastName());

        // 2. Get all users
        System.out.println("\nGetting all users...");
        UserList allUsers = client.getAllUsers();
        allUsers.getUsersList().forEach(user ->
                System.out.println("User: " + user.getId() + " - " + user.getFirstName() + " " + user.getLastName() + " (" + user.getEmail() + ")")
        );

        // 3. Get user by ID
        System.out.println("\nGetting user by ID (1)...");
        User userById = client.getUserById(1);
        if (userById != null) {
            System.out.println("User by ID 1: " + userById.getFirstName() + " " + userById.getLastName());
        } else {
            System.out.println("User not found.");
        }
    }

    public User createUser(User user) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf("application/x-protobuf"));
        headers.setAccept(Arrays.asList(MediaType.valueOf("application/x-protobuf"))); // Set accepted media type

        HttpEntity<User> requestEntity = new HttpEntity<>(user, headers);
        return restTemplate.exchange(BASE_URL, HttpMethod.POST, requestEntity, User.class).getBody();
    }

    public UserList getAllUsers() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.valueOf("application/x-protobuf")));
        HttpEntity<String> requestEntity = new HttpEntity<>(headers); // No body needed for GET

        return restTemplate.exchange(BASE_URL, HttpMethod.GET, requestEntity, UserList.class).getBody();
    }

    public User getUserById(int id) {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.valueOf("application/x-protobuf")));
        HttpEntity<String> requestEntity = new HttpEntity<>(headers);

        try {
            return restTemplate.exchange(BASE_URL + "/" + id, HttpMethod.GET, requestEntity, User.class).getBody();
        } catch (Exception e) {
            System.err.println("Error getting user by ID: " + e.getMessage());
            return null;
        }
    }
}

මේ client එක run කරන්න කලින්, client project එකේ pom.xml එකටත් protobuf-java, protobuf-java-util dependencies සහ protobuf-maven-plugin එකතු කරලා mvn clean install කරන්න ඕනේ, නැත්නම් UserProto classes හම්බ වෙන්නේ නෑ. ඒ වගේම Spring Boot application එකත් run වෙලා තියෙන්න ඕනේ.

Best Practices (හොඳම පුරුදු)

  • Schema Versioning: .proto files change වෙනකොට, backwards compatibility ගැන හිතන්න. Fields add කරනකොට අලුත් field numbers දාන්න, remove කරනකොට ඒ field numbers ආයෙ පාවිච්චි කරන්න එපා.
  • Error Handling: API errors JSON (or Problem Details) වලින් return කරන එක වඩා හොඳයි. Client එකට Protobuf error messages deserialize කරනවාට වඩා JSON error messages handle කරන එක පහසුයි.
  • When to Use: හැම API එකටම Protobuf අවශ්‍ය නැහැ. Internal microservices communication, high-volume data streams වගේ performance critical තැන්වලට Protobuf නියමයි. Small, public facing APIs වලට JSON හොඳටම ඇති.
  • Monitoring: Protobuf පාවිච්චි කරනකොට data size එකයි, serialization/deserialization times එකයි monitor කරන්න. ඇත්තටම performance improvement එකක් තියෙනවද කියලා බලන්න.

අවසාන වශයෙන් (In Conclusion)

හරි යාලුවනේ, අද අපි Spring Boot එක්ක Protobuf පාවිච්චි කරලා කොහොමද high-performance APIs හදන්නේ කියලා බැලුවා. JSON වලට වඩා Protobuf වල තියෙන වාසි, කොහොමද project එකට set කරගන්නේ, API එකක් implement කරන්නේ කොහොමද වගේ හැමදේම අපි ඉගෙන ගත්තා. මේක ඔයාගේ future projects වලට අනිවාර්යයෙන්ම ගොඩක් උදව් වෙයි. විශේෂයෙන්ම microservices architecture එකකදී data transfer efficiency එක ගැන හිතනකොට, Protobuf කියන්නේ මාරම බලගතු tool එකක්.

ඉතින්, මේක කියවලා විතරක් මදි. අනිවාර්යයෙන්ම ඔයාගේම Spring Boot project එකක මේක implement කරලා බලන්න. .proto file එකේ වෙනස්කම් කරලා බලන්න, Controller එකේ අලුත් endpoints add කරලා බලන්න. ඒ වගේම, Protobuf client එකක් හදලා test කරන එකෙන් ඔයාට මේකේ සම්පූර්ණ flow එක ගැන හොඳ අවබෝධයක් ගන්න පුළුවන්. මොකද, theory එකෙන් විතරක් වැඩක් නෑනේ, practice කරන්න ඕනේ!

ඔයාලට මේ post එක ගැන අදහස්, ප්‍රශ්න තියෙනවා නම් පහලින් comment section එකේ දාන්න. අපි ඒ ගැන කතා කරමු. තව මේ වගේ technical topics ගැන දැනගන්න කැමති නම් ඒකත් කියන්න. එහෙනම්, තවත් අලුත් Technical post එකකින් හමුවෙමු, හැමෝටම ජය වේවා! Happy Coding!