Spring Boot gRPC: අධි-ක්‍රියාකාරී සේවා ගොඩනගමු | SC Guide

Spring Boot gRPC: අධි-ක්‍රියාකාරී සේවා ගොඩනගමු | SC Guide

කොහොමද ඉතින් මචංලා! වැඩ ටික සාමාන්‍ය විදිහට කරගෙන යනවා නේද? අද අපි කතා කරන්න යන්නේ ටිකක් "පට්ට" වගේම ටිකක් "Serious" මාතෘකාවක් ගැන. මේ දවස්වල Microservices ගැන කතා කරනකොට, services අතරේ communication එක කොහොමද කරන්නේ කියන එක ලොකු ප්‍රශ්නයක්. REST APIs පාවිච්චි කරන එක බොහොම සාමාන්‍ය දෙයක් වුණත්, සමහර වෙලාවට ඒකේ තියෙන සීමා අපිට දැනෙනවා. විශේෂයෙන්ම, "performance" කියන එක ගැන සැරටම හිතනවා නම්, REST වලට වඩා ගොඩක් දේවල් කරන්න පුළුවන් තවත් options තියෙනවා. අන්න ඒ වගේ තැනක තමයි gRPC කියන concept එක කරලියට එන්නේ. අද අපි Spring Boot එක්ක gRPC එකතු කරගෙන, අධි-ක්‍රියාකාරී (high-performance) services කොහොමද හදන්නේ කියලා පියවරෙන් පියවර බලමු. ඇත්තටම මේක දැනටමත් ලොකු ලොකු companies වල production වලට දාලා තියෙන තාක්ෂණයක්. එහෙනම් වැඩේට බහිමුද?

gRPC කියන්නේ මොකක්ද මේ?

මුලින්ම බලමු මේ gRPC කියන්නේ මොකක්ද කියලා. gRPC කියන්නේ Google එකෙන් open-source කරපු High-performance, open-source Universal RPC framework එකක්. RPC කියන්නේ Remote Procedure Call එකක්. සරලවම කිව්වොත්, ඔයාගේ computer එකේ තියෙන program එකකින්, වෙන computer එකක තියෙන program එකක function එකක් call කරනවා වගේ වැඩක් තමයි මේකෙන් වෙන්නේ. හරි, එතකොට REST APIs වගේ ඒවා තියෙද්දි ඇයි අපිට gRPC ඕනේ වෙන්නේ? මේකයි කතාව:

  • Performance: gRPC වලදී HTTP/2 protocols භාවිතා කරනවා. මේකෙන් එක Connection එකක් හරහා එකවර Calls කිහිපයක් කරන්න පුළුවන් (multiplexing), Header compression වගේ දේවල් තියෙන නිසා REST වලට වඩා ගොඩක් වේගවත්. අනික gRPC වලින් data transmit කරන්නේ Protocol Buffers කියන binary serialization format එකකින්. මේක JSON වලට වඩා ගොඩක් lightweight වගේම වේගවත්.
  • Efficiency: Protocol Buffers නිසා data size එක අඩුවෙනවා. ඒ වගේම HTTP/2 වල server push, flow control වගේ features තියෙන නිසා network usage එකත් අඩු වෙනවා.
  • Strongly Typed Contracts: Protocol Buffers වලින් services වලට සහ data structures වලට clear schema එකක් define කරන්න පුළුවන්. මේකෙන් Compile-time errors අඩුවෙනවා වගේම development process එකත් පහසු වෙනවා.
  • Multi-Language Support: gRPC client සහ server implementations විවිධ programming languages වලට support කරනවා. Java, Go, Python, Node.js, C#, Ruby, Dart, C++ වගේ ඕනෑම භාෂාවක ලියපු service එකක් එක්ක communicate කරන්න පුළුවන්. මේක Microservices architecture එකේදී ගොඩක් වැදගත්.
  • Streaming: gRPC Unary (single request/single response) calls වලට අමතරව Server Streaming, Client Streaming, සහ Bidirectional Streaming calls වලටත් support කරනවා. Real-time applications වලට මේක පට්ට feature එකක්.

ඉතින් ඔයාලා දකිනවා ඇති gRPC කියන්නේ නිකම්ම නිකම් RPC එකක් නෙවෙයි, වේගවත්, කාර්‍යක්ෂම, සහ powerful communication method එකක් කියලා.

Spring Boot එක්ක gRPC පටලවගමු

දැන් බලමු කොහොමද මේ gRPC කියන සුපිරි තාක්ෂණය අපේ ආදරණීය Spring Boot applications එක්ක එකතු කරගන්නේ කියලා. Spring Boot කියන්නේ Rapid application development වල රජානේ. ඉතින් gRPC එක්ක එකතු වුණාම ඒක තවත් powerful වෙනවා. අපි මුලින්ම Spring Boot project එකක් හදාගමු. Spring Initializr (start.spring.io) එකට ගිහින් පහත dependencies තෝරගන්න:

  • Spring Web
  • Lombok (කැමති නම්)

