8
0
0

Android Hacks: Scan Android classpath

ivanviaMind The Robot
December 10, 2010




Why scan the classpath? There are various patterns that are often used in enterprise Java applications that require scanning of classpath and getting the list of all classes that are present in the application.

For example, if you want to discover all classes with a certain annotation (such as @Component in Spring Framework) to process them in a special way, you need a way to go over all classes in your application and select some of them based on which annotations they have.

However, neither Java SE nor Android have built-in facilities to safely get the list of all classes in your application in runtime. The reason for that is the theory behind classloaders in Java – the ability to go over all classes is neither needed in classic OOP nor feasible for all theoretically possible classloader implementations. However, in practice, scanning classpath and discovering the classes you need is quite possible in most cases, both in your web app and on Android. This is always going to be more or less a hack, but if it has been useful in web applications, it can also be useful in Android apps – with some caution, of course.

Thus, in this article I will show and explain a piece of code that does exactly that – scans your classpath and gives you the ability to go over all classes in your app.

Action plan

So here’s how we could approach the problem of going over the set of application classes in runtime:

  1. We assume that Android PathClassLoader (see source here) is used for classloading in your app.
  2. As you can see in the source above, the list of DEX files (typically one but theoretically more) that contain your app classes, is kept in the private field called mDexs. We can use invasive reflection to get the field value and thus the list of DEXs.
  3. Once we get the list of DEXs, we can use the API of the DexFile class to get the list of its entries (which are class names) and there you go.

As I said above, this plan is 90% hacking so we should take whatever assumptions we make with a grain of salt, and do as much error handling and checking as possible, if we actually choose to use this approach in a production app.

Now let’s write some code.

Code example

Say we have an annotation called Foo and few classes decorated with it:

@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
	String value();
}

@Foo("person")
public class Person {

}

@Foo("order")
public class Order {

}

Here’s a simple class that goes over all classes in your app in runtime, finds those marked with @Foo and get the value of the annotation. Do not copy and paste this code into your app – it has a lot to be fixed and added, as described below the code.

public class ClasspathScanner {
	private static final String TAG = ClasspathScanner.class.getSimpleName();

	private static Field dexField;
	
	static {
		try {
			dexField = PathClassLoader.class.getDeclaredField("mDexs");
			dexField.setAccessible(true);
		} catch (Exception e) {
			// TODO (1): handle this case gracefully - nobody promised that this field will always be there
			Log.e(TAG, "Failed to get mDexs field");
		} 
	}
	
	public void run() {
		try {
			// TODO (2): check here - in theory, the class loader is not required to be a PathClassLoader
			PathClassLoader classLoader = (PathClassLoader) Thread.currentThread().getContextClassLoader();

			DexFile[] dexs = (DexFile[]) dexField.get(classLoader);
			for (DexFile dex : dexs) {
				Enumeration<String> entries = dex.entries();
				while (entries.hasMoreElements()) {
					// (3) Each entry is a class name, like "foo.bar.MyClass"
					String entry = entries.nextElement();
					Log.d(TAG, "Entry: " + entry);

					// (4) Load the class
					Class<?> entryClass = dex.loadClass(entry, classLoader);
					if (entryClass != null) {
						Foo annotation = entryClass.getAnnotation(Foo.class);
						if (annotation != null) {
							Log.d(TAG, entry + ": " + annotation.value());
						}
					}
				}
			}
		} catch (Exception e) {
			// TODO (5): more precise error handling
			Log.e(TAG, "Error", e);
		}
	}
}

Let’s review what we have there.

  • (1) A future version of Android might change the internal structure of PathClassLoader and the field might not be found. Production code should somehow deal with it.
  • (2) A future version of the platform might use other classloader than PathClassLoader – another thing to be handled in real code.
  • (3) To save some time, you might scan only some specific packages rather than everything.
  • (4) A real good improvement would be to get class metadata without loading the class – something that javassist can do in Java SE. In this implementation, every class will actually be loaded which is slow.
  • (5) And of course you should handle each possible exception type specifically.

As you can see, the solution can’t really be called reliable or future-proof. Use with caution.

Discussion

-