从类路径中的所有 jar 文件中抽取扩展"Randomizer"的类



主题确实说明了我的目标,但我对实现感到困惑。我有一个程序,它接受不同的对象来扩展我的Randomizer类。我想这样做,这样就可以在类路径中放置一个JAR文件,程序在运行它时会搜索它,并将它添加到主程序中。到目前为止,这是我尝试过的,但当我意识到java.util.jar.JarFile不能给你Classes或Methods时,我停止了。

由于它依赖于它,我不妨提到我的类ArrayPP<T>类似于ArrayList,但有更多的方法。这里显示的addAll方法与add方法类似,但具有多个参数或泛型类型T的对象数组。

  private static Randomizer[] loadExternalRandomizers() throws IOException
  {
    java.io.File classPath = new java.io.File(System.getProperty("user.dir"));
    ArrayPP<Randomizer> r = new ArrayPP<>();
    if (classPath.isDirectory())
    {
      r.addAll(getRandomizersIn(classPath));
    }
    return r.toArray();
  }
  private static Randomizer[] getRandomizersIn(File dir) throws IOException
  {
    ArrayPP<Randomizer> r = new ArrayPP<>();
    java.io.File fs[] = dir.listFiles(new java.io.FileFilter() {
      @Override
      public boolean accept(File pathname)
      {
        return pathname.isDirectory() || pathname.toString().endsWith(".jar");
      }
    });
    java.util.jar.JarFile jr;
    java.util.Enumeration<java.util.jar.JarEntry> entries;
    java.util.jar.JarEntry thisEntry;
    for (java.io.File f : fs)
    {
      if (f.isDirectory())
      {
        r.addAll(getRandomizersIn(f));
        continue;
      }
      jr = new java.util.jar.JarFile(f);
      entries = jr.entries();
      while (entries.hasMoreElements())
      {
        thisEntry = entries.nextElement();
        //if (the jar file contains a class that extends Randomizer
        //  add that class to r
      }
    }
    return r.toArray();
  }

如果有帮助的话,我会在Java7上构建它。我也希望在不使用任何库的情况下做到这一点。

实施解决方案


我尝试过实现Ryan Stewart描述的解决方案,如下所示。我正在使用一个名为BHR2 - Ranger.jar的测试JAR来处理它,它在包bhr2.plugins中包含一个扩展Randomizer的类,称为RangerJAR在其META-INFservices文件夹中包含一个名为bhr2.plugins.Ranger的文件,其中一行读取bhr2.Randomizer # Abstract Randomizer

  private static ArrayPP<Randomizer> loadExternalRandomizers() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException
  {
    java.io.File classPath = new java.io.File(System.getProperty("user.dir"));
    ArrayPP<Randomizer> r = new ArrayPP<>();
    if (classPath.isDirectory())
    {
      r.addAll(getRandomizersIn(classPath));
    }
    return r;
  }
  private static int depth = 0;
  private static ArrayPP<Randomizer> getRandomizersIn(File dir) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException
  {
    ArrayPP<Randomizer> r = new ArrayPP<>();
    java.io.File fs[] = dir.listFiles(new java.io.FileFilter() {
      @Override
      public boolean accept(File pathname)
      {
        return pathname.isDirectory() || pathname.toString().endsWith(".jar");
      }
    });
    for (java.io.File f : fs)
    {
      for (int i=0; i < depth; i++)
        System.out.print("  ");
      System.out.println(f);
      if (f.isDirectory())
      {
        if (depth < 4)
        {
          depth++;
          r.addAll(getRandomizersIn(f));
          depth--;
        }
        else
          System.out.println("Skipping directory due to depth");
        continue;
      }
        java.util.ServiceLoader<Randomizer> sl = ServiceLoader.loadInstalled(Randomizer.class);
        for (Randomizer rand : sl)
        {
          r.add(rand);
          System.out.println("adding " + rand);
        }
    }
    return r == null || r.isEmpty() ? new ArrayPP<Randomizer>() : r;
  }

但当我运行它时,在它开始做其他事情之前,我得到的只是输出:

I:JavaNetBeansProjectsBHRandomizer2nbproject
  I:JavaNetBeansProjectsBHRandomizer2nbprojectprivate
I:JavaNetBeansProjectsBHRandomizer2src
  I:JavaNetBeansProjectsBHRandomizer2srcbhr2
    I:JavaNetBeansProjectsBHRandomizer2srcbhr2resources
    I:JavaNetBeansProjectsBHRandomizer2srcbhr2randomizers
  I:JavaNetBeansProjectsBHRandomizer2srcbht
    I:JavaNetBeansProjectsBHRandomizer2srcbhttest
    I:JavaNetBeansProjectsBHRandomizer2srcbhttools
      I:JavaNetBeansProjectsBHRandomizer2srcbhttoolscomps
        I:JavaNetBeansProjectsBHRandomizer2srcbhttoolscompsgameboard
