├── .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 | ![alt text](git_workflow.jpg) 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 | --------------------------------------------------------------------------------