├── .github └── workflows │ └── clojure.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── bin └── package.sh ├── project.clj └── src └── spring └── cloud ├── app.clj ├── components └── ienv.clj ├── models └── greeting.clj └── rest ├── env.clj ├── greeting.clj └── hello.clj /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/checkout@v1 13 | with: 14 | repository: scotthaleen/lein-spring-boot-jar 15 | path: ./lein-spring-boot-jar 16 | ref: refs/heads/master 17 | - name: Install compile dependencies 18 | working-directory: ../lein-spring-boot-jar 19 | run: lein install 20 | - name: Compile 21 | run: lein spring-boot-jar 22 | -------------------------------------------------------------------------------- /.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 | dist/spring-boot-loader* 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | ## 0.2.0 (2020-03-11) 6 | 7 | #### Changes 8 | 9 | - Added this Changelog 10 | - Bumped Clojure to 1.10.1 11 | - Updated to Spring 2.2.5 12 | - Updated main to use `CommandLineRunner` `@Bean` to reflect "Getting Started" which apparently changed at some point 13 | - Added active `@Profile` example 14 | - Added `@Autowired` magic based on active profile `spring.profiles.active` 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Cloud 2 | [![Actions Status](https://github.com/scotthaleen/clojure-spring-cloud/workflows/Clojure%20CI/badge.svg)](https://github.com/scotthaleen/clojure-spring-cloud/actions) 3 | 4 | 5 | Proof of concept Clojure Implementation of [Spring Boot Getting Started](https://spring.io/guides/gs/spring-boot/) 6 | 7 | This project was created against 'Spring 2.2.5.RELEASE' 8 | 9 | ## Setup 10 | 11 | [Spring Boot](http://projects.spring.io/spring-boot/) uses its own JarLauncher `org.springframework.boot.loader.JarLauncher` 12 | Maven and Gradle have plugins to package the jar for you, Leiningen does not. So I had to create a custom lein plugin. 13 | 14 | Goto - [https://github.com/scotthaleen/lein-spring-boot-jar] and install the plugin locally to compile this project. 15 | 16 | Note: for history I have left the custom bash script (`./bin/package.sh`) used before I created the lein plugin. 17 | 18 | ## Compile 19 | 20 | ```bash 21 | lein spring-boot-jar 22 | ``` 23 | 24 | ## Run 25 | 26 | Choose a profile either `dev` and `prod` and specify it using the 27 | `spring.profiles.active` parameter. There is no `default` (intentionally) the 28 | application will fail to start when no valid profile is not specified. 29 | 30 | ```bash 31 | $ java -jar -Dspring.profiles.active=dev target/boot-spring-cloud-0.1.0-SNAPSHOT.jar 32 | 16:04:58.120 [main] INFO spring.cloud.App - 33 | ____ _ _ 34 | / ___| | ___ (_)_ _ _ __ ___ 35 | | | | |/ _ \| | | | | '__/ _ \ 36 | | |___| | (_) | | |_| | | | __/ 37 | \____|_|\___// |\__,_|_| \___| 38 | |__/ 39 | 40 | 41 | 42 | . ____ _ __ _ _ 43 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 44 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 45 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 46 | ' |____| .__|_| |_|_| |_\__, | / / / / 47 | =========|_|==============|___/=/_/_/_/ 48 | :: Spring Boot :: (v2.2.5.RELEASE) 49 | ... 50 | 51 | 52 | $ curl localhost:8080 53 | Greetings from Clojure!% 54 | 55 | $ curl http://localhost:8080/greeting 56 | {"id":1,"content":"Hello, world!"}% 57 | 58 | $ curl "http://localhost:8080/greeting?name=User" 59 | {"id":2,"content":"Hello, User!"}% 60 | 61 | # Depending on -Dspring.profiles.active={dev,prod} 62 | # Without an active profile the server will fail to start 63 | $ curl http://localhost:8080/env 64 | dev 65 | 66 | ``` 67 | 68 | 69 | ## TODO 70 | 71 | - [x] Spring Boot Lein Package Plugin 72 | - [x] Spring Profiles [example](src/spring/cloud/components/ienv.clj) 73 | - [x] Spring Autowiring [magic](src/spring/cloud/rest/env.clj) 74 | 75 | ## Repobeats 76 | ![Alt](https://repobeats.axiom.co/api/embed/48512415195fc6d1b1e022a40c27d01ac4b7d978.svg "Repobeats analytics image") 77 | 78 | ## License 79 | 80 | Copyright © 2025 81 | 82 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 83 | 84 | -------------------------------------------------------------------------------- /bin/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | 6 | lein clean 7 | lein compile 8 | 9 | JAR=$( a=(dist/spring-boot-loader-*.jar); printf "${a[-1]}" ) 10 | 11 | if [[ ! -f ${JAR} ]]; then 12 | wget -P dist/ http://repo.spring.io/milestone/org/springframework/boot/spring-boot-loader/2.1.2.RELEASE/spring-boot-loader-2.1.2.RELEASE.jar 13 | 14 | fi 15 | 16 | 17 | # build BOOT-INF 18 | # configured in project.clj "target/spring-boot/BOOT-INF/lib" 19 | lein libdir 20 | #mkdir -p target/spring-boot/BOOT-INF 21 | cp -R target/classes target/spring-boot/BOOT-INF/classes 22 | 23 | cd target/spring-boot 24 | 25 | jar xvf ../../dist/spring-boot-loader-*.jar org 26 | 27 | cd - 28 | 29 | jar cm0vf dist/META-INF/MANIFEST.MF target/boot.jar -C target/spring-boot BOOT-INF -C target/spring-boot org 30 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (def SPRING-BOOT-VERSION "2.2.5.RELEASE") 2 | 3 | (defproject spring-cloud "0.2.0-SNAPSHOT" 4 | :description "" 5 | :dependencies [[org.clojure/clojure "1.10.1"] 6 | [org.springframework.boot/spring-boot-starter-web ~SPRING-BOOT-VERSION 7 | :exclusions [org.springframework.boot/spring-boot-starter-tomcat]] 8 | [org.springframework.boot/spring-boot-starter-jetty ~SPRING-BOOT-VERSION] 9 | [org.springframework.boot/spring-boot-starter-actuator ~SPRING-BOOT-VERSION]] 10 | :repositories [["spring-milestone" "https://repo.spring.io/milestone"]] 11 | :main spring.cloud.App 12 | :spring-boot-loader-version "2.2.5.RELEASE" 13 | :plugins [[lein-spring-boot-jar "0.1.0"] 14 | [lein-ancient "0.6.15"]] 15 | :aliases {"build" ["do" ["clean"] ["spring-boot-jar"]]} 16 | :aot :all) 17 | -------------------------------------------------------------------------------- /src/spring/cloud/app.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.app 2 | (:require [clojure.string :as s]) 3 | (:import 4 | [java.util Arrays] 5 | [org.springframework.boot CommandLineRunner SpringApplication] 6 | [org.springframework.boot.autoconfigure SpringBootApplication] 7 | [org.springframework.context ApplicationContext] 8 | [org.springframework.context.annotation Bean] 9 | [org.slf4j Logger LoggerFactory]) 10 | (:gen-class 11 | :name ^{org.springframework.boot.autoconfigure.SpringBootApplication {}} spring.cloud.App 12 | :methods [[^{org.springframework.context.annotation.Bean {}} commandLineRunner 13 | [org.springframework.context.ApplicationContext] org.springframework.boot.CommandLineRunner]])) 14 | 15 | ;; Updated example Spring 2.2.5 16 | 17 | ;; import java.util.Arrays; 18 | 19 | ;; import org.springframework.boot.CommandLineRunner; 20 | ;; import org.springframework.boot.SpringApplication; 21 | ;; import org.springframework.boot.autoconfigure.SpringBootApplication; 22 | ;; import org.springframework.context.ApplicationContext; 23 | ;; import org.springframework.context.annotation.Bean; 24 | 25 | ;; @SpringBootApplication 26 | ;; public class Application { 27 | 28 | ;; public static void main(String[] args) { 29 | ;; SpringApplication.run(Application.class, args); 30 | ;; } 31 | 32 | ;; @Bean 33 | ;; public CommandLineRunner commandLineRunner(ApplicationContext ctx) { 34 | ;; return args -> { 35 | 36 | ;; System.out.println("Let's inspect the beans provided by Spring Boot:"); 37 | 38 | ;; String[] beanNames = ctx.getBeanDefinitionNames(); 39 | ;; Arrays.sort(beanNames); 40 | ;; for (String beanName : beanNames) { 41 | ;; System.out.println(beanName); 42 | ;; } 43 | 44 | ;; }; 45 | ;; } 46 | 47 | ;; } 48 | 49 | 50 | (set! *warn-on-reflection* true) 51 | 52 | (def ^org.slf4j.Logger logger (LoggerFactory/getLogger spring.cloud.App)) 53 | 54 | (defn print-clojure [] 55 | (letfn [(fn-str [& args] (s/join \newline args))] 56 | (.info logger (fn-str 57 | "" 58 | " ____ _ _ " 59 | " / ___| | ___ (_)_ _ _ __ ___ " 60 | " | | | |/ _ \\| | | | | '__/ _ \\ " 61 | " | |___| | (_) | | |_| | | | __/ " 62 | " \\____|_|\\___// |\\__,_|_| \\___| " 63 | " |__/ " 64 | "" 65 | "")) 66 | 67 | )) 68 | 69 | (defn -commandLineRunner 70 | [this ctx] 71 | (reify org.springframework.boot.CommandLineRunner 72 | (run [this args] 73 | (.info logger "The CommandLineRunner Bean") 74 | (doseq [bean (sort (.getBeanDefinitionNames ctx))] 75 | (.info logger bean))))) 76 | 77 | (defn -main [& args] 78 | (print-clojure) 79 | (let [^ApplicationContext ctx (SpringApplication/run 80 | ^Object spring.cloud.App 81 | ^"[Ljava.lang.String;" (into-array String args))])) 82 | -------------------------------------------------------------------------------- /src/spring/cloud/components/ienv.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.components.ienv 2 | (:import 3 | [org.springframework.context.annotation Primary Profile] 4 | [org.springframework.stereotype Component])) 5 | 6 | ;; Setup components for autowiring magic based on active profiles 7 | 8 | ;; import org.springframework.context.annotation.Profile; 9 | ;; import org.springframework.stereotype.Component; 10 | 11 | ;; public interface IEnv { 12 | ;; public String env(); 13 | ;; } 14 | 15 | (gen-interface 16 | :name spring.cloud.components.IEnv 17 | :methods [[env [] String]]) 18 | 19 | ;; @Component 20 | ;; @Profile("dev") 21 | ;; public class DevEnv implements IEnv { 22 | ;;     @Override 23 | ;;     public String env() { 24 | ;;         return "dev"; 25 | ;;     } 26 | ;; } 27 | 28 | (gen-class 29 | :name ^{org.springframework.stereotype.Component {} 30 | org.springframework.context.annotation.Profile ["dev"] 31 | } spring.cloud.components.DevEnv 32 | :implements [spring.cloud.components.IEnv] 33 | :state state 34 | :init init 35 | :prefix "-dev-" 36 | :constructors {[] []}) 37 | 38 | (defn -dev-init [] 39 | [[] "dev"]) ;; <- set state to "dev" 40 | 41 | (defn -dev-env [this] 42 | (.state this)) 43 | 44 | ;; @Component 45 | ;; @Profile("prod") 46 | ;; public class ProdEnv implements IEnv { 47 | ;;     @Override 48 | ;;     public String env() { 49 | ;;         return "prod"; 50 | ;;     } 51 | ;; } 52 | 53 | (gen-class 54 | :name ^{org.springframework.stereotype.Component {} 55 | org.springframework.context.annotation.Profile ["prod"] 56 | } spring.cloud.components.ProdEnv 57 | :implements [spring.cloud.components.IEnv] 58 | :state state 59 | :init init 60 | :prefix "-prod-" 61 | :constructors {[] []}) 62 | 63 | (defn -prod-init [] 64 | [[] "prod"]) ;; <- set state to "prod" 65 | 66 | (defn ^String -prod-env [this] 67 | (.state this)) 68 | -------------------------------------------------------------------------------- /src/spring/cloud/models/greeting.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.models.greeting 2 | (:import 3 | [com.fasterxml.jackson.annotation JsonIgnoreProperties]) 4 | (:gen-class 5 | ;; ignore clojure internal `.state` field during serialization 6 | :name ^{com.fasterxml.jackson.annotation.JsonIgnoreProperties ["state"]} spring.cloud.models.Greeting 7 | :init init 8 | :state state 9 | :constructors {[long String] []} 10 | :methods [[getId [] long] 11 | [getContent [] String]])) 12 | 13 | (defn -init [id content] 14 | [[] {:id id :content content}]) 15 | 16 | (defn -getId [this] 17 | (:id (.state this))) 18 | 19 | (defn -getContent [this] 20 | (:content (.state this))) 21 | -------------------------------------------------------------------------------- /src/spring/cloud/rest/env.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.rest.env 2 | (:import 3 | [spring.cloud.components IEnv] 4 | [org.springframework.beans.factory.annotation Autowired] 5 | [org.springframework.web.bind.annotation RequestMapping RequestParam RestController] 6 | [org.slf4j Logger LoggerFactory]) 7 | (:gen-class 8 | :name ^{org.springframework.web.bind.annotation.RestController {}} spring.cloud.rest.EnvController 9 | :state state 10 | :init init 11 | :constructors {[^{org.springframework.beans.factory.annotation.Autowired {:required true}} spring.cloud.components.IEnv] []} 12 | :methods [[^{org.springframework.web.bind.annotation.RequestMapping ["/env"]} env [] String]])) 13 | 14 | 15 | ;;@RestController 16 | ;; public class EnvController { 17 | ;; 18 | ;; private IEnv envComponent; 19 | ;; 20 | ;; public EnvController(@Autowired IEnv env) { 21 | ;; this.envComponent = env; 22 | ;; } 23 | ;; 24 | ;; @RequestMapping("/env") 25 | ;; public String env() { 26 | ;; return envComponent.env(); 27 | ;; } 28 | ;; } 29 | 30 | 31 | (def ^org.slf4j.Logger logger (LoggerFactory/getLogger spring.cloud.rest.EnvController)) 32 | 33 | (defn -init [env] 34 | [[spring.cloud.components.IEnv] (atom env)]) 35 | 36 | (defn -env [this] 37 | (let [env (.env @(.state this))] 38 | (.info logger "Which Environment: {}" env) 39 | env)) 40 | -------------------------------------------------------------------------------- /src/spring/cloud/rest/greeting.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.rest.greeting 2 | (:import 3 | [org.springframework.web.bind.annotation RequestMapping RequestParam RestController] 4 | [org.slf4j Logger LoggerFactory] 5 | [spring.cloud.models Greeting]) 6 | (:gen-class 7 | :name ^{org.springframework.web.bind.annotation.RestController {}} spring.cloud.rest.GreetingController 8 | :state state 9 | :init init 10 | :constructors {[] []} 11 | :methods [[^{org.springframework.web.bind.annotation.RequestMapping ["/greeting"]} greeting 12 | [^{org.springframework.web.bind.annotation.RequestParam 13 | {:value "name" 14 | :defaultValue "world"}} String] 15 | spring.cloud.models.Greeting]])) 16 | 17 | ;;import java.util.concurrent.atomic.AtomicLong; 18 | ;;import org.springframework.web.bind.annotation.RequestMapping; 19 | ;;import org.springframework.web.bind.annotation.RequestParam; 20 | ;;import org.springframework.web.bind.annotation.RestController; 21 | 22 | ;;@RestController 23 | ;; public class GreetingController { 24 | 25 | ;; private static final String template = "Hello, %s!"; 26 | ;; private final AtomicLong counter = new AtomicLong(); 27 | 28 | ;; @RequestMapping("/greeting") 29 | ;; public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { 30 | ;; return new Greeting(counter.incrementAndGet(), 31 | ;; String.format(template, name)); 32 | ;; } 33 | ;; } 34 | 35 | (def ^org.slf4j.Logger logger (LoggerFactory/getLogger spring.cloud.rest.GreetingController)) 36 | (def ^{:private true} template "Hello, %s!") 37 | 38 | (defn -init [] 39 | [[] (atom 0)]) 40 | 41 | (defn -greeting [this name] 42 | (let [n (swap! (.state this) inc) 43 | s (format template name)] 44 | (.info logger (format "id %s, greeting: %s" n s)) 45 | (Greeting. n s))) 46 | -------------------------------------------------------------------------------- /src/spring/cloud/rest/hello.clj: -------------------------------------------------------------------------------- 1 | (ns spring.cloud.rest.hello 2 | (:import 3 | [org.springframework.web.bind.annotation RestController RequestMapping] 4 | [org.slf4j Logger LoggerFactory]) 5 | (:gen-class 6 | :name ^{org.springframework.web.bind.annotation.RestController {}} spring.cloud.rest.HelloController 7 | :methods [[^{org.springframework.web.bind.annotation.RequestMapping ["/"]} index [] String]])) 8 | 9 | 10 | ;; import org.springframework.web.bind.annotation.RestController; 11 | ;; import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | ;; @RestController 14 | ;; public class HelloController { 15 | 16 | ;; @RequestMapping("/") 17 | ;; public String index() { 18 | ;; return "Greetings from Spring Boot!"; 19 | ;; } 20 | 21 | ;; } 22 | 23 | (def ^org.slf4j.Logger logger (LoggerFactory/getLogger spring.cloud.rest.HelloController)) 24 | 25 | (defn -index 26 | [this] 27 | (.info logger "index: Greetings") 28 | "Greetings from Clojure!") 29 | --------------------------------------------------------------------------------