URLClassLoader和包私有方法的可访问性

16

我有一个名为Formula的类,位于包javaapplication4中,我使用URLClassLoader加载它。然而,当我从另一个位于同一包中的类Test1中调用该类的默认访问修饰符的方法时(我可以访问公共方法),我无法访问它们。

我得到以下异常:

java.lang.IllegalAccessException: Class javaapplication4.Test1 can not access a member of class javaapplication4.Formula with modifiers ""

如何访问在运行时从同一包中加载的类的包级私有方法?

我想这可能是使用不同的类加载器导致的问题,但不确定原因(我已经设置了URLClassLoader的父级)。

SSCCE复现问题(Windows路径)- 我认为问题在loadClass方法中:

public class Test1 {

    private static final Path TEMP_PATH = Paths.get("C:/temp/");

    public static void main(String[] args) throws Exception {
        String thisPackage = Test1.class.getPackage().getName();
        String className = thisPackage + ".Formula"; //javaapplication4.Formula
        String body = "package " + thisPackage + ";   "
                    + "public class Formula {         "
                    + "    double calculateFails() {  "
                    + "        return 123;            "
                    + "    }                          "
                    + "    public double calculate() {"
                    + "        return 123;            "
                    + "    }                          "
                    + "}                              ";

        compile(className, body, TEMP_PATH);
        Class<?> formulaClass = loadClass(className, TEMP_PATH);

        Method calculate = formulaClass.getDeclaredMethod("calculate");
        double value = (double) calculate.invoke(formulaClass.newInstance());
        //next line prints 123
        System.out.println("value = " + value);

        Method calculateFails = formulaClass.getDeclaredMethod("calculateFails");
        //next line throws exception:
        double valueFails = (double) calculateFails.invoke(formulaClass.newInstance());
        System.out.println("valueFails = " + valueFails);
    }

    private static Class<?> loadClass(String className, Path path) throws Exception {
        URLClassLoader loader = new URLClassLoader(new URL[]{path.toUri().toURL()}, Test1.class.getClassLoader());
        return loader.loadClass(className);
    }

    private static void compile(String className, String body, Path path) throws Exception {
        List<JavaSourceFromString> sourceCode = Arrays.asList(new JavaSourceFromString(className, body));

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(path.toFile()));
        boolean ok = compiler.getTask(null, fileManager, null, null, null, sourceCode).call();

