I am attempting to load a set of JAR files that go together to make up an API. For some reason I can only load classes not dependent on definitions in other JARs. I am beginning to suspect that the Android classloaders simply do not handle implementing an interface from one JAR file in another. For this reason I've also unpacked the classes into a common dir however this doesn't work either.
Please see the following code. Apologies for any anomalies, but I've tried to ensure it will compile straight up if pasted into an ADT project called MyProj.
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import dalvik.system.PathClassLoader;
import android.content.Context;
// IPluginSource simply defines the method here at the top.
public class AndroidPluginSource implements IPluginSource
{
@Override
public void doSearching(ArrayList<ClassLoader> classLoaders, ArrayList<String> classNames)
{
String jarPaths = "";
// For each of the raw resources, JARs compiled into the 'res/raw' dir...
for (Field str : R.raw.class.getFields())
{
String resName = str.getName();
Logger.log(Level.FINE, "Resource: " + str);
try
{
// Copy the JAR file to the local FS.
InputStream is = MyProj.self.getResources().openRawResource(str.getInt(this));
OutputStream os = MyProj.self.openFileOutput(resName + ".jar", Context.MODE_PRIVATE);
copyData(is, os);
is.close();
os.close();
// Get JAR location.
String jarLoc = MyProj.self.getFilesDir() + File.separator + resName + ".jar";
// First attempt is just single classloaders, so we aren't suprised this won't work.
classLoaders.add(new PathClassLoader(jarLoc, MyProj.self.getClassLoader()));
//Logger.log(Level.FINE, " LOC: " + jarLoc);
// Keep running list of JAR paths, will that work?
if (jarPaths.length() > 0) jarPaths += File.pathSeparator;
jarPaths += jarLoc;
// We have to go through the JARs to get class names...
JarFile jar = new JarFile(jarLoc);
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.endsWith(".class"))
{
classNames.add(toClassName(entryName));
Logger.log(Level.FINE, " ENT: " + entryName);
// ...while we're here lets get the class out as a file.
String classLoc = MyProj.self.getFilesDir() + File.separator + entryName;
Logger.log(Level.FINER, " CLS: " + classLoc);
File classFile = new File(classLoc);
classFile.delete();
classFile.getParentFile().mkdirs();
InputStream jis = jar.getInputStream(entry);
//OutputStream jos = MyProj.self.openFileOutput(classLoc, Context.MODE_PRIVATE);
OutputStream jos = new FileOutputStream(classFile);
copyData(jis, jos);
jos.close();
jis.close();
}
}
}
catch (Exception ex)
{
Logger.log(Level.SEVERE, "Failed plugin search", ex);
}
}
File f = MyProj.self.getFilesDir();
recursiveList(f, 0);
// So we have a class loader loading classes...
PathClassLoader cl = new PathClassLoader(VermilionAndroid.self.getFilesDir().getAbsolutePath(), ClassLoader.getSystemClassLoader());
classLoaders.add(cl);
// A JAR loader loading all the JARs...
PathClassLoader jl = new PathClassLoader(jarPaths, ClassLoader.getSystemClassLoader());
classLoaders.add(jl);
// And if edited as below we also have a DexLoader and URLClassLoader.
}
// This is just so we can check the classes were all unpacked together.
private void recursiveList(File f, int indent)
{
StringBuilder sb = new StringBuilder();
for (int x = 0; x < indent; x++) sb.append(" ");
sb.append(f.toString());
Logger.log(Level.INFO, sb.toString());
File[] subs = f.listFiles();
if (subs != null)
{
for (File g : subs) recursiveList(g, indent+4);
}
}
// Android helper copy file function.
private void copyData(InputStream is, OutputStream os)
{
try
{
int bytesRead = 1;
byte[] buffer = new byte[4096];
while (bytesRead > 0)
{
bytesRead = is.read(buffer);
if (bytesRead > 0) os.write(buffer, 0, bytesRead);
}
}
catch (Exception ex) {}
}
// Goes from a file name or JAR entry name to a full classname.
private static String toClassName(String fileName)
{
// The JAR entry always has the directories as "/".
String className = fileName.replace(".class", "").replace(File.separatorChar, '.').replace('/', '.');
return className;
}
}
The following code is where this is called from.
public void enumeratePlugins(IPluginSource source)
{
ArrayList<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
ArrayList<String> classNames = new ArrayList<String>();
source.doSearching(classLoaders, classNames);
logger.log(Level.FINE, "Trying discovered classes");
logger.log(Level.INFO, "Listing plugins...");
StringBuilder sb = new StringBuilder();
// Try to load the classes we found.
for (String className : classNames)
{
//boolean loadedOK = false;
Throwable lastEx = null;
for (int x = 0; x < classLoaders.size(); x++)
{
ClassLoader classLoader = classLoaders.get(x);
try
{
Class dynamic = classLoader.loadClass(className);
if(PluginClassBase.class.isAssignableFrom(dynamic) &&
!dynamic.isInterface() && !Modifier.isAbstract(dynamic.getModifiers()))
{
PluginClassBase obj = (PluginClassBase) dynamic.newInstance();
String classType = obj.getType();
String typeName = obj.getName();
classes.put(typeName, new PluginClassDef(typeName, classType, dynamic));
logger.log(Level.FINE, "Loaded plugin: {0}, classType: {1}", new Object[] {typeName, classType});
sb.append(typeName).append(" [").append(classType).append("], ");
if (sb.length() > 70)
{
logger.log(Level.INFO, sb.toString());
sb.setLength(0);
}
}
lastEx = null;
break;
}
catch (Throwable ex)
{
lastEx = ex;
}
}
if (lastEx != null)
{
logger.log(Level.INFO, "Plugin instantiation exception", lastEx);
}
}
if (sb.length() > 0)
{
logger.log(Level.INFO, sb.substring(0, sb.length()-2));
sb.setLength(0);
}
logger.log(Level.FINE, "Finished examining classes");
}
Thanks for your help.
EDIT: I have also tried adding
URLClassLoader ul = null;
try
{
URL[] contents = new URL[jarURLs.size()];
ul = new URLClassLoader(jarURLs.toArray(contents), ClassLoader.getSystemClassLoader());
}
catch (Exception e) {}
classLoaders.add(ul);
...which gives rise to a new exception - UnsupportedOperationException: Can't load this type of class file.
AND:
DexClassLoader dl = new DexClassLoader(jarPaths, "/tmp", null, getClass().getClassLoader());
classLoaders.add(dl);
Also didn't work correctly, but thanks for the suggestion Peter Knego
I should clarify that in the JAR files I have:
JAR1:
public interface IThing
public class ThingA implements IThing
JAR2:
public class ThingB implements IThing