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

2年前 (2022) 程序员胖胖胖虎阿
782 0 0

背景

本文章主要解决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包
SpringBoot第十二篇:热加载第三方jar包(解决嵌套jar读取、加载、动态配置、bean注册、依赖等问题),及其精髓
又或者在你本地maven依赖库中,对应版本的目录下会有一个.pom文件,里面也说明了依赖库有哪些
SpringBoot第十二篇:热加载第三方jar包(解决嵌套jar读取、加载、动态配置、bean注册、依赖等问题),及其精髓
注意:看第三方库的依赖,注意<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");
    }

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

相关文章

暂无评论

暂无评论...