记一次加载外部jar遇到的问题

young 147 2024-11-12

项目中有一个需求,需要在上传文件的时候,判断文件后缀与文件实际类型是否一致,防止恶意篡改的情况。

hutool的工具包中,提供了FileTypeUtil的工具类,用于判断一些常见文件的魔法值(File Magic),其实现是通过检测二进制中的指定字节,判断是否是否为某一个类型的问题。

在测试的过程中,发现docx,xlsx,pptx这种文件,通过工具类检测出来的类型是zip,在file signatures网站上,找到了关于此类文件的判断方案,将其用压缩包打开,然后找到里面的[Content_Types].xml文件,检查里面是否有指定的字符串。

考虑到后期可能还有vsdx、ofd等hutool不支持的类型需要进行校验,但是不能每次加一个类型就停机部署一次,于是考虑采用动态加载外部jar的方式进行实现。

大概步骤如下:

  • 创建一个新的maven模块filetype-match,里面定义一个接口
public interface FileTypeMatch {
  	/**
  	 * 匹配的文件后缀
  	 */
    List<String> extension();
		/**
		 * 根据文件流进行判断是否匹配
		 */
    boolean match(InputStream inputStream);
}
  • 创建一个maven模块filetype-check,在此模块中进行文件类型与后缀的校验工作。

  • 校验流程大概就是就是如果hutool内置的工具不能校验,则采用自定义的匹配器进行校验。

  • 监控指定的目录,根据其中jar包的变动,来动态的修改支持的文件类型,考虑到每个jar包中实现类的全路径类名不太方便获得,于是考虑采用SPI机制进行实现类的获取。

目录的监听,采用hutool提供的Watch工具,同时采用Map来维护自定义Jar的新增和移除操作,具体不在此进行赘述。

第一版加载类实现如下

private void loadJar(String filePath) {
  try {
      log.info("load jar from path:{}", filePath);
      try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://"+filePath)})){
          ServiceLoader<FileTypeMatch> load = ServiceLoader.load(FileTypeMatch.class, urlClassLoader);
          Iterator<FileTypeMatch> iterator = load.iterator();
          while (iterator.hasNext()) {
              FileTypeMatch next = iterator.next();
              log.info("registry class {}", next.getClass());
              fileTypeMatcher.registry(next);
              List<FileTypeMatch> fileTypeMatches = filePathAndInstanceMapping.computeIfAbsent(filePath, k -> new ArrayList<>());
              synchronized (FileTypeMatchJarWatch.class) {
                  fileTypeMatches.add(next);
              }
          }
      }
  } catch (Exception e) {
      log.error("load {} error", filePath, e);
  }
}

采用URLClassLoader,通过file协议,将外部jar加载到JVM中,再通过SPI机制加载具体的接口实现类,并注册到匹配器中去。

存在的问题:本人的个人电脑是Mac系统,通过IDEA进行单元测试没有问题,于是就提交了,使用Windows系统的开发同学拉取代码之后,发现无法进行对扩展的类型进行校验。

经过断点检查,发现ServiceLoader的迭代器中没有元素。怀疑是不同系统中file协议写法的问题,于是修改代码

第二版代码

private void loadJar(String filePath) {
  try {
      log.info("load jar from path:{}", filePath);
      URL fileUrl = new File(filePath).toURI().toURL();
      try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{fileUrl})){
          ServiceLoader<FileTypeMatch> load = ServiceLoader.load(FileTypeMatch.class, urlClassLoader);
          Iterator<FileTypeMatch> iterator = load.iterator();
          while (iterator.hasNext()) {
              FileTypeMatch next = iterator.next();
              log.info("registry class {}", next.getClass());
              fileTypeMatcher.registry(next);
              List<FileTypeMatch> fileTypeMatches = filePathAndInstanceMapping.computeIfAbsent(filePath, k -> new ArrayList<>());
              synchronized (FileTypeMatchJarWatch.class) {
                  fileTypeMatches.add(next);
              }
          }
      }
  } catch (Exception e) {
      log.error("load {} error", filePath, e);
  }
}

修改之后的代码,URL直接通过File类进行转换,由于File的toURL()方法标记为了过期,所以采用先转换成URI,在由URI转换为URL的方式的进行。

存在的问题:改完之后,使用windows系统进行开发的同学也在本地进行了单元测试,发现没有问题,于是提交到测试环境准备进行测试,但是在打包发布之后,应用却无法启动,一直在抛出ClassNotfoundException,提示找不到FileTypeMatch的class,经检查,发现相关的Class都已经在jar包中,并且外部jar中涉及到SPI部分的路径也没有任何问题。

最后发现是URLClassLoader没有指定parent导致的,于是加上parent之后,代码成功运行。

第三版代码

private void loadJar(String filePath) {
  try {
      log.info("load jar from path:{}", filePath);
      File file = new File(filePath);
      URL fileUrl = file.toURI().toURL();
      log.info("fileUrl:{}", fileUrl);
      try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{fileUrl},FileTypeMatch.class.getClassLoader())){
          ServiceLoader<FileTypeMatch> load = ServiceLoader.load(FileTypeMatch.class, urlClassLoader);
          Iterator<FileTypeMatch> iterator = load.iterator();
          while (iterator.hasNext()) {
              FileTypeMatch next = iterator.next();
              log.info("registry class {}", next.getClass());
              fileTypeMatcher.registry(next);
              List<FileTypeMatch> fileTypeMatches = filePathAndInstanceMapping.computeIfAbsent(filePath, k -> new ArrayList<>());
              synchronized (FileTypeMatchJarWatch.class) {
                  fileTypeMatches.add(next);
              }
          }
      }
  } catch (Exception e) {
      log.error("load {} error", filePath, e);
  }
}

在这个需求的开发过程中,没有考虑到不同操作系统的文件协议之间的差异,而且对类加载器的了解不够深,导致了这些问题,记录下来避免再次犯错。