mirror of https://github.com/halo-dev/halo
feat: support fixed plugin path for development mode (#2321)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 /area core <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 目前插件开发模式允许配置项目目录,但必须配置到项目目录的上一级,并不友好 此 PR 提供了 fixedPluginPath 选项允许在开发模式时配置它为插件项目目录 在开发 gradle 插件时更易于通过配置此选项来加载插件 #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes # #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2333/head
parent
9911ba927d
commit
9b3b49d028
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.pf4j.DevelopmentPluginRepository;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class DefaultDevelopmentPluginRepository extends DevelopmentPluginRepository {
|
||||||
|
private final List<Path> fixedPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
public DefaultDevelopmentPluginRepository(Path... pluginsRoots) {
|
||||||
|
super(pluginsRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultDevelopmentPluginRepository(List<Path> pluginsRoots) {
|
||||||
|
super(pluginsRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFixedPath(Path path) {
|
||||||
|
fixedPaths.add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFixedPaths(List<Path> paths) {
|
||||||
|
if (CollectionUtils.isEmpty(paths)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fixedPaths.clear();
|
||||||
|
fixedPaths.addAll(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Path> getPluginPaths() {
|
||||||
|
List<Path> paths = new ArrayList<>(fixedPaths);
|
||||||
|
paths.addAll(super.getPluginPaths());
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,12 +6,16 @@ import java.nio.file.Path;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.ClassLoadingStrategy;
|
import org.pf4j.ClassLoadingStrategy;
|
||||||
import org.pf4j.CompoundPluginLoader;
|
import org.pf4j.CompoundPluginLoader;
|
||||||
|
import org.pf4j.CompoundPluginRepository;
|
||||||
|
import org.pf4j.DefaultPluginRepository;
|
||||||
import org.pf4j.DevelopmentPluginLoader;
|
import org.pf4j.DevelopmentPluginLoader;
|
||||||
import org.pf4j.JarPluginLoader;
|
import org.pf4j.JarPluginLoader;
|
||||||
|
import org.pf4j.JarPluginRepository;
|
||||||
import org.pf4j.PluginClassLoader;
|
import org.pf4j.PluginClassLoader;
|
||||||
import org.pf4j.PluginDescriptor;
|
import org.pf4j.PluginDescriptor;
|
||||||
import org.pf4j.PluginLoader;
|
import org.pf4j.PluginLoader;
|
||||||
import org.pf4j.PluginManager;
|
import org.pf4j.PluginManager;
|
||||||
|
import org.pf4j.PluginRepository;
|
||||||
import org.pf4j.PluginStatusProvider;
|
import org.pf4j.PluginStatusProvider;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
@ -33,6 +37,7 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||||
public class PluginAutoConfiguration {
|
public class PluginAutoConfiguration {
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
@Qualifier("webFluxContentTypeResolver")
|
@Qualifier("webFluxContentTypeResolver")
|
||||||
private final RequestedContentTypeResolver requestedContentTypeResolver;
|
private final RequestedContentTypeResolver requestedContentTypeResolver;
|
||||||
|
|
||||||
|
@ -126,11 +131,23 @@ public class PluginAutoConfiguration {
|
||||||
}
|
}
|
||||||
return super.createPluginStatusProvider();
|
return super.createPluginStatusProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginRepository createPluginRepository() {
|
||||||
|
var developmentPluginRepository =
|
||||||
|
new DefaultDevelopmentPluginRepository(getPluginsRoots());
|
||||||
|
developmentPluginRepository
|
||||||
|
.setFixedPaths(pluginProperties.getFixedPluginPath());
|
||||||
|
return new CompoundPluginRepository()
|
||||||
|
.add(developmentPluginRepository, this::isDevelopment)
|
||||||
|
.add(new JarPluginRepository(getPluginsRoots()), this::isNotDevelopment)
|
||||||
|
.add(new DefaultPluginRepository(getPluginsRoots()),
|
||||||
|
this::isNotDevelopment);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
|
pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
|
||||||
pluginManager.setSystemVersion(pluginProperties.getSystemVersion());
|
pluginManager.setSystemVersion(pluginProperties.getSystemVersion());
|
||||||
|
|
||||||
return pluginManager;
|
return pluginManager;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.core.extension.Plugin;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PluginDevelopmentInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
||||||
|
|
||||||
|
private final HaloPluginManager haloPluginManager;
|
||||||
|
|
||||||
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
|
public PluginDevelopmentInitializer(HaloPluginManager haloPluginManager,
|
||||||
|
PluginProperties pluginProperties, ExtensionClient extensionClient) {
|
||||||
|
this.haloPluginManager = haloPluginManager;
|
||||||
|
this.pluginProperties = pluginProperties;
|
||||||
|
this.extensionClient = extensionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||||
|
if (!haloPluginManager.isDevelopment()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createFixedPluginIfNecessary(haloPluginManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFixedPluginIfNecessary(HaloPluginManager pluginManager) {
|
||||||
|
for (Path path : pluginProperties.getFixedPluginPath()) {
|
||||||
|
String pluginId = pluginManager.loadPlugin(path);
|
||||||
|
PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginId);
|
||||||
|
if (pluginWrapper == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Plugin plugin = new YamlPluginFinder().find(pluginWrapper.getPluginPath());
|
||||||
|
extensionClient.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||||
|
.ifPresentOrElse(persistent -> {
|
||||||
|
plugin.getMetadata().setVersion(persistent.getMetadata().getVersion());
|
||||||
|
extensionClient.update(plugin);
|
||||||
|
}, () -> extensionClient.create(plugin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
@ -22,6 +23,12 @@ public class PluginProperties {
|
||||||
*/
|
*/
|
||||||
private boolean autoStartPlugin = true;
|
private boolean autoStartPlugin = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default plugin path is obtained through file scanning.
|
||||||
|
* In the development mode, you can specify the plugin path as the project directory.
|
||||||
|
*/
|
||||||
|
private List<Path> fixedPluginPath = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugins disabled by default.
|
* Plugins disabled by default.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -35,6 +35,13 @@ public class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMap
|
||||||
private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =
|
private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =
|
||||||
new LinkedMultiValueMap<>();
|
new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initHandlerMethods() {
|
||||||
|
// Parent method will scan beans in the ApplicationContext
|
||||||
|
// detect and register handler methods.
|
||||||
|
// but this is superfluous for this class.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register handler methods according to the plugin id and the controller(annotated
|
* Register handler methods according to the plugin id and the controller(annotated
|
||||||
* {@link Controller}) bean.
|
* {@link Controller}) bean.
|
||||||
|
|
|
@ -6,11 +6,9 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
@ -19,17 +17,12 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A value fetcher for pPlugin form configuration.</p>
|
* <p>A value fetcher for pPlugin form configuration.</p>
|
||||||
* <p>The method of obtaining values is lazy,only when it is used for the first time can the real
|
|
||||||
* query value be obtained from {@link ConfigMap}.</p>
|
|
||||||
* <p>It is thread safe.</p>
|
|
||||||
*
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class SettingFetcher {
|
public class SettingFetcher {
|
||||||
|
|
||||||
private final AtomicReference<Map<String, JsonNode>> valueRef = new AtomicReference<>(null);
|
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
private final String pluginName;
|
private final String pluginName;
|
||||||
|
@ -40,13 +33,13 @@ public class SettingFetcher {
|
||||||
this.pluginName = pluginName;
|
this.pluginName = pluginName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
public <T> T getGroupForObject(String group, Class<T> clazz) {
|
public <T> Optional<T> fetch(String group, Class<T> clazz) {
|
||||||
return convertValue(getInternal(group), clazz);
|
return Optional.ofNullable(convertValue(getInternal(group), clazz));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public JsonNode getGroup(String group) {
|
public JsonNode get(String group) {
|
||||||
return getInternal(group);
|
return getInternal(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,13 +50,11 @@ public class SettingFetcher {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Map<String, JsonNode> getValues() {
|
public Map<String, JsonNode> getValues() {
|
||||||
Map<String, JsonNode> values =
|
return Map.copyOf(getValuesInternal());
|
||||||
valueRef.updateAndGet(m -> m != null ? m : getValuesInternal());
|
|
||||||
return Map.copyOf(values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode getInternal(String group) {
|
private JsonNode getInternal(String group) {
|
||||||
return Optional.ofNullable(getValues().get(group))
|
return Optional.ofNullable(getValuesInternal().get(group))
|
||||||
.orElse(JsonNodeFactory.instance.missingNode());
|
.orElse(JsonNodeFactory.instance.missingNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,29 +64,28 @@ class SettingFetcherTest {
|
||||||
// The extensionClient will only be called once
|
// The extensionClient will only be called once
|
||||||
Map<String, JsonNode> callAgain = settingFetcher.getValues();
|
Map<String, JsonNode> callAgain = settingFetcher.getValues();
|
||||||
assertThat(callAgain).isNotNull();
|
assertThat(callAgain).isNotNull();
|
||||||
verify(extensionClient, times(1)).fetch(eq(ConfigMap.class), any());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getGroupForObject() throws JSONException {
|
void getGroupForObject() throws JSONException {
|
||||||
Sns sns = settingFetcher.getGroupForObject("sns", Sns.class);
|
Optional<Sns> sns = settingFetcher.fetch("sns", Sns.class);
|
||||||
assertThat(sns).isNotNull();
|
assertThat(sns.isEmpty()).isFalse();
|
||||||
JSONAssert.assertEquals(getSns(), JsonUtils.objectToJson(sns), true);
|
JSONAssert.assertEquals(getSns(), JsonUtils.objectToJson(sns.get()), true);
|
||||||
|
|
||||||
Sns missing = settingFetcher.getGroupForObject("sns1", Sns.class);
|
Optional<Sns> missing = settingFetcher.fetch("sns1", Sns.class);
|
||||||
assertThat(missing).isNull();
|
assertThat(missing.isEmpty()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getGroup() {
|
void getGroup() {
|
||||||
JsonNode jsonNode = settingFetcher.getGroup("basic");
|
JsonNode jsonNode = settingFetcher.get("basic");
|
||||||
assertThat(jsonNode).isNotNull();
|
assertThat(jsonNode).isNotNull();
|
||||||
assertThat(jsonNode.isObject()).isTrue();
|
assertThat(jsonNode.isObject()).isTrue();
|
||||||
assertThat(jsonNode.get("color").asText()).isEqualTo("red");
|
assertThat(jsonNode.get("color").asText()).isEqualTo("red");
|
||||||
assertThat(jsonNode.get("width").asInt()).isEqualTo(100);
|
assertThat(jsonNode.get("width").asInt()).isEqualTo(100);
|
||||||
|
|
||||||
// missing key will return empty json node
|
// missing key will return empty json node
|
||||||
JsonNode emptyNode = settingFetcher.getGroup("basic1");
|
JsonNode emptyNode = settingFetcher.get("basic1");
|
||||||
assertThat(emptyNode.isEmpty()).isTrue();
|
assertThat(emptyNode.isEmpty()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue