diff --git a/src/integration/spring_boot_test.go b/src/integration/spring_boot_test.go index fd4afe9e1..c8344daec 100644 --- a/src/integration/spring_boot_test.go +++ b/src/integration/spring_boot_test.go @@ -114,7 +114,6 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin // Verify Container Security Provider opts use runtime paths ($DEPS_DIR), not staging paths Eventually(deployment).Should(matchers.Serve(And( - ContainSubstring("-Xbootclasspath/a:"), Not(ContainSubstring("/tmp/contents")), // Should NOT have staging path ContainSubstring("-Djava.security.properties="), )).WithEndpoint("/jvm-args")) @@ -199,7 +198,6 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin ContainSubstring("-Xmx384M"), ContainSubstring("customProp=testValue"), // Framework 2: Container Security Provider opts - ContainSubstring("-Xbootclasspath/a:"), ContainSubstring("-Djava.security.properties="), // Framework 3: Debug opts (JDWP agent) ContainSubstring("-agentlib:jdwp="), @@ -243,7 +241,6 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin // Configured opts from buildpack (Framework 1) ContainSubstring("configProp=fromBuildpack"), // Framework 2: Container Security Provider opts - ContainSubstring("-Xbootclasspath/a:"), // Framework 3: Debug opts (JDWP agent) ContainSubstring("-agentlib:jdwp="), // Framework 4: JRebel opts diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index 9d4df698d..d6016d52b 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -246,15 +246,17 @@ func (s *SpringBootContainer) Release() (string, error) { // Verify this is actually a Spring Boot application if s.isSpringBootExplodedJar(buildDir) { - // True Spring Boot exploded JAR - use JarLauncher - // Determine the correct JarLauncher class name based on Spring Boot version - jarLauncherClass := s.getJarLauncherClass(buildDir) + // True Spring Boot exploded JAR - use main class from manifest or fallback to JarLauncher based on spring-boot version + launcherClass := s.getLauncherClass(buildDir) // 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 $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", jarLauncherClass), nil + return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", launcherClass), nil } // Exploded JAR but NOT Spring Boot - use Main-Class from MANIFEST.MF - mainClass := s.readMainClassFromManifest(buildDir) + mainClass, err := s.readMainClassFromManifest(buildDir) + if err != nil { + s.context.Log.Debug("Could not read MANIFEST.MF: %s", err.Error()) + } if mainClass != "" { // Use classpath from BOOT-INF/classes and BOOT-INF/lib // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) @@ -288,15 +290,14 @@ func (s *SpringBootContainer) Release() (string, error) { // isSpringBootExplodedJar checks if an exploded JAR is actually a Spring Boot application // by looking for Spring Boot-specific markers in MANIFEST.MF func (s *SpringBootContainer) isSpringBootExplodedJar(buildDir string) bool { - manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF") - data, err := os.ReadFile(manifestPath) + manifestData, err := s.readManifestFile(buildDir) if err != nil { s.context.Log.Debug("Could not read MANIFEST.MF: %s", err.Error()) return false } // Parse MANIFEST.MF and look for Spring Boot markers - lines := strings.Split(string(data), "\n") + lines := strings.Split(manifestData, "\n") for _, line := range lines { line = strings.TrimSpace(line) @@ -318,70 +319,66 @@ func (s *SpringBootContainer) isSpringBootExplodedJar(buildDir string) bool { return false } -// readMainClassFromManifest reads the Main-Class entry from MANIFEST.MF -func (s *SpringBootContainer) readMainClassFromManifest(buildDir string) string { +func (s *SpringBootContainer) readManifestFile(buildDir string) (string, error) { manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF") data, err := os.ReadFile(manifestPath) if err != nil { s.context.Log.Debug("Could not read MANIFEST.MF: %s", err.Error()) - return "" + return "", err } + return string(data), nil +} +func (s *SpringBootContainer) readManifestField(manifest, field string) string { // Parse MANIFEST.MF file (simple line-by-line parsing) - lines := strings.Split(string(data), "\n") + lines := strings.Split(manifest, "\n") for _, line := range lines { line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Main-Class:") { - mainClass := strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:")) - s.context.Log.Debug("Found Main-Class in MANIFEST.MF: %s", mainClass) - return mainClass + if strings.HasPrefix(line, field) { + fieldValue := strings.TrimSpace(strings.TrimPrefix(line, field)) + s.context.Log.Debug("Found %s in MANIFEST.MF with value: %s", field, fieldValue) + return fieldValue } } return "" } -// getJarLauncherClass returns the correct JarLauncher class name based on Spring Boot version +// readMainClassFromManifest reads the Main-Class entry from MANIFEST.MF +func (s *SpringBootContainer) readMainClassFromManifest(buildDir string) (string, error) { + manifestData, err := s.readManifestFile(buildDir) + if err != nil { + return "", err + } + + return s.readManifestField(manifestData, "Main-Class:"), nil +} + +// getLauncherClass returns the launcher class from manifest. If missing tries to determine correct JarLauncher class name +// based on Spring Boot version // Spring Boot 2.x uses: org.springframework.boot.loader.JarLauncher // Spring Boot 3.x uses: org.springframework.boot.loader.launch.JarLauncher -func (s *SpringBootContainer) getJarLauncherClass(buildDir string) string { - manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF") - data, err := os.ReadFile(manifestPath) +func (s *SpringBootContainer) getLauncherClass(buildDir string) string { + manifestData, err := s.readManifestFile(buildDir) if err != nil { s.context.Log.Debug("Could not read MANIFEST.MF for version detection: %s", err.Error()) // Default to Spring Boot 3.x (newer) launcher return "org.springframework.boot.loader.launch.JarLauncher" } - // Parse MANIFEST.MF to get Main-Class which tells us the actual launcher class - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Main-Class:") { - mainClass := strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:")) - s.context.Log.Debug("Found Main-Class in MANIFEST.MF: %s", mainClass) - - // If Main-Class is set to JarLauncher, use that exact class - if strings.Contains(mainClass, "JarLauncher") { - return mainClass - } - } + // Return launcher class from manifest. If missing try using JarLauncher based on Spring-Boot-Version + mainClass := s.readManifestField(manifestData, "Main-Class:") + if mainClass != "" { + return mainClass } - // If we couldn't determine from Main-Class, check Spring Boot version - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Spring-Boot-Version:") { - version := strings.TrimSpace(strings.TrimPrefix(line, "Spring-Boot-Version:")) - s.context.Log.Debug("Found Spring-Boot-Version: %s", version) - - // Spring Boot 3.x changed the loader package structure - if strings.HasPrefix(version, "3.") { - return "org.springframework.boot.loader.launch.JarLauncher" - } - // Spring Boot 2.x uses the old loader package - return "org.springframework.boot.loader.JarLauncher" - } + springBootVersion := s.readManifestField(manifestData, "Spring-Boot-Version:") + if strings.HasPrefix(springBootVersion, "3.") { + return "org.springframework.boot.loader.launch.JarLauncher" + } + // Spring Boot 2.x uses the old loader package + if strings.HasPrefix(springBootVersion, "2.") { + return "org.springframework.boot.loader.JarLauncher" } // Default to Spring Boot 3.x (newer) launcher if version couldn't be determined