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

Reply via email to