Overriding default accessor method across different classloaders breaks polymorphism

≯℡__Kan透↙ 提交于 2019-12-30 18:31:21

问题


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?


回答1:


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.



回答2:


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



回答3:


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.



来源:https://stackoverflow.com/questions/4060842/overriding-default-accessor-method-across-different-classloaders-breaks-polymorp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!