Skip to content

Commit

Permalink
Add Kotlin Spring Boot starter (#433)
Browse files Browse the repository at this point in the history
* Introduce sdk-spring-boot-kotlin-starter and move common spring boot integration code in sdk-spring-boot

* Dependency cleanup
  • Loading branch information
slinkydeveloper authored Dec 9, 2024
1 parent b40c904 commit f19f538
Show file tree
Hide file tree
Showing 24 changed files with 216 additions and 114 deletions.
7 changes: 3 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
plugins {
id("com.github.jk1.dependency-license-report") version "2.0"
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"

id("org.jetbrains.dokka") version "1.9.20"
alias(libs.plugins.dependency.license.report)
alias(libs.plugins.nexus.publish)
alias(libs.plugins.dokka)

// https://github.com/gradle/gradle/issues/20084#issuecomment-1060822638
id(libs.plugins.spotless.get().pluginId) apply false
Expand Down
8 changes: 3 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ ksp = "2.0.21-1.0.28"
protobuf = "4.29.0"
opentelemetry = "1.44.1"
vertx = "4.5.11"
dokka = "1.9.20"
kotlinx-serialization = "1.7.3"
kotlinx-coroutines = "1.9.0"
junit = "5.10.2"
Expand All @@ -20,7 +19,6 @@ jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
jackson-jdk8 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", version.ref = "jackson" }
jackson-parameter-names = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "jackson" }
handlebars = "com.github.jknack:handlebars:4.3.1"
victools-jsonschema-generator = { module = "com.github.victools:jsonschema-generator", version.ref = "victools-json-schema" }
Expand All @@ -30,7 +28,6 @@ tink = "com.google.crypto.tink:tink:1.15.0"
ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
jwt = "com.nimbusds:nimbus-jose-jwt:9.47"
opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", version.ref = "opentelemetry" }
opentelemetry-kotlin = { module = "io.opentelemetry:opentelemetry-extension-kotlin", version.ref = "opentelemetry" }
Expand All @@ -44,6 +41,7 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo
tomcat-annotations = "org.apache.tomcat:annotations-api:6.0.53"
assertj = "org.assertj:assertj-core:3.26.0"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
jspecify = "org.jspecify:jspecify:1.0.0"
Expand All @@ -62,9 +60,9 @@ jib = "com.google.cloud.tools.jib:3.4.4"
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
protobuf = "com.google.protobuf:0.9.4"
aggregate-javadoc = "io.freefair.aggregate-javadoc:8.6"
gradle-nexus-publish-plugin = "io.github.gradle-nexus.publish-plugin:1.3.0"
nexus-publish = "io.github.gradle-nexus.publish-plugin:1.3.0"
spring-dependency-management = "io.spring.dependency-management:1.1.6"
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
dokka = "org.jetbrains.dokka:1.9.20"
jsonschema2pojo = "org.jsonschema2pojo:1.2.1"
openapi-generator = "org.openapi.generator:7.5.0"
spotless = "com.diffplug.spotless:6.25.0"
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,25 @@ class ServiceProcessor(private val logger: KSPLogger, private val codeGenerator:
.getClassDeclarationByName<dev.restate.sdk.annotation.Workflow>()!!
.qualifiedName!!,
ServiceType.WORKFLOW))

// Add spring annotations, if available
resolver.getClassDeclarationByName("dev.restate.sdk.springboot.RestateService")?.let {
metaAnnotationsToProcess.add(MetaRestateAnnotation(it.qualifiedName!!, ServiceType.SERVICE))
}
resolver.getClassDeclarationByName("dev.restate.sdk.springboot.RestateVirtualObject")?.let {
metaAnnotationsToProcess.add(
MetaRestateAnnotation(it.qualifiedName!!, ServiceType.VIRTUAL_OBJECT))
}
resolver.getClassDeclarationByName("dev.restate.sdk.springboot.RestateWorkflow")?.let {
metaAnnotationsToProcess.add(MetaRestateAnnotation(it.qualifiedName!!, ServiceType.WORKFLOW))
}

val discoveredAnnotations = mutableSetOf<String>()

var metaAnnotation = metaAnnotationsToProcess.removeFirstOrNull()
while (metaAnnotation != null) {
if (!discoveredAnnotations.add(metaAnnotation.annotationName.asString())) {
// We alredy discovered it, skip
// We already discovered it, skip
continue
}
for (annotatedElement in
Expand Down
25 changes: 25 additions & 0 deletions sdk-spring-boot-kotlin-starter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
`kotlin-conventions`
`library-publishing-conventions`
alias(libs.plugins.ksp)
alias(libs.plugins.spring.dependency.management)
}

description = "Restate SDK Spring Boot Kotlin starter"

dependencies {
compileOnly(libs.jspecify)

api(project(":sdk-api-kotlin"))
api(project(":sdk-spring-boot"))

kspTest(project(":sdk-api-kotlin-gen"))
testImplementation(project(":sdk-testing"))
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.spring.boot.starter.test)

// We need these for the deployment manifest
testImplementation(project(":sdk-core"))
testImplementation(libs.jackson.annotations)
testImplementation(libs.jackson.databind)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot.kotlin

import dev.restate.sdk.annotation.Handler
import dev.restate.sdk.kotlin.Context
import dev.restate.sdk.springboot.RestateService
import org.springframework.beans.factory.annotation.Value

@RestateService(name = "greeter")
class Greeter {
@Value("\${greetingPrefix}") lateinit var greetingPrefix: String

@Handler
fun greet(ctx: Context, person: String): String {
return greetingPrefix + person
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot.kotlin

import com.fasterxml.jackson.databind.ObjectMapper
import dev.restate.sdk.core.manifest.EndpointManifestSchema
import dev.restate.sdk.core.manifest.Service
import dev.restate.sdk.springboot.RestateHttpEndpointBean
import java.io.IOException
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(
classes = [RestateHttpEndpointBean::class, Greeter::class],
properties = ["restate.sdk.http.port=0"])
class RestateHttpEndpointBeanTest {
@Autowired lateinit var restateHttpEndpointBean: RestateHttpEndpointBean

@Test
@Throws(IOException::class, InterruptedException::class)
fun httpEndpointShouldBeRunning() {
assertThat(restateHttpEndpointBean.isRunning).isTrue()
assertThat(restateHttpEndpointBean.actualPort()).isPositive()

// Check if discovery replies containing the Greeter service
val client = HttpClient.newHttpClient()
val response =
client.send<String?>(
HttpRequest.newBuilder()
.GET()
.uri(
URI.create(
("http://localhost:" + restateHttpEndpointBean.actualPort()).toString() +
"/discover"))
.header("Accept", "application/vnd.restate.endpointmanifest.v1+json")
.build(),
HttpResponse.BodyHandlers.ofString())
Assertions.assertThat(response.statusCode()).isEqualTo(200)

val endpointManifest =
ObjectMapper()
.readValue<EndpointManifestSchema>(response.body(), EndpointManifestSchema::class.java)

Assertions.assertThat<Service?>(endpointManifest.services)
.map<String>({ it.name })
.containsOnly("greeter")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot.kotlin

import dev.restate.sdk.client.Client
import dev.restate.sdk.testing.BindService
import dev.restate.sdk.testing.RestateClient
import dev.restate.sdk.testing.RestateTest
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(classes = [Greeter::class], properties = ["greetingPrefix=Something something "])
@RestateTest(containerImage = "ghcr.io/restatedev/restate:main")
class SdkTestingIntegrationTest {
@Autowired @BindService lateinit var greeter: Greeter

@Test
@Timeout(value = 10)
fun greet(@RestateClient ingressClient: Client) = runTest {
val client = greeterClient.fromClient(ingressClient)

Assertions.assertThat(client.greet("Francesco")).isEqualTo("Something something Francesco")
}
}
26 changes: 1 addition & 25 deletions sdk-spring-boot-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ description = "Restate SDK Spring Boot starter"
dependencies {
compileOnly(libs.jspecify)

api(project(":sdk-common")) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}
api(project(":sdk-spring-boot"))
api(project(":sdk-api")) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
Expand All @@ -30,25 +25,6 @@ dependencies {
exclude(group = "com.fasterxml.jackson.datatype")
}

implementation(project(":sdk-http-vertx")) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}
implementation(project(":sdk-request-identity"))
implementation(libs.vertx.core) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}

implementation(libs.spring.boot.starter)

// Spring is going to bring jackson in with this
implementation(libs.spring.boot.starter.json)

// We need these for the deployment manifest
testImplementation(project(":sdk-core"))
testImplementation(libs.jackson.annotations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot;
package dev.restate.sdk.springboot.java;

import dev.restate.sdk.Context;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.springboot.RestateService;
import org.springframework.beans.factory.annotation.Value;

@RestateService(name = "greeter")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot;
package dev.restate.sdk.springboot.java;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.restate.sdk.core.manifest.EndpointManifestSchema;
import dev.restate.sdk.springboot.RestateHttpEndpointBean;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot;
package dev.restate.sdk.springboot.java;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down
43 changes: 43 additions & 0 deletions sdk-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
plugins {
`java-conventions`
`java-library`
`test-jar-conventions`
`library-publishing-conventions`
alias(libs.plugins.spring.dependency.management)
}

description = "Restate SDK Spring Boot integration"

dependencies {
compileOnly(libs.jspecify)

api(project(":sdk-common")) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}

implementation(project(":sdk-http-vertx")) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}
implementation(project(":sdk-request-identity"))
implementation(libs.vertx.core) {
// Let spring bring jackson in
exclude(group = "com.fasterxml.jackson")
exclude(group = "com.fasterxml.jackson.core")
exclude(group = "com.fasterxml.jackson.datatype")
}

implementation(libs.spring.boot.starter)

// Spring is going to bring jackson in with this
implementation(libs.spring.boot.starter.json)

testImplementation(libs.spring.boot.starter.test)
}

tasks.withType<JavaCompile> { options.compilerArgs.add("-parameters") }
Loading

0 comments on commit f19f538

Please sign in to comment.