Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding default accessor method across different classloaders breaks polymorphism

I come across to a strange behavior while trying to override a method with default accessor (ex: void run()). According to Java spec, a class can use or override default members of base class if classes belongs to the same package. Everything works correctly while all classes loaded from the same classloader. But if I try to load a subclass from separate classloader then polymorphism don't work.

Here is sample:

App.java:

import java.net.*;
import java.lang.reflect.Method;

public class App {
    public static class Base {
        void run() {
            System.out.println("error");
        }
    }
    public static class Inside extends Base {
        @Override
        void run() {
            System.out.println("ok. inside");
        }
    }
    public static void main(String[] args) throws Exception {
        {
            Base p = (Base) Class.forName(Inside.class.getName()).newInstance();
            System.out.println(p.getClass());
            p.run();
        } {
            // path to Outside.class
            URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") };
            URLClassLoader ucl = URLClassLoader.newInstance(url);
            final Base p = (Base) ucl.loadClass("Outside").newInstance();
            System.out.println(p.getClass());
            p.run();
            // try reflection
            Method m = p.getClass().getDeclaredMethod("run");
            m.setAccessible(true);
            m.invoke(p);
        }
    }
}

Outside.java: should be in separate folder. otherwise classloader will be the same

public class Outside extends App.Base {
    @Override
    void run() {
        System.out.println("ok. outside");
    }
}

The output:

class App$Inside
ok. inside
class Outside
error
ok. outside

So then I call Outside#run() I got Base#run() ("error" in output). Reflections works correctly.

Whats wrong? Or is it expected behavior? Can I go around this problem somehow?

like image 378
mart Avatar asked Oct 30 '10 21:10

mart


3 Answers

From Java Virtual Machine Specification:

5.3 Creation and Loading
...
At run time, a class or interface is determined not by its name alone, but by a pair: its fully qualified name and its defining class loader. Each such class or interface belongs to a single runtime package. The runtime package of a class or interface is determined by the package name and defining class loader of the class or interface.


5.4.4 Access Control
...
A field or method R is accessible to a class or interface D if and only if any of the following conditions is true:
  • ...
  • R is either protected or package private (that is, neither public nor protected nor private), and is declared by a class in the same runtime package as D.
like image 101
axtavt Avatar answered Oct 16 '22 06:10

axtavt


The Java Language Specification mandates that a class can only override methods that it can access. If the super class method is not accessible, it is shadowed rather than overridden.

Reflection "works" because you ask Outside.class for its run method. If you ask Base.class instead, you'll get the super implementation:

        Method m = Base.class.getDeclaredMethod("run");
        m.setAccessible(true);
        m.invoke(p);

You can verify that the method is deemed inaccessible by doing:

public class Outside extends Base {
    @Override
    public void run() {
        System.out.println("Outside.");
        super.run(); // throws an IllegalAccessError
    }
}

So, why is the method not accessible? I am not totally sure, but I suspect that just like equally named classes loaded by different class loaders result in different runtime classes, equally named packages loaded by different class loaders result in different runtime packages.

Edit: Actually, the reflection API says that it's the same package:

    Base.class.getPackage() == p.getClass().getPackage() // true
like image 29
meriton Avatar answered Oct 16 '22 05:10

meriton


I found the (hack) way to load external class in main classloader so this problem is gone.

Read a class as bytes and invoke protected ClassLoader#defineClass method.

code:

URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") };
URLClassLoader ucl = URLClassLoader.newInstance(url);

InputStream is = ucl.getResourceAsStream("Outside.class");
byte[] bytes = new byte[is.available()];
is.read(bytes);
Method m = ClassLoader.class.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class, int.class, int.class });
m.setAccessible(true);
Class<Base> outsideClass = (Class<Base>) m.invoke(Base.class.getClassLoader(), "Outside", bytes, 0, bytes.length);

Base p = outsideClass.newInstance();
System.out.println(p.getClass());
p.run();

outputs ok. outside as expected.

like image 1
mart Avatar answered Oct 16 '22 05:10

mart