        System.out.println("compilation ok = " + ok);
    }

    public static class JavaSourceFromString extends SimpleJavaFileObject {
        final String code;

        JavaSourceFromString(String name, String code) {
            super(URI.create("string:///" + name.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension),
                    JavaFileObject.Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }
}

你尝试过使用URLClassLoader.newInstance()吗? (编辑:可能没有区别) - fge
2
你的代码在我的电脑上无法编译,我不得不把所有的 double 改成了 Double (使用的是 Oracle JDK 1.7u10)。 - fge
@fge 谢谢分享。这很奇怪 - 我正在使用 JDK 1.7u9 并且它可以工作(从 Netbeans 编译和运行)。 - assylias
1
抱歉,我的意思不是到处都要修改——只有在.invoke()调用时需要。而且我在同一位置上也会得到相同的异常。 - fge
4个回答

9

运行时的类由其完全限定名称和ClassLoader所标识。

例如,当您比较两个Class<T>对象是否相等时,如果它们具有相同的规范名称但从不同的ClassLoader加载,则它们不会相等。

为了使两个类属于同一包(进而能够访问包私有方法),它们也需要从相同的ClassLoader加载,这在这里并非如此。事实上,Test1由系统ClassLoader加载,而Formula是由loadClass()内部创建的URLClassLoader加载的。

如果您为您的URLClassLoader指定一个父加载器以使其加载Test1,仍然使用了两个不同的加载器(您可以通过断言加载器的相等性来检查)。

我认为您无法使Formula类由相同的Test1 ClassLoader加载(您必须使用一个已知的路径,并将其放在CLASSPATH上),但是我找到了一种方法来做相反的操作:在用于加载公式的ClassLoader中加载另一个Test1实例。以下是伪代码布局:

class Test1 {

  public static void main(String... args) {
    loadClass(formula);
  }

  static void loadClass(location) {
    ClassLoader loader = new ClassLoader();
    Class formula = loader.load(location);
    Class test1 = loader.load(Test1);
    // ...
    Method compute = test1.getMethod("compute");
    compute.invoke(test1, formula);
  }

  static void compute(formula) {
    print formula;
  }
}

这里是pastebin。需要注意的是:我为URLClassLoader指定了一个null父级,以避免上述问题,并操作字符串以实现目的 - 但不知道在其他部署场景中这种方法的可靠性如何。此外,我使用的URLCLassLoader仅在两个目录中搜索类定义,而不是在CLASSPATH中列出的所有条目中搜索。


我该如何在主类加载器中加载我的类(该类在编译时不可用,因此我需要“下载”类文件)? - assylias
@assylias 不知道在那个程序中是否可以,除非TMP_PATH是众所周知的,并且在程序启动之前添加到CLASSPATH中。您可以尝试相反的操作,在新加载器中加载“Test1”(除了不使用包私有成员的平凡解决方案)。 - Raffaele
1
@assylias 如果你在classpath中包含了特定的目录(例如C:/temp/),那么类加载器将会在你尝试加载它时读取该类。至少UrlClassLoader是这样做的。只需确保你按照包结构(javaapplication4)保存在子目录中即可。 - gaborsch

8
答案是:
sun.reflect.Reflection 包中有一个名为 isSameClassPackage 的方法(实际签名为 private static boolean isSameClassPackage(ClassLoader arg0, String arg1, ClassLoader arg2, String arg3);)。该方法负责决定两个类是否属于同一包。
该方法进行的第一个检查是比较 arg0 和 arg2(即两个类加载器),如果它们不同,则返回 false。
因此,如果对这两个类使用不同的类加载器,则它们将无法匹配。
编辑: 完整的调用链(应要求)是:
Method.invoke()
Method.checkAccess() -- fallback to the real check
Method.slowCheckMemberAccess()   -- first thing to do to call
Reflection.ensureMemberAccess()  -- check some nulls, then
Reflection.verifyMemberAccess()  -- if public, it,'s OK, otherwise check further
Reflection.isSameClassPackage(Class, Class) -- get the class loaders of 2 classes
Reflection.isSameClassPackage(ClassLoader, String, ClassLoader, String) 

有趣,但你介意打印“调用链”(即我们如何从 .invoke() 到达这个方法)吗? - fge
除此之外,这不是实现细节,而是语言设计选择。Assylias记录了相关的JLS,因此实际链路并不重要,因为这确实是预期行为。 - Raffaele
@Raffaele 这是真的,但深入了解代码,了解它为什么会以这种方式工作总是很有趣。无论如何,我所描绘的调用链对于我的JDK7是准确的,可能与其他实现有所不同。 - gaborsch
@GaborSch 当然,这总是一件好事。我只是想指出这是预期的行为,顺便说一下,链只是 OP 异常的反向堆栈跟踪。 - Raffaele

3
我在JVM规范5.4.4中找到了解释(重点是我的):

如果满足以下任何一个条件,则字段或方法R对于类或接口D可访问:

  • [...]
  • R是protected或具有默认访问权限(即既不是public也不是protected也不是private),并且由与D位于同一运行时包中的类声明。

而运行时包定义在规范 #5.3中:

类或接口的运行时包由类或接口的包名和定义该类或接口的类加载器确定。

总之,这是期望的行为。


这是我在答案中所说的。然而,我找到了一个解决方案使其工作。 - Raffaele
@Raffaele 是的,我猜我没有将你说的和包(而不是类)也由类加载器定义之间的联系联系起来。感谢你的建议-现在正在检查它。 - assylias

1
c:\temp添加到Java类路径中,并使用与Test1.class相同的ClassLoader加载Formula.class。
Class<?> formulaClass = Class.forName(className);

这将解决您的问题。


谢谢,这最终就是我所做的。 - assylias

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