diff --git a/debug-tools-attach/pom.xml b/debug-tools-attach/pom.xml
index 5c1dc62e..9f65ce53 100644
--- a/debug-tools-attach/pom.xml
+++ b/debug-tools-attach/pom.xml
@@ -48,6 +48,11 @@
debug-tools-hotswap-spring-plugin
${revision}
+
+ io.github.future0923
+ debug-tools-hotswap-forest-plugin
+ ${revision}
+
io.github.future0923
debug-tools-hotswap-mybatis-plugin
@@ -247,22 +252,22 @@
run
-
-
-
+
+
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
+
+
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/pom.xml b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/pom.xml
new file mode 100644
index 00000000..56b233a4
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+ io.github.future0923
+ debug-tools-hotswap-plugin
+ ${revision}
+
+
+ debug-tools-hotswap-forest-plugin
+
+
+
+ io.github.future0923
+ debug-tools-hotswap-core
+ ${revision}
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ com.dtflys.forest
+ forest-spring
+ 1.5.32
+ true
+
+
+ org.springframework
+ spring-beans
+ true
+
+
+ org.springframework
+ spring-context
+ true
+
+
+ org.springframework
+ spring-tx
+ true
+
+
+ io.github.future0923
+ debug-tools-hotswap-spring-plugin
+ 5.0.1
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/ForestPlugin.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/ForestPlugin.java
new file mode 100644
index 00000000..771cae26
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/ForestPlugin.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest;
+
+import io.github.future0923.debug.tools.base.logging.Logger;
+import io.github.future0923.debug.tools.base.utils.DebugToolsStringUtils;
+import io.github.future0923.debug.tools.hotswap.core.annotation.Init;
+import io.github.future0923.debug.tools.hotswap.core.annotation.OnClassLoadEvent;
+import io.github.future0923.debug.tools.hotswap.core.annotation.Plugin;
+import io.github.future0923.debug.tools.hotswap.core.command.Scheduler;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.patch.ForestPatcher;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.watcher.ForestWatchEventListener;
+import io.github.future0923.debug.tools.hotswap.core.util.IOUtils;
+import io.github.future0923.debug.tools.hotswap.core.util.PluginManagerInvoker;
+import io.github.future0923.debug.tools.hotswap.core.util.classloader.ClassLoaderHelper;
+import io.github.future0923.debug.tools.hotswap.core.watch.Watcher;
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtConstructor;
+import javassist.CtMethod;
+import javassist.NotFoundException;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * Forest热重载插件
+ */
+@Plugin(
+ name = "Forest",
+ description = "Reload Forest after class change.",
+ testedVersions = {"1.5.32"}, expectedVersions = {"1.5.x"},
+ supportClass = {
+ ForestPatcher.class
+ }
+)
+public class ForestPlugin {
+
+ private static final Logger logger = Logger.getLogger(ForestPlugin.class);
+
+ @Init
+ static Watcher watcher;
+
+ @Init
+ static Scheduler scheduler;
+
+ @Init
+ static ClassLoader appClassLoader;
+ /**
+ * 不能使用注解,因为注解只能获取AppClassLoader
+ */
+ private static ClassLoader userClassLoader;
+
+ /**
+ * 获取OpenFeign的类加载器和注册者
+ */
+ public void init(ClassLoader classLoader, Object feignClientsRegistrar) {
+ ForestPlugin.userClassLoader = classLoader;
+ }
+
+ public static ClassLoader getUserClassLoader() {
+ return userClassLoader == null ? appClassLoader : userClassLoader;
+ }
+
+ public static void registerBasePackage(final List basePackages) {
+ for (String basePackage : basePackages) {
+ String classNameRegExp = DebugToolsStringUtils.getClassNameRegExp(basePackage);
+ Enumeration resourceUrls;
+ try {
+ resourceUrls = ClassLoaderHelper.getResources(ForestPlugin.getUserClassLoader(), classNameRegExp);
+ } catch (IOException e) {
+ logger.error("Unable to resolve forest base package {} in classloader {}.", classNameRegExp, ForestPlugin.getUserClassLoader());
+ return;
+ }
+ while (resourceUrls.hasMoreElements()) {
+ URL basePackageURL = resourceUrls.nextElement();
+ if (!IOUtils.isFileURL(basePackageURL)) {
+ logger.debug("forest basePackage '{}' - unable to watch files on URL '{}' for changes (JAR file?), limited hotswap reload support. Use extraClassPath configuration to locate class file on filesystem.", basePackage, basePackageURL);
+ } else {
+ watcher.addEventListener(ForestPlugin.getUserClassLoader(), basePackage, basePackageURL, new ForestWatchEventListener(scheduler, ForestPlugin.getUserClassLoader(), basePackage));
+ }
+ }
+ }
+
+ }
+
+ @OnClassLoadEvent(classNameRegexp = "com.dtflys.forest.springboot.annotation.ForestScannerRegister")
+ public static void patchFeignClientsRegistrar(CtClass ctClass, ClassPool classPool) throws NotFoundException, CannotCompileException {
+ StringBuilder src = new StringBuilder("{");
+ src.append(PluginManagerInvoker.buildInitializePlugin(ForestPlugin.class));
+ src.append(PluginManagerInvoker.buildCallPluginMethod(ForestPlugin.class, "init",
+ "com.dtflys.forest.springboot.annotation.ForestScannerRegister.class.getClassLoader()", ClassLoader.class.getName(),
+ "this", Object.class.getName()));
+ src.append("}");
+ CtConstructor[] constructors = ctClass.getConstructors();
+ for (CtConstructor constructor : constructors) {
+ constructor.insertAfter(src.toString());
+ }
+
+ CtMethod getBasePackages = ctClass.getDeclaredMethod("getBasePackages");
+ getBasePackages.insertAfter("{" +
+ " io.github.future0923.debug.tools.hotswap.core.plugin.forest.ForestPlugin.registerBasePackage($_);" +
+ "}");
+ }
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/command/ForestReloadCommand.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/command/ForestReloadCommand.java
new file mode 100644
index 00000000..0a329e79
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/command/ForestReloadCommand.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest.command;
+
+import io.github.future0923.debug.tools.base.hutool.core.util.ReflectUtil;
+import io.github.future0923.debug.tools.base.logging.Logger;
+import io.github.future0923.debug.tools.hotswap.core.command.EventMergeableCommand;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.dto.ForestClientReloadDTO;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.reload.ForestReload;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.watcher.ForestWatchEventListener;
+import io.github.future0923.debug.tools.hotswap.core.watch.WatchFileEvent;
+
+import java.util.Objects;
+
+public class ForestReloadCommand extends EventMergeableCommand {
+
+ private static final Logger logger = Logger.getLogger(ForestReloadCommand.class);
+
+ private final ClassLoader userClassLoader;
+
+ private final String className;
+
+ private final byte[] bytes;
+
+ private final String path;
+
+ private WatchFileEvent event;
+
+ /**
+ * 当class新增时,通过{@link ForestWatchEventListener#onEvent(WatchFileEvent)}创建命令后调用这
+ */
+ public ForestReloadCommand(ClassLoader userClassLoader, String className, byte[] bytes, String path, WatchFileEvent event) {
+ this.userClassLoader = userClassLoader;
+ this.className = className;
+ this.bytes = bytes;
+ this.path = path;
+ this.event = event;
+ }
+
+ @Override
+ protected WatchFileEvent event() {
+ return event;
+ }
+
+ @Override
+ public void executeCommand() {
+ if (isDeleteEvent()) {
+ logger.trace("Skip reload for delete event on class '{}'", className);
+ return;
+ }
+ try {
+ ClassLoader orginalClassLoader = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(userClassLoader);
+ Class> reloadClass = userClassLoader.loadClass(ForestReload.class.getName());
+ ReflectUtil.invoke(ReflectUtil.newInstance(reloadClass), "reload", ReflectUtil.newInstance(userClassLoader.loadClass(ForestClientReloadDTO.class.getName()), className, bytes, path));
+ Thread.currentThread().setContextClassLoader(orginalClassLoader);
+ } catch (Exception e) {
+ logger.error("reloadConfiguration error", e);
+ }
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (!(o instanceof ForestReloadCommand)) return false;
+ ForestReloadCommand that = (ForestReloadCommand) o;
+ return Objects.equals(className, that.className);
+ }
+
+ @Override
+ public int hashCode() {
+ return className.hashCode();
+ }
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/dto/ForestClientReloadDTO.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/dto/ForestClientReloadDTO.java
new file mode 100644
index 00000000..d72a99d5
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/dto/ForestClientReloadDTO.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest.dto;
+
+
+public class ForestClientReloadDTO {
+
+ private final String className;
+
+ private final byte[] bytes;
+
+ private final String path;
+
+ public ForestClientReloadDTO(String className, byte[] bytes, String path) {
+ this.className = className;
+ this.bytes = bytes;
+ this.path = path;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public String getPath() {
+ return path;
+ }
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/patch/ForestPatcher.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/patch/ForestPatcher.java
new file mode 100644
index 00000000..b1590c1b
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/patch/ForestPatcher.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest.patch;
+
+import io.github.future0923.debug.tools.base.logging.Logger;
+import io.github.future0923.debug.tools.hotswap.core.annotation.OnClassLoadEvent;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.reload.ForestReload;
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtConstructor;
+import javassist.CtMethod;
+import javassist.NotFoundException;
+
+public class ForestPatcher {
+
+ private static final Logger logger = Logger.getLogger(ForestPatcher.class);
+
+ @OnClassLoadEvent(classNameRegexp = "com.dtflys.forest.scanner.ClassPathClientScanner")
+ public static void patchForestScanner(CtClass ctClass, ClassPool classPool) throws NotFoundException, CannotCompileException {
+ logger.debug("enhance forest package ClassPathClientScanner");
+ CtConstructor[] declaredConstructors = ctClass.getDeclaredConstructors();
+ for (CtConstructor constructor : declaredConstructors) {
+ constructor.insertAfter(
+ "{" +
+ ForestReload.class.getName() + ".initScanner(this);" +
+ "}");
+ }
+ }
+
+
+ @OnClassLoadEvent(classNameRegexp = "com.dtflys.forest.proxy.InterfaceProxyHandler")
+ public static void patchForestProxyInvoke(CtClass ctClass, ClassPool classPool) throws NotFoundException, CannotCompileException {
+ logger.debug("enhance forest package InterfaceProxyHandler");
+ CtMethod invokeMethod = ctClass.getDeclaredMethod("invoke",
+ new CtClass[]{
+ classPool.get("java.lang.Object"),
+ classPool.get("java.lang.reflect.Method"),
+ classPool.get("java.lang.Object[]")
+ }
+ );
+
+ invokeMethod.insertBefore("{ initMethods(); }");
+ }
+
+ @OnClassLoadEvent(classNameRegexp = "com.dtflys.forest.reflection.ForestMethod")
+ public static void patchDefaultReflectorFactory(CtClass ctClass, ClassPool classPool) throws NotFoundException, CannotCompileException {
+ logger.debug("enhance forest package ForestMethod");
+ CtMethod initMethods = ctClass.getDeclaredMethod("initMethod");
+ String newBody =
+ "{ " +
+ " synchronized (INIT_LOCK) { " +
+ " processBaseProperties(); " +
+ " processMethodAnnotations(); " +
+ " initialized = true; " +
+ " } " +
+ "}";
+
+ initMethods.setBody(newBody);
+ }
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/reload/ForestReload.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/reload/ForestReload.java
new file mode 100644
index 00000000..f23a3a20
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/reload/ForestReload.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest.reload;
+
+import com.dtflys.forest.scanner.ClassPathClientScanner;
+import io.github.future0923.debug.tools.base.constants.ProjectConstants;
+import io.github.future0923.debug.tools.base.logging.Logger;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.dto.ForestClientReloadDTO;
+import io.github.future0923.debug.tools.hotswap.core.plugin.spring.scanner.ClassPathBeanDefinitionScannerAgent;
+import io.github.future0923.debug.tools.hotswap.core.util.ReflectionHelper;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanDefinitionHolder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanNameGenerator;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+@SuppressWarnings("unchecked")
+public class ForestReload {
+
+
+ private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>();
+
+ public static Object getLock(String className) {
+ return LOCKS.computeIfAbsent(className, k -> new Object());
+ }
+
+ private static final Logger logger = Logger.getLogger(ForestReload.class);
+
+ private static final Set RELOADING_CLASS = ConcurrentHashMap.newKeySet();
+
+ private static ClassPathClientScanner FOREST_SCANNER;
+
+ public static void initScanner(ClassPathClientScanner scanner) {
+ if (FOREST_SCANNER == null) {
+ FOREST_SCANNER = scanner;
+ }
+ }
+
+ private ForestReload() {
+
+ }
+
+ protected void reload(ForestClientReloadDTO dto) throws Exception {
+ String className = dto.getClassName();
+ // 同类中取重
+ if (!RELOADING_CLASS.add(className)) {
+ if (ProjectConstants.DEBUG) {
+ logger.info("{} plus reload task is already running, skip.", className);
+ }
+ return;
+ }
+ // 不同类中串行
+ Object lock = getLock(className);
+ try {
+ synchronized (lock) {
+ defineBean(className, dto.getBytes(), dto.getPath());
+ logger.reload("reload {} in {}", className);
+ }
+ } catch (Exception e) {
+ logger.error("refresh forest client error", e);
+ } finally {
+ RELOADING_CLASS.remove(className);
+ }
+ }
+
+
+ protected void forestBeanDefinition(ClassPathClientScanner scanner, BeanDefinitionHolder holder) {
+ try {
+ Set holders = new HashSet<>();
+ holders.add(holder);
+ Class> classPathMapperScanner = Class.forName("com.dtflys.forest.scanner.ClassPathClientScanner");
+ Method method = classPathMapperScanner.getDeclaredMethod("processBeanDefinitions", Set.class);
+ boolean isAccess = method.isAccessible();
+ method.setAccessible(true);
+ method.invoke(scanner, holders);
+ method.setAccessible(isAccess);
+ } catch (Exception e) {
+ logger.error("freshForest err", e);
+ }
+ }
+
+ protected void defineBean(String className, byte[] bytes, String path) throws IOException {
+ if (FOREST_SCANNER == null) {
+ logger.debug("forestScanner is null");
+ return;
+ }
+ ClassPathBeanDefinitionScannerAgent scannerAgent = ClassPathBeanDefinitionScannerAgent.getInstance(FOREST_SCANNER);
+ BeanDefinition beanDefinition = scannerAgent.resolveBeanDefinition(bytes);
+ if (beanDefinition == null) {
+ logger.error("not found beanDefinition:{}", className);
+ return;
+ }
+ scannerAgent.defineBean(beanDefinition, path);
+ BeanNameGenerator beanNameGenerator = (BeanNameGenerator) ReflectionHelper.get(FOREST_SCANNER, "beanNameGenerator");
+ BeanDefinitionRegistry registry = (BeanDefinitionRegistry) ReflectionHelper.get(scannerAgent, "registry");
+ String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
+ BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
+ forestBeanDefinition(FOREST_SCANNER, definitionHolder);
+ logger.reload("register forest client {} in spring bean", className);
+ }
+
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/watcher/ForestWatchEventListener.java b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/watcher/ForestWatchEventListener.java
new file mode 100644
index 00000000..781420a0
--- /dev/null
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/debug-tools-hotswap-forest-plugin/src/main/java/io/github/future0923/debug/tools/hotswap/core/plugin/forest/watcher/ForestWatchEventListener.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.hotswap.core.plugin.forest.watcher;
+
+import io.github.future0923.debug.tools.base.logging.Logger;
+import io.github.future0923.debug.tools.hotswap.core.annotation.FileEvent;
+import io.github.future0923.debug.tools.hotswap.core.command.Scheduler;
+import io.github.future0923.debug.tools.hotswap.core.plugin.forest.command.ForestReloadCommand;
+import io.github.future0923.debug.tools.hotswap.core.plugin.spring.transformer.SpringBeanWatchEventListener;
+import io.github.future0923.debug.tools.hotswap.core.util.IOUtils;
+import io.github.future0923.debug.tools.hotswap.core.watch.WatchEventListener;
+import io.github.future0923.debug.tools.hotswap.core.watch.WatchFileEvent;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @author future0923
+ */
+public class ForestWatchEventListener implements WatchEventListener {
+
+ private static final Logger logger = Logger.getLogger(SpringBeanWatchEventListener.class);
+
+ private final Scheduler scheduler;
+
+ private final ClassLoader appClassLoader;
+
+ private final String basePackage;
+
+ public ForestWatchEventListener(Scheduler scheduler, ClassLoader appClassLoader, String basePackage) {
+ this.scheduler = scheduler;
+ this.appClassLoader = appClassLoader;
+ this.basePackage = basePackage;
+ }
+
+ @Override
+ public void onEvent(WatchFileEvent event) {
+ logger.debug("{}, {}", event.getEventType(), event.getURI().toString());
+ // 创建了class新文件
+ if (FileEvent.CREATE.equals(event.getEventType()) && event.isFile() && event.getURI().toString().endsWith(".class")) {
+ // 检查该类尚未被类加载器加载(避免重复重新加载)。
+ String className;
+ try {
+ className = IOUtils.urlToClassName(event.getURI());
+ } catch (IOException e) {
+ logger.trace("Watch event on resource '{}' skipped, probably Ok because of delete/create event sequence (compilation not finished yet).", e, event.getURI());
+ return;
+ }
+ try {
+ appClassLoader.loadClass(className);
+ } catch (ClassNotFoundException e) {
+ logger.warning("not found class", e);
+ return;
+ }
+ if (isForest(appClassLoader)) {
+ byte[] bytes = IOUtils.toByteArray(event.getURI());
+ scheduler.scheduleCommand(new ForestReloadCommand(appClassLoader, className, bytes, event.getURI().getPath(), event), 1000);
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ForestWatchEventListener that = (ForestWatchEventListener) o;
+ return Objects.equals(appClassLoader, that.appClassLoader) && Objects.equals(basePackage, that.basePackage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(appClassLoader, basePackage);
+ }
+
+ public static boolean isForest(ClassLoader appClassLoader) {
+ try {
+ appClassLoader.loadClass("com.dtflys.forest.scanner.ClassPathClientScanner");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/debug-tools-hotswap/debug-tools-hotswap-plugin/pom.xml b/debug-tools-hotswap/debug-tools-hotswap-plugin/pom.xml
index 6accc2bb..6dd9d522 100644
--- a/debug-tools-hotswap/debug-tools-hotswap-plugin/pom.xml
+++ b/debug-tools-hotswap/debug-tools-hotswap-plugin/pom.xml
@@ -25,6 +25,7 @@
debug-tools-hotswap-solon-plugin
debug-tools-hotswap-intellij-plugin
debug-tools-hotswap-feign-plugin
+ debug-tools-hotswap-forest-plugin
\ No newline at end of file
diff --git a/debug-tools-test/debug-tools-test-spring-boot-mybatis/pom.xml b/debug-tools-test/debug-tools-test-spring-boot-mybatis/pom.xml
index f65ba909..0cdb925e 100644
--- a/debug-tools-test/debug-tools-test-spring-boot-mybatis/pom.xml
+++ b/debug-tools-test/debug-tools-test-spring-boot-mybatis/pom.xml
@@ -125,6 +125,11 @@
spring-boot-starter-test
test
+
+ com.dtflys.forest
+ forest-spring-boot-starter
+ 1.5.32
+
diff --git a/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/clients/ForestTestClient.java b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/clients/ForestTestClient.java
new file mode 100644
index 00000000..129b1020
--- /dev/null
+++ b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/clients/ForestTestClient.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.test.spring.boot.mybatis.clients;
+
+import com.dtflys.forest.annotation.Get;
+import com.dtflys.forest.annotation.Post;
+
+public interface ForestTestClient {
+
+ @Get("http://localhost:8111/forest/test")
+ String test();
+}
\ No newline at end of file
diff --git a/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/controller/ForestController.java b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/controller/ForestController.java
new file mode 100644
index 00000000..66f00613
--- /dev/null
+++ b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/controller/ForestController.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.test.spring.boot.mybatis.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/forest")
+public class ForestController {
+
+ @GetMapping("/test")
+ public String normalComment() {
+ return "this is wo ai wei zong";
+ }
+}
diff --git a/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/service/ForestService.java b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/service/ForestService.java
new file mode 100644
index 00000000..2a763da2
--- /dev/null
+++ b/debug-tools-test/debug-tools-test-spring-boot-mybatis/src/main/java/io/github/future0923/debug/tools/test/spring/boot/mybatis/service/ForestService.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024-2025 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.github.future0923.debug.tools.test.spring.boot.mybatis.service;
+
+import io.github.future0923.debug.tools.test.spring.boot.mybatis.clients.ForestTestClient;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+@Service
+public class ForestService {
+ @Resource
+ private ForestTestClient forestTestClient;
+
+ public String forestTest() {
+ return forestTestClient.test();
+ }
+}