Sneaking Clojure into an older Java project

18 Jan 2017

Now I’m not saying I did this, but if I was going to do it and took notes for future reference, this is how I’d sneak some Clojure code into an older, Ant based Java project.

First, figuring out how to just compile any Clojure code without Leiningen or Boot. You’ll need the Clojure jar, so download that from clojure.org first. The magic incantation to compile Clojure code manually is:

$ java -cp clojure-1.8.0.jar:<src-dir>:<out-dir> -Dclojure.compile.path=<out-dir> clojure.lang.Compile your.namespace

Important: both your source directory and your build output directory need to be on the classpath, and you need to tell Clojure’s compiler where to stick your output files with -Dclojure.compile.path. Also, you don’t give it a specific file to build, you tell it what namespace you want to build.

So say your source code is in src, you want to stick your compiled classes in out, and the root namespace you want to compile is blah.core. Your command line will be:

$ java -cp clojure-1.8.0.jar:src:out -Dclojure.compile.path=out clojure.lang.Compile blah.core

Next then, let’s get it into Ant. This particular project that I totally didn’t sneak Clojure into resolves most of its dependencies via Ivy, but we vendor in a few things (Ivy just doesn’t seem to pull in native libraries cleanly, for example), so I went the easy route and just added the Clojure jar to our vendored library directory. (Except I totally didn’t. No way.) The Ant target ended up looking like this:

<!-- Compile the Clojure bits of the project. -->
<target name="clojurec" depends="javac">
  <java failonerror="true" fork="true" classname="clojure.lang.Compile">
    <classpath>
      <path refid="build.class.path"/>
      <pathelement path="${out.classes.dir}"/>
      <pathelement location="lib/clojure-1.8.0.jar" />
      <pathelement path="src/clj" />
    </classpath>
    
    <jvmarg value="-Dclojure.compile.path=${out.classes.dir}" />
    <arg value="namespace.core" />
  </java>
</target>

Pretty straightforward. It’ll be relying on existing Java code, so we run javac first. Output goes into the same directory as the Java classes (defined elsewhere as out.classes.dir), and both that directory and the source directory are added to the classpath.

Except! I just noticed that even though we’re ahead-of-time compiling this code, Clojure’s compiler is also copying my source files over into the output directory. I don’t see a way to skip that, so we’ll probably just need to filter out the source directories when creating artifacts.

Anyway, how do we use that Clojure code? At the moment, I think we’re stuck using Reflection from Java code to find and call it. Let’s generate a simple class from Clojure to make sure we can call things correctly.

(gen-class
 :name myproject.ClojureTest
 :prefix clojureTest-
 :methods [[sayHello [] void]])

(defn clojureTest-sayHello [this]
  (println "Hello from Clojure land."))

Compile that with ant clojurec, then take a look at the output class file. Looks good!

$ javap build/classes/myproject/ClojureTest.class 
public class myproject.ClojureTest {
  public static {};
  public myproject.ClojureTest();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public int hashCode();
  public java.lang.Object clone();
  public void sayHello();
}

We have our class file, and it has the sayHello method. Let’s see if we can call it from Java land (obviously leaving out some error handling):

private void clojureTest()
{
  try
  {
    Class c = Class.forName("myproject.ClojureTest");
    Object o = c.newInstance();
    Method m = o.getClass().getMethod("sayHello", null);
    m.invoke(o, null);
  }
  catch (Exception e)
  {
    e.printStackTrace();
  }
}

And when I compile and run, I see this little bit hidden among all of our logging spew:

 [java] Hello from Clojure land.

Yay!

Bonus Round!

We use Guava in this project that I’m totally not monkeying with right now, and this Clojure code that of course I’m not adding is going to need to participate in Guava’s Event Bus. How do we do that?

Something like this:

(ns eventtest.core
  (:import [com.google.common.eventbus EventBus Subscribe]))

(defrecord AnEvent [])
(defrecord AnotherKindOfEvent [])

(definterface MyDummyInterface
  (^{Subscribe []} handleAnEvent [^eventtest.core.AnEvent evt])
  (^{Subscribe []} handleAnotherEvent [^eventtest.core.AnotherKindOfEvent evt]))

(defrecord MyListener []
  MyDummyInterface
  (handleAnEvent [this evt]
    (println "Hey, I got an event."))

  (handleAnotherEvent [this evt]
    (println "Hey, I got another event.")))

Now your Clojure code can participate in the event bus without any trouble.

;; Create an event bus
(def bus (EventBus.))

;; Create a listener record
(def listener (MyListener.))

;; Register the listener with the bus. This will hook up those interface
;;  methods for you
(.register bus listener)

;; Post events to the bus, and the proper methods on that listener
;; record will be called.
(.post bus (AnEvent.))
(.post bus (AnotherKindOfEvent.))