├── .github ├── template-files │ ├── CHANGELOG.md │ └── README.md └── workflows │ ├── build.yml │ └── template.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build.sbt ├── deployment └── k8s │ └── helm │ ├── Makefile │ └── chart │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── docker-entrypoint.sh ├── generate-build-info.sh ├── generate-keystore.sh ├── mvnw ├── mvnw.cmd ├── pom.xml ├── project ├── build.properties └── plugins.sbt ├── smoke-tests.sh └── src ├── main ├── resources │ ├── application-prod.yml │ ├── application.yml │ └── banner.txt └── scala │ └── spring │ └── boot │ └── scala │ └── example │ ├── ExampleApp.scala │ ├── ObjectMapperCustomizer.scala │ └── controller │ ├── BuildInfoController.scala │ ├── ProbeController.scala │ └── RootController.scala └── test └── scala └── spring └── boot └── scala └── example └── controller ├── BaseAppTest.scala ├── BuildInfoControllerTest.scala ├── ProbeControllerTest.scala └── RootControllerTest.scala /.github/template-files/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # %NAME% Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Added 6 | 7 | - Initial project from [spring-boot-scala-example](https://github.com/jecklgamis/spring-boot-scala-example) 8 | -------------------------------------------------------------------------------- /.github/template-files/README.md: -------------------------------------------------------------------------------- 1 | # %NAME% 2 | 3 | [![Build](https://github.com/%REPOSITORY%/actions/workflows/build.yml/badge.svg)](https://github.com/%REPOSITORY%/actions/workflows/build.yml) 4 | 5 | This project was generated from [spring-boot-scala-example](https://github.com/jecklgamis/spring-boot-scala-example). 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ main ] 5 | tags: 6 | - v* 7 | pull_request: 8 | types: [ opened, reopened ] 9 | branches: [ main ] 10 | jobs: 11 | build-with-maven: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | - name: Cache Maven packages 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.m2 25 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 26 | restore-keys: ${{ runner.os }}-m2 27 | - name: Generate build info 28 | run: ./generate-build-info.sh 29 | - name: Generate keystore 30 | run: ./generate-keystore.sh 31 | - name: Build with Maven 32 | run: mvn --batch-mode --update-snapshots verify 33 | - name: Extract metadata (tags, labels) for Docker 34 | id: meta 35 | uses: docker/metadata-action@v3 36 | with: 37 | images: ${{ github.repository_owner }}/${{ github.event.repository.name }} 38 | - name: Log in to Docker Hub 39 | if: ${{ github.event.repository.name == 'spring-boot-scala-example' && github.event_name != 'pull_request' }} 40 | uses: docker/login-action@v1 41 | with: 42 | username: ${{ secrets.DOCKER_USERNAME }} 43 | password: ${{ secrets.DOCKER_PASSWORD }} 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: . 48 | push: ${{ github.event.repository.name == 'spring-boot-scala-example' && github.event_name != 'pull_request' }} 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | build-with-sbt: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Checkout sources 55 | uses: actions/checkout@v4 56 | - name: Set up JDK 21 57 | uses: actions/setup-java@v2 58 | with: 59 | java-version: '21' 60 | distribution: 'temurin' 61 | - name: Setup sbt launcher 62 | uses: sbt/setup-sbt@v1 63 | - name: Generate build info 64 | run: ./generate-build-info.sh 65 | - name: Generate keystore 66 | run: ./generate-keystore.sh 67 | - name: Build with sbt 68 | run: sbt -v test assembly 69 | -------------------------------------------------------------------------------- /.github/workflows/template.yml: -------------------------------------------------------------------------------- 1 | name: Template 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | prepare: 8 | name: Prepare 9 | runs-on: ubuntu-latest 10 | if: github.event.repository.name != 'spring-boot-scala-example' 11 | steps: 12 | - name: Fetch Sources 13 | uses: actions/checkout@v2.3.4 14 | - name: Prepare 15 | run: | 16 | set -ex 17 | export LC_CTYPE=C 18 | export LANG=C 19 | 20 | # Prepare variables 21 | NAME="${GITHUB_REPOSITORY##*/}" 22 | ACTOR=$(echo ${GITHUB_ACTOR} | tr '[:upper:]' '[:lower:]') 23 | SAFE_NAME=$(echo ${NAME} | sed 's/[^a-zA-Z0-9]//g' | tr '[:upper:]' '[:lower:]') 24 | SAFE_ACTOR=$(echo $ACTOR | sed 's/[^a-zA-Z0-9]//g' | tr '[:upper:]' '[:lower:]') 25 | GROUP="com.github.${SAFE_ACTOR}.${SAFE_NAME}" 26 | 27 | echo "Using GITHUB_REPOSITORY=${GITHUB_REPOSITORY}" 28 | echo "Using GITHUB_ACTOR=${GITHUB_ACTOR}" 29 | echo "Using NAME=${NAME}" 30 | echo "Using GROUP=${GROUP}" 31 | 32 | # Replace placeholders in the template-files files 33 | sed -i "s/%NAME%/${NAME}/g" .github/template-files/* 34 | sed -i "s/%REPOSITORY%/${GITHUB_REPOSITORY/\//\\/}/g" .github/template-files/* 35 | sed -i "s/%GROUP%/${GROUP}/g" .github/template-files/* 36 | 37 | # Update package references in sources 38 | find src -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 39 | find src -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 40 | 41 | # Update K8s deployment files 42 | find deployment -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 43 | find deployment -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 44 | find deployment -type f -exec sed -i "s/jecklgamis/${ACTOR}/g" {} + 45 | 46 | # Update shell scripts 47 | find . -name "*.sh" -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 48 | find . -name "*.sh" -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 49 | find . -name "*.sh" -type f -exec sed -i "s/jecklgamis/${ACTOR}/g" {} + 50 | 51 | # Update Makefile 52 | find . -name Makefile -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 53 | find . -name Makefile -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 54 | find . -name Makefile -type f -exec sed -i "s/jecklgamis/${ACTOR}/g" {} + 55 | 56 | # Update pom.xml 57 | find . -name pom.xml -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 58 | find . -name pom.xml -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 59 | find . -name pom.xml -type f -exec sed -i "s/com.jecklgamis/${GROUP}/g" {} + 60 | 61 | # Update build.sbt 62 | find . -name build.sbt -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 63 | find . -name build.sbt -type f -exec sed -i "s/spring.boot.scala.example/${GROUP}/g" {} + 64 | find . -name build.sbt -type f -exec sed -i "s/com.jecklgamis/${GROUP}/g" {} + 65 | 66 | # Update Dockerfile 67 | find . -name Dockerfile -type f -exec sed -i "s/spring-boot-scala-example/${NAME}/g" {} + 68 | find . -name Dockerfile -type f -exec sed -i "s/Jerrico Gamis/FirstName LastName/g" {} + 69 | find . -name Dockerfile -type f -exec sed -i "s/jecklgamis@gmail.com/user@some-domain/g" {} + 70 | 71 | # Copy content 72 | mkdir -p src/main/scala/${GROUP//.//} 73 | mkdir -p src/test/scala/${GROUP//.//} 74 | cp -R .github/template-files/* . 75 | cp -R src/main/scala/spring/boot/scala/example/* src/main/scala/${GROUP//.//}/ 76 | cp -R src/test/scala/spring/boot/scala/example/* src/test/scala/${GROUP//.//}/ 77 | 78 | # Clean up 79 | rm -rf \ 80 | .circleci \ 81 | .github/readme \ 82 | .github/template-files \ 83 | .github/workflows/template.yml \ 84 | .idea/icon.png \ 85 | src/main/scala/spring \ 86 | src/test/scala/spring \ 87 | LICENSE 88 | - name: Commit files 89 | run: | 90 | export LC_CTYPE=C 91 | export LANG=C 92 | git config --local user.email "action@github.com" 93 | git config --local user.name "GitHub Action" 94 | git add . 95 | git commit -a -m "Prepare repo" 96 | - name: Push changes 97 | uses: ad-m/github-push-action@master 98 | with: 99 | branch: main 100 | github_token: ${{ secrets.GITHUB_TOKEN }} 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | MAINTAINER Jerrico Gamis 3 | 4 | RUN apt update -y && apt install -y openjdk-21-jre-headless && rm -rf /var/lib/apt/lists/* 5 | 6 | ENV APP_HOME /app 7 | 8 | RUN groupadd -r app && useradd -r -gapp app 9 | RUN mkdir -m 0755 -p ${APP_HOME}/bin 10 | RUN mkdir -m 0755 -p ${APP_HOME}/config 11 | RUN mkdir -m 0755 -p ${APP_HOME}/logs/ 12 | 13 | COPY target/spring-boot-scala-example.jar ${APP_HOME}/bin 14 | COPY docker-entrypoint.sh / 15 | 16 | RUN chown -R app:app ${APP_HOME} 17 | RUN chmod +x /docker-entrypoint.sh 18 | 19 | EXPOSE 8080 20 | EXPOSE 8443 21 | 22 | WORKDIR ${APP_HOME} 23 | CMD ["/docker-entrypoint.sh"] 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME:=jecklgamis/spring-boot-scala-example 2 | IMAGE_TAG:=latest 3 | 4 | default: 5 | @cat ./Makefile 6 | dist: keystore build-info 7 | ./mvnw -Dmaven.artifact.threads=8 -T 1C clean verify 8 | image: 9 | docker build -t $(IMAGE_NAME):$(IMAGE_TAG) . 10 | run: 11 | docker run -p 8080:8080 -p 8443:8443 $(IMAGE_NAME):$(IMAGE_TAG) 12 | run-bash: 13 | docker run -i -t $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash 14 | build-info: 15 | @./generate-build-info.sh 16 | keystore: 17 | @./generate-keystore.sh 18 | chart: 19 | cd deployment/k8s/helm && make package 20 | all: dist image 21 | up: all run 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Scala Example 2 | 3 | [![Build](https://github.com/jecklgamis/spring-boot-scala-example/actions/workflows/build.yml/badge.svg)](https://github.com/jecklgamis/spring-boot-scala-example/actions/workflows/build.yml) 4 | 5 | This is an example Spring Boot app using Scala. 6 | 7 | Docker run: 8 | ``` 9 | docker run -p 8080:8080 jecklgamis/spring-boot-scala-example:main 10 | ``` 11 | 12 | What's In the Box? 13 | 14 | * Scala 3 15 | * Maven and SBT build 16 | * Ubuntu Docker image 17 | * Jetty web container 18 | * HTTPS listener (prod profile) using self-signed certs 19 | * JUnit5 tests 20 | * Actuator endpoints (health, metrics) 21 | * Build info, liveness and readiness probe endpoints 22 | * Example Kubernetes deployment 23 | 24 | This is a **Github Template** project. You can create a copy of this project from a clean slate. Simply click 25 | Use this template button. 26 | 27 | ## Building 28 | Ensure you have Java 21, Docker, and Make installed. 29 | 30 | ``` 31 | make all 32 | ``` 33 | This will create build info, keystore, executable jar, and Docker image in one go. Explore the `Makefile` for details. 34 | 35 | ## Running 36 | 37 | Run using Docker: 38 | ```bash 39 | make run 40 | ``` 41 | 42 | Run using executable jar: 43 | ```bash 44 | java -jar target/spring-boot-scala-example.jar 45 | ``` 46 | 47 | ## Endpoints 48 | Point your browser to the urls below or use `curl` in command line. 49 | ```bash 50 | curl http://localhost:8080/ 51 | curl http://localhost:8080/buildInfo 52 | curl http://localhost:8080/probe/live 53 | curl http://localhost:8080/probe/ready 54 | curl http://localhost:8080/actuator/metrics 55 | curl http://localhost:8080/actuator/health 56 | ``` 57 | or you can run `./smoke-tests.sh` 58 | 59 | ## Building With [SBT](https://www.scala-sbt.org/) 60 | 61 | Ensure you have SBT installed. 62 | 63 | Mac OS: 64 | ```bash 65 | brew install sbt 66 | ``` 67 | 68 | Build executable jar: 69 | ```bash 70 | sbt assembly 71 | ``` 72 | 73 | Run main class: 74 | ```bash 75 | sbt run 76 | ``` 77 | 78 | To build interactively, enter the SBT shell by typing `sbt`. You should then 79 | be able to compile, test, assemble, or run any other `sbt` commands. 80 | 81 | ## Deploying To Kubernetes 82 | Assumptions: 83 | * You have `helm`command installed (Mac OS: `brew install helm`) 84 | * You can deploy to a Kubernetes cluster that can access Docker Hub 85 | 86 | Build and install Helm chart: 87 | ```bash 88 | cd deployment/k8s/helm 89 | make install 90 | ``` 91 | This creates: 92 | * a service account 93 | * a pod running on port 8080 94 | * a service listening on port 80 (and implicitly endpoint resources corresponding to the number of pods) 95 | * a deployment (and implicitly replicaset) 96 | * an ingress for Istio (change `kubernetes.io/ingress.class` if you're using a different ingress controller such as nginx) 97 | 98 | To connect to the app locally, create a tunnel to the service: 99 | ```bash 100 | kubectl port-forward service/spring-boot-scala-example 18080:80 101 | curl http://localhost:18080 102 | ``` 103 | 104 | If you have ingress controller installed in your cluster, you can connect using 105 | ```` 106 | curl -v -H "Host:spring-boot-scala-example.local" http:// 107 | ```` 108 | 109 | ## Contributing 110 | Please raise issue or pull request! Thanks! -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val springBootVersion = "3.4.5" 2 | val jacksonVersion = "2.19.0" 3 | 4 | Global / onChangedBuildSource := ReloadOnSourceChanges 5 | 6 | lazy val root = project 7 | .in(file(".")) 8 | .settings( 9 | name := "spring-boot-scala-example", 10 | version := "1.0.0-SNAPSHOT", 11 | scalaVersion := "3.7.2-RC1-bin-20250512-e52986c-NIGHTLY", 12 | assembly / mainClass := Some("spring.boot.scala.example.ExampleApp"), 13 | assembly / assemblyJarName := "spring-boot-scala-example.jar", 14 | libraryDependencies += "org.springframework.boot" % "spring-boot-starter-web" % springBootVersion 15 | exclude("org.springframework.boot", "spring-boot-starter-tomcat"), 16 | libraryDependencies += "org.springframework.boot" % "spring-boot-starter-jetty" % springBootVersion, 17 | libraryDependencies += "jakarta.servlet" % "jakarta.servlet-api" % "6.1.0", 18 | libraryDependencies += "org.springframework.boot" % "spring-boot-starter-actuator" % springBootVersion, 19 | libraryDependencies += "org.springframework.boot" % "spring-boot-starter-test" % springBootVersion % Test, 20 | libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, 21 | libraryDependencies += "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion, 22 | libraryDependencies += "com.fasterxml.jackson.module" % "jackson-module-scala_2.13" % jacksonVersion, 23 | libraryDependencies += "org.scalactic" %% "scalactic" % "3.3.0-alpha.1", 24 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.3.0-alpha.1" % Test, 25 | libraryDependencies += "org.junit.jupiter" % "junit-jupiter-engine" % "5.13.0-M1" % Test, 26 | libraryDependencies += "net.aichler" % "jupiter-interface" % "0.11.1" % Test 27 | ) 28 | 29 | ThisBuild / javacOptions ++= Seq("-source", "21") 30 | ThisBuild / resolvers += Resolver.jcenterRepo 31 | 32 | ThisBuild / assemblyMergeStrategy := { 33 | case PathList(ps@_*) if ps.contains("module-info.class") => MergeStrategy.concat 34 | case PathList("META-INF", "spring-configuration-metadata.json") => MergeStrategy.concat 35 | case PathList("META-INF", "additional-spring-configuration-metadata.json") => MergeStrategy.concat 36 | case PathList("META-INF", "spring.handlers") => MergeStrategy.concat 37 | case PathList("META-INF", "spring.schemas") => MergeStrategy.concat 38 | case PathList("META-INF", "spring.factories") => MergeStrategy.concat 39 | case PathList("META-INF", "web-fragment.xml") => MergeStrategy.concat 40 | case PathList("META-INF", "spring-autoconfigure-metadata.properties") => MergeStrategy.concat 41 | case PathList("META-INF", "spring", "aot.factories") => MergeStrategy.concat 42 | case PathList("META-INF", "spring", "org.springframework.boot.autoconfigure.AutoConfiguration.imports") => MergeStrategy.concat 43 | case x => MergeStrategy.defaultMergeStrategy(x) 44 | } 45 | -------------------------------------------------------------------------------- /deployment/k8s/helm/Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME:=spring-boot-scala-example 2 | VERSION:=$(shell cat chart/Chart.yaml | yq .appVersion) 3 | HELM_CHART=$(APP_NAME)-$(VERSION).tgz 4 | 5 | default: 6 | @cat ./Makefile 7 | package: 8 | @rm -f $(HELM_CHART) 9 | helm package ./chart 10 | dry-run: 11 | @helm install --dry-run --debug $(APP_NAME) ./chart | tee dry-run.txt 12 | install: package 13 | @echo $(VERSION) 14 | @helm install $(APP_NAME) $(HELM_CHART) 15 | uninstall: 16 | @helm uninstall $(APP_NAME) 17 | upgrade: package 18 | @helm upgrade $(APP_NAME) $(HELM_CHART) 19 | rollback: 20 | @helm rollback $(APP_NAME) 21 | clean: 22 | @rm -f $(HELM_CHART) 23 | all: package 24 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: spring-boot-scala-example 3 | description: An example Spring Boot app using Scala 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: v1.0.0-rc.1 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "v1.0.0-rc.1" 25 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "spring-boot-scala-example.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "spring-boot-scala-example.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "spring-boot-scala-example.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "spring-boot-scala-example.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "spring-boot-scala-example.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "spring-boot-scala-example.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "spring-boot-scala-example.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "spring-boot-scala-example.labels" -}} 37 | helm.sh/chart: {{ include "spring-boot-scala-example.chart" . }} 38 | {{ include "spring-boot-scala-example.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "spring-boot-scala-example.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "spring-boot-scala-example.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "spring-boot-scala-example.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "spring-boot-scala-example.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "spring-boot-scala-example.fullname" . }} 5 | labels: 6 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "spring-boot-scala-example.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "spring-boot-scala-example.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "spring-boot-scala-example.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 8080 39 | protocol: TCP 40 | livenessProbe: 41 | httpGet: 42 | path: /probe/live 43 | port: http 44 | readinessProbe: 45 | httpGet: 46 | path: /probe/ready 47 | port: http 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | {{- with .Values.nodeSelector }} 51 | nodeSelector: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.affinity }} 55 | affinity: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.tolerations }} 59 | tolerations: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "spring-boot-scala-example.fullname" . }} 6 | labels: 7 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "spring-boot-scala-example.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "spring-boot-scala-example.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | defaultBackend: 40 | service: 41 | name: front-gate 42 | port: 43 | number: 80 44 | rules: 45 | {{- range .Values.ingress.hosts }} 46 | - host: {{ .host | quote }} 47 | http: 48 | paths: 49 | {{- range .paths }} 50 | - path: {{ .path }} 51 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 52 | pathType: {{ .pathType }} 53 | {{- end }} 54 | backend: 55 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 56 | service: 57 | name: {{ $fullName }} 58 | port: 59 | number: {{ $svcPort }} 60 | {{- else }} 61 | serviceName: {{ $fullName }} 62 | servicePort: {{ $svcPort }} 63 | {{- end }} 64 | {{- end }} 65 | {{- end }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "spring-boot-scala-example.fullname" . }} 5 | labels: 6 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: 8080 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "spring-boot-scala-example.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "spring-boot-scala-example.serviceAccountName" . }} 6 | labels: 7 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "spring-boot-scala-example.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "spring-boot-scala-example.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "spring-boot-scala-example.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /deployment/k8s/helm/chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for spring-boot-scala-example. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: jecklgamis/spring-boot-scala-example 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "main" 12 | 13 | imagePullSecrets: 14 | - name: dockerhub 15 | nameOverride: "" 16 | fullnameOverride: "" 17 | 18 | serviceAccount: 19 | # Specifies whether a service account should be created 20 | create: true 21 | # Annotations to add to the service account 22 | annotations: {} 23 | # The name of the service account to use. 24 | # If not set and create is true, a name is generated using the fullname template 25 | name: "" 26 | 27 | podAnnotations: {} 28 | 29 | podSecurityContext: {} 30 | # fsGroup: 2000 31 | 32 | securityContext: {} 33 | # capabilities: 34 | # drop: 35 | # - ALL 36 | # readOnlyRootFilesystem: true 37 | # runAsNonRoot: true 38 | # runAsUser: 1000 39 | 40 | service: 41 | type: ClusterIP 42 | port: 80 43 | 44 | ingress: 45 | enabled: true 46 | className: "" 47 | annotations: 48 | kubernetes.io/ingress.class: "nginx" 49 | nginx.ingress.kubernetes.io/rewrite-target: / 50 | cert-manager.io/issuer: "letsencrypt-prod" 51 | nginx.ingress.kubernetes.io/proxy-body-size: "0" 52 | nginx.ingress.kubernetes.io/proxy-read-timeout: "600" 53 | hosts: 54 | - host: spring-boot-scala-example.jecklgamis.com 55 | paths: 56 | - path: / 57 | pathType: Prefix 58 | tls: 59 | - secretName: spring-boot-scala-example 60 | hosts: 61 | - spring-boot-scala-example.jecklgamis.com 62 | 63 | resources: {} 64 | # We usually recommend not to specify default resources and to leave this as a conscious 65 | # choice for the user. This also increases chances charts run on environments with little 66 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 67 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 68 | # limits: 69 | # cpu: 100m 70 | # memory: 128Mi 71 | # requests: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | 75 | autoscaling: 76 | enabled: false 77 | minReplicas: 1 78 | maxReplicas: 100 79 | targetCPUUtilizationPercentage: 80 80 | # targetMemoryUtilizationPercentage: 80 81 | 82 | nodeSelector: {} 83 | 84 | tolerations: [] 85 | 86 | affinity: {} 87 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec java -jar "bin/spring-boot-scala-example.jar" 3 | 4 | -------------------------------------------------------------------------------- /generate-build-info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 4 | VERSION=$(git rev-parse HEAD) 5 | BUILD_TIME=$(date +"%Y-%m-%dT%H:%M:%S%z") 6 | BUILD_INFO_FILE=./src/main/resources/build-info.json 7 | 8 | cat < ${BUILD_INFO_FILE} 9 | { "branch": "${BRANCH}", "build-time": "${BUILD_TIME}", "version": "${VERSION}" } 10 | EOF 11 | 12 | echo "Wrote $BUILD_INFO_FILE" && cat ${BUILD_INFO_FILE} 13 | -------------------------------------------------------------------------------- /generate-keystore.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | KEYSTORE_FILE=src/main/resources/keystore.pfx 3 | SERVER_CERT=src/main/resources/server.crt 4 | 5 | rm -f ${KEYSTORE_FILE} 6 | rm -f ${SERVER_CERT} 7 | 8 | keytool -genkey -alias spring-boot-scala-example \ 9 | -keyalg RSA \ 10 | -keysize 2048 \ 11 | -keystore ${KEYSTORE_FILE} \ 12 | -storepass changeit \ 13 | -keypass changeit \ 14 | -storetype PKCS12 \ 15 | -dname "CN=spring-boot-scala-example, OU=Org Unit, O=Org, L=Locality, S=State, C=Country" \ 16 | 17 | echo "Wrote ${KEYSTORE_FILE}" 18 | 19 | keytool -exportcert -rfc -keystore ${KEYSTORE_FILE} -storepass changeit -alias spring-boot-scala-example -file ${SERVER_CERT} 20 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.jecklgamis 8 | spring-boot-scala-example 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 3.4.5 15 | 16 | 17 | 18 | 19 | spring-snapshots 20 | https://repo.spring.io/snapshot 21 | 22 | true 23 | 24 | 25 | 26 | spring-milestones 27 | https://repo.spring.io/milestone 28 | 29 | 30 | 31 | 32 | 33 | spring-snapshots 34 | https://repo.spring.io/snapshot 35 | 36 | 37 | spring-milestones 38 | https://repo.spring.io/milestone 39 | 40 | 41 | 42 | 43 | 44 | org.scala-lang 45 | scala3-library_3 46 | 3.7.2-RC1-bin-20250512-e52986c-NIGHTLY 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-tomcat 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-jetty 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-actuator 65 | 66 | 67 | com.fasterxml.jackson.core 68 | jackson-databind 69 | 2.19.0 70 | 71 | 72 | com.fasterxml.jackson.core 73 | jackson-annotations 74 | 3.0-rc4 75 | 76 | 77 | com.fasterxml.jackson.module 78 | jackson-module-scala_2.13 79 | 2.19.0 80 | 81 | 82 | jakarta.servlet 83 | jakarta.servlet-api 84 | 6.1.0 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-test 89 | test 90 | 91 | 92 | 93 | 94 | ${project.artifactId} 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 3.4.5 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-compiler-plugin 104 | 3.14.0 105 | 106 | 21 107 | 21 108 | UTF-8 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-source-plugin 114 | 3.3.1 115 | 116 | 117 | attach-sources 118 | 119 | jar 120 | 121 | 122 | 123 | 124 | 125 | net.alchim31.maven 126 | scala-maven-plugin 127 | 4.9.5 128 | 129 | 130 | scala-compile-first 131 | process-resources 132 | 133 | add-source 134 | compile 135 | 136 | 137 | 138 | scala-test-compile 139 | process-test-resources 140 | 141 | testCompile 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5") 2 | 3 | resolvers += Resolver.jcenterRepo 4 | addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.11.1") 5 | 6 | -------------------------------------------------------------------------------- /smoke-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare -a urls=( 4 | "http://localhost:8080/" 5 | "http://localhost:8080/buildInfo" 6 | "http://localhost:8080/probe/live" 7 | "http://localhost:8080/probe/ready" 8 | "http://localhost:8080/actuator/metrics" 9 | "http://localhost:8080/actuator/beans" 10 | ) 11 | 12 | exit_code=0 13 | for url in "${urls[@]}"; do 14 | result=$(curl -o /dev/null -s -w "%{http_code}" "$url") 15 | if [ "${result}" == "200" ]; then 16 | echo "${url} OK"; 17 | else 18 | echo "${url} NG (${result})"; 19 | exit_code=1 20 | fi 21 | done 22 | 23 | exit ${exit_code} -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8443 3 | ssl: 4 | key-store: classpath:keystore.pfx 5 | key-store-password: changeit 6 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | application: 2 | name: "spring-boot-scala-example" 3 | description: "An example Spring Boot app using Scala" 4 | 5 | server: 6 | port: 8080 7 | 8 | management: 9 | endpoints: 10 | enabled-by-default: true 11 | web: 12 | exposure: 13 | include: "*" 14 | endpoint: 15 | metrics: 16 | enabled: true 17 | health: 18 | enabled: true 19 | show-details: always 20 | 21 | logging: 22 | file: 23 | name: logs/spring-boot-scala-example.log 24 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | .__ ___. __ 2 | ___________________|__| ____ ____\_ |__ ____ _____/ |_ 3 | / ___/\____ \_ __ \ |/ \ / ___\| __ \ / _ \ / _ \ __\ 4 | \___ \ | |_> > | \/ | | \/ /_/ > \_\ ( <_> | <_> ) | 5 | /____ >| __/|__| |__|___| /\___ /|___ /\____/ \____/|__| 6 | \/ |__| \//_____/ \/ -------------------------------------------------------------------------------- /src/main/scala/spring/boot/scala/example/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | 6 | 7 | @SpringBootApplication 8 | class ExampleApp 9 | 10 | object ExampleApp { 11 | def main(args: Array[String]): Unit = SpringApplication.run(classOf[ExampleApp]) 12 | } -------------------------------------------------------------------------------- /src/main/scala/spring/boot/scala/example/ObjectMapperCustomizer.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example 2 | 3 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 4 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 5 | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer 6 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder 7 | import org.springframework.stereotype.Component 8 | 9 | @Component 10 | class ObjectMapperCustomizer extends Jackson2ObjectMapperBuilderCustomizer : 11 | override def customize(builder: Jackson2ObjectMapperBuilder): Unit = 12 | builder.modules(DefaultScalaModule, new JavaTimeModule) 13 | -------------------------------------------------------------------------------- /src/main/scala/spring/boot/scala/example/controller/BuildInfoController.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import io.micrometer.core.annotation.Timed 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.http.{CacheControl, MediaType, ResponseEntity} 6 | import org.springframework.web.bind.annotation.RequestMethod.GET 7 | import org.springframework.web.bind.annotation.{RequestMapping, RestController} 8 | 9 | import java.time.LocalDateTime 10 | import scala.io.Source 11 | 12 | @RestController 13 | class BuildInfoController: 14 | @Value("${application.name}") 15 | private val appName: String = null 16 | private val buildInfo = loadBuildInfo() 17 | 18 | def loadBuildInfo(): String = 19 | Source.fromInputStream(getClass.getClassLoader.getResourceAsStream("build-info.json")).getLines.mkString 20 | 21 | @RequestMapping(path = Array("/buildInfo"), method = Array(GET), produces = Array(MediaType.APPLICATION_JSON_VALUE)) 22 | def get(): ResponseEntity[String] = 23 | ResponseEntity.ok().cacheControl(CacheControl.noStore()).body(buildInfo) 24 | -------------------------------------------------------------------------------- /src/main/scala/spring/boot/scala/example/controller/ProbeController.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import io.micrometer.core.annotation.Timed 4 | import org.springframework.beans.factory.annotation.{Autowired, Value} 5 | import org.springframework.http.{MediaType, ResponseEntity} 6 | import org.springframework.web.bind.annotation.* 7 | import org.springframework.web.bind.annotation.RequestMethod.{GET, PUT} 8 | 9 | import java.time.LocalDateTime 10 | 11 | @RestController 12 | @RequestMapping(path = Array("/probe"), produces = Array(MediaType.APPLICATION_JSON_VALUE)) 13 | class ProbeController: 14 | 15 | @Timed 16 | @RequestMapping(path = Array("/live"), method = Array(GET)) 17 | def live(): ResponseEntity[Map[String, Any]] = 18 | ResponseEntity.ok().body(Map("message" -> "I'm alive!")) 19 | 20 | @Timed 21 | @RequestMapping(path = Array("/ready"), method = Array(GET)) 22 | def ready(): ResponseEntity[Map[String, Any]] = 23 | ResponseEntity.ok().body(Map("message" -> "I'm ready!")) 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/scala/spring/boot/scala/example/controller/RootController.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import io.micrometer.core.annotation.Timed 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.web.bind.annotation.RequestMethod.GET 6 | import org.springframework.web.bind.annotation.{RequestMapping, RestController} 7 | 8 | import java.time.LocalDateTime 9 | 10 | @RestController 11 | class RootController: 12 | @Value("${application.name}") 13 | val appName: String = null 14 | 15 | @RequestMapping(path = Array("/"), method = Array(GET)) 16 | @Timed 17 | def root(): Map[String, Any] = Map("name" -> appName, "message" -> "It works on my machine!") 18 | 19 | -------------------------------------------------------------------------------- /src/test/scala/spring/boot/scala/example/controller/BaseAppTest.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue} 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import org.springframework.boot.test.web.server.LocalServerPort 10 | import org.springframework.http.* 11 | import org.springframework.test.context.ActiveProfiles 12 | import spring.boot.scala.example.controller.RootController 13 | 14 | import java.util.{Collections, UUID} 15 | 16 | 17 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 18 | @ActiveProfiles(Array("dev")) 19 | class BaseAppTest: 20 | @Autowired 21 | val restTemplate: TestRestTemplate = null 22 | @LocalServerPort 23 | val port = 0 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/scala/spring/boot/scala/example/controller/BuildInfoControllerTest.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue} 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import org.springframework.http.* 10 | import org.springframework.test.context.ActiveProfiles 11 | import spring.boot.scala.example.controller.RootController 12 | 13 | import java.util.{Collections, UUID} 14 | 15 | 16 | class BuildInfoControllerTest extends BaseAppTest : 17 | @Autowired 18 | val buildInfoController: BuildInfoController = null 19 | 20 | @Test 21 | def testGetBuildInfo(): Unit = 22 | val buildInfo = restTemplate.getForObject(s"http://localhost:$port/buildInfo", classOf[Map[String, Any]]) 23 | assertTrue(!buildInfo("version").toString.isEmpty) 24 | assertTrue(!buildInfo("build-time").toString.isEmpty) 25 | assertTrue(!buildInfo("branch").toString.isEmpty) 26 | 27 | -------------------------------------------------------------------------------- /src/test/scala/spring/boot/scala/example/controller/ProbeControllerTest.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue} 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import org.springframework.http.* 10 | import org.springframework.test.context.ActiveProfiles 11 | import spring.boot.scala.example.controller.RootController 12 | 13 | import java.util.{Collections, UUID} 14 | 15 | 16 | class ProbeControllerTest extends BaseAppTest: 17 | @Autowired 18 | val probeController: ProbeController = null 19 | 20 | @Test 21 | def testLivenessProbe(): Unit = 22 | val response = restTemplate.getForObject(s"http://localhost:$port/probe/live", classOf[Map[String, Any]]) 23 | assertEquals("I'm alive!", response("message")) 24 | 25 | @Test 26 | def testReadinessProbe(): Unit = 27 | val response = restTemplate.getForObject(s"http://localhost:$port/probe/ready", classOf[Map[String, Any]]) 28 | assertEquals("I'm ready!", response("message")) 29 | -------------------------------------------------------------------------------- /src/test/scala/spring/boot/scala/example/controller/RootControllerTest.scala: -------------------------------------------------------------------------------- 1 | package spring.boot.scala.example.controller 2 | 3 | import org.junit.jupiter.api.Assertions.assertTrue 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import spring.boot.scala.example.controller.RootController 10 | 11 | class RootControllerTest extends BaseAppTest: 12 | @Autowired 13 | val rootController: RootController = null 14 | 15 | @Test 16 | def testRootEndpoint(): Unit = 17 | val entity = restTemplate.getForObject(s"http://localhost:$port/", classOf[Map[String, Any]]) 18 | assertTrue(entity.contains("name")) 19 | assertTrue(entity.contains("message")) 20 | 21 | --------------------------------------------------------------------------------