Chapter 5. Developing with Immutant
5.1. Introduction
Immutant fully embraces the time-honored Lisp tradition of incremental development at a REPL. This effectively puts you inside the application server, with all its integrated services at your fingertips.
It is quite an intimate experience to build your application while it's deployed and running in Immutant, with frictionless access to its services in real-time, running tests against those services immediately, as you edit them. And no mocking, packaging, or deployment steps to interrupt your flow.
5.2. In-Container Testing
At the REPL, you're free to use whichever Clojure testing library
you prefer, e.g. clojure.test
or Midje. Each has its own way of
invoking tests, allowing you to, for example, run one at a time or
perhaps run every test in one or more namespaces. These should work
fine at the Immutant REPL, and you're free to refer to the
integrated Immutant services in your test definitions.
There are some handy reflective functions available in the
immutant.util namespace, specifically app-uri
, which can be used
to construct URL's in the tests for your web apps. This ensures
your tests can always "find" your app, regardless of context
path or HTTP port, for example.
5.2.1. lein immutant test
The test
task of the lein-immutant plugin enables you to run your
in-container tests when not connected to an Immutant REPL. It will
find all the tests (or Midje facts) in a project, fire up an
Immutant instance, deploy the project to it, connect to its REPL,
run all the tests, undeploy the app, shutdown the Immutant, and
display the results, returning success only if all tests pass.
Because it conveniently runs all your tests inside the app server, a successful run yields a high confidence that your code will run correctly when it counts – in production, when deployed to the same app server. For this reason, it may also be useful to run it on your app's Continuous Integration host whenever any changes are committed.
Note: the log output for the Immutant instance used for the test
run will be located in
<project-root>/target/isolated-immutant/standalone/log/
.
5.3. The REPL
Immutant provides support for connecting to an application's runtime via nREPL.
Each REPL service runs on a unique port, allowing you to have REPLs into multiple applications within the same Immutant, or multiple REPLs within the same application.
REPL services can be started at application deploy time, or dynamically from application code. REPLs started dynamically can be also be shutdown dynamically. REPLs started at deploy time and any dynamically started REPLs that are not shut down by the application will be shut down when the application is undeployed.
See initialization configuration for a list of configuration options
(like :nrepl-port
) and how to apply them.
5.3.1. Binding To An Interface
AS7 is setup to use multiple network interfaces, and allows you to specify
different interfaces for your server: public, management, and unsecure.
By default, these interfaces are all the same - 127.0.0.1
. But you can
specify different addresses for each named interface, aiding in the security
setup of your management ports.
By default, the repl services started by Immutant bind to the management interface.
5.3.2. nREPL
nREPL is a client/server protocol that provides a Clojure REPL across a network. It has several clients that support it.
5.3.2.1. Starting nREPL
You can start an nREPL service using one (or both) of two methods:
specifying :nrepl-port
and/or :nrepl-interface
options in your
configuration, or by calling immutant.repl/start-nrepl from
application code.
5.3.2.2. Starting nREPL via configuration
To have Immutant start a nREPL service on your behalf, you can specify one or both of the following options:
Option | Default | Description |
---|---|---|
:nrepl-port | none, 0 if :dev profile is active or :nrepl-interface provided | The port for binding the nREPL endpoint. 0 means "any available port" |
:nrepl-interface | none, :management if :nrepl-port provided | Can be one of :public , :management , or :unsecure , or an ip address string. See the section on interface aliases. |
If :nrepl-port
is set to 0, the choice of an available port is
delegated to the operating system. Immutant will start up an nREPL
service using that port, and write it to a file beneath your
project's target/
directory called repl-port
and also to a
file in the root of the project called .nrepl-port
, following
Leiningen's convention. Many nREPL clients will use one of these
files to establish a connection. Both will be deleted when the
Immutant process exits.
When the :dev
profile is active, :nrepl-port
defaults to 0.
This means that, by default, every application you deploy will
have its own dedicated nREPL, and most nREPL clients will
automatically detect and connect to it using the files mentioned
above.
To prevent the nREPL server from starting by default, simply
deploy your application without the :dev
profile active, or set
:nrepl-port
to nil
.
Immutant also honors any lein-ring options specified in your
project.clj
. These options will only be used if no :nrepl-port
is specified in your configuration. If :start?
is falsey, no
nREPL will be started even when the :dev
profile is active.
5.3.2.3. Starting nREPL from code
You can also start an nREPL service from your own code, which is
useful if you need to start it in response to a runtime event. To
do so, simply call immutant.repl/start-nrepl. nREPL allows you to
have multiple services in the same runtime, so you need to save
the return value of start-nrepl
if you want to shut down the
nREPL service yourself:
;; bind to the given port on the management interface, and ;; save the service handle for later (def nrepl (immutant.repl/start-nrepl 4242)) ;; bind to the given port and interface (immutant.repl/start-nrepl "127.0.0.1" 4242) ;; bind to the given port on the :public interface (immutant.repl/start-nrepl :public 4242)
5.3.2.4. Connecting to nREPL
- Via Emacs
To connect to nREPL from Emacs, first install cider (formerly nrepl.el), then connect to your running nREPL via the
nrepl
function (M-x nrepl RET
). It will prompt you for the host and port, and if you're somewhere beneath your project, it should default you with the correct port.
- Via Leiningen
Leiningen provides a REPL client (based on reply, an enhanced REPL client) that supports connecting to an nREPL service:
# connect to an nREPL bound to port 4242 on localhost $ lein repl :connect 4242 # connect to an nREPL bound to port 4242 on 10.0.0.10 $ lein repl :connect 10.0.0.10:4242 # if inside the project directory $ lein repl :connect
- Via Counterclockwise
Counterclockwise is an Eclipse plugin for Clojure development. You can use it to connect to your Immutant nREPL session via Window -> Connect to REPL. It will prompt you for an ip address and port to connect to.
5.3.2.5. Shutting Down nREPL
Immutant will automatically shutdown any nREPL services for you when your
application is undeployed, but if you need to do so before undeploy you
can do so using the immutant.repl/stop-nrepl function. You'll need to
pass it the service handle returned by the start-nrepl
call:
(clojure.repl/stop-nrepl nrepl)
Since you need the service handle to stop an nREPL service, you can only
manually stop nREPL's you start yourself. If you start an nREPL service
via the :nrepl-port
configuration option, your only recourse is to
allow Immutant to shut it down for you on undeploy.
5.4. Application Dependencies
Immutant provides tools to aid interactive REPL-based development via the immutant.dev namespace. Currently, it provides three functions for reloading, updating, and viewing your application's dependencies in container:
- immutant.dev/reload-project! Resets the application's class loader
to provide the paths and dependencies in the from the given
project. If no project is provided, the
project.clj
for the appplication is loaded from disk. Also makes any new data readers from the dependencies available. Returns the project map. - immutant.dev/add-dependencies! Adds the given dependencies into
the currently active project's dependency set and resets the
application's class loader to provide the paths and dependencies
from that project (via
reload-project!
). Each dep can either be a lein coordinate ('[foo-bar "0.1.0"]) or a path (as a String) to be added to:source-paths
. Returns the project map. - immutant.dev/current-project Returns the map representing the
currently active leiningen project. This will be the last project
reloaded by
reload-project!
, or the map read fromproject.clj
ifreload-project!
has yet to be called.Examples:
;; assuming we have an app loaded that initially only depends on clojure ;; (from its project.clj) (require '[immutant.dev :as dev] '[clojure.java.io :as io]) (:dependencies (dev/current-project)) ; => #{[org.clojure/clojure "1.4.0"]} ;; let's add more deps (dev/add-dependencies! '[dep-1 "1.0.0"] '[dep/two "0.1.0-SNAPSHOT"] "extra") (:dependencies (dev/current-project)) ; => #{[org.clojure/clojure "1.4.0"] [dep-1 "1.0.0"] [dep/two "0.1.0-SNAPSHOT"]} (:source-paths (dev/current-project)) ; => [["/path/to/app/root/src", "/path/to/app/root/extra"]] ;; now let's reset the deps to those specified in project.clj (dev/reload-project!) (:dependencies (dev/current-project)) ; => #{[org.clojure/clojure "1.4.0"]} ;; let's add a path to :source-paths directly (dev/reload-project! ((dev/current-project) [:source-paths] #(conj % "something"))) (:source-paths (dev/current-project)) ; => [["/path/to/app/root/src", "/path/to/app/root/something"]]