Skip to content

Commit 22c4139

Browse files
Fix/uberjar shadow9 3.x (#473)
* Fix java-cfenv-all uber jar broken by Shadow 9 upgrade Fixes #470. The Shadow 8.3.9 -> 9.6.1 upgrade changed two behaviours that the build was not updated for, producing broken `java-cfenv-all` artifacts in 3.5.2 and 4.0.1. Shadow 9 defaults `duplicatesStrategy` to EXCLUDE, and duplicates are dropped before transformers run. `java-cfenv-all` ships its own `META-INF/spring.factories`, so it won that race and every dependency's copy was discarded before `PropertiesFileTransformer` could merge them. The published jar therefore only registered `CloudProfileApplicationListener`, and both `CfDataSourceEnvironmentPostProcessor` and `CfEnvironmentPostProcessor` were missing, so `spring.datasource.url` was never derived from `VCAP_SERVICES`. Setting `duplicatesStrategy` to INCLUDE lets the transformer see and merge every copy. Shadow 9 also no longer treats empty name/version segments in the `dependency(String)` notation as wildcards, so `dependency('org.springframework.boot::')` matched nothing and Spring Boot and Spring Framework were bundled into the uber jar. On Spring Boot 3+ that causes a classloader identity conflict. Using the explicit `:.*:.*` form restores the intended exclusion. The resulting jar drops from 7.9M to 1.7M, contains no `org/springframework` classes, and its `spring.factories` matches the last good release (4.0.0) exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Verify the uber jar contents as part of check The Shadow 9 breakage in #470 shipped in two releases while every unit test passed, because nothing inspected the packaged artifact. Add a `verifyUberJar` task, wired into `check`, that opens the uber jar and asserts what actually has to hold: - no bundled `org/springframework` classes - `com.cedarsoftware.io` relocated, with the shaded classes present - `spring.factories` contains the entries contributed by each module, including both EnvironmentPostProcessors - `spring.factories` does not re-register Spring Boot's own post-processors, which happens if the factories are merged while Spring Boot is still bundled Reverting either half of the fix fails the task, as does reverting both (the state that shipped in 3.5.2 and 4.0.1). Also pin `shadowJar` to run after `jar`. Both write the same archive name because `archiveClassifier` is empty, so which one survives on disk was left to task scheduling; Gradle flags this as an implicit dependency once another task reads the artifact. Note java-util (`com.cedarsoftware.util`) is deliberately not relocated, matching the last good releases, so the check is scoped to `com.cedarsoftware.io`. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6a51977 commit 22c4139

1 file changed

Lines changed: 111 additions & 2 deletions

File tree

java-cfenv-all/build.gradle

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323

2424
shadowJar {
2525
archiveClassifier.set('')
26+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
2627
mergeServiceFiles()
2728
transform(PropertiesFileTransformer) {
2829
paths = ['META-INF/spring.factories']
@@ -79,12 +80,120 @@ shadowJar {
7980
}
8081
})
8182
dependencies {
82-
exclude(dependency('org.springframework.boot::'))
83-
exclude(dependency('org.springframework::'))
83+
exclude(dependency('org.springframework.boot:.*:.*'))
84+
exclude(dependency('org.springframework:.*:.*'))
8485
}
8586
relocate 'com.cedarsoftware.io', 'io.pivotal.cfenv.shaded.com.cedarsoftware.io'
8687
}
8788

