2010-10-30 20 views
7

Je rencontre un comportement étrange en essayant de surcharger une méthode avec accesseur par défaut (ex: void run()). Selon Java spec, une classe peut utiliser ou remplacer les membres par défaut de la classe de base si les classes appartiennent au même package. Tout fonctionne correctement lorsque toutes les classes sont chargées à partir du même chargeur de classe. Mais si j'essaie de charger une sous-classe de séparé classloader alors le polymorphisme ne fonctionne pas.Le remplacement de la méthode d'accès par défaut à travers différents classloaders rompt le polymorphisme

est Voici un exemple:

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: devrait être dans le dossier séparé. sinon classloader sera le même

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

La sortie:

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

Alors j'appelle Outside#run() je suis arrivé Base#run() ("erreur" en sortie). Les réflexions fonctionnent correctement.

Qu'est-ce qui ne va pas? Ou est-ce un comportement attendu? Puis-je contourner ce problème en quelque sorte?

+0

Je remarque que votre classe 'Outside' déclare' run' être public ... Je me demande si c'est le problème. Pouvez-vous l'essayer avec 'Outside.run' ayant un accès par défaut? –

+0

Si 'Outside # run' a un accesseur par défaut quand Reflections (sans' setAccessible (true) ') _also_ ne fonctionne pas:' 'Exception dans le thread" main "' java.lang.IllegalAccessException: L'application Class ne peut pas accéder à un membre de class Outside avec des modificateurs "" ' – mart

+0

J'ai réécrit l'échantillon pour Outside # run a l'accesseur par défaut – mart

Répondre

5

De Java Virtual Machine Specification:

5,3 Création et chargement
...
A temps d'exécution, une classe ou une interface est déterminée non pas uniquement par son nom, mais par une paire: son nom complet et son chargeur de classe de définition. Chaque classe ou interface appartient à un package d'exécution .Le package d'exécution d'une classe ou d'une interface est déterminé par le nom du package et en définissant le chargeur de classes de la classe ou l'interface .


5.4.4 Contrôle d'accès

... un champ ou une méthode R est accessible à une classe ou d'une interface D si et seulement si l'une des des conditions suivantes est remplie:

  • ...
  • R est soit protected ou package privé (qui est, ni public ni protected ni private), et est déclarée par une classe dans le même package d'exécution en tant que D.
+0

Merci. Maintenant, le problème est clair. – mart

+0

Est-il possible d'ajouter une classe externe au classloader principal? – mart

+1

@mart: En fait, vous pouvez appeler la méthode protégée 'URLClassLoader.addURL()' via la réflexion (avec 'setAccessible (true)'), et cela fonctionne comme vous le souhaitez, mais c'est certainement un hack. Cependant, vous pouvez écrire votre propre classloader où l'ajout de ressources à l'exécution serait un comportement légitime, et l'utiliser pour charger toutes ces classes. – axtavt

2

La spécification de langage Java stipule qu'une classe peut uniquement remplacer les méthodes auxquelles elle peut accéder. Si la méthode super classe n'est pas accessible, elle est ombrée plutôt que remplacée. La réflexion "fonctionne" parce que vous demandez Outside.class pour sa méthode d'exécution. Si vous demandez Base.class à la place, vous aurez la super mise en œuvre:

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

Vous pouvez vérifier que la méthode est jugée inaccessible en faisant:

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

Alors, pourquoi est la méthode pas accessible? Je ne suis pas tout à fait sûr, mais je soupçonne que, tout comme les classes nommées par différents chargeurs de classes, aboutissent à des classes d'exécution différentes, les paquets nommés de la même manière par différents chargeurs de classes aboutissent à des paquets d'exécution différents.

Modifier: En fait, l'API de réflexion dit que c'est le même paquet:

Base.class.getPackage() == p.getClass().getPackage() // true 
+0

Merci de votre réponse. Votre suspicion semble raisonnable. Peut-être que la JVM considère que les paquets sont différents pour différents chargeurs de classe. – mart

+0

Est-ce le moyen d'éviter cela? Peut-être qu'il est possible d'ajouter une classe au classloader principal? – mart

1

J'ai trouvé le (pirater) façon de charger la classe externe principale classloader si ce problème a disparu.

Lire une classe en tant qu'octets et appeler la méthode protégée ClassLoader#defineClass.

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(); 

sorties ok. outside comme prévu.