This is an automated email from the ASF dual-hosted git repository.
tzssangglass pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/apisix-java-plugin-runner.git
The following commit(s) were added to refs/heads/main by this push:
new 4194525 feat: support hot reload of plugin filters (#158)
4194525 is described below
commit 4194525ae68e7015d39f20914846e59f5f38187f
Author: Eric Liu <[email protected]>
AuthorDate: Tue Jul 19 03:20:13 2022 -0700
feat: support hot reload of plugin filters (#158)
---
.../apisix/plugin/runner/DynamicClassLoader.java | 87 +++++++++++++
.../apisix/plugin/runner/HotReloadProcess.java | 145 +++++++++++++++++++++
.../plugin/runner/PluginRunnerApplication.java | 14 +-
runner-starter/src/main/resources/application.yaml | 9 +-
4 files changed, 252 insertions(+), 3 deletions(-)
diff --git
a/runner-starter/src/main/java/org/apache/apisix/plugin/runner/DynamicClassLoader.java
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/DynamicClassLoader.java
new file mode 100644
index 0000000..308035c
--- /dev/null
+++
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/DynamicClassLoader.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.apisix.plugin.runner;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class DynamicClassLoader extends ClassLoader {
+ private final Logger logger =
LoggerFactory.getLogger(DynamicClassLoader.class);
+
+ private String name;
+ private String classDir;
+ private String packageName;
+
+ public DynamicClassLoader(ClassLoader parent) {
+ super(parent);
+ }
+
+ @Override
+ public Class<?> findClass(String name) throws ClassNotFoundException {
+ if (this.name == null) {
+ return super.findClass(name);
+ }
+
+ // can we do replacements for windows only?
+ String packagePath = packageName.replaceAll("\\.", "/");
+ String classPath = "file:" + classDir + "/" + packagePath + "/" +
this.name + ".class";
+
+ URL url;
+ URLConnection connection;
+ try {
+ url = new URL(classPath);
+ connection = url.openConnection();
+ } catch (IOException e) {
+ logger.error("failed to open class file: {}", classPath, e);
+ throw new RuntimeException(e);
+ }
+ try (InputStream input = connection.getInputStream();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
+ int data = input.read();
+ while (data != -1) {
+ buffer.write(data);
+ data = input.read();
+ }
+ input.close();
+ byte[] classData = buffer.toByteArray();
+ String fullyQualifiedName = packageName + "." + name;
+ return defineClass(fullyQualifiedName, classData, 0,
classData.length);
+ } catch (IOException e) {
+ logger.error("failed to read class file: {}", classPath, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void setClassDir(String classDir) {
+ this.classDir = classDir;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setPackageName(String name) {
+ packageName = name;
+ }
+}
diff --git
a/runner-starter/src/main/java/org/apache/apisix/plugin/runner/HotReloadProcess.java
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/HotReloadProcess.java
new file mode 100644
index 0000000..0e60349
--- /dev/null
+++
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/HotReloadProcess.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.apisix.plugin.runner;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.tools.JavaCompiler;
+import javax.tools.ToolProvider;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+@Component
+public class HotReloadProcess implements ApplicationContextAware {
+ private final Logger logger =
LoggerFactory.getLogger(HotReloadProcess.class);
+ private ApplicationContext ctx;
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
+ this.ctx = applicationContext;
+ }
+
+
@Value("${apisix.runner.dynamic-filter.load-path:/runner-plugin/src/main/java/org/apache/apisix/plugin/runner/filter/}")
+ private String loadPath;
+
+
@Value("${apisix.runner.dynamic-filter.package-name:org.apache.apisix.plugin.runner.filter}")
+ private String packageName;
+
+ private BeanDefinitionBuilder compile(String userDir, String filterName,
String filePath) throws ClassNotFoundException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+ String classDir = userDir + "/target/classes";
+ File file = new File(userDir);
+ if (!file.exists() && !file.isDirectory()) {
+ boolean flag = file.mkdirs();
+ if (!flag) {
+ logger.error("mkdirs:{} error", file.getAbsolutePath());
+ }
+ }
+
+ String[] args = {"-d", classDir, filePath};
+ compiler.run(null, null, null, args);
+
+ ClassLoader parentClassLoader =
DynamicClassLoader.class.getClassLoader();
+ DynamicClassLoader classLoader = new
DynamicClassLoader(parentClassLoader);
+ classLoader.setClassDir(classDir);
+ classLoader.setName(filterName);
+ classLoader.setPackageName(packageName);
+ Class<?> myObjectClass = classLoader.loadClass(filterName);
+ return
BeanDefinitionBuilder.genericBeanDefinition(myObjectClass).setLazyInit(true);
+ }
+
+ @Scheduled(fixedRate = 1000, initialDelay = 1000)
+ private void watch() {
+ final BeanDefinitionRegistry registry = (BeanDefinitionRegistry)
ctx.getAutowireCapableBeanFactory();
+ long now = System.currentTimeMillis() / 1000;
+ logger.warn("Fixed rate task with one second initial delay - {}", now);
+ String userDir = System.getProperty("user.dir");
+ String workDir = userDir + loadPath;
+
+ try (WatchService watchService =
FileSystems.getDefault().newWatchService()) {
+ Path path = Paths.get(workDir);
+ path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY,
ENTRY_DELETE);
+
+ while (true) {
+ final WatchKey key = watchService.take();
+ for (WatchEvent<?> watchEvent : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = watchEvent.kind();
+ final String filterFile = watchEvent.context().toString();
+
+ // ignore the file that is not java file
+ if (!filterFile.endsWith(".java")) {
+ continue;
+ }
+
+ String filterName = filterFile.substring(0,
filterFile.length() - 5);
+ String filterBean =
Character.toLowerCase(filterFile.charAt(0)) + filterName.substring(1);
+ final String filePath = workDir + filterFile;
+
+ if (kind == ENTRY_CREATE) {
+ logger.info("file create: {}", filePath);
+ BeanDefinitionBuilder builder = compile(userDir,
filterName, filePath);
+ registry.registerBeanDefinition(filterBean,
builder.getBeanDefinition());
+ } else if (kind == ENTRY_MODIFY) {
+ logger.info("file modify: {}", filePath);
+ registry.removeBeanDefinition(filterBean);
+ BeanDefinitionBuilder builder = compile(userDir,
filterName, filePath);
+ registry.registerBeanDefinition(filterBean,
builder.getBeanDefinition());
+ } else if (kind == ENTRY_DELETE) {
+ if (registry.containsBeanDefinition(filterBean)) {
+ logger.info("file delete: {}, and remove filter:
{} ", filePath, filterBean);
+ registry.removeBeanDefinition(filterBean);
+ /*TODO: we need to remove the filter from the
filter chain
+ * by remove the conf token in cache or other way
+ * */
+ }
+ } else {
+ logger.warn("unknown event: {}", kind);
+ }
+ }
+
+ boolean valid = key.reset();
+ if (!valid) {
+ logger.warn("key is invalid");
+ }
+ }
+ } catch (IOException | InterruptedException | ClassNotFoundException
e) {
+ logger.error("watch error", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git
a/runner-starter/src/main/java/org/apache/apisix/plugin/runner/PluginRunnerApplication.java
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/PluginRunnerApplication.java
index 560bc41..109f699 100644
---
a/runner-starter/src/main/java/org/apache/apisix/plugin/runner/PluginRunnerApplication.java
+++
b/runner-starter/src/main/java/org/apache/apisix/plugin/runner/PluginRunnerApplication.java
@@ -20,14 +20,24 @@ package org.apache.apisix.plugin.runner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class PluginRunnerApplication {
-
+ private static ClassLoader PARENT_CLASS_LOADER;
+ private static DynamicClassLoader CLASS_LOADER;
+
public static void main(String[] args) {
+
+ //load specified classes using dynamic class loader
+ PARENT_CLASS_LOADER = DynamicClassLoader.class.getClassLoader();
+ CLASS_LOADER = new DynamicClassLoader(PARENT_CLASS_LOADER);
+ Thread.currentThread().setContextClassLoader(CLASS_LOADER);
new SpringApplicationBuilder(PluginRunnerApplication.class)
.web(WebApplicationType.NONE)
.run(args);
}
-
}
+
+
diff --git a/runner-starter/src/main/resources/application.yaml
b/runner-starter/src/main/resources/application.yaml
index 0c6c50c..17c64a4 100644
--- a/runner-starter/src/main/resources/application.yaml
+++ b/runner-starter/src/main/resources/application.yaml
@@ -20,4 +20,11 @@ cache.config:
capacity: 1000
socket:
- file: ${APISIX_LISTEN_ADDRESS}
\ No newline at end of file
+ file: ${APISIX_LISTEN_ADDRESS}
+
+
+#apisix:
+# runner:
+# dynamic-filter:
+# load-path:
+# package-name:
\ No newline at end of file