如何设置上下文类加载器的类路径以进行运行时编译?

5

我希望在Weblogic 10.3服务器上运行时编译和加载新的类。类加载似乎有点简单:

class ClassFileManager 
extends ForwardingJavaFileManager<StandardJavaFileManager> {

  Map<String, JavaClassObject> classes = new HashMap<String, JavaClassObject>();

  public ClassFileManager(StandardJavaFileManager standardManager) {
    super(standardManager);
  }

  @Override
  public ClassLoader getClassLoader(Location location) {
    return new SecureClassLoader(currentThread().getContextClassLoader()) {
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = classes.get(name).getBytes();
        return super.defineClass(name, b, 0, b.length);
      }
    };
  }

  @Override
  public JavaFileObject getJavaFileForOutput(
      Location location, String className, Kind kind, FileObject sibling)
      throws IOException {
    JavaClassObject result = new JavaClassObject(className, kind);
    classes.put(className, result);
    return result;
  }
}

似乎最简单的进行类加载的方法是初始化一个 SecureClassLoader 并将其使用 contextClassLoader 作为父级。
但是,在为JDK的运行时编译器设置 -classpath 选项时,我似乎找不到以字符串形式表示的 "context classpath"。下面这个方法有点取巧,但足够好用:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
ClassFileManager fileManager = 
    new ClassFileManager(compiler.getStandardFileManager(null, null, null));
List<String> options = new ArrayList<String>();
options.add("-classpath");
options.add(System.getProperty("java.class.path") + ";" +
    getClass().getProtectionDomain()
              .getCodeSource().getLocation()
              .toURI().toString()
              .replace("file:/", "").replace("/", "\\"));

但它无法生成上下文类加载器的完整类路径。我怎样才能可靠地做到这一点?我能吗?


1
有几个问题,哈希映射表应该是某种并发类型,并且类似于 if (classes.get(name)==null) return super.findClass(name); 的检查应该在方法的开头。此外,让方法返回一个新的ClassLoader非常可能是错误的(因为您希望所有文件都只有一个加载器)。 - bestsss
为什么你需要向编译器传递一个类路径呢?如果你向编译器传递了一个委托给上下文类加载器的类加载器,那么这不应该已经使得上下文中的所有类都可以被解析了吗? - Peter G
我手头没有WebLogic,也不太熟悉它,所以无法保证它是否会返回您的WebLogic设置所期望的路径,但是使用ClassLoader#getResources()并传递一个空字符串将为您提供所有类路径资源的URL枚举。另请参见https://dev59.com/d3A75IYBdhLWcg3wkZ-A#3223019等。 - BalusC
@BalusC:嗯,我确实遇到过这种方法,但在简单检查中,它似乎没有提供应列出的所有资源的完整列表...不过我会再次检查的。 - Lukas Eder
@LukasEder 你将 fileManager 传递给了 getTask,是吗?虽然你已经重写了它的 getClassLoader 方法,但也许它正在委托错误的类加载器。 - Peter G
显示剩余4条评论
5个回答

2

WebLogic 10.3.6 的 ClassLoader 实现相当复杂。幸运的是,用于 web 应用程序的类加载器暴露了一个 getClassPath 方法。

ClassLoader cl = Thread.currentThread().getContextClassLoader();
String classPath = ((weblogic.utils.classloaders.GenericClassLoader)cl).getClassPath();

// Once we have a classpath it's standard procedure
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager sfm = compiler.getStandardFileManager(null, null, null);
List<String> optionList = new ArrayList<String>();
optionList.addAll(Arrays.asList("-classpath", classPath));
compiler.getTask(null, sfm, null, optionList, null, sources).call();

嗯,这将会出奇的简单!我得再检查一下,看看我的 WLS ClassLoader 实例是否有一个 getClassPath() 方法。如果有的话,我会为自己的疏忽感到惋惜;-) - Lukas Eder
太好了!这个东西列出了当前类路径上的所有内容。 - Lukas Eder

