SpringBoot第十二篇:热加载第三方jar包(解决嵌套jar读取、加载、动态配置、bean注册、依赖等问题),及其精髓

背景

本文章主要解决SpringBoot在启动时动态从application.yaml配置文件中获取指定要动态加载的jar包,并成功加载到jvm中,顺便对包含spring注解的类进行注册bean,由此保证程序在使用动态加载的jar包的类时不报错

应用场景:动态扩展第三方功能、无需重复打包切换数据库等第三方依赖的版本jar包

本文会优先将解决此需求过程中遇到的各个问题的解决方案记录下来,以便给后来人解惑

参考博客

spring boot 动态加载模块(加载外部jar包)
ImportBeanDefinitionRegistrar)
Spring Boot 如何热加载jar实现动态插件?

解决了哪些问题

1.解决在springBoot项目启动时进行热加载jar,但要在SpringBoot自己的依赖jar包运行后再进行(使用)

使用@Import + ImportBeanDefinitionRegistrar实现

// SpringBoot启动类上加@Import("你定义热加载逻辑的类.class")
@Import("PluginImportBeanDefinitionRegistrar.class")
@SpringBootApplication()
public class SpringBootApplication{
    public void static void main(String[] args){
		SpringApplication.run(SpringBootApplication.class, args);
	}
}

//PluginImportBeanDefinitionRegistrar进行热加载处理
public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //处理热加载jar包的逻辑
    }
}
2.解决读取配置文件的问题(使用)

实现EnviromentAware,通过getProperty获取,不要通过@Value获取,这时候获取不到的

//PluginImportBeanDefinitionRegistrar进行热加载处理
public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

	/**
	* jar包存放路径
	*/
	private String libPath;

	/**
	* 动态加载jar包名称,多个用英文逗号隔开
	*/
	private String loadJarNames;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //处理热加载jar包的逻辑
    }
    
	@Override
	public void setEnvironment(Enviroment environment){
		this.libPath = environment.getProperty("libPath");
		this.loadJarNames = environment.getProperty("loadJarNames");
	}
}
3.解决加载jar包class文件过程中出现的NoClassDefFoundError问题(使用)

此问题是由于你加载的jar包可能依赖于其他jar包,由于当前热加载不依赖于maven帮你解决依赖问题,所以需要手动将此jar包依赖的jar包在它之前加载
如何确定它依赖于哪些jar包呢?
如下图所示,在你引用的jar,用压缩工具打开,在/META-INF/maven/路径下,总会找到一个pom.xml文件,这里面就能找到它所依赖的其他jar包
在这里插入图片描述
又或者在你本地maven依赖库中,对应版本的目录下会有一个.pom文件,里面也说明了依赖库有哪些
在这里插入图片描述
注意:看第三方库的依赖,注意<scope></scope>标签,如果是provide、test可以不用管,compile和runtime必须要加载

确定要优先加载哪些依赖jar包后,就需要一个地方来控制加载顺序,解决方案看第4点

4.解决打成jar包读取resource下文件(非jar包)问题(未使用)

为了解决加载顺序问题,以及动态控制加载的jar包问题,刚开始的方案是通过在resource目录下创建一个config目录,生成一个loadSort.txt,代码读取每一行,放在LinkedList中,按顺序加载即可,后来通过application.yaml文件用一个配置参数配置,多个jar包用英文逗号隔开链接成一排,后续通过切割字符串进行处理成LinkedList

刚开始通过ClassPathResource(path)即可拿到,但是打成jar包就不能通过这种方式获取到了,解决方案如下:

// 获取加载顺序配置文件
InputStream loadSortTxtInputStream = this.getClass().getResourceAsStream("/config/loadSort.txt");
BufferedReader bufferedReader;
List<String> jarList = new LinkedList<>();
try{
	bufferedReader = new BufferedReader(new InputStreamReader(loadSortTxtInputStream));
	String jarName;
	while((jarName = bufferedReader.readLine()) != null){
		jarList.add(jarName);
	}
	loadSortTxtInputStream.close();
	bufferedReader.close();
} catch(IOException e){
	e.printStackTrace();
}
5.解决打成jar包读取/BOOT-INF/lib下依赖的jar列表问题(使用)

为了解决jar包依赖冲突问题,就想着把/BOOT-INF/lib下加载的jar包列表搞出来,然后加载jar包之前判断下是否已经加载了同样的jar包,如果有就跳过加载这个jar包

解决方案如下:

//匹配数字的正则
private static Pattern pattern = Pattern.compile("[0-9]");


// 注意,idea运行此代码报错,原因是idea的启动和java -jar方式的启动不一样
// 获取当前运行的jar的完整路径
ApplicationHome home = new ApplicationHome();
File applicationJarFile = home.getSource();
String applicationJarFilePath = applicationJarFile.getAbsolutePath();
List<String> classJars = getDependence(applicationJarFilePath);
//获取到的jar名称是带了路径的,例如/BOOT-INF/lib/a.jar

