-
Notifications
You must be signed in to change notification settings - Fork 21
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
Add Native Build support #143
Comments
A couple of weeks ago i tried to compile a spring boot based operator with graalvm v21. The operater uses spring cloud dependencies (kubernetes config and service discovery for example). i had to add many build args and RuntimeHints and reflection configuration to get it to work. Not sure how quarkus handles this. i'm wondering why i had to add the fabric8 stuff to it when native builds are supported by quarkus. figured they are using fabric8 as well build.gradle.kts dependencies {
...
// fabric8 native compile
implementation("org.reflections:reflections:0.10.2")
}
graalvmNative {
binaries {
named("main") {
// fabric8 kubernetes client -> okHttp needs all charsets in native
buildArgs.add("-H:+AddAllCharsets")
// experimental features
buildArgs.add("-H:+UnlockExperimentalVMOptions")
//buildArgs.add("--enable-all-security-services")
buildArgs.add("-H:+EnableSecurityServicesFeature")
// @TODO: use gradle path selectors
// because of ReconcilerUtils.loadYaml
buildArgs.add("-H:ReflectionConfigurationFiles=../../resources/main/META-INF/native-image/reflection-config.json")
buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime")
// nice to have
buildArgs.add("-H:+ReportExceptionStackTraces")
buildArgs.add("-H:+PrintClassInitialization")
}
}
} KubernetesClientRuntimeHintsRegistrar.ktimport com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import io.fabric8.kubernetes.api.model.KubernetesResource
import io.fabric8.kubernetes.client.Client
import io.fabric8.kubernetes.client.VersionInfo
import org.apache.commons.logging.LogFactory
import org.reflections.Reflections
import org.springframework.aot.hint.MemberCategory
import org.springframework.aot.hint.RuntimeHints
import org.springframework.aot.hint.RuntimeHintsRegistrar
import org.springframework.aot.hint.registerType
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ImportRuntimeHints
/* the following class is needed to configure fabric8 native compile */
@ImportRuntimeHints(KubernetesClientRuntimeHintsRegistrar::class)
class KubernetesClientRuntimeHintsRegistrar : RuntimeHintsRegistrar {
private val log = LogFactory.getLog(javaClass)
override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
hints.resources().registerPattern("reflection-config.json");
hints.resources().registerPattern("META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource")
hints.resources().registerPattern("META-INF/services/io.fabric8.kubernetes.client.http.HttpClient\$Factory")
hints.reflection().registerType<VersionInfo>(*MemberCategory.values())
registerClients(hints)
registerJacksonKubernetesModels(hints)
}
private fun registerClients(hints: RuntimeHints) {
val clazz = Client::class.java
val reflections = Reflections(clazz.packageName, clazz)
val clients = reflections.getSubTypesOf(Client::class.java) + Client::class.java
for (client in clients) {
hints.reflection().registerType(client, *MemberCategory.values())
log.info { "registering ${client.name} for reflection" }
}
}
private fun registerJacksonKubernetesModels(hints: RuntimeHints) {
val clazz = KubernetesResource::class.java
val reflections = Reflections(clazz.packageName, clazz)
val kubernetesModels = reflections.getSubTypesOf(KubernetesResource::class.java)
val combined = buildSet {
addAll(kubernetesModels)
addAll(resolveSerializationClasses<JsonSerialize>(reflections))
addAll(resolveSerializationClasses<JsonDeserialize>(reflections))
}
for (model in combined) {
hints.reflection().registerType(model, *MemberCategory.values())
log.info { "registering ${model.name} for reflection" }
}
}
/**
* Extracts Jacksons Deserializer / Serializers specified in the classes annotations
*/
private inline fun <reified R : Annotation> resolveSerializationClasses(reflections: Reflections): List<Class<*>> {
val clazz = R::class.java
val method = clazz.getMethod("using")
val classes = reflections.getTypesAnnotatedWith(clazz)
return classes.mapNotNull { clazzWithAnnotation ->
val annotation = clazzWithAnnotation.getAnnotation(clazz)
if (annotation != null) method.invoke(annotation) as Class<*> else null
}
}
} reflection-config.json[ { "name": "io.javaoperatorsdk.operator.processing.retry.GenericRetry", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "pl.ows.springoperator.dependentresource.DeploymentDependentResource", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true }, { "name": "io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentConverter", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "pl.ows.springoperator.dependentresource.ServiceDependentResource", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "java.util.TreeMap", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "pl.ows.springoperator.customresource.SpringAppSpec", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "pl.ows.springoperator.customresource.SpringAppStatus", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "io.fabric8.kubernetes.api.model.IntOrString", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "io.fabric8.kubernetes.api.model.Config", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "io.fabric8.kubernetes.api.model.Preferences", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "io.fabric8.kubernetes.client.impl.KubernetesClientImpl", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true } ] |
Hey @bullshit, many thank's for your input! It's "calming" to see that you had to struggle with similar problems as me.. I used a similar principle. In the beginning I also added single dependencies, but when I added about 40-50 dependencies manually and it still didn't work, I stopped doing that and tried to load necessary dependencies more "generically", but this leads to loading more dependencies than I might really need (but I hope that my solution is now more robust and future changes in fabric8 or josdk won't have any impact so quickly..). Unfortunately my operator application doesn't work with your solution either, I had to load more dependencies via reflection. My setup: I've been playing around with my solution and yours to see the effects of loading more dependencies on the native image size..
I generally
That's my solution: OperatorRuntimeHintsRegistrar.kt
|
Hello @ebma16, did you measure your startup times as well? for me the differences where 7s normal start from IDE and 400ms with a native binary. I'm to new to native builds to understand how quarkus manages to build native when fabric8 does not provide runtime hints. I stumbled across this project https://github.com/kubernetes-native-java/fabric8-spring-native but is outdated and the intention was educational. |
Hello @bullshit, I have not analysed the start time exactly, I have only started the application of the respective build three times in a row (I always created the image manually and loaded it into a local Kubernetes cluster where the application was started -> so same circumstance/config, but different images):
Normal build (408 MB):
I found the same project while researching how to create a native image with fabric8. I also had to adapt it as it still refers to Spring Boot 2, but it was a good basis and took a few tests/investigations off my hands :) |
I'm also very much interested in this. Spring provides APIs to declare runtime hints for native image builds, such as |
I'm want to compile and run my operator as a native image in a setup with Kotlin/Java 21 + Spring Boot 3 + GraalVM.
So far Native Build is currently only explicitly supported by the Quarkus extension. A explicit Native Build support for Spring Boot is missing and has to be added.
If anybody knows already any examples or reference projects/repos that have done the same, please shout out :) Otherwise, I will look into adding support in a reasonable amount of time.
The text was updated successfully, but these errors were encountered: