Ant + Hibernate = Confusion and Pain

Posted

Once again, an attempt to get two stable, mature, well-documented open source Java tools to talk to each other has resulted in several hours of frustration, as well as all the bad karma associated with using Those Words (as my Grade 3 teacher called them) out loud. I finally figured out how to avoid the problem, but I still don't understand why it happens in the first place. If any Ant and/or Hibernate gurus would care to enlighten me, I would be grateful.

Here's what happened:

Background
I wanted to create a near-trivial example application to introduce future generations of students to Hibernate (an object/relational mapping framework for Java). Hibernate gives developers two options: write a JavaBean class, then write a mapping file to describe how that class's members map to database tables, or write the mapping file, and then have Hibernate generate the JavaBean class. Half of this fall's students did it one way, half did it the other; based on their experiences, I decided to go the code generation route.
Setup
I created four files:
  • hibernate.cfg.xml: the Hibernate configuration file.
  • build.xml: the Ant build file. (If you haven't seen Ant, it's a pure-Java replacement for Make, which has become the standard build tool for Java programming).
  • src/User.hbm.xml: the Hibernate mapping file for my User class.
  • src/Test.java: a simple test program. I initially used JUnit, but when things started to go pear-shaped [1], I yanked it out in an attempt to isolate the problem.

Note that my source files are in a src directory. build.xml creates two output directories at the start of the build process: one called gen, for generated code, and another called class, for compiled .class files.

build.xml has four targets:

  1. clean: deletes the gen and class directories, and also the data directory (described below).
  2. codegen: invokes Hibernate's Hbm2JavaTask to generate gen/User.java from src/User.hbm.xml.
  3. compile: compiles gen/User.java and src/Test.java to create class/User.class and class/Test.class.
  4. schema: creates a data directory, and invokes Hibernate's SchemaExportTask to generate database schema files in that directory. SchemaExportTask reads src/User.hbm.xml, checks that it's consistent with class/User.class (compile-time checking—excellent), and creates data/hippo.log and data/hippo.properties, which between them tell HSQLDB (the database I'm using) what table(s) to create, and how.
The first hour and a half
The examples in Hibernate in Action (the book I'm using) show several examples of hibernate.properties files, which use the older Java properties file syntax, but when it comes to XML configuration files, the book says, "Go see your install documentation." I didn't find that documentation helpful; in particular, it wasn't clear when the XML configuration file's properties had to have the same names as were used in the plain text configuration file, and when they had to be different. Hibernate's error messages didn't help at all: all I could do to debug was make more-or-less random changes to the configuration file, and see what effect they had.
The next hour and a half

I finally annealed [2] my configuration file into a working state. A fresh cup of tea, and I was ready to start making some progress—except now Ant was unhappy. The four targets in build.xml are linearly dependent: schema depends on compile, while compile depends on codegen. ant schema should therefore erase the working directories, generate gen/User.java, compile it and src/Test.java, and generate a database schema.

When I tried it, the first three steps worked perfectly, but schema generation failed with an error message saying, "Unable to read User.class". My first thought was that this was yet another classpath problem, but then I discovered that if I ran ant schema again, without running ant clean in between, Ant and Hibernate would generate my schema for me.

After ninety minutes and a lot of bad language, I convinced myself that this was some sort of timing problem. If I ran ant compile, then ran the schema-generation target on its own, everything worked. If I left the dependency between the schema and compile targets in build.xml, and tried to do everything in a single Ant run, it failed. I went so far as to revive a little Python inventory tool I wrote a while back, which walked through the directory tree, recording timestamps, file sizes, and MD5 checksums. As far as I could tell, there was no difference in the files generated by running Ant in two steps, versus running it in one.

And no, it isn't Windows-specific: I get exactly the same behavior on Debian Linux…

At this point, I know how to work around the problem (run Ant twice), and could go back to working on my Hibernate example. But how am I going to explain this to students? "Hi, everyone, this term we're going to be using Ant. It's a replacement for Make, and sometimes, well, sometimes you just have to run it a couple of times to get it to work." I don't think it's acceptable for software to act like a spoiled six-year-old ("I won't! I won't! I won't I won't I won't and you can't make me!"); I certainly don't feel comfortable telling my students that when it does, they should just put up with it.

If you know something about Ant and Hibernate, and would like to figure out what's going on, I've put the project's four files on the web, and I'm easy to reach. I look forward to hearing from you.

[1] "Pear-shaped" is a British expression meaning "gone bad". No idea how it originated; if anyone knows, I'd like to hear.

[2] "Annealing" is the process of banging on metal to get the kinks out. "Simulated annealing" is an optimization technique in which you make random changes to your proposed solution, keeping those that make things better, and throwing aways those that don't. Simulated annealing is a lousy way to debug programs.