Skip to content

Commit

Permalink
Support configuration properties mechanism for plugin in Halo core (h…
Browse files Browse the repository at this point in the history
…alo-dev#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 halo-dev#4015 for more.

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

Fixes halo-dev#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 注解
```
  • Loading branch information
JohnNiang authored Jun 7, 2023
1 parent a56d4f2 commit 31740e7
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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");
Expand All @@ -36,6 +51,7 @@ public PluginApplicationInitializer(HaloPluginManager haloPluginManager,
this.rootApplicationContext = rootApplicationContext;
sharedApplicationContextHolder = rootApplicationContext
.getBean(SharedApplicationContextHolder.class);
haloProperties = rootApplicationContext.getBean(HaloProperties.class);
}

private PluginApplicationContext createPluginApplicationContext(String pluginId) {
Expand All @@ -45,19 +61,24 @@ private PluginApplicationContext createPluginApplicationContext(String pluginId)
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();

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 =
Expand Down Expand Up @@ -169,4 +190,55 @@ private Set<Class<?>> findCandidateComponents(String pluginId) {
stopWatch.prettyPrint());
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);
}
}
}
78 changes: 78 additions & 0 deletions docs/developer-guide/plugin-configuration-properties.md
Original file line number Diff line number Diff line change
@@ -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
```

## 可能存在的问题

- 增加未来实现"集群"架构的难度。

0 comments on commit 31740e7

Please sign in to comment.