Support configuration properties mechanism for plugin in Halo core (#4043)

#### What type of PR is this?

/kind feature
/area core
/area plugin

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

This PR adds property sources into PluginApplicationContext environment to support configuration properties mechanism.

See https://github.com/halo-dev/halo/issues/4015 for more.

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

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

#### Special notes for your reviewer:

You can verify the mechanism in [plugin-starter](https://github.com/halo-dev/plugin-starter) according to documentation `docs/developer-guide/plugin-configuration-properties.md`.

I've only tested it on macOS, looking forward to feedback on Windows.

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

```release-note
支持在插件中定义 @ConfigurationProperties 注解
```
pull/4070/head
John Niang 2023-06-07 17:55:23 +08:00 committed by GitHub
parent a56d4f2a92
commit 31740e732f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 151 additions and 1 deletions

View File

@ -1,17 +1,30 @@
package run.halo.app.plugin; 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.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginWrapper; import org.pf4j.PluginWrapper;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; 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.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.DefaultResourceLoader; 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.lang.NonNull;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
import reactor.core.Exceptions;
import run.halo.app.extension.ReactiveExtensionClient; 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 * 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 SharedApplicationContextHolder sharedApplicationContextHolder;
private final ApplicationContext rootApplicationContext; private final ApplicationContext rootApplicationContext;
private final HaloProperties haloProperties;
public PluginApplicationInitializer(HaloPluginManager haloPluginManager, public PluginApplicationInitializer(HaloPluginManager haloPluginManager,
ApplicationContext rootApplicationContext) { ApplicationContext rootApplicationContext) {
Assert.notNull(haloPluginManager, "The haloPluginManager must not be null"); Assert.notNull(haloPluginManager, "The haloPluginManager must not be null");
@ -36,6 +51,7 @@ public class PluginApplicationInitializer {
this.rootApplicationContext = rootApplicationContext; this.rootApplicationContext = rootApplicationContext;
sharedApplicationContextHolder = rootApplicationContext sharedApplicationContextHolder = rootApplicationContext
.getBean(SharedApplicationContextHolder.class); .getBean(SharedApplicationContextHolder.class);
haloProperties = rootApplicationContext.getBean(HaloProperties.class);
} }
private PluginApplicationContext createPluginApplicationContext(String pluginId) { private PluginApplicationContext createPluginApplicationContext(String pluginId) {
@ -45,12 +61,12 @@ public class PluginApplicationInitializer {
StopWatch stopWatch = new StopWatch("initialize-plugin-context"); StopWatch stopWatch = new StopWatch("initialize-plugin-context");
stopWatch.start("Create PluginApplicationContext"); stopWatch.start("Create PluginApplicationContext");
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext(); PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
pluginApplicationContext.setClassLoader(pluginClassLoader);
if (sharedApplicationContextHolder != null) { if (sharedApplicationContextHolder != null) {
pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance()); pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance());
} }
pluginApplicationContext.setClassLoader(pluginClassLoader);
// populate plugin to plugin application context // populate plugin to plugin application context
pluginApplicationContext.setPluginId(pluginId); pluginApplicationContext.setPluginId(pluginId);
stopWatch.stop(); stopWatch.stop();
@ -58,6 +74,11 @@ public class PluginApplicationInitializer {
stopWatch.start("Create DefaultResourceLoader"); stopWatch.start("Create DefaultResourceLoader");
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader); DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader);
pluginApplicationContext.setResourceLoader(defaultResourceLoader); pluginApplicationContext.setResourceLoader(defaultResourceLoader);
var mutablePropertySources = pluginApplicationContext.getEnvironment().getPropertySources();
resolvePropertySources(pluginId, pluginApplicationContext)
.forEach(mutablePropertySources::addLast);
stopWatch.stop(); stopWatch.stop();
DefaultListableBeanFactory beanFactory = DefaultListableBeanFactory beanFactory =
@ -169,4 +190,55 @@ public class PluginApplicationInitializer {
stopWatch.prettyPrint()); stopWatch.prettyPrint());
return candidateComponents; return candidateComponents;
} }
private List<PropertySource<?>> resolvePropertySources(String pluginId,
ResourceLoader resourceLoader) {
var propertySourceLoader = new YamlPropertySourceLoader();
var propertySources = new ArrayList<PropertySource<?>>();
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<PropertySource<?>> 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);
}
}
} }

View File

@ -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
```
## 可能存在的问题
- 增加未来实现"集群"架构的难度。