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
16 changes: 12 additions & 4 deletions docs/container-java_main.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Command line arguments may optionally be configured.
<table>
<tr>
<td><strong>Detection Criteria</strong></td>
<td><tt>Main-Class</tt> attribute set in <tt>META-INF/MANIFEST.MF</tt> or <tt>java_main_class</tt> set in <tt>config/java_main.yml<tt></td>
<td><tt>Main-Class</tt> attribute set in <tt>META-INF/MANIFEST.MF</tt>, or <tt>java_main_class</tt> set in <tt>JBP_CONFIG_JAVA_MAIN</tt></td>
</tr>
<tr>
<td><strong>Tags</strong></td>
Expand All @@ -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/
Expand Down
14 changes: 14 additions & 0 deletions src/integration/java_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
})
})

Expand All @@ -81,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.
})
})

Expand All @@ -98,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.
})
})

Expand Down
20 changes: 19 additions & 1 deletion src/java/containers/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions src/java/containers/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
69 changes: 67 additions & 2 deletions src/java/containers/java_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 != "" {
Expand Down Expand Up @@ -168,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")
Expand All @@ -180,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)
}
Expand Down Expand Up @@ -230,10 +282,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).
if cfg.JavaMainClass != "" {
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
Expand All @@ -247,5 +312,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
}
115 changes: 113 additions & 2 deletions src/java/containers/java_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,89 @@ 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("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()
Expand Down Expand Up @@ -303,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"))
})
})

})
})