From 582bc9325c371c97a8710330fe3e8f83d8891268 Mon Sep 17 00:00:00 2001 From: guslegend <1670547022@qq.com> Date: Wed, 1 Jul 2026 20:24:34 +0800 Subject: [PATCH] fix(core): reuse classpath skill jar file systems --- .../repository/ClasspathSkillRepository.java | 70 ++++++++++++++- .../ClasspathSkillRepositoryTest.java | 86 +++++++++++++++++++ 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/repository/ClasspathSkillRepository.java b/agentscope-core/src/main/java/io/agentscope/core/skill/repository/ClasspathSkillRepository.java index ef9c97d24b..2d0ab37b28 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/repository/ClasspathSkillRepository.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/repository/ClasspathSkillRepository.java @@ -22,10 +22,13 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,11 +77,16 @@ */ public class ClasspathSkillRepository implements AgentSkillRepository { + private static final Object FILE_SYSTEM_MONITOR = new Object(); + + private static final Map SHARED_FILE_SYSTEMS = new HashMap<>(); + private final Logger logger = LoggerFactory.getLogger(ClasspathSkillRepository.class); private final FileSystem fileSystem; private final Path skillBasePath; private final boolean isJar; + private final URI jarFileSystemUri; private final String source; private final String resourcePath; private final AtomicBoolean closed = new AtomicBoolean(false); @@ -139,7 +147,8 @@ protected ClasspathSkillRepository(String resourcePath, String source, ClassLoad if ("jar".equals(uri.getScheme())) { this.isJar = true; - this.fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + this.jarFileSystemUri = uri; + this.fileSystem = acquireFileSystem(uri); String schemeSpecificUriPath = uri.getSchemeSpecificPart(); String actualResourcePath = schemeSpecificUriPath.substring(schemeSpecificUriPath.lastIndexOf("!") + 1); @@ -148,6 +157,7 @@ protected ClasspathSkillRepository(String resourcePath, String source, ClassLoad } else { this.isJar = false; this.fileSystem = null; + this.jarFileSystemUri = null; this.skillBasePath = Path.of(uri); } logger.info("is in Jar environment: {}", this.isJar); @@ -266,6 +276,48 @@ public boolean isJarEnvironment() { return isJar; } + private static FileSystem acquireFileSystem(URI uri) throws IOException { + synchronized (FILE_SYSTEM_MONITOR) { + SharedFileSystemRef existingRef = SHARED_FILE_SYSTEMS.get(uri); + if (existingRef != null && existingRef.fileSystem.isOpen()) { + existingRef.refCount++; + return existingRef.fileSystem; + } + SHARED_FILE_SYSTEMS.remove(uri); + + FileSystem fileSystem; + boolean closeWhenUnused = false; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (FileSystemNotFoundException ignored) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + closeWhenUnused = true; + } + + SHARED_FILE_SYSTEMS.put(uri, new SharedFileSystemRef(fileSystem, closeWhenUnused)); + return fileSystem; + } + } + + private static void releaseFileSystem(URI uri) throws IOException { + synchronized (FILE_SYSTEM_MONITOR) { + SharedFileSystemRef ref = SHARED_FILE_SYSTEMS.get(uri); + if (ref == null) { + return; + } + + ref.refCount--; + if (ref.refCount > 0) { + return; + } + + SHARED_FILE_SYSTEMS.remove(uri); + if (ref.closeWhenUnused && ref.fileSystem.isOpen()) { + ref.fileSystem.close(); + } + } + } + private void checkNotClosed() { if (closed.get()) { throw new IllegalStateException("ClasspathSkillRepository has been closed"); @@ -283,12 +335,24 @@ public void close() { if (!closed.compareAndSet(false, true)) { return; } - if (fileSystem != null) { + if (jarFileSystemUri != null) { try { - fileSystem.close(); + releaseFileSystem(jarFileSystemUri); } catch (IOException e) { throw new RuntimeException("Failed to close classpath file system", e); } } } + + private static final class SharedFileSystemRef { + + private final FileSystem fileSystem; + private final boolean closeWhenUnused; + private int refCount = 1; + + private SharedFileSystemRef(FileSystem fileSystem, boolean closeWhenUnused) { + this.fileSystem = fileSystem; + this.closeWhenUnused = closeWhenUnused; + } + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/repository/ClasspathSkillRepositoryTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/repository/ClasspathSkillRepositoryTest.java index fc43a83961..c2b901a73f 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/skill/repository/ClasspathSkillRepositoryTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/repository/ClasspathSkillRepositoryTest.java @@ -214,6 +214,30 @@ void testLoadAllSkillsFromJar() throws Exception { } } + @Test + @DisplayName("Should reuse JAR file system across repository instances") + void testReuseJarFileSystemAcrossRepositories() throws Exception { + Path jarPath = createTestJarInFolder("shared-skill", "Shared Skill", "Shared content"); + + try (URLClassLoader classLoader = new URLClassLoader(new URL[] {jarPath.toUri().toURL()}); + ClasspathSkillRepository firstRepository = + new ClasspathSkillRepositoryWithClassLoader("jar-skills", classLoader); + ClasspathSkillRepository secondRepository = + new ClasspathSkillRepositoryWithClassLoader("jar-skills", classLoader)) { + + AgentSkill firstSkill = firstRepository.getSkill("shared-skill"); + AgentSkill secondSkill = secondRepository.getSkill("shared-skill"); + + assertEquals("shared-skill", firstSkill.getName()); + assertEquals("shared-skill", secondSkill.getName()); + + firstRepository.close(); + + AgentSkill stillAccessible = secondRepository.getSkill("shared-skill"); + assertEquals("shared-skill", stillAccessible.getName()); + } + } + @Test @DisplayName("Should load skills from Spring Boot Fat JAR (BOOT-INF/classes/)") void testLoadFromSpringBootJar() throws Exception { @@ -317,6 +341,37 @@ protected URLConnection openConnection(URL u) } } + @Test + @DisplayName("Should reuse nested JAR file system across repository instances") + void testReuseNestedJarFileSystemAcrossRepositories() throws Exception { + Path outerJarPath = + createSpringBootNestedLibTestJar( + "nested-shared-skill", "Nested Shared Skill", "Nested shared content"); + Path innerJarPath = extractInnerJar(outerJarPath, "BOOT-INF/lib/nested-skill.jar"); + + TestNestedFileSystemProvider.configuredInnerJarPath = innerJarPath; + try (ClasspathSkillRepository firstRepository = + new ClasspathSkillRepositoryWithClassLoader( + "jar-skills", createNestedJarClassLoader(outerJarPath)); + ClasspathSkillRepository secondRepository = + new ClasspathSkillRepositoryWithClassLoader( + "jar-skills", createNestedJarClassLoader(outerJarPath))) { + + AgentSkill firstSkill = firstRepository.getSkill("nested-shared-skill"); + AgentSkill secondSkill = secondRepository.getSkill("nested-shared-skill"); + + assertEquals("nested-shared-skill", firstSkill.getName()); + assertEquals("nested-shared-skill", secondSkill.getName()); + + firstRepository.close(); + + AgentSkill stillAccessible = secondRepository.getSkill("nested-shared-skill"); + assertEquals("nested-shared-skill", stillAccessible.getName()); + } finally { + TestNestedFileSystemProvider.configuredInnerJarPath = null; + } + } + // ==================== getSource Tests ==================== @Test @@ -695,6 +750,37 @@ private Path extractInnerJar(Path outerJarPath, String innerEntryName) throws IO return innerJarPath; } + private ClassLoader createNestedJarClassLoader(Path outerJarPath) { + return new ClassLoader(ClassLoader.getSystemClassLoader()) { + @Override + public URL getResource(String name) { + if ("jar-skills".equals(name)) { + try { + String nestedUrlStr = + "jar:nested:" + + outerJarPath.toUri().getRawPath() + + "/!BOOT-INF/lib/nested-skill.jar!/" + + name; + return new URL( + null, + nestedUrlStr, + new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) + throws IOException { + throw new UnsupportedOperationException( + "nested URL for test only"); + } + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return super.getResource(name); + } + }; + } + /** * Custom adapter that uses a specific ClassLoader for testing JAR loading. */