Fix PathMatchingResourcePatternResolver manifest classpath discovery
Update `PathMatchingResourcePatternResolver` so that in addition to searching the `java.class.path` system property for classpath enties, it also searches the `MANIFEST.MF` files from within those jars. Prior to this commit, the `addClassPathManifestEntries()` method expected that the JVM had added `Class-Path` manifest entries to the `java.class.path` system property, however, this did not always happen. The updated code now performs a deep search by loading `MANIFEST.MF` files from jars discovered from the system property. To deal with potential performance issue, loaded results are also now cached. The updated code has been tested with Spring Boot 3.3 jars extracted using `java -Djarmode=tools`. See gh-33705
This commit is contained in:
committed by
Juergen Hoeller
parent
b7fc4bc5c8
commit
1c69a3c521
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.io.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class packaged into a temporary jar to test
|
||||
* {@link PathMatchingResourcePatternResolver} detection of classpath manifest
|
||||
* entries.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ClassPathManifestEntriesTestApplication {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
System.out.println("!!!!" + List.of(resolver.getResources("classpath*:/**/*.txt")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,23 +16,44 @@
|
||||
|
||||
package org.springframework.core.io.support;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -278,6 +299,103 @@ class PathMatchingResourcePatternResolverTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ClassPathManifestEntries {
|
||||
|
||||
@TempDir
|
||||
Path temp;
|
||||
|
||||
@Test
|
||||
void javaDashJarFindsClassPathManifestEntries() throws Exception {
|
||||
Path lib = this.temp.resolve("lib");
|
||||
Files.createDirectories(lib);
|
||||
writeAssetJar(lib.resolve("asset.jar"));
|
||||
writeApplicationJar(this.temp.resolve("app.jar"));
|
||||
String java = ProcessHandle.current().info().command().get();
|
||||
Process process = new ProcessBuilder(java, "-jar", "app.jar")
|
||||
.directory(this.temp.toFile())
|
||||
.start();
|
||||
assertThat(process.waitFor()).isZero();
|
||||
String result = StreamUtils.copyToString(process.getInputStream(), StandardCharsets.UTF_8);
|
||||
assertThat(result.replace("\\", "/")).contains("!!!!").contains("/lib/asset.jar!/assets/file.txt");
|
||||
}
|
||||
|
||||
private void writeAssetJar(Path path) throws Exception {
|
||||
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()))) {
|
||||
jar.putNextEntry(new ZipEntry("assets/"));
|
||||
jar.closeEntry();
|
||||
jar.putNextEntry(new ZipEntry("assets/file.txt"));
|
||||
StreamUtils.copy("test", StandardCharsets.UTF_8, jar);
|
||||
jar.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeApplicationJar(Path path) throws Exception {
|
||||
Manifest manifest = new Manifest();
|
||||
Attributes mainAttributes = manifest.getMainAttributes();
|
||||
mainAttributes.put(Name.CLASS_PATH, buildSpringClassPath() + "lib/asset.jar");
|
||||
mainAttributes.put(Name.MAIN_CLASS, ClassPathManifestEntriesTestApplication.class.getName());
|
||||
mainAttributes.put(Name.MANIFEST_VERSION, "1.0");
|
||||
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()), manifest)) {
|
||||
String appClassResource = ClassUtils.convertClassNameToResourcePath(
|
||||
ClassPathManifestEntriesTestApplication.class.getName())
|
||||
+ ClassUtils.CLASS_FILE_SUFFIX;
|
||||
String folder = "";
|
||||
for (String name : appClassResource.split("/")) {
|
||||
if (!name.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) {
|
||||
folder += name + "/";
|
||||
jar.putNextEntry(new ZipEntry(folder));
|
||||
jar.closeEntry();
|
||||
}
|
||||
else {
|
||||
jar.putNextEntry(new ZipEntry(folder + name));
|
||||
try (InputStream in = getClass().getResourceAsStream(name)) {
|
||||
in.transferTo(jar);
|
||||
}
|
||||
jar.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSpringClassPath() throws Exception {
|
||||
return copyClasses(PathMatchingResourcePatternResolver.class, "spring-core")
|
||||
+ copyClasses(LogFactory.class, "commons-logging");
|
||||
}
|
||||
|
||||
private String copyClasses(Class<?> sourceClass, String destinationName)
|
||||
throws URISyntaxException, IOException {
|
||||
Path destination = this.temp.resolve(destinationName);
|
||||
String resourcePath = ClassUtils.convertClassNameToResourcePath(sourceClass.getName())
|
||||
+ ClassUtils.CLASS_FILE_SUFFIX;
|
||||
URL resource = getClass().getClassLoader().getResource(resourcePath);
|
||||
URL url = new URL(resource.toString().replace(resourcePath, ""));
|
||||
URLConnection connection = url.openConnection();
|
||||
if (connection instanceof JarURLConnection jarUrlConnection) {
|
||||
try (JarFile jarFile = jarUrlConnection.getJarFile()) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
if (!entry.isDirectory()) {
|
||||
Path entryPath = destination.resolve(entry.getName());
|
||||
try (InputStream in = jarFile.getInputStream(entry)) {
|
||||
Files.createDirectories(entryPath.getParent());
|
||||
Files.copy(in, destination.resolve(entry.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
File source = new File(url.toURI());
|
||||
Files.createDirectories(destination);
|
||||
FileSystemUtils.copyRecursively(source, destination.toFile());
|
||||
}
|
||||
return destinationName + "/ ";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void assertFilenames(String pattern, String... filenames) {
|
||||
assertFilenames(pattern, false, filenames);
|
||||
|
||||
Reference in New Issue
Block a user