springboot动态加载第三方jar包,可随时卸载和添加jar包

类似于微服务架构那样,但是不使用微服务架构。动态加载第三方jar包,在项目不停止运行的情况下,可随时卸载和添加jar包。
我使用URLClassLoader实现,但是jar包中有关spring的所有注解都使用不了,@Autowired获取不到实体类,@Value获取不到配置文件中的配置
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:D:/jar/uin-msggateway-lianlu-0.0.1-SNAPSHOT.jar"),new URL("file:D:/jar/uin-msggateway-ronglian-0.0.1-SNAPSHOT.jar")});

我这里提供一个实现的思路作为参考吧,想要实现你目前的功能的话:

加载

  1. 首先将bean工厂先持有起来,作为父类工厂。
  2. 另外构建一个插件管理器,这个管理器负责动态加载、卸载jar的管理,并持有bean工厂的实例。【相当于一个子工厂】

    开始构建jar中的实例,分析实例的元数据【接口、父类等等】

  3. 1 读取jar中的实例[大鹏cool ]已经给了案例了,在读取到jar对应的class文件的时候,需要
  • 实例化
  • 属性注入的时候,可以从bean工厂中查找对应的类型set进去

    此时,jar中的实例全部持有在插件管理器中。可以缓存起来根据jar名称啥的,方便卸载的时候直接定位到。

在业务代码中进行应用

  1. 比如你需要根据接口去获取对应的插件实现,你需要从插件管理器中先获取对象,缓存中找不到则再从bean工厂中查找。不能通过@Autowired去注入。
// 每次获取都是手动去查找,SendService就不能通过注入的方式了。
public void send(){
   SendService service = PluginManagerHelper.getBean(SendService.class);
   service.send();
}
// PluginManagerHelper的话,就遍历所有插件去找对应的类型。至于顺序是否唯一的话,根据你的实际情况写逻辑。

尽量保持这个插件管理器中的所有对象不要被外面持有,否则卸载的时候就会出现无法回收的问题。

卸载

所有的jar对象已经被插件管理器所管理起来,卸载的话只需要从缓存中找到匹配的插件对象,删除缓存就行了,等待gc的回收。
当业务需要再次获取SendService,遍历所有插件的时候发现那个有的插件已经不存在了,所以就匹配不到了。

本质上还是将插件的加载作为一个子工厂来维护,不会融入到ioc容器中。这样的好处就是插件jar是有边界的。

另外我想了一下,如果要做到和ioc融入到一起,太复杂了,容器启动的时候所有bean的周期已经确定完毕,你如果这时候要动态来一个bean,不仅要通知已经加载过的bean去看看要不要改变,而且要不断刷新容器,不过也有这样的场景,比如动态更新配置中心,但配置中心的话不会出现要卸载的情况。当然这都是 题外话了。

以上仅为个人思路,希望能帮助到你。

一楼正解,但是还要仔细推敲,认真修改

你想实现的这个是很难的,controller的解析也是初始化的时候做的

不建议动态注册非类路径下 jar 包中的类为 bean,如果一定要注册,这里有一个不成熟的方案,代码已经过自测。

思路:

  1. 动态加载非类路径下的类,因此需要自定义 ClassLoader,可以使用 URLClassLoader。
  2. 项目需要动态注册非类路径的类为 bean,因此需要使用 ClassPathBeanDefinitionScanner 扫描类,并指定类加载器,以便加载非类路径下的类。
  3. 扫描只是读取类文件,并未将类加载到 JVM 中,在获取 bean 实例的时候还需要使用底层的 DefaultListableBeanFactory,并指定类加载器,以便通过反射创建 bean 实例。

非类路径 jar 包中的代码如下。

img

读取并注册非类路径下 bean 的示例代码如下。

@RestController
@RequestMapping("/test")
public class UserController {


    @Autowired
    private DefaultListableBeanFactory beanFactory;

    @GetMapping("/dynamic")
    public Map<String, Object> dynamic() throws MalformedURLException {

        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:/Users/zzuhkp/hkp/project/demo/target/bean-1.0-SNAPSHOT.jar")});

        // 创建扫描 bean 定义的扫描器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.beanFactory);
        // 设置读取资源文件的资源加载器
        scanner.setResourceLoader(new PathMatchingResourcePatternResolver(urlClassLoader));

        // 开始扫描,将 bean 定义注册到 BeanFactory 中
        scanner.scan("com.zzuhkp.bean");

        // 备份 BeanFactory 中的旧类加载器
        ClassLoader oldClassLoader = beanFactory.getBeanClassLoader();
        // 设置用于加载非类路径下类的类加载器
        beanFactory.setBeanClassLoader(urlClassLoader);

        // 获取 bean 实例,此时会使用前面自定义的类加载器创建 bean 实例
        Object customBean = beanFactory.getBean("bean");

        // 最后恢复 BeanFactory 中保存的类加载器
        beanFactory.setBeanClassLoader(oldClassLoader);

        System.out.println(customBean);

        return new HashMap<>();
    }

}

