Saturday, October 22, 2011

Running a webapp on Android

TL;DR -- it didn't work because of code generation.

Why would you want to run a webapp on your phone?
My job is writing internally facing webapps. Recently we've been working on a fairly large refactoring of one particular one -- gutting it out and replacing it with a different technology stack, whilst ensuring the existing pages still function ok.

We're replacing Wicket with the open-source framework UtterlyIdle, which we're adding functionality to as we need it. Something we added was MD5 hashing of responses for ETag headers.

When we tested this on our production-like hardware, we were a bit upset with its performance (summary: 300ms requests for a no-content response, eventually diagnosed as network troubles). So, on the assumption that our production hardware was terrible, I decided to run the app on my phone and performance test it there.

Luckily for me, there's a servlet container available for Android called I-Jetty. It installed and started just fine (kudos), but it doesn't support standard war files -- it needs what it called a downloadable webapp.

Downloadable War Files
The developers of I-Jetty have documented how to convert a war file to a downloadable webapp, but to clarify:
  1. Extract all classes (both yours and your dependencies) into a folder. I call this the uber-jar folder.
  2. Run dx over this directory. The arguments on the I-Jetty documentation seem to work ok, but boost the Xmx setting if you have problems.
  3. If dx doesn't run successfully (the end of the output will mention how many errors you have), you'll need to scroll up a bit to find the last actual error. Searching for "unexpected top-level" generally finds the exception section, as it's not always near the bottom. I also had to pass the switch "--no-locals" because of my dependencies.
  4. dx doesn't work if you have a large number of dependencies -- you end up exceeding the dalvik vm's maximum number of symbols.
  5. The output of dx is a .dex file; zip this up and place it in your WEB-INF/lib folder.
  6. If you have resources in your webapp, they must also be zipped up and placed in the WEB-INF/lib folder.
  7. There shouldn't be anything else in the WEB-INF/lib folder; the I-Jetty classloader only reads zip files, so it won't hurt but it will make your war file unnecessarily large.
I had to give up on the app I was using and use a much smaller one instead. Eventually I got this into a downloadable webapp suitable for I-Jetty.

Running it
Everything looked ok to start with; however, this app uses cglib because it needs to proxy classes, not just interfaces. The first problem was java.lang.VerifyErrors -- these are a little bit opaque, as all they mean is the Dalvik VM is refusing to load a particular class. Unless you use logcat, you can't tell why.

I did use logcat, and I found the classes were not being loaded because of NoClassDefFoundErrors -- it's easy to forget the Android API isn't a complete port of Java. So, I grabbed the missing package (java.beans) from Apache Harmony, compiled it, dx'd it along with my other classes, and banished the errors.

Apart from the ones from cglib. Cglib generates bytecode, you see, and it currently doesn't know how to generate Dalvik bytecode. Android doesn't support it.

You should be able to run a war file on Android (and I did) -- but if it needs cglib like mine did, you're out of luck. Oh well.