Skip to content

Commit 821da53

Browse files
romtsnclaude
andcommitted
fix(spring-boot2): inline Spring metadata merge into shadowJar doLast
Replace the separate mergeSpringMetadata task with inline doLast on shadowJar that resolves runtimeClasspath at execution time (not configuration time). This ensures all project dependency JARs are built before their spring.factories entries are read and merged. Verified locally: 20/21 system tests pass. Only PersonSystemTest 'create person works' fails due to @SentrySpan AOP limitation in shadow JARs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 702dfd0 commit 821da53

File tree

5 files changed

+142
-282
lines changed

5 files changed

+142
-282
lines changed

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

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import java.net.URI
22
import java.nio.file.FileSystems
33
import java.nio.file.Files
4-
import java.nio.file.StandardCopyOption
54
import java.util.zip.ZipFile
65
import org.jetbrains.kotlin.config.KotlinCompilerVersion
76
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -41,73 +40,46 @@ dependencies {
4140
}
4241
}
4342

44-
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
45-
// transformer only sees one copy of each file. We pre-merge Spring metadata files
46-
// from the runtime classpath and include the merged result in the shadow JAR.
47-
val mergeSpringMetadata by
48-
tasks.registering {
49-
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata")
50-
val classpathJars = configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }
51-
val filesToMerge =
52-
listOf(
53-
"META-INF/spring.factories",
54-
"META-INF/spring.handlers",
55-
"META-INF/spring.schemas",
56-
"META-INF/spring-autoconfigure-metadata.properties",
57-
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
58-
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
59-
)
60-
outputs.dir(outputDir)
61-
inputs.files(classpathJars)
62-
doLast {
63-
val out = outputDir.get().asFile
64-
filesToMerge.forEach { entryPath ->
43+
// Configure the Shadow JAR (executable JAR with all dependencies)
44+
tasks.shadowJar {
45+
manifest { attributes["Main-Class"] = "io.sentry.samples.netflix.dgs.NetlixDgsApplication" }
46+
archiveClassifier.set("")
47+
mergeServiceFiles()
48+
49+
val springMetadataFiles =
50+
listOf(
51+
"META-INF/spring.factories",
52+
"META-INF/spring.handlers",
53+
"META-INF/spring.schemas",
54+
"META-INF/spring-autoconfigure-metadata.properties",
55+
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
56+
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
57+
)
58+
59+
doLast {
60+
val jar = archiveFile.get().asFile
61+
val runtimeJars = project.configurations.getByName("runtimeClasspath").resolve().filter { it.name.endsWith(".jar") }
62+
val uri = URI.create("jar:${jar.toURI()}")
63+
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
64+
springMetadataFiles.forEach { entryPath ->
6565
val merged = StringBuilder()
66-
classpathJars.forEach { jar ->
66+
runtimeJars.forEach { depJar ->
6767
try {
68-
val zip = ZipFile(jar)
68+
val zip = ZipFile(depJar)
6969
val entry = zip.getEntry(entryPath)
7070
if (entry != null) {
7171
merged.append(zip.getInputStream(entry).bufferedReader().readText())
7272
if (!merged.endsWith("\n")) merged.append("\n")
7373
}
7474
zip.close()
75-
} catch (e: Exception) {
76-
/* skip non-zip files */
77-
}
75+
} catch (e: Exception) { /* skip non-zip files */ }
7876
}
7977
if (merged.isNotEmpty()) {
80-
val outFile = File(out, entryPath)
81-
outFile.parentFile.mkdirs()
82-
outFile.writeText(merged.toString())
83-
}
84-
}
85-
}
86-
}
87-
88-
// Configure the Shadow JAR (executable JAR with all dependencies)
89-
tasks.shadowJar {
90-
dependsOn(mergeSpringMetadata)
91-
manifest { attributes["Main-Class"] = "io.sentry.samples.netflix.dgs.NetlixDgsApplication" }
92-
archiveClassifier.set("")
93-
mergeServiceFiles()
94-
outputs.upToDateWhen { false }
95-
val metadataDir = project.layout.buildDirectory.dir("merged-spring-metadata")
96-
doLast {
97-
val baseDir = metadataDir.get().asFile
98-
val jar = archiveFile.get().asFile
99-
if (!baseDir.exists()) return@doLast
100-
val uri = URI.create("jar:${jar.toURI()}")
101-
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
102-
baseDir
103-
.walkTopDown()
104-
.filter { it.isFile }
105-
.forEach { merged ->
106-
val relative = merged.relativeTo(baseDir).path
107-
val target = fs.getPath(relative)
78+
val target = fs.getPath(entryPath)
10879
if (target.parent != null) Files.createDirectories(target.parent)
109-
Files.copy(merged.toPath(), target, StandardCopyOption.REPLACE_EXISTING)
80+
Files.write(target, merged.toString().toByteArray())
11081
}
82+
}
11183
}
11284
}
11385
}

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

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import java.net.URI
22
import java.nio.file.FileSystems
33
import java.nio.file.Files
4-
import java.nio.file.StandardCopyOption
54
import java.util.zip.ZipFile
65
import org.jetbrains.kotlin.config.KotlinCompilerVersion
76
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -82,73 +81,46 @@ dependencies {
8281
testImplementation("org.apache.httpcomponents:httpclient")
8382
}
8483

