Monday, June 01, 2009

Because Clojure has such tight integration with the JVM, embedding it into a Java application is a snap. In particular, writing Clojure code that runs inside of a J2EE web container and handles web requests is trivial.

We just need to create two simple pieces of scaffolding code in Java to help the container find and dispatch web requests to our Clojure functions.

The first item is a ServletContextListener that initializes the Clojure runtime and evaluates our code when the web application is first loaded by the container:

package myapp.servlet;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import clojure.lang.RT;

public class ClojureContextListener implements ServletContextListener {

public void contextInitialized(ServletContextEvent sce) {
try {
ServletContext sc = sce.getServletContext();
String script = sc.getInitParameter("evalOnContextInitialized");
RT.loadResourceScript(script);
}
catch (Exception e) {
// log an error message ...
}
}

public void contextDestroyed(ServletContextEvent sce) {
// nothing to do here ...
}

}

We'll expect a<context-param> "evalOnContextInitialized" from the application's deployment descriptor to contain the name of a file containing our functions.

The second bit of helper code is a simple HttpServlet that delegates calls to its doService method to a Clojure function of our choice:

package myapp.servlet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import clojure.lang.RT;
import clojure.lang.Var;

public class ClojureServlet extends HttpServlet {

private Var entryPoint;

@Override
public void init(ServletConfig sc) throws ServletException {
String entryPointString = sc.getInitParameter("entryPoint");
if (entryPointString != null) {
String[] nsAndName = entryPointString.split("/", 2);
entryPoint = RT.var(nsAndName[0], nsAndName[1]);
}
else {
throw new ServletException("Servlet " + sc.getServletName() +
" missing 'entryPoint' parameter!");
}
}

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException {
try {
entryPoint.invoke(req, resp);
}
catch (Exception e) {
throw new ServletException(e);
}
}

}

The name of the Clojure function will be specified as an <init-param> in the servlet's configuration. We will follow the Clojure convention of namespace/name convention for identifying our function.

The final (and somewhat anti-climactic) piece of the puzzle is to create the Clojure code that actually handles the web request. This will be be a function of two arguments that accepts the javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse objects provided by the web container. We'll put in a simple "Hello world" handler to demonstrate.

(ns myapp.web)

(defn hello
"Hello world"
[req resp]
(.. resp (getWriter) (println) (str "<html><body>" "Hello world" "</html><body>"))

Since our hello function is in the the "myapp.web" namespace, it should be contained in a file named myapp/web.clj, that is on the application's CLASSPATH. In our case, this means in a directory beneath the WEB-INF/classes of the WAR file.

Accordingly, our web.xml file should look like:
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<context-param>
<param-name>evalOnContextInitialized</param-name>
<param-value>myapp/web.clj</param-value>
</context-param>

<listener>
<listener-class>myapp.servlet.ClojureContextListener</listener-class>
</listener>

<servlet>
<servlet-name>ClojureServlet</servlet-name>
<servlet-class>myapp.servlet.ClojureServlet</servlet-class>
<init-param>
<param-name>entryPoint</param-name>
<param-value>myapp.web/hello</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>ClojureServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

</web-app>

And the resulting web application will be laid out as follows:

myapp.war/
WEB-INF/
web.xml
classes/
myapp/
web.clj
servlet/
ClojureServlet.class
ClojureContextListener.class
lib/
clojure-1.0.0.jar


Note that the single clojure-1.0.0.jar file is the only required dependency. That's all there is to it!

No comments: