From 0dbed4902f521d1e44d2b00c146e79b9cd75dcc3 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 11 May 2026 10:04:02 +0000 Subject: [PATCH 1/5] Support java_main_class in JBP_CONFIG_JAVA_MAIN --- src/java/containers/java_main.go | 34 ++++++++++++++++ src/java/containers/java_main_test.go | 57 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index d75b7a1af..e90fa16f6 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -11,6 +11,25 @@ import ( "github.com/cloudfoundry/java-buildpack/src/java/common" ) +type javaMainConfig struct { + JavaMainClass string `yaml:"java_main_class"` + Arguments string `yaml:"arguments"` +} + +func loadJavaMainConfig(log interface{ Warning(string, ...interface{}) }) javaMainConfig { + cfg := javaMainConfig{} + raw := os.Getenv("JBP_CONFIG_JAVA_MAIN") + if raw == "" { + return cfg + } + yamlHandler := common.YamlHandler{} + if err := yamlHandler.ValidateFields([]byte(raw), &cfg); err != nil { + log.Warning("Unknown JBP_CONFIG_JAVA_MAIN values: %s", err.Error()) + } + _ = yamlHandler.Unmarshal([]byte(raw), &cfg) + return cfg +} + // JavaMainContainer handles standalone JAR applications with a main class type JavaMainContainer struct { context *common.Context @@ -29,6 +48,14 @@ func NewJavaMainContainer(ctx *common.Context) *JavaMainContainer { func (j *JavaMainContainer) Detect() (string, error) { buildDir := j.context.Stager.BuildDir() + // JBP_CONFIG_JAVA_MAIN with java_main_class always wins (Ruby parity) + cfg := loadJavaMainConfig(j.context.Log) + if cfg.JavaMainClass != "" { + j.mainClass = cfg.JavaMainClass + j.context.Log.Debug("Detected Java Main application via JBP_CONFIG_JAVA_MAIN: %s", j.mainClass) + return "Java Main", nil + } + // Look for JAR files with Main-Class manifest mainClass, jarFile := j.findMainClass(buildDir) if mainClass != "" { @@ -230,6 +257,13 @@ func (j *JavaMainContainer) buildClasspath() (string, error) { // Release returns the Java Main startup command func (j *JavaMainContainer) Release() (string, error) { + // JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class. + // Use classpath mode so the configured class is actually invoked (not the manifest's). + cfg := loadJavaMainConfig(j.context.Log) + if cfg.JavaMainClass != "" { + return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", cfg.JavaMainClass), nil + } + if j.jarFile != "" { // JAR has its own Main-Class in the manifest — java -jar handles it // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index b5e083e4d..bf00c1fb1 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -197,6 +197,63 @@ var _ = Describe("Java Main Container", func() { }) }) + Context("with JBP_CONFIG_JAVA_MAIN java_main_class overriding manifest Main-Class", func() { + // Ruby parity: config[MAIN_CLASS_PROPERTY] takes precedence over manifest Main-Class + // This is how PropertiesLauncher is used with Spring Boot exploded JARs + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: org.springframework.boot.loader.launch.PropertiesLauncher}") + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: org.springframework.boot.loader.JarLauncher\n", + )).To(Succeed()) + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("uses the configured java_main_class instead of the manifest Main-Class", func() { + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).To(ContainSubstring("org.springframework.boot.loader.launch.PropertiesLauncher")) + Expect(cmd).NotTo(ContainSubstring("JarLauncher")) + }) + + It("uses classpath mode (not java -jar) so the overridden main class is actually invoked", func() { + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).NotTo(ContainSubstring("-jar")) + Expect(cmd).To(ContainSubstring("-cp")) + }) + }) + + Context("with JBP_CONFIG_JAVA_MAIN java_main_class on app with no manifest Main-Class", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.CustomMain}") + // App has no Main-Class in manifest — detection still works via JBP_CONFIG_JAVA_MAIN + os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644) + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("detects as Java Main application", func() { + name, err := container.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("Java Main")) + }) + + It("uses the configured main class", func() { + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).To(ContainSubstring("com.example.CustomMain")) + }) + }) + Context("without main class or JAR", func() { It("returns error", func() { _, err := container.Release() From f2e9e4ddd76aff50d0f696c3f78353d8acf8724a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 11 May 2026 10:08:23 +0000 Subject: [PATCH 2/5] Support arguments in JBP_CONFIG_JAVA_MAIN --- src/java/containers/java_main.go | 14 ++++++++++---- src/java/containers/java_main_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index e90fa16f6..b08432285 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -257,17 +257,23 @@ func (j *JavaMainContainer) buildClasspath() (string, error) { // Release returns the Java Main startup command func (j *JavaMainContainer) Release() (string, error) { + cfg := loadJavaMainConfig(j.context.Log) + + args := "" + if cfg.Arguments != "" { + args = " " + cfg.Arguments + } + // JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class. // Use classpath mode so the configured class is actually invoked (not the manifest's). - cfg := loadJavaMainConfig(j.context.Log) if cfg.JavaMainClass != "" { - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", cfg.JavaMainClass), nil + return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", cfg.JavaMainClass, args), nil } if j.jarFile != "" { // JAR has its own Main-Class in the manifest — java -jar handles it // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", j.jarFile), nil + return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s%s", j.jarFile, args), nil } // Classpath mode: need an explicit main class @@ -281,5 +287,5 @@ func (j *JavaMainContainer) Release() (string, error) { } // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", mainClass), nil + return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args), nil } diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index bf00c1fb1..63c32bbc3 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -254,6 +254,32 @@ var _ = Describe("Java Main Container", func() { }) }) + Context("with JBP_CONFIG_JAVA_MAIN arguments", func() { + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("appends arguments after main class when using java_main_class", func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: com.example.Main, arguments: "--server.port=$PORT"}`) + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).To(ContainSubstring("com.example.Main --server.port=$PORT")) + }) + + It("appends arguments after main class when using manifest Main-Class", func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{arguments: "--foo=bar"}`) + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).To(ContainSubstring("--foo=bar")) + }) + }) + Context("without main class or JAR", func() { It("returns error", func() { _, err := container.Release() From cc32e1de169145cc7e36bd34635f597a4852de9c Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 11 May 2026 10:44:07 +0000 Subject: [PATCH 3/5] Add note on integration test limitation for java_main_class override --- src/integration/java_main_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/integration/java_main_test.go b/src/integration/java_main_test.go index 2a6e958ad..616b2a408 100644 --- a/src/integration/java_main_test.go +++ b/src/integration/java_main_test.go @@ -62,6 +62,16 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing. // Verify buildpack detects and applies explicit main class configuration Expect(logs.String()).To(ContainSubstring("Java Buildpack")) Expect(logs.String()).To(ContainSubstring("Java Main")) + + // NOTE: this test does NOT verify that java_main_class actually overrides the + // manifest Main-Class, because: + // 1. The fixture's MANIFEST.MF already has Main-Class: io.pivotal.SimpleJava + // (same value as JBP_CONFIG_JAVA_MAIN), so the test passes even if the + // config is ignored. + // 2. switchblade's Deployment struct does not expose the release command, + // so we cannot assert -cp vs -jar or which class was used. + // The override behaviour and -cp mode are covered by unit tests in + // src/java/containers/java_main_test.go. }) }) From 073de0a7daa0bfbc571173f255fccaebb5b9ec0a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 11 May 2026 10:46:43 +0000 Subject: [PATCH 4/5] Add notes on integration test limitations for arguments and JAVA_OPTS --- src/integration/java_main_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/integration/java_main_test.go b/src/integration/java_main_test.go index 616b2a408..954ddd83f 100644 --- a/src/integration/java_main_test.go +++ b/src/integration/java_main_test.go @@ -91,6 +91,8 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing. // Verify app can start (validates command with arguments is valid) Eventually(deployment.ExternalURL).ShouldNot(BeEmpty()) + // NOTE: does not verify arguments are actually appended to the command line; + // that is covered by unit tests in src/java/containers/java_main_test.go. }) }) @@ -108,6 +110,8 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing. // Verify buildpack stages successfully with JAVA_OPTS Expect(logs.String()).To(ContainSubstring("Java Buildpack")) Expect(logs.String()).To(ContainSubstring("Java Main")) + // NOTE: does not verify JAVA_OPTS are applied to the JVM command line; + // staging success only confirms the options did not break the build. }) }) From 82888e212d7806ac7ca48a79db596cbd1b4bcf0d Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 11 May 2026 15:51:59 +0000 Subject: [PATCH 5/5] Fix JBP_CONFIG_JAVA_MAIN not taking effect when app is detected as Spring Boot The registry stopped at Spring Boot (highest priority) without ever calling JavaMainContainer.Detect(). If java_main_class is set in JBP_CONFIG_JAVA_MAIN, select Java Main unconditionally before the normal detection loop, so it can override higher-priority containers such as Spring Boot. Also adds Ruby parity for SERVER_PORT: when java_main_class is a Spring Boot launcher (JarLauncher, PropertiesLauncher, WarLauncher), write SERVER_PORT=$PORT to profile.d so the app binds to the CF-assigned port. Example use case: JBP_CONFIG_JAVA_MAIN: '{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher", arguments: "--loader.home=/home/vcap/data"}' Updates container-java_main.md: references JBP_CONFIG_JAVA_MAIN instead of config/java_main.yml, corrects SERVER_PORT behaviour, adds example. --- docs/container-java_main.md | 16 ++++++++++---- src/java/containers/container.go | 20 ++++++++++++++++- src/java/containers/container_test.go | 31 ++++++++++++++++++++++++++ src/java/containers/java_main.go | 25 +++++++++++++++++++++ src/java/containers/java_main_test.go | 32 +++++++++++++++++++++++++-- 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/docs/container-java_main.md b/docs/container-java_main.md index 62d65f14e..f2bcfc92b 100644 --- a/docs/container-java_main.md +++ b/docs/container-java_main.md @@ -10,7 +10,7 @@ Command line arguments may optionally be configured. - + @@ -23,17 +23,25 @@ If the application uses Spring, [Spring profiles][] can be specified by setting ## Spring Boot -If the main class is Spring Boot's `JarLauncher`, `PropertiesLauncher` or `WarLauncher`, the Java Main Container adds a `--server.port` argument to the command so that the application uses the correct port. +If `java_main_class` is set to one of Spring Boot's launchers (`JarLauncher`, `PropertiesLauncher` or `WarLauncher`), the Java Main Container sets `SERVER_PORT=$PORT` so that the application binds to the CF-assigned port. ## Configuration For general information on configuring the buildpack, including how to specify configuration values through environment variables, refer to [Configuration and Extension][]. -The container can be configured by modifying the `config/java_main.yml` file in the buildpack fork. +The container can be configured using the `JBP_CONFIG_JAVA_MAIN` environment variable. | Name | Description | ---- | ----------- | `arguments` | Optional command line arguments to be passed to the Java main class. The arguments are specified as a single YAML scalar in plain style or enclosed in single or double quotes. -| `java_main_class` | Optional Java class name to run. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line. If not specified, the Java Manifest value of `Main-Class` is used. +| `java_main_class` | Optional Java class name to run. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line. If not specified, the Java Manifest value of `Main-Class` is used. Setting this overrides container detection — even Spring Boot apps will use the Java Main container when this is set. + +### Example: PropertiesLauncher with external config + +```yaml +env: + JBP_CONFIG_JAVA_MAIN: '{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher", arguments: "--loader.home=/home/vcap/data"}' + JAVA_OPTS: '-Dloader.path=/home/vcap/data/lib' +``` [Configuration and Extension]: ../README.md#configuration-and-extension [Spring profiles]:http://blog.springsource.com/2011/02/14/spring-3-1-m1-introducing-profile/ diff --git a/src/java/containers/container.go b/src/java/containers/container.go index d525a9916..eab82e50c 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -39,8 +39,26 @@ func (r *Registry) Register(c Container) { r.containers = append(r.containers, c) } -// Detect finds the first container that can handle the application +// Detect finds the first container that can handle the application. +// If JBP_CONFIG_JAVA_MAIN specifies an explicit java_main_class, the Java Main +// container is selected unconditionally — before the normal priority order — +// so it can override higher-priority containers such as Spring Boot. func (r *Registry) Detect() (Container, string, error) { + cfg := loadJavaMainConfig(r.context.Log) + if cfg.JavaMainClass != "" { + for _, container := range r.containers { + if _, ok := container.(*JavaMainContainer); ok { + name, err := container.Detect() + if err != nil { + return nil, "", err + } + if name != "" { + return container, name, nil + } + } + } + } + for _, container := range r.containers { name, err := container.Detect() if err != nil { diff --git a/src/java/containers/container_test.go b/src/java/containers/container_test.go index df67fa514..d130296b3 100644 --- a/src/java/containers/container_test.go +++ b/src/java/containers/container_test.go @@ -84,6 +84,37 @@ var _ = Describe("Container Registry", func() { }) }) + Context("with Spring Boot app and JBP_CONFIG_JAVA_MAIN java_main_class set", func() { + BeforeEach(func() { + // App looks like Spring Boot + os.MkdirAll(filepath.Join(buildDir, "BOOT-INF"), 0755) + os.MkdirAll(filepath.Join(buildDir, "META-INF"), 0755) + manifest := "Manifest-Version: 1.0\nStart-Class: com.example.App\nSpring-Boot-Version: 2.7.0\n" + os.WriteFile(filepath.Join(buildDir, "META-INF", "MANIFEST.MF"), []byte(manifest), 0644) + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher", arguments: "--loader.home=/home/vcap/data"}`) + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("selects Java Main container instead of Spring Boot", func() { + container, name, err := registry.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(container).NotTo(BeNil()) + Expect(name).To(Equal("Java Main")) + }) + + It("uses the configured class and arguments in the start command", func() { + container, _, err := registry.Detect() + Expect(err).NotTo(HaveOccurred()) + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd).To(ContainSubstring("org.springframework.boot.loader.launch.PropertiesLauncher")) + Expect(cmd).To(ContainSubstring("--loader.home=/home/vcap/data")) + }) + }) + Context("with no detectable app", func() { It("returns nil container", func() { container, name, err := registry.Detect() diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index b08432285..99570f339 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -195,6 +195,20 @@ func (j *JavaMainContainer) Supply() error { return nil } +// isSpringBootLauncher returns true if the given class is one of the Spring Boot launchers. +func isSpringBootLauncher(mainClass string) bool { + switch mainClass { + case "org.springframework.boot.loader.JarLauncher", + "org.springframework.boot.loader.WarLauncher", + "org.springframework.boot.loader.PropertiesLauncher", + "org.springframework.boot.loader.launch.JarLauncher", + "org.springframework.boot.loader.launch.WarLauncher", + "org.springframework.boot.loader.launch.PropertiesLauncher": + return true + } + return false +} + // Finalize performs final Java Main configuration func (j *JavaMainContainer) Finalize() error { j.context.Log.BeginStep("Finalizing Java Main") @@ -207,6 +221,17 @@ func (j *JavaMainContainer) Finalize() error { profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", classpath) + // Ruby parity: set SERVER_PORT=$PORT when the main class is a Spring Boot launcher + // so the app binds to the CF-assigned port. + cfg := loadJavaMainConfig(j.context.Log) + mainClass := cfg.JavaMainClass + if mainClass == "" { + mainClass = j.mainClass + } + if isSpringBootLauncher(mainClass) { + profileScript += "export SERVER_PORT=$PORT\n" + } + if err := j.context.Stager.WriteProfileD("java_main.sh", profileScript); err != nil { return fmt.Errorf("failed to write java_main.sh profile.d script: %w", err) } diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index 63c32bbc3..2a0d0f0cd 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -386,16 +386,44 @@ var _ = Describe("Java Main Container", func() { }) }) - Context("with empty build directory", func() { + Context("with Spring Boot launcher in JBP_CONFIG_JAVA_MAIN", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher"}`) + os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644) + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("writes SERVER_PORT=$PORT to profile.d for Ruby parity", func() { + container.Detect() + err := container.Finalize() + Expect(err).NotTo(HaveOccurred()) + + profileScript := filepath.Join(depsDir, "0", "profile.d", "java_main.sh") + data, err := os.ReadFile(profileScript) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(ContainSubstring("export SERVER_PORT=$PORT\n")) + }) + }) + + Context("with non-Spring-Boot main class", func() { BeforeEach(func() { os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644) }) - It("creates minimal classpath", func() { + It("does not write SERVER_PORT to profile.d", func() { container.Detect() err := container.Finalize() Expect(err).NotTo(HaveOccurred()) + + profileScript := filepath.Join(depsDir, "0", "profile.d", "java_main.sh") + data, err := os.ReadFile(profileScript) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).NotTo(ContainSubstring("SERVER_PORT")) }) }) + }) })
Detection CriteriaMain-Class attribute set in META-INF/MANIFEST.MF or java_main_class set in config/java_main.ymlMain-Class attribute set in META-INF/MANIFEST.MF, or java_main_class set in JBP_CONFIG_JAVA_MAIN
Tags