Ever wished to introduce Clojure into your SpringBoot app (organization)?
And, want to start very small - nrepl
and a ring-like sub-module - and then use REPL-power for live-coding and adding new functionality?
Then, spring-boost
simplifies it for you!
A complete example of how it works can be found at jaju/spring-boost-example.
For the web-endpoints, the library assumes Spring Webflux. It will not work with standard, thread-ed spring-boot.
But nREPL
will work just fine irrespective.
Here’s how you can write Clojure code, using Compojure, with Springboot. The interface is ring like - Mostly ring, but with a few quirks that should not matter for most common use-cases.
Here is an example code that you can use in your SpringBoot application! It works along-side your existing Java code, without interfering, and you can access Clojure from your Java.
The following code shows
- Setting up end-points - routes and all - with Compojure
- Setting up a websocket handler - builds on top of Springboot’s websocket infrastructure.
(ns org.msync.spring-clj.core
(:require [org.msync.spring-boost :as boost]
[compojure.core :refer :all]
[compojure.route :refer [not-found]]
[clojure.string])
(:import [java.util.logging Logger]
[org.springframework.context ApplicationContext]))
(defonce logger (Logger/getLogger (str *ns*)))
(defroutes app
"Root hello-world GET endpoint, and another echo end-point that handles both GET and POST.
The :body entry in the request-map comes in either as a map for JSON requests, or as a String
for other types."
(GET "/" [:as {query-string :query-string}]
(str "<h1>Hello World.</h1>"
(if-not (clojure.string/blank? query-string) (str "We received a query-string " query-string))))
(GET "/echo/:greeter" [greeter]
{:status 200
:headers {:content-type "application/json"}
:body {:greeting (str "Hello, " greeter)}})
(POST "/echo/:greeter" [greeter :as request]
{:status 200
:headers {:content-type "application/json"}
:body {:greetings (str "Hello, " greeter)
:echo (:body request)}})
(not-found "<h1>Page not found</h1>"))
(defn web-socket-handler [session]
(pr-str session)
;; Use the session as you wish - to create session-specific handlers
(fn [^String message]
(str "Hello, " (.toUpperCase message))))
(defn main
"Set this as your entry-point for the Clojure code in your spring-boot app.
Gets the ApplicationContext object as an argument - which you are free to ignore or use."
[^ApplicationContext application-context]
(.info logger (str "[spring-clj] Initializing clojure app..."))
(boost/set-handler! app)
(boost/set-websocket-handler! web-socket-handler))
Note that the paths are relative to the base path set in application.yml
. Hence, /echo/:greetings
will be accessible at /clojure/echo/:greetings
.
This is the Gradle-Kotlin version.
repositories {
maven {
name = "Clojars"
url = uri("https://clojars.org/repo")
}
}
dependencies {
developmentOnly("org.msync:spring-boost:0.2.0-SNAPSHOT")
developmentOnly("compojure:compojure:1.6.3")
}
Assuming you will write your clojure code in src/main/clojure
tasks.register<Copy>("copyClojure") {
into("build/classes/java/main") {
destinationDir = file(".")
from("src/main/clojure")
}
}
tasks.getByName("bootRun").dependsOn("copyClojure")
tasks.getByName("bootJar").dependsOn("copyClojure")
package com.company.my_application;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("org.msync.spring_boost")
// And other standard stuff
public class YourApplication {
// Your code here
}
By default, port 7888 is used. But add clojure-component.nrepl-port
to your application.yml
(or equivalent) file as follows
# ...
clojure-component:
nrepl-port: 8190
nrepl-start: true
root-path: /clojure
ws-path: /ws
init-symbol: org.msync.spring-clj.core/main
# ...
And, run!
./gradlew bootRun
And you should see something like the following
... [2021-09-10 12:08:14,182] INFO [main] org.msync.spring_boost.application_context$_component_init::invokeStatic Initializing the ClojureComponent [2021-09-10 12:08:14,984] INFO [main] org.msync.spring_boost.Boost::startNrepl nREPL server started on port = 8190 [2021-09-10 12:08:14,986] INFO [main] org.msync.spring_boost.Boost::setupAppInit Initializing clojure code: org.msync.spring-clj.core/main [2021-09-10 12:08:21,097] INFO [main] jdk.internal.reflect.NativeMethodAccessorImpl::invoke0 [spring-clj] Initializing clojure app...n ...
Starting nREPL
by default can be controlled via configuration. But you can easily start/stop nREPL
using two exposed end-points, that take POST requests.
For your convenience, there’s a namespace you can switch to and get hold of the ApplicationContext
object via the state atom’s :ctx
key.
user> @org.msync.spring-boost.application-context/state
;; =>
{:ctx #object[org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
0x333bd779
"org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@333bd779, started on Wed Sep 01 21:47:28 IST 2021"]}
curl -XPOST http://host:port/clojure/nrepl-start
curl -XPOST http://host:port/clojure/nrepl-stop
Copyright © 2020-22 - Ravindra R. Jaju
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.