From 73afeb042002af84c36d4ab224c11a321f04b73c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:56:15 +0000 Subject: [PATCH 01/10] fix: apply all 10 bug fixes from code review Agent-Logs-Url: https://github.com/microsphere-projects/microsphere-java/sessions/43b3b061-3f1c-40d4-ad76-5e234af35e41 Co-authored-by: mercyblitz <533114+mercyblitz@users.noreply.github.com> --- .../processor/ResourceProcessor.java | 10 +++- .../StreamArtifactResourceResolver.java | 19 ++++--- .../java/io/microsphere/net/URLUtils.java | 11 ++-- .../microsphere/process/ProcessExecutor.java | 20 ++++---- .../java/io/microsphere/util/ClassUtils.java | 27 +++++----- .../io/microsphere/util/jar/JarUtils.java | 50 +++++++++++++------ .../jdk/tools/compiler/Compiler.java | 15 ++++-- .../ResolvableAnnotationValueVisitor.java | 2 +- 8 files changed, 98 insertions(+), 56 deletions(-) diff --git a/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java b/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java index 71852e810..2b12ee46d 100644 --- a/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java +++ b/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java @@ -23,6 +23,7 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.tools.FileObject; import javax.tools.JavaFileManager.Location; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; @@ -241,6 +242,13 @@ public void processInResourceWriter(String resourceName, ThrowableConsumer 0; + if (resource == null) { + return false; + } + try (InputStream inputStream = resource.openInputStream()) { + return true; + } catch (IOException e) { + return false; + } } } diff --git a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java index 0b4312624..3edff1227 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java +++ b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java @@ -18,6 +18,7 @@ import io.microsphere.annotation.Nullable; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -148,15 +149,19 @@ protected InputStream readArtifactMetadataDataFromArchiveFile(File archiveFile) @Nullable protected InputStream readArtifactMetadataDataFromFile(File archiveFile) throws IOException { - JarFile jarFile = new JarFile(archiveFile); - JarEntry jarEntry = findArtifactMetadataEntry(jarFile); - if (jarEntry == null) { - if (logger.isTraceEnabled()) { - logger.trace("The artifact metadata entry can't be resolved from the JarFile[path: '{}']", archiveFile); + try (JarFile jarFile = new JarFile(archiveFile)) { + JarEntry jarEntry = findArtifactMetadataEntry(jarFile); + if (jarEntry == null) { + if (logger.isTraceEnabled()) { + logger.trace("The artifact metadata entry can't be resolved from the JarFile[path: '{}']", archiveFile); + } + return null; + } + // Buffer the entire content so the JarFile can be closed safely + try (InputStream inputStream = jarFile.getInputStream(jarEntry)) { + return new ByteArrayInputStream(inputStream.readAllBytes()); } - return null; } - return jarFile.getInputStream(jarEntry); } @Nullable diff --git a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java index e8387f49d..ef924d31a 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java @@ -12,6 +12,7 @@ import io.microsphere.util.Utils; import java.io.File; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; @@ -74,7 +75,6 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; -import static java.util.Objects.nonNull; /** * {@link URL} Utility class @@ -666,8 +666,13 @@ public static boolean isJarURL(URL url) { String protocol = url.getProtocol(); boolean flag = false; if (FILE_PROTOCOL.equals(protocol)) { - JarFile jarFile = toJarFile(url); - flag = nonNull(jarFile); + try (JarFile jarFile = toJarFile(url)) { + flag = jarFile != null; + } catch (IOException e) { + if (logger.isTraceEnabled()) { + logger.trace("Failed to close the JarFile opened from the url : {}", url, e); + } + } } else if (JAR_PROTOCOL.equals(protocol)) { flag = true; } diff --git a/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java b/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java index a21a695d2..f44a17c85 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java +++ b/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java @@ -25,7 +25,6 @@ import static io.microsphere.util.ExceptionUtils.wrap; import static java.lang.Long.getLong; import static java.lang.Long.parseLong; -import static java.lang.Runtime.getRuntime; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -90,8 +89,6 @@ public class ProcessExecutor { private final ProcessManager processManager = INSTANCE; - private final Runtime runtime = getRuntime(); - private final String options; private final String commandLine; @@ -152,17 +149,18 @@ public void execute(OutputStream outputStream, long timeoutInMilliseconds) throw public void execute(OutputStream outputStream, long timeout, TimeUnit timeUnit) throws IOException, TimeoutException { Future future = executor.submit(() -> { - Process process = runtime.exec(commandLine); + // Use ProcessBuilder with merged stderr so we only need to drain one stream, + // avoiding the classic stdout/stderr deadlock caused by sequential draining. + Process process = new ProcessBuilder(commandLine.split("\\s+")) + .redirectErrorStream(true) + .start(); InputStream processInputStream = process.getInputStream(); - InputStream processErrorInputStream = process.getErrorStream(); FastByteArrayOutputStream targetOutputStream = new FastByteArrayOutputStream(); int exitValue = -1; try { processManager.addUnfinishedProcess(process, options); - // Copy the standard input stream + // Copy the merged standard + error output stream copy(processInputStream, targetOutputStream); - // Copy the error input stream - copy(processErrorInputStream, targetOutputStream); // wait for the process being executed process.waitFor(timeout, timeUnit); @@ -185,10 +183,10 @@ public void execute(OutputStream outputStream, long timeout, TimeUnit timeUnit) try { byte[] bytes = future.get(timeout, timeUnit); copy(new FastByteArrayInputStream(bytes), outputStream); + } catch (TimeoutException e) { + future.cancel(true); + throw e; } catch (Exception e) { - if (e instanceof TimeoutException) { - throw (TimeoutException) e; - } throw wrap(e, IOException.class); } } diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java index 4f2c8b315..b3ded020a 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java @@ -1260,21 +1260,22 @@ public static Set findClassNamesInJarFile(@Nullable File jarFile, boolea } Set classNames; try { - JarFile jarFile_ = new JarFile(jarFile); - Set jarEntries = INSTANCE.scan(jarFile_, recursive, ClassFileJarEntryFilter.INSTANCE); - int size = size(jarEntries); - if (size == 0) { - classNames = emptySet(); - } else { - classNames = newLinkedHashSet(size); - for (JarEntry jarEntry : jarEntries) { - String jarEntryName = jarEntry.getName(); - String className = resolveClassName(jarEntryName); - if (isNotBlank(className)) { - classNames.add(className); + try (JarFile jarFile_ = new JarFile(jarFile)) { + Set jarEntries = INSTANCE.scan(jarFile_, recursive, ClassFileJarEntryFilter.INSTANCE); + int size = size(jarEntries); + if (size == 0) { + classNames = emptySet(); + } else { + classNames = newLinkedHashSet(size); + for (JarEntry jarEntry : jarEntries) { + String jarEntryName = jarEntry.getName(); + String className = resolveClassName(jarEntryName); + if (isNotBlank(className)) { + classNames.add(className); + } } + classNames = unmodifiableSet(classNames); } - classNames = unmodifiableSet(classNames); } } catch (Exception e) { classNames = emptySet(); diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index 233536bd1..dc8dd6b99 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -249,8 +249,14 @@ public static JarEntry findJarEntry(URL jarURL) { return null; } final String relativePath = resolveRelativePath(jarURL); - JarEntry jarEntry = jarFile.getJarEntry(relativePath); - return jarEntry; + try (JarFile jf = jarFile) { + return jf.getJarEntry(relativePath); + } catch (IOException e) { + if (logger.isTraceEnabled()) { + logger.trace("Failed to close the JarFile opened from the url : {}", jarURL, e); + } + return null; + } } /** @@ -305,8 +311,9 @@ public static void extract(File jarSourceFile, File targetDirectory) throws IOEx * @throws IOException if an I/O error occurs during extraction or if the provided file is not a valid JAR */ public static void extract(File jarSourceFile, File targetDirectory, JarEntryFilter jarEntryFilter) throws IOException { - final JarFile jarFile = new JarFile(jarSourceFile); - extract(jarFile, targetDirectory, jarEntryFilter); + try (JarFile jarFile = new JarFile(jarSourceFile)) { + extract(jarFile, targetDirectory, jarEntryFilter); + } } /** @@ -370,19 +377,27 @@ public static void extract(JarFile jarFile, File targetDirectory, JarEntryFilter */ public static void extract(URL jarResourceURL, File targetDirectory, JarEntryFilter jarEntryFilter) throws IOException { final JarFile jarFile = toJarFile(jarResourceURL); - final String relativePath = resolveRelativePath(jarResourceURL); - final JarEntry jarEntry = jarFile.getJarEntry(relativePath); - final boolean isDirectory = jarEntry.isDirectory(); - List jarEntriesList = filter(jarFile, entry -> { - String name = entry.getName(); - if (isDirectory && name.equals(relativePath)) { - return true; - } else return name.startsWith(relativePath); - }); + if (jarFile == null) { + return; + } + try (JarFile jf = jarFile) { + final String relativePath = resolveRelativePath(jarResourceURL); + final JarEntry jarEntry = jf.getJarEntry(relativePath); + if (jarEntry == null) { + return; + } + final boolean isDirectory = jarEntry.isDirectory(); + List jarEntriesList = filter(jf, entry -> { + String name = entry.getName(); + if (isDirectory && name.equals(relativePath)) { + return true; + } else return name.startsWith(relativePath); + }); - jarEntriesList = doFilter(jarEntriesList, jarEntryFilter); + jarEntriesList = doFilter(jarEntriesList, jarEntryFilter); - doExtract(jarFile, jarEntriesList, targetDirectory); + doExtract(jf, jarEntriesList, targetDirectory); + } } @@ -451,9 +466,14 @@ protected static void doExtract(JarFile jarFile, Collection jarEntries if (jarFile == null || isEmpty(jarEntries)) { return; } + final String targetCanonicalPath = targetDirectory.getCanonicalPath() + File.separator; for (JarEntry jarEntry : jarEntries) { String jarEntryName = jarEntry.getName(); File targetFile = new File(targetDirectory, jarEntryName); + // Zip Slip protection: ensure the output file stays within the target directory + if (!targetFile.getCanonicalPath().startsWith(targetCanonicalPath)) { + throw new IOException("JAR entry '" + jarEntryName + "' would be extracted outside of the target directory"); + } if (jarEntry.isDirectory()) { targetFile.mkdirs(); } else { diff --git a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java index 09ab04336..02705ae2a 100644 --- a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java +++ b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java @@ -102,6 +102,10 @@ public Compiler(File defaultSourceDirectory, File targetDirectory) { this.sourcePaths = newLinkedHashSet(defaultSourceDirectory); this.targetDirectory = targetDirectory; this.javaCompiler = getSystemJavaCompiler(); + if (this.javaCompiler == null) { + throw new IllegalStateException( + "No Java compiler available. Ensure this process is running on a JDK (not just a JRE)."); + } } public Compiler options(String... options) { @@ -151,11 +155,12 @@ public Compiler charset(Charset charset) { public boolean compile(Class... sourceClasses) throws IOException { JavaCompiler javaCompiler = getJavaCompiler(); - StandardJavaFileManager javaFileManager = getJavaFileManager(); - CompilationTask task = javaCompiler.getTask(null, javaFileManager, - getDiagnosticListener(), getOptions(), null, getJavaFileObjects(javaFileManager, sourceClasses)); - task.setProcessors(this.getProcessors()); - return task.call(); + try (StandardJavaFileManager javaFileManager = getJavaFileManager()) { + CompilationTask task = javaCompiler.getTask(null, javaFileManager, + getDiagnosticListener(), getOptions(), null, getJavaFileObjects(javaFileManager, sourceClasses)); + task.setProcessors(this.getProcessors()); + return task.call(); + } } public JavaCompiler getJavaCompiler() { diff --git a/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java b/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java index 86bce41b5..3b50ae1a9 100644 --- a/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java +++ b/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java @@ -164,7 +164,7 @@ public Object visitAnnotation(AnnotationMirror a, ExecutableElement attributeMet attributesMap.put(attributeName, attributeValue); } - if (nestedAnnotationsAsMap) { + if (nestedAnnotationsAsMap || annotationForMapMethod == null) { return attributesMap; } From 2595480f320583faa4e03228fb00e7736ed79c1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:58:35 +0000 Subject: [PATCH 02/10] fix: add clarifying comment on JarEntry/JarFile independence in findJarEntry Agent-Logs-Url: https://github.com/microsphere-projects/microsphere-java/sessions/43b3b061-3f1c-40d4-ad76-5e234af35e41 Co-authored-by: mercyblitz <533114+mercyblitz@users.noreply.github.com> --- .../src/main/java/io/microsphere/util/jar/JarUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index dc8dd6b99..859f7a885 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -249,6 +249,8 @@ public static JarEntry findJarEntry(URL jarURL) { return null; } final String relativePath = resolveRelativePath(jarURL); + // JarEntry is metadata-only (extends ZipEntry) and does not hold a reference back + // to the JarFile, so it is safe to close the JarFile after retrieving the entry. try (JarFile jf = jarFile) { return jf.getJarEntry(relativePath); } catch (IOException e) { From d69cdf02c3103e5f75bfd3974e234e4fc28096dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 12:33:13 +0000 Subject: [PATCH 03/10] refactor: use IOUtils.toByteArray and FastByteArrayInputStream in StreamArtifactResourceResolver Agent-Logs-Url: https://github.com/microsphere-projects/microsphere-java/sessions/10a7a5c5-6aee-457c-ae4d-dfde02082f85 Co-authored-by: mercyblitz <533114+mercyblitz@users.noreply.github.com> --- .../classloading/StreamArtifactResourceResolver.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java index 3edff1227..5fba8bc6d 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java +++ b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java @@ -17,8 +17,8 @@ package io.microsphere.classloading; import io.microsphere.annotation.Nullable; +import io.microsphere.io.FastByteArrayInputStream; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -33,6 +33,7 @@ import static io.microsphere.collection.ListUtils.first; import static io.microsphere.io.FileUtils.resolveRelativePath; import static io.microsphere.io.IOUtils.close; +import static io.microsphere.io.IOUtils.toByteArray; import static io.microsphere.io.scanner.SimpleFileScanner.INSTANCE; import static io.microsphere.net.URLUtils.resolveArchiveFile; import static io.microsphere.util.Assert.assertNotNull; @@ -159,7 +160,7 @@ protected InputStream readArtifactMetadataDataFromFile(File archiveFile) throws } // Buffer the entire content so the JarFile can be closed safely try (InputStream inputStream = jarFile.getInputStream(jarEntry)) { - return new ByteArrayInputStream(inputStream.readAllBytes()); + return new FastByteArrayInputStream(toByteArray(inputStream)); } } } From 6f39ef55a3914322cca1d6aa2b914594bb43017e Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:31:08 +0800 Subject: [PATCH 04/10] Simplify ResourceProcessor.exists check Replace stream-based existence check with a non-IO check using FileObject.getLastModified(). Removes unused IOException import and avoids opening an InputStream (and related exception handling) to determine whether a resource exists, reducing I/O overhead. --- .../annotation/processor/ResourceProcessor.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java b/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java index 2b12ee46d..71852e810 100644 --- a/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java +++ b/microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ResourceProcessor.java @@ -23,7 +23,6 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.tools.FileObject; import javax.tools.JavaFileManager.Location; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; @@ -242,13 +241,6 @@ public void processInResourceWriter(String resourceName, ThrowableConsumer 0; } } From b1ef9839a72add95894ccc29e3f640374a95d3b6 Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:44:20 +0800 Subject: [PATCH 05/10] Use IOUtils.close for JarFile cleanup Replace try-with-resources around toJarFile(url) with an explicit try/finally that calls IOUtils.close(jarFile). Add IOUtils and Objects.nonNull imports and remove the now-unused IOException import. This simplifies resource cleanup, delegates close/error handling to IOUtils, and keeps the null check explicit when determining if a file URL refers to a JarFile. --- .../main/java/io/microsphere/net/URLUtils.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java index ef924d31a..c1b98258d 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java @@ -7,12 +7,12 @@ import io.microsphere.annotation.Nonnull; import io.microsphere.annotation.Nullable; import io.microsphere.constants.SymbolConstants; +import io.microsphere.io.IOUtils; import io.microsphere.logging.Logger; import io.microsphere.util.ArrayUtils; import io.microsphere.util.Utils; import java.io.File; -import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; @@ -75,6 +75,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.nonNull; /** * {@link URL} Utility class @@ -666,12 +667,12 @@ public static boolean isJarURL(URL url) { String protocol = url.getProtocol(); boolean flag = false; if (FILE_PROTOCOL.equals(protocol)) { - try (JarFile jarFile = toJarFile(url)) { - flag = jarFile != null; - } catch (IOException e) { - if (logger.isTraceEnabled()) { - logger.trace("Failed to close the JarFile opened from the url : {}", url, e); - } + JarFile jarFile = null; + try { + jarFile = toJarFile(url); + flag = nonNull(jarFile); + } finally { + IOUtils.close(jarFile); } } else if (JAR_PROTOCOL.equals(protocol)) { flag = true; From b4cae21d25aa3209a85dfa56ded9f937978c0b2a Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:44:30 +0800 Subject: [PATCH 06/10] Use IOUtils.close and FILE_SEPARATOR in JarUtils Refactor JarUtils: import IOUtils and use IOUtils.close to close JarFile in a finally block instead of try-with-resources when resolving a JarEntry, and use the SeparatorConstants.FILE_SEPARATOR constant when building target canonical paths. These changes standardize resource cleanup and separator usage across the codebase and add the necessary imports. --- .../io/microsphere/util/jar/JarUtils.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index 859f7a885..106bc15ff 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -8,6 +8,7 @@ import io.microsphere.annotation.Nullable; import io.microsphere.constants.ProtocolConstants; import io.microsphere.filter.JarEntryFilter; +import io.microsphere.io.IOUtils; import io.microsphere.logging.Logger; import io.microsphere.net.URLUtils; import io.microsphere.util.Utils; @@ -30,6 +31,7 @@ import static io.microsphere.constants.ProtocolConstants.FILE_PROTOCOL; import static io.microsphere.constants.ProtocolConstants.JAR_PROTOCOL; import static io.microsphere.constants.SeparatorConstants.ARCHIVE_ENTRY_SEPARATOR; +import static io.microsphere.constants.SeparatorConstants.FILE_SEPARATOR; import static io.microsphere.io.IOUtils.close; import static io.microsphere.io.IOUtils.copy; import static io.microsphere.logging.LoggerFactory.getLogger; @@ -244,20 +246,18 @@ protected static List doFilter(Iterable jarEntries, JarEntry */ @Nullable public static JarEntry findJarEntry(URL jarURL) { - JarFile jarFile = toJarFile(jarURL); - if (jarFile == null) { - return null; - } - final String relativePath = resolveRelativePath(jarURL); - // JarEntry is metadata-only (extends ZipEntry) and does not hold a reference back - // to the JarFile, so it is safe to close the JarFile after retrieving the entry. - try (JarFile jf = jarFile) { - return jf.getJarEntry(relativePath); - } catch (IOException e) { - if (logger.isTraceEnabled()) { - logger.trace("Failed to close the JarFile opened from the url : {}", jarURL, e); + JarFile jarFile = null; + try { + jarFile = toJarFile(jarURL); + if (jarFile == null) { + return null; } - return null; + final String relativePath = resolveRelativePath(jarURL); + // JarEntry is metadata-only (extends ZipEntry) and does not hold a reference back + // to the JarFile, so it is safe to close the JarFile after retrieving the entry. + return jarFile.getJarEntry(relativePath); + } finally { + close(jarFile); } } @@ -468,7 +468,7 @@ protected static void doExtract(JarFile jarFile, Collection jarEntries if (jarFile == null || isEmpty(jarEntries)) { return; } - final String targetCanonicalPath = targetDirectory.getCanonicalPath() + File.separator; + final String targetCanonicalPath = targetDirectory.getCanonicalPath() + FILE_SEPARATOR; for (JarEntry jarEntry : jarEntries) { String jarEntryName = jarEntry.getName(); File targetFile = new File(targetDirectory, jarEntryName); From c0bd8c11032d3adc1777d749c2e98ce16fae496b Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:47:44 +0800 Subject: [PATCH 07/10] Use concise local variable name for JarFile Replace the local variable name jarFile_ with jf in ClassUtils.java when opening the JarFile and passing it to INSTANCE.scan. This is a small refactor to simplify the local variable name and improve readability without changing behavior. --- .../src/main/java/io/microsphere/util/ClassUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java index b3ded020a..a22ba0508 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java @@ -1260,8 +1260,8 @@ public static Set findClassNamesInJarFile(@Nullable File jarFile, boolea } Set classNames; try { - try (JarFile jarFile_ = new JarFile(jarFile)) { - Set jarEntries = INSTANCE.scan(jarFile_, recursive, ClassFileJarEntryFilter.INSTANCE); + try (JarFile jf = new JarFile(jarFile)) { + Set jarEntries = INSTANCE.scan(jf, recursive, ClassFileJarEntryFilter.INSTANCE); int size = size(jarEntries); if (size == 0) { classNames = emptySet(); From 5b1f90aa98eaff25c7f127df0f1d16e8cc1f8b15 Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:50:41 +0800 Subject: [PATCH 08/10] Use assertNotNull for javaCompiler check Replace the manual null-check and IllegalStateException throw with a call to io.microsphere.util.Assert.assertNotNull and add its static import. This simplifies the constructor by centralizing null validation while preserving the original error message about requiring a JDK. --- .../java/io/microsphere/jdk/tools/compiler/Compiler.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java index 02705ae2a..50aa1588b 100644 --- a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java +++ b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java @@ -47,6 +47,7 @@ import static io.microsphere.logging.LoggerFactory.getLogger; import static io.microsphere.text.FormatUtils.format; import static io.microsphere.util.ArrayUtils.ofArray; +import static io.microsphere.util.Assert.assertNotNull; import static io.microsphere.util.ClassUtils.getTypeName; import static io.microsphere.util.StringUtils.substringBefore; import static java.io.File.separatorChar; @@ -102,10 +103,7 @@ public Compiler(File defaultSourceDirectory, File targetDirectory) { this.sourcePaths = newLinkedHashSet(defaultSourceDirectory); this.targetDirectory = targetDirectory; this.javaCompiler = getSystemJavaCompiler(); - if (this.javaCompiler == null) { - throw new IllegalStateException( - "No Java compiler available. Ensure this process is running on a JDK (not just a JRE)."); - } + assertNotNull(this.javaCompiler, () -> "No Java compiler available. Ensure this process is running on a JDK (not just a JRE)."); } public Compiler options(String... options) { From 6e2e705e519b20e5cd4616ffdb4a1f3793239001 Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 09:57:24 +0800 Subject: [PATCH 09/10] Remove unused IOUtils import from JarUtils Remove the unused import of io.microsphere.io.IOUtils from JarUtils.java to clean up code and eliminate a compiler/IDE warning. No functional changes were made. --- .../src/main/java/io/microsphere/util/jar/JarUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index 106bc15ff..ba36227b9 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -8,7 +8,6 @@ import io.microsphere.annotation.Nullable; import io.microsphere.constants.ProtocolConstants; import io.microsphere.filter.JarEntryFilter; -import io.microsphere.io.IOUtils; import io.microsphere.logging.Logger; import io.microsphere.net.URLUtils; import io.microsphere.util.Utils; From 8a15d444928a9cac7800c5ea213d43775e15b189 Mon Sep 17 00:00:00 2001 From: Mercy Ma Date: Tue, 26 May 2026 10:23:30 +0800 Subject: [PATCH 10/10] Add null checks and logging to JarUtils Fix JarUtils logger reference and improve null-safety and diagnostics: import and use assertNotNull, add @Nonnull/@Nullable annotations, document thrown NullPointerException, and add parameter null checks in extract/doExtract. Resolve and log invalid JAR/entry cases (guarded warn logs) and tidy minor formatting/comments. Update tests: expand LoggingTest value source and extend JarUtilsTest with cases for extraction success, null arguments, missing JAR file, and missing JAR entry. --- .../io/microsphere/util/jar/JarUtils.java | 25 +++++++++++++----- .../test/java/io/microsphere/LoggingTest.java | 2 +- .../io/microsphere/util/jar/JarUtilsTest.java | 26 ++++++++++++++++--- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index ba36227b9..cceb2ced5 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -38,6 +38,7 @@ import static io.microsphere.net.URLUtils.normalizePath; import static io.microsphere.net.URLUtils.resolveArchiveFile; import static io.microsphere.text.FormatUtils.format; +import static io.microsphere.util.Assert.assertNotNull; import static io.microsphere.util.StringUtils.EMPTY; import static io.microsphere.util.StringUtils.substringAfter; import static java.util.Collections.emptyList; @@ -53,7 +54,7 @@ */ public abstract class JarUtils implements Utils { - private static final Logger logger = getLogger(URLUtils.class); + private static final Logger logger = getLogger(JarUtils.class); /** * The resource path of Manifest file in JAR archive. @@ -78,10 +79,11 @@ public abstract class JarUtils implements Utils { * * @param jarURL the URL pointing to a JAR file or entry; must not be {@code null} * @return a new {@link JarFile} instance if resolved successfully, or {@code null} if resolution fails + * @throws NullPointerException if the provided {@code jarURL} is {@code null} * @throws IllegalArgumentException if the URL protocol is neither "jar" nor "file" */ @Nullable - public static JarFile toJarFile(URL jarURL) throws IllegalArgumentException { + public static JarFile toJarFile(URL jarURL) throws NullPointerException, IllegalArgumentException { final String jarAbsolutePath = resolveJarAbsolutePath(jarURL); if (jarAbsolutePath == null) { return null; @@ -107,7 +109,7 @@ public static JarFile toJarFile(URL jarURL) throws IllegalArgumentException { * file} */ protected static void assertJarURLProtocol(URL jarURL) throws NullPointerException, IllegalArgumentException { - final String protocol = jarURL.getProtocol(); //NPE check + final String protocol = jarURL.getProtocol(); // NPE check if (!JAR_PROTOCOL.equals(protocol) && !FILE_PROTOCOL.equals(protocol)) { String message = format("the protocol['{}'] of 'jarURL' is unsupported, except '{}' and '{}' ", protocol, JAR_PROTOCOL, FILE_PROTOCOL); throw new IllegalArgumentException(message); @@ -374,17 +376,27 @@ public static void extract(JarFile jarFile, File targetDirectory, JarEntryFilter * @param jarResourceURL the URL pointing to a resource within a JAR file; must not be {@code null} * @param targetDirectory the directory where the contents should be extracted; must not be {@code null} * @param jarEntryFilter an optional filter to determine which entries to extract; if {@code null}, all entries are extracted - * @throws IOException if an I/O error occurs during extraction or resolving the JAR resource + * @throws IllegalArgumentException if {@code jarResourceURL} or {@code targetDirectory} is {@code null} + * @throws IOException if an I/O error occurs during extraction or resolving the JAR resource */ - public static void extract(URL jarResourceURL, File targetDirectory, JarEntryFilter jarEntryFilter) throws IOException { + public static void extract(@Nonnull URL jarResourceURL, @Nonnull File targetDirectory, @Nullable JarEntryFilter jarEntryFilter) + throws NullPointerException, IOException { + assertNotNull(jarResourceURL, () -> "The 'jarResourceURL' argument must not be null"); + assertNotNull(targetDirectory, () -> "The 'targetDirectory' argument must not be null"); final JarFile jarFile = toJarFile(jarResourceURL); if (jarFile == null) { + if (logger.isWarnEnabled()) { + logger.warn("The provided URL does not point to a valid JAR resource: {}", jarResourceURL); + } return; } try (JarFile jf = jarFile) { final String relativePath = resolveRelativePath(jarResourceURL); final JarEntry jarEntry = jf.getJarEntry(relativePath); if (jarEntry == null) { + if (logger.isWarnEnabled()) { + logger.warn("The provided URL does not point to a valid JAR entry: {}", jarResourceURL); + } return; } final boolean isDirectory = jarEntry.isDirectory(); @@ -460,7 +472,8 @@ public static boolean isDirectoryEntry(URL url) { * @param jarFile the source JAR file; if {@code null}, no extraction is performed * @param jarEntries the collection of entries to extract; if empty or {@code null}, no extraction is performed * @param targetDirectory the target directory for extraction; must not be {@code null} - * @throws IOException if an I/O error occurs during extraction + * @throws NullPointerException if {@code targetDirectory} is {@code null} + * @throws IOException if an I/O error occurs during extraction * @since 1.0.0 */ protected static void doExtract(JarFile jarFile, Collection jarEntries, File targetDirectory) throws IOException { diff --git a/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java b/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java index 578b52537..c3c778241 100644 --- a/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java +++ b/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java @@ -38,7 +38,7 @@ * @since 1.0.0 */ @ParameterizedClass -@ValueSource(strings = {"INFO", "TRACE"}) +@ValueSource(strings = {"TRACE", "INFO", "ERROR"}) @Disabled public abstract class LoggingTest { diff --git a/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java b/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java index 11c03dea8..d4ec4e9c4 100644 --- a/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java +++ b/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java @@ -125,16 +125,36 @@ void testToJarFileOnNPE() { } @Test - void testFindJarEntry() throws Exception { + void testFindJarEntry() { URL resourceURL = getClassResource(this.classLoader, Nonnull.class); JarEntry jarEntry = findJarEntry(resourceURL); assertNotNull(jarEntry); } @Test - void testExtract() throws IOException { + void testExtract() { String jarAbsolutePath = resolveJarAbsolutePath(this.resourceURL); - extract(new File(jarAbsolutePath), this.targetDirectory); + assertDoesNotThrow(() -> extract(new File(jarAbsolutePath), this.targetDirectory)); + } + + @Test + void testExtractOnNull() { + assertThrows(IllegalArgumentException.class, () -> extract((URL) null, this.targetDirectory, null)); + assertThrows(IllegalArgumentException.class, () -> extract(this.resourceURL, null, null)); + assertDoesNotThrow(() -> extract(this.resourceURL, this.targetDirectory, null)); + } + + @Test + void testExtractOnJarFileNotFound() { + URL resourceURL = ofURL("jar:file:/path/to/file.jar!/entry"); + assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null)); + } + + @Test + void testExtractOnJarEntryNotFound() { + String resource = this.resourceURL.toString(); + URL resourceURL = ofURL(resource.replace("Nonnull.class", "NotFound.class")); + assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null)); } @Test