16

我有一个类Formula,位于 packagejavaapplication4中,我使用 URLClassLoader 加载它。但是,当我从位于同一个包中的另一个类调用它时Test1,我无法访问其具有默认访问修饰符的方法(我可以访问公共方法)。

我得到以下异常:

java.lang.IllegalAccessException:类 javaapplication4.Test1 无法使用修饰符“”访问类 javaapplication4.Formula 的成员

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

我想这是使用不同的类加载器的问题,但不知道为什么(我已经设置了 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;
        }
    }
}
4

4 回答 4

9

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

例如,当您测试两个Class<T>对象的相等性时,如果它们具有相同的规范名称但从不同的 ClassLoader 加载,它们将不相等。

对于属于同一个包的两个类(进而能够访问包私有方法),它们也需要从同一个 ClassLoader 加载,这里不是这种情况。实际上Test1是由系统类加载器加载的,而公式是由内部创建的 URLClassLoader 加载的loadClass()

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

我不认为你可以让Formula同一个Test1ClassLoader 加载类(你必须使用众所周知的路径并将其放在 CLASSPATH 上),但我找到了一种相反的方法:加载另一个Test1in实例用于加载公式的 ClassLoader。这是伪代码中的布局:

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。一些注意事项:我null为 URLClassLoader 指定了一个父级以避免上面列出的问题,并且我操纵了字符串来达到这个目的 - 但不知道这种方法在其他部署场景中的鲁棒性如何。此外,我使用的 URLCLassLoader 仅在两个目录中搜索以查找类定义,而不是 CLASSPATH 中列出的所有条目

于 2013-01-11T17:00:35.200 回答
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) 
于 2013-01-11T17:38:30.300 回答
3

我在JVM 规范 5.4.4(强调我的)中找到了解释:

当且仅当以下任一条件为真时,类或接口 D 才能访问字段或方法 R:

  • [...]
  • R 要么是受保护的,要么具有默认访问权限(即既不是公共的,也不是受保护的,也不是私有的),并且由与 D相同的运行时包中的类声明。

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

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

底线:这是预期的行为。

于 2013-01-11T17:47:09.000 回答
1

添加c:\temp到 java 类路径并使用与 Test1.class 相同的 ClassLoader 加载 Formula.class

Class<?> formulaClass = Class.forName(className);

这将解决您的问题。

于 2013-01-12T01:57:30.817 回答