1
也许这可以帮助您。它在我的WebLogic项目中有效。
String getClassPath() {
    final String BASE_PATH = "<your_project_folder_name>";
    String path = "";

    String classPathProperty = System.getProperty("java.class.path");
    if (classPathProperty != null) {
        path = classPathProperty + File.pathSeparator;
    }

    URL classLocation = this.getClass().getProtectionDomain().getCodeSource().getLocation();
    URL classesLocation = this.getClass().getClassLoader().getResource("/");
    if (classesLocation == null) {
        path = path + classLocation.getPath();
    }
    else {
        String classesLocationPath = classesLocation.getPath();
        String libsLocationPath = classesLocationPath + "../lib";
        File libsLocation = new File(libsLocationPath);
        if (libsLocation.exists() == false) {
            libsLocationPath = URLDecoder.decode(classesLocationPath + "../" + BASE_PATH + "/WEB-INF/lib/");
            libsLocation = new File(libsLocationPath);
        }

        File[] filesInLibraryPath = libsLocation.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        });
        if (filesInLibraryPath != null) {
            for (File libraryFile : filesInLibraryPath) {
                libsLocationPath += File.pathSeparator + URLDecoder.decode(libraryFile.getAbsolutePath());
            }
        }
        path =  path +
                classLocation.getPath() + File.pathSeparator + 
                classesLocationPath + File.pathSeparator + 
                libsLocationPath;
        path = URLDecoder.decode(path);
    }
    return path;
}

是的,我们实际上在Weblogic项目中也尝试过这样做。它似乎可以工作,但非常不可靠。它实际上依赖于我们所有.war文件包含的.jars都有效地包含在相同的临时目录中这一事实。此外,我们无法访问.war文件类路径上的所有其他对象,例如认可的目录等等。因此,如果有什么的话,这真的是一个hack :-) - Lukas Eder

1
Tomcat使用开源Jasper JSP编译器查询上下文URLClassLoader以生成传递给编译器的类路径字符串。
如果WebLogic没有公开getURLs方法,则可以使用JavaFileManager的自定义实现,该实现使用上下文类加载器getResource()方法来获取类文件作为替代方法。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager customFileManager = ...
compiler.getTask(null, customFileManager, null, null, null, sources).call();

这里有一个完整的示例,点击此处查看。


不幸的是,我认为WLS没有公开URLClassLoader(请参见此答案)。然而,使用JavaFileManager是一个有趣的想法。你能提供一个例子来详细说明吗? - Lukas Eder

1
我建议您让自定义的JavaFileManager意识到上下文类加载器,并将其作为参数指定给JavaCompiler.getTask,按照@anttix的想法进行操作。
有关更多信息和包括解释(太冗长以至于无法在此重复)的示例实现,请参见博客文章使用具有自定义类加载器的内置JavaCompiler

谢谢您的建议。幸运的是,最终找到了一个更强大且直接的解决方案。 - Lukas Eder
非常好,您选择的解决方案看起来很简单 - 但是针对WebLogic。如果干净地实现,我的建议可能在任何容器中都能工作。这就是所谓的健壮性。 :-) - kriegaex
这是一份在银行的承包职位。他们过去30年一直使用WLS。他们将继续在未来50年内使用它。公平地说,我已经通过在问题上放置[标签:Weblogic]标记来警告你;-)但同意。健壮性取决于上下文 :-) - Lukas Eder
对我来说没问题。也许两年后,当他们意外切换他们的AS时,你可以通过改进代码的那部分来赚取额外的收入。;-) 作为敏捷教练和干净代码的支持者,我听到过这种论点(“它一直是这样,永远不会改变”)太多次了,并且看到它改变得太多,以至于不能再依赖它。我帮助过数十个Scrum团队通过提醒他们客户(特别是那些没有技术知识的客户)可能会出乎意料地改变主意,从而拯救了他们自己的屁股,在很多情况下他们确实改变了,而团队总是感到惊讶,但是安全的。 - kriegaex

0

由于"WebApplicationClassLoader"是一种"URLClassLoader",也许您可以使用此代码片段。

ClassLoader classLoader = getClass().getClassLoader();
System.out.println("ClassLoader: " + classLoader);
URLClassLoader urlClassLoader = (URLClassLoader)classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL u : urls) {
    System.out.println("url: " + u);
}

这段代码列出了类路径中的所有JAR包和目录。

不幸的是,在我们的安装中,这将会给你一个ClassCastException - Lukas Eder
1
@LukasEder 我可以这样做,但我下载了Weblogic并发布了另一个答案,提供了一种更简单的方法。问题出在接口中的列表方法上。ClassLoader没有提供列出类的方法,因此您必须使用第三方Reflections库来枚举类。需要相当大量的代码来实现它。 - anttix
这是关于Reflections库使用的参考链接:https://dev59.com/73E85IYBdhLWcg3w6oB0 - anttix
为了参考,我们的ClassLoader是一个ChangeAwareClassLoader,所以这个解决方案不起作用,很遗憾。@anttix:不错的技巧,但我更喜欢你的另一个答案;-) - Lukas Eder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接