Skip to content

Commit 00c8173

Browse files
romtsnclaude
andcommitted
fix(spring-boot2): pre-merge Spring metadata for Shadow 9.x compatibility
Shadow 9.x enforces DuplicatesStrategy before transformers run, which breaks the `append` transformer for spring.factories and other Spring metadata files. Only the last copy survives instead of being concatenated. Replace `append` calls with a pre-merge task that manually concatenates Spring metadata files (spring.factories, spring.handlers, spring.schemas, spring-autoconfigure-metadata.properties) from the runtime classpath before the shadow JAR is built. The merged files are included first in the shadow JAR so they take precedence over duplicates from dependency JARs. This fixes the PersonSystemTest failure where @SentrySpan AOP and JDBC instrumentation weren't working because SentryAutoConfiguration wasn't properly registered in the merged spring.factories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f47be74 commit 00c8173

File tree

5 files changed

+180
-25
lines changed

5 files changed

+180
-25
lines changed

sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import java.util.zip.ZipFile
12
import org.jetbrains.kotlin.config.KotlinCompilerVersion
23
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
34

@@ -36,16 +37,45 @@ dependencies {
3637
}
3738
}
3839

40+
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
41+
// transformer only sees one copy of each file. We pre-merge Spring metadata files
42+
// from the runtime classpath and include the merged result in the shadow JAR.
43+
val mergeSpringMetadata by tasks.registering {
44+
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata/META-INF")
45+
val filesToMerge =
46+
listOf("spring.factories", "spring.handlers", "spring.schemas", "spring-autoconfigure-metadata.properties")
47+
outputs.dir(outputDir)
48+
inputs.files(configurations.runtimeClasspath)
49+
doLast {
50+
val out = outputDir.get().asFile
51+
out.mkdirs()
52+
filesToMerge.forEach { fileName ->
53+
val merged = StringBuilder()
54+
configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }.forEach { jar ->
55+
try {
56+
val zip = ZipFile(jar)
57+
val entry = zip.getEntry("META-INF/$fileName")
58+
if (entry != null) {
59+
merged.append(zip.getInputStream(entry).bufferedReader().readText())
60+
if (!merged.endsWith("\n")) merged.append("\n")
61+
}
62+
zip.close()
63+
} catch (e: Exception) { /* skip non-zip files */ }
64+
}
65+
if (merged.isNotEmpty()) { File(out, fileName).writeText(merged.toString()) }
66+
}
67+
}
68+
}
69+
3970
// Configure the Shadow JAR (executable JAR with all dependencies)
4071
tasks.shadowJar {
72+
dependsOn(mergeSpringMetadata)
4173
manifest { attributes["Main-Class"] = "io.sentry.samples.netflix.dgs.NetlixDgsApplication" }
4274
archiveClassifier.set("")
43-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
75+
from(mergeSpringMetadata.map { project.layout.buildDirectory.dir("merged-spring-metadata") }) {
76+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
77+
}
4478
mergeServiceFiles()
45-
append("META-INF/spring.handlers")
46-
append("META-INF/spring.schemas")
47-
append("META-INF/spring.factories")
48-
append("META-INF/spring-autoconfigure-metadata.properties")
4979
}
5080

