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(); + } +}