2013-01-11 7 views
14

Ho una classe Formula, che si trova nella confezione javaapplication4, che mi carico con un URLClassLoader. Tuttavia, quando lo chiamo da un'altra classe Test1, che si trova nello stesso pacchetto, non posso accedere ai suoi metodi che hanno un modificatore di accesso predefinito (posso accedere ai metodi pubblici).URLClassLoader e l'accessibilità dei metodi del pacchetto-privato

ottengo la seguente eccezione:

java.lang.IllegalAccessException: Classe javaapplication4.Test1 non può accedere a un membro della classe di javaapplication4.Formula con modificatori ""

Come posso accedere ai metodi package-private di una classe caricata in runtime dallo stesso pacchetto?

Suppongo che sia un problema con l'utilizzo di un caricatore di classi diverso, ma non sono sicuro del perché (ho impostato il genitore di URLClassLoader).

SSCCE riprodurre il problema (percorsi di Windows) - suppongo che il problema è nel metodo 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

Hai provato a utilizzare 'URLClassLoader.newInstance()'? (edit: probabilmente non fa la differenza) – fge

+0

@fge Stesso comportamento – assylias

+2

Il tuo codice non è stato compilato per me, ho dovuto cambiare 'double' per' Double' ovunque (Oracle JDK 1.7u10) – fge

risposta

6

Una classe in fase di esecuzione è identificata dal sia il suo nome completo e la sua ClassLoader.

Ad esempio, quando si testano due oggetti Class<T> per l'uguaglianza, se hanno lo stesso nome canonico ma sono stati caricati da diversi ClassLoaders, non saranno uguali.

Affinché due classi appartengano allo stesso pacchetto (e, a sua volta, siano in grado di accedere ai metodi package-private), devono essere caricate anche dallo stesso ClassLoader, il che non è il caso qui. Infatti, Test1 viene caricato dal classloader di sistema, mentre la Formula viene caricata da URLClassLoader creata all'interno di loadClass().

Se si specifica un caricatore genitore per URLClassLoader in modo da farlo caricare Test1, vengono comunque utilizzati due caricatori diversi (è possibile verificarlo affermando l'uguaglianza dei caricatori).

Non penso che tu possa fare la classe Formula caricata dallo stesso Test1 ClassLoader (dovresti usare un percorso noto e metterlo sul CLASSPATH), ma ho trovato un modo per fare il contrario : caricamento di un'altra istanza di Test1 nel ClassLoader utilizzato per caricare la formula. Questo è il layout in pseudocodice:

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

Ecco il pastebin. Un paio di note: ho specificato un genitore null per URLClassLoader per evitare il problema elencato sopra e ho manipolato le stringhe per raggiungere lo scopo, ma non so quanto questo approccio possa essere robusto in altri scenari di distribuzione. Inoltre, l'URLClassLoader ho usato solo le ricerche in due directory per trovare definizioni di classe, non tutte le voci elencate nel CLASSPATH

+0

Come posso caricare la mia classe nel caricatore della classe principale (la classe è non disponibile in fase di compilazione, quindi devo "scaricare" il file di classe)? – assylias

+0

@assylias Non so se è possibile in quel programma, a meno che TMP_PATH non sia noto e aggiunto a CLASSPATH prima dell'avvio del programma. Puoi provare il contrario, caricando 'Test1' nel nuovo caricatore (oltre alla soluzione banale di non utilizzare i membri del pacchetto-privato) – Raffaele

+1

@assylias Potrebbe essere sufficiente includere una determinata directory (es. C:/temp /) sul tuo classpath, il classloader leggerà la classe solo allora quando proverai a caricarlo. Almeno UrlClassLoader fa questo. Assicurati di salvare in una sottodirectory in base alla struttura del pacchetto (javaapplication4). – gaborsch

5

La risposta è:

Nel pacchetto sun.reflect.Reflection c'è un metodo chiamato isSameClassPackage (la firma effettiva è private static boolean isSameClassPackage(ClassLoader arg0, String arg1, ClassLoader arg2, String arg3);). Questo metodo è responsabile per decidere se due classi appartengono allo stesso pacchetto o meno.

Il primo controllo questo metodo sta facendo che confronta arg0 e arg2, (i due classloader) se sono diversi, restituisce false.

Quindi, se si utilizzano diversi classloader per le due classi, non corrisponderà.

EDIT: La catena completa di chiamata (su richiesta) è:

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

Interessante, ma ti dispiacerebbe stampare la "catena di chiamate" (cioè, come otteniamo da ".invoke()" con questo metodo)? – fge

+0

Tranne che questo non è un dettaglio di implementazione, ma una scelta di progettazione linguistica.Assylias ha annotato il JLS pertinente, quindi la catena attuale è irrilevante poiché è in effetti un comportamento previsto – Raffaele

+0

@Raffaele Questo è vero, ma è sempre interessante scavare nel codice, per sapere perché esattamente sta funzionando in quel modo. In ogni caso, la catena di chiamata su cui ho abbozzato era accurata per il mio JDK7 e potrebbe differire da altre implementazioni. – gaborsch

2

ho trovato la spiegazione nel JVM specification 5.4.4 (sottolineatura mia):

Un campo o un metodo R è accessibile a una classe o interfaccia D se e solo se si verifica una delle seguenti condizioni:

  • [...]
  • R o è protetta o ha accesso predefinito (vale a dire, né pubblico né protetta né privato), ed è dichiarata da una classe nello stesso pacchetto di runtime come D.

e il runtime pacchetto è definito nel specs #5.3:

il pacchetto runtime di una classe o interfaccia è determinato dal nome del pacchetto e definente classe loader della classe o interfaccia.

Bottom line: questo è il comportamento previsto.

+0

Questo è ciò che ho detto nella mia risposta. Tuttavia ho trovato [una soluzione] (http://pastebin.com/eN5DyBrC) per farlo funzionare. – Raffaele

+0

@Raffaele Sì, immagino di non aver fatto il collegamento tra ciò che hai detto e il fatto che il pacchetto (non la classe) è definito anche dal classloader. Grazie per il tuo suggerimento - controllalo ora. – assylias

1

Aggiungi c:\temp a java classpath e caricare Formula.class con la stessa ClassLoader come Test1.class

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

questo risolverà il vostro problema.

+0

Grazie è quello che ho fatto alla fine. – assylias