Skip to content

Commit b42f056

Browse files
committed
Don't close jar files early
Update `JarFile` and related classes so that `close()` is not longer called early. Prior to this commit, we would always immediately close the underlying jar file to prevent file locking issues with our build. This causes issues on certain JVMs when they attempt to verify a signed jar. The file lock issues have now been solved by returning a custom input stream from `JarUrlConnection` which captures and delegates the close method. Fixes gh-29356
1 parent 49b7100 commit b42f056

File tree

9 files changed

+158
-28
lines changed

9 files changed

+158
-28
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,8 +26,11 @@
2626
import java.net.URLStreamHandler;
2727
import java.net.URLStreamHandlerFactory;
2828
import java.security.Permission;
29+
import java.util.ArrayList;
30+
import java.util.Collections;
2931
import java.util.Enumeration;
3032
import java.util.Iterator;
33+
import java.util.List;
3134
import java.util.Spliterator;
3235
import java.util.Spliterators;
3336
import java.util.function.Supplier;
@@ -93,6 +96,8 @@ public class JarFile extends AbstractJarFile implements Iterable<java.util.jar.J
9396

9497
private volatile JarFileWrapper wrapper;
9598

99+
private final List<JarFile> nestedJars = Collections.synchronizedList(new ArrayList<>());
100+
96101
/**
97102
* Create a new {@link JarFile} backed by the specified file.
98103
* @param file the root jar file
@@ -128,9 +133,6 @@ private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccess
128133
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
129134
JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
130135
super(rootFile.getFile());
131-
if (System.getSecurityManager() == null) {
132-
super.close();
133-
}
134136
this.rootFile = rootFile;
135137
this.pathFromRoot = pathFromRoot;
136138
CentralDirectoryParser parser = new CentralDirectoryParser();
@@ -142,8 +144,7 @@ private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccess
142144
}
143145
catch (RuntimeException ex) {
144146
try {
145-
this.rootFile.close();
146-
super.close();
147+
close();
147148
}
148149
catch (IOException ioex) {
149150
}
@@ -188,8 +189,13 @@ public void visitEnd() {
188189
JarFileWrapper getWrapper() throws IOException {
189190
JarFileWrapper wrapper = this.wrapper;
190191
if (wrapper == null) {
191-
wrapper = new JarFileWrapper(this);
192-
this.wrapper = wrapper;
192+
synchronized (this) {
193+
if (this.wrapper != null) {
194+
return this.wrapper;
195+
}
196+
wrapper = new JarFileWrapper(this);
197+
this.wrapper = wrapper;
198+
}
193199
}
194200
return wrapper;
195201
}
@@ -334,8 +340,10 @@ private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
334340
+ "mechanism used to create your executable jar file");
335341
}
336342
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
337-
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
343+
JarFile nestedJar = new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
338344
JarFileType.NESTED_JAR);
345+
this.nestedJars.add(nestedJar);
346+
return nestedJar;
339347
}
340348

341349
@Override
@@ -355,11 +363,19 @@ public void close() throws IOException {
355363
if (this.closed) {
356364
return;
357365
}
358-
super.close();
359-
if (this.type == JarFileType.DIRECT) {
360-
this.rootFile.close();
366+
synchronized (this) {
367+
super.close();
368+
if (this.type == JarFileType.DIRECT) {
369+
this.rootFile.close();
370+
}
371+
if (this.wrapper != null) {
372+
this.wrapper.close();
373+
}
374+
for (JarFile nestedJar : this.nestedJars) {
375+
nestedJar.close();
376+
}
377+
this.closed = true;
361378
}
362-
this.closed = true;
363379
}
364380

365381
private void ensureOpen() {

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,9 +40,6 @@ class JarFileWrapper extends AbstractJarFile {
4040
JarFileWrapper(JarFile parent) throws IOException {
4141
super(parent.getRootJarFile().getFile());
4242
this.parent = parent;
43-
if (System.getSecurityManager() == null) {
44-
super.close();
45-
}
4643
}
4744

4845
@Override

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.FileNotFoundException;
21+
import java.io.FilterInputStream;
2122
import java.io.IOException;
2223
import java.io.InputStream;
2324
import java.io.UnsupportedEncodingException;
@@ -165,7 +166,7 @@ public InputStream getInputStream() throws IOException {
165166
if (inputStream == null) {
166167
throwFileNotFound(this.jarEntryName, this.jarFile);
167168
}
168-
return inputStream;
169+
return new ConnectionInputStream(inputStream);
169170
}
170171

171172
private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException {
@@ -290,6 +291,19 @@ private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryN
290291
return new JarURLConnection(null, jarFile, jarEntryName);
291292
}
292293

294+
private class ConnectionInputStream extends FilterInputStream {
295+
296+
ConnectionInputStream(InputStream in) {
297+
super(in);
298+
}
299+
300+
@Override
301+
public void close() throws IOException {
302+
JarURLConnection.this.jarFile.close();
303+
}
304+
305+
}
306+
293307
/**
294308
* A JarEntryName parsed from a URL String.
295309
*/

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@ void setup(@TempDir File temp) throws Exception {
6161
@AfterEach
6262
void cleanup() throws Exception {
6363
this.parent.close();
64+
this.wrapper.close();
6465
}
6566

6667
private File createTempJar(File temp) throws IOException {

spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ dependencies {
1414
app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository")
1515
app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository")
1616
app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository")
17+
app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository")
18+
app("org.bouncycastle:bcprov-jdk15on:1.70")
1719

1820
intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent")))
1921
intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
@@ -39,6 +41,18 @@ task buildApp(type: GradleBuild) {
3941
tasks = ["build"]
4042
}
4143

44+
task syncSignedJarUnpackAppSource(type: org.springframework.boot.build.SyncAppSource) {
45+
sourceDirectory = file("spring-boot-loader-tests-signed-jar-unpack-app")
46+
destinationDirectory = file("${buildDir}/spring-boot-loader-tests-signed-jar-unpack-app")
47+
}
48+
49+
task buildSignedJarUnpackApp(type: GradleBuild) {
50+
dependsOn syncSignedJarUnpackAppSource, syncMavenRepository
51+
dir = "${buildDir}/spring-boot-loader-tests-signed-jar-unpack-app"
52+
startParameter.buildCacheEnabled = false
53+
tasks = ["build"]
54+
}
55+
4256
intTest {
43-
dependsOn buildApp
57+
dependsOn buildApp, buildSignedJarUnpackApp
4458
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
id "java"
3+
id "org.springframework.boot"
4+
}
5+
6+
apply plugin: "io.spring.dependency-management"
7+
8+
repositories {
9+
maven { url "file:${rootDir}/../int-test-maven-repository"}
10+
mavenCentral()
11+
maven { url "https://repo.spring.io/snapshot" }
12+
maven { url "https://repo.spring.io/milestone" }
13+
}
14+
15+
dependencies {
16+
implementation("org.springframework.boot:spring-boot-starter")
17+
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
18+
}
19+
20+
bootJar {
21+
requiresUnpack '**/bcprov-jdk15on-*.jar'
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
pluginManagement {
2+
repositories {
3+
maven { url "file:${rootDir}/../int-test-maven-repository"}
4+
mavenCentral()
5+
maven { url "https://repo.spring.io/snapshot" }
6+
maven { url "https://repo.spring.io/milestone" }
7+
}
8+
resolutionStrategy {
9+
eachPlugin {
10+
if (requested.id.id == "org.springframework.boot") {
11+
useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}"
12+
}
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loaderapp;
18+
19+
import java.security.Security;
20+
import javax.crypto.Cipher;
21+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
22+
23+
import org.springframework.boot.SpringApplication;
24+
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
26+
@SpringBootApplication
27+
public class LoaderSignedJarTestApplication {
28+
29+
public static void main(String[] args) throws Exception {
30+
Security.addProvider(new BouncyCastleProvider());
31+
Cipher.getInstance("AES/CBC/PKCS5Padding","BC");
32+
System.out.println("Legion of the Bouncy Castle");
33+
SpringApplication.run(LoaderSignedJarTestApplication.class, args);
34+
}
35+
36+
}

spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,40 @@ class LoaderIntegrationTests {
5151
@ParameterizedTest
5252
@MethodSource("javaRuntimes")
5353
void readUrlsWithoutWarning(JavaRuntime javaRuntime) {
54-
try (GenericContainer<?> container = createContainer(javaRuntime)) {
54+
try (GenericContainer<?> container = createContainer(javaRuntime, "spring-boot-loader-tests-app")) {
5555
container.start();
5656
System.out.println(this.output.toUtf8String());
5757
assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from").doesNotContain("WARNING:")
5858
.doesNotContain("illegal").doesNotContain("jar written to temp");
5959
}
6060
}
6161

62-
private GenericContainer<?> createContainer(JavaRuntime javaRuntime) {
62+
@ParameterizedTest
63+
@MethodSource("javaRuntimes")
64+
void runSignedJarWhenUnpacked(JavaRuntime javaRuntime) {
65+
try (GenericContainer<?> container = createContainer(javaRuntime,
66+
"spring-boot-loader-tests-signed-jar-unpack-app")) {
67+
container.start();
68+
System.out.println(this.output.toUtf8String());
69+
assertThat(this.output.toUtf8String()).contains("Legion of the Bouncy Castle");
70+
}
71+
}
72+
73+
private GenericContainer<?> createContainer(JavaRuntime javaRuntime, String name) {
6374
return javaRuntime.getContainer().withLogConsumer(this.output)
64-
.withCopyFileToContainer(MountableFile.forHostPath(findApplication().toPath()), "/app.jar")
75+
.withCopyFileToContainer(findApplication(name), "/app.jar")
6576
.withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5)))
6677
.withCommand("java", "-jar", "app.jar");
6778
}
6879

69-
private File findApplication() {
70-
String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app");
71-
File jar = new File(name);
72-
Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?");
80+
private MountableFile findApplication(String name) {
81+
return MountableFile.forHostPath(findJarFile(name).toPath());
82+
}
83+
84+
private File findJarFile(String name) {
85+
String path = String.format("build/%1$s/build/libs/%1$s.jar", name);
86+
File jar = new File(path);
87+
Assert.state(jar.isFile(), () -> "Could not find " + path + ". Have you built it?");
7388
return jar;
7489
}
7590

0 commit comments

Comments
 (0)