//获取依赖的jar包名称列表
private List<String> getDependence(String jarPath){
	List<String> result = new LinkedList<>();
	if(!jarPath.endWith(".jar")){
		return result;
	}
	URL url;
	try{
		url = new URL("jar:file:/" + jarPath + "/");
	} catch(MalformedURLException){
		e.printStackTrace();
		return result;
	}
	try{
		JarURLConnection connection = (JarURLConnection) url.openConnection();
		JarFile jarFile = connection.getJarFile();
		for(Enumeration<JarEntry> ea = jarFile.entries();ea.hasMoreElements();){
			JarEntry jarEntry = ea.nextElement();
			if(jarEntry.getName().startWith("/BOOT-INF/lib/") && arEntry.getName().endWith(".jar")){
				result.add(jarEntry.getName().substring((jarEntry.getName().lastIndexOf("/")+ 1));
			}
		}
	}catch(){
		e.printStackTrace();
	}
	return result;
}

//判断是否存在同样的依赖jar包名,TODO:判断版本号高低
private boolean validateJarAndVersion(List<String> dependenceList, String jarName){
	boolean result = true;
	int firstNumIndex = getFirstNumIndex(jarName);
	String jarSimpleName = jarName.substring(0,firstNumIndex);
	for(String dependence : dependenceList){
		if(dependence.startWith(jarSimpleName)){
			result = false;
			break;
		}
	}
	return result;
}

//获取名字中第一个出现数字的下标
private int getFirstNumIndex(String jarName){
	Matcher matcher = pattern.matcher(jarName);
	if(matcher.find()){
		return matcher.start();
	}else{
		return -1;
	}
}
6.解决打成jar包读取resource下文件(jar包)问题(使用)

嵌套jar包的读取,无法通过file(path)去获取,因为项目打成jar包,路径就只能到jar包这一层,要想变成file读取,思路就是先获取到它的inputstream流,保存在一个临时的jar包中,然后用完删除掉临时文件。

	ClassPathResource classPathResource = new ClassPathResource(libPath + "/" + jarName);
	File jar = new File("/temp/" + jarName);
	FileUtils.copyToFile(classPathResource.getInputStream(), jar);
	JarFile jarFile = new JarFile(jar);
	//使用完后
	jar.delete();

完整的PluginImportBeanDefinitionRegistrar文件代码

这里就提供核心的PluginImportBeanDefinitionRegistrar文件的代码,至于启动类的@Importresource的loadSort.txtresource下的lib目录下的jar包配置文件就不提供了,自己按需添加即可
pom文件需要加commons-io的依赖

public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

	private static final Logger LOGGER = LoggerFactory.getLogger(PluginImportBeanDefinitionRegistrar.class);

    //匹配数字的正则
    private static Pattern pattern = Pattern.compile("[0-9]");

    /**
     * jar包存放路径
     */
    private String libPath;

    /**
     * 动态加载jar包名称,多个用英文逗号隔开
     */
    private String loadJarNames;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //获取要动态加载的jar列表
        List<String> jarList = new LinkedList<>();
        if(Strings.isNotBlank(loadJarNames)){
            jarList.addAll(Arrays.asList(loadJarNames.split(",")));
        }
        ApplicationHome home = new ApplicationHome();
        File applicationJarFile = home.getSource();
        String applicationJarFilePath = applicationJarFile.getAbsolutePath();
        List<String> classJars = getDependence(applicationJarFilePath);
        //开始加载jar包
        try{
            if(jarList.size() > 0){
                //循环按顺序加载
                for(String jarName : jarList){
                    if(validateJarAndVersion(classJars, jarName)){
                        LOGGER.info("开始热加载jar包 ---------------> {}", jarName);
                        ClassPathResource classPathResource = new ClassPathResource(libPath + "/" + jarName);
                        File jar = new File("/temp/" + jarName);
                        FileUtils.copyToFile(classPathResource.getInputStream(), jar);
                        JarFile jarFile = new JarFile(jar);
                        URI uri = jar.toURI();
                        URL url = uri.toURL();
                        //获取classloader
                        URLClassLoader classLoader =URLClassLoaderThread.currentThread().getContextClassLoader();
                        //利用反射获取方法
                        Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                        if(!method.isAccessible()){
                            method.setAccessible(true);
                        }
                        method.invoke(classLoader,url);
                        for(Enumeration<JarEntry> ea = jarFile.entries(); ea.hasMoreElements();){
                            JarEntry jarEntry = ea.nextElement();
                            String name = jarEntry.getName();
                            if(name != null && name.endsWith(".class")){
                                String loadName = name.replace("/",".").substring(0,name.length() -6);
                                //加载class
                                Class<?> c = classLoader.loadClass(loadName);
                                //注册bean
                                insertBean(c, registry);
                            }
                        }
                        LOGGER.info("结束热加载jar包 ---------------> {}", jarName);
                        jar.delete();
                    }else{
                        LOGGER.info("依赖中已存在该jar包 ---------------> {}", jarName);
                    }
                }
            }
        } catch(Exception e){
            LOGGER.error("热加载jar包异常");
            e.printStackTrace();
        }
    }

    private void insertBean(Class<?> c, BeanDefinitionRegistry registry){
        if(isSpringBeanClass(c)){
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(c);
            BeanDefinition beanDefinition = builder.getBeanDefinition();
            registry.registerBeanDefinition(c.getName(),beanDefinition);
        }
    }

    //获取依赖的jar包名称列表
    private List<String> getDependence(String jarPath){
        List<String> result = new LinkedList<>();
        if(!jarPath.endsWith(".jar")){
            return result;
        }
        URL url;
        try{
            url = new URL("jar:file:/" + jarPath + "/");
        } catch(MalformedURLException e){
            e.printStackTrace();
            return result;
        }
        try{
            JarURLConnection connection = (JarURLConnection) url.openConnection();
            JarFile jarFile = connection.getJarFile();
            for(Enumeration<JarEntry> ea = jarFile.entries();ea.hasMoreElements();){
                JarEntry jarEntry = ea.nextElement();
                if(jarEntry.getName().startsWith("/BOOT-INF/lib/") && jarEntry.getName().endsWith(".jar")){
                    result.add(jarEntry.getName().substring((jarEntry.getName().lastIndexOf("/")+ 1)));
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return result;
    }

    //判断是否存在同样的依赖jar包名,TODO:判断版本号高低
    private boolean validateJarAndVersion(List<String> dependenceList, String jarName){
        boolean result = true;
        int firstNumIndex = getFirstNumIndex(jarName);
        String jarSimpleName = jarName.substring(0,firstNumIndex);
        for(String dependence : dependenceList){
            if(dependence.startsWith(jarSimpleName)){
                result = false;
                break;
            }
        }
        return result;
    }

    //获取名字中第一个出现数字的下标
    private int getFirstNumIndex(String jarName){
        Matcher matcher = pattern.matcher(jarName);
        if(matcher.find()){
            return matcher.start();
        }else{
            return -1;
        }
    }

    /**
     * 方法描述 判断class对象是否带有spring的注解
     * @method isSpringBeanClass
     * @param cla jar中的每一个class
     * @return true 是spring bean   false 不是spring bean
     */
    public boolean isSpringBeanClass(Class<?> cla){
        if(cla==null){
            return false;
        }
        //是否是接口
        if(cla.isInterface()){
            return false;
        }
        //是否是抽象类
        if( Modifier.isAbstract(cla.getModifiers())){
            return false;
        }
        if(cla.getAnnotation(Component.class)!=null){
            return true;
        }
        if(cla.getAnnotation(Repository.class)!=null){
            return true;
        }
        if(cla.getAnnotation(Service.class)!=null){
            return true;
        }
        return false;
    }

    @Override
    public void setEnvironment(Environment environment){
        this.libPath = environment.getProperty("libPath");
        this.loadJarNames = environment.getProperty("loadJarNames");
    }

以上代码均为手打,如果错误,请留言指正,谢谢

  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
Spring Boot中,你可以使用以下方式指定某个第三方jar的类加载器: 1. 使用Maven或Gradle等构建工具将第三方jar包打成独立的jar包,然后将其放置在Spring Boot应用程序的classpath中,以便Spring Boot应用程序可以直接使用该jar包。 2. 如果第三方jar包无法打成独立的jar包,你可以使用Spring Boot的自定义ClassLoader来加载该类。你需要自定义一个ClassLoader,并将其设置为Spring Boot应用程序的ClassLoader。例如: ```java public class ThirdPartyClassLoader extends URLClassLoader { public ThirdPartyClassLoader(URL[] urls) { super(urls, Thread.currentThread().getContextClassLoader()); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (name.startsWith("com.example.thirdparty.")) { return super.loadClass(name); } return super.getParent().loadClass(name); } } ``` 然后在Spring Boot应用程序的启动类中,使用以下代码设置ClassLoader: ```java public static void main(String[] args) { URL[] urls = new URL[]{new URL("file:/path/to/third-party.jar")}; ClassLoader classLoader = new ThirdPartyClassLoader(urls); Thread.currentThread().setContextClassLoader(classLoader); SpringApplication.run(Application.class, args); } ``` 在上面的示例中,我们使用`ThirdPartyClassLoader`来加载`com.example.thirdparty`包中的类,其他类则由父ClassLoader加载。你可以根据需要修改`loadClass`方法来自定义ClassLoader的加载策略。
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值