feat: 添加扩展接口, 通过 SPI 机制允许外部模块为脚本添加功能组件.

通过扩展接口, 可让第三方为脚本提供功能组件, 而无需局限于 Sentry 内置功能组件.

Closes #6
This commit is contained in:
LamGC 2021-10-10 11:12:49 +08:00
parent 7a008848db
commit e6ff28e077
Signed by: LamGC
GPG Key ID: 6C5AE2A913941E1D
6 changed files with 107 additions and 34 deletions

View File

@ -4,11 +4,9 @@ import com.google.common.base.Throwables;
import net.lamgc.oracle.sentry.oci.account.OracleAccount;
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.ScriptComponentExtension;
import net.lamgc.oracle.sentry.script.ScriptComponents;
import net.lamgc.oracle.sentry.script.ScriptLoggerFactory;
import net.lamgc.oracle.sentry.script.ScriptManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -24,6 +22,7 @@ import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ServiceLoader;
/**
* @author LamGC
@ -96,18 +95,29 @@ class ApplicationInitiation {
@Bean("sentry.script.manager")
@Autowired
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager, OracleAccountManager accountManager) {
ScriptComponents context = new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create()
.build()),
instanceManager,
new ScriptLoggerFactory(),
accountManager
);
ScriptComponents components = new ScriptComponents();
components.addComponentObject("InstanceManager", instanceManager);
components.addComponentObject("AccountManager", accountManager);
ScriptManager manager = new ScriptManager(new File(scriptsLocation), context);
configureScriptComponentsFromExtension(components);
ScriptManager manager = new ScriptManager(new File(scriptsLocation), components);
manager.loadScripts();
return manager;
}
private void configureScriptComponentsFromExtension(ScriptComponents components) {
ServiceLoader<ScriptComponentExtension> extensions = ServiceLoader.load(ScriptComponentExtension.class);
for (ScriptComponentExtension extension : extensions) {
try {
extension.configureScriptComponents(components);
} catch (Exception e) {
log.error("脚本组件扩展配置组件时发生未捕获异常.(Extension: {})\n{}",
extension.getClass().getName(), Throwables.getStackTraceAsString(e));
}
}
}
@PostConstruct
@Order(1)
@SuppressWarnings({"MismatchedReadAndWriteOfArray", "RedundantOperationOnEmptyContainer"})

View File

@ -0,0 +1,13 @@
package net.lamgc.oracle.sentry.script;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
public class BuiltinComponentExtension implements ScriptComponentExtension{
@Override
public void configureScriptComponents(ScriptComponents components) {
components.addComponentObject("HTTP", new ScriptHttpClient(HttpClientBuilder.create()
.build()));
components.addComponentFactory(new ScriptLoggerFactory());
}
}

View File

@ -0,0 +1,18 @@
package net.lamgc.oracle.sentry.script;
/**
* 脚本组件扩展.
* <p> 通过实现该接口, 可在脚本加载前设置组件对象, 为脚本提供更多功能.
* <p> 实现接口后, 需按 SPI 方式添加实现.
* @author LamGC
*/
public interface ScriptComponentExtension {
/**
* 配置脚本组件.
* <p> 在方法中为组件集合添加组件.
* @param components 脚本组件集合.
*/
void configureScriptComponents(final ScriptComponents components);
}

View File

@ -1,8 +1,8 @@
package net.lamgc.oracle.sentry.script;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import com.google.common.base.Strings;
import java.util.*;
/**
* 脚本组件集合.
@ -10,11 +10,49 @@ import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
* <p> 后续可能会改成用 {@link javax.script.Bindings} 之类的.
* @author LamGC
*/
public final record ScriptComponents(
ScriptHttpClient HTTP,
ComputeInstanceManager InstanceManager,
ScriptLoggerFactory loggerFactory,
OracleAccountManager AccountManager
) {
public final class ScriptComponents {
private final ScriptComponents parent;
private final Map<String, Object> componentObjects = new HashMap<>();
private final Set<ScriptComponentFactory<?>> factories = new HashSet<>();
public ScriptComponents() {
this.parent = null;
}
public ScriptComponents(ScriptComponents parent) {
this.parent = parent;
}
public void addComponentObject(String componentName, Object componentObject) {
if (Strings.isNullOrEmpty(componentName)) {
throw new NullPointerException("The component name is null or empty.");
} else if (componentObject == null) {
throw new NullPointerException("ComponentObject is null");
} else if (componentObjects.containsKey(componentName)) {
throw new IllegalArgumentException("The corresponding object already exists for the component name.");
}
componentObjects.put(componentName, componentObject);
}
public void addComponentFactory(ScriptComponentFactory<?> factory) {
Objects.requireNonNull(factory);
factories.add(factory);
}
public Map<String, Object> getComponentObjects() {
Map<String, Object> componentObjects = this.parent == null ?
new HashMap<>() : new HashMap<>(this.parent.getComponentObjects());
componentObjects.putAll(this.componentObjects);
return Collections.unmodifiableMap(componentObjects);
}
public Set<ScriptComponentFactory<?>> getScriptComponentFactories() {
Set<ScriptComponentFactory<?>> factories = this.parent == null ?
new HashSet<>() : new HashSet<>(this.parent.factories);
factories.addAll(this.factories);
return Collections.unmodifiableSet(factories);
}
}

View File

@ -17,7 +17,6 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
@ -96,19 +95,15 @@ public class GroovyScriptLoader implements ScriptLoader {
private static Binding createBinding(ScriptComponents components, ScriptInfo info) {
Binding binding = new Binding();
for (Field field : components.getClass().getDeclaredFields()) {
try {
String name = field.getName();
field.setAccessible(true);
Object o = field.get(components);
if (o instanceof ScriptComponentFactory factory) {
binding.setProperty(factory.getPropertyName(), factory.getInstance(info));
} else {
binding.setProperty(name, o);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
components.getComponentObjects().forEach(binding::setProperty);
for (ScriptComponentFactory<?> factory : components.getScriptComponentFactories()) {
String componentName = factory.getPropertyName();
if (binding.hasVariable(componentName)) {
log.warn("脚本组件名发生冲突: 工厂 {} 所给定的组件名已经存在静态组件, 将跳过创建.", factory.getClass());
continue;
}
Object componentObject = factory.getInstance(info);
binding.setProperty(componentName, componentObject);
}
return binding;
}

View File

@ -13,8 +13,7 @@ class ScriptManagerTest {
@Test
public void loadScriptTest() {
ScriptManager manager = new ScriptManager(new File("./run/scripts"),
new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create().build()),
new ComputeInstanceManager(), new ScriptLoggerFactory(), new OracleAccountManager()));
new ScriptComponents());
manager.loadScripts();