Instructions
Step by step implement gRPC for Micronaut Kotlin:
1. Add gRPC and protobuf version in gradle.properties
file: gradle.properties
... protobufVersion=3.21.11 grpcProtobufVersion=1.51.1 grpcKotlinVersion=1.3.0
2. Add gradle plugin and version definition in build.gradle.kts
file: build.gradle.kts
import com.google.protobuf.gradle.id plugins { ... id("com.google.protobuf") version "0.9.1" } val protobufVersion = project.properties["protobufVersion"] val grpcProtobufVersion = project.properties["grpcProtobufVersion"] val grpcKotlinVersion = project.properties["grpcKotlinVersion"]
3. Add gRPC and protobuf dependencies
file: build.gradle.kts
dependencies { ... implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion") compileOnly("io.grpc:grpc-stub:$grpcProtobufVersion") implementation("io.grpc:grpc-protobuf:$grpcProtobufVersion") implementation("com.google.protobuf:protobuf-kotlin:$protobufVersion") implementation("io.micronaut.grpc:micronaut-grpc-server-runtime:3.4.0") protobuf("com.google.protobuf:protobuf-java:$protobufVersion") }
4. Add source set in build.gradle.kts
file: build.gradle.kts
sourceSets { main { java { srcDirs("build/generated/source/proto/main/grpc") srcDirs("build/generated/source/proto/main/grpckt") srcDirs("build/generated/source/proto/main/java") } } }
5. Add protobuf task in build.gradle.kts
file: build.gradle.kts
protobuf { protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:$grpcProtobufVersion" } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk8@jar" } } generateProtoTasks { all().forEach { it.plugins { id("grpc") id("grpckt") } } } }
6. Create protobuf file in folder src/main/proto/
file: src/main/proto/CardManagerV1.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "cardManagerV1"; option java_outer_classname = "CardManagerV1Proto"; option objc_class_prefix = "CMS"; package cardmanagerv1; service CardManagerV1 { rpc GetCardDetails (GetCardDetailsMessageV1) returns (CardDetailsMessageV1) {} } message CardDetailsMessageV1 { string cardId = 1; string accountNo = 2; string accountType = 3; string customerId = 4; string cardType = 5; string cardPicture = 6; optional bool namePrinted = 7; optional bool dynamicCVV = 8; optional bool internationalBlockingFlag = 9; string embossName = 10; string maskingCardNo = 11; string expiration = 12; string cardStatus = 13; optional int32 orderingNumber = 14; string key = 15; optional string createdAt = 16; optional string updatedAt = 17; } message GetCardDetailsMessageV1 { string cardId = 1; bool isSyncTracking = 2; }
Or you can split the definition by creating the others .proto
file to define each message model:
file: src/main/proto/messages/CardDetailsMessageV1.proto
syntax = "proto3"; option java_package = "cardManagerV1"; option java_outer_classname = "CardDetailsMessageV1Proto"; option objc_class_prefix = "CMS"; message CardDetailsMessageV1 { string cardId = 1; string accountNo = 2; string accountType = 3; string customerId = 4; string cardType = 5; string cardPicture = 6; optional bool namePrinted = 7; optional bool dynamicCVV = 8; optional bool internationalBlockingFlag = 9; string embossName = 10; string maskingCardNo = 11; string expiration = 12; string cardStatus = 13; optional int32 orderingNumber = 14; string key = 15; optional string createdAt = 16; optional string updatedAt = 17; }
file: src/main/proto/messages/GetCardDetailsMessageV1.proto
syntax = "proto3"; option java_package = "cardManagerV1"; option java_outer_classname = "GetCardDetailsMessageV1Proto"; option objc_class_prefix = "CMS"; message GetCardDetailsMessageV1 { string cardId = 1; bool isSyncTracking = 2; }
file: src/main/proto/CardManagerV1.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "cardManagerV1"; option java_outer_classname = "CardManagerV1Proto"; option objc_class_prefix = "CMS"; package cardmanagerv1; import "messages/CardDetailsMessageV1.proto"; import "messages/GetCardDetailsMessageV1.proto"; service CardManagerV1 { rpc GetCardDetails (GetCardDetailsMessageV1) returns (CardDetailsMessageV1) {} }
More details: https://developers.google.com/protocol-buffers/docs/proto3
7. Generate protobuf class
To generate protobuf to be java/kotlin class you can run ./gradlew generateProto
or just build the project. Then the generated code will be stored in build/generated/source/proto/*
8. Create gRPC resolver class
file: src/main/kotlin/ph.safibank.cardmanager/grpc/CardManagerGrpcV1.kt
package ph.safibank.cardmanager.grpc import cardManagerV1.CardDetailsMessageV1Proto import cardManagerV1.CardManagerV1GrpcKt import cardManagerV1.GetCardDetailsMessageV1Proto import jakarta.inject.Singleton import ph.safibank.cardmanager.grpc.mapper.toCardDetailsMessageV1 import ph.safibank.cardmanager.service.CardService @Singleton class CardManagerGrpcV1(private val cardService: CardService) : CardManagerV1GrpcKt.CardManagerV1CoroutineImplBase() { override suspend fun getCardDetails(request: GetCardDetailsMessageV1Proto.GetCardDetailsMessageV1): CardDetailsMessageV1Proto.CardDetailsMessageV1 { val result = cardService.getCardByCardId(request.cardId, request.isSyncTracking) return result.toCardDetailsMessageV1() } }
In above example, class CardManagerGrpcV1
has method getCardDetails
that return type is CardDetailsMessageV1Proto.CardDetailsMessageV1
. Basically the generated proto will create the builder class to generate the message object, then in this example I split it to mapper function for resuseability.
file: src/main/kotlin/ph.safibank.cardmanager/grpc/mapper/ToCardDetailsMessageV1.kt
package ph.safibank.cardmanager.grpc.mapper import cardManagerV1.CardDetailsMessageV1Proto import ph.safibank.cardmanager.model.Card fun Card.toCardDetailsMessageV1(): CardDetailsMessageV1Proto.CardDetailsMessageV1 { val cardDetailsBuilder = CardDetailsMessageV1Proto.CardDetailsMessageV1.newBuilder() cardDetailsBuilder .setCardId(this.cardId) .setAccountNo(this.accountNo) .setCustomerId(this.customerId.toString()) .setCardType(this.cardType) .setCardPicture(this.cardPicture) .setEmbossName(this.embossName) .setMaskingCardNo(this.maskingCardNo) .setExpiration(this.expiration) .setCardStatus(this.cardStatus) .setKey(this.key) if (this.namePrinted != null) { cardDetailsBuilder.namePrinted = this.namePrinted!! } if (this.dynamicCVV != null) { cardDetailsBuilder.dynamicCVV = this.dynamicCVV!! } if (this.internationalBlockingFlag != null) { cardDetailsBuilder.internationalBlockingFlag = this.internationalBlockingFlag!! } if (this.createdAt != null) { cardDetailsBuilder.createdAt = this.createdAt!!.toString() } if (this.updatedAt != null) { cardDetailsBuilder.updatedAt = this.updatedAt!!.toString() } if (this.orderingNumber != null) { cardDetailsBuilder.orderingNumber = this.orderingNumber!! } return cardDetailsBuilder.build() }
9. Update application.yml
To prevent port conflict I suggest to change the port of gRPC server by add following properties to application.yml
file: src/main/resources/application.yml
... grpc: server: port: 8081
10. Done, start the application
See example: https://github.com/SafiBank/SaFiMono/tree/SM-XXX/PoC-grpc-micronaut
Related articles
https://micronaut-projects.github.io/micronaut-grpc/3.4.0/guide/index.html#server