├── .gitignore
├── .gitlab-ci.yml
├── README.md
├── git_workflow.jpg
├── ivans_checkstyle_config.xml
├── pom.xml
└── src
├── main
├── docker
│ ├── Dockerfile
│ └── application
│ │ ├── bin
│ │ └── start-app.sh
│ │ └── config
│ │ ├── application.properties
│ │ └── log4j2.xml
├── java
│ ├── module-info.java
│ └── se
│ │ └── ivankrizsan
│ │ └── spring
│ │ └── hellowebapp
│ │ ├── HelloHandler.java
│ │ ├── HelloRouter.java
│ │ └── HelloWebappApplication.java
└── resources
│ └── application.properties
└── test
└── java
└── se
└── ivankrizsan
└── spring
└── hellowebapp
├── HelloHandlerTests.java
└── HelloWebappApplicationTests.java
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**
5 | !**/src/test/**
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 |
30 | ### VS Code ###
31 | .vscode/
32 |
33 | # To avoid Maven checking in the entire local repository when pushing
34 | # changes to the repository.
35 | .m2/
36 | # Maven-related files that are never to be pushed to the repository.
37 | pom.xml.tag
38 | pom.xml.releaseBackup
39 | pom.xml.versionsBackup
40 | pom.xml.next
41 | release.properties
42 | dependency-reduced-pom.xml
43 | buildNumber.properties
44 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: maven:3.6.1-jdk-11
2 |
3 | variables:
4 | MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
5 | MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end"
6 | DOCKER_IMAGE_TO_SCAN: hello-webapp:latest
7 |
8 | # Cache the Maven repository so that each job does not have to download it.
9 | cache:
10 | key: mavenrepo
11 | paths:
12 | - ./.m2/repository/
13 |
14 | stages:
15 | - build
16 | - release
17 | - create_docker_image
18 | - scan_docker_image
19 | - push_docker_image
20 |
21 | # Run tests.
22 | test:
23 | stage: build
24 | script:
25 | - 'mvn $MAVEN_CLI_OPTS install'
26 |
27 | # Checkstyle source code standard review.
28 | checkstyle:
29 | stage: build
30 | script:
31 | - 'mvn $MAVEN_CLI_OPTS -Pcicdprofile checkstyle:check'
32 |
33 | # PMD code quality analysis.
34 | pmd:
35 | stage: build
36 | script:
37 | - 'mvn $MAVEN_CLI_OPTS -Pcicdprofile pmd:check'
38 |
39 | # SpotBugs code quality analysis.
40 | spotbugs:
41 | stage: build
42 | script:
43 | - 'mvn $MAVEN_CLI_OPTS -Pcicdprofile spotbugs:check'
44 |
45 | # Test code coverage analysis.
46 | code-coverage:
47 | stage: build
48 | script:
49 | - 'mvn $MAVEN_CLI_OPTS -P-Pcicdprofile install'
50 |
51 | # Supplies the option to perform Maven releases from the master branch.
52 | # Releases need to be triggered manually in the GitLab CI/CD pipeline.
53 | master-release:
54 | stage: release
55 | when: manual
56 | script:
57 | - git config --global user.email "gitlab@ivankrizsan.se"
58 | - git config --global user.name "GitLab CI/CD"
59 | # Fix the repository URL, replacing any host, localhost in my case, with gitlab.
60 | # Note that gitlab is the name of the container in which GitLab is running.
61 | # Insert GitLab access token into URL so release tag and next snapshot version
62 | # can be pushed to the repository.
63 | - export NEW_REPO_URL=$(echo $CI_REPOSITORY_URL | sed 's/@[^/]*/@gitlab/' | sed 's/\(http[s]*\):\/\/[^@]*/\1:\/\/oauth2:'$GITLAB_CICD_TOKEN'/')
64 | # Debug git interaction.
65 | - 'export GIT_TRACING=2'
66 | - 'export GIT_CURL_VERBOSE=1'
67 | # Remove the SNAPSHOT from the project's version thus creating the release version number.
68 | - 'mvn $MAVEN_CLI_OPTS versions:set -DremoveSnapshot -DprocessAllModules=true'
69 | - 'export RELEASE_VERSION=$(mvn --batch-mode --no-transfer-progress --non-recursive help:evaluate -Dexpression=project.version | grep -v "\[.*")'
70 | - 'echo "Release version: $RELEASE_VERSION"'
71 | # Push the release version to a new tag.
72 | # This relies on the .m2 directory containing the Maven repository
73 | # in the build directory being included in the .gitignore file in the
74 | # project, since we do not want to commit the contents of the Maven repository.
75 | - 'git add $CI_PROJECT_DIR'
76 | - 'git commit -m "Create release version"'
77 | - 'git tag -a $RELEASE_VERSION -m "Create release version tag"'
78 | - 'git remote set-url --push origin $NEW_REPO_URL'
79 | - 'git push origin $RELEASE_VERSION'
80 | # Update master branch to next snapshot version.
81 | # If automatic building of the master branch is desired, remove
82 | # the "[ci skip]" part in the commit message.
83 | - 'git checkout master'
84 | - 'git reset --hard "origin/master"'
85 | - 'git remote set-url --push origin $NEW_REPO_URL'
86 | - 'mvn $MAVEN_CLI_OPTS versions:set -DnextSnapshot=true -DprocessAllModules=true'
87 | - 'git add $CI_PROJECT_DIR'
88 | - 'git commit -m "Create next snapshot version [ci skip]"'
89 | - 'git push origin master'
90 | only:
91 | - master
92 |
93 | # Builds release version tags as to create release artifact(s).
94 | # Artifacts are retained 2 weeks if the Keep button in the web GUI
95 | # is not clicked, in which case they will be retained forever.
96 | release-build:
97 | stage: release
98 | script:
99 | - 'mvn $MAVEN_CLI_OPTS install'
100 | only:
101 | - /^\d+\.\d+\.\d+$/
102 | - tags
103 | artifacts:
104 | paths:
105 | - target/*.jar
106 | expire_in: 2 weeks
107 |
108 | # Build a Docker image.
109 | # Action can be manually triggered in the GitLab CI/CD pipeline
110 | # of release tags.
111 | create-docker-image:
112 | stage: create_docker_image
113 | when: manual
114 | before_script:
115 | # Install a Docker client in the container as to be able to build Docker image(s).
116 | - wget -q https://download.docker.com/linux/static/stable/x86_64/docker-18.09.6.tgz
117 | - tar zxvf docker*.tgz
118 | - cp docker/docker /usr/local/bin/docker
119 | script:
120 | - mvn -Pdockerimage docker:build
121 | only:
122 | - /^\d+\.\d+\.\d+$/
123 | - tags
124 |
125 | # Scan the (local) Docker image using Clair.
126 | scan-docker-image:
127 | image: docker:stable
128 | stage: scan_docker_image
129 | when: manual
130 | variables:
131 | DOCKER_HOST: unix:///var/run/docker.sock
132 | CLAIR_DB_CONTAINER_NAME: clairdb_$CI_CONCURRENT_PROJECT_ID
133 | CLAIR_CONTAINER_NAME: clair_$CI_CONCURRENT_PROJECT_ID
134 | before_script:
135 | # Start an instance of Postgresql with a pre-populated Clair DB.
136 | - docker run -d --name $CLAIR_DB_CONTAINER_NAME --network=gitlabnetwork arminc/clair-db:latest
137 | # Start the Clair server.
138 | - docker run -p 6060:6060 --link $CLAIR_DB_CONTAINER_NAME:postgres --network=gitlabnetwork -d --name $CLAIR_CONTAINER_NAME --restart on-failure arminc/clair-local-scan:v2.0.6
139 | # Download Clair scanner client.
140 | - wget -nv -qO clair-scanner https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
141 | - chmod +x clair-scanner
142 | script:
143 | # Scan the Docker image built in the previous step using Clair.
144 | - ./clair-scanner --ip="$(hostname -i)" -c "http://$CLAIR_CONTAINER_NAME:6060" $DOCKER_IMAGE_TO_SCAN
145 | after_script:
146 | # Stop and remove the Clair DB container.
147 | - if docker stop $CLAIR_DB_CONTAINER_NAME ; then echo "Clair DB container stopped"; else echo "There is no Clair DB container to stop"; fi
148 | - if docker rm $CLAIR_DB_CONTAINER_NAME ; then echo "Clair DB container removed"; else echo "There is no Clair DB container to remove"; fi
149 | # Remove the Clair container.
150 | - if docker stop $CLAIR_CONTAINER_NAME ; then echo "Clair container stopped"; else echo "There is no Clair container to stop"; fi
151 | - if docker rm $CLAIR_CONTAINER_NAME ; then echo "Clair container removed"; else echo "There is no Clair container to remove"; fi
152 | only:
153 | - /^\d+\.\d+\.\d+$/
154 | - tags
155 |
156 | # Push the Docker image to a repository.
157 | # In this example the repository is DockerHub.
158 | push-docker-image:
159 | stage: push_docker_image
160 | when: manual
161 | script:
162 | - docker login --username ivan --password secret
163 | - docker push $DOCKER_IMAGE_TO_SCAN
164 | only:
165 | - /^\d+\.\d+\.\d+$/
166 | - tags
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Git Workflow Sample
2 | A trivial Maven-based Spring Boot application that is used as an example project in which
3 | a GitLab CI/CD pipeline for the following git workflow is added.
4 | 
5 | ## Code Quality
6 | The following Maven goals can be executed on the project to ensure code quality.
7 | In the version of the project that has no CI/CD pipeline, these goals need to be executed manually.
8 |
9 |
mvn -Pcicdprofile checkstyle:check
10 |
Verify that the source-code adheres to the Ivan Coding Style.
11 | mvn -Pcicdprofile pmd:check
12 |
Performs source-code analysis checking for common programming flaws.
13 | mvn -Pcicdprofile spotbugs:check
14 |
Performs static code analysis looking for common bug patterns.
15 |
16 | mvn -Pcicdprofile install
17 |
Ensure that the minimum amounts of code covered by tests is reached.
18 |
19 | ## After
20 | This branch contains the project after the GitLab CI/CD pipeline was added.
21 |
22 | # Article
23 | Link to article in which this project is used:
24 | https://www.ivankrizsan.se/2019/09/28/gitlab-ci-cd-pipeline-for-maven-based-applications/
25 |
--------------------------------------------------------------------------------
/git_workflow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krizsan/git-workflow-sampleapp/19ca0678d3e163bbb32770f0b0cba8e2f8456305/git_workflow.jpg
--------------------------------------------------------------------------------
/ivans_checkstyle_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
164 |
165 |
166 |
167 |
168 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.1.6.RELEASE
9 |
10 |
11 | se.ivankrizsan.spring
12 | git-workflow-sampleapp
13 | 1.0.0-SNAPSHOT
14 | git-workflow-sampleapp
15 |
16 |
17 | 11
18 |
19 |
20 |
21 |
22 | org.springframework.boot
23 | spring-boot-starter-webflux
24 |
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-starter-test
29 | test
30 |
31 |
32 | io.projectreactor
33 | reactor-test
34 | test
35 |
36 |
37 |
38 |
39 |
40 |
41 | org.springframework.boot
42 | spring-boot-maven-plugin
43 |
44 |
45 |
46 |
47 |
48 |
49 | cicdprofile
50 |
51 | -Xdoclint:none
52 |
53 |
54 |
55 |
56 |
57 | org.jacoco
58 | jacoco-maven-plugin
59 | 0.8.4
60 |
61 |
62 | default-prepare-agent
63 |
64 | prepare-agent
65 |
66 |
67 |
68 | default-report
69 |
70 | report
71 |
72 |
73 |
74 | default-check
75 |
76 | check
77 |
78 |
79 |
80 |
81 | CLASS
82 |
83 |
84 |
85 |
86 | LINE
87 | COVEREDRATIO
88 | 70%
89 |
90 |
91 |
92 |
93 | BUNDLE
94 |
95 |
96 |
97 |
98 | CLASS
99 | COVEREDRATIO
100 | 90%
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | org.apache.maven.plugins
112 | maven-pmd-plugin
113 | 3.12.0
114 |
115 | ${java.version}
116 | true
117 | true
118 | true
119 |
120 |
121 |
122 |
123 | com.github.spotbugs
124 | spotbugs-maven-plugin
125 | 3.1.12
126 |
127 | true
128 | Max
129 | Low
130 | true
131 |
132 |
133 |
134 |
135 | org.apache.maven.plugins
136 | maven-checkstyle-plugin
137 | 3.1.0
138 |
139 | ivans_checkstyle_config.xml
140 | true
141 | module-info.java
142 | true
143 | true
144 | true
145 |
146 |
147 |
148 |
149 |
150 |
151 |
156 |
157 | dockerimage
158 |
159 |
160 | hello-webapp
161 |
162 | src/main/docker
163 |
168 | ${project.build.directory}/dockerimgbuild
169 |
170 | unix://var/run/docker.sock
171 |
172 | Dockerfile
173 |
174 |
175 |
176 |
177 | maven-resources-plugin
178 |
179 |
180 | copy-resources
181 | package
182 |
183 | copy-resources
184 |
185 |
186 | ${docker.build.directory}
187 |
188 |
189 | ${docker.image.src.root}
190 | false
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
202 |
203 | org.apache.maven.plugins
204 | maven-dependency-plugin
205 |
206 |
207 | copy
208 | package
209 |
210 | copy
211 |
212 |
213 |
214 |
215 |
221 | ${project.groupId}
222 | ${project.artifactId}
223 | ${project.version}
224 | jar
225 | true
226 | ${docker.build.directory}/application/lib
227 |
231 | hello-webapp.jar
232 |
233 |
237 |
238 | ${docker.build.directory}
239 | true
240 | true
241 |
242 |
243 |
244 |
245 |
246 |
247 | io.fabric8
248 | docker-maven-plugin
249 | 0.19.0
250 |
251 | ${docker.host.url}
252 |
253 |
254 | ${docker.image.name}
255 |
256 |
257 | ${project.version}
258 | latest
259 |
260 | ${docker.build.directory}/${docker.file.name}
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
--------------------------------------------------------------------------------
/src/main/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:11.0.3-jdk-stretch
2 |
3 | # Absolute path to the JAR file to be launched when a Docker container is started.
4 | ENV JAR_PATH=/application/lib/hello-webapp.jar \
5 | # Default is to set timezone on container start.
6 | SET_CONTAINER_TIMEZONE=true \
7 | # Default timezone.
8 | CONTAINER_TIMEZONE=Europe/Stockholm \
9 | # Path to application configuration file in image.
10 | CONFIG_LOCATION=file:/application/config/application.properties \
11 | # User which will run the application in containers.
12 | RUN_AS_USER=runner
13 |
14 | # User which will run the application in containers - build argument.
15 | # Must be the same as the above RUN_AS_USER environment variable.
16 | ARG RUN_AS_USER_ARG=runner
17 |
18 | # Add user and group which will run the application in containers.
19 | RUN groupadd -f ${RUN_AS_USER_ARG} && \
20 | useradd --system --home /home/${RUN_AS_USER_ARG} -g ${RUN_AS_USER_ARG} ${RUN_AS_USER_ARG} && \
21 |
22 | # Install NTP for time synchronization and gosu for step-down from root.
23 | # Cannot use the USER command in the Docker file since the startup script
24 | # needs to be executed as root in order to be able to start NTP.
25 | apt-get update && \
26 | apt-get dist-upgrade -y && \
27 | apt-get install -y ntp gosu && \
28 | # Clean up after instals.
29 | apt-get autoclean && apt-get --purge -y autoremove && \
30 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
31 |
32 | # Create directory to hold the application and all its contents in the Docker image.
33 | mkdir /application && \
34 | mkdir /application/config && \
35 | mkdir /application/certs && \
36 | mkdir /application/bin
37 |
38 | # Copy all the static contents to be included in the Docker image.
39 | COPY ./application/ /application/
40 | COPY ./application/config/* /application/config/
41 | COPY ./application/bin/* /application/bin/
42 |
43 | # Make all scripts in the bin directory executable. Includes start-script.
44 | RUN chmod +x /application/bin/*.sh && \
45 | # Set the owner of all application-related files to the user which will
46 | # run the application in containers.
47 | chown -R ${RUN_AS_USER_ARG}:${RUN_AS_USER_ARG} /application/
48 |
49 | # Web port.
50 | EXPOSE 8080
51 |
52 | CMD [ "/application/bin/start-app.sh" ]
53 |
54 |
--------------------------------------------------------------------------------
/src/main/docker/application/bin/start-app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | # Set the timezone of the container.
5 | if [ "$SET_CONTAINER_TIMEZONE" = "true" ]; then
6 | echo ${CONTAINER_TIMEZONE} >/etc/timezone && \
7 | ln -sf /usr/share/zoneinfo/${CONTAINER_TIMEZONE} /etc/localtime && \
8 | dpkg-reconfigure -f noninteractive tzdata
9 | echo "Container timezone set to: $CONTAINER_TIMEZONE"
10 | else
11 | echo "Container timezone not modified"
12 | fi
13 |
14 | # Synchronize the time of the container.
15 | ntpd -gq
16 | service ntp start
17 |
18 | echo "Using configuration location: ${CONFIG_LOCATION}"
19 |
20 | # Start the application that is to run in the container.
21 | gosu $RUN_AS_USER java -jar ${JAR_PATH} --spring.config.location=${CONFIG_LOCATION}
22 |
23 |
--------------------------------------------------------------------------------
/src/main/docker/application/config/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krizsan/git-workflow-sampleapp/19ca0678d3e163bbb32770f0b0cba8e2f8456305/src/main/docker/application/config/application.properties
--------------------------------------------------------------------------------
/src/main/docker/application/config/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 | %d %-5p %-30C - %m%n
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 |
2 | open module se.ivankrizsan.spring.hellowebapp {
3 | requires reactor.core;
4 | requires spring.web;
5 | requires spring.context;
6 | requires spring.webflux;
7 | requires spring.boot;
8 | requires spring.boot.autoconfigure;
9 | exports se.ivankrizsan.spring.hellowebapp to spring.beans, spring.context;
10 | }
--------------------------------------------------------------------------------
/src/main/java/se/ivankrizsan/spring/hellowebapp/HelloHandler.java:
--------------------------------------------------------------------------------
1 | package se.ivankrizsan.spring.hellowebapp;
2 |
3 | import org.springframework.http.MediaType;
4 | import org.springframework.stereotype.Component;
5 | import org.springframework.web.reactive.function.BodyInserters;
6 | import org.springframework.web.reactive.function.server.ServerRequest;
7 | import org.springframework.web.reactive.function.server.ServerResponse;
8 | import reactor.core.publisher.Mono;
9 |
10 | import java.time.LocalTime;
11 |
12 | /**
13 | * Handles hello requests.
14 | *
15 | * @author Ivan Krizsan
16 | */
17 | @Component
18 | public class HelloHandler {
19 |
20 | /**
21 | * Handles the supplied request emitting a greeting.
22 | *
23 | * @param inServerRequest Request to handle.
24 | * @return Response mono.
25 | */
26 | public Mono hello(final ServerRequest inServerRequest) {
27 | final LocalTime theLocalTime = LocalTime.now();
28 |
29 | return ServerResponse
30 | .ok()
31 | .contentType(MediaType.TEXT_PLAIN)
32 | .body(
33 | BodyInserters
34 | .fromObject(
35 | "Hello World, the time is now: " + theLocalTime));
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/java/se/ivankrizsan/spring/hellowebapp/HelloRouter.java:
--------------------------------------------------------------------------------
1 | package se.ivankrizsan.spring.hellowebapp;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.http.MediaType;
6 | import org.springframework.web.reactive.function.server.RequestPredicates;
7 | import org.springframework.web.reactive.function.server.RouterFunction;
8 | import org.springframework.web.reactive.function.server.RouterFunctions;
9 | import org.springframework.web.reactive.function.server.ServerResponse;
10 |
11 | /**
12 | * Configures routing of requests.
13 | *
14 | * @author Ivan Krizsan
15 | */
16 | @Configuration
17 | public class HelloRouter {
18 |
19 | /**
20 | * Bean defining the route from the /hello path to the
21 | * supplied {@code HelloHandler}.
22 | *
23 | * @param inHelloHandler Hello handler to route requests to.
24 | * @return Route function.
25 | */
26 | @Bean
27 | public RouterFunction routeHello(final HelloHandler inHelloHandler) {
28 | return RouterFunctions
29 | .route(
30 | RequestPredicates.GET("/hello")
31 | .and(
32 | RequestPredicates
33 | .accept(MediaType.ALL)), inHelloHandler::hello);
34 | }
35 | }
--------------------------------------------------------------------------------
/src/main/java/se/ivankrizsan/spring/hellowebapp/HelloWebappApplication.java:
--------------------------------------------------------------------------------
1 | package se.ivankrizsan.spring.hellowebapp;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /**
7 | * Hello World reactive Spring Boot web application.
8 | *
9 | * @author Ivan Krizsan
10 | */
11 | @SpringBootApplication
12 | public class HelloWebappApplication {
13 |
14 | /**
15 | * Main method used to start the application.
16 | *
17 | * @param args Command line arguments. Not used.
18 | */
19 | public static void main(String[] args) {
20 | SpringApplication.run(HelloWebappApplication.class, args);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # Allow for Spring bean definition overriding in order to
2 | # avoid strange errors when running tests.
3 | spring.main.allow-bean-definition-overriding=true
4 |
5 |
--------------------------------------------------------------------------------
/src/test/java/se/ivankrizsan/spring/hellowebapp/HelloHandlerTests.java:
--------------------------------------------------------------------------------
1 | package se.ivankrizsan.spring.hellowebapp;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 | import org.springframework.http.HttpMethod;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.http.MediaType;
9 | import org.springframework.mock.web.reactive.function.server.MockServerRequest;
10 | import org.springframework.web.reactive.function.server.ServerRequest;
11 | import org.springframework.web.reactive.function.server.ServerResponse;
12 | import reactor.core.publisher.Mono;
13 |
14 | /**
15 | * Tests for the {@code HelloHandler} class.
16 | *
17 | * @author Ivan Krizsan
18 | */
19 | public class HelloHandlerTests {
20 | /* Constant(s): */
21 |
22 | /* Instance variable(s): */
23 | protected HelloHandler mHandlerUnderTest;
24 |
25 |
26 | /**
27 | * Sets up before tests.
28 | */
29 | @Before
30 | public void setup() {
31 | mHandlerUnderTest = new HelloHandler();
32 | }
33 |
34 | /**
35 | * Tests sending of a good request to the greeting handler.
36 | * Expected result:
37 | * The response should be successfully handled and the response
38 | * body should contain a greeting.
39 | */
40 | @Test
41 | public void successfulGetGreetingTest() {
42 | final ServerRequest theRequest = MockServerRequest
43 | .builder()
44 | .method(HttpMethod.GET)
45 | .build();
46 |
47 | final Mono theResponseMono =
48 | mHandlerUnderTest.hello(theRequest);
49 | final ServerResponse theResponse = theResponseMono.block();
50 |
51 | Assert.assertNotNull("There should be a response", theResponse);
52 |
53 | final HttpStatus theResponseStatus = theResponse.statusCode();
54 | final MediaType theResponseType = theResponse.headers().getContentType();
55 |
56 | Assert.assertNotNull(
57 | "There should be a HTTP response status",
58 | theResponseStatus);
59 | Assert.assertEquals(
60 | "The response should be successfully handled",
61 | HttpStatus.OK,
62 | theResponseStatus);
63 | Assert.assertEquals(
64 | "The content type should be text/plain",
65 | theResponseType,
66 | MediaType.TEXT_PLAIN);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/se/ivankrizsan/spring/hellowebapp/HelloWebappApplicationTests.java:
--------------------------------------------------------------------------------
1 | package se.ivankrizsan.spring.hellowebapp;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.http.MediaType;
10 | import org.springframework.test.context.junit4.SpringRunner;
11 | import org.springframework.test.web.reactive.server.EntityExchangeResult;
12 | import org.springframework.test.web.reactive.server.WebTestClient;
13 |
14 | /**
15 | * Integrationtests of the Hello web application.
16 | *
17 | * @author Ivan Krizsan
18 | */
19 | @RunWith(SpringRunner.class)
20 | @SpringBootTest(
21 | classes = { HelloRouter.class, HelloHandler.class },
22 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
23 | @EnableAutoConfiguration
24 | public class HelloWebappApplicationTests {
25 | /* Constant(s): */
26 | protected static final String EXPECTED_GREETING = "Hello World";
27 |
28 | /* Instance variable(s): */
29 | @Autowired
30 | protected WebTestClient mWebTestClient;
31 |
32 |
33 | /**
34 | * Tests sending a good request to the greeting endpoint.
35 | * Expected result:
36 | * The response message should contain a greeting string.
37 | */
38 | @Test
39 | public void goodRequestTest() {
40 | final EntityExchangeResult theExchangeResult = mWebTestClient
41 | .get()
42 | .uri("/hello")
43 | .accept(MediaType.TEXT_PLAIN)
44 | .exchange()
45 | .expectBody(String.class)
46 | .returnResult();
47 |
48 | final String theResponseBody = theExchangeResult.getResponseBody();
49 | Assert.assertNotNull(
50 | "There should be a response body",
51 | theResponseBody);
52 | Assert.assertTrue("Response message should contain a greeting",
53 | theResponseBody.contains(EXPECTED_GREETING));
54 | }
55 |
56 | /**
57 | * Tests starting the application using the main entry class.
58 | * Expected result:
59 | * The application should start without errors.
60 | */
61 | @Test
62 | public void startApplicationTest() {
63 | HelloWebappApplication.main(new String[0]);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------