{"id":19215043,"url":"https://github.com/chgl16/hotloading","last_synced_at":"2026-06-16T03:32:12.160Z","repository":{"id":52536173,"uuid":"204149845","full_name":"chgl16/hotloading","owner":"chgl16","description":":leaves: 保证服务不重启的情况下热加载外部class到Spring容器，并实现属性依赖注入","archived":false,"fork":false,"pushed_at":"2021-04-26T19:27:39.000Z","size":506,"stargazers_count":2,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-11-14T03:29:29.537Z","etag":null,"topics":["classloader","hotloading","spring-classloader"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chgl16.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-08-24T11:45:14.000Z","updated_at":"2021-08-03T08:24:47.000Z","dependencies_parsed_at":"2022-09-06T15:20:22.932Z","dependency_job_id":null,"html_url":"https://github.com/chgl16/hotloading","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chgl16/hotloading","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chgl16%2Fhotloading","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chgl16%2Fhotloading/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chgl16%2Fhotloading/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chgl16%2Fhotloading/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chgl16","download_url":"https://codeload.github.com/chgl16/hotloading/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chgl16%2Fhotloading/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34390052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["classloader","hotloading","spring-classloader"],"created_at":"2024-11-09T14:12:34.623Z","updated_at":"2026-06-16T03:32:12.134Z","avatar_url":"https://github.com/chgl16.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 热加载模块\n    保证服务不重启的情况下热加载外部class到Spring容器，并实现属性依赖注入\n    \n## 一、需求描述\n![效果展示](https://i.loli.net/2019/08/24/waHUJ1583f6iOgP.png)\n\n\u0026emsp;\u0026emsp;服务使用过程中，希望在不重启服务的情况下，动态加载某些class/scalar/jar到项目中使用，\n即托管到Spring容器，并能实现属性依赖的注入。（本模块只加载class），同时也保证**热更新**。  \n\u0026emsp;\u0026emsp;这些外部网络加载来的class一般实现服务里的某些业务接口，即希望后续动作bean组件使用。\n指定beanName即可，这里默认beanName为类名lowCamel形式。\n\n## 二、核心实现\n\n### ①相关代码   \n\n| 类 \u0026 接口 | 说明  |\n| :---: | :---:|\n| FileStoreUtil | 文件保存工具类 |\n| CustomClassLoader | 自定义类加载器，指定父加载器为Spring的加载器 |\n| SpringContextUtil | 服务的spring容器操作工具类 |\n| RegisterBeanUtil | 动态注册bean到容器的工具类，会注入依赖 |\n| DynamicInterface | 模拟需求存在服务的class接口|\n| DynamicClass | 模拟需求需要加载的class，提供参考，加载的是类似的DynamicClass2 |\n\n### ②实现方法\n##### 方法1： class文件直接保存到classpath对应的包目录下。  \n方法这种方法实现比较简单，不需要自定义类加载器，直接  \n```java\n// 反射获取Class，加载到方法区\nClass\u003c?\u003e cls = Class.forName(\"class全名\");\n\n// 调用注册方法，托管到Spring容器\nregister(Class\u003c?\u003e cls, String beanName);\n```\n但是这种方法拓展性很差，一般服务打包为jar包，根本无法往里面存文件。  \n\n##### 方法2： 自定义类加载器  \n这个方法不需要保存到项目classpath中class对应的目录下。可以任意目录，Class.formName()对比加载器最大就是这个区别  \n\u003e本项目保存到classpath:resource/hotLoadingClasses只为方便测试  \n\n* CustomClassLoader\n```java\n/**\n * 自定义类加载器加载外部class\n * 不需要重写加载的双亲委派逻辑\n *\n * @author chgl16\n * @date 2019/8/24 16:24\n */\n@Component\npublic class CustomClassLoader extends ClassLoader {\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n\n    /**\n     * 主要为了指定父加载器为Spring类加载器\n     * 因此在构造方法中配置调用\n     * 同时此加载器必须由Spring类加载器加载，即必须声明为@Component\n     */\n    public CustomClassLoader() {\n        super(CustomClassLoader.class.getClassLoader());\n    }\n\n    /**\n     * 加载某个目录下的一个class文件，返回类的类类型\n     *\n     * @param name 类的全名，如 java.util.concurrent.ConcurrentHashMap\n     * @return\n     */\n    @Override\n    protected  Class\u003c?\u003e findClass(String name) {\n        // 这个路径就是网络路径，必须都是 \"/\"，不能存在 \"\\\"，否则URL转换失败\n        String myPath = \"file:///\" + FileStoreUtil.BASE_FOLDER.replace(\"\\\\\", \"/\") + name.replace(\".\",\"/\") + \".class\";\n\n        /*\n         * !!本打算以此去掉可能存在的后缀BeanInfo和Customizer的class信息类加载失败的问题\n         * 》实践结果：无法去掉，这是spring加载器无法加载，本加载器被迫加载造成的错误，\n         * 》而如果class在classpath下是没有问题，但是这个问题不影响使用，因此隐藏了日志\n         */\n//        myPath = myPath.replace(\"BeanInfo\", \"\").replace(\"Customizer\", \"\");\n\n        logger.warn(\"使用了自定义加载器加载该类: \" +  myPath);\n\n        byte[] classBytes = null;\n        Path path = null;\n        try {\n            path = Paths.get(new URI(myPath));\n            classBytes = Files.readAllBytes(path);\n        } catch (IOException | URISyntaxException e) {\n            e.printStackTrace();\n        }\n        Class cls = defineClass(name, classBytes, 0, classBytes.length);\n\n        return cls;\n    }\n}\n```\n自定义类加载器只需要重写findClass(name)方法即可，调用的时候是调用loadClass(name)方法，因为这里不需要\n破坏双亲委派逻辑，调用链为loadClass -\u003e findClass -\u003e defineClass。  \n\n这里需要注意的是**要保证自定义的类加载器父加载器是Spring加载器**，因为加载的class存在某些spring特性类（比如注解@Autowrited）,这些是需要Spring加载器加载的。  \n\n实现方法很简单，把CustomClassLoader注册为@Component，即自定义的加载器被Spring加载器加载，那么构造方法的super(CustomClassLoader.class.getClassLoader())就是指定了父加载器。  \n\nClassLoader源码\n```java\n /**\n * Creates a new class loader using the specified parent class loader for\n * delegation.\n *\n * \u003cp\u003e If there is a security manager, its {@link\n * SecurityManager#checkCreateClassLoader()\n * \u003ctt\u003echeckCreateClassLoader\u003c/tt\u003e} method is invoked.  This may result in\n * a security exception.  \u003c/p\u003e\n *\n * @param  parent\n *         The parent class loader\n *\n * @throws  SecurityException\n *          If a security manager exists and its\n *          \u003ctt\u003echeckCreateClassLoader\u003c/tt\u003e method doesn't allow creation\n *          of a new class loader.\n *\n * @since  1.2\n */\nprotected ClassLoader(ClassLoader parent) {\n    this(checkCreateClassLoader(), parent);\n}\n```    \n需要加载的class类似如下：\n```java\npublic class DynamicClassBO implements DynamicInterface {\n\n    @Autowired(required = false)\n    public PropertyBean propertyBean;\n\n    public void sayHi() {\n        System.out.println(\"Hi: 第一个实现类**************************\");\n        propertyBean.fun();\n\n    }\n\n    public PropertyBean getPropertyBean() {\n        return propertyBean;\n    }\n\n    public void setPropertyBean(PropertyBean propertyBean) {\n        this.propertyBean = propertyBean;\n    }\n}\n```\n加载这个类用到的是自定义加载器的findClass方法，但是里面的@Autowired标注的属性会委派到Spring加载器加载。  \n因此如果属性使用@Autowired如此，在构造bean的时候是不需要手动注入属性的，Spring加载胡自动注入。  \n\u003e@Autowired的属性这里不需要setter/getter方法，如果没用此注解需要提供setter方法，不然无法构造属性注入  \n```java\n// Class注入属性\nField[] fields = cls.getFields();\nfor (Field field : fields) {\n    // 容器存在这个属性则注入，按编码习惯的名字，没有按类型。（用户输入的也是名字）\n    if (defaultListableBeanFactory.isBeanNameInUse(field.getName())) {\n        /*\n           @第一个参数是类的属性名，第二个是容器中需要的bean的beanNam\n           1. 如果是@Autowired注解的属性不需要这样添加了，注释掉下面代码\n           2. 如果不是@Autowired需要另外添加setter方法\n         */\n        beanDefinitionBuilder.addPropertyReference(field.getName(), field.getName());\n    }\n}\n```  \n\n* ApplicationContext\n```java\n@SpringBootApplication\npublic class HotloadingApplication {\n\n    public static void main(String[] args) {\n        ApplicationContext ac = SpringApplication.run(HotloadingApplication.class, args);\n        // 保存同一个容器使用\n        SpringContextUtil.setApplicationContext(ac);\n    }\n}\n```\n容器在启动类处保存到工具类即可。保证容器唯一。  \n以下这种方法会错误失败\n```java\n@Autowired\nprivate ApplicationContext applicationContext;\n```\n  \n\n* RegisterBeanUtil\n```java\n/**\n * 注册Bean\n *\n * @author chgl16\n * @date 2019/8/24 16:20\n */\npublic class RegisterBeanUtil {\n\n    /**\n     * 注册一个Class到IOC容器，并且返回调用\n     * class不要求在classpath路径下\n     *\n     * @param cls  注册的bean Class\n     * @param beanName 注册的bean Id\n     * @return\n     */\n    public static Object register(Class\u003c?\u003e cls, String beanName) {\n\n        //将applicationContext转换为ConfigurableApplicationContext\n        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextUtil.getApplicationContext();\n\n        // 获取bean工厂并转换为DefaultListableBeanFactory\n        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();\n\n        /*\n            如果存在需要热更新，即先从工厂删去\n            这里不能使用SpringContext.getBean(beanName) 或者 defaultListableBeanFactory.getBean(beanName)判断\n            因为这个getBean方法必须保证bean存在容器的，不存在不会有null返回，直接异常中断程序，当然可以选择捕获异常不抛出保证程序继续执行\n         */\n        if (defaultListableBeanFactory.isBeanNameInUse(beanName)) {\n            defaultListableBeanFactory.removeBeanDefinition(beanName);\n        }\n\n        // 通过BeanDefinitionBuilder创建bean定义\n        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(cls);\n\n        // 所有属性\n        Field[] fields = cls.getFields();\n        for (Field field : fields) {\n            // 容器存在这个属性则注入，按编码习惯的名字，没有按类型。（用户输入的也是名字）\n            if (defaultListableBeanFactory.isBeanNameInUse(field.getName())) {\n                // 第一个参数是类的属性名，第二个是容器中需要的bean的beanNam\n                beanDefinitionBuilder.addPropertyReference(field.getName(), field.getName());\n            }\n        }\n\n        // 注册bean\n        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());\n\n        // 返回\n        return SpringContextUtil.getBean(beanName);\n    }\n\n    /**\n     * 注册一个Class到IOC容器，并且返回调用\n     * 此name对应的class必须在classpath下\n     * 这种不会出现BeanInfo和Customize的后缀问题\n     *\n     * @param name 类的全名，如java.util.concurrent.ConcurrentHashMap\n     * @param beanName 注册的bean Id\n     * @return\n     */\n    public static Object register(String name, String beanName) {\n        Class\u003c?\u003e cls = null;\n        try {\n            // 如果此name对应的class必须在classpath下，则反射成功\n            cls = Class.forName(name);\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        }\n        return register(cls, beanName);\n    }\n\n}\n```\n最核心的一个工具类，这些spring都封装很多了，spring容器相当于把加载到方法区Class获取创建id-class对象的形式维护到一个map中，  \n这里的builder先获取class定义（创建者模式源码内部创建了一个实例，属于bean生命周期第一步）然后加入属性（就是属性setter注入，属于bean生命周期的第二步）。  \n最后就算注册到容器了。  \n\n\u003e这里的判断容器是否存在一个bean这里不能使用SpringContext.getBean(beanName) 或者 defaultListableBeanFactory.getBean(beanName)判断。  \n\u003e因为这个getBean方法必须保证bean存在容器的，不存在不会有null返回，直接异常中断程序，当然可以选择捕获异常不抛出保证程序继续执行。  \n使用boolean defaultListableBeanFactory.isBeanNameInUse(beanName)才是正解\n\n\n# 三、问题事项\n1. 会报错XxxBeanInfo.class 或者 XxxCustomizer.class找不到  \n\u003eXxx表示的是加载的类名，调试源码显示除了加载DynamicClassBO，还加载了DynamicClassBOBeanInfo和DynamicClassBOCustomizer  \n  \n这两个不知道为啥还是调用了自定义加载器的findClass方法，因为这两个应该是Spring加载器加载的，是一些bean的属性和信息类。不过并不影响使用  \n```console\n2019-08-24 22:07:54.476  WARN 4420 --- [nio-8080-exec-8] x.c.hotloading.util.CustomClassLoader    : 使用了自定义加载器加载该类: file:///F:/project/hotloading/src/main/resources/hotLoadingClasses/xyz/cglzwz/hotloading/bo/DynamicClass7BOCustomizer.class\njava.nio.file.NoSuchFileException: F:\\project\\hotloading\\src\\main\\resources\\hotLoadingClasses\\xyz\\cglzwz\\hotloading\\bo\\DynamicClass7BOCustomizer.class\n\tat sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)\n\tat sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)\n\tat sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)\n\tat sun.nio.fs.WindowsFileSystemProvider.newByteChannel(WindowsFileSystemProvider.java:230)\n\tat java.nio.file.Files.newByteChannel(Files.java:361)\n\tat java.nio.file.Files.newByteChannel(Files.java:407)\n\tat java.nio.file.Files.readAllBytes(Files.java:3152)\n\tat xyz.cglzwz.hotloading.util.CustomClassLoader.findClass(CustomClassLoader.java:59)\n\tat java.lang.ClassLoader.loadClass(ClassLoader.java:424)\n\tat java.lang.ClassLoader.loadClass(ClassLoader.java:357)\n\tat java.lang.Class.forName0(Native Method)\n\tat java.lang.Class.forName(Class.java:348)\n\tat com.sun.beans.finder.ClassFinder.findClass(ClassFinder.java:103)\n\tat java.beans.Introspector.findCustomizerClass(Introspector.java:1301)\n\tat java.beans.Introspector.getTargetBeanDescriptor(Introspector.java:1295)\n\tat java.beans.Introspector.getBeanInfo(Introspector.java:425)\n\tat java.beans.Introspector.getBeanInfo(Introspector.java:262)\n\tat java.beans.Introspector.getBeanInfo(Introspector.java:204)\n\tat org.springframework.beans.CachedIntrospectionResults.getBeanInfo(CachedIntrospectionResults.java:248)\n\tat org.springframework.beans.CachedIntrospectionResults.\u003cinit\u003e(CachedIntrospectionResults.java:273)\n\tat org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:177)\n\tat org.springframework.beans.BeanWrapperImpl.getCachedIntrospectionResults(BeanWrapperImpl.java:174)\n\tat org.springframework.beans.BeanWrapperImpl.getLocalPropertyHandler(BeanWrapperImpl.java:230)\n\tat org.springframework.beans.BeanWrapperImpl.getLocalPropertyHandler(BeanWrapperImpl.java:63)\n```  \n当然如果使用方法1不会出现这种情况。比较保存到外面路径的class本身就无法生成对应的XxxBeanInfo和XxxCustomizer到相同目录下。  \n疑点还是不知道为是作为父加载器的spring加载器不加载。  \n\u003e这种异常不影响程序执行，不会中断，为解决时可以捕获异常不处理即可。  \n```java\n@Override\nprotected  Class\u003c?\u003e findClass(String name) {\n    // ...    \n\n    byte[] classBytes = null;\n    Path path = null;\n    try {\n        path = Paths.get(new URI(myPath));\n        classBytes = Files.readAllBytes(path);\n    } catch (IOException | URISyntaxException e) {\n        // 捕获不处理，不打印\n        // e.printStackTrace();\n    }\n    Class cls = defineClass(name, classBytes, 0, classBytes.length);\n    return cls;\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchgl16%2Fhotloading","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchgl16%2Fhotloading","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchgl16%2Fhotloading/lists"}