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 P开发者_如何学GoathClassLoader(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
I'm not entirely sure what you're trying to do, but I suspect what you want isn't supported by the Java language's definition of a class loader.
Class loaders are arranged in a hierarchy. If you ask class loader CL1 for a copy of class Foo, it will ask its parent if it knows what Foo is. That goes up the chain to the bootstrap loader, and as things fail it comes back down, until eventually CL1 gets a chance to go out and find a copy. (It doesn't have to work this way, and there's a test case that deliberately does it "wrong", but it's almost always done like this.)
Suppose CL1 does define Foo. Foo implements IBar, so when preparing that class the VM will ask CL1 to find IBar. The usual search is done.
If IBar is defined in CL2, and CL2 is not a parent of CL1, then nothing in CL1 will be able to see IBar.
So if you're creating a bunch of "peer" class loaders for various jar files, you can't directly reference classes between them.
This is not unique to Android.
If you create a single class loader and put the whole set of jars in the path, you can mix and match however you want.
There was no solution to this problem, I have so-far worked around it.
精彩评论