运行截图如下。

img

最后,使用自定义 ClassLoader 动态加载类,如果自定义的 ClassLoader 加载的类被其他类引用,或者自定义的 ClassLoader 没有被 JVM 回收,将导致堆内存或者元空间的内存不断升高,最终 OOM。

不确定你为什么有这种需求,尽量还是不要这样做,如果做的话要多加测试。

你想给bean池传到字包中,这个意思?

osgi可以

想要和热备份一样的动态加载可能不太能实现,要是有多个服务器或者虚拟服务器可以一个升级,另一个提供服务,由调度服务器把数据编程不升级的服务器提供,等升级的服务器里加载完包以后,再由调度服务器切换。

我猜测你的需求并不需要动态加载jar包,如果只是为了切换短信渠道的话用个策略模式就可以了

楼上+1,你只是做个切换不同实现类,最最简单的就是配置beanId到数据库,然后getBean就能动态切换实现类了,所有使用的地方都不是直接注入你的接口,而是通过工厂类去获取bean,这样就保证全局唯一而且可变

你看看下面这个项目
https://github.com/pf4j/pf4j
我以前参考上面这个做过加载第三方jar包的,但我做的那个不是动态加载的

直接把jar包全部让spring全部加载了,动态执行使用mvel表达式

看了下你的需求:其中难点就是要在不停止服务的情况下,更新服务中的短信接口。
正常来说,不停止服务的情况下是可以动态加载jar包这些的,但是因为bean扫描、注解这些都是启动时完成的,所以动态加载就不能有。
上面楼直接暴力解决,应该是可以搞的。但是这样搞应该和更新代码、重启服务器差不多吧。

看了下个人感觉你这需求是有点问题的,应该尽量避免在jar包中使用注解。应该在你的项目代码中提前写好适配接口代码。

想拿这个采纳也真不容易呀,哈哈哈,我又去试了一下,想到一个折中的方法,使用urlclassloader可以加载jar,但是并没有扫描注解,又不知道怎么让容器去扫描新加载的jar,咋办呢?xml手动配置bean,亲测有效,上代码,希望对题主有帮助。题主如果有好的解决方案希望可以分享,谢谢
spring容器工具类:

package xxx.xxx.xxx;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        if (SpringUtil.applicationContext == null) {

            SpringUtil.applicationContext = applicationContext;

        }

        log.info("ApplicationContext配置成功,applicationContext对象:" + SpringUtil.applicationContext);

    }

    public static ApplicationContext getApplicationContext() {

        return applicationContext;

    }

    public static Object getBean(String name) {

        return getApplicationContext().getBean(name);

    }

    public static <T> T getBean(Class<T> clazz) {

        return getApplicationContext().getBean(clazz);

    }

    public static <T> T getBean(String name, Class<T> clazz) {

        return getApplicationContext().getBean(name, clazz);

    }

}


测试demo:

package xxx.xxx;

import com.demo.service.DemoService;
import com.demo.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.xml.ResourceEntityResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringRunner;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoServerApplication.class)
@Slf4j
public class Demo {
    /**
     * 删除原有的jar,添加新jar,执行这个方法更新容器中的实现
     */
    @Test
    public void send() {
        //先清理容器中的对象
        String beanName = "demoService";
        BeanDefinitionRegistry beanDefReg = (DefaultListableBeanFactory) SpringUtil.getApplicationContext().getAutowireCapableBeanFactory();
        if(beanDefReg.isBeanNameInUse(beanName)){
            beanDefReg.removeBeanDefinition(beanName);
        }
        //加载jar并根据jar中xml加载bean到容器中
        this.addUrlToClassPath("/Users/qishi/IdeaProjects-mycloud/home-demo/home-demo-impl/target/home-demo-impl-1.0-SNAPSHOT.jar");
        this.readXml("spring-demo.xml");

        //检查容器中是否有想要加载上去的bean
        DemoService bean = (DemoService) SpringUtil.getBean(beanName);
        bean.demo();
    }

    private static void addUrlToClassPath(String jarPath) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        try {
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
            add.setAccessible(true);
            add.invoke(classLoader, new Object[]{new URL("file:" + jarPath)});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void readXml(String xmlName) {
        ApplicationContext applicationContext = SpringUtil.getApplicationContext();
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(
                (BeanDefinitionRegistry) ((ConfigurableApplicationContext) applicationContext).getBeanFactory());
        beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(applicationContext));
        Resource resouce =  new ClassPathResource(xmlName);
        beanDefinitionReader.loadBeanDefinitions(resouce);
    }

}


xml配置bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="demoService" class="com.demo.service.impl.DemoServiceImpl"></bean>
</beans>

完善了一下demo,使用send方法基本就是替换整个jar的流程了

可以考虑其他代替方案啊,这个有点涉及底层逻辑了

这种情况下已经无法动态加载了

动态添加,这么牛掰,不得修改完,重新部署项目这种吧,类似tomcat热启动,那问题来了,你确定卸载完了,程序能正常运行。

spring包扫描在启动的时候就完成了