ඊට පස්සේ project එක generate කරලා Download කරගන්න. Project එක Extract කරලා ඔයාගේ IDE එකෙන් (IntelliJ IDEA, VS Code වගේ) Open කරගන්න. දැන් අපිට gRPC support එක add කරගන්න ඕනේ. ඒකට Netty සහ Protobuf integration support කරන "grpc-spring-boot-starter" කියන dependency එක භාවිතා කරනවා. මේකේදී අපි Netty server එකක් එක්ක තමයි gRPC server එක configure කරන්නේ. pom.xml (Maven නම්) file එකට මේ dependencies add කරන්න.

Maven Dependencies (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-spring-boot-starter</artifactId>
        <version>2.15.0.RELEASE</version> <!-- නවතම stable version එක භාවිතා කරන්න -->
        <exclusions>
            <exclusion>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-netty-shaded</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-client-spring-boot-starter</artifactId>
        <version>2.15.0.RELEASE</version> <!-- Client එකට අවශ්‍ය නම් -->
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty</artifactId>
        <version>1.59.0</version> <!-- grpc-spring-boot-starter එකත් එක්ක ගැලපෙන version එකක් -->
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.59.0</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.59.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.1</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact> <!-- protobuf compiler version -->
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.59.0:exe:${os.detected.classifier}</pluginArtifact> <!-- grpc plugin version -->
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

මතක තියාගන්න, මෙතනදී versions ටික හරියට configure කරගන්න ඕනේ. නැත්නම් dependency conflicts එන්න පුළුවන්. `grpc-netty-shaded` එක exclude කරලා `grpc-netty` වෙනම ඇඩ් කරගන්නේ shaded version එක Spring Boot එක්ක ගැටුම් ඇතිකරගන්න පුළුවන් නිසා.

.proto ෆයිල් එකක් ලියමු

gRPC වලදී Service definition එකයි, data structures ටිකයි define කරන්නේ Protocol Buffers කියන භාෂාවෙන් ලියන `.proto` files වලින්. මේක තමයි gRPC contract එක. අපේ server සහ client දෙගොල්ලොම මේ file එක පාවිච්චි කරලා තමයි එකිනෙකා එක්ක කතා කරන්නේ. අපි පොඩි service එකක් හදමු, ඒකෙන් client කෙනෙක්ට 'Hello' message එකක් යවන්න පුළුවන් වෙන්න.

මුලින්ම project එකේ `src/main` යටතේ `proto` කියලා folder එකක් හදන්න. ඒක ඇතුලේ `greeter.proto` කියලා file එකක් හදලා මේ code එක paste කරන්න:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.grpc.springbootgrpc.greeter"; // ඔයාගේ package name එකට අනුව වෙනස් කරන්න
option java_outer_classname = "GreeterServiceProto";

package greeter;

// The greeter service definition.
service GreeterService {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // Sends multiple greetings in a stream
  rpc SayHelloServerStream (HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

මේ `greeter.proto` file එකේ මොනවද තියෙන්නේ කියලා පොඩ්ඩක් බලමු:

  • `syntax = "proto3";`: අපි Protocol Buffers version 3 භාවිතා කරනවා කියලා කියනවා.
  • `option java_multiple_files = true;`, `option java_package = "..."`, `option java_outer_classname = "...";`: මේවා Java code generation options. මේවා නිසා generate වෙන Java files organize වෙන්නේ කොහොමද කියලා කියනවා.
  • `package greeter;`: Protocol Buffers package name එක.
  • `service GreeterService { ... }`: මේකෙන් අපි service එකක් define කරනවා. මේ service එකට `SayHello` කියන method එකක් තියෙනවා, ඒක `HelloRequest` එකක් අරගෙන `HelloReply` එකක් return කරනවා. `SayHelloServerStream` කියන method එක server streaming වලට උදාහරණයක්.
  • `message HelloRequest { ... }`: මේක data structure එකක්, input parameter එක. `name` කියන field එක `string` type එකක්, ඒ වගේම `1` කියන්නේ field number එක. මේ numbers unique වෙන්න ඕනේ.
  • `message HelloReply { ... }`: මේකත් data structure එකක්, output එක. `message` කියන field එක `string` type එකක්.

මේ file එක save කරලා, ඔයාගේ Maven project එක clean install කරන්න. (IDE එකෙන් Maven -> lifecycle -> clean, install කරන්න). එහෙම නැත්නම් command line එකේ `mvn clean install` කියලා type කරන්න. එතකොට `target/generated-sources` folder එක ඇතුලේ මේ `.proto` file එකෙන් generate වුණ Java classes ටිකක් හැදෙනවා. මේවා තමයි අපේ gRPC server එක හදන්නයි client එක හදන්නයි අවශ්‍ය වෙන skeleton classes.

gRPC Service එක ගොඩනගමු

දැන් අපි generate වුණු classes භාවිතා කරලා අපේ gRPC service එක implement කරමු. `greeter.proto` file එක compile කලාම `GreeterServiceGrpc.GreeterServiceImplBase` වගේ class එකක් generate වෙනවා. අපි මේක extend කරලා තමයි අපේ logic එක ලියන්නේ.

`src/main/java` යටතේ ඔයාගේ main package එක ඇතුලේ (උදා: `com.example.grpc.springbootgrpc`) `GreeterServiceImpl` කියලා class එකක් හදන්න:

package com.example.grpc.springbootgrpc;

import com.example.grpc.springbootgrpc.greeter.HelloReply;
import com.example.grpc.springbootgrpc.greeter.HelloRequest;
import com.example.grpc.springbootgrpc.greeter.GreeterServiceGrpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class GreeterServiceImpl extends GreeterServiceGrpc.GreeterServiceImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        String name = request.getName();
        String message = "ආයුබෝවන් " + name + " මහත්මයා! Spring Boot gRPC සේවාවෙන් ඔබට සුබ පැතුම්!";
        HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }

    @Override
    public void sayHelloServerStream(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        String name = request.getName();
        try {
            for (int i = 0; i < 5; i++) {
                String message = "සුභ පැතුම් " + (i + 1) + " : " + name + "!";
                HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
                responseObserver.onNext(reply);
                Thread.sleep(500); // Simulate some delay
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            responseObserver.onError(e);
        } finally {
            responseObserver.onCompleted();
        }
    }
}

මේ code එකේදී අපි කරන්නේ:

  • `@GrpcService` annotation එකෙන් Spring Boot වලට මේක gRPC service එකක් කියලා අඳුනගන්න පුළුවන් වෙනවා.
  • `GreeterServiceGrpc.GreeterServiceImplBase` එක extend කරනවා. මේක තමයි generate වුණු abstract base class එක.
  • `sayHello` method එක override කරනවා. මේකේදී `HelloRequest` එකෙන් `name` එක අරගෙන, custom message එකක් හදලා `HelloReply` එකක් විදිහට `responseObserver.onNext()` එකට දෙනවා. `responseObserver.onCompleted()` කියන්නේ response එක ඉවරයි කියලා client ට කියන එක.
  • `sayHelloServerStream` method එක server streaming වලට. මේකේදී එක request එකකට multiple responses (stream එකක් විදිහට) යවනවා.

gRPC Server Port Configuration

දැන් අපි gRPC server එක run වෙන්න ඕනේ port එක configure කරගන්න ඕනේ. `src/main/resources/application.properties` file එකට මේ line එක add කරන්න:

grpc.server.port=9090

මේකෙන් කියන්නේ අපේ gRPC server එක 9090 port එකේදී listen කරනවා කියන එකයි.

දැන් ඔයාට පුළුවන් `SpringBootGrpcApplication.java` (main class එක) එක run කරන්න. Server එක start වුණාම console එකේ gRPC server එක 9090 port එකේ listen කරනවා කියලා පෙනෙයි.

Client පැත්තෙන් කොහොමද කතා කරන්නේ?

දැන් අපි server එක හැදුවා. එතකොට client කෙනෙක් මේ server එකට කොහොමද call කරන්නේ? අපි Spring Boot client එකක්ම හදමු. මේකටත් අපිට `grpc-client-spring-boot-starter` dependency එක ඕනේ වෙනවා (මේක අපි කලින්ම pom.xml එකට දැම්මා).

මුලින්ම client side එකටත් .proto file එක compile කරලා generate වුණු classes ටික ඕනේ. ඒක කලින් කළා වගේම තමයි. දැන් අපි client service එකක් හදමු.

project එකේ `com.example.grpc.springbootgrpc` package එක ඇතුලේ (නැත්නම් client කෝඩ් වලට වෙනම package එකක් හදන්න පුළුවන්) `GreeterClient` කියලා class එකක් හදන්න:

package com.example.grpc.springbootgrpc;

import com.example.grpc.springbootgrpc.greeter.HelloReply;
import com.example.grpc.springbootgrpc.greeter.HelloRequest;
import com.example.grpc.springbootgrpc.greeter.GreeterServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

@Service
public class GreeterClient {

    private static final Logger logger = Logger.getLogger(GreeterClient.class.getName());

    private GreeterServiceGrpc.GreeterServiceBlockingStub blockingStub; // Unary calls
    private GreeterServiceGrpc.GreeterServiceStub asyncStub; // Streaming calls
    private ManagedChannel channel;

    @PostConstruct
    public void init() {
        // gRPC server එක run වෙන host එකටයි port එකටයි connect වෙනවා.
        // production වලදී මේවා externalize කරන්න පුළුවන් (e.g., application.properties)
        channel = ManagedChannelBuilder.forAddress("localhost", 9090)
                .usePlaintext() // Production වලදී SSL/TLS භාවිතා කරන්න
                .build();
        blockingStub = GreeterServiceGrpc.newBlockingStub(channel);
        asyncStub = GreeterServiceGrpc.newStub(channel);
    }

    @PreDestroy
    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public String greet(String name) {
        logger.info("Client: Greeting " + name + "...");
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        HelloReply response = blockingStub.sayHello(request); // Unary call
        logger.info("Client: Received: " + response.getMessage());
        return response.getMessage();
    }

    public void greetServerStream(String name) throws InterruptedException {
        logger.info("Client: Server Streaming for " + name + "...");
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();

        CountDownLatch finishLatch = new CountDownLatch(1);

        asyncStub.sayHelloServerStream(request, new StreamObserver<HelloReply>() {
            @Override
            public void onNext(HelloReply reply) {
                logger.info("Client Stream OnNext: " + reply.getMessage());
            }

            @Override
            public void onError(Throwable t) {
                logger.warning("Client Stream Error: " + t.getMessage());
                finishLatch.countDown();
            }

            @Override
            public void onCompleted() {
                logger.info("Client Stream OnCompleted: Server stream finished.");
                finishLatch.countDown();
            }
        });
        finishLatch.await(1, TimeUnit.MINUTES); // Wait for stream to complete
    }
}

දැන් මේ client එක call කරන්න පුළුවන්. උදාහරණයක් විදිහට, Spring Boot REST Controller එකකින් මේ client service එකට Inject කරලා endpoint එකකින් call කරන්න පුළුවන්.

package com.example.grpc.springbootgrpc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetController {

    private final GreeterClient greeterClient;

    @Autowired
    public GreetController(GreeterClient greeterClient) {
        this.greeterClient = greeterClient;
    }

    @GetMapping("/greet/{name}")
    public String greetUser(@PathVariable String name) {
        return greeterClient.greet(name);
    }

    @GetMapping("/greet/stream/{name}")
    public String greetUserStream(@PathVariable String name) throws InterruptedException {
        greeterClient.greetServerStream(name);
        return "Server stream initiated for " + name + ". Check console for responses.";
    }
}

දැන් ඔයාට පුළුවන් server එක run කරලා, `http://localhost:8080/greet/Nimal` වගේ URL එකකට call කරලා result එක බලන්න. Streaming call එකට `http://localhost:8080/greet/stream/Kamal` වගේ call කරලා server console එකේ responses stream වෙන හැටි බලන්න පුළුවන්. Client side එකෙන් `StreamObserver` එකක් implement කරලා තමයි streaming responses handle කරන්නේ.

අවසන් වශයෙන්...

ඔන්න ඉතින් මචංලා, අපි Spring Boot එක්ක gRPC services කොහොමද ගොඩනගන්නේ කියලා පියවරෙන් පියවර බැලුවා. gRPC වල අධි-ක්‍රියාකාරීත්වය (high-performance), කාර්‍යක්ෂමතාව (efficiency) සහ බහු-භාෂා සහයෝගය (multi-language support) නිසා, Microservices architectures වලදී වගේම distributed systems වලදී communication වලට gRPC කියන්නේ නියම විසඳුමක්. විශේෂයෙන්ම real-time applications, IoT solutions, සහ mobile backends වගේ තැන් වලට gRPC ගොඩක්ම ගැලපෙනවා.

මේ concepts ටික අරගෙන ඔයාලට පුළුවන් ඔයාලගේම applications වලට gRPC integrate කරලා බලන්න. මුලින් පොඩ්ඩක් අමුතු වගේ දැනුනත්, මේක පුරුදු වුණාම හරිම පහසුයි. වැදගත්ම දේ තමයි මේකෙන් ඔයාලගේ applications වල performance එක සැලකිය යුතු මට්ටමකින් වැඩි කරගන්න පුළුවන් වෙන එක.

ඉතින් මේ ගැන ඔයාලට මොනවද හිතෙන්නේ? gRPC ගැන කලින් අහලා තිබුණද? නැත්නම් මේක අලුත් දෙයක්ද? මේ ගැන ඔයාලගේ අදහස්, ප්‍රශ්න පහළ comment section එකේ දාගෙන යන්න. තවත් මේ වගේ වැදගත් article එකකින් හමුවෙමු. එහෙනම් හැමෝටම ජය වේවා! Happy Coding!