Skipping directory due to depth
      I:JavaNetBeansProjectsBHRandomizer2srcbhttoolseffects
      I:JavaNetBeansProjectsBHRandomizer2srcbhttoolsmisc
      I:JavaNetBeansProjectsBHRandomizer2srcbhttoolsutilities
    I:JavaNetBeansProjectsBHRandomizer2srcbhtresources
I:JavaNetBeansProjectsBHRandomizer2lib
  I:JavaNetBeansProjectsBHRandomizer2libCopyLibs
    I:JavaNetBeansProjectsBHRandomizer2libCopyLibsorg-netbeans-modules-java-j2seproject-copylibstask.jar
  I:JavaNetBeansProjectsBHRandomizer2libswing-layout
    I:JavaNetBeansProjectsBHRandomizer2libswing-layoutswing-layout-1.0.4.jar
I:JavaNetBeansProjectsBHRandomizer2BHR2 - Ranger
  I:JavaNetBeansProjectsBHRandomizer2BHR2 - RangerMETA-INF
  I:JavaNetBeansProjectsBHRandomizer2BHR2 - Rangerbhr2
    I:JavaNetBeansProjectsBHRandomizer2BHR2 - Rangerbhr2plugins
I:JavaNetBeansProjectsBHRandomizer2build
  I:JavaNetBeansProjectsBHRandomizer2buildclasses
    I:JavaNetBeansProjectsBHRandomizer2buildclassesbhr2
      I:JavaNetBeansProjectsBHRandomizer2buildclassesbhr2randomizers
      I:JavaNetBeansProjectsBHRandomizer2buildclassesbhr2resources
    I:JavaNetBeansProjectsBHRandomizer2buildclassesbht
      I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttools
        I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttoolscomps
Skipping directory due to depth
        I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttoolsutilities
Skipping directory due to depth
        I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttoolseffects
Skipping directory due to depth
        I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttoolsmisc
Skipping directory due to depth
      I:JavaNetBeansProjectsBHRandomizer2buildclassesbhtresources
      I:JavaNetBeansProjectsBHRandomizer2buildclassesbhttest
    I:JavaNetBeansProjectsBHRandomizer2buildclassesMETA-INF
  I:JavaNetBeansProjectsBHRandomizer2buildempty
  I:JavaNetBeansProjectsBHRandomizer2buildgenerated-sources
    I:JavaNetBeansProjectsBHRandomizer2buildgenerated-sourcesap-source-output
I:JavaNetBeansProjectsBHRandomizer2dist
  I:JavaNetBeansProjectsBHRandomizer2distBHRandomizer2.jar
I:JavaNetBeansProjectsBHRandomizer2BHR2 - Ranger.jar

这个问题的一个典型解决方案是构建jar文件,该文件在META-INF中有一个文件,"告诉"您的程序从该jar加载什么类。例如,Spring自定义名称空间处理程序和JDBC驱动程序就是以这种方式加载的。

