Wednesday, September 22, 2010

On the Oddness of javac

At work recently, we had a strange problem: one of our jars was not being loaded in Tomcat. Some closer investigation revealed the all too familiar error message instructing us to See Servlet Spec 2.3, section 9.7.2. Normally I see this when the servlet api jar makes its way into lib directory by mistake (usually a poorly configured pom.xml file). However, this was in one of our jars -- and we certainly don't check in the servlet api in our codebase. What gives?

It's always Maven's fault, right?


Well, usually it is. First of all, we (rather densely) configured Maven to realise the servlet api was a provided library. About two seconds of thought showed this wasn't the problem -- the servlet classes must have been actually in our jar file.

We knew they weren't checked in, so we whacked Maven into verbose mode (mvn -X for those of you playing at home) and scanned the output, line by line. No clues there. At this point my coworker began pushing for a manual compile using javac, and after about an hour of me saying "no, it can't possibly be javac" but not being able to find the problem, I relented. (The verbose output proved helpful to get the classpath out, even if Maven doesn't print it out in the standard format.)

Et tu, javac?


We did a manual compile, and I was gobsmacked when the servlet files were in the resulting output directory. javac, which I had considered a close personal friend (except when it exhibited this bug), was the culprit. There must have been a reasonable explanation, and I was out to find it.

I headed to the javac documentation page, and it didn't take too long to find this part:
Note: Classes found through the classpath may be subject to automatic recompilation if their sources are also found.


What?! Lemme get this straight: if there are java source files in the classpath, they can be compiled and placed into the output directory? Alas, it is true. This meant that somewhere, we had the servlet source code in our classpath, and that it was newer than the regular servlet api.

GWT: Gee, What Twits


Some spectacularly 1337 searching found that GWT distributes the servlet api and sources inside its jars (note that we were using version 1.7.1. Maybe it's fixed now?)

Including somebody else's library inside yours is a really bad idea. Sure, it's convenient -- junit distributes hamcrest matchers -- but unless you also provide a version that doesn't include them (like junit-dep), your users are in for a world of hurt. If they need a different version of the library from the one you're distributing, well, they'll have to rely on the magic of the classpath to find the one they want before they find yours (and good luck with that if you're using Tomcat, which doesn't define the jar order when it loads jar files).

Lessons


Here are the lessons I learnt that day:
  1. javac has some interesting features that you might not come across on a day to day basis.

  2. GWT is evil.

In the end, we hacked the build file to exclude anything in the javax directory from being jar'ed up. That solved the problem, and I went home to take a long shower.

Footnote


Why didn't we RTFM and use javac -implicit:none? Cos we were using Java 1.5, that's why.