16

I have a class Formula, located in package javaapplication4, which I load with a URLClassLoader. However, when I call it from another class Test1, located in the same package, I can't access its methods that have a default access modifier (I can access public methods).

I get the following exception:

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

How can I access package-private methods of a class loaded at runtime from the same package?

I suppose it is a problem with using a different class loader, but not sure why (I have set the parent of the URLClassLoader).

SSCCE reproducing the issue (Windows paths) - I suppose the issue is in the loadClass method:

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;
        }
    }
}
assylias
  • 321,522
  • 82
  • 660
  • 783
  • Have you tried using `URLClassLoader.newInstance()`? (edit: probably does not make a difference) – fge Jan 11 '13 at 17:08
  • 2
    Your code didn't compile for me, I had to change `double` to `Double` everywhere (Oracle JDK 1.7u10) – fge Jan 11 '13 at 17:23
  • @fge Thanks for sharing. That is weird - I'm using JDK 1.7u9 and it works (compiled and run from Netbeans). – assylias Jan 11 '13 at 17:25
  • 1
    (Sorry, I didn't mean everywhere -- only on `.invoke()` calls. And I do obtain the same exception at the same spot) – fge Jan 11 '13 at 17:38

4 Answers4

9

A class at runtime is identified by both its fully qualified name and its ClassLoader.

For example, when you test two Class<T> objects for equality, if they have the same canonical name but were loaded from different ClassLoaders, they won't be equal.

For two classes to belong to the same package (and in turn being able to access package-private methods), they need to be loaded from the same ClassLoader too, which is not the case here. In fact Test1 is loaded by the system classloader, while the Formula is loaded by the URLClassLoader created inside loadClass().

If you specify a parent loader for your URLClassLoader in order to make it load Test1, still two different loaders are used (you can check it by asserting loaders equality).

I don't think you can make the Formula class loaded by the same Test1 ClassLoader (you'd have to use a well-known path and put it on the CLASSPATH), but I found a way to do the opposite: loading another instance of Test1 in the ClassLoader used for loading the formula. This is the layout in pseudocode:

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

Here is the pastebin. A couple of notes: I specifed a null parent for the URLClassLoader to avoid the issue listed above, and I manipulated strings to achieve the purpose - but don't know how robust this approach can be in other deployment scenarios. Also, the URLCLassLoader I used only searches in two directories to find class definitions, not all the entries listed in the CLASSPATH

Raffaele
  • 20,627
  • 6
  • 47
  • 86
  • How could I load my class in the main class loader (the class is not available at compile time so I need to "download" the class file)? – assylias Jan 11 '13 at 17:01
  • @assylias Don't know if you can in that program, unless the TMP_PATH is well-known and added to the CLASSPATH before the program starts. You may try the opposite, loading `Test1` in the new loader (beside the trivial solution of not using package-private members) – Raffaele Jan 11 '13 at 17:10
  • 1
    @assylias It may be enough if you include a certain directory (e.g. C:/temp/) on your classpath, the classloader will read the class just then when you try to load it. At least UrlClassLoader does this. Just make sure that you save in a subdirectory according to the package structure (javaapplication4). – gaborsch Jan 11 '13 at 17:54
8

The answer is:

In the sun.reflect.Reflection package there is a method called isSameClassPackage (the actual signature is private static boolean isSameClassPackage(ClassLoader arg0, String arg1, ClassLoader arg2, String arg3);). This method is responsible for deciding whether two classes belong to the same package or not.

The first check this method is doing that it compares arg0 and arg2, (the two classloaders) if they are different, it returns false.

So, if you use different classloaders for the two classes, it will not match.

EDIT: The full call chain (on request) is:

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) 
gaborsch
  • 15,408
  • 6
  • 37
  • 48
  • Interesting, but would you mind printing the "call chain" (ie, how do we get from `.invoke()` to this method)? – fge Jan 11 '13 at 17:57
  • Except that this is not an implementation detail, but a language design choice. Assylias wrote down the relevant JLS, so the actual chain is irrelevant since it's indeed expected behavior – Raffaele Jan 11 '13 at 18:23
  • @Raffaele That's true, but it is always interesting to dig into the code, to know why exactly it is working that way. Anyway, the call chain I sketched on was accurate for my JDK7, and may differ from other implementations. – gaborsch Jan 11 '13 at 19:47
  • @GaborSch Sure, it's always a good thing. I just wanted to point out that it's expected behavior, and btw the chain is just the reversed stack trace of the OP exception – Raffaele Jan 11 '13 at 19:58
3

I found the explanation in the JVM specification 5.4.4 (emphasis mine):

A field or method R is accessible to a class or interface D if and only if any of the following conditions are true:

  • [...]
  • R is either protected or has default access (that is, neither public nor protected nor private), and is declared by a class in the same runtime package as D.

And the runtime package is defined in the specs #5.3:

The runtime package of a class or interface is determined by the package name and defining class loader of the class or interface.

Bottom line: this is the expected behaviour.

assylias
  • 321,522
  • 82
  • 660
  • 783
  • This is what I said in my answer. However I found [a solution](http://pastebin.com/eN5DyBrC) to make it work. – Raffaele Jan 11 '13 at 17:58
  • @Raffaele Yes I guess I had not made the link between what you said and the fact that the package (not the class) is defined by the classloader too. Thank you for your suggestion - checking it now. – assylias Jan 11 '13 at 18:02
1

Add c:\temp to java classpath and load Formula.class with the same ClassLoader as Test1.class

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

this will solve your problem.

Evgeniy Dorofeev
  • 133,369
  • 30
  • 199
  • 275