Skip to content

Introduce Clojure and live-coding power to your Spring Boot application!

License

Notifications You must be signed in to change notification settings

jaju/spring-boost

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

What is spring-boost?

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.

Caveat

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.

The Pitch: Sample Application Code

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.

What will my Clojure look like?

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

  1. Setting up end-points - routes and all - with Compojure
  2. 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.

Configure Gradle

Modify build.gradle.kts (or, build.gradle)

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")
}

Ensure your clojure code is copied to the classpath

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")

Hint Springboot using @ComponentScan

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
}

Modify application.yml (or application.properties)

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
# ...

Run “bootRun”

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
...

Connect to the NREPL

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"]}

Control the NREPL server

Start it

curl -XPOST http://host:port/clojure/nrepl-start

Stop it

curl -XPOST http://host:port/clojure/nrepl-stop

License

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.