2013-01-11 19 views
14

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

我得到以下异常:

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; 
     } 
    } 
} 
+0

您是否尝试过使用'URLClassLoader.newInstance()'? (编辑:可能没有什么区别) – fge

+0

@fge同样的行为 – assylias

+2

你的代码没有为我编译,我不得不在每个地方都把'double'换成'Double'(Oracle JDK 1.7u10) – fge

回答

6

在运行时类是由它的全名及其ClassLoader的这两个标识。

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

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

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

我不认为你可以使Formula类加载相同的Test1类加载器(你必须使用一个众所周知的路径,并把它放在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中列出的条目

+0

我怎样才能加载我的类在主类加载器(类是在编译时不可用,所以我需要“下载”类文件)? – assylias

+0

@assylias不知道你是否可以在那个程序中,除非TMP_PATH是众所周知的,并在程序启动之前添加到CLASSPATH中。你可以尝试相反的方法,在新加载器中加载'Test1'(在不使用包私有成员的简单解决方案旁边) – Raffaele

+1

@assylias如果你在你的目录中包含某个目录(例如C:/ temp /)类路径,类加载器将在您尝试加载时读取该类。至少UrlClassLoader这样做。只要确保根据包结构(javaapplication4)保存在一个子目录中。 – gaborsch

5

答案是:

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) 
+0

有趣,但你会介意打印“调用链”(即,我们如何从'.invoke()'得到这个方法)? – fge

+0

除了这不是实现细节,而是语言设计选择。Assylias写下了相关的JLS,所以实际的链是无关紧要的,因为它的确是预期的行为 – Raffaele

+0

@Raffaele确实如此,但深入研究代码总是很有趣,要知道为什么它正以这种方式工作。无论如何,我勾画的调用链对我的JDK7来说是准确的,并且可能与其他实现不同。 – gaborsch

2

我发现,在JVM specification 5.4.4(重点煤矿)的解释:

的字段或方法R是可访问类别或接口D当且仅当满足以下任一条件时:

  • R是保护或具有默认访问(即,既不公开,也不保护非私有的),并且是由类在同一个运行时包声明为D.

和运行时包在specs #5.3定义:

一个类或接口的运行时包由包名称并限定类加载器的类或接口的确定。

底线:这是预期的行为。

+0

这就是我在我的回答中所说的。不过,我发现[解决方案](http://pastebin.com/eN5DyBrC)使其工作。 – Raffaele

+0

@Raffaele是的,我想我没有把你说的和类包装器(而不是类)定义的事实联系起来。感谢您的建议 - 现在检查它。 – assylias

1

添加c:\temp到Java类路径和负载Formula.class具有相同的类加载器Test1.class

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

这将解决你的问题。

+0

谢谢,这是我最后做的。 – assylias