与其弄清楚如何真正扫描一个jar中的所有类(没有人这么做,所以我认为这是不可能/不可行的(,不如让代码在类路径上的每个jar中查找一个特定的文件,该文件列出了它所包含的Randomizer实现。例如,期望一个名为META-INF/randomizers.list的文件有一个类名列表,每行一个,这些类名是实现Randomizer的jar中的类。读取文件,对于每一行,使用Class.forName((按名称加载类,然后使用newInstance((实例化它。

编辑:用于从类路径中的任何位置加载"列表"文件:

Enumeration<URL> resources = getClassLoader().getResources(
    "/META-INF/randomizers.list");
while (resources.hasMoreElements()) {
    URL url = resources.nextElement();
    // Load the Randomizer(s) specified in this file
}

编辑:所以JDK公开了它用于这类事情的机制,这是我以前不知道的。只需使用ServiceLoader。文档解释了如何使用它,我也编写了一个如何使用它的示例。你可以在github上找到代码,也可以简单地克隆并自己运行:

git clone git://github.com/zzantozz/testbed.git tmp
cd tmp
mvn install -pl serviceloader-example/service-usage -am
mvn -q exec:java -D exec.mainClass=rds.serviceloader.ServiceLoaderExample -pl serviceloader-example/service-usage

该示例由五个模块组成:一个模块定义服务接口,三个模块定义单独的服务实现,一个模块使用ServiceLoader加载实现。最后一个被称为"服务使用",它演示了如何使用ServiceLoader类来加载其他三个模块/jar中定义的三个服务,这些模块/jar实现了第一个模块/jar中定义的接口。

编辑:由于您在示例项目中似乎遇到了问题,以下是基本信息。

  1. 每个包含一个或多个Randomizer实现的jar文件都应该包含一个名为META-INF/services/com.foo.Randomizer的文件(其中com.foo是您的包名(
  2. 这个文件应该包含一个类名列表,每行一个,每个类名都是Randomizer的实现
  3. 有了这些文件,你所要做的就是获得所有Randomizer实例

    ServiceLoader<Randomizer> loader = ServiceLoader.load(Randomizer.class);
    for (Randomizer randomizer : loader) {
        randomizer.doWhatever();
    }
    

从Manifest中获取类列表当然是更好的解决方案。但如果你不希望罐子里包含这样的信息,你可以回到这个:

  private static void scanClasses(File file) throws MalformedURLException, IOException {
    ClassLoader classLoader = new URLClassLoader(new URL[]{ file.toURI().toURL() });
    JarFile jar = new JarFile(file.getAbsoluteFile());
    Enumeration<JarEntry> jarEntries = jar.entries();
    while (jarEntries.hasMoreElements()) {
      JarEntry je = jarEntries.nextElement();
      if (je.getName().endsWith(".class")) {
        String clazzName = je.getName();
        clazzName = clazzName.substring(0, clazzName.length() - ".class".length()).replaceAll("/", ".");
        clazzName = clazzName.replaceAll("/", ".");
        try {
          Class clazz = Class.forName(clazzName, false, classLoader);
          if (Randomizer.class.isAssignableFrom(clazz)) {
            System.out.println("Found Randomizer: " + clazz);
          }
        } catch (ClassNotFoundException ex) {
          // this really should not happen, 
          // since we *know* the class exists
          throw new AssertionError(ex.getMessage());
        }
      }
    }
  }

这里是我用来检索属于一个包和任何子包的所有类的代码。您只需要在返回类的列表中搜索那些使用反射实现Randomizer的类。

/**
 * Scans all classes accessible from the context class loader which belong
 * to the given package and subpackages.
 * <p>
 * Inspired from post on: {@code http://snippets.dzone.com/posts/show/4831}
 */
public static List<Class<?>> getClasses(String packageName)
        throws ClassNotFoundException, IOException {
    // Retrieving current class loader
    final ClassLoader classLoader
            = Thread.currentThread().getContextClassLoader();
    // Computing path from the package name
    final String path = packageName.replace('.', '/');
    // Retrieving all accessible resources
    final Enumeration<URL> resources = classLoader.getResources(path);
    final List<File> dirs = new ArrayList<File>();
    while (resources.hasMoreElements()) {
        final URL resource = resources.nextElement();
        final String fileName = resource.getFile();
        final String fileNameDecoded = URLDecoder.decode(fileName, "UTF-8");
        dirs.add(new File(fileNameDecoded));
    }
    // Preparing result
    final ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
    // Processing each resource recursively
    for (File directory : dirs) {
        classes.addAll(findClasses(directory, packageName));
    }
    // Returning result
    return classes;
}
/**
 * Recursive method used to find all classes in a given directory and
 * subdirs.
 */
public static List<Class<?>> findClasses(File directory, String packageName)
        throws ClassNotFoundException {
    // Preparing result
    final List<Class<?>> classes = new ArrayList<Class<?>>();
    if ( directory == null )
        return classes;
    if (!directory.exists())
        return classes;
    // Retrieving the files in the directory
    final File[] files = directory.listFiles();
    for (File file : files) {
        final String fileName = file.getName();
        // Do we need to go recursive?
        if (file.isDirectory()) {
            classes.addAll(findClasses(file, packageName + "." + fileName));
        } else if (fileName.endsWith(".class") && !fileName.contains("$")) {
            Class<?> _class;
                _class = Class.forName(packageName + '.'
                        + fileName.substring(0, fileName.length() - 6));
            classes.add(_class);
        }
    }
    return classes;
}

我认为您应该坚持使用java ServiceLoader。那你就不需要自己摆弄罐子了。这里有一个很好的教程:http://java.sun.com/developer/technicalArticles/javase/extensible/index.html

如果您正在开发一个可扩展的桌面应用程序,那么netbeans平台可能是适合您的解决方案。它有一个更高的学习曲线,但它是非常值得的。http://netbeans.org/features/platform/index.html

最新更新