Skip to content

feat: GraalVM native image support for Spring server #1769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/graalvm-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
branches:
- master
paths:
- 'generator/**'
- 'servers/**'
- 'plugins/**'
- 'integration/graalvm/**'

jobs:
Expand All @@ -20,7 +17,7 @@ jobs:
working-directory: integration/graalvm
strategy:
matrix:
server: ['ktor-graalvm-server', 'maven-graalvm-server']
server: ['ktor-graalvm-server', 'maven-graalvm-server', 'spring-graalvm-server']

steps:
- name: Checkout Repository
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ jobs:
build-examples:
needs: build-libraries
uses: ./.github/workflows/build-examples.yml

graalvm-integration:
needs: build-libraries
uses: ./.github/workflows/graalvm-integration.yml
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ classgraph = "4.8.149"
dataloader = "3.2.0"
federation = "3.0.0"
graphql-java = "20.2"
graalvm = "0.9.20"
graalvm = "0.9.21"
jackson = "2.14.1"
kotlin = "1.7.21"
kotlinx-benchmark = "0.4.4"
Expand All @@ -16,11 +16,11 @@ maven-plugin-api = "3.6.3"
maven-project = "2.2.1"
poet = "1.12.0"
## reactorVersion should be the same reactor-core version pulled from spring-boot-starter-webflux
reactor-core = "3.5.1"
reactor-extensions = "1.2.1"
reactor-core = "3.5.5"
reactor-extensions = "1.2.2"
slf4j = "1.7.36"
spring = "6.0.3"
spring-boot = "3.0.1"
spring = "6.0.8"
spring-boot = "3.0.6"

# test dependencies
# kotlin-compile-testing has to be using the same kotlin version as the kotlinx-serialization compiler
Expand Down
5 changes: 5 additions & 0 deletions integration/graalvm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import java.util.Properties

@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
plugins {
alias(libs.plugins.graalvm.native) apply false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraalVM native plugin dynamically generates some "helper" methods so we need to ensure they are generated just once for whole composite project, otherwise build blows up with classpath issues.

}

