| /* |
| * 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.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; |
| import org.springframework.scheduling.config.ScheduledTask; |
| import org.springframework.scheduling.config.Task; |
| import org.springframework.scheduling.support.ScheduledMethodRunnable; |
| 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.Files; |
| 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 java.util.Objects; |
| import java.util.Set; |
| |
| 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; |
| |
| private final ScheduledAnnotationBeanPostProcessor postProcessor; |
| |
| public HotReloadProcess(ScheduledAnnotationBeanPostProcessor postProcessor) { |
| this.postProcessor = postProcessor; |
| } |
| |
| @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; |
| |
| @Value("${apisix.runner.dynamic-filter.enable:false}") |
| private Boolean enableHotReload; |
| |
| 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 hotReloadFilter() { |
| if (!enableHotReload) { |
| cancelHotReload("hotReloadFilter"); |
| return; |
| } |
| |
| final BeanDefinitionRegistry registry = (BeanDefinitionRegistry) ctx.getAutowireCapableBeanFactory(); |
| String userDir = System.getProperty("user.dir"); |
| userDir = userDir.substring(0, userDir.lastIndexOf("apisix-java-plugin-runner") + 25); |
| String workDir = userDir + loadPath; |
| |
| Path path = Paths.get(workDir); |
| boolean exists = Files.exists(path); |
| if (!exists) { |
| logger.warn("The filter workdir for hot reload {} not exists", workDir); |
| cancelHotReload("hotReloadFilter"); |
| return; |
| } |
| |
| try (WatchService watchService = FileSystems.getDefault().newWatchService()) { |
| path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); |
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
| try { |
| watchService.close(); |
| } catch (IOException e) { |
| logger.error(e.getMessage()); |
| } |
| })); |
| |
| 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); |
| } |
| } |
| |
| public void cancelHotReload(String taskName) { |
| Set<ScheduledTask> tasks = postProcessor.getScheduledTasks(); |
| tasks.forEach(task -> { |
| Task t = task.getTask(); |
| ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) t.getRunnable(); |
| if (Objects.equals(runnable.getMethod().getName(), taskName)) { |
| postProcessor.postProcessBeforeDestruction(runnable.getTarget(), taskName); |
| logger.warn("Cancel hot reload schedule task"); |
| } |
| }); |
| } |
| } |