How to invoke a method (reflection) with a not generic parameter?

prdatur picture prdatur · Jan 10, 2012 · Viewed 7.3k times · Source

I have a little problem. I am developing an Android applikation. There you can dynamicly load classes from other applications (packages). First of all, i do not want to "hack" an third-party app, i want to try to build up plugins for my own app. So what do i have?

2 Test applications and 1 library which is in both apps included.

So the code for app1:

package com.ftpsynctest.app1;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import android.app.Activity;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import com.syncoorp.ftpsyncx.commons.SyncFile;
import dalvik.system.PathClassLoader;
public class App1Activity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        SyncFile f = new SyncFile("bla");
        String classname = "com.ftpsynctest.app2.classcall";
        String classpath = getApk("com.ftpsynctest.app1") + ":" + getApk("com.ftpsynctest.app2");
        PathClassLoader myClassLoader = new dalvik.system.PathClassLoader(classpath, ClassLoader.getSystemClassLoader());
        try {
            Class c = Class.forName(classname, true, myClassLoader);
            for (Method m : c.getDeclaredMethods()) {
                System.out.println("Method: " + m.getName());
                for (Type t : m.getGenericParameterTypes()) {
                    System.out.println(" - type: " + t.toString());
                }
                m.invoke(c.newInstance(), new Object[] {
                    new com.syncoorp.ftpsyncx.commons.SyncFile("bla")
                });
                break;
            }
        }
        catch (ClassNotFoundException e) {e.printStackTrace();}
        catch (IllegalArgumentException e) {e.printStackTrace();}
        catch (IllegalAccessException e) {e.printStackTrace();}
        catch (InvocationTargetException e) {e.printStackTrace();}
        catch (InstantiationException e) {e.printStackTrace();}
    }

    private String getApk(String packageName) {
        try { return this.getPackageManager().getApplicationInfo(packageName, 0).sourceDir;}
        catch (NameNotFoundException e) {e.printStackTrace();}
        return "";
    }
}

So i want to create the class com.ftpsynctest.app2.classcall and call the method modify with a parameter of type com.syncoorp.ftpsyncx.commons.SyncFile.

My app2 code:

package com.ftpsynctest.app2;
import com.syncoorp.ftpsyncx.commons.SyncFile;
public class classcall {
    public SyncFile modify(SyncFile file) {
        file.change_date = 123;
        return file;
    }
}

I first installed app2 to provide the class to app1. After that succeed i started app1.

My Output:

01-10 22:21:48.804: INFO/System.out(4681): Method: modify
01-10 22:21:48.809: INFO/System.out(4681): - type: class com.syncoorp.ftpsyncx.commons.SyncFile

So for now it looks good. the parameter type of the found method is com.syncoorp.ftpsyncx.commons.SyncFile and my provided one is the same.

But i get the following error:

 java.lang.IllegalArgumentException: argument type mismatch  
      at java.lang.reflect.Method.invokeNative(Native Method)  
      at java.lang.reflect.Method.invoke(Method.java:507)  
      at com.ftpsynctest.app1.App1Activity.onCreate(App1Activity.java:44)  
      at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)  
      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1615)  
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1667)  
      at android.app.ActivityThread.access$1500(ActivityThread.java:117)  
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:935)  
      at android.os.Handler.dispatchMessage(Handler.java:99)  
      at android.os.Looper.loop(Looper.java:130)  
      at android.app.ActivityThread.main(ActivityThread.java:3691)  
      at java.lang.reflect.Method.invokeNative(Native Method)  
      at java.lang.reflect.Method.invoke(Method.java:507)  
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)  
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:665)  
      at dalvik.system.NativeStart.main(Native Method)

But why? my output tells me that it is SyncFile and i put SyncFile to the invoke command. Whats the problem there? Can it be that compiling app2 creates a class from SyncFile which is different from the compiled app1 ? if yes, why ? the SyncFile class is the same physical class within my "commons" library which both projects share.

Anybody has a solution or answer?

Answer

JimmyB picture JimmyB · Feb 8, 2019

There may be two identically named classes SyncFile visible to different classloaders in this case. Even if the classes are named exactly the same, in the same package, even with the same byte code they will be considered different classes by the VM because they come from different locations (classloaders).

At runtime, the identity of a class is defined by its package, its name and the classloader instance that loaded it. It is expected that every class can only be found/loaded by exactly one classloader. If that is not the case, the result will vary based upon which classloader is in effect when the class is accessed.

new com.syncoorp.ftpsyncx.commons.SyncFile will probably use the class loaded by and associated with the app's local classloader whereas the called method expects the version associated with myClassLoader. Since both classloaders know the 'same' class (identified by package+class name), but each one only knows one of them, from the JVM's perspective, they are two different classes.

You could try and create your SyncFile instance via reflection from the SyncFile class loaded by myClassloader, i.e.

Class sfClass = Class.forName("com.syncoorp.ftpsyncx.commons.SyncFile", true, myClassLoader);
Object param = sfClass.newInstance("bla"); // param must be Object because the 'local' SyncFile is not the same as the SyncFile represented by sfClass! 

Note that you will face this problem everywhere where your app and a 'plugin' exchange instances of any class they both contain, i.e. reflection everywhere. Consider if it's worth the hassle or if you want to resort to some better way, e.g. IPC.