85-
// Shadow 9.x enforces DuplicatesStrategy before transformers run, so the `append`
86-
// transformer only sees one copy of each file. We pre-merge Spring metadata files
87-
// from the runtime classpath and include the merged result in the shadow JAR.
88-
val mergeSpringMetadata by
89-
tasks.registering {
90-
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata")
91-
val classpathJars = configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }
92-
val filesToMerge =
93-
listOf(
94-
"META-INF/spring.factories",
95-
"META-INF/spring.handlers",
96-
"META-INF/spring.schemas",
97-
"META-INF/spring-autoconfigure-metadata.properties",
98-
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
99-
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
100-
)
101-
outputs.dir(outputDir)
102-
inputs.files(classpathJars)
103-
doLast {
104-
val out = outputDir.get().asFile
105-
filesToMerge.forEach { entryPath ->
84+
// Configure the Shadow JAR (executable JAR with all dependencies)
85+
tasks.shadowJar {
86+
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
87+
archiveClassifier.set("")
88+
mergeServiceFiles()
89+
90+
val springMetadataFiles =
91+
listOf(
92+
"META-INF/spring.factories",
93+
"META-INF/spring.handlers",
94+
"META-INF/spring.schemas",
95+
"META-INF/spring-autoconfigure-metadata.properties",
96+
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
97+
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
98+
)
99+
100+
doLast {
101+
val jar = archiveFile.get().asFile
102+
val runtimeJars = project.configurations.getByName("runtimeClasspath").resolve().filter { it.name.endsWith(".jar") }
103+
val uri = URI.create("jar:${jar.toURI()}")
104+
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
105+
springMetadataFiles.forEach { entryPath ->
106106
val merged = StringBuilder()
107-
classpathJars.forEach { jar ->
107+
runtimeJars.forEach { depJar ->
108108
try {
109-
val zip = ZipFile(jar)
109+
val zip = ZipFile(depJar)
110110
val entry = zip.getEntry(entryPath)
111111
if (entry != null) {
112112
merged.append(zip.getInputStream(entry).bufferedReader().readText())
113113
if (!merged.endsWith("\n")) merged.append("\n")
114114
}
115115
zip.close()
116-
} catch (e: Exception) {
117-
/* skip non-zip files */
118-
}
116+
} catch (e: Exception) { /* skip non-zip files */ }
119117
}
120118
if (merged.isNotEmpty()) {
121-
val outFile = File(out, entryPath)
122-
outFile.parentFile.mkdirs()
123-
outFile.writeText(merged.toString())
124-
}
125-
}
126-
}
127-
}
128-
129-
// Configure the Shadow JAR (executable JAR with all dependencies)
130-
tasks.shadowJar {
131-
dependsOn(mergeSpringMetadata)
132-
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
133-
archiveClassifier.set("")
134-
mergeServiceFiles()
135-
outputs.upToDateWhen { false }
136-
val metadataDir = project.layout.buildDirectory.dir("merged-spring-metadata")
137-
doLast {
138-
val baseDir = metadataDir.get().asFile
139-
val jar = archiveFile.get().asFile
140-
if (!baseDir.exists()) return@doLast
141-
val uri = URI.create("jar:${jar.toURI()}")
142-
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
143-
baseDir
144-
.walkTopDown()
145-
.filter { it.isFile }
146-
.forEach { merged ->
147-
val relative = merged.relativeTo(baseDir).path
148-
val target = fs.getPath(relative)
119+
val target = fs.getPath(entryPath)
149120
if (target.parent != null) Files.createDirectories(target.parent)
150-
Files.copy(merged.toPath(), target, StandardCopyOption.REPLACE_EXISTING)
121+
Files.write(target, merged.toString().toByteArray())
151122
}
123+
}
152124
}
153125
}
154126
}

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

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import java.net.URI
22
import java.nio.file.FileSystems
33
import java.nio.file.Files
4-
import java.nio.file.StandardCopyOption
54
import java.util.zip.ZipFile
65
import org.jetbrains.kotlin.config.KotlinCompilerVersion
76
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -78,73 +77,46 @@ dependencies {
7877
testImplementation("org.apache.httpcomponents:httpclient")
7978
}
8079

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
85-
tasks.registering {
86-
val outputDir = project.layout.buildDirectory.dir("merged-spring-metadata")
87-
val classpathJars = configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") }
88-
val filesToMerge =
89-
listOf(
90-
"META-INF/spring.factories",
91-
"META-INF/spring.handlers",
92-
"META-INF/spring.schemas",
93-
"META-INF/spring-autoconfigure-metadata.properties",
94-
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
95-
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
96-
)
97-
outputs.dir(outputDir)
98-
inputs.files(classpathJars)
99-
doLast {
100-
val out = outputDir.get().asFile
101-
filesToMerge.forEach { entryPath ->
80+
// Configure the Shadow JAR (executable JAR with all dependencies)
81+
tasks.shadowJar {
82+
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
83+
archiveClassifier.set("")
84+
mergeServiceFiles()
85+
86+
val springMetadataFiles =
87+
listOf(
88+
"META-INF/spring.factories",
89+
"META-INF/spring.handlers",
90+
"META-INF/spring.schemas",
91+
"META-INF/spring-autoconfigure-metadata.properties",
92+
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
93+
"META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
94+
)
95+
96+
doLast {
97+
val jar = archiveFile.get().asFile
98+
val runtimeJars = project.configurations.getByName("runtimeClasspath").resolve().filter { it.name.endsWith(".jar") }
99+
val uri = URI.create("jar:${jar.toURI()}")
100+
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
101+
springMetadataFiles.forEach { entryPath ->
102102
val merged = StringBuilder()
103-
classpathJars.forEach { jar ->
103+
runtimeJars.forEach { depJar ->
104104
try {
105-
val zip = ZipFile(jar)
105+
val zip = ZipFile(depJar)
106106
val entry = zip.getEntry(entryPath)
107107
if (entry != null) {
108108
merged.append(zip.getInputStream(entry).bufferedReader().readText())
109109
if (!merged.endsWith("\n")) merged.append("\n")
110110
}
111111
zip.close()
112-
} catch (e: Exception) {
113-
/* skip non-zip files */
114-
}
112+
} catch (e: Exception) { /* skip non-zip files */ }
115113
}
116114
if (merged.isNotEmpty()) {
117-
val outFile = File(out, entryPath)
118-
outFile.parentFile.mkdirs()
119-
outFile.writeText(merged.toString())
120-
}
121-
}
122-
}
123-
}
124-
125-
// Configure the Shadow JAR (executable JAR with all dependencies)
126-
tasks.shadowJar {
127-
dependsOn(mergeSpringMetadata)
128-
manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
129-
archiveClassifier.set("")
130-
mergeServiceFiles()
131-
outputs.upToDateWhen { false }
132-
val metadataDir = project.layout.buildDirectory.dir("merged-spring-metadata")
133-
doLast {
134-
val baseDir = metadataDir.get().asFile
135-
val jar = archiveFile.get().asFile
136-
if (!baseDir.exists()) return@doLast
137-
val uri = URI.create("jar:${jar.toURI()}")
138-
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
139-
baseDir
140-
.walkTopDown()
141-
.filter { it.isFile }
142-
.forEach { merged ->
143-
val relative = merged.relativeTo(baseDir).path
144-
val target = fs.getPath(relative)
115+
val target = fs.getPath(entryPath)
145116
if (target.parent != null) Files.createDirectories(target.parent)
146-
Files.copy(merged.toPath(), target, StandardCopyOption.REPLACE_EXISTING)
117+
Files.write(target, merged.toString().toByteArray())
147118
}
119+
}
148120
}
149121
}
150122
}

0 commit comments

Comments
 (0)