Adding Awesomeness to your Legacy Java projects
22.03.2014 Permalink Suppose you are an enthusiastic Clojure developer, but unfortunately you have to work with plain old Java for a living. You managed to convince your team mates that Clojure would actually make quite some things simpler. Now how can you add Clojure to your Java code base without disruption?TL;DR see the sample project on Github for a working example.
The required steps are independent of your IDE or build system, but to make the description concrete I assume the following:- For Java development your team uses Eclipse Kepler or above.
- Your build system is Maven 3.x.
- You want to work with the latest Clojure version, which as of this writing is the upcoming 1.6.
In Eclipse install Counterclockwise plugin
Go to Help / Install new software and add the link to the update site. You can use any name, I prefer "CCW". Check the Counterclockwise item under Clojure programming and hit Next. Follow the instructions. Counterclockwise allows you to edit Clojure code and nicely interact with a REPL. If you enable strict Paredit mode in Eclipse preferences and reorder the Eclipse views a bit, you will get a tool that allows for the desired flow of interactive development that Lisp is famous for. (I admit Emacs is my favorite editor, but introducing Clojure PLUS Emacs in your Java organisation could be asking too much of your co-workers.)Drop Clojure and other required libs into your pom.xml
The language Jar takes less than 3.5 MB, and additional Clojure libraries are tiny compared to their Java counterparts. So size is certainly not an issue. Here's a minimal pom.xml to get you started:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.friemen.samples</groupId> <artifactId>legacy-java</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.clojure</groupId> <artifactId>clojure</artifactId> <version>1.6.0-RC2</version> </dependency> </dependencies> </project>
<repositories> <repository> <id>clojars.org</id> <url>http://clojars.org/repo</url> </repository> </repositories>
<dependency> <groupId>org.clojure</groupId> <artifactId>java.data</artifactId> <version>0.1.1</version> </dependency>
Create your own namespace to add custom Clojure code (optional)
You can include Clojure and some libraries and use them without writing your own Clojure code. In order to add your own awesome functions you can right-click the target Java package in the Package Explorer and choose New / Other / Clojure Namespace. Enter the unqualified name of your new namespace. Counterclockwise will add the Java package as prefix to the toplevel ns form. Here is some sample Clojure code for demonstration purposes:(ns de.friemen.samples.awesome) (defn hello-world [] (println "Hello World")) (defn map-inc [ints] (map inc ints)) (defn sum-by [f employees] (reduce (fn [sums e] (update-in sums [(f e)] #(+ (or % 0) (:salary e)))) {} employees))
Call Clojure functions from Java
Clojure 1.6 provides a neat official API to make sure Java code, which directly depends on Clojure functions, won't break in the future. The API contains only two static methods:var
and read
. The first returns a var from a namespace, the second evaluates an EDN String and returns its Java representation.
This API is already sufficient for simple scenarios, so I could stop here... though, if this would be extensively used in numerous Java classes, maintainability would certainly suffer. In addition, Alex Miller (@puredanger) pointed me to potential performance problems, which can be solved by caching. So read on, if you want to see how I tackle these problems.
First I added a small convenience class on top of clojure.java.api.Clojure
:
package de.friemen.samples; import java.util.Collections; import java.util.HashMap; import java.util.Map; import clojure.java.api.Clojure; import clojure.lang.IFn; public class Cljns { public final static String CLOJURE_CORE = "clojure.core"; private final Map<String, IFn> cachedFns = Collections.synchronizedMap(new HashMap<String, IFn>()); private final Map<String, Object> cachedKeywords = Collections.synchronizedMap(new HashMap<String, Object>()); protected final String ns; protected final static IFn REQUIRE = Clojure.var(CLOJURE_CORE, "require"); protected final static IFn DEREF = Clojure.var(CLOJURE_CORE, "deref"); public final static Cljns core = new Cljns(CLOJURE_CORE); public Cljns (String ns) { this.ns = ns; REQUIRE.invoke(Clojure.read(ns)); } public String getName() { return ns; } public IFn fn(String symbolName) { IFn f = cachedFns.get(symbolName); if (f == null) { f = Clojure.var(ns, symbolName); cachedFns.put(symbolName, f); } return f; } public Object deref(String symbolName) { return DEREF.invoke(fn(symbolName)); } public Object keyword(String s) { final String kwKey = s.startsWith(":") ? s : ":" + s; Object kw = cachedKeywords.get(kwKey); if (kw == null) { kw = Clojure.read(":" + s); cachedKeywords.put(kwKey, kw); } return kw; } }
Cljns
, so these instances should be kept as well, for example as application scoped singletons in your Spring context.
With a static import to Cljns.core
, here's the simplest way to call a clojure.core function:
// access clojure.core lib core.fn("println").invoke("Hello Awesomeness!");
final List<Integer> output = (List<Integer>) core.fn("map").invoke( core.fn("inc"), new Integer[] { 1, 2, 3});
(map inc [1 2 3])
, so we certainly don't want to see this type of Java code scattered all over our code base.
Instead we write expressions of this kind as Clojure functions in our own namespace (as shown above). In order to invoke them we need the namespace and use it to call one of the contained functions:
// adhoc instantiate a namespace by name final Cljns awesome = new Cljns("de.friemen.samples.awesome"); // invoke a function of this namespace final List<Integer> output = (List<Integer>) awesome.fn("map-inc").invoke(Arrays.asList(1, 2, 3));
String
or int
are used.
But how can we pass in a collection of Java Beans and expect for example a Java Map as result? Of course our own Clojure function could expect Java beans and collections but this would considerably harm our ability to write idiomatic Clojure code and make efficient use of the REPL.
To combine idiomatic Clojure code with convenient invocation from Java we need two things: 1) a transformation of Java Beans to/from Clojure data, and 2) a convenient wrapper around our own functions that applies the conversion as necessary. The next snippet shows how we like to call this wrapper:
// use a dedicated class to hold real Java wrappers for more convenience final Awesome awesome = new Awesome(); final Map<String, Integer> result = awesome.sumBy("branch", Arrays.asList( new Employee("Donald Duck", "Bonn", 150), new Employee("Daisy Duck", "Bonn", 200), new Employee("Mini Mouse", "Luenen", 300), new Employee("Micky Mouse", "Luenen", 50)));
Cljns
with a new class
Awesome
, which holds our wrapper method:
package de.friemen.samples; import java.util.ArrayList; import java.util.List; import java.util.Map; import clojure.lang.LazySeq; public class Awesome extends Cljns { private static Cljns data = new Cljns("clojure.java.data"); public Awesome() { super("de.friemen.samples.awesome"); } @SuppressWarnings("unchecked") public Map<String, Integer> sumBy (String field, List<Employee> es) { return (Map<String, Integer>) fn("sum-by").invoke( keyword(field), data.fn("from-java").invoke(es)); } }
to-java
and from-java
which handle the conversion for us.
Equipped with the explicit Java-Clojure adaption layer that the Awesome
class represents, we are now able to create awesome Clojure functions and conveniently call them from Java.
I don't advocate to wrap every single Clojure function like this, in simple cases it doesn't pay off. Keeping the function name in a final String field is sufficient in most cases to save us from shotgun surgery when function names change.
I pushed the complete Java project with sample code to GitHub.