├── .env
├── .github
├── dependabot.yml
└── workflows
│ ├── docker-image.yml
│ └── maven.yml
├── .gitignore
├── .mvn
└── wrapper
│ └── maven-wrapper.properties
├── .vscode
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── hooks
└── pre-commit
├── images
└── spring-boot-admin-dashboard.png
├── justfile
├── mvnw
├── mvnw.cmd
├── pom.xml
├── spotbugs-exclude.xml
├── spotbugs-include.xml
├── src
├── main
│ ├── java
│ │ └── com
│ │ │ └── miguno
│ │ │ └── javadockerbuild
│ │ │ ├── App.java
│ │ │ ├── controllers
│ │ │ ├── RootController.java
│ │ │ └── WelcomeController.java
│ │ │ ├── models
│ │ │ └── Welcome.java
│ │ │ └── security
│ │ │ ├── AppSecurityConfiguration.java
│ │ │ └── CustomCsrfFilter.java
│ └── resources
│ │ ├── application.properties
│ │ └── templates
│ │ └── .keep
└── test
│ └── java
│ └── com
│ └── miguno
│ └── javadockerbuild
│ ├── SmokeTest.java
│ └── controllers
│ ├── WelcomeControllerIT.java
│ └── WelcomeControllerTest.java
└── tools
├── create_image.sh
└── start_container.sh
/.env:
--------------------------------------------------------------------------------
1 | # .env
2 | #
3 | # This file pre-defines environment variables and is read by the `*.sh` scripts
4 | # and by `just`.
5 | #
6 | # If needed, you can override the variables defined here when invoking `just`
7 | # by manually setting the variable in the shell environment. For example, the
8 | # command below sets `DOCKER_IMAGE_NAME` to "foo/bar":
9 | #
10 | # $ DOCKER_IMAGE_NAME="foo/bar" just ...
11 | #
12 |
13 | ### App-related settings
14 | # APP_PORT must match `server.port` setting in `application.properties`.
15 | APP_PORT=8123
16 |
17 | ### Docker-related settings
18 | DOCKER_IMAGE_NAME="miguno/java-docker-build-tutorial"
19 | DOCKER_IMAGE_TAG="latest"
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
9 | - package-ecosystem: "maven" # See documentation for possible values
10 | directory: "/" # Location of package manifests
11 | schedule:
12 | interval: "daily"
13 | ignore:
14 | # Ignore Maven APIs/SPIs.
15 | - dependency-name: org.apache.maven:*
16 | - package-ecosystem: "github-actions"
17 | directory: "/"
18 | schedule:
19 | interval: "daily"
20 |
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Docker Image CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | # To cancel a currently running workflow from the same PR, branch, or tag
10 | # when a new workflow is triggered.
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 |
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | - name: Build the Docker image
26 | run: docker buildx build . --file Dockerfile --tag miguno/java-docker-build-tutorial:$(date +%s)
27 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push:
13 | branches: ["main"]
14 | pull_request:
15 | branches: ["main"]
16 |
17 | # To cancel a currently running workflow from the same PR, branch, or tag
18 | # when a new workflow is triggered.
19 | concurrency:
20 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | build:
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - uses: actions/checkout@v4
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 |
32 | - name: Set up JDK 23
33 | uses: actions/setup-java@v4 # https://github.com/actions/setup-java
34 | with:
35 | java-version: '23'
36 | distribution: 'temurin'
37 | cache: maven
38 |
39 | - name: Print Java version
40 | run: java -version
41 |
42 | - name: Verify and Package with Maven
43 | run: ./mvnw --batch-mode --file pom.xml verify package
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dependency-reduced-pom.xml
2 | infer-out/
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.6/apache-maven-3.9.6-bin.zip
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "interactive"
3 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | # We use a multi-stage build setup.
4 | # (https://docs.docker.com/build/building/multi-stage/)
5 |
6 | ###############################################################################
7 | # Stage 1 of 2 (to create a "build" image) #
8 | ###############################################################################
9 | # https://hub.docker.com/_/eclipse-temurin
10 | FROM eclipse-temurin:23-jdk-alpine AS builder
11 |
12 | # Smoke test to verify if java is available.
13 | RUN java -version
14 |
15 | ### Build a downsized JRE
16 | # Required for jlink's `--strip-debug` option.
17 | RUN apk add --no-cache binutils
18 | RUN jlink \
19 | --verbose \
20 | --add-modules ALL-MODULE-PATH \
21 | --compress=2 \
22 | --no-header-files \
23 | --no-man-pages \
24 | --strip-debug \
25 | --output /minimal-jre
26 |
27 | # Build and package the app.
28 | COPY . /usr/src/myapp/
29 | WORKDIR /usr/src/myapp/
30 | RUN ./mvnw package
31 |
32 | ###############################################################################
33 | # Stage 2 of 2 (to create a downsized "container executable", ~161MB) #
34 | ###############################################################################
35 | # https://hub.docker.com/_/alpine
36 | FROM alpine:latest
37 | ENV JAVA_HOME=/jre
38 | ENV PATH="${JAVA_HOME}/bin:${PATH}"
39 | RUN apk --no-cache add ca-certificates
40 |
41 | # Add app user.
42 | ARG USER_NAME="appuser"
43 | ARG USER_ID="1000"
44 | ARG GROUP_NAME="apps"
45 | ARG GROUP_ID="1000"
46 | RUN addgroup --gid $GROUP_ID $GROUP_NAME && \
47 | adduser --no-create-home --disabled-password --ingroup $GROUP_NAME --uid $USER_ID $USER_NAME
48 |
49 | # Configure work directory.
50 | ARG APP_DIR=/app
51 | RUN mkdir $APP_DIR && \
52 | chown -R $USER_NAME:$GROUP_NAME $APP_DIR
53 | WORKDIR $APP_DIR
54 |
55 | # Copy downsized JRE from builder image.
56 | COPY --from=builder /minimal-jre $JAVA_HOME
57 |
58 | # Copy packaged app from builder image.
59 | COPY --from=builder --chown=$USER_NAME:$GROUP_NAME /usr/src/myapp/target/app.jar ./app.jar
60 |
61 | # Run the application.
62 | USER $USER_NAME:$GROUP_NAME
63 | EXPOSE 8123
64 | ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "./app.jar"]
65 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Template: Create a Docker image for a Java application
2 |
3 | [](https://github.com/miguno/java-docker-build-tutorial/fork)
4 | [](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/docker-image.yml)
5 | [](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/maven.yml)
6 | [](https://opensource.org/licenses/Apache-2.0)
7 |
8 | A template project to create a Docker image for a Java application.
9 | The [example application](src/main/java/com/miguno/javadockerbuild/App.java)
10 | uses Spring Boot to expose an HTTP endpoint at
11 | [`/welcome`](http://localhost:8123/welcome).
12 |
13 | > [!TIP]
14 | >
15 | > **Golang developer?** Check out https://github.com/miguno/golang-docker-build-tutorial
16 |
17 | Features:
18 |
19 | - The Docker build uses a
20 | [multi-stage build setup](https://docs.docker.com/build/building/multi-stage/)
21 | including a downsized JRE (built inside Docker via `jlink`)
22 | to minimize the size of the generated Docker image, which is **161MB**.
23 | - Supports [Docker BuildKit](https://docs.docker.com/build/)
24 | - Java 23 (Eclipse Temurin) with the [generational ZGC garbage
25 | collector](https://docs.oracle.com/en/java/javase/21/gctuning/z-garbage-collector.html)
26 | - [JUnit 5](https://github.com/junit-team/junit5) for testing,
27 | [Jacoco](https://github.com/jacoco/jacoco) for code coverage,
28 | [SpotBugs](https://github.com/spotbugs/spotbugs) for static code analysis
29 | - Swagger UI and OpenAPI v3 integration via [springdoc](https://springdoc.org/)
30 | at endpoints [/swagger-ui.html](http://localhost:8123/swagger-ui.html) and
31 | and [/v3/api-docs](http://localhost:8123/v3/api-docs)
32 | - [Spring Actuator](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html)
33 | at endpoint [/actuator](http://localhost:8123/actuator), e.g. for
34 | [healthchecks](http://localhost:8123/actuator/health) or [Prometheus
35 | metrics](http://localhost:8123/actuator/prometheus)
36 | - Maven for build management (see [pom.xml](pom.xml)), using
37 | [Maven Wrapper](https://github.com/apache/maven-wrapper)
38 | - [GitHub Actions workflows](https://github.com/miguno/java-docker-build-tutorial/actions) for
39 | [Maven](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/maven.yml)
40 | and
41 | [Docker](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/docker-image.yml)
42 | - Optionally, uses
43 | [just](https://github.com/casey/just)
44 | 
45 | for running common commands conveniently, see [justfile](justfile).
46 | - Uses [.env](.env) as central configuration to set variables used by
47 | [justfile](justfile) and other helper scripts in this project.
48 |
49 | # Requirements
50 |
51 | Docker must be installed on your local machine. That's it. You do not need a
52 | Java JDK or Maven installed.
53 |
54 | # Usage and Demo
55 |
56 | **Step 1:** Create the Docker image according to [Dockerfile](Dockerfile).
57 | This step uses Maven to build, test, and package the Java application according
58 | to [pom.xml](pom.xml). The resulting image is 161MB in size, of which 44MB are
59 | the underlying `alpine` image.
60 |
61 | ```shell
62 | # ***Creating an image may take a few minutes!***
63 | $ docker build --platform linux/x86_64/v8 -t miguno/java-docker-build-tutorial:latest .
64 |
65 | # You can also build with the new BuildKit.
66 | # https://docs.docker.com/build/
67 | $ docker buildx build --platform linux/x86_64/v8 -t miguno/java-docker-build-tutorial:latest .
68 | ```
69 |
70 | Optionally, you can check the size of the generated Docker image:
71 |
72 | ```shell
73 | $ docker images miguno/java-docker-build-tutorial
74 | REPOSITORY TAG IMAGE ID CREATED SIZE
75 | miguno/java-docker-build-tutorial latest bd64d898a04e 2 minutes ago 131MB
76 | ```
77 |
78 | **Step 2:** Start a container for the Docker image.
79 |
80 | ```shell
81 | $ docker run -p 8123:8123 miguno/java-docker-build-tutorial:latest
82 | ```
83 |
84 |
85 | Example output (click to expand)
86 |
87 | ```
88 | Running container from docker image ...
89 | Starting container for image 'miguno/java-docker-build-tutorial:latest', exposing port 8123/tcp
90 | - Run 'curl http://localhost:8123/welcome' to send a test request to the containerized app.
91 | - Enter Ctrl-C to stop the container.
92 |
93 | . ____ _ __ _ _
94 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
95 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
96 | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
97 | ' |____| .__|_| |_|_| |_\__, | / / / /
98 | =========|_|==============|___/=/_/_/_/
99 |
100 | :: Spring Boot :: (v3.3.3)
101 |
102 | 2024-08-26T15:45:08.859Z INFO 1 --- [main] com.miguno.javadockerbuild.App : Starting App v1.0.0-SNAPSHOT using Java 22.0.2 with PID 1 (/app/app.jar started by appuser in /app)
103 | 2024-08-26T15:45:08.868Z INFO 1 --- [main] com.miguno.javadockerbuild.App : No active profile set, falling back to 1 default profile: "default"
104 | 2024-08-26T15:45:10.930Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8123 (http)
105 | 2024-08-26T15:45:10.950Z INFO 1 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
106 | 2024-08-26T15:45:10.951Z INFO 1 --- [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.28]
107 | 2024-08-26T15:45:10.991Z INFO 1 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
108 | 2024-08-26T15:45:10.992Z INFO 1 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2004 ms
109 | 2024-08-26T15:45:12.452Z INFO 1 --- [main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
110 | 2024-08-26T15:45:12.562Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8123 (http) with context path '/'
111 | 2024-08-26T15:45:12.597Z INFO 1 --- [main] com.miguno.javadockerbuild.App : Started App in 5.0 seconds (process running for 6.246)
112 | ```
113 |
114 |
115 |
116 | **Step 3:** Open another terminal and access the example API endpoint of the
117 | running container.
118 |
119 | ```shell
120 | $ curl http://localhost:8123/welcome
121 | {"welcome":"Hello, World!"}
122 | ```
123 |
124 | # Local usage without Docker
125 |
126 | You can also build, test, package, and run the Java application locally
127 | (without Docker) if you have JDK 22+ installed. You do not need to have Maven
128 | installed, because this repository contains the
129 | [Maven Wrapper](https://github.com/apache/maven-wrapper) `mvnw` (use `mvnw.cmd`
130 | on Windows).
131 |
132 | ```shell
133 | # Build, test, package the application locally.
134 | $ ./mvnw clean verify package
135 |
136 | # Run the application locally.
137 | $ ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-XX:+UseZGC -XX:+ZGenerational"
138 |
139 | # Alternatively, run the application locally via its jar file.
140 | $ java -XX:+UseZGC -XX:+ZGenerational -jar target/app.jar
141 | ```
142 |
143 | # Appendix
144 |
145 | ## Hot reloading during development
146 |
147 | This project uses
148 | [spring-boot-devtools](https://docs.spring.io/spring-boot/reference/using/devtools.html)
149 | for fast, automatic application
150 | [restarts](https://docs.spring.io/spring-boot/reference/using/devtools.html#using.devtools.restart)
151 | after code changes.
152 |
153 | - Restarts will be triggered whenever files in the classpath changed, e.g.,
154 | after you ran `./mvnw compile` or after you re-built the project in your IDE.
155 | - This feature works both when running the application inside an IDE like
156 | IntelliJ IDEA as well as when running the application in a terminal with
157 | `./mvnw spring-boot:run`.
158 | - Be patient. After a file changed, it may take a few seconds for the refresh
159 | to happen.
160 |
161 | In IntelliJ IDEA, you can also enable automatic project builds for even more
162 | convenience, using the following settings. Then, whenever you modify a source
163 | file, IDEA will automatically rebuild the project in the background and thus
164 | trigger an automatic restart:
165 |
166 | - `Settings` > `Build, Execution, Deployment` > `Compiler`:
167 | [X] Build project automatically
168 | - `Settings` > `Advanced Settings`:
169 | [X] Allow auto-make to start even if developed application is currently running
170 |
171 | **Restart vs. Reload:** If you want true
172 | [hot reloads](https://docs.spring.io/spring-boot/reference/using/devtools.html#using.devtools.restart.restart-vs-reload)
173 | that are even faster than automatic restarts, look at tools like
174 | [JRebel](https://jrebel.com/software/jrebel/).
175 |
176 | ## Usage with just
177 |
178 | If you have [just](https://github.com/casey/just) installed, you can run the
179 | commands above more conveniently as per this project's [justfile](justfile):
180 |
181 | ```shell
182 | $ just
183 | Available recipes:
184 | [benchmarking]
185 | benchmark-plow # benchmark the app's HTTP endpoint with plow (requires https://github.com/six-ddc/plow)
186 | benchmark-wrk # benchmark the app's HTTP endpoint with wrk (requires https://github.com/wg/wrk)
187 |
188 | [development]
189 | analyze # perform static code analysis
190 | build # alias for 'compile'
191 | clean # clean (remove) the build artifacts
192 | compile # compile the project
193 | coverage # create coverage report
194 | dependencies # list dependency tree of this project
195 | docs # generate Java documentation
196 | format # format sources
197 | format-check # check formatting of sources (without modifying)
198 | infer # static code analysis with infer (requires https://github.com/facebook/infer)
199 | outdated # list outdated dependencies
200 | outdated-plugins # list outdated maven plugins
201 | package # package the app to create an uber jar
202 | send-request-to-app # send request to the app's HTTP endpoint (requires running app)
203 | site # generate site incl. reports for spotbugs, dependencies, javadocs, licenses
204 | spotbugs # static code analysis with spotbugs
205 | start # start the app
206 | start-jar # start the app via its packaged jar (requires 'package' step)
207 | test # run unit tests
208 | verify # run unit and integration tests, coverage check, static code analysis
209 |
210 | [docker]
211 | docker-image-create # create a docker image (requires Docker)
212 | docker-image-run # run the docker image (requires Docker)
213 | docker-image-size # size of the docker image (requires Docker)
214 |
215 | [maven]
216 | maven-active-profiles # list active profiles
217 | maven-all-profiles # list all profiles
218 | maven-help # show help of maven-help-plugin
219 | maven-lifecycles # show maven lifecycles like 'clean', 'compile'
220 | maven-pom # print effective pom.xml
221 | maven-system # print platform details like system properties, env variables
222 | mvnw-upgrade # upgrade maven wrapper
223 |
224 | [project-agnostic]
225 | default # print available targets
226 | evaluate # evaluate and print all just variables
227 | system-info # print system information such as OS and architecture
228 | ```
229 |
230 | Example:
231 |
232 | ```shell
233 | $ just docker-image-create
234 | ```
235 |
236 | # References
237 |
238 | - [How to reduce Java Docker image size](https://blog.monosoul.dev/2022/04/25/reduce-java-docker-image-size/)
239 | (with `jlink`)
240 | - [Creating your own runtime using jlink](https://adoptium.net/blog/2021/10/jlink-to-produce-own-runtime/)
241 | - [Using Jlink in Dockerfiles instead of a JRE](https://adoptium.net/blog/2021/08/using-jlink-in-dockerfiles/)
242 |
--------------------------------------------------------------------------------
/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Run spotless checks on staged files only.
4 | # This is faster than running spotless on all files.
5 | ./mvnw -q spotless:check -DspotlessFiles="$(git diff --staged --name-only | grep ".java$" | sed 's/^/.*/' | paste -sd ',' -)"
6 |
7 | declare errcode=$?
8 | if [ $errcode -ne 0 ]; then
9 | echo
10 | echo "Run \`./mvnw spotless:apply\` to automatically fix these format violations."
11 | exit $errcode
12 | fi
13 |
--------------------------------------------------------------------------------
/images/spring-boot-admin-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miguno/java-docker-build-tutorial/d884d121ae5d6eb7ebfcab38263bac6fd4065e4c/images/spring-boot-admin-dashboard.png
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | # This justfile requires https://github.com/casey/just
2 |
3 | # Load environment variables from `.env` file.
4 | set dotenv-load
5 | # Fail the script if the env file is not found.
6 | set dotenv-required
7 |
8 | project_dir := justfile_directory()
9 | build_dir := project_dir + "/target"
10 | app_uber_jar := build_dir + "/app.jar"
11 |
12 | # print available targets
13 | [group("project-agnostic")]
14 | default:
15 | @just --list --justfile {{justfile()}}
16 |
17 | # evaluate and print all just variables
18 | [group("project-agnostic")]
19 | evaluate:
20 | @just --evaluate
21 |
22 | # print system information such as OS and architecture
23 | [group("project-agnostic")]
24 | system-info:
25 | @echo "architecture: {{arch()}}"
26 | @echo "os: {{os()}}"
27 | @echo "os family: {{os_family()}}"
28 |
29 | # perform static code analysis
30 | [group("development")]
31 | analyze:
32 | #!/usr/bin/env bash
33 | echo "Running static code analysis with spotbugs"
34 | just spotbugs
35 | if command -v infer &>/dev/null; then
36 | echo "Running static code analysis with infer"
37 | just infer
38 | fi
39 |
40 | # benchmark the app's HTTP endpoint with plow (requires https://github.com/six-ddc/plow)
41 | [group("benchmarking")]
42 | benchmark-plow:
43 | @echo plow -c 100 --duration=30s http://localhost:${APP_PORT}/welcome
44 | @plow -c 100 --duration=30s http://localhost:${APP_PORT}/welcome
45 |
46 | # benchmark the app's HTTP endpoint with wrk (requires https://github.com/wg/wrk)
47 | [group("benchmarking")]
48 | benchmark-wrk:
49 | @echo wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/welcome
50 | @wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/welcome
51 |
52 | # alias for 'compile'
53 | [group("development")]
54 | build: compile
55 |
56 | # clean (remove) the build artifacts
57 | [group("development")]
58 | clean:
59 | @./mvnw clean
60 |
61 | # compile the project
62 | [group("development")]
63 | compile:
64 | @./mvnw compile
65 |
66 | # create coverage report
67 | [group("development")]
68 | coverage: verify
69 | @./mvnw jacoco:report && \
70 | echo "Coverage report is available under {{build_dir}}/site/jacoco/"
71 |
72 | # list dependency tree of this project
73 | [group("development")]
74 | dependencies:
75 | @./mvnw dependency:tree
76 |
77 | # create a docker image (requires Docker)
78 | [group("docker")]
79 | docker-image-create:
80 | @echo "Creating a docker image ..."
81 | @./tools/create_image.sh
82 |
83 | # size of the docker image (requires Docker)
84 | [group("docker")]
85 | docker-image-size:
86 | @docker images $DOCKER_IMAGE_NAME
87 |
88 | # run the docker image (requires Docker)
89 | [group("docker")]
90 | docker-image-run:
91 | @echo "Running container from docker image ..."
92 | @./tools/start_container.sh
93 |
94 | # generate Java documentation
95 | [group("development")]
96 | docs:
97 | @./mvnw javadoc:javadoc
98 |
99 | # format sources
100 | [group("development")]
101 | format:
102 | @./mvnw spotless:apply
103 |
104 | # check formatting of sources (without modifying)
105 | [group("development")]
106 | format-check:
107 | @./mvnw spotless:check
108 |
109 | # static code analysis with infer (requires https://github.com/facebook/infer)
110 | [group("development")]
111 | infer:
112 | @infer run -- ./mvnw clean compile
113 |
114 | # list active profiles
115 | [group("maven")]
116 | maven-active-profiles:
117 | @./mvnw help:active-profiles
118 |
119 | # list all profiles
120 | [group("maven")]
121 | maven-all-profiles:
122 | @./mvnw help:all-profiles
123 |
124 | # show maven lifecycles like 'clean', 'compile'
125 | [group("maven")]
126 | maven-lifecycles:
127 | @echo "See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference"
128 |
129 | # print effective pom.xml
130 | [group("maven")]
131 | maven-pom:
132 | @./mvnw help:effective-pom
133 |
134 | # show help of maven-help-plugin
135 | [group("maven")]
136 | maven-help:
137 | @./mvnw help:help
138 |
139 | # print platform details like system properties, env variables
140 | [group("maven")]
141 | maven-system:
142 | @./mvnw help:system
143 |
144 | # upgrade maven wrapper
145 | [group("maven")]
146 | mvnw-upgrade:
147 | @./mvnw wrapper:wrapper
148 |
149 | # list outdated dependencies
150 | [group("development")]
151 | outdated:
152 | @./mvnw versions:display-dependency-updates
153 |
154 | # list outdated maven plugins
155 | [group("development")]
156 | outdated-plugins:
157 | @./mvnw versions:display-plugin-updates
158 |
159 | # package the app to create an uber jar
160 | [group("development")]
161 | package:
162 | @./mvnw verify package
163 |
164 | # send request to the app's HTTP endpoint (requires running app)
165 | [group("development")]
166 | send-request-to-app:
167 | @echo curl http://localhost:${APP_PORT}/welcome
168 | @curl http://localhost:${APP_PORT}/welcome
169 |
170 | # generate site incl. reports for spotbugs, dependencies, javadocs, licenses
171 | [group("development")]
172 | site: compile
173 | @./mvnw site && \
174 | echo "Reports are available under {{build_dir}}/site/" && \
175 | echo "Javadocs are available under {{build_dir}}/site/apidocs/"
176 |
177 | # static code analysis with spotbugs
178 | [group("development")]
179 | spotbugs: compile
180 | @./mvnw spotbugs:check
181 |
182 | # start the app
183 | [group("development")]
184 | start:
185 | #!/usr/bin/env bash
186 | declare -r JVM_ARGS="-XX:+UseZGC -XX:+ZGenerational"
187 | ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="$JVM_ARGS"
188 |
189 | # start the app via its packaged jar (requires 'package' step)
190 | [group("development")]
191 | start-jar:
192 | #!/usr/bin/env bash
193 | APP_JAR="{{app_uber_jar}}"
194 | if [ ! -f "$APP_JAR" ]; then
195 | just package
196 | else
197 | echo "Using existing application uber jar at $APP_JAR."
198 | echo "If you want to recompile the uber jar, run \`./mvnw package\` (or \`just package\`) manually."
199 | fi
200 | declare -r JVM_ARGS="-XX:+UseZGC -XX:+ZGenerational"
201 | java $JVM_ARGS -jar "$APP_JAR"
202 |
203 | # run unit tests
204 | [group("development")]
205 | test:
206 | @./mvnw test
207 |
208 | # run unit and integration tests, coverage check, static code analysis
209 | [group("development")]
210 | verify:
211 | @./mvnw verify
212 |
213 |
--------------------------------------------------------------------------------
/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 |
8 | org.springframework.boot
9 | spring-boot-starter-parent
10 | 3.5.0
11 |
12 |
13 |
14 | com.miguno
15 | java-docker-build
16 | jar
17 | 1.0.0-SNAPSHOT
18 | java-docker-build
19 | A template project to create a minimal Docker image for a Java application
20 | https://github.com/miguno/java-docker-build-tutorial
21 | 2018
22 |
23 |
24 |
25 | Apache License 2.0
26 | https://www.apache.org/licenses/LICENSE-2.0.html
27 | repo
28 |
29 |
30 |
31 |
32 |
33 | miguno
34 | Michael G. Noll
35 |
36 | author
37 |
38 | Europe/Berlin
39 |
40 |
41 |
42 |
43 | https://github.com/miguno/java-docker-build-tutorial
44 | scm:git:https://github.com/miguno/java-docker-build-tutorial.git
45 | scm:git:git@github.com:miguno/java-docker-build-tutorial.git
46 | HEAD
47 |
48 |
49 |
50 |
51 | UTF-8
52 | UTF-8
53 |
54 | 22
55 | ${java.version}
56 | true
57 | ${java.version}
58 | ${java.version}
59 | ${java.version}
60 |
61 | 1.15.0
62 | 2.8.8
63 | 3.4.5
64 |
65 | 3.9.0
66 | 3.5.0
67 | 3.14.0
68 | 3.11.2
69 | 3.5.3
70 | 3.5.0
71 | 0.8.13
72 | 4.9.3
73 | 4.9.3.0
74 | 1.14.0
75 | 2.44.5
76 | 0.15.0
77 |
78 | false
79 | ${skipTests}
80 | ${skipTests}
81 |
90 |
91 |
92 |
93 |
94 |
95 | org.springframework.boot
96 | spring-boot-starter-web
97 |
98 |
99 |
100 |
101 | org.springframework.boot
102 | spring-boot-starter-actuator
103 |
104 |
105 |
106 | org.springframework.boot
107 | spring-boot-starter-security
108 |
109 |
110 |
111 | org.springframework.boot
112 | spring-boot-devtools
113 | true
114 |
115 |
116 |
117 | org.springframework.boot
118 | spring-boot-starter-test
119 | test
120 |
121 |
122 |
123 |
126 | com.github.therapi
127 | therapi-runtime-javadoc
128 | ${therapi.version}
129 |
130 |
131 |
132 |
141 | io.micrometer
142 | micrometer-registry-prometheus
143 | ${micrometer.version}
144 |
145 |
146 |
147 |
155 | org.springdoc
156 | springdoc-openapi-starter-webmvc-ui
157 | ${springdoc.version}
158 |
159 |
160 | org.springdoc
161 | springdoc-openapi-starter-common
162 | ${springdoc.version}
163 |
164 |
165 |
166 |
170 | com.github.spotbugs
171 | spotbugs-annotations
172 | ${spotbugs.version}
173 | provided
174 |
175 |
176 |
177 |
178 |
182 | app
183 |
184 |
185 |
186 |
187 | org.apache.maven.plugins
188 | maven-enforcer-plugin
189 | ${maven-enforcer-plugin.version}
190 |
191 |
192 |
193 | org.apache.maven.plugins
194 | maven-compiler-plugin
195 | ${maven-compiler-plugin.version}
196 |
197 |
198 |
199 |
200 | org.apache.maven.plugins
201 | maven-surefire-plugin
202 | ${maven-surefire-plugin.version}
203 |
204 |
205 |
206 |
207 | org.apache.maven.plugins
208 | maven-failsafe-plugin
209 | ${maven-surefire-plugin.version}
210 |
211 |
212 |
213 |
214 | org.apache.maven.plugins
215 | maven-javadoc-plugin
216 | ${maven-javadoc-plugin.version}
217 |
218 |
219 |
220 |
221 | com.github.spotbugs
222 | spotbugs-maven-plugin
223 | ${spotbugs-maven-plugin.version}
224 |
225 |
229 |
230 | com.github.spotbugs
231 | spotbugs
232 | ${spotbugs.version}
233 |
234 |
235 |
236 |
237 |
238 |
239 | org.jacoco
240 | jacoco-maven-plugin
241 | ${jacoco.version}
242 |
243 |
244 |
245 |
246 | com.diffplug.spotless
247 | spotless-maven-plugin
248 | ${spotless.version}
249 |
250 |
251 |
252 |
253 | com.rudikershaw.gitbuildhook
254 | git-build-hook-maven-plugin
255 | ${git-build-hook-maven-plugin.version}
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | true
264 | org.apache.maven.plugins
265 | maven-enforcer-plugin
266 |
267 |
268 | enforce-maven-version
269 |
270 | enforce
271 |
272 |
273 |
274 |
275 | 3.9.0
276 |
277 |
278 | true
279 |
280 |
281 |
282 |
283 |
284 |
285 | org.apache.maven.plugins
286 | maven-compiler-plugin
287 |
288 |
293 | -proc:full
294 | ${maven.compiler.parameters}
295 |
296 |
301 |
302 |
303 | com.github.therapi
304 | therapi-runtime-javadoc-scribe
305 | ${therapi.version}
306 |
307 |
308 |
309 |
310 |
311 |
312 | org.apache.maven.plugins
313 | maven-surefire-plugin
314 |
315 |
321 | @{argLine} -XX:+EnableDynamicAgentLoading
322 |
323 | 1
324 | ${skipUTs}
325 |
326 |
327 |
328 |
329 | org.apache.maven.plugins
330 | maven-failsafe-plugin
331 |
332 |
338 | @{argLine} -XX:+EnableDynamicAgentLoading
339 |
340 | 1
341 | ${skipTests}
342 | ${skipITs}
343 |
344 |
345 |
346 |
347 | integration-test
348 | verify
349 |
350 |
351 |
352 |
353 |
354 |
355 | org.apache.maven.plugins
356 | maven-javadoc-plugin
357 |
358 |
359 | attach-javadocs
360 |
361 | jar
362 |
363 |
364 |
365 |
366 |
367 |
368 |
373 | org.jacoco
374 | jacoco-maven-plugin
375 |
376 |
377 | default-prepare-agent
378 |
379 | prepare-agent
380 |
381 |
382 | ${project.basedir}/target/jacoco-unit.exec
383 |
384 |
385 |
386 | default-prepare-agent-integration
387 |
388 | prepare-agent-integration
389 |
390 |
391 | ${project.basedir}/target/jacoco-integration.exec
392 | true
393 |
394 |
395 |
396 | default-report
397 | post-integration-test
398 |
399 | merge
400 | report
401 |
402 |
403 |
404 | ${project.basedir}/target/jacoco.exec
405 |
406 |
407 | ${project.basedir}/target/
408 |
409 | jacoco-*.exec
410 |
411 |
412 |
413 |
414 | ${project.basedir}/target/jacoco.exec
415 |
416 |
417 |
418 | default-report-integration
419 |
420 | report-integration
421 |
422 |
423 | ${project.basedir}/target/jacoco.exec
424 |
425 |
426 |
427 | default-check
428 |
429 | check
430 |
431 |
432 |
433 |
434 | BUNDLE
435 |
436 |
437 | COMPLEXITY
438 | COVEREDRATIO
439 | 0.60
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
453 |
454 | com.github.spotbugs
455 | spotbugs-maven-plugin
456 |
457 |
458 | Max
459 | Low
460 | true
461 | spotbugs-include.xml
462 | spotbugs-exclude.xml
463 |
464 |
465 |
466 | com.h3xstream.findsecbugs
467 | findsecbugs-plugin
468 | ${findsecbugs.version}
469 |
470 |
471 |
472 |
473 | true
474 | ${project.basedir}/target/site
475 |
476 |
477 |
478 |
479 | mvn-package-runs-spotbugs
480 |
481 | check
482 |
483 | package
484 |
485 |
486 |
487 |
488 |
489 | com.diffplug.spotless
490 | spotless-maven-plugin
491 |
492 |
493 |
494 | apply
495 |
496 | compile
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 | src/main/java/**/*.java
507 | src/test/java/**/*.java
508 |
509 |
510 |
511 | java|javax,,\#
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 | com.rudikershaw.gitbuildhook
521 | git-build-hook-maven-plugin
522 | ${git-build-hook-maven-plugin.version}
523 |
524 |
525 |
526 | hooks/
527 |
528 |
529 |
530 |
531 |
532 |
533 | configure
534 |
535 |
536 |
537 |
538 |
539 |
540 | org.springframework.boot
541 | spring-boot-maven-plugin
542 |
543 |
544 | pre-integration-test
545 |
546 | start
547 |
548 |
549 |
550 | post-integration-test
551 |
552 | stop
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 | org.apache.maven.plugins
564 | maven-project-info-reports-plugin
565 | ${maven-project-info-reports-plugin.version}
566 |
567 |
568 |
569 | org.apache.maven.plugins
570 | maven-javadoc-plugin
571 | ${maven-javadoc-plugin.version}
572 |
573 |
574 |
575 |
576 | com.github.spotbugs
577 | spotbugs-maven-plugin
578 | ${spotbugs-maven-plugin.version}
579 |
580 |
581 | -Duser.language=en
582 | Max
583 | Low
584 | true
585 | spotbugs-include.xml
586 | spotbugs-exclude.xml
587 |
588 |
589 |
590 | com.h3xstream.findsecbugs
591 | findsecbugs-plugin
592 | ${findsecbugs.version}
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
--------------------------------------------------------------------------------
/spotbugs-exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/spotbugs-include.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/App.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /** An example application that exposes an HTTP endpoint. */
7 | @SpringBootApplication
8 | public class App {
9 |
10 | public static void main(String[] args) {
11 | SpringApplication.run(App.class, args);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/controllers/RootController.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.controllers;
2 |
3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | /** Implements a basic landing page at endpoint `/`. */
9 | @SuppressFBWarnings("SPRING_ENDPOINT")
10 | @RestController
11 | public class RootController {
12 |
13 | @Value("${app.spring-boot-admin.role.user.name}")
14 | private String roleUserName;
15 |
16 | @Value("${spring.application.name}")
17 | private String appName;
18 |
19 | @Value("${app.spring-boot-admin.role.user.password}")
20 | private String roleUserPassword;
21 |
22 | @Value("${app.spring-boot-admin.role.admin.name}")
23 | private String roleAdminName;
24 |
25 | @Value("${app.spring-boot-admin.role.admin.password}")
26 | private String roleAdminPassword;
27 |
28 | /**
29 | * Returns a basic landing page for this application.
30 | *
31 | * @return Basic landing page in HTML format.
32 | */
33 | @GetMapping("/")
34 | @SuppressFBWarnings("VA_FORMAT_STRING_USES_NEWLINE")
35 | public String root() {
36 | return String.format(
37 | """
38 | Welcome to %s
39 | Enjoy playing around with this application!
40 | Example Endpoints
41 |
47 | User Accounts
48 | For endpoints that require login.
49 |
50 | - Admin user:
%s
with password %s
51 | - Regular user:
%s
with password %s
52 |
53 |
55 | """,
56 | appName, roleAdminName, roleAdminPassword, roleUserName, roleUserPassword);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/controllers/WelcomeController.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.controllers;
2 |
3 | import com.miguno.javadockerbuild.models.Welcome;
4 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.PathVariable;
7 | import org.springframework.web.bind.annotation.RequestParam;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | /** The API endpoint exposed by this example application. */
11 | @SuppressFBWarnings("SPRING_ENDPOINT")
12 | @RestController
13 | public class WelcomeController {
14 |
15 | private static final String template = "Hello, %s!";
16 |
17 | /**
18 | * Returns a welcome message to the client.
19 | *
20 | * @param name The name to greet.
21 | * @return A personalized welcome message.
22 | */
23 | @GetMapping("/welcome")
24 | public Welcome welcome(@RequestParam(value = "name", defaultValue = "World") String name) {
25 | // Note: If you make changes to the URL path, remember to update AppSecurityConfiguration.
26 | return new Welcome(String.format(template, name));
27 | }
28 |
29 | /**
30 | * Returns a welcome message to the client.
31 | *
32 | * @param name The name to greet.
33 | * @return A personalized welcome message.
34 | */
35 | @GetMapping("/welcome/{name}")
36 | public Welcome welcomeWithPathVariable(@PathVariable(value = "name") String name) {
37 | // Note: If you make changes to the URL path, remember to update AppSecurityConfiguration.
38 | return new Welcome(String.format(template, name));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/models/Welcome.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.models;
2 |
3 | public record Welcome(String welcome) {}
4 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/security/AppSecurityConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.security;
2 |
3 | import java.util.UUID;
4 |
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.boot.autoconfigure.security.SecurityProperties;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.security.config.Customizer;
10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12 | import org.springframework.security.core.userdetails.User;
13 | import org.springframework.security.core.userdetails.UserDetails;
14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15 | import org.springframework.security.crypto.password.PasswordEncoder;
16 | import org.springframework.security.provisioning.InMemoryUserDetailsManager;
17 | import org.springframework.security.web.SecurityFilterChain;
18 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
19 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
20 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
21 | import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
22 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
23 |
24 | /** Secures the endpoints of this application. */
25 | @Configuration(proxyBeanMethods = false)
26 | @EnableWebSecurity
27 | public class AppSecurityConfiguration {
28 |
29 | @Value("${app.spring-boot-admin.role.user.name}")
30 | private String roleUserName;
31 |
32 | @Value("${app.spring-boot-admin.role.user.password}")
33 | private String roleUserPassword;
34 |
35 | @Value("${app.spring-boot-admin.role.admin.name}")
36 | private String roleAdminName;
37 |
38 | @Value("${app.spring-boot-admin.role.admin.password}")
39 | private String roleAdminPassword;
40 |
41 | public AppSecurityConfiguration(SecurityProperties security) {}
42 |
43 | /**
44 | * Applies security policies such as authentication requirements to endpoints.
45 | *
46 | * @param http Supplied by Spring.
47 | * @return The applications' security filter chain.
48 | * @throws Exception Unclear when that happens.
49 | */
50 | @Bean
51 | protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
52 | SavedRequestAwareAuthenticationSuccessHandler successHandler =
53 | new SavedRequestAwareAuthenticationSuccessHandler();
54 | successHandler.setTargetUrlParameter("redirectTo");
55 | successHandler.setDefaultTargetUrl("/");
56 |
57 | // NOTE: In this project, the Spring Boot Admin server and client are colocated in the same
58 | // application for demonstration purposes. In production, you would typically not do that
59 | // and instead separate the code and functionality. See the recommendations of Spring Boot
60 | // Admin at https://docs.spring-boot-admin.com/current/faq.html.
61 | // The effect of this colocation is that this application contains endpoints for both
62 | // server and client, and the authorization settings below also apply to both: if you
63 | // permit access to a URL in the "for the server" section you also permit access for the
64 | // client, and vice versa. Again, this would be different in production where the server
65 | // and the clients would be separate applications and processes.
66 | http.authorizeHttpRequests(
67 | (authorizeRequests) ->
68 | authorizeRequests
69 | .requestMatchers(
70 | new AntPathRequestMatcher("/"),
71 | // Permit public access to this app's example endpoint at `/welcome`.
72 | new AntPathRequestMatcher("/welcome/**"),
73 | // Permit public access to Swagger.
74 | new AntPathRequestMatcher("/swagger-ui.html"),
75 | new AntPathRequestMatcher("/v3/api-docs"),
76 | // Permit public access to a subset of actuator endpoints.
77 | new AntPathRequestMatcher("/actuator/health"),
78 | new AntPathRequestMatcher("/actuator/info"),
79 | new AntPathRequestMatcher("/actuator/prometheus"))
80 | .permitAll()
81 | // All other requests must be authenticated.
82 | .anyRequest()
83 | .authenticated())
84 | // Enables HTTP Basic Authentication support.
85 | .httpBasic(Customizer.withDefaults());
86 |
87 | // Enables CSRF-Protection using cookies.
88 | http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class)
89 | .csrf(
90 | (csrf) ->
91 | csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
92 | .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()));
93 |
94 | http.rememberMe(
95 | (rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600));
96 |
97 | return http.build();
98 | }
99 |
100 | /** Required to provide UserDetailsService for "remember functionality". */
101 | @Bean
102 | public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
103 | // NOTE: HTTP Basic Authentication itself is not recommended for production.
104 | UserDetails user =
105 | User.withUsername(roleUserName)
106 | .password(passwordEncoder.encode(roleUserPassword))
107 | .roles("USER")
108 | .build();
109 | UserDetails admin =
110 | User.withUsername(roleAdminName)
111 | .password(passwordEncoder.encode(roleAdminPassword))
112 | .roles("ADMIN", "USER")
113 | .build();
114 | return new InMemoryUserDetailsManager(user, admin);
115 | }
116 |
117 | @Bean
118 | public PasswordEncoder passwordEncoder() {
119 | return new BCryptPasswordEncoder();
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/com/miguno/javadockerbuild/security/CustomCsrfFilter.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.security;
2 |
3 | import java.io.IOException;
4 |
5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
6 | import jakarta.servlet.FilterChain;
7 | import jakarta.servlet.ServletException;
8 | import jakarta.servlet.http.Cookie;
9 | import jakarta.servlet.http.HttpServletRequest;
10 | import jakarta.servlet.http.HttpServletResponse;
11 | import org.springframework.lang.NonNull;
12 | import org.springframework.security.web.csrf.CsrfToken;
13 | import org.springframework.web.filter.OncePerRequestFilter;
14 | import org.springframework.web.util.WebUtils;
15 |
16 | /** A custom CSRF Filter, derived from the Spring Boot Admin documentation. */
17 | public class CustomCsrfFilter extends OncePerRequestFilter {
18 |
19 | public static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";
20 |
21 | @SuppressFBWarnings(
22 | value = "COOKIE_USAGE",
23 | justification =
24 | "CSRF tokens are designed to be stored in cookies with appropriate security controls")
25 | @Override
26 | protected void doFilterInternal(
27 | @NonNull HttpServletRequest request,
28 | @NonNull HttpServletResponse response,
29 | @NonNull FilterChain filterChain)
30 | throws ServletException, IOException {
31 | CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
32 | if (csrf != null) {
33 | Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME);
34 | String token = csrf.getToken();
35 |
36 | if (cookie == null || token != null && !token.equals(cookie.getValue())) {
37 | cookie = new Cookie(CSRF_COOKIE_NAME, token);
38 | cookie.setPath("/");
39 | cookie.setHttpOnly(true);
40 | cookie.setSecure(true);
41 | response.addCookie(cookie);
42 | }
43 | }
44 | filterChain.doFilter(request, response);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | ### Custom app-specific settings ###
3 | ################################################################################
4 | # Credentials of role "ADMIN" for HTTP Basic Authentication.
5 | app.spring-boot-admin.role.admin.name=admin
6 | app.spring-boot-admin.role.admin.password=admin
7 | # Credentials of role "USER" for HTTP Basic Authentication.
8 | app.spring-boot-admin.role.user.name=demouser
9 | app.spring-boot-admin.role.user.password=demopass
10 |
11 | ################################################################################
12 | ### Spring settings ###
13 | ################################################################################
14 | spring.application.name=java-docker-build-tutorial
15 | server.port=8123
16 |
17 | # Enable virtual threads (requires Java 21+).
18 | # Virtual threads may come with downsides for your Spring Boot application,
19 | # see read the documentation before you enable them here.
20 | # https://docs.spring.io/spring-boot/reference/features/spring-application.html#features.spring-application.virtual-threads
21 | #spring.threads.virtual.enabled=true
22 |
23 | # Spring Actuator configuration
24 | # https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
25 | #
26 | # Expose all endpoints by default via `*`. However, `AppSecurityConfiguration`
27 | # only permits public access to a subset of endpoints, whereas the rest requires
28 | # HTTP Basic Authentication.
29 | #
30 | # IMPORTANT: In production, you should choose carefully what endpoints to expose!
31 | management.endpoints.web.exposure.include=*
32 | # Enable the env contributor.
33 | management.info.env.enabled=true
34 |
--------------------------------------------------------------------------------
/src/main/resources/templates/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miguno/java-docker-build-tutorial/d884d121ae5d6eb7ebfcab38263bac6fd4065e4c/src/main/resources/templates/.keep
--------------------------------------------------------------------------------
/src/test/java/com/miguno/javadockerbuild/SmokeTest.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild;
2 |
3 | import com.miguno.javadockerbuild.controllers.WelcomeController;
4 | import org.junit.jupiter.api.Test;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 |
8 | @SpringBootTest
9 | class SmokeTest {
10 |
11 | @Autowired private WelcomeController controller;
12 |
13 | @Test
14 | void verifyThatApplicationContextLoads() {
15 | // Will fail if the application context cannot start.
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/java/com/miguno/javadockerbuild/controllers/WelcomeControllerIT.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.controllers;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.skyscreamer.jsonassert.JSONAssert;
5 | import org.skyscreamer.jsonassert.JSONCompareMode;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.context.SpringBootTest;
8 | import org.springframework.boot.test.web.client.TestRestTemplate;
9 | import org.springframework.http.ResponseEntity;
10 |
11 | /** An example integration test for the API endpoint `/welcome`. */
12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
13 | public class WelcomeControllerIT {
14 |
15 | @Autowired private TestRestTemplate template;
16 |
17 | @Test
18 | public void welcome() throws Exception {
19 | ResponseEntity response = template.getForEntity("/welcome", String.class);
20 | String expectedJson =
21 | """
22 | {"welcome":"Hello, World!"}
23 | """;
24 | JSONAssert.assertEquals(expectedJson, response.getBody(), JSONCompareMode.STRICT);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/java/com/miguno/javadockerbuild/controllers/WelcomeControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.miguno.javadockerbuild.controllers;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.test.web.servlet.MockMvc;
9 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
10 |
11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
13 |
14 | /** Example unit tests for the API endpoint `/welcome`. */
15 | @SpringBootTest
16 | @AutoConfigureMockMvc
17 | public class WelcomeControllerTest {
18 |
19 | @Autowired private MockMvc mvc;
20 |
21 | @Test
22 | public void getWelcome() throws Exception {
23 | String expectedJson =
24 | """
25 | {"welcome":"Hello, World!"}
26 | """;
27 |
28 | mvc.perform(MockMvcRequestBuilders.get("/welcome").accept(MediaType.APPLICATION_JSON))
29 | .andExpect(status().isOk())
30 | .andExpect(content().json(expectedJson));
31 | }
32 |
33 | @Test
34 | public void getWelcomeWithPathVariable() throws Exception {
35 | String expectedJson =
36 | """
37 | {"welcome":"Hello, Gandalf!"}
38 | """;
39 |
40 | mvc.perform(MockMvcRequestBuilders.get("/welcome/Gandalf").accept(MediaType.APPLICATION_JSON))
41 | .andExpect(status().isOk())
42 | .andExpect(content().json(expectedJson));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tools/create_image.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # shellcheck disable=SC2155
3 |
4 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
5 | # `-u`: Errors if a variable is referenced before being set
6 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked
7 | set -uo pipefail
8 |
9 | declare -r SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
10 | declare -r PROJECT_DIR=$(readlink -f "$SCRIPT_DIR/..")
11 |
12 | # Import environment variables from .env
13 | set -o allexport && source "$PROJECT_DIR/.env" && set +o allexport
14 |
15 | # Check requirements
16 | if ! command -v docker &>/dev/null; then
17 | echo "ERROR: 'docker' command not available. Is Docker installed?"
18 | exit 1
19 | fi
20 |
21 | echo "Building image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG'..."
22 | # TIP: Add `--progress=plain` to see the full docker output when you are
23 | # troubleshooting the build setup of your image.
24 | #
25 | # Force amd64 as the platform. This workaround is needed on Apple Silicon
26 | # machines. Details at https://stackoverflow.com/questions/72152446/.
27 | declare -r DOCKER_OPTIONS="--platform linux/amd64"
28 | # Use BuildKit, i.e. `buildx build` instead of just `build`
29 | # https://docs.docker.com/build/
30 | #
31 | # shellcheck disable=SC2086
32 | docker buildx build $DOCKER_OPTIONS -t "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG" .
33 |
--------------------------------------------------------------------------------
/tools/start_container.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # shellcheck disable=SC2155
3 |
4 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
5 | # `-u`: Errors if a variable is referenced before being set
6 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked
7 | set -uo pipefail
8 |
9 | declare -r SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
10 | declare -r PROJECT_DIR=$(readlink -f "$SCRIPT_DIR/..")
11 |
12 | # Import environment variables from .env
13 | set -o allexport && source "$PROJECT_DIR/.env" && set +o allexport
14 |
15 | # Check requirements
16 | if ! command -v docker &>/dev/null; then
17 | echo "ERROR: 'docker' command not available. Is Docker installed?"
18 | exit 1
19 | fi
20 |
21 | # shellcheck disable=SC2155
22 | declare -r OS="$(uname -s)"
23 | # "arm64" for Apple Silicon (M1/M2/M3/etc.)
24 | # shellcheck disable=SC2155
25 | declare -r ARCH="$(uname -m)"
26 |
27 | DOCKER_OPTIONS=""
28 | if [[ "$OS" = "Darwin" && "$ARCH" = "arm64" ]]; then
29 | # Force amd64 as the platform. This workaround is needed on Apple Silicon
30 | # machines. Details at https://stackoverflow.com/questions/72152446/.
31 | DOCKER_OPTIONS="--platform linux/amd64"
32 | fi
33 |
34 | echo "Starting container for image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG', exposing port ${APP_PORT}/tcp"
35 | echo "- Run 'curl http://localhost:${APP_PORT}/welcome' to send a test request to the containerized app."
36 | echo "- Enter Ctrl-C to stop the container."
37 | # shellcheck disable=SC2086
38 | docker run $DOCKER_OPTIONS -p "$APP_PORT:$APP_PORT" "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG"
39 |
--------------------------------------------------------------------------------