类似于微服务架构那样,但是不使用微服务架构。动态加载第三方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")});
我这里提供一个实现的思路作为参考吧,想要实现你目前的功能的话:
开始构建jar中的实例,分析实例的元数据【接口、父类等等】
此时,jar中的实例全部持有在插件管理器中。可以缓存起来根据jar名称啥的,方便卸载的时候直接定位到。
不能通过@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,如果一定要注册,这里有一个不成熟的方案,代码已经过自测。
思路:
非类路径 jar 包中的代码如下。
读取并注册非类路径下 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<>();
}
}
运行截图如下。
最后,使用自定义 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包扫描在启动的时候就完成了