Fix the problem where extension point might not be obtained when the plugin was started (#6006)

#### What type of PR is this?

/kind bug
/area plugin
/area core

#### What this PR does / why we need it:

This PR refactors SpringComponentsFinder to prevent unexpected cleanup, which might lead to not be able to obtain plugin extension when the plugin was started.

The real reason is that entries is initialized by AttachmentReconciler during initialization of some plugins.

Please note the problem cannot be reproduced stably.

Refs:

- e3125f2998/pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java (L229-L249)
- e3125f2998/pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java (L312-L316)

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/5999

#### Does this PR introduce a user-facing change?

```release-note
修复插件启动成功但仍然可能无法获取扩展点导致页面无法访问的问题
```
pull/6011/head
John Niang 2024-05-29 14:35:10 +08:00 committed by GitHub
parent 608f2bbca3
commit 08ff7e6e00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 73 deletions

View File

@ -5,19 +5,17 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
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 java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.AbstractExtensionFinder;
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;
/**
* <p>The spring component finder. it will read {@code META-INF/plugin-components.idx} file in
@ -31,53 +29,41 @@ import org.springframework.util.Assert;
@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);
entries = new ConcurrentHashMap<>();
}
@Override
public Map<String, Set<String>> readClasspathStorages() {
log.debug("Reading components storages from classpath");
return Collections.emptyMap();
throw new UnsupportedOperationException();
}
@Override
public Map<String, Set<String>> readPluginsStorages() {
// We have to copy the source code from `org.pf4j.LegacyExtensionFinder.readPluginsStorages`
// because we have to adapt to the new extensions resource location
// `META-INF/plugin-components.idx`.
log.debug("Reading components storages from plugins");
Map<String, Set<String>> result = new LinkedHashMap<>();
throw new UnsupportedOperationException();
}
List<PluginWrapper> plugins = pluginManager.getPlugins();
for (PluginWrapper plugin : plugins) {
String pluginId = plugin.getDescriptor().getPluginId();
private Set<String> readPluginStorage(PluginWrapper pluginWrapper) {
var pluginId = pluginWrapper.getPluginId();
log.debug("Reading extensions storage from plugin '{}'", pluginId);
Set<String> bucket = new HashSet<>();
var bucket = new HashSet<String>();
try {
log.debug("Read '{}'", EXTENSIONS_RESOURCE);
ClassLoader pluginClassLoader = plugin.getPluginClassLoader();
try (var resourceStream =
pluginClassLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) {
var classLoader = pluginWrapper.getPluginClassLoader();
try (var resourceStream = classLoader.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("Failed to read components from " + EXTENSIONS_RESOURCE, e);
}
}
return result;
return bucket;
}
private void collectExtensions(InputStream inputStream, Set<String> bucket) throws IOException {
@ -86,20 +72,15 @@ public class SpringComponentsFinder extends AbstractExtensionFinder {
}
}
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);
@Override
public void pluginStateChanged(PluginStateEvent event) {
var pluginState = event.getPluginState();
String pluginId = event.getPlugin().getPluginId();
if (pluginState == PluginState.UNLOADED) {
entries.remove(pluginId);
} else if (pluginState == PluginState.CREATED || pluginState == PluginState.RESOLVED) {
entries.computeIfAbsent(pluginId, id -> readPluginStorage(event.getPlugin()));
}
}
return contains;
}
}

View File

@ -1,18 +1,25 @@
package run.halo.app.plugin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.FileNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Set;
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.PluginManager;
import org.pf4j.PluginState;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginWrapper;
import org.springframework.util.ResourceUtils;
@ -25,29 +32,64 @@ import org.springframework.util.ResourceUtils;
@ExtendWith(MockitoExtension.class)
class SpringComponentsFinderTest {
private File testFile;
@Mock
private HaloPluginManager pluginManager;
@Mock
private PluginWrapper pluginWrapper;
@Mock
private PluginClassLoader pluginClassLoader;
private PluginManager pluginManager;
@InjectMocks
private SpringComponentsFinder springComponentsFinder;
private SpringComponentsFinder finder;
@BeforeEach
void setUp() throws FileNotFoundException {
testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx");
@Test
void shouldNotInvokeReadClasspathStorages() {
assertThrows(UnsupportedOperationException.class,
() -> finder.readClasspathStorages()
);
}
@Test
void containsPlugin() {
boolean exist = springComponentsFinder.containsComponentsStorage("NotExist");
assertThat(exist).isFalse();
assertThatThrownBy(() -> springComponentsFinder.containsComponentsStorage(null))
.hasMessage("The pluginId cannot be null");
void shouldNotInvokeReadPluginsStorages() {
assertThrows(UnsupportedOperationException.class,
() -> finder.readPluginsStorages()
);
}
@Test
void shouldPutEntryIfPluginCreated() throws FileNotFoundException {
var pluginWrapper = mockPluginWrapper();
when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED);
var event = new PluginStateEvent(pluginManager, pluginWrapper, null);
finder.pluginStateChanged(event);
var classNames = finder.findClassNames("fake-plugin");
assertEquals(Set.of("run.halo.fake.FakePlugin"), classNames);
}
@Test
void shouldRemoveEntryIfPluginUnloaded() throws FileNotFoundException {
var pluginWrapper = mockPluginWrapper();
when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED);
var event = new PluginStateEvent(pluginManager, pluginWrapper, null);
finder.pluginStateChanged(event);
var classNames = finder.findClassNames("fake-plugin");
assertFalse(classNames.isEmpty());
when(pluginWrapper.getPluginState()).thenReturn(PluginState.UNLOADED);
event = new PluginStateEvent(pluginManager, pluginWrapper, null);
finder.pluginStateChanged(event);
classNames = finder.findClassNames("fake-plugin");
assertTrue(classNames.isEmpty());
}
private PluginWrapper mockPluginWrapper() throws FileNotFoundException {
var pluginWrapper = mock(PluginWrapper.class);
when(pluginWrapper.getPluginId()).thenReturn("fake-plugin");
var pluginRootUrl = ResourceUtils.getURL("classpath:plugin/plugin-for-finder/");
var classLoader = new URLClassLoader(new URL[] {pluginRootUrl});
when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);
return pluginWrapper;
}
}

View File

@ -0,0 +1,2 @@
# Generated by Halo
run.halo.fake.FakePlugin

View File

@ -1,2 +0,0 @@
# Generated by Halo
run.halo.links.LinkPlugin