diff --git a/README.md b/README.md index 29c413c..195cca1 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,21 @@ The buildpack will do the following: [c]: https://github.com/buildpacks/spec/blob/main/extensions/bindings.md ## Configuration -| Environment Variable | Description | -|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `$BP_SPRING_CLOUD_BINDINGS_DISABLED` | Whether to contribute Spring Cloud Bindings support to the image at build time. Defaults to false. | -| `$BPL_SPRING_CLOUD_BINDINGS_DISABLED` | Whether to auto-configure Spring Boot environment properties from bindings at runtime. This requires Spring Cloud Bindings to have been installed at build time or it will do nothing. Defaults to false. | +| Environment Variable | Description | +|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `$BP_SPRING_CLOUD_BINDINGS_DISABLED` | Whether to contribute Spring Cloud Bindings support to the image at build time. Defaults to false. | +| `$BPL_SPRING_CLOUD_BINDINGS_DISABLED` | Whether to auto-configure Spring Boot environment properties from bindings at runtime. This requires Spring Cloud Bindings to have been installed at build time or it will do nothing. Defaults to false. | | `$BPL_SPRING_CLOUD_BINDINGS_ENABLED` | Deprecated in favour of `$BPL_SPRING_CLOUD_BINDINGS_DISABLED`. Whether to auto-configure Spring Boot environment properties from bindings at runtime. This requires Spring Cloud Bindings to have been installed at build time or it will do nothing. Defaults to true. | -| `$BP_SPRING_CLOUD_BINDINGS_VERSION` | Explicit version of Spring Cloud Bindings library to install. | -| `$BP_SPRING_AOT_ENABLED` | Whether to contribute `$BPL_SPRING_AOT_ENABLED` at runtime. Beware that the Spring Boot app needs to have been AOT instrumented (presence of `META-INF/native-image`) too. Defaults to false. | -| `$BPL_SPRING_AOT_ENABLED` | Whether to contribute `-Dspring.aot.enabled=true` to `JAVA_TOOL_OPTIONS` at runtime. Defaults to yes if the above conditions were met; false otherwise | -| `$BP_JVM_CDS_ENABLED` | Whether to perform the CDS training run (that will generate the caching file `application.jsa`). Defaults to false. | -| `$CDS_TRAINING_JAVA_TOOL_OPTIONS` | Allow the user to override the default `JAVA_TOOL_OPTIONS`, only for the CDS training run. Useful to configure your app not to reach external services during training run for example. | -| `$BPL_JVM_CDS_ENABLED` | Whether to load the CDS caching file (`-XX:SharedArchiveFile=application.jsa`) that was generated during the CDS training run. Defaults to the value of `BP_JVM_CDS_ENABLED` | +| `$BP_SPRING_CLOUD_BINDINGS_VERSION` | Explicit version of Spring Cloud Bindings library to install. | +| `$BP_SPRING_AOT_ENABLED` | Whether to contribute `$BPL_SPRING_AOT_ENABLED` at runtime. Beware that the Spring Boot app needs to have been AOT instrumented (presence of `META-INF/native-image`) too. Defaults to false. | +| `$BPL_SPRING_AOT_ENABLED` | Whether to contribute `-Dspring.aot.enabled=true` to `JAVA_TOOL_OPTIONS` at runtime. Defaults to yes if the above conditions were met; false otherwise | +| `$BP_UNPACK_LAYOUT_ONLY` | Whether to only unpack the Spring Boot app, and not apply a CDS / AOT Cache training run | +| `$BP_JVM_CDS_ENABLED` | Deprecated, use `BP_JVM_AOTCACHE_ENABLED` - Whether to perform the CDS training run (that will generate the caching file `application.jsa`). Defaults to false. | +| `$BPL_JVM_CDS_ENABLED` | Deprecated, use `BPL_JVM_AOTCACHE_ENABLED` - Whether to load the CDS caching file (`-XX:SharedArchiveFile=application.jsa`) that was generated during the CDS training run. Defaults to the value of `BP_JVM_CDS_ENABLED` | +| `$BP_JVM_AOTCACHE_ENABLED` | Whether to perform the AOT Cache training run (that will generate the caching file `application.jsa`). Defaults to false. | +| `$BPL_JVM_AOTCACHE_ENABLED` | Whether to load the CDS caching file (`-XX:SharedArchiveFile=application.jsa`) that was generated during the CDS training run. Defaults to the value of `BP_JVM_CDS_ENABLED` | +| `$CDS_TRAINING_JAVA_TOOL_OPTIONS` | Deprecated, use `TRAINING_RUN_JAVA_TOOL_OPTIONS` - Allow the user to override the default `JAVA_TOOL_OPTIONS`, only for the CDS training run. Useful to configure your app not to reach external services during training run for example. | +| `$TRAINING_RUN_JAVA_TOOL_OPTIONS` | Allow the user to override the default `JAVA_TOOL_OPTIONS`, only for training run. Useful to configure your app not to reach external services during training run for example. | ## Bindings The buildpack optionally accepts the following bindings: diff --git a/boot/build.go b/boot/build.go index ff3b095..f2655a3 100644 --- a/boot/build.go +++ b/boot/build.go @@ -55,6 +55,14 @@ const ( NestedFileSystemProvider = "org.springframework.boot.loader.nio.file.NestedFileSystemProvider" ) +type SpringPerformanceType int + +const ( + Without SpringPerformanceType = iota + ExtractLayout + CdsAotCache +) + type Build struct { Logger bard.Logger Executor effect.Executor @@ -74,7 +82,14 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) } - trainingRun := sherpa.ResolveBool("BP_JVM_CDS_ENABLED") + performanceType := Without + if sherpa.ResolveBool("BP_UNPACK_LAYOUT_ONLY") { + performanceType = ExtractLayout + } + + if sherpa.ResolveBool("BP_JVM_CDS_ENABLED") || sherpa.ResolveBool("BP_JVM_AOTCACHE_ENABLED") { + performanceType = CdsAotCache + } version, versionFound := manifest.Get("Spring-Boot-Version") if !versionFound { @@ -90,12 +105,12 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } mainClass, _ := manifest.Get("Main-Class") - if trainingRun { + if performanceType == CdsAotCache || performanceType == ExtractLayout { if bootCDSExtractionSupported(version) { reZipExplodedJar = true } else { - b.Logger.Bodyf("You enabled CDS optimization with BP_JVM_CDS_ENABLED=true but your Spring Boot app version is: %s, you need to upgrade to Spring Boot >= 3.3 first!\nCancelling CDS optimization", version) - trainingRun = false + b.Logger.Bodyf("You enabled CDS_AOTCACHE optimization or extract mode only but your Spring Boot app version is: %s, you need to upgrade to Spring Boot >= 3.3 first!\nCancelling performance optimization", version) + performanceType = Without } } @@ -232,13 +247,13 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { b.Logger.Bodyf("unable to find AOT processed dir %s, however BP_SPRING_AOT_ENABLED has been set to true. Ensure that your app is AOT processed", dir) } - cdsTrainingJavaToolOptions := sherpa.GetEnvWithDefault("CDS_TRAINING_JAVA_TOOL_OPTIONS", "") - if trainingRun || aotEnabled { + cdsTrainingJavaToolOptions := sherpa.GetEnvWithDefault("TRAINING_RUN_JAVA_TOOL_OPTIONS", sherpa.GetEnvWithDefault("CDS_TRAINING_JAVA_TOOL_OPTIONS", "")) + if aotEnabled || performanceType == CdsAotCache || performanceType == ExtractLayout { helpers = append(helpers, "performance") cdsTrainingJavaToolOptionsProvided := cdsTrainingJavaToolOptions != "" - if cdsTrainingJavaToolOptionsProvided && trainingRun && aotEnabled { + if cdsTrainingJavaToolOptionsProvided && performanceType == CdsAotCache && aotEnabled { b.Logger.Infof(color.RedString("ERROR: CDS_TRAINING_JAVA_TOOL_OPTIONS is not compatible with BP_SPRING_AOT_ENABLED - as the AOT classes used during training run won't be compatible with a different set of JAVA_TOOL_OPTIONS at runtime \n" + "The Spring team explains this issue in detail here: https://github.com/spring-projects/spring-boot/issues/41348 \n" + "If you need to provide CDS_TRAINING_JAVA_TOOL_OPTIONS (to disable a connection to a remote service for example), you need to disable BP_SPRING_AOT_ENABLED ")) @@ -249,7 +264,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { cdsTrainingJavaToolOptions = sherpa.GetEnvWithDefault("JAVA_TOOL_OPTIONS", "") } - if trainingRun { + if performanceType == CdsAotCache || performanceType == ExtractLayout { mainClass, _ = manifest.Get("Start-Class") classpathString = "runner.jar" if len(additionalLibs) > 0 { @@ -261,7 +276,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - cdsLayer := NewSpringPerformance(dc, context.Application.Path, manifest, aotEnabled, trainingRun, classpathString, reZipExplodedJar, cdsTrainingJavaToolOptions) + cdsLayer := NewSpringPerformance(dc, context.Application.Path, manifest, aotEnabled, performanceType, classpathString, reZipExplodedJar, cdsTrainingJavaToolOptions) cdsLayer.Logger = b.Logger result.Layers = append(result.Layers, cdsLayer) @@ -300,7 +315,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { at.Logger = b.Logger result.Layers = append(result.Layers, at) - if !trainingRun { + if performanceType == Without { // Slices if index, ok := manifest.Get("Spring-Boot-Layers-Index"); ok { b.Logger.Header("Creating slices from layers index") @@ -314,7 +329,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { result = b.contributeHelpers(context, result, helpers) } - if bootJarFound || trainingRun { + if bootJarFound || performanceType == CdsAotCache || performanceType == ExtractLayout { if mainClass != "" { result.Processes = append(result.Processes, b.setProcessTypes(mainClass, classpathString)...) } else { diff --git a/boot/build_test.go b/boot/build_test.go index 8a6e726..bcf3f5d 100644 --- a/boot/build_test.go +++ b/boot/build_test.go @@ -678,7 +678,7 @@ Spring-Boot-Lib: BOOT-INF/lib ctx.Buildpack.API = "0.6" - it("contributes CDS layer & helper for Boot 3.3+ apps", func() { + it("contributes CdsAotCache layer & helper for Boot 3.3+ apps", func() { Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` Spring-Boot-Version: 3.3.1 Start-Class: test-class @@ -694,7 +694,7 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Layers[2].(libpak.HelperLayerContributor).Names).To(Equal([]string{"performance"})) }) - it("contributes CDS layer & helper for Boot 3.3+ apps even when they're jar'ed", func() { + it("contributes CdsAotCache layer & helper for Boot 3.3+ apps even when they're jar'ed", func() { Copy("cds", "spring-app-3.3-no-dependencies.jar", "") @@ -706,7 +706,7 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Layers[2].(libpak.HelperLayerContributor).Names).To(Equal([]string{"performance"})) }) - it("does not contribute CDS layer & helper for Boot < 3.3 apps", func() { + it("does not contribute CdsAotCache layer & helper for Boot < 3.3 apps", func() { Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` Spring-Boot-Version: 3.2.1 Start-Class: test-class @@ -721,7 +721,7 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Layers[0].Name()).To(Equal("web-application-type")) }) - it("contributes CDS layer & helper for Boot 3.3+ apps with BP_SPRING_AOT_ENABLED and CDS_TRAINING_JAVA_TOOL_OPTIONS not set", func() { + it("contributes CdsAotCache layer & helper for Boot 3.3+ apps with BP_SPRING_AOT_ENABLED and CDS_TRAINING_JAVA_TOOL_OPTIONS not set", func() { t.Setenv("BP_SPRING_AOT_ENABLED", "true") Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` Spring-Boot-Version: 3.3.1 @@ -756,7 +756,7 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(err.Error()).To(Equal("build failed because of invalid user configuration")) }) - it("contributes CDS layer & helper for Boot 3.3+ apps with CDS_TRAINING_JAVA_TOOL_OPTIONS but BP_SPRING_AOT_ENABLED is disabled", func() { + it("contributes CdsAotCache layer & helper for Boot 3.3+ apps with CDS_TRAINING_JAVA_TOOL_OPTIONS but BP_SPRING_AOT_ENABLED is disabled", func() { t.Setenv("BP_SPRING_AOT_ENABLED", "false") t.Setenv("CDS_TRAINING_JAVA_TOOL_OPTIONS", "user-cds-opt") @@ -782,7 +782,7 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Layers[2].(libpak.HelperLayerContributor).Names).To(Equal([]string{"performance"})) }) - it("contributes CDS layer & helper for Boot 3.3+ apps with BP_SPRING_AOT_ENABLED and JAVA_TOOL_OPTIONS set", func() { + it("contributes CdsAotCache layer & helper for Boot 3.3+ apps with BP_SPRING_AOT_ENABLED and JAVA_TOOL_OPTIONS set", func() { t.Setenv("BP_SPRING_AOT_ENABLED", "true") t.Setenv("CDS_TRAINING_JAVA_TOOL_OPTIONS", "default-opt") diff --git a/boot/detect.go b/boot/detect.go index f2f8537..2483ec7 100644 --- a/boot/detect.go +++ b/boot/detect.go @@ -53,7 +53,7 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error }, Requires: []libcnb.BuildPlanRequire{ {Name: PlanEntryJVMApplication}, - {Name: PlanEntrySpringBoot} }, + {Name: PlanEntrySpringBoot}}, }, }, } @@ -75,7 +75,7 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error Requires: []libcnb.BuildPlanRequire{ {Name: PlanEntryJVMApplication}, {Name: PlanEntrySpringBoot}, - // Require a JRE at build time to perform CDS training run + // Require a JRE at build time to perform CdsAotCache training run {Name: PlanEntryJRE, Metadata: map[string]interface{}{"build": true}}, }, }, diff --git a/boot/spring_performance.go b/boot/spring_performance.go index f81981b..26ae595 100644 --- a/boot/spring_performance.go +++ b/boot/spring_performance.go @@ -42,13 +42,13 @@ type SpringPerformance struct { AppPath string Manifest *properties.Properties AotEnabled bool - DoTrainingRun bool + PerformanceType SpringPerformanceType ClasspathString string ReZip bool TrainingRunJavaToolOptions string } -func NewSpringPerformance(cache libpak.DependencyCache, appPath string, manifest *properties.Properties, aotEnabled bool, doTrainingRun bool, classpathString string, reZip bool, trainingRunJavaToolOptions string) SpringPerformance { +func NewSpringPerformance(cache libpak.DependencyCache, appPath string, manifest *properties.Properties, aotEnabled bool, performanceType SpringPerformanceType, classpathString string, reZip bool, trainingRunJavaToolOptions string) SpringPerformance { contributor := libpak.NewLayerContributor("Performance", cache, libcnb.LayerTypes{ Build: true, Launch: true, @@ -59,7 +59,7 @@ func NewSpringPerformance(cache libpak.DependencyCache, appPath string, manifest AppPath: appPath, Manifest: manifest, AotEnabled: aotEnabled, - DoTrainingRun: doTrainingRun, + PerformanceType: performanceType, TrainingRunJavaToolOptions: trainingRunJavaToolOptions, ClasspathString: classpathString, ReZip: reZip, @@ -72,12 +72,12 @@ func (s SpringPerformance) Contribute(layer libcnb.Layer) (libcnb.Layer, error) layer.LaunchEnvironment.Default("BPL_SPRING_AOT_ENABLED", s.AotEnabled) - if !s.DoTrainingRun { + if s.PerformanceType == Without { return layer, nil + } else if s.PerformanceType == CdsAotCache { + layer.LaunchEnvironment.Default("BPL_JVM_CDS_ENABLED", true) } - layer.LaunchEnvironment.Default("BPL_JVM_CDS_ENABLED", s.DoTrainingRun) - // prepare the training run JVM opts var trainingRunArgs []string @@ -110,9 +110,14 @@ func (s SpringPerformance) Contribute(layer libcnb.Layer) (libcnb.Layer, error) javaCommand := JavaCommand() - if err := s.springBootJarCDSLayoutExtract(javaCommand, jarPath); err != nil { + if err := s.springBootJarLayoutExtract(javaCommand, jarPath); err != nil { return layer, fmt.Errorf("error extracting Boot jar at %s\n%w", jarPath, err) } + + if s.PerformanceType == ExtractLayout { + return layer, nil + } + startClassValue, _ := s.Manifest.Get("Start-Class") if err := fs.WalkDir(os.DirFS(s.AppPath), ".", func(path string, d fs.DirEntry, err error) error { @@ -126,13 +131,19 @@ func (s SpringPerformance) Contribute(layer libcnb.Layer) (libcnb.Layer, error) return libcnb.Layer{}, err } - trainingRunArgs = append(trainingRunArgs, - "-Dspring.context.exit=onRefresh", - "-XX:ArchiveClassesAtExit=application.jsa", - "-cp", - ) - trainingRunArgs = append(trainingRunArgs, s.ClasspathString) - trainingRunArgs = append(trainingRunArgs, startClassValue) + jreVersion, err := JavaMajorVersionFromJRE(s.Executor) + if err != nil { + return layer, fmt.Errorf("error extracting finding out Java Version\n%w", err) + } + + trainingRunArgs = append(trainingRunArgs, "-Dspring.context.exit=onRefresh") + if jreVersion >= 25 { + // we can use https://openjdk.org/jeps/514 + trainingRunArgs = append(trainingRunArgs, "-XX:AOTCacheOutput=application.aot -Xlog:cds") + } else { + trainingRunArgs = append(trainingRunArgs, "-XX:ArchiveClassesAtExit=application.jsa -Xlog:cds") + } + trainingRunArgs = append(trainingRunArgs, "-cp", s.ClasspathString, startClassValue) var trainingRunEnvVariables []string @@ -141,7 +152,7 @@ func (s SpringPerformance) Contribute(layer libcnb.Layer) (libcnb.Layer, error) trainingRunEnvVariables = append(trainingRunEnvVariables, fmt.Sprintf("JAVA_TOOL_OPTIONS=%s", s.TrainingRunJavaToolOptions)) } - // perform the training run, application.dsa, the cache file, will be created + // perform the training run, application.dsa or .aot, the cache file, will be created if err := s.Executor.Execute(effect.Execution{ Command: javaCommand, Env: trainingRunEnvVariables, @@ -157,7 +168,7 @@ func (s SpringPerformance) Contribute(layer libcnb.Layer) (libcnb.Layer, error) }) if err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to contribute spring-cds layer\n%w", err) + return libcnb.Layer{}, fmt.Errorf("unable to contribute spring-performance layer\n%w", err) } return layer, nil } @@ -166,7 +177,7 @@ func (s SpringPerformance) Name() string { return s.LayerContributor.Name } -func (s SpringPerformance) springBootJarCDSLayoutExtract(javaCommand string, jarPath string) error { +func (s SpringPerformance) springBootJarLayoutExtract(javaCommand string, jarPath string) error { s.Logger.Bodyf("Extracting Jar") if err := s.Executor.Execute(effect.Execution{ Command: javaCommand, diff --git a/boot/spring_performance_test.go b/boot/spring_performance_test.go index 776bc62..79406ab 100644 --- a/boot/spring_performance_test.go +++ b/boot/spring_performance_test.go @@ -39,10 +39,10 @@ func testSpringPerformance(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - ctx libcnb.BuildContext - executor *mocks.Executor - aotEnabled bool - cdsEnabled bool + ctx libcnb.BuildContext + executor *mocks.Executor + aotEnabled bool + performanceType boot.SpringPerformanceType ) it.Before(func() { @@ -63,11 +63,13 @@ func testSpringPerformance(t *testing.T, context spec.G, it spec.S) { it.After(func() { Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) - aotEnabled, cdsEnabled = false, false + aotEnabled = false + performanceType = boot.Without }) - it("contributes Spring Performance for Boot 3.3+, both CDS & AOT enabled", func() { - aotEnabled, cdsEnabled = true, true + it("contributes Spring Performance for Boot 3.3+, both CdsAotCache & AOT enabled", func() { + aotEnabled = true + performanceType = boot.CdsAotCache dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -79,7 +81,7 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") @@ -102,7 +104,8 @@ Spring-Boot-Lib: BOOT-INF/lib }) it("contributes Spring Performance for Boot 3.3+, AOT only enabled", func() { - aotEnabled, cdsEnabled = true, false + aotEnabled = true + performanceType = boot.Without dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -114,7 +117,7 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") @@ -131,8 +134,9 @@ Spring-Boot-Lib: BOOT-INF/lib }) - it("contributes Spring Performance for Boot 3.3+, CDS only enabled", func() { - aotEnabled, cdsEnabled = false, true + it("contributes Spring Performance for Boot 3.3+, extract layout only enabled", func() { + aotEnabled = false + performanceType = boot.ExtractLayout dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -144,7 +148,45 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") + s.Executor = executor + + layer, err := ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + + layer, err = s.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(layer.LaunchEnvironment["BPL_SPRING_AOT_ENABLED.default"]).To(Equal("false")) + Expect(layer.LaunchEnvironment["BPL_JVM_CDS_ENABLED.default"]).To(Equal("")) + Expect(executor.Calls).To(HaveLen(1)) + + e, ok := executor.Calls[0].Arguments[0].(effect.Execution) + Expect(ok).To(BeTrue()) + Expect(e.Args).NotTo(ContainElement("-Dspring.aot.enabled=true")) + Expect(e.Args).NotTo(ContainElements("-Dspring.context.exit=onRefresh", + "-XX:ArchiveClassesAtExit=application.jsa", "-cp")) + Expect(e.Args).To(ContainElement("-Djarmode=tools")) + + Expect(layer.Build).To(BeTrue()) + + }) + + it("contributes Spring Performance for Boot 3.3+, CdsAotCache only enabled", func() { + aotEnabled = false + performanceType = boot.CdsAotCache + dc := libpak.DependencyCache{CachePath: "testdata"} + executor.On("Execute", mock.Anything).Return(nil) + + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` +Spring-Boot-Version: 3.3.1 +Spring-Boot-Classes: BOOT-INF/classes +Spring-Boot-Lib: BOOT-INF/lib +`), 0644)).To(Succeed()) + props, err := libjvm.NewManifest(ctx.Application.Path) + Expect(err).NotTo(HaveOccurred()) + + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") @@ -169,8 +211,8 @@ Spring-Boot-Lib: BOOT-INF/lib it("contributes user-provided JAVA_TOOL_OPTIONS to training run", func() { Expect(os.Setenv("JAVA_TOOL_OPTIONS", "default-opt")).To(Succeed()) - - aotEnabled, cdsEnabled = false, true + aotEnabled = false + performanceType = boot.CdsAotCache dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -182,7 +224,7 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "user-cds-opt") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "user-cds-opt") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") @@ -202,8 +244,9 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(os.Unsetenv("CDS_TRAINING_JAVA_TOOL_OPTIONS")).To(Succeed()) }) - it("contributes Spring Performance for Boot 3.3+, both CDS & AOT enabled - with SCB symlink", func() { - aotEnabled, cdsEnabled = true, true + it("contributes Spring Performance for Boot 3.3+, both CdsAotCache & AOT enabled - with SCB symlink", func() { + aotEnabled = true + performanceType = boot.CdsAotCache dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -221,7 +264,7 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") @@ -252,7 +295,8 @@ Spring-Boot-Lib: BOOT-INF/lib it("fails with a non existing JRE_HOME path", func() { Expect(os.Setenv("JRE_HOME", "/that/does/not/exist")).To(Succeed()) - aotEnabled, cdsEnabled = true, true + aotEnabled = true + performanceType = boot.CdsAotCache dc := libpak.DependencyCache{CachePath: "testdata"} executor.On("Execute", mock.Anything).Return(nil) @@ -264,7 +308,7 @@ Spring-Boot-Lib: BOOT-INF/lib props, err := libjvm.NewManifest(ctx.Application.Path) Expect(err).NotTo(HaveOccurred()) - s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, cdsEnabled, "", true, "") + s := boot.NewSpringPerformance(dc, ctx.Application.Path, props, aotEnabled, performanceType, "", true, "") s.Executor = executor layer, err := ctx.Layers.Layer("test-layer") diff --git a/buildpack.toml b/buildpack.toml index 9353cf1..ceef70f 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -67,6 +67,12 @@ api = "0.7" description = "whether to enable Spring AOT at runtime" name = "BPL_SPRING_AOT_ENABLED" + [[metadata.configurations]] + build = true + default = "false" + description = "whether to unpack (-Djarmode=tools [...] extract) the Spring Boot archive and run it in extracted mode" + name = "BP_UNPACK_LAYOUT_ONLY" + [[metadata.configurations]] build = true default = "false" @@ -79,6 +85,18 @@ api = "0.7" description = "whether to enable CDS optimizations at runtime" name = "BPL_JVM_CDS_ENABLED" + [[metadata.configurations]] + build = true + default = "false" + description = "whether to enable Spring AOT Cache & perform JVM training run" + name = "BP_JVM_AOTCACHE_ENABLED" + + [[metadata.configurations]] + build = true + default = "false" + description = "whether to enable Spring AOT Cache optimizations at runtime" + name = "BPL_JVM_AOTCACHE_ENABLED" + [[metadata.dependencies]] cpes = ["cpe:2.3:a:vmware:spring_cloud_bindings:1.13.0:*:*:*:*:*:*:*"] id = "spring-cloud-bindings" diff --git a/helper/spring_performance.go b/helper/spring_performance.go index 7a463c3..59e4e7b 100644 --- a/helper/spring_performance.go +++ b/helper/spring_performance.go @@ -17,6 +17,7 @@ package helper import ( + "os" "github.com/paketo-buildpacks/libpak/bard" "github.com/paketo-buildpacks/libpak/sherpa" @@ -29,20 +30,34 @@ type SpringPerformance struct { func (s SpringPerformance) Execute() (map[string]string, error) { var values []string aot := sherpa.ResolveBool("BPL_SPRING_AOT_ENABLED") - cds := sherpa.ResolveBool("BPL_JVM_CDS_ENABLED") - if !aot && !cds{ + aotCache := sherpa.ResolveBool("BPL_JVM_CDS_ENABLED") || sherpa.ResolveBool("BPL_JVM_AOTCACHE_ENABLED") + if !aot && !aotCache { return nil, nil } - + if aot { s.Logger.Info("Spring AOT Enabled, contributing -Dspring.aot.enabled=true to JAVA_TOOL_OPTIONS") values = append(values, "-Dspring.aot.enabled=true") } - if cds { - s.Logger.Info("Spring CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_TOOL_OPTIONS") - values = append(values, "-XX:SharedArchiveFile=application.jsa") + if aotCache { + + applicationJsa := "application.jsa" + applicationAot := "application.aot" + + if _, errJsa := os.Stat(applicationJsa); errJsa == nil { + s.Logger.Info("Spring CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_TOOL_OPTIONS") + values = append(values, "-XX:SharedArchiveFile=application.jsa") + } else { + if _, errAot := os.Stat(applicationAot); errAot == nil { + s.Logger.Info("Spring AOT Cache Enabled, contributing -XX:AOTCache=application.aot to JAVA_TOOL_OPTIONS") + values = append(values, "-XX:AOTCache=application.aot") + } else { + s.Logger.Info("Something went wrong, neither application.jsa nor application.aot found, CDS/AOT Cache optimization disabled") + s.Logger.Infof("Errors looking for application.jsa and application.aot: %v - %v", errJsa, errAot) + } + } } opts := sherpa.AppendToEnvVar("JAVA_TOOL_OPTIONS", " ", values...) return map[string]string{"JAVA_TOOL_OPTIONS": opts}, nil -} \ No newline at end of file +}