├── .gitignore ├── LICENSE ├── README.md ├── dev └── user.clj ├── javadoc-opts.txt ├── project.clj └── src ├── clojure └── spring_repl │ ├── bootstrap.clj │ ├── consumers.clj │ ├── context.clj │ ├── nrepl.clj │ └── pubsub.clj └── java └── spring └── repl ├── Channel.java ├── ContextKey.java ├── LogMessage.java ├── ReplAgent.java └── Topic.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | *.iml 13 | .clj-kondo 14 | .idea 15 | .rebel_readline_history 16 | .lsp/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2021 Todd Stout. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-repl 2 | 3 | A java.lang.instrument agent providing a clojure repl that is spring aware. 4 | The intent is to allow any spring based service or app to be started with this agent 5 | and provide useful clojurey things to java-based software. Even if you don't care about 6 | spring, this agent can still be useful to create an nrepl server in an existing application without 7 | modifying any code or dependencies of the application. 8 | 9 | ## Usage 10 | 11 | Create the agent jar via: 12 | 13 | ``` 14 | lein uberjar 15 | ``` 16 | 17 | Add this option to your java application's startup args: 18 | 19 | ``` 20 | -javaagent:/your/path/to/target/spring-repl-1.0.0-standalone.jar 21 | ``` 22 | 23 | At startup, this agent will create an nrepl server on port 8000. You can then use any nrepl client to connect. 24 | In addition to starting an nrepl server, the agent detects the creation of a Spring ApplicationContext, and maintains 25 | a clojure var referring to it. This allows clojure code to access any beans in the application context. However, even if your application/service is not using spring, the repl has full access to anything on the classpath of the JVM you are running. 26 | 27 | For example 28 | 29 | ``` 30 | lein repl :connect localhost:8000 31 | ``` 32 | 33 | Execute the following to list spring-related functions available: 34 | 35 | ``` 36 | (dir spring-repl.context) 37 | ``` 38 | 39 | For convenience, you might want to require the namespace and give it a shorter alias: 40 | 41 | ``` 42 | (require '[spring-repl.context :as ctx]) 43 | ``` 44 | 45 | In the future I will likely make this require automatic. 46 | 47 | Keep in mind that you can also import any java classes available on the classpath and invoke instances via clojure's java interop. This tool has utility beyond spring environments. 48 | 49 | ## Spring Boot In an IDE 50 | 51 | Add this to your build.gradle for running in your IDE: 52 | 53 | ``` 54 | bootRun { 55 | jvmArgs = ["-javaagent:/your/path/to/target/spring-repl-1.0.0-standalone.jar"] 56 | } 57 | ``` 58 | 59 | If you are running a spring boot app outside an IDE, you should not need any special config 60 | other than the -javaagent JVM option. 61 | 62 | ## Historical Caveats 63 | 64 | The first couple of verisions of this agent included dependencies which could cause 65 | all manner of chaos at runtime. These dependencies have been removed. I included them 66 | for my own experiments in using the agent as a vehicle to inject new dependencies into a running 67 | JVM for running tests in an unorthodox manner. 68 | 69 | ## TODO 70 | 71 | The spring-specific functions provided are minimal at the moment. I will likely add more over time. 72 | I have exclusively been using this agent as a vehicle for injecting clojure code into crufty legacy 73 | java code for testing purposes. Specifically, I have used this to experiment with writing integration tests for 74 | java code which will never be refactored for testability due to resource constraints. Having a repl 75 | which can access all java packages and spring beans in a full running application has been interesting and 76 | useful. 77 | 78 | ## License 79 | 80 | Copyright © 2017 Todd Stout 81 | Distributed under the Eclipse Public License either version 1.0 or (at 82 | your option) any later version. 83 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (prn "---REPL Customizations Initialized---") 4 | 5 | (defn load-vars [] 6 | (require 7 | '[clojure.string :as str] 8 | '[clojure.java.data :as jd] 9 | '[spring-repl.bootstrap :as bs])) 10 | 11 | (load-vars) 12 | -------------------------------------------------------------------------------- /javadoc-opts.txt: -------------------------------------------------------------------------------- 1 | -windowtitle "Spring REPL" 2 | -Xdoclint:none 3 | -link "https://docs.oracle.com/javase/11/docs/api/" 4 | -link "https://www.javadoc.io/static/org.clojure/clojure/1.10.1" 5 | -d "target/javadoc/out" 6 | --source-path "src/java" 7 | -cp "target/classes:/Users/tstout/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/Users/tstout/.m2/repository/net/bytebuddy/byte-buddy/1.10.7/byte-buddy-1.10.7.jar:/Users/tstout/.m2/repository/net/bytebuddy/byte-buddy-agent/1.10.5/byte-buddy-agent-1.10.5.jar" 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.github.tstout/spring-repl "1.0.0" 2 | 3 | :scm {:name "git" :url "https://github.com/tstout/spring-repl"} 4 | :description "java agent for injecting a clojure repl" 5 | :url "https://github.com/tstout/spring-repl" 6 | :license {:name "The MIT License" 7 | :url "http://opensource.org/licenses/MIT"} 8 | :pom-addition [:developers [:developer 9 | [:id "todd.tstout@gmail.com"] 10 | [:name "Todd Stout"] 11 | [:url "https://github.com/tstout"]]] 12 | :dependencies [[org.clojure/clojure "1.10.1"] 13 | [org.clojure/core.async "0.7.559"] 14 | [org.clojure/java.data "0.1.1"] 15 | [net.bytebuddy/byte-buddy "1.10.7"] 16 | [lein-jdk-tools "0.1.1"] 17 | [nrepl "0.6.0"]] 18 | :min-lein-version "2.0.0" 19 | :source-paths ["src/clojure"] 20 | :java-source-paths ["src/java"] 21 | :profiles {:uberjar {:aot :all} 22 | :dev {:source-paths ["dev" "src/java"] 23 | :dependencies [[org.clojure/tools.namespace "0.2.11"] 24 | [org.clojure/java.classpath "0.2.3"]]}} 25 | :manifest {"Premain-Class" "spring.repl.ReplAgent" 26 | "Agent-Class" "spring.repl.ReplAgent" 27 | "Main-Class" "spring.repl.ReplAgent"} 28 | 29 | :plugins [[lein-javadoc "0.3.0"] 30 | [lein-release "1.0.9"] 31 | [lein-jdk-tools "0.1.1"]] 32 | 33 | ;; :javadoc-opts {:package-names ["spring.repl"] 34 | ;; :output-dir "target/javadoc/out" 35 | ;; :additional-args ["-windowtitle" "Spring REPL" 36 | ;; "-quiet" 37 | ;; "-Xdoclint:none" 38 | ;; "-link" "https://docs.oracle.com/javase/8/docs/api/" 39 | ;; "-link" "https://www.javadoc.io/static/org.clojure/clojure/1.10.1"]} 40 | 41 | 42 | ;; Before running lein deploy, execute 43 | ;; javadoc spring.repl @javadoc-opts.txt 44 | 45 | :classifiers {:sources {:prep-tasks ^:replace []} 46 | :javadoc {:prep-tasks ^:replace ["javadoc"] 47 | :omit-source true 48 | :filespecs ^:replace [{:type :path, :path "target/javadoc/out"}]}} 49 | 50 | :repositories 51 | {"snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots"}} 52 | 53 | :deploy-repositories 54 | {"releases" {:url "https://oss.sonatype.org/service/local/staging/deploy/maven2" 55 | :username [:gpg :env/sonatype_username] 56 | :password [:gpg :env/sonatype_password]} 57 | "snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots" 58 | :username [:gpg :env/sonatype_username] 59 | :password [:gpg :env/sonatype_password]}}) -------------------------------------------------------------------------------- /src/clojure/spring_repl/bootstrap.clj: -------------------------------------------------------------------------------- 1 | (ns spring-repl.bootstrap 2 | (:require [spring-repl.nrepl :refer [start-repl]] 3 | [spring-repl.consumers :refer [info-listener 4 | error-listener 5 | main-listener]])) 6 | 7 | (defn boot [] 8 | (error-listener) 9 | (info-listener) 10 | (main-listener) 11 | (start-repl)) -------------------------------------------------------------------------------- /src/clojure/spring_repl/consumers.clj: -------------------------------------------------------------------------------- 1 | (ns spring-repl.consumers 2 | (:require [clojure.core.async :refer [ info :evt :message))) 16 | (recur)))) 17 | 18 | (defn error-listener 19 | "Listen for error events" 20 | [] 21 | (let [ch (sub-evt :error-log :err-ch)] 22 | (go-loop 23 | [] 24 | (let [info ( 19 | (get-ctx) 20 | (.getBeanDefinitionNames) 21 | seq 22 | sort)) 23 | 24 | (defn ls-beans 25 | "list all bean names in the context" 26 | [] 27 | (pprint (beans))) 28 | 29 | (defn bean-named 30 | "Find bean by name" 31 | [name] 32 | (-> 33 | (get-ctx) 34 | (.getBean name))) 35 | 36 | (defn bean-info 37 | "Verbose bean information obtained from clojure.reflect" 38 | [name] 39 | (-> 40 | name 41 | bean-named 42 | r/reflect)) 43 | 44 | (defn bean-methods 45 | "Filter the verbose clojure.reflect to only include method information" 46 | [name] 47 | (->> 48 | name 49 | bean-info 50 | :members)) -------------------------------------------------------------------------------- /src/clojure/spring_repl/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns spring-repl.nrepl 2 | (:require 3 | [nrepl.server :refer [start-server stop-server]] 4 | [clojure.tools.logging :as log])) 5 | 6 | (def repl-server (atom nil)) 7 | 8 | (defn start-repl [] 9 | (log/info "Starting nrepl server") 10 | (reset! repl-server (start-server :port 8000 :bind "0.0.0.0"))) 11 | -------------------------------------------------------------------------------- /src/clojure/spring_repl/pubsub.clj: -------------------------------------------------------------------------------- 1 | (ns spring-repl.pubsub 2 | "Convenience wrappers for core.async pub/sub." 3 | (:require [clojure.core.async :refer [! timeout close! go go-loop]])) 4 | 5 | (def mk-ch 6 | "A memoized fn for creating named channels" 7 | (memoize (fn [_] (chan)))) 8 | 9 | (def evt-publication 10 | (pub (mk-ch :evt-pub) :topic)) 11 | 12 | (defn pub-evt [topic evt] 13 | (put! (mk-ch :evt-pub) {:topic topic :evt evt})) 14 | 15 | (defn sub-evt [topic ch-name] 16 | {:pre [(every? keyword? [topic ch-name])]} 17 | (let [out-ch (mk-ch ch-name)] 18 | (sub evt-publication topic out-ch) 19 | out-ch)) 20 | -------------------------------------------------------------------------------- /src/java/spring/repl/Channel.java: -------------------------------------------------------------------------------- 1 | package spring.repl; 2 | 3 | import clojure.java.api.Clojure; 4 | import clojure.lang.IFn; 5 | 6 | /** 7 | * Conduit for publishing information from java code to 8 | * subscribers written in clojure. 9 | */ 10 | public class Channel { 11 | private final IFn pubEvt; 12 | private final IFn fromJava; 13 | private static final Channel instance = new Channel(); 14 | 15 | private Channel() { 16 | IFn require = Clojure.var("clojure.core", "require"); 17 | require.invoke(Clojure.read("spring-repl.pubsub")); 18 | require.invoke(Clojure.read("clojure.java.data")); 19 | fromJava = Clojure.var("clojure.java.data", "from-java"); 20 | pubEvt = Clojure.var("spring-repl.pubsub", "pub-evt"); 21 | } 22 | 23 | /** 24 | * Works for case where obj is comprised of primitive types. 25 | * @param t 26 | * @param obj 27 | */ 28 | public static void pub(Topic t, Object obj) { 29 | instance.pubEvt.invoke( 30 | t.keyword, 31 | instance.fromJava.invoke(obj)); 32 | } 33 | 34 | /** 35 | * Pass raw java object to clojure code. 36 | * @param t 37 | * @param obj 38 | */ 39 | public static void pubObj(Topic t, Object obj) { 40 | instance.pubEvt.invoke(t.keyword, obj); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/java/spring/repl/ContextKey.java: -------------------------------------------------------------------------------- 1 | package spring.repl; 2 | 3 | import clojure.lang.Keyword; 4 | 5 | import static clojure.lang.Keyword.*; 6 | 7 | public enum ContextKey { 8 | APP_CONTEXT("app-context"); 9 | 10 | ContextKey(String s) { 11 | keyword = intern(s); 12 | } 13 | 14 | public final Keyword keyword; 15 | } 16 | -------------------------------------------------------------------------------- /src/java/spring/repl/LogMessage.java: -------------------------------------------------------------------------------- 1 | package spring.repl; 2 | 3 | public class LogMessage { 4 | private final String message; 5 | 6 | public LogMessage(String message) { 7 | this.message = message; 8 | } 9 | 10 | public String getMessage() { 11 | return message; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/java/spring/repl/ReplAgent.java: -------------------------------------------------------------------------------- 1 | package spring.repl; 2 | 3 | import clojure.java.api.Clojure; 4 | import clojure.lang.IFn; 5 | import clojure.lang.Keyword; 6 | import net.bytebuddy.agent.builder.AgentBuilder; 7 | import net.bytebuddy.asm.Advice; 8 | import net.bytebuddy.asm.Advice.Origin; 9 | import net.bytebuddy.asm.Advice.This; 10 | import net.bytebuddy.description.type.TypeDescription; 11 | import net.bytebuddy.dynamic.DynamicType; 12 | import net.bytebuddy.utility.JavaModule; 13 | 14 | import java.lang.instrument.Instrumentation; 15 | import java.lang.reflect.Constructor; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | import static net.bytebuddy.matcher.ElementMatchers.*; 20 | import static spring.repl.ContextKey.*; 21 | import static spring.repl.Topic.*; 22 | 23 | public class ReplAgent { 24 | 25 | public static void main(String[] args) { 26 | bootStrap(); 27 | } 28 | 29 | private static void bootStrap() { 30 | IFn require = Clojure.var("clojure.core", "require"); 31 | require.invoke(Clojure.read("spring-repl.bootstrap")); 32 | IFn boot = Clojure.var("spring-repl.bootstrap", "boot"); 33 | boot.invoke(); 34 | Channel.pub(Topic.INFO, new LogMessage("Bootstrap of spring-repl complete")); 35 | } 36 | 37 | public static void premain(String arguments, Instrumentation instrumentation) { 38 | try { 39 | bootStrap(); 40 | 41 | new AgentBuilder.Default().with(new DebugListener()) 42 | .ignore(nameStartsWith("net.bytebuddy.").or(nameStartsWith("sun.reflect")) 43 | .or(nameStartsWith("org.apache.log4j")).or(nameStartsWith("sun.misc")) 44 | .or(nameStartsWith("org.codehaus.groovy"))) 45 | .type(hasSuperType(named("org.springframework.context.ApplicationContext"))) 46 | .transform((builder, typeDescription, classLoader, module) -> builder 47 | .visit(Advice.to(ConstructorInterceptor.class).on(isConstructor()))) 48 | .installOn(instrumentation); 49 | } catch (Throwable t) { 50 | Channel.pub(Topic.ERROR, new LogMessage(t.getMessage())); 51 | } 52 | } 53 | 54 | // Needed for starting an agent after main has already executed. 55 | public static void agentmain(String arguments, Instrumentation instrumentation) { 56 | premain(arguments, instrumentation); 57 | } 58 | 59 | public static class ConstructorInterceptor { 60 | @Advice.OnMethodExit 61 | public static void intercept(@Origin Constructor m, @This Object inst) throws Exception { 62 | Map ctx = new HashMap<>(); 63 | ctx.put(APP_CONTEXT.keyword, inst); 64 | 65 | Channel.pubObj(MAIN_BUS, ctx); 66 | } 67 | } 68 | 69 | static class DebugListener implements AgentBuilder.Listener { 70 | 71 | @Override 72 | public void onDiscovery( 73 | String typeName, 74 | ClassLoader classLoader, 75 | JavaModule module, 76 | boolean loaded) { 77 | } 78 | 79 | @Override 80 | public void onTransformation( 81 | TypeDescription typeDescription, 82 | ClassLoader classLoader, 83 | JavaModule module, 84 | boolean loaded, DynamicType dynamicType) { 85 | } 86 | 87 | @Override 88 | public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, 89 | boolean loaded) { 90 | } 91 | 92 | @Override 93 | public void onError( 94 | String typeName, 95 | ClassLoader classLoader, 96 | JavaModule module, 97 | boolean loaded, 98 | Throwable throwable) { 99 | Channel.pub(Topic.ERROR, new LogMessage(throwable.getMessage())); 100 | } 101 | 102 | @Override 103 | public void onComplete( 104 | String typeName, 105 | ClassLoader classLoader, 106 | JavaModule module, 107 | boolean loaded) { 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/java/spring/repl/Topic.java: -------------------------------------------------------------------------------- 1 | package spring.repl; 2 | 3 | import clojure.lang.Keyword; 4 | 5 | import static clojure.lang.Keyword.*; 6 | 7 | public enum Topic { 8 | MAIN_BUS("main-bus"), 9 | INFO("info-log"), 10 | ERROR("error-log"); 11 | 12 | Topic(String s) { 13 | keyword = intern(s); 14 | } 15 | 16 | public final Keyword keyword; 17 | } 18 | --------------------------------------------------------------------------------