5181
tasks.jar {

sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import java.util.zip.ZipFile
12
import org.jetbrains.kotlin.config.KotlinCompilerVersion
23
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
34

@@ -77,16 +78,45 @@ dependencies {
7778
testImplementation("org.apache.httpcomponents:httpclient")
7879
}
7980

81+
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
82+
// transformer only sees one copy of each file. We pre-merge Spring metadata files
83+
// from the runtime classpath and include the merged result in the shadow JAR.
84+
val mergeSpringMetadata by tasks.registering {
85+
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata/META-INF")
86+
val filesToMerge =
87+
listOf("spring.factories", "spring.handlers", "spring.schemas", "spring-autoconfigure-metadata.properties")
88+
outputs.dir(outputDir)
89+
inputs.files(configurations.runtimeClasspath)
90+
doLast {
91+
val out = outputDir.get().asFile
92+
out.mkdirs()
93+
filesToMerge.forEach { fileName ->
94+
val merged = StringBuilder()
95+
configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }.forEach { jar ->
96+
try {
97+
val zip = ZipFile(jar)
98+
val entry = zip.getEntry("META-INF/$fileName")
99+
if (entry != null) {
100+
merged.append(zip.getInputStream(entry).bufferedReader().readText())
101+
if (!merged.endsWith("\n")) merged.append("\n")
102+
}
103+
zip.close()
104+
} catch (e: Exception) { /* skip non-zip files */ }
105+
}
106+
if (merged.isNotEmpty()) { File(out, fileName).writeText(merged.toString()) }
107+
}
108+
}
109+
}
110+
80111
// Configure the Shadow JAR (executable JAR with all dependencies)
81112
tasks.shadowJar {
113+
dependsOn(mergeSpringMetadata)
82114
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
83115
archiveClassifier.set("")
84-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
116+
from(mergeSpringMetadata.map { project.layout.buildDirectory.dir("merged-spring-metadata") }) {
117+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
118+
}
85119
mergeServiceFiles()
86-
append("META-INF/spring.handlers")
87-
append("META-INF/spring.schemas")
88-
append("META-INF/spring.factories")
89-
append("META-INF/spring-autoconfigure-metadata.properties")
90120
}
91121

92122
tasks.jar {

sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import java.util.zip.ZipFile
12
import org.jetbrains.kotlin.config.KotlinCompilerVersion
23
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
34

@@ -73,16 +74,45 @@ dependencies {
7374
testImplementation("org.apache.httpcomponents:httpclient")
7475
}
7576

77+
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
78+
// transformer only sees one copy of each file. We pre-merge Spring metadata files
79+
// from the runtime classpath and include the merged result in the shadow JAR.
80+
val mergeSpringMetadata by tasks.registering {
81+
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata/META-INF")
82+
val filesToMerge =
83+
listOf("spring.factories", "spring.handlers", "spring.schemas", "spring-autoconfigure-metadata.properties")
84+
outputs.dir(outputDir)
85+
inputs.files(configurations.runtimeClasspath)
86+
doLast {
87+
val out = outputDir.get().asFile
88+
out.mkdirs()
89+
filesToMerge.forEach { fileName ->
90+
val merged = StringBuilder()
91+
configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }.forEach { jar ->
92+
try {
93+
val zip = ZipFile(jar)
94+
val entry = zip.getEntry("META-INF/$fileName")
95+
if (entry != null) {
96+
merged.append(zip.getInputStream(entry).bufferedReader().readText())
97+
if (!merged.endsWith("\n")) merged.append("\n")
98+
}
99+
zip.close()
100+
} catch (e: Exception) { /* skip non-zip files */ }
101+
}
102+
if (merged.isNotEmpty()) { File(out, fileName).writeText(merged.toString()) }
103+
}
104+
}
105+
}
106+
76107
// Configure the Shadow JAR (executable JAR with all dependencies)
77108
tasks.shadowJar {
109+
dependsOn(mergeSpringMetadata)
78110
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
79111
archiveClassifier.set("")
80-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
112+
from(mergeSpringMetadata.map { project.layout.buildDirectory.dir("merged-spring-metadata") }) {
113+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
114+
}
81115
mergeServiceFiles()
82-
append("META-INF/spring.handlers")
83-
append("META-INF/spring.schemas")
84-
append("META-INF/spring.factories")
85-
append("META-INF/spring-autoconfigure-metadata.properties")
86116
}
87117

