Spring boot 业务插件开发

时间:2024-03-24 07:51:32

本文主要介绍使用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代码如下:

Spring boot 业务插件开发

test-imp代码如下:

Spring boot 业务插件开发

将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的话,代码如下:

Spring boot 业务插件开发

其中SpringBeanUtil代码如下:

Spring boot 业务插件开发

这样,我们就可以将test-imp包放到D:/classtest目录下,并点击url访问系统,完成bean的动态加载。

springboot启动时动态加载bean

这里我们可能需要使用springboot的初始化器或者监听器,让springboot在启动的某个阶段进行动态加载。这里我以spring初始化器为例子进行说明,代码如下:

Spring boot 业务插件开发

需要注意的是,可能我们在代码中会依赖到动态加载的bean,因此尽量让动态加载的bean在springboot启动的前期阶段进行。这样springboot在启动的时候,就会去加载D:/classtest下的test-imp包或其他class文件。

如何使用动态加载的bean?

Spring boot 业务插件开发

这样就实现了我们所要的功能。不重启服务,又能增加业务逻辑;或者让springboot启动时,加载特殊业务逻辑。