diff --git a/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/src/main/java/run/halo/app/plugin/HaloPluginManager.java index e86fc03de..8e83fec31 100644 --- a/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -182,6 +182,7 @@ public class HaloPluginManager extends DefaultPluginManager long ts = System.currentTimeMillis(); for (PluginWrapper pluginWrapper : resolvedPlugins) { + checkExtensionFinderReady(pluginWrapper); PluginState pluginState = pluginWrapper.getPluginState(); if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) { try { @@ -232,6 +233,8 @@ public class HaloPluginManager extends DefaultPluginManager checkPluginId(pluginId); PluginWrapper pluginWrapper = getPlugin(pluginId); + checkExtensionFinderReady(pluginWrapper); + PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); PluginState pluginState = pluginWrapper.getPluginState(); if (PluginState.STARTED == pluginState) { @@ -286,6 +289,15 @@ public class HaloPluginManager extends DefaultPluginManager return pluginWrapper.getPluginState(); } + private void checkExtensionFinderReady(PluginWrapper pluginWrapper) { + if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) { + springComponentsFinder.readPluginStorageToMemory(pluginWrapper); + return; + } + // should never happen + throw new PluginRuntimeException("Plugin component classes may not loaded yet."); + } + private void doStopPlugins() { startingErrors.clear(); // stop started plugins in reverse order @@ -367,6 +379,7 @@ public class HaloPluginManager extends DefaultPluginManager * Release plugin holding release on stop. */ public void releaseAdditionalResources(String pluginId) { + removePluginComponentsCache(pluginId); // release request mapping requestMappingManager.removeHandlerMappings(pluginId); try { @@ -389,5 +402,10 @@ public class HaloPluginManager extends DefaultPluginManager return null; } + private void removePluginComponentsCache(String pluginId) { + if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) { + springComponentsFinder.removeComponentsStorage(pluginId); + } + } // end-region } diff --git a/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java b/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java index 01545f8e2..1e22b101d 100644 --- a/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java +++ b/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java @@ -138,6 +138,10 @@ public class PluginApplicationInitializer { stopWatch.start("getExtensionClassNames"); Set extensionClassNames = haloPluginManager.getExtensionClassNames(pluginId); + if (extensionClassNames == null) { + log.debug("No components class names found for plugin [{}]", pluginId); + extensionClassNames = Set.of(); + } stopWatch.stop(); // add extensions for each started plugin diff --git a/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java b/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java index ef2a18747..66ba0a0cd 100644 --- a/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java +++ b/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java @@ -6,16 +6,22 @@ import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.StampedLock; import lombok.extern.slf4j.Slf4j; import org.pf4j.AbstractExtensionFinder; +import org.pf4j.PluginDependency; import org.pf4j.PluginManager; +import org.pf4j.PluginState; +import org.pf4j.PluginStateEvent; import org.pf4j.PluginWrapper; import org.pf4j.processor.ExtensionStorage; +import org.springframework.util.Assert; /** *

The spring component finder. it will read {@code META-INF/plugin-components.idx} file in @@ -29,6 +35,7 @@ import org.pf4j.processor.ExtensionStorage; @Slf4j public class SpringComponentsFinder extends AbstractExtensionFinder { public static final String EXTENSIONS_RESOURCE = "META-INF/plugin-components.idx"; + private final StampedLock entryStampedLock = new StampedLock(); public SpringComponentsFinder(PluginManager pluginManager) { super(pluginManager); @@ -36,48 +43,124 @@ public class SpringComponentsFinder extends AbstractExtensionFinder { @Override public Map> readClasspathStorages() { - log.debug("Reading extensions storages from classpath"); + log.debug("Reading components storages from classpath"); return Collections.emptyMap(); } @Override public Map> readPluginsStorages() { - log.debug("Reading extensions storages from plugins"); + log.debug("Reading components storages from plugins"); Map> result = new LinkedHashMap<>(); List plugins = pluginManager.getPlugins(); for (PluginWrapper plugin : plugins) { - String pluginId = plugin.getDescriptor().getPluginId(); - log.debug("Reading extensions storage from plugin '{}'", pluginId); - Set bucket = new HashSet<>(); - - try { - log.debug("Read '{}'", EXTENSIONS_RESOURCE); - ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); - try (InputStream resourceStream = pluginClassLoader.getResourceAsStream( - EXTENSIONS_RESOURCE)) { - if (resourceStream == null) { - log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); - } else { - collectExtensions(resourceStream, bucket); - } - } - - debugExtensions(bucket); - - result.put(pluginId, bucket); - } catch (IOException e) { - log.error(e.getMessage(), e); - } + readPluginStorageToMemory(plugin); } return result; } + @Override + public void pluginStateChanged(PluginStateEvent event) { + // see supper class for more details + if (checkForExtensionDependencies == null && PluginState.STARTED.equals( + event.getPluginState())) { + for (PluginDependency dependency : event.getPlugin().getDescriptor() + .getDependencies()) { + if (dependency.isOptional()) { + log.debug("Enable check for extension dependencies via ASM."); + checkForExtensionDependencies = true; + break; + } + } + } + } + private void collectExtensions(InputStream inputStream, Set bucket) throws IOException { try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { ExtensionStorage.read(reader, bucket); } } + + protected void readPluginStorageToMemory(PluginWrapper pluginWrapper) { + String pluginId = pluginWrapper.getPluginId(); + if (containsComponentsStorage(pluginId)) { + return; + } + log.debug("Reading components storage from plugin '{}'", pluginId); + Set bucket = new HashSet<>(); + + try { + log.debug("Read '{}'", EXTENSIONS_RESOURCE); + ClassLoader pluginClassLoader = pluginWrapper.getPluginClassLoader(); + try (InputStream resourceStream = pluginClassLoader.getResourceAsStream( + EXTENSIONS_RESOURCE)) { + if (resourceStream == null) { + log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); + } else { + collectExtensions(resourceStream, bucket); + } + } + + debugExtensions(bucket); + + putComponentsStorage(pluginId, bucket); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + protected boolean containsComponentsStorage(String pluginId) { + Assert.notNull(pluginId, "The pluginId cannot be null"); + long stamp = entryStampedLock.tryOptimisticRead(); + boolean contains = super.entries != null && super.entries.containsKey(pluginId); + if (!entryStampedLock.validate(stamp)) { + stamp = entryStampedLock.readLock(); + try { + return super.entries != null && entries.containsKey(pluginId); + } finally { + entryStampedLock.unlockRead(stamp); + } + } + return contains; + } + + protected void putComponentsStorage(String pluginId, Set components) { + Assert.notNull(pluginId, "The pluginId cannot be null"); + // When the lock remains in write mode, the read lock cannot be obtained + long stamp = entryStampedLock.writeLock(); + try { + Map> componentNamesMap; + if (super.entries == null) { + componentNamesMap = new HashMap<>(); + } else { + componentNamesMap = new HashMap<>(super.entries); + } + log.debug("Load [{}] component names into storage cache for plugin [{}].", + components.size(), pluginId); + componentNamesMap.put(pluginId, components); + super.entries = componentNamesMap; + } finally { + entryStampedLock.unlockWrite(stamp); + } + } + + protected void removeComponentsStorage(String pluginId) { + Assert.notNull(pluginId, "The pluginId cannot be null"); + long stamp = entryStampedLock.writeLock(); + try { + Map> componentNamesMap; + if (super.entries == null) { + componentNamesMap = new HashMap<>(); + } else { + componentNamesMap = new HashMap<>(super.entries); + } + log.debug("Removing components storage from cache [{}].", pluginId); + componentNamesMap.remove(pluginId); + super.entries = componentNamesMap; + } finally { + entryStampedLock.unlockWrite(stamp); + } + } } diff --git a/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java b/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java new file mode 100644 index 000000000..ec9a21d19 --- /dev/null +++ b/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java @@ -0,0 +1,92 @@ +package run.halo.app.plugin; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pf4j.PluginClassLoader; +import org.pf4j.PluginWrapper; +import org.springframework.util.ResourceUtils; + +/** + * Tests for {@link SpringComponentsFinder}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class SpringComponentsFinderTest { + + private File testFile; + @Mock + private HaloPluginManager pluginManager; + @Mock + private PluginWrapper pluginWrapper; + @Mock + private PluginClassLoader pluginClassLoader; + + @InjectMocks + private SpringComponentsFinder springComponentsFinder; + + @BeforeEach + void setUp() throws FileNotFoundException { + testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx"); + } + + @Test + void readPluginStorageToMemory() throws FileNotFoundException { + boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); + assertThat(contains).isFalse(); + + when(pluginWrapper.getPluginId()).thenReturn("fakePlugin"); + when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader); + when(pluginClassLoader.getResourceAsStream(any())) + .thenReturn(new FileInputStream(testFile)); + + springComponentsFinder.readPluginStorageToMemory(pluginWrapper); + + contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); + assertThat(contains).isTrue(); + + verify(pluginClassLoader, times(1)).getResourceAsStream(any()); + + // repeat it + springComponentsFinder.readPluginStorageToMemory(pluginWrapper); + verify(pluginClassLoader, times(1)).getResourceAsStream(any()); + } + + @Test + void containsPlugin() { + boolean exist = springComponentsFinder.containsComponentsStorage("NotExist"); + assertThat(exist).isFalse(); + + assertThatThrownBy(() -> springComponentsFinder.containsComponentsStorage(null)) + .hasMessage("The pluginId cannot be null"); + } + + @Test + void removeComponentsCache() { + springComponentsFinder.putComponentsStorage("fakePlugin", Set.of("A")); + boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); + assertThat(contains).isTrue(); + + springComponentsFinder.removeComponentsStorage("fakePlugin"); + + contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); + assertThat(contains).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/resources/plugin/test-plugin-components.idx b/src/test/resources/plugin/test-plugin-components.idx new file mode 100644 index 000000000..1835b4bbf --- /dev/null +++ b/src/test/resources/plugin/test-plugin-components.idx @@ -0,0 +1,2 @@ +# Generated by Halo +run.halo.links.LinkPlugin