88118
tasks.jar {

sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import java.util.zip.ZipFile
12
import org.jetbrains.kotlin.config.KotlinCompilerVersion
23
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
34

@@ -46,16 +47,45 @@ dependencies {
4647
testImplementation("org.apache.httpcomponents:httpclient")
4748
}
4849

50+
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
51+
// transformer only sees one copy of each file. We pre-merge Spring metadata files
52+
// from the runtime classpath and include the merged result in the shadow JAR.
53+
val mergeSpringMetadata by tasks.registering {
54+
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata/META-INF")
55+
val filesToMerge =
56+
listOf("spring.factories", "spring.handlers", "spring.schemas", "spring-autoconfigure-metadata.properties")
57+
outputs.dir(outputDir)
58+
inputs.files(configurations.runtimeClasspath)
59+
doLast {
60+
val out = outputDir.get().asFile
61+
out.mkdirs()
62+
filesToMerge.forEach { fileName ->
63+
val merged = StringBuilder()
64+
configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }.forEach { jar ->
65+
try {
66+
val zip = ZipFile(jar)
67+
val entry = zip.getEntry("META-INF/$fileName")
68+
if (entry != null) {
69+
merged.append(zip.getInputStream(entry).bufferedReader().readText())
70+
if (!merged.endsWith("\n")) merged.append("\n")
71+
}
72+
zip.close()
73+
} catch (e: Exception) { /* skip non-zip files */ }
74+
}
75+
if (merged.isNotEmpty()) { File(out, fileName).writeText(merged.toString()) }
76+
}
77+
}
78+
}
79+
4980
// Configure the Shadow JAR (executable JAR with all dependencies)
5081
tasks.shadowJar {
82+
dependsOn(mergeSpringMetadata)
5183
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
5284
archiveClassifier.set("")
53-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
85+
from(mergeSpringMetadata.map { project.layout.buildDirectory.dir("merged-spring-metadata") }) {
86+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
87+
}
5488
mergeServiceFiles()
55-
append("META-INF/spring.handlers")
56-
append("META-INF/spring.schemas")
57-
append("META-INF/spring.factories")
58-
append("META-INF/spring-autoconfigure-metadata.properties")
5989
}
6090

6191
tasks.jar {

sentry-samples/sentry-samples-spring-boot/build.gradle.kts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import org.jetbrains.kotlin.config.KotlinCompilerVersion
22
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
import java.util.zip.ZipFile
34

45
plugins {
56
java
@@ -73,16 +74,50 @@ dependencies {
7374
testImplementation("org.apache.httpcomponents:httpclient")
7475
}
7576

77+
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
78+
// transformer only sees one copy of each file. We pre-merge Spring metadata files
79+
// from the runtime classpath and include the merged result in the shadow JAR.
80+
val mergeSpringMetadata by tasks.registering {
81+
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata/META-INF")
82+
val filesToMerge =
83+
listOf("spring.factories", "spring.handlers", "spring.schemas", "spring-autoconfigure-metadata.properties")
84+
85+
outputs.dir(outputDir)
86+
inputs.files(configurations.runtimeClasspath)
87+
88+
doLast {
89+
val out = outputDir.get().asFile
90+
out.mkdirs()
91+
filesToMerge.forEach { fileName ->
92+
val merged = StringBuilder()
93+
configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }.forEach { jar ->
94+
try {
95+
val zip = ZipFile(jar)
96+
val entry = zip.getEntry("META-INF/$fileName")
97+
if (entry != null) {
98+
merged.append(zip.getInputStream(entry).bufferedReader().readText())
99+
if (!merged.endsWith("\n")) merged.append("\n")
100+
}
101+
zip.close()
102+
} catch (e: Exception) { /* skip non-zip files */ }
103+
}
104+
if (merged.isNotEmpty()) {
105+
File(out, fileName).writeText(merged.toString())
106+
}
107+
}
108+
}
109+
}
110+
76111
// Configure the Shadow JAR (executable JAR with all dependencies)
77112
tasks.shadowJar {
113+
dependsOn(mergeSpringMetadata)
78114
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
79115
archiveClassifier.set("")
80-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
116+
// Pre-merged Spring metadata files must come first so they win over duplicates from JARs
117+
from(mergeSpringMetadata.map { project.layout.buildDirectory.dir("merged-spring-metadata") }) {
118+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
119+
}
81120
mergeServiceFiles()
82-
append("META-INF/spring.handlers")
83-
append("META-INF/spring.schemas")
84-
append("META-INF/spring.factories")
85-
append("META-INF/spring-autoconfigure-metadata.properties")
86121
}
87122

88123
tasks.jar {

0 commit comments

Comments
 (0)