89+
// Guards #470: the Shadow 9 upgrade silently produced an uber jar with an
90+
// unmerged spring.factories and bundled Spring classes. No unit test could
91+
// see it, so verify the packaged artifact itself.
92+
// shadowJar and jar share one archive name (archiveClassifier is ''), so pin
93+
// the order to guarantee the uber jar is what ends up on disk rather than
94+
// leaving it to task scheduling.
95+
tasks.shadowJar.mustRunAfter(tasks.jar)
96+
97+
def uberJar = tasks.shadowJar.archiveFile
98+
99+
def verifyUberJar = tasks.register('verifyUberJar') {
100+
group = 'verification'
101+
description = 'Checks the uber jar merges spring.factories and bundles no Spring classes'
102+
103+
dependsOn tasks.jar, tasks.shadowJar
104+
inputs.file(uberJar).withPropertyName('uberJar')
105+
def stamp = layout.buildDirectory.file('verifyUberJar/passed.txt')
106+
outputs.file(stamp)
107+
108+
// Spring Boot 3 registers post-processors under
109+
// org.springframework.boot.env; Spring Boot 4 moved the interface up to
110+
// org.springframework.boot. Accept whichever this branch targets so the
111+
// same check works on 3.x and main.
112+
def eppKeys = ['org.springframework.boot.EnvironmentPostProcessor',
113+
'org.springframework.boot.env.EnvironmentPostProcessor']
114+
def requiredEpps = ['io.pivotal.cfenv.spring.boot.CfDataSourceEnvironmentPostProcessor',
115+
'io.pivotal.cfenv.spring.boot.CfEnvironmentPostProcessor']
116+
117+
def required = [
118+
'org.springframework.context.ApplicationListener': [
119+
'io.pivotal.cfenv.profile.CloudProfileApplicationListener'],
120+
'io.pivotal.cfenv.spring.boot.CfEnvProcessor' : [
121+
'io.pivotal.cfenv.spring.boot.RedisCfEnvProcessor',
122+
'io.pivotal.cfenv.boot.scs.CfConfigClientProcessor',
123+
'io.pivotal.cfenv.boot.sso.CfSingleSignOnProcessor'],
124+
]
125+
126+
doLast {
127+
def jarFile = uberJar.get().asFile
128+
def problems = []
129+
130+
new java.util.zip.ZipFile(jarFile).withCloseable { zip ->
131+
def names = Collections.list(zip.entries())*.name
132+
133+
// Spring must stay out of the uber jar; bundling it causes a
134+
// classloader identity conflict on Spring Boot 3+.
135+
def bundledSpring = names.findAll { it.startsWith('org/springframework/') && it.endsWith('.class') }
136+
if (!bundledSpring.isEmpty()) {
137+
problems << "bundles ${bundledSpring.size()} Spring class(es), e.g. ${bundledSpring.take(3)}"
138+
}
139+
140+
// Fix #275: json-io must be relocated, not shipped under its own
141+
// package. Only com.cedarsoftware.io is relocated; java-util
142+
// (com.cedarsoftware.util) is deliberately left alone.
143+
def unrelocated = names.findAll { it.startsWith('com/cedarsoftware/io/') }
144+
if (!unrelocated.isEmpty()) {
145+
problems << "ships ${unrelocated.size()} unrelocated json-io class(es)"
146+
}
147+
if (!names.any { it.startsWith('io/pivotal/cfenv/shaded/com/cedarsoftware/io/') }) {
148+
problems << 'is missing the relocated json-io classes'
149+
}
150+
151+
// Every module's spring.factories must be merged in, not overwritten.
152+
def entry = zip.getEntry('META-INF/spring.factories')
153+
if (entry == null) {
154+
problems << 'has no META-INF/spring.factories'
155+
}
156+
else {
157+
def props = new Properties()
158+
zip.getInputStream(entry).withCloseable { props.load(it) }
159+
required.each { key, values ->
160+
def actual = (props.getProperty(key) ?: '').split(',')*.trim() as Set
161+
def missing = values.findAll { !actual.contains(it) }
162+
if (!missing.isEmpty()) {
163+
problems << "spring.factories '${key}' is missing ${missing}"
164+
}
165+
}
166+
def eppKey = eppKeys.find { props.getProperty(it) != null }
167+
if (eppKey == null) {
168+
problems << "spring.factories registers no EnvironmentPostProcessor (looked for ${eppKeys})"
169+
}
170+
else {
171+
def registered = props.getProperty(eppKey).split(',')*.trim() as Set
172+
def missingEpps = requiredEpps.findAll { !registered.contains(it) }
173+
if (!missingEpps.isEmpty()) {
174+
problems << "spring.factories '${eppKey}' is missing ${missingEpps}"
175+
}
176+
// Merging must not pull in Spring Boot's own registrations.
177+
def leaked = registered.findAll { it.startsWith('org.springframework.') }
178+
if (!leaked.isEmpty()) {
179+
problems << "spring.factories re-registers Spring Boot's own post-processors ${leaked.sort()}"
180+
}
181+
}
182+
}
183+
}
184+
185+
if (!problems.isEmpty()) {
186+
throw new GradleException("${jarFile.name} is not a valid uber jar:\n - " + problems.join('\n - '))
187+
}
188+
189+
def out = stamp.get().asFile
190+
out.parentFile.mkdirs()
191+
out.text = 'passed'
192+
}
193+
}
194+
195+
tasks.named('check') { dependsOn verifyUberJar }
196+
88197
publishing {
89198
publications {
90199
shadow(MavenPublication) { publication ->

0 commit comments

Comments
 (0)