diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java index ea3d825de..924110a46 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java @@ -1,17 +1,30 @@ package run.halo.app.plugin; +import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; + +import java.io.IOException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.pf4j.PluginWrapper; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.core.env.PropertySource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.lang.NonNull; import org.springframework.util.Assert; import org.springframework.util.StopWatch; +import reactor.core.Exceptions; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.properties.HaloProperties; /** * Plugin application initializer will create plugin application context by plugin id and @@ -28,6 +41,8 @@ public class PluginApplicationInitializer { private final SharedApplicationContextHolder sharedApplicationContextHolder; private final ApplicationContext rootApplicationContext; + private final HaloProperties haloProperties; + public PluginApplicationInitializer(HaloPluginManager haloPluginManager, ApplicationContext rootApplicationContext) { Assert.notNull(haloPluginManager, "The haloPluginManager must not be null"); @@ -36,6 +51,7 @@ public class PluginApplicationInitializer { this.rootApplicationContext = rootApplicationContext; sharedApplicationContextHolder = rootApplicationContext .getBean(SharedApplicationContextHolder.class); + haloProperties = rootApplicationContext.getBean(HaloProperties.class); } private PluginApplicationContext createPluginApplicationContext(String pluginId) { @@ -45,12 +61,12 @@ public class PluginApplicationInitializer { StopWatch stopWatch = new StopWatch("initialize-plugin-context"); stopWatch.start("Create PluginApplicationContext"); PluginApplicationContext pluginApplicationContext = new PluginApplicationContext(); + pluginApplicationContext.setClassLoader(pluginClassLoader); if (sharedApplicationContextHolder != null) { pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance()); } - pluginApplicationContext.setClassLoader(pluginClassLoader); // populate plugin to plugin application context pluginApplicationContext.setPluginId(pluginId); stopWatch.stop(); @@ -58,6 +74,11 @@ public class PluginApplicationInitializer { stopWatch.start("Create DefaultResourceLoader"); DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader); pluginApplicationContext.setResourceLoader(defaultResourceLoader); + + var mutablePropertySources = pluginApplicationContext.getEnvironment().getPropertySources(); + resolvePropertySources(pluginId, pluginApplicationContext) + .forEach(mutablePropertySources::addLast); + stopWatch.stop(); DefaultListableBeanFactory beanFactory = @@ -169,4 +190,55 @@ public class PluginApplicationInitializer { stopWatch.prettyPrint()); return candidateComponents; } + + private List> resolvePropertySources(String pluginId, + ResourceLoader resourceLoader) { + var propertySourceLoader = new YamlPropertySourceLoader(); + var propertySources = new ArrayList>(); + var configsPath = haloProperties.getWorkDir().resolve("plugins").resolve("configs"); + + // resolve user defined config + Stream.of( + configsPath.resolve(pluginId + ".yaml"), + configsPath.resolve(pluginId + ".yml") + ) + .map(path -> resourceLoader.getResource(path.toUri().toString())) + .forEach(resource -> { + var sources = + loadPropertySources("user-defined-config", resource, propertySourceLoader); + propertySources.addAll(sources); + }); + + // resolve default config + Stream.of( + CLASSPATH_URL_PREFIX + "/config.yaml", + CLASSPATH_URL_PREFIX + "/config.yaml" + ) + .map(resourceLoader::getResource) + .forEach(resource -> { + var sources = loadPropertySources("default-config", resource, propertySourceLoader); + propertySources.addAll(sources); + }); + return propertySources; + } + + private List> loadPropertySources(String propertySourceName, + Resource resource, + PropertySourceLoader propertySourceLoader) { + logConfigLocation(resource); + if (resource.exists()) { + try { + return propertySourceLoader.load(propertySourceName, resource); + } catch (IOException e) { + throw Exceptions.propagate(e); + } + } + return List.of(); + } + + private void logConfigLocation(Resource resource) { + if (log.isDebugEnabled()) { + log.debug("Loading property sources from {}", resource); + } + } } diff --git a/docs/developer-guide/plugin-configuration-properties.md b/docs/developer-guide/plugin-configuration-properties.md new file mode 100644 index 000000000..cf685546b --- /dev/null +++ b/docs/developer-guide/plugin-configuration-properties.md @@ -0,0 +1,78 @@ +# 插件外部配置 + +插件外部配置功能允许用户在特定目录添加插件相关的配置,插件启动的时候能够自动读取到该配置。 + +## 配置优先级 + +> 优先级从上到下由高到低。 + +1. `${halo.work-dir}/plugins/configs/${plugin-id}.{yaml|yml}` +2. `classpath:/config.{yaml|yml}` + +插件开发者可在 `Class Path` 下 添加 `config.{yaml|yml}` 作为默认配置。当 `.yaml` 和 `.yml` 同时出现时,以 `.yml` 的配置将会被忽略。 + +## 插件中定义配置并使用 + +- `src/main/java/my/plugin/MyPluginProperties.java` + + ```java + @Data + @ConfigurationProperties + public class MyPluginProperties { + + private String encryptKey; + + private String certPath; + } + ``` + +- `src/main/java/my/plugin/MyPluginConfiguration.java` + + ```java + @EnableConfigurationProperties(MyPluginProperties.class) + @Configuration + public class MyPluginConfiguration { + + } + ``` + +- `src/main/java/my/plugin/MyPlugin.java` + + ```java + @Component + @Slf4j + public class MyPlugin extends BasePlugin { + + private final MyPluginProperties storeProperties; + + public MyPlugin(PluginWrapper wrapper, MyPluginProperties storeProperties) { + super(wrapper); + this.storeProperties = storeProperties; + } + + @Override + public void start() { + log.info("My plugin properties: {}", storeProperties); + } + } + ``` + +- `src/main/resources/config.yaml` + + ```yaml + encryptKey: encrytkey== + certPath: /path/to/cert + ``` + +## 插件使用者配置 + +- `${halo.work-dir}/plugins/configs/${plugin-id}.{yaml|yml}` + + ```yaml + encryptKey: override encrytkey== + certPath: /another/path/to/cert + ``` + +## 可能存在的问题 + +- 增加未来实现"集群"架构的难度。