Skip to content

Commit 78d3014

Browse files
authored
feat: GraalVM native image support for Spring server (#1769)
### 📝 Description Changes to enable GraalVM native image support for `graphql-kotlin-spring-server`.
1 parent bc8ee25 commit 78d3014

File tree

24 files changed

+696
-38
lines changed

24 files changed

+696
-38
lines changed

.github/workflows/graalvm-integration.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ on:
66
branches:
77
- master
88
paths:
9-
- 'generator/**'
10-
- 'servers/**'
11-
- 'plugins/**'
129
- 'integration/graalvm/**'
1310

1411
jobs:
@@ -20,7 +17,7 @@ jobs:
2017
working-directory: integration/graalvm
2118
strategy:
2219
matrix:
23-
server: ['ktor-graalvm-server', 'maven-graalvm-server']
20+
server: ['ktor-graalvm-server', 'maven-graalvm-server', 'spring-graalvm-server']
2421

2522
steps:
2623
- name: Checkout Repository

.github/workflows/pr-check.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ jobs:
2020
build-examples:
2121
needs: build-libraries
2222
uses: ./.github/workflows/build-examples.yml
23+
24+
graalvm-integration:
25+
needs: build-libraries
26+
uses: ./.github/workflows/graalvm-integration.yml

gradle/libs.versions.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ classgraph = "4.8.149"
44
dataloader = "3.2.0"
55
federation = "3.0.0"
66
graphql-java = "20.2"
7-
graalvm = "0.9.20"
7+
graalvm = "0.9.21"
88
jackson = "2.14.1"
99
kotlin = "1.7.21"
1010
kotlinx-benchmark = "0.4.4"
@@ -16,11 +16,11 @@ maven-plugin-api = "3.6.3"
1616
maven-project = "2.2.1"
1717
poet = "1.12.0"
1818
## reactorVersion should be the same reactor-core version pulled from spring-boot-starter-webflux
19-
reactor-core = "3.5.1"
20-
reactor-extensions = "1.2.1"
19+
reactor-core = "3.5.5"
20+
reactor-extensions = "1.2.2"
2121
slf4j = "1.7.36"
22-
spring = "6.0.3"
23-
spring-boot = "3.0.1"
22+
spring = "6.0.8"
23+
spring-boot = "3.0.6"
2424

2525
# test dependencies
2626
# kotlin-compile-testing has to be using the same kotlin version as the kotlinx-serialization compiler

integration/graalvm/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import java.util.Properties
22

3+
@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
4+
plugins {
5+
alias(libs.plugins.graalvm.native) apply false
6+
}
7+
38
allprojects {
49
repositories {
510
mavenCentral()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graalvm.ktor.schema
17+
package com.expediagroup.graalvm.schema
1818

1919
import com.expediagroup.graphql.server.operations.Query
2020
import graphql.schema.DataFetchingEnvironment

integration/graalvm/ktor-graalvm-server/src/main/kotlin/com/expediagroup/graalvm/ktor/Application.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ package com.expediagroup.graalvm.ktor
1818

1919
import com.expediagroup.graalvm.hooks.CustomHooks
2020
import com.expediagroup.graalvm.ktor.context.CustomContextFactory
21-
import com.expediagroup.graalvm.ktor.schema.ContextualQuery
2221
import com.expediagroup.graalvm.schema.ArgumentQuery
2322
import com.expediagroup.graalvm.schema.AsyncQuery
2423
import com.expediagroup.graalvm.schema.BasicMutation
24+
import com.expediagroup.graalvm.schema.ContextualQuery
2525
import com.expediagroup.graalvm.schema.CustomScalarQuery
2626
import com.expediagroup.graalvm.schema.EnumQuery
2727
import com.expediagroup.graalvm.schema.ErrorQuery

integration/graalvm/settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ dependencyResolutionManagement {
2121
includeBuild("../..")
2222

2323
include(":common-graalvm-server")
24-
//include(":spring-graalvm-server")
2524
include(":ktor-graalvm-server")
25+
include(":spring-graalvm-server")
2626
include(":maven-graalvm-server")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import com.expediagroup.graphql.plugin.gradle.graphql
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
5+
plugins {
6+
alias(libs.plugins.kotlin.jvm)
7+
application
8+
alias(libs.plugins.graalvm.native)
9+
alias(libs.plugins.kotlin.spring)
10+
alias(libs.plugins.spring.boot)
11+
id("com.expediagroup.graphql")
12+
}
13+
14+
dependencies {
15+
implementation("com.expediagroup", "graphql-kotlin-spring-server")
16+
implementation(projects.commonGraalvmServer)
17+
testImplementation(libs.junit.api)
18+
testImplementation(libs.kotlin.test)
19+
testImplementation(libs.spring.boot.test)
20+
}
21+
22+
tasks.test {
23+
useJUnitPlatform()
24+
}
25+
26+
tasks.withType<KotlinCompile> {
27+
kotlinOptions.jvmTarget = "17"
28+
}
29+
30+
graalvmNative {
31+
toolchainDetection.set(false)
32+
binaries {
33+
named("main") {
34+
verbose.set(true)
35+
}
36+
metadataRepository {
37+
enabled.set(true)
38+
}
39+
}
40+
}
41+
42+
graphql {
43+
graalVm {
44+
packages = listOf("com.expediagroup.graalvm")
45+
mainClassName = "com.expediagroup.graalvm.spring.ApplicationKt"
46+
}
47+
}
48+
49+
tasks.register("buildGraalVmNativeImage") {
50+
dependsOn("build")
51+
dependsOn("nativeCompile")
52+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graalvm.spring
18+
19+
import com.expediagroup.graalvm.hooks.CustomHooks
20+
import com.expediagroup.graalvm.schema.ArgumentQuery
21+
import com.expediagroup.graalvm.schema.AsyncQuery
22+
import com.expediagroup.graalvm.schema.BasicMutation
23+
import com.expediagroup.graalvm.schema.ContextualQuery
24+
import com.expediagroup.graalvm.schema.CustomScalarQuery
25+
import com.expediagroup.graalvm.schema.EnumQuery
26+
import com.expediagroup.graalvm.schema.ErrorQuery
27+
import com.expediagroup.graalvm.schema.IdQuery
28+
import com.expediagroup.graalvm.schema.InnerClassQuery
29+
import com.expediagroup.graalvm.schema.ListQuery
30+
import com.expediagroup.graalvm.schema.PolymorphicQuery
31+
import com.expediagroup.graalvm.schema.ScalarQuery
32+
import com.expediagroup.graalvm.schema.TypesQuery
33+
import com.expediagroup.graalvm.schema.dataloader.ExampleDataLoader
34+
import com.expediagroup.graalvm.schema.model.ExampleInterface
35+
import com.expediagroup.graalvm.schema.model.ExampleUnion
36+
import com.expediagroup.graalvm.schema.model.FirstImpl
37+
import com.expediagroup.graalvm.schema.model.FirstUnionMember
38+
import com.expediagroup.graalvm.schema.model.SecondImpl
39+
import com.expediagroup.graalvm.schema.model.SecondUnionMember
40+
import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory
41+
import com.expediagroup.graphql.generator.GraphQLTypeResolver
42+
import com.expediagroup.graphql.generator.SimpleTypeResolver
43+
import org.springframework.boot.autoconfigure.SpringBootApplication
44+
import org.springframework.boot.runApplication
45+
import org.springframework.context.annotation.Bean
46+
47+
@SpringBootApplication
48+
class Application {
49+
50+
@Bean
51+
fun hooks(): CustomHooks = CustomHooks()
52+
53+
@Bean
54+
fun dataLoaderRegistryFactory(): KotlinDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
55+
ExampleDataLoader
56+
)
57+
58+
@Bean
59+
fun typeResolver(): GraphQLTypeResolver = SimpleTypeResolver(
60+
mapOf(
61+
ExampleInterface::class to listOf(FirstImpl::class, SecondImpl::class),
62+
ExampleUnion::class to listOf(FirstUnionMember::class, SecondUnionMember::class)
63+
)
64+
)
65+
66+
// queries
67+
@Bean
68+
fun argumentQuery() = ArgumentQuery()
69+
70+
@Bean
71+
fun asyncQuery() = AsyncQuery()
72+
73+
@Bean
74+
fun contextualQuery() = ContextualQuery()
75+
76+
@Bean
77+
fun customScalarQuery() = CustomScalarQuery()
78+
79+
@Bean
80+
fun enumQuery() = EnumQuery()
81+
82+
@Bean
83+
fun errorQuery() = ErrorQuery()
84+
85+
@Bean
86+
fun idQuery() = IdQuery()
87+
88+
@Bean
89+
fun innerClassQuery() = InnerClassQuery()
90+
91+
@Bean
92+
fun listQuery() = ListQuery()
93+
94+
@Bean
95+
fun polymorphicQuery() = PolymorphicQuery()
96+
97+
@Bean
98+
fun scalarQuery() = ScalarQuery()
99+
100+
@Bean
101+
fun typesQuery() = TypesQuery()
102+
103+
// mutations
104+
@Bean
105+
fun basicMutation() = BasicMutation()
106+
}
107+
108+
fun main(args: Array<String>) {
109+
runApplication<Application>(*args)
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graalvm.spring.context
18+
19+
import com.expediagroup.graphql.generator.extensions.plus
20+
import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory
21+
import graphql.GraphQLContext
22+
import org.springframework.stereotype.Component
23+
import org.springframework.web.reactive.function.server.ServerRequest
24+
import java.util.UUID
25+
26+
@Component
27+
class CustomContextFactory : DefaultSpringGraphQLContextFactory() {
28+
override suspend fun generateContext(request: ServerRequest): GraphQLContext {
29+
return super.generateContext(request).plus(mapOf("custom" to (request.headers().firstHeader("X-Custom-Header") ?: UUID.randomUUID().toString())))
30+
}
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
graphql:
2+
packages:
3+
- "com.expediagroup.graalvm"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.expediagroup.graalvm.spring.schema
17+
18+
import com.expediagroup.graalvm.schema.model.InputOnly
19+
import com.expediagroup.graphql.server.types.GraphQLRequest
20+
import org.junit.jupiter.api.Test
21+
import org.junit.jupiter.api.TestInstance
22+
import org.springframework.beans.factory.annotation.Autowired
23+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
24+
import org.springframework.boot.test.context.SpringBootTest
25+
import org.springframework.http.MediaType
26+
import org.springframework.test.web.reactive.server.WebTestClient
27+
28+
@SpringBootTest
29+
@AutoConfigureWebTestClient
30+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
31+
class TypesQueryTest(@Autowired private val testClient: WebTestClient) {
32+
33+
@Test
34+
fun `verify input only query`() {
35+
val request = GraphQLRequest(
36+
query = "query InputOnlyQuery(\$inputArg: InputOnlyInput){ inputTypeQuery(arg: \$inputArg) }",
37+
operationName = "InputOnlyQuery",
38+
variables = mapOf("inputArg" to InputOnly(id = 123))
39+
)
40+
41+
testClient.post()
42+
.uri("/graphql")
43+
.accept(MediaType.APPLICATION_JSON)
44+
.contentType(MediaType.APPLICATION_JSON)
45+
.bodyValue(request)
46+
.exchange()
47+
.expectStatus().isOk
48+
.expectBody()
49+
.jsonPath("$.data.inputTypeQuery").exists()
50+
.jsonPath("$.data.inputTypeQuery").isEqualTo("InputOnly(id=123)")
51+
.jsonPath("$.errors").doesNotExist()
52+
.jsonPath("$.extensions").doesNotExist()
53+
}
54+
}

plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLGraalVmMetadataTask.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ abstract class GraphQLGraalVmMetadataTask : SourceTask() {
8080
throw RuntimeException("attempt to generate SDL failed - missing required supportedPackages property")
8181
}
8282

83-
val targetDirectory = outputDirectory.dir("META-INF/native-image/${project.group}/${project.name}").get().asFile
83+
val targetDirectory = outputDirectory.dir("META-INF/native-image/${project.group}/${project.name}/graphql").get().asFile
8484
if (!targetDirectory.isDirectory && !targetDirectory.mkdirs()) {
8585
throw RuntimeException("failed to generate target resource directory = $targetDirectory")
8686
}

plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/GenerateGraalVmMetadataMojo.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class GenerateGraalVmMetadataMojo : AbstractSourceMojo() {
3434
override lateinit var outputDirectory: File
3535

3636
override fun generate() {
37-
val metadataDirectory = File(outputDirectory, "META-INF/native-image/${project.groupId}/${project.name}")
37+
val metadataDirectory = File(outputDirectory, "META-INF/native-image/${project.groupId}/${project.name}/graphql")
3838
if (!metadataDirectory.isDirectory && !metadataDirectory.mkdirs()) {
3939
throw RuntimeException("failed to create reachability metadata directory")
4040
}

0 commit comments

Comments
 (0)