allprojects {
repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graalvm.ktor.schema
package com.expediagroup.graalvm.schema

import com.expediagroup.graphql.server.operations.Query
import graphql.schema.DataFetchingEnvironment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ package com.expediagroup.graalvm.ktor

import com.expediagroup.graalvm.hooks.CustomHooks
import com.expediagroup.graalvm.ktor.context.CustomContextFactory
import com.expediagroup.graalvm.ktor.schema.ContextualQuery
import com.expediagroup.graalvm.schema.ArgumentQuery
import com.expediagroup.graalvm.schema.AsyncQuery
import com.expediagroup.graalvm.schema.BasicMutation
import com.expediagroup.graalvm.schema.ContextualQuery
import com.expediagroup.graalvm.schema.CustomScalarQuery
import com.expediagroup.graalvm.schema.EnumQuery
import com.expediagroup.graalvm.schema.ErrorQuery
Expand Down
2 changes: 1 addition & 1 deletion integration/graalvm/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ dependencyResolutionManagement {
includeBuild("../..")

include(":common-graalvm-server")
//include(":spring-graalvm-server")
include(":ktor-graalvm-server")
include(":spring-graalvm-server")
include(":maven-graalvm-server")
52 changes: 52 additions & 0 deletions integration/graalvm/spring-graalvm-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import com.expediagroup.graphql.plugin.gradle.graphql
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
plugins {
alias(libs.plugins.kotlin.jvm)
application
alias(libs.plugins.graalvm.native)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot)
id("com.expediagroup.graphql")
}

dependencies {
implementation("com.expediagroup", "graphql-kotlin-spring-server")
implementation(projects.commonGraalvmServer)
testImplementation(libs.junit.api)
testImplementation(libs.kotlin.test)
testImplementation(libs.spring.boot.test)
}

tasks.test {
useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}

graalvmNative {
toolchainDetection.set(false)
binaries {
named("main") {
verbose.set(true)
}
metadataRepository {
enabled.set(true)
}
}
}

graphql {
graalVm {
packages = listOf("com.expediagroup.graalvm")
mainClassName = "com.expediagroup.graalvm.spring.ApplicationKt"
}
}

tasks.register("buildGraalVmNativeImage") {
dependsOn("build")
dependsOn("nativeCompile")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graalvm.spring

import com.expediagroup.graalvm.hooks.CustomHooks
import com.expediagroup.graalvm.schema.ArgumentQuery
import com.expediagroup.graalvm.schema.AsyncQuery
import com.expediagroup.graalvm.schema.BasicMutation
import com.expediagroup.graalvm.schema.ContextualQuery
import com.expediagroup.graalvm.schema.CustomScalarQuery
import com.expediagroup.graalvm.schema.EnumQuery
import com.expediagroup.graalvm.schema.ErrorQuery
import com.expediagroup.graalvm.schema.IdQuery
import com.expediagroup.graalvm.schema.InnerClassQuery
import com.expediagroup.graalvm.schema.ListQuery
import com.expediagroup.graalvm.schema.PolymorphicQuery
import com.expediagroup.graalvm.schema.ScalarQuery
import com.expediagroup.graalvm.schema.TypesQuery
import com.expediagroup.graalvm.schema.dataloader.ExampleDataLoader
import com.expediagroup.graalvm.schema.model.ExampleInterface
import com.expediagroup.graalvm.schema.model.ExampleUnion
import com.expediagroup.graalvm.schema.model.FirstImpl
import com.expediagroup.graalvm.schema.model.FirstUnionMember
import com.expediagroup.graalvm.schema.model.SecondImpl
import com.expediagroup.graalvm.schema.model.SecondUnionMember
import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory
import com.expediagroup.graphql.generator.GraphQLTypeResolver
import com.expediagroup.graphql.generator.SimpleTypeResolver
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class Application {

@Bean
fun hooks(): CustomHooks = CustomHooks()

@Bean
fun dataLoaderRegistryFactory(): KotlinDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
ExampleDataLoader
)

@Bean
fun typeResolver(): GraphQLTypeResolver = SimpleTypeResolver(
mapOf(
ExampleInterface::class to listOf(FirstImpl::class, SecondImpl::class),
ExampleUnion::class to listOf(FirstUnionMember::class, SecondUnionMember::class)
)
)

// queries
@Bean
fun argumentQuery() = ArgumentQuery()

@Bean
fun asyncQuery() = AsyncQuery()

@Bean
fun contextualQuery() = ContextualQuery()

@Bean
fun customScalarQuery() = CustomScalarQuery()

@Bean
fun enumQuery() = EnumQuery()

@Bean
fun errorQuery() = ErrorQuery()

@Bean
fun idQuery() = IdQuery()

@Bean
fun innerClassQuery() = InnerClassQuery()

@Bean
fun listQuery() = ListQuery()

@Bean
fun polymorphicQuery() = PolymorphicQuery()

@Bean
fun scalarQuery() = ScalarQuery()

@Bean
fun typesQuery() = TypesQuery()

// mutations
@Bean
fun basicMutation() = BasicMutation()
}

fun main(args: Array<String>) {
runApplication<Application>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graalvm.spring.context

import com.expediagroup.graphql.generator.extensions.plus
import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory
import graphql.GraphQLContext
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import java.util.UUID

@Component
class CustomContextFactory : DefaultSpringGraphQLContextFactory() {
override suspend fun generateContext(request: ServerRequest): GraphQLContext {
return super.generateContext(request).plus(mapOf("custom" to (request.headers().firstHeader("X-Custom-Header") ?: UUID.randomUUID().toString())))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
graphql:
packages:
- "com.expediagroup.graalvm"
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.expediagroup.graalvm.spring.schema

import com.expediagroup.graalvm.schema.model.InputOnly
import com.expediagroup.graphql.server.types.GraphQLRequest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient

@SpringBootTest
@AutoConfigureWebTestClient
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TypesQueryTest(@Autowired private val testClient: WebTestClient) {

@Test
fun `verify input only query`() {
val request = GraphQLRequest(
query = "query InputOnlyQuery(\$inputArg: InputOnlyInput){ inputTypeQuery(arg: \$inputArg) }",
operationName = "InputOnlyQuery",
variables = mapOf("inputArg" to InputOnly(id = 123))
)

testClient.post()
.uri("/graphql")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.exchange()
.expectStatus().isOk
.expectBody()
.jsonPath("$.data.inputTypeQuery").exists()
.jsonPath("$.data.inputTypeQuery").isEqualTo("InputOnly(id=123)")
.jsonPath("$.errors").doesNotExist()
.jsonPath("$.extensions").doesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ abstract class GraphQLGraalVmMetadataTask : SourceTask() {
throw RuntimeException("attempt to generate SDL failed - missing required supportedPackages property")
}

val targetDirectory = outputDirectory.dir("META-INF/native-image/${project.group}/${project.name}").get().asFile
val targetDirectory = outputDirectory.dir("META-INF/native-image/${project.group}/${project.name}/graphql").get().asFile
if (!targetDirectory.isDirectory && !targetDirectory.mkdirs()) {
throw RuntimeException("failed to generate target resource directory = $targetDirectory")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class GenerateGraalVmMetadataMojo : AbstractSourceMojo() {
override lateinit var outputDirectory: File

override fun generate() {
val metadataDirectory = File(outputDirectory, "META-INF/native-image/${project.groupId}/${project.name}")
val metadataDirectory = File(outputDirectory, "META-INF/native-image/${project.groupId}/${project.name}/graphql")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraalVM plugin looks for reachability metadata under any META-INF/native-image subdirectories. Per GraalVM documentation they suggest to generate reachability metadata under <groupId>/<artifactId> subdirectory to avoid collisions. Adding explicit graphql subdirectory to avoid collision with SpringBoot plugin generating metadata under the same directory.

if (!metadataDirectory.isDirectory && !metadataDirectory.mkdirs()) {
throw RuntimeException("failed to create reachability metadata directory")
}
Expand Down
Loading