Skip to content

Commit 8f65df7

Browse files
authored
Merge pull request #1268 from stokpop/issue-1267-fix-jbp-config-java-main
Issue 1267 fix: jbp config java main
2 parents e09a6b1 + 74da783 commit 8f65df7

6 files changed

Lines changed: 257 additions & 9 deletions

File tree

docs/container-java_main.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Command line arguments may optionally be configured.
1010
<table>
1111
<tr>
1212
<td><strong>Detection Criteria</strong></td>
13-
<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>
13+
<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>
1414
</tr>
1515
<tr>
1616
<td><strong>Tags</strong></td>
@@ -23,17 +23,25 @@ If the application uses Spring, [Spring profiles][] can be specified by setting
2323

2424
## Spring Boot
2525

26-
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.
26+
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.
2727

2828
## Configuration
2929
For general information on configuring the buildpack, including how to specify configuration values through environment variables, refer to [Configuration and Extension][].
3030

31-
The container can be configured by modifying the `config/java_main.yml` file in the buildpack fork.
31+
The container can be configured using the `JBP_CONFIG_JAVA_MAIN` environment variable.
3232

3333
| Name | Description
3434
| ---- | -----------
3535
| `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.
36-
| `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.
36+
| `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.
37+
38+
### Example: PropertiesLauncher with external config
39+
40+
```yaml
41+
env:
42+
JBP_CONFIG_JAVA_MAIN: '{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher", arguments: "--loader.home=/home/vcap/data"}'
43+
JAVA_OPTS: '-Dloader.path=/home/vcap/data/lib'
44+
```
3745
3846
[Configuration and Extension]: ../README.md#configuration-and-extension
3947
[Spring profiles]:http://blog.springsource.com/2011/02/14/spring-3-1-m1-introducing-profile/

src/integration/java_main_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing.
6262
// Verify buildpack detects and applies explicit main class configuration
6363
Expect(logs.String()).To(ContainSubstring("Java Buildpack"))
6464
Expect(logs.String()).To(ContainSubstring("Java Main"))
65+
66+
// NOTE: this test does NOT verify that java_main_class actually overrides the
67+
// manifest Main-Class, because:
68+
// 1. The fixture's MANIFEST.MF already has Main-Class: io.pivotal.SimpleJava
69+
// (same value as JBP_CONFIG_JAVA_MAIN), so the test passes even if the
70+
// config is ignored.
71+
// 2. switchblade's Deployment struct does not expose the release command,
72+
// so we cannot assert -cp vs -jar or which class was used.
73+
// The override behaviour and -cp mode are covered by unit tests in
74+
// src/java/containers/java_main_test.go.
6575
})
6676
})
6777

@@ -81,6 +91,8 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing.
8191

8292
// Verify app can start (validates command with arguments is valid)
8393
Eventually(deployment.ExternalURL).ShouldNot(BeEmpty())
94+
// NOTE: does not verify arguments are actually appended to the command line;
95+
// that is covered by unit tests in src/java/containers/java_main_test.go.
8496
})
8597
})
8698

@@ -98,6 +110,8 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing.
98110
// Verify buildpack stages successfully with JAVA_OPTS
99111
Expect(logs.String()).To(ContainSubstring("Java Buildpack"))
100112
Expect(logs.String()).To(ContainSubstring("Java Main"))
113+
// NOTE: does not verify JAVA_OPTS are applied to the JVM command line;
114+
// staging success only confirms the options did not break the build.
101115
})
102116
})
103117

src/java/containers/container.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,27 @@ func (r *Registry) Register(c Container) {
3939
r.containers = append(r.containers, c)
4040
}
4141

42-
// Detect finds the first container that can handle the application
42+
// Detect finds the first container that can handle the application.
43+
// If JBP_CONFIG_JAVA_MAIN specifies an explicit java_main_class, the Java Main
44+
// container is selected unconditionally — before the normal priority order —
45+
// so it can override higher-priority containers such as Spring Boot.
46+
// Java Main is always registered last (lowest priority), so it is the last element.
4347
func (r *Registry) Detect() (Container, string, error) {
48+
cfg := loadJavaMainConfig(r.context.Log)
49+
if cfg.JavaMainClass != "" && len(r.containers) > 0 {
50+
if jm, ok := r.containers[len(r.containers)-1].(*JavaMainContainer); ok {
51+
name, err := jm.Detect()
52+
if err != nil {
53+
return nil, "", err
54+
}
55+
if name != "" {
56+
return jm, name, nil
57+
}
58+
} else {
59+
r.context.Log.Warning("JBP_CONFIG_JAVA_MAIN java_main_class is set but JavaMain container is not available; ignoring override")
60+
}
61+
}
62+
4463
for _, container := range r.containers {
4564
name, err := container.Detect()
4665
if err != nil {

src/java/containers/container_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ var _ = Describe("Container Registry", func() {
8484
})
8585
})
8686

87+
Context("with Spring Boot app and JBP_CONFIG_JAVA_MAIN java_main_class set", func() {
88+
BeforeEach(func() {
89+
// App looks like Spring Boot
90+
os.MkdirAll(filepath.Join(buildDir, "BOOT-INF"), 0755)
91+
os.MkdirAll(filepath.Join(buildDir, "META-INF"), 0755)
92+
manifest := "Manifest-Version: 1.0\nStart-Class: com.example.App\nSpring-Boot-Version: 2.7.0\n"
93+
os.WriteFile(filepath.Join(buildDir, "META-INF", "MANIFEST.MF"), []byte(manifest), 0644)
94+
os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher", arguments: "--loader.home=/home/vcap/data"}`)
95+
})
96+
97+
AfterEach(func() {
98+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
99+
})
100+
101+
It("selects Java Main container instead of Spring Boot", func() {
102+
container, name, err := registry.Detect()
103+
Expect(err).NotTo(HaveOccurred())
104+
Expect(container).NotTo(BeNil())
105+
Expect(name).To(Equal("Java Main"))
106+
})
107+
108+
It("uses the configured class and arguments in the start command", func() {
109+
container, _, err := registry.Detect()
110+
Expect(err).NotTo(HaveOccurred())
111+
cmd, err := container.Release()
112+
Expect(err).NotTo(HaveOccurred())
113+
Expect(cmd).To(ContainSubstring("org.springframework.boot.loader.launch.PropertiesLauncher"))
114+
Expect(cmd).To(ContainSubstring("--loader.home=/home/vcap/data"))
115+
})
116+
})
117+
87118
Context("with no detectable app", func() {
88119
It("returns nil container", func() {
89120
container, name, err := registry.Detect()

src/java/containers/java_main.go

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ import (
1111
"github.com/cloudfoundry/java-buildpack/src/java/common"
1212
)
1313

14+
type javaMainConfig struct {
15+
JavaMainClass string `yaml:"java_main_class"`
16+
Arguments string `yaml:"arguments"`
17+
}
18+
19+
func loadJavaMainConfig(log interface{ Warning(string, ...interface{}) }) javaMainConfig {
20+
cfg := javaMainConfig{}
21+
raw := os.Getenv("JBP_CONFIG_JAVA_MAIN")
22+
if raw == "" {
23+
return cfg
24+
}
25+
yamlHandler := common.YamlHandler{}
26+
if err := yamlHandler.ValidateFields([]byte(raw), &cfg); err != nil {
27+
log.Warning("Unknown JBP_CONFIG_JAVA_MAIN values: %s", err.Error())
28+
}
29+
_ = yamlHandler.Unmarshal([]byte(raw), &cfg)
30+
return cfg
31+
}
32+
1433
// JavaMainContainer handles standalone JAR applications with a main class
1534
type JavaMainContainer struct {
1635
context *common.Context
@@ -29,6 +48,14 @@ func NewJavaMainContainer(ctx *common.Context) *JavaMainContainer {
2948
func (j *JavaMainContainer) Detect() (string, error) {
3049
buildDir := j.context.Stager.BuildDir()
3150

51+
// JBP_CONFIG_JAVA_MAIN with java_main_class always wins (Ruby parity)
52+
cfg := loadJavaMainConfig(j.context.Log)
53+
if cfg.JavaMainClass != "" {
54+
j.mainClass = cfg.JavaMainClass
55+
j.context.Log.Debug("Detected Java Main application via JBP_CONFIG_JAVA_MAIN: %s", j.mainClass)
56+
return "Java Main", nil
57+
}
58+
3259
// Look for JAR files with Main-Class manifest
3360
mainClass, jarFile := j.findMainClass(buildDir)
3461
if mainClass != "" {
@@ -168,6 +195,20 @@ func (j *JavaMainContainer) Supply() error {
168195
return nil
169196
}
170197

198+
// isSpringBootLauncher returns true if the given class is one of the Spring Boot launchers.
199+
func isSpringBootLauncher(mainClass string) bool {
200+
switch mainClass {
201+
case "org.springframework.boot.loader.JarLauncher",
202+
"org.springframework.boot.loader.WarLauncher",
203+
"org.springframework.boot.loader.PropertiesLauncher",
204+
"org.springframework.boot.loader.launch.JarLauncher",
205+
"org.springframework.boot.loader.launch.WarLauncher",
206+
"org.springframework.boot.loader.launch.PropertiesLauncher":
207+
return true
208+
}
209+
return false
210+
}
211+
171212
// Finalize performs final Java Main configuration
172213
func (j *JavaMainContainer) Finalize() error {
173214
j.context.Log.BeginStep("Finalizing Java Main")
@@ -180,6 +221,17 @@ func (j *JavaMainContainer) Finalize() error {
180221

181222
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", classpath)
182223

224+
// Ruby parity: set SERVER_PORT=$PORT when the main class is a Spring Boot launcher
225+
// so the app binds to the CF-assigned port.
226+
cfg := loadJavaMainConfig(j.context.Log)
227+
mainClass := cfg.JavaMainClass
228+
if mainClass == "" {
229+
mainClass = j.mainClass
230+
}
231+
if isSpringBootLauncher(mainClass) {
232+
profileScript += "export SERVER_PORT=$PORT\n"
233+
}
234+
183235
if err := j.context.Stager.WriteProfileD("java_main.sh", profileScript); err != nil {
184236
return fmt.Errorf("failed to write java_main.sh profile.d script: %w", err)
185237
}
@@ -230,10 +282,23 @@ func (j *JavaMainContainer) buildClasspath() (string, error) {
230282

231283
// Release returns the Java Main startup command
232284
func (j *JavaMainContainer) Release() (string, error) {
285+
cfg := loadJavaMainConfig(j.context.Log)
286+
287+
args := ""
288+
if cfg.Arguments != "" {
289+
args = " " + cfg.Arguments
290+
}
291+
292+
// JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class.
293+
// Use classpath mode so the configured class is actually invoked (not the manifest's).
294+
if cfg.JavaMainClass != "" {
295+
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
296+
}
297+
233298
if j.jarFile != "" {
234299
// JAR has its own Main-Class in the manifest — java -jar handles it
235300
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
236-
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", j.jarFile), nil
301+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s%s", j.jarFile, args), nil
237302
}
238303

239304
// Classpath mode: need an explicit main class
@@ -247,5 +312,5 @@ func (j *JavaMainContainer) Release() (string, error) {
247312
}
248313

249314
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
250-
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", mainClass), nil
315+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args), nil
251316
}

src/java/containers/java_main_test.go

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,89 @@ var _ = Describe("Java Main Container", func() {
197197
})
198198
})
199199

200+
Context("with JBP_CONFIG_JAVA_MAIN java_main_class overriding manifest Main-Class", func() {
201+
// Ruby parity: config[MAIN_CLASS_PROPERTY] takes precedence over manifest Main-Class
202+
// This is how PropertiesLauncher is used with Spring Boot exploded JARs
203+
BeforeEach(func() {
204+
os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: org.springframework.boot.loader.launch.PropertiesLauncher}")
205+
Expect(createJar(
206+
filepath.Join(buildDir, "app.jar"),
207+
"Manifest-Version: 1.0\nMain-Class: org.springframework.boot.loader.JarLauncher\n",
208+
)).To(Succeed())
209+
})
210+
211+
AfterEach(func() {
212+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
213+
})
214+
215+
It("uses the configured java_main_class instead of the manifest Main-Class", func() {
216+
container.Detect()
217+
cmd, err := container.Release()
218+
Expect(err).NotTo(HaveOccurred())
219+
Expect(cmd).To(ContainSubstring("org.springframework.boot.loader.launch.PropertiesLauncher"))
220+
Expect(cmd).NotTo(ContainSubstring("JarLauncher"))
221+
})
222+
223+
It("uses classpath mode (not java -jar) so the overridden main class is actually invoked", func() {
224+
container.Detect()
225+
cmd, err := container.Release()
226+
Expect(err).NotTo(HaveOccurred())
227+
Expect(cmd).NotTo(ContainSubstring("-jar"))
228+
Expect(cmd).To(ContainSubstring("-cp"))
229+
})
230+
})
231+
232+
Context("with JBP_CONFIG_JAVA_MAIN java_main_class on app with no manifest Main-Class", func() {
233+
BeforeEach(func() {
234+
os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.CustomMain}")
235+
// App has no Main-Class in manifest — detection still works via JBP_CONFIG_JAVA_MAIN
236+
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644)
237+
})
238+
239+
AfterEach(func() {
240+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
241+
})
242+
243+
It("detects as Java Main application", func() {
244+
name, err := container.Detect()
245+
Expect(err).NotTo(HaveOccurred())
246+
Expect(name).To(Equal("Java Main"))
247+
})
248+
249+
It("uses the configured main class", func() {
250+
container.Detect()
251+
cmd, err := container.Release()
252+
Expect(err).NotTo(HaveOccurred())
253+
Expect(cmd).To(ContainSubstring("com.example.CustomMain"))
254+
})
255+
})
256+
257+
Context("with JBP_CONFIG_JAVA_MAIN arguments", func() {
258+
AfterEach(func() {
259+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
260+
})
261+
262+
It("appends arguments after main class when using java_main_class", func() {
263+
os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: com.example.Main, arguments: "--server.port=$PORT"}`)
264+
container.Detect()
265+
cmd, err := container.Release()
266+
Expect(err).NotTo(HaveOccurred())
267+
Expect(cmd).To(ContainSubstring("com.example.Main --server.port=$PORT"))
268+
})
269+
270+
It("appends arguments after main class when using manifest Main-Class", func() {
271+
os.Setenv("JBP_CONFIG_JAVA_MAIN", `{arguments: "--foo=bar"}`)
272+
Expect(createJar(
273+
filepath.Join(buildDir, "app.jar"),
274+
"Manifest-Version: 1.0\nMain-Class: com.example.Main\n",
275+
)).To(Succeed())
276+
container.Detect()
277+
cmd, err := container.Release()
278+
Expect(err).NotTo(HaveOccurred())
279+
Expect(cmd).To(ContainSubstring("--foo=bar"))
280+
})
281+
})
282+
200283
Context("without main class or JAR", func() {
201284
It("returns error", func() {
202285
_, err := container.Release()
@@ -303,16 +386,44 @@ var _ = Describe("Java Main Container", func() {
303386
})
304387
})
305388

306-
Context("with empty build directory", func() {
389+
Context("with Spring Boot launcher in JBP_CONFIG_JAVA_MAIN", func() {
390+
BeforeEach(func() {
391+
os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher"}`)
392+
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644)
393+
})
394+
395+
AfterEach(func() {
396+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
397+
})
398+
399+
It("writes SERVER_PORT=$PORT to profile.d for Ruby parity", func() {
400+
container.Detect()
401+
err := container.Finalize()
402+
Expect(err).NotTo(HaveOccurred())
403+
404+
profileScript := filepath.Join(depsDir, "0", "profile.d", "java_main.sh")
405+
data, err := os.ReadFile(profileScript)
406+
Expect(err).NotTo(HaveOccurred())
407+
Expect(string(data)).To(ContainSubstring("export SERVER_PORT=$PORT\n"))
408+
})
409+
})
410+
411+
Context("with non-Spring-Boot main class", func() {
307412
BeforeEach(func() {
308413
os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644)
309414
})
310415

311-
It("creates minimal classpath", func() {
416+
It("does not write SERVER_PORT to profile.d", func() {
312417
container.Detect()
313418
err := container.Finalize()
314419
Expect(err).NotTo(HaveOccurred())
420+
421+
profileScript := filepath.Join(depsDir, "0", "profile.d", "java_main.sh")
422+
data, err := os.ReadFile(profileScript)
423+
Expect(err).NotTo(HaveOccurred())
424+
Expect(string(data)).NotTo(ContainSubstring("SERVER_PORT"))
315425
})
316426
})
427+
317428
})
318429
})

0 commit comments

Comments
 (0)