Search code examples
javaspringspring-bootclojureread-eval-print-loop

Is it possible to modify Clojure code running as a Spring component on a remote server and download the modified code using the REPL?


Imagine I have the following setup:

  1. A Spring Boot application.
  2. Inside it is a Camunda workflow engine.
  3. There are multiple components (@org.springframework.stereotype.Component) inside that application that are written in Clojure and are used by the Camunda workflow engine.

Setup: Spring Boot application with Camunda engine and multiple components inside it

I heard that allegedly it is possible to modify the code of a Clojure application without restarting it.

So, I want to

  1. modify the code of those components (without restarting the application),
  2. add new components to the application (without restarting the application), and
  3. after I'm done prototyping, download the current version of all components there.

The idea is that I prototype the components from the REPL until they work as designed. This means that the Camunda workflow engine will use the components modified by my actions on the REPL.

Then, I download the current version of the components in the application (so that they are not lost when the application is shut down). This code is then cleaned up, refactored, covered by unit tests and put under version control.

Question:

  1. Is it theoretically possible to implement such workflow with Clojure (not necessarily out of the box)?
  2. Are there any known limitations which would make such workflow absolutely impossible?

Update 1

Found following projects which prima facie allow you to interact with Java code using a REPL:

  1. spring-boot-bugger
  2. spring-repl

However, I don't know whether you can use them to change the code.


Solution

  • This is a very rough and simple example of how to do this. I have dropped Camunda here because I don't think it's actually relevant.

    The approach here:

    • Create Spring services, that delegate to actual Clojure code.
    • Make sure to load a "current good version" of that code into your process
    • Start a "server REPL" to allow overriding

    The full project can be found here

    Provide a service, that delegates to some Clojure code:

    @Service
    class ClojureBackedService {
        BigDecimal add(BigDecimal a, BigDecimal b) {
            Clojure.var('net.ofnir.repl', 'add').invoke(a, b)
        }
    }
    

    Start a REPL and load some "good initial setup":

    @Service
    class ClojureRepl {
    
        @PostConstruct
        def init() {
            def require = Clojure.var('clojure.core', 'require')
            require.invoke(Clojure.read('net.ofnir.repl'))
            Clojure.var('clojure.core.server', 'start-server').invoke(
                    Clojure.read("{:port 5555 :name spring-repl :accept clojure.core.server/repl}")
            )
        }
    
        @PreDestroy
        def destroy() {
            Clojure.var('clojure.core.server', 'stop-server').invoke(
                    Clojure.read("{:name spring-repl}")
            )
        }
    
    }
    

    For showing how this works, provide a web endpoint, that adds two numbers:

    @RestController
    class MathController {
        private final ClojureBackedService backend
    
        MathController(ClojureBackedService backend) {
            this.backend = backend
        }
    
        @PostMapping
        def add(BigDecimal a, BigDecimal b) {
            backend.add(a, b)
        }
    }
    

    Run the application and do a first test:

    $ curl -da=5 -db=5 localhost:8080                                     
    10
    

    Looks good. Now connect to the REPL and replace the add function:

    $ telnet localhost 5555
    Trying ::1...
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    user=> (ns net.ofnir.repl)
    nil
    net.ofnir.repl=> (defn add [a b] (* a b))
    #'net.ofnir.repl/add
    

    No call the endpoint again:

    $ curl -da=5 -db=5 localhost:8080
    25
    

    And there you have your spring service changed at runtime.