Skip to content
Open
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
3 changes: 0 additions & 3 deletions src/integration/spring_boot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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="),
Expand Down Expand Up @@ -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
Expand Down
91 changes: 44 additions & 47 deletions src/java/containers/spring_boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down