本文主要介绍使用springboot动态加载类、jar包,这些类和jar包不在classpath下,而是在磁盘的某个位置。
之前接触过Solr,而Solr提供的插件式开发方式相当灵活,Solr对开发者提供了一个核心api jar包,开发者如果想扩展Solr某一项功能比如 中文分词,只需要继承Solr提供的分词接口添加自己的实现,然后把自己的分词jar包拷贝到Solr指定目录,并在solr配置文件中配置,重启即可生效。
类似在使用springboot开发过程中,也有类似的需求,比如做一个业务功能卖给其他公司客户,不同的客户可能需求不一样,如果把这些区别写在代码中,不用说也知道有问题,这个时候,通常的做法是,先为这些业务功能提供一套接口,再针对不同客户使用接口的不同实现,最终将实现打成jar包,放到服务器的某个目录,springboot系统在启动的时候,去动态加载jar包,以这种灵活的方式来实现业务插件式的开发。
我们既可以在系统启动的时候去动态加载jar包,也可以在系统启动好后,通过访问某个url地址去动态加载jar包。
假设有3个jar包,一个是接口api(fundta-test-api),一个是接口具体的业务实现(test-imp),一个是springboot启动jar包(fundta-web),fundta-test-api包是fundta-web classpath下的一个项目。
依赖关系如下:
fundta-web依赖fundta-test-api;
test-imp依赖fundta-test-api;
fundta-test-api代码如下:
test-imp代码如下:
将fundta-test-api和test-imp分别打成jar包。
fundta-web包相关代码介绍
动态加载类信息
如果是加载classpath下的class文件或者jar包,则很简单,但加载非classpath下本地路径class文件或jar包,则相对复杂些,具体代码如下:
/** * 从本地磁盘的某个路径上加载类, 如果是class文件: * filePath路径应该为class文件包名的上一级,如D:\\workspace\\classes\\com\\test\\helloworld.class,那么filePath则应该是D:\\workspace\\classes * 如果是jar包: * 则是jar包所在目录,如D:\\workspace\\classes\\helloword.jar,那么filePath则应该为D:\\workspace\\classes * */ public static List<Class<?>> loadClass(String filePath, ClassLoader beanClassLoader) { List<Class<?>> classList = new ArrayList<>(); File file = new File(filePath); if (file.exists() && file.isDirectory()) { Stack<File> stack = new Stack<>(); stack.push(file); while (!stack.isEmpty()) { File path = stack.pop(); // 只需要class文件或者jar包 File[] classFiles = path.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.isDirectory() || pathname.getName().endsWith(".class") || pathname.getName().endsWith(".jar"); } }); for (File subFile : classFiles) { if (subFile.isDirectory()) { // 如果是目录,则加入栈中 stack.push(subFile); } else { URL url = null; JarFile jar = null; Method method = null; Boolean accessible = null; String className = subFile.getAbsolutePath(); try { // 反射并调用URLClassLoader的addURL方法 URLClassLoader classLoader = (URLClassLoader) beanClassLoader; method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); accessible = method.isAccessible(); if (accessible == false) { method.setAccessible(true); } if (className.endsWith(".class")) { // 一次性加载目录下的所有class文件 这里一定不要写成url = subFile.toURI().toURL(); url = file.toURI().toURL(); method.invoke(classLoader, url); // 拼类名,并进行类型加载 int clazzPathLen = file.getAbsolutePath().length() + 1; className = className.substring(clazzPathLen, className.length() - 6); className = className.replace(File.separatorChar, '.'); classList.add(classLoader.loadClass(className)); } else if (className.endsWith(".jar")) { // 如果是jar包,加载该jar包 url = subFile.toURI().toURL(); method.invoke(classLoader, url); // 获取jar jar = new JarFile(new File(className)); // 从此jar包 得到一个枚举类 Enumeration<JarEntry> entries = jar.entries(); // 同样的进行循环迭代 while (entries.hasMoreElements()) { // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); String name = entry.getName(); // 如果是以/开头的 if (name.charAt(0) == '/') { // 获取后面的字符串 name = name.substring(1); } name = name.replace(File.separatorChar, '.'); // 获取class文件 if (name.endsWith(".class") && !entry.isDirectory()) { String className1 = name.substring(0, name.length() - 6); // 添加到classes classList.add(classLoader.loadClass(className1)); } } } } catch (Exception e) { logger.error(e.getMessage()); throw new RuntimeException(e.getMessage()); } finally { if (null != jar) { try { jar.close(); } catch (IOException e) { logger.error(e.getMessage()); throw new RuntimeException(e.getMessage()); } } if (null != method && null != accessible) { method.setAccessible(accessible); } } } } } } return classList; } |
该方法会加载某个本地目录下的jar包或者class文件,并返回加载到的类集合。
springboot启动后动态加载bean
这里需要注意,如果在springboot启动完成后,动态加载bean的话,代码如下:
其中SpringBeanUtil代码如下:
这样,我们就可以将test-imp包放到D:/classtest目录下,并点击url访问系统,完成bean的动态加载。
springboot启动时动态加载bean
这里我们可能需要使用springboot的初始化器或者监听器,让springboot在启动的某个阶段进行动态加载。这里我以spring初始化器为例子进行说明,代码如下:
需要注意的是,可能我们在代码中会依赖到动态加载的bean,因此尽量让动态加载的bean在springboot启动的前期阶段进行。这样springboot在启动的时候,就会去加载D:/classtest下的test-imp包或其他class文件。
如何使用动态加载的bean?
这样就实现了我们所要的功能。不重启服务,又能增加业务逻辑;或者让springboot启动时,加载特殊业务逻辑。