Support obtaining plugins root in plugins (#6269)

#### What type of PR is this?

/kind feature
/kind api-change
/area core
/area plugin

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

This PR supports obtaining plugins root in plugins. Below is an example in plugin:

```java
@Component
class PluginsRootGetterDemo {

    private final PluginsRootGetter pluginsRootGetter;

    PluginsRootGetterDemo(PluginsRootGetter pluginsRootGetter) {
        this.pluginsRootGetter = pluginsRootGetter;
    }

}
```

Meanwhile, I remove the `PluginProperties#pluginsRoot` for a clear way to obtain plugins root.

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

```release-note
支持在插件中获取插件根目录
```
pull/6271/head
John Niang 2024-07-04 21:36:33 +08:00 committed by GitHub
parent 36fb44c8b7
commit ad66247872
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 101 additions and 38 deletions

View File

@ -0,0 +1,14 @@
package run.halo.app.plugin;
import java.nio.file.Path;
import java.util.function.Supplier;
/**
* An interface to get the root path of plugins.
*
* @author johnniang
* @since 2.18.0
*/
public interface PluginsRootGetter extends Supplier<Path> {
}

View File

@ -13,7 +13,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
@ -61,8 +60,8 @@ import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.FileUtils;
import run.halo.app.infra.utils.VersionUtils; import run.halo.app.infra.utils.VersionUtils;
import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.PluginUtils; import run.halo.app.plugin.PluginUtils;
import run.halo.app.plugin.PluginsRootGetter;
import run.halo.app.plugin.SpringPluginManager; import run.halo.app.plugin.SpringPluginManager;
import run.halo.app.plugin.YamlPluginDescriptorFinder; import run.halo.app.plugin.YamlPluginDescriptorFinder;
import run.halo.app.plugin.YamlPluginFinder; import run.halo.app.plugin.YamlPluginFinder;
@ -79,7 +78,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
private final SystemVersionSupplier systemVersion; private final SystemVersionSupplier systemVersion;
private final PluginProperties pluginProperties; private final PluginsRootGetter pluginsRootGetter;
private final SpringPluginManager pluginManager; private final SpringPluginManager pluginManager;
@ -93,11 +92,13 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
private Clock clock = Clock.systemUTC(); private Clock clock = Clock.systemUTC();
public PluginServiceImpl(ReactiveExtensionClient client, SystemVersionSupplier systemVersion, public PluginServiceImpl(ReactiveExtensionClient client,
PluginProperties pluginProperties, SpringPluginManager pluginManager) { SystemVersionSupplier systemVersion,
PluginsRootGetter pluginsRootGetter,
SpringPluginManager pluginManager) {
this.client = client; this.client = client;
this.systemVersion = systemVersion; this.systemVersion = systemVersion;
this.pluginProperties = pluginProperties; this.pluginsRootGetter = pluginsRootGetter;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.jsBundleCache = new BundleCache(".js"); this.jsBundleCache = new BundleCache(".js");
@ -424,7 +425,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
return Mono.fromCallable( return Mono.fromCallable(
() -> { () -> {
var fileName = PluginUtils.generateFileName(plugin); var fileName = PluginUtils.generateFileName(plugin);
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot()); var pluginRoot = pluginsRootGetter.get();
try { try {
Files.createDirectories(pluginRoot); Files.createDirectories(pluginRoot);
} catch (IOException e) { } catch (IOException e) {

View File

@ -1,12 +1,10 @@
package run.halo.app.plugin; package run.halo.app.plugin;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Stack; import java.util.Stack;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.CompoundPluginLoader; import org.pf4j.CompoundPluginLoader;
import org.pf4j.CompoundPluginRepository; import org.pf4j.CompoundPluginRepository;
import org.pf4j.DefaultPluginManager; import org.pf4j.DefaultPluginManager;
@ -46,13 +44,16 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
private final PluginProperties pluginProperties; private final PluginProperties pluginProperties;
private final PluginsRootGetter pluginsRootGetter;
public HaloPluginManager(ApplicationContext rootContext, public HaloPluginManager(ApplicationContext rootContext,
PluginProperties pluginProperties, PluginProperties pluginProperties,
SystemVersionSupplier systemVersionSupplier) { SystemVersionSupplier systemVersionSupplier, PluginsRootGetter pluginsRootGetter) {
this.pluginProperties = pluginProperties; this.pluginProperties = pluginProperties;
this.rootContext = rootContext; this.rootContext = rootContext;
// We have to initialize share context lazily because the root context has not refreshed // We have to initialize share context lazily because the root context has not refreshed
this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext)); this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext));
this.pluginsRootGetter = pluginsRootGetter;
super.runtimeMode = pluginProperties.getRuntimeMode(); super.runtimeMode = pluginProperties.getRuntimeMode();
setExactVersionAllowed(pluginProperties.isExactVersionAllowed()); setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
@ -124,11 +125,7 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
@Override @Override
protected List<Path> createPluginsRoot() { protected List<Path> createPluginsRoot() {
var pluginsRoot = pluginProperties.getPluginsRoot(); return List.of(pluginsRootGetter.get());
if (StringUtils.isNotBlank(pluginsRoot)) {
return List.of(Paths.get(pluginsRoot));
}
return super.createPluginsRoot();
} }
@Override @Override

View File

@ -45,8 +45,11 @@ public class PluginAutoConfiguration {
@Bean @Bean
public SpringPluginManager pluginManager(ApplicationContext context, public SpringPluginManager pluginManager(ApplicationContext context,
SystemVersionSupplier systemVersionSupplier, SystemVersionSupplier systemVersionSupplier,
PluginProperties pluginProperties) { PluginProperties pluginProperties,
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier); PluginsRootGetter pluginsRootGetter) {
return new HaloPluginManager(
context, pluginProperties, systemVersionSupplier, pluginsRootGetter
);
} }
@Bean @Bean

View File

@ -60,10 +60,4 @@ public class PluginProperties {
*/ */
private RuntimeMode runtimeMode = RuntimeMode.DEPLOYMENT; private RuntimeMode runtimeMode = RuntimeMode.DEPLOYMENT;
/**
* Plugin root directory: default plugins; when non-jar mode plugin, the value should be an
* absolute directory address.
*/
private String pluginsRoot;
} }

View File

@ -0,0 +1,28 @@
package run.halo.app.plugin;
import java.nio.file.Path;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.infra.properties.HaloProperties;
/**
* Default implementation of {@link PluginsRootGetter}.
*
* @author johnniang
*/
@Component
public class PluginsRootGetterImpl implements PluginsRootGetter {
private final HaloProperties haloProperties;
public PluginsRootGetterImpl(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
}
@Override
@NonNull
public Path get() {
return haloProperties.getWorkDir().resolve("plugins");
}
}

View File

@ -62,6 +62,10 @@ public enum SharedApplicationContextFactory {
rootContext.getBean(CacheManager.class)); rootContext.getBean(CacheManager.class));
beanFactory.registerSingleton("loginHandlerEnhancer", beanFactory.registerSingleton("loginHandlerEnhancer",
rootContext.getBean(LoginHandlerEnhancer.class)); rootContext.getBean(LoginHandlerEnhancer.class));
rootContext.getBeanProvider(PluginsRootGetter.class)
.ifUnique(pluginsRootGetter ->
beanFactory.registerSingleton("pluginsRootGetter", pluginsRootGetter)
);
// TODO add more shared instance here // TODO add more shared instance here
sharedContext.refresh(); sharedContext.refresh();

View File

@ -34,8 +34,6 @@ spring:
halo: halo:
work-dir: ${user.home}/.halo2 work-dir: ${user.home}/.halo2
plugin:
plugins-root: ${halo.work-dir}/plugins
attachment: attachment:
resource-mappings: resource-mappings:
- pathPattern: /upload/** - pathPattern: /upload/**

View File

@ -55,15 +55,11 @@ import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.FileUtils;
import run.halo.app.plugin.PluginProperties;
@Slf4j @Slf4j
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PluginEndpointTest { class PluginEndpointTest {
@Mock
PluginProperties pluginProperties;
@Mock @Mock
private ReactiveExtensionClient client; private ReactiveExtensionClient client;
@ -221,8 +217,6 @@ class PluginEndpointTest {
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0")); lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
tempDirectory = Files.createTempDirectory("halo-test-plugin-upgrade-"); tempDirectory = Files.createTempDirectory("halo-test-plugin-upgrade-");
lenient().when(pluginProperties.getPluginsRoot())
.thenReturn(tempDirectory.resolve("plugins").toString());
plugin002 = tempDirectory.resolve("plugin-0.0.2.jar"); plugin002 = tempDirectory.resolve("plugin-0.0.2.jar");
var plugin002Uri = requireNonNull( var plugin002Uri = requireNonNull(

View File

@ -58,7 +58,7 @@ import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.exception.PluginAlreadyExistsException; import run.halo.app.infra.exception.PluginAlreadyExistsException;
import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.FileUtils;
import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginProperties; import run.halo.app.plugin.PluginsRootGetter;
import run.halo.app.plugin.SpringPluginManager; import run.halo.app.plugin.SpringPluginManager;
import run.halo.app.plugin.YamlPluginFinder; import run.halo.app.plugin.YamlPluginFinder;
@ -72,7 +72,7 @@ class PluginServiceImplTest {
ReactiveExtensionClient client; ReactiveExtensionClient client;
@Mock @Mock
PluginProperties pluginProperties; PluginsRootGetter pluginsRootGetter;
@Mock @Mock
SpringPluginManager pluginManager; SpringPluginManager pluginManager;
@ -123,9 +123,6 @@ class PluginServiceImplTest {
getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI(); getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI();
FileUtils.jar(Paths.get(fakePluingUri), tempDirectory.resolve("plugin-0.0.2.jar")); FileUtils.jar(Paths.get(fakePluingUri), tempDirectory.resolve("plugin-0.0.2.jar"));
lenient().when(pluginProperties.getPluginsRoot())
.thenReturn(tempDirectory.resolve("plugins").toString());
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0")); lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
} }
@ -144,6 +141,7 @@ class PluginServiceImplTest {
@Test @Test
void installWhenPluginNotExist() { void installWhenPluginNotExist() {
when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve("plugins"));
when(client.fetch(Plugin.class, "fake-plugin")).thenReturn(Mono.empty()); when(client.fetch(Plugin.class, "fake-plugin")).thenReturn(Mono.empty());
var createdPlugin = mock(Plugin.class); var createdPlugin = mock(Plugin.class);
when(client.create(isA(Plugin.class))).thenReturn(Mono.just(createdPlugin)); when(client.create(isA(Plugin.class))).thenReturn(Mono.just(createdPlugin));
@ -154,7 +152,6 @@ class PluginServiceImplTest {
verify(client).fetch(Plugin.class, "fake-plugin"); verify(client).fetch(Plugin.class, "fake-plugin");
verify(systemVersionSupplier).get(); verify(systemVersionSupplier).get();
verify(pluginProperties).getPluginsRoot();
verify(client).create(isA(Plugin.class)); verify(client).create(isA(Plugin.class));
} }
@ -181,6 +178,8 @@ class PluginServiceImplTest {
@Test @Test
void upgradeNormally() { void upgradeNormally() {
when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve("plugins"));
var oldFakePlugin = createPlugin("fake-plugin", plugin -> { var oldFakePlugin = createPlugin("fake-plugin", plugin -> {
plugin.getSpec().setEnabled(true); plugin.getSpec().setEnabled(true);
plugin.getSpec().setVersion("0.0.1"); plugin.getSpec().setVersion("0.0.1");

View File

@ -22,7 +22,7 @@ class DefaultPluginApplicationContextFactoryTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
factory = new DefaultPluginApplicationContextFactory((SpringPluginManager) pluginManager); factory = new DefaultPluginApplicationContextFactory(pluginManager);
} }
@Test @Test
@ -41,6 +41,7 @@ class DefaultPluginApplicationContextFactoryTest {
assertInstanceOf(PluginApplicationContext.class, context); assertInstanceOf(PluginApplicationContext.class, context);
assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique()); assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique());
assertNotNull(context.getBeanProvider(PluginsRootGetter.class).getIfUnique());
// TODO Add more assertions here. // TODO Add more assertions here.
} }

View File

@ -0,0 +1,30 @@
package run.halo.app.plugin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.nio.file.Paths;
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 run.halo.app.infra.properties.HaloProperties;
@ExtendWith(MockitoExtension.class)
class PluginsRootGetterImplTest {
@Mock
HaloProperties haloProperties;
@InjectMocks
PluginsRootGetterImpl pluginsRootGetter;
@Test
void shouldGetterPluginsRootCorrectly() {
var haloWorkDir = Paths.get("halo-work-dir");
when(haloProperties.getWorkDir()).thenReturn(haloWorkDir);
assertEquals(haloWorkDir.resolve("plugins"), pluginsRootGetter.get());
}
}