diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e440161 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + + - package-ecosystem: "maven" + directories: + - "/initial" + - "/complete" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + schedule: + interval: "monthly" + target-branch: "main" + groups: + guide-dependencies-maven: + patterns: + - "*" + + - package-ecosystem: "gradle" + directories: + - "/initial" + - "/complete" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + schedule: + interval: "monthly" + target-branch: "main" + groups: + guide-dependencies-gradle: + patterns: + - "*" \ No newline at end of file diff --git a/.github/workflows/continuous-integration-build.yml b/.github/workflows/continuous-integration-build.yml new file mode 100644 index 0000000..5d3e68e --- /dev/null +++ b/.github/workflows/continuous-integration-build.yml @@ -0,0 +1,13 @@ +name: CI Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + uses: spring-guides/getting-started-macros/.github/workflows/build_initial_complete_maven_gradle.yml@main \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 2087732..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,44 +0,0 @@ -pipeline { - agent none - - triggers { - pollSCM 'H/10 * * * *' - } - - options { - disableConcurrentBuilds() - buildDiscarder(logRotator(numToKeepStr: '14')) - } - - stages { - stage("test: baseline (jdk17)") { - agent { - docker { - image 'harbor-repo.vmware.com/dockerhub-proxy-cache/library/adoptopenjdk/openjdk17:latest' - args '-v $HOME/.m2:/tmp/jenkins-home/.m2' - } - } - options { timeout(time: 30, unit: 'MINUTES') } - steps { - sh 'test/run.sh' - } - } - - } - - post { - changed { - script { - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#sagan-content', - message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") - emailext( - subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", - mimeType: 'text/html', - recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], - body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") - } - } - } -} diff --git a/README.adoc b/README.adoc index b49c674..53638cf 100644 --- a/README.adoc +++ b/README.adoc @@ -2,98 +2,55 @@ :icons: font :source-highlighter: prettify :project_id: gs-spring-data-reactive-redis +:java_version: 17 +:build_system: maven +:build_name: demo +:build_version: 0.0.1-SNAPSHOT +:network_container: guide-redis + This guide walks you through the process of creating a functional reactive application that uses Spring Data to interact with Redis using the non-blocking Lettuce driver. == What You Will Build You'll build a Spring application that uses https://projects.spring.io/spring-data-redis/[Spring Data Redis] and https://projectreactor.io/[Project Reactor] to interact with a Redis data store reactively, storing and retrieving `Coffee` objects without blocking. This application uses Reactor's `Publisher` implementations based upon the Reactive Streams specification, namely `Mono` (for a Publisher returning 0 or 1 value) and `Flux` (for a Publisher returning 0 to n values). -== What You Need +include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/guide_introduction.adoc[] -:java_version: 17 -include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/prereq_editor_jdk_buildtools.adoc[] +== Setting up the Redis Server + +Before you can build a messaging application, you need to set up the server to +handle receiving and sending messages. +include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/docker_compose_support.adoc[] -include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/how_to_complete_this_guide.adoc[] +If you choose to run the Redis server yourself instead of using Spring Boot Docker Compose support, you have a few options: +- https://redis.io/download[Download the server] and manually run it +- Install with Homebrew, if you use a Mac +- Manually run the `compose.yaml` file with `docker compose up` + +If you go with any of these alternate approaches, you should remove the `spring-boot-docker-compose` dependency from the Maven or Gradle build file. +You also need to add configuration to an `application.properties` file, as described in greater detail in the <<_preparing_to_build_the_application>> section. +As mentioned earlier, this guide assumes that you use Docker Compose support in Spring Boot, so additional changes to `application.properties` are not required at this point. [[scratch]] == Starting with Spring Initializr -You can use this https://start.spring.io/#!type=maven-project&language=java&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=data-redis-reactive,lombok,webflux[pre-initialized project] and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial. +You can use this https://start.spring.io/#!type=maven-project&language=java&packaging=jar&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=data-redis-reactive,webflux,docker-compose[pre-initialized project] and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial. To manually initialize the project: . Navigate to https://start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup for you. . Choose either Gradle or Maven and the language you want to use. This guide assumes that you chose Java. -. Click *Dependencies* and select *Spring Reactive Web*, *Spring Data Reactive Redis*, and *Lombok*. +. Click *Dependencies* and select *Spring Reactive Web*, *Spring Data Reactive Redis*, and *Docker Compose Support*. . Click *Generate*. . Download the resulting ZIP file, which is an archive of a web application that is configured with your choices. NOTE: If your IDE has the Spring Initializr integration, you can complete this process from your IDE. -NOTE: You can also fork the project from Github and open it in your IDE or other editor. - -[[scratch]] -== Standing up a Redis Server - -Before you can build a messaging application, you need to set up the server that will -handle receiving and sending messages. - -Redis is an open source, BSD-licensed, key-value data store that also comes with a -messaging system. The server is freely available at https://redis.io/download. You can -download it manually, or, if you use a Mac, with Homebrew, by running the following -command in a terminal window: - -==== -[source,bash] ----- -brew install redis ----- -==== - -Once you unpack Redis, you can launch it with its default settings by running the following command: - -==== -[source,bash] ----- -redis-server ----- -==== - -You should see a message similar to the following: - -==== -[source,text] ----- -[35142] 01 May 14:36:28.939 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf -[35142] 01 May 14:36:28.940 * Max number of open files set to 10032 - _._ - _.-``__ ''-._ - _.-`` `. `_. ''-._ Redis 2.6.12 (00000000/0) 64 bit - .-`` .-```. ```\/ _.,_ ''-._ - ( ' , .-` | `, ) Running in stand alone mode - |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 - | `-._ `._ / _.-' | PID: 35142 - `-._ `-._ `-./ _.-' _.-' - |`-._`-._ `-.__.-' _.-'_.-'| - | `-._`-._ _.-'_.-' | https://redis.io - `-._ `-._`-.__.-'_.-' _.-' - |`-._`-._ `-.__.-' _.-'_.-'| - | `-._`-._ _.-'_.-' | - `-._ `-._`-.__.-'_.-' _.-' - `-._ `-.__.-' _.-' - `-._ _.-' - `-.__.-' - -[35142] 01 May 14:36:28.941 # Server started, Redis version 2.6.12 -[35142] 01 May 14:36:28.941 * The server is now ready to accept connections on port 6379 ----- -==== - [[initial]] == Create a Domain Class -Create a class representing a type of coffee we wish to stock in our coffee catalog: +Create a record representing a type of coffee we wish to stock in our coffee catalog: `src/main/java/com/example/demo/Coffee.java` [source,java,tabsize=2] @@ -101,9 +58,6 @@ Create a class representing a type of coffee we wish to stock in our coffee cata include::complete/src/main/java/com/example/demo/Coffee.java[] ---- -NOTE: I use Lombok in this example to eliminate the boilerplate code for constructors and so-called "data class" methods ( accessors/mutators, `equals()`, `toString()`, & `hashCode()`). - - == Create a Configuration Class Create a class that includes Spring Beans that support reactive Redis operations: @@ -114,7 +68,6 @@ Create a class that includes Spring Beans that support reactive Redis operations include::complete/src/main/java/com/example/demo/CoffeeConfiguration.java[] ---- - == Create a Spring Bean to Load Data Create a Spring Bean to load sample data for our application when we start it: @@ -138,29 +91,83 @@ Create a `RestController` to provide an external interface for our application: include::complete/src/main/java/com/example/demo/CoffeeController.java[] ---- +== Run the Application -== Make the Application Executable +You can run the main method through your IDE. +Note that, if you have cloned the project from the solution repository, your IDE may look in the wrong place for the `compose.yaml` file. +You can configure your IDE to look in the correct place or you could use the command line to run the application. +The `./gradlew bootRun` and `./mvnw spring-boot:run` commands will launch the application and automatically find the compose.yaml file. -Although you can package this service as a traditional link:/understanding/WAR[WAR] file for deployment to an external application server, the simpler approach shown here creates a standalone application. You package everything in a single, executable JAR file, driven by a good old Java `main()` method. Along the way, you use Spring's support for embedding the link:/understanding/Netty[Netty] an asynchronous "container" as the HTTP runtime instead of deploying to an external instance. +== Test the Application +With the application running, run the following command from a new terminal: +[source,bash] +``` +curl http://localhost:8080/coffees +``` -`src/main/java/com/example/demo/DemoApplication.java` -[source,java,tabsize=2] +You should see the following output: +[source,json] +``` +[ + { + "id": "04ce0843-c9f8-40f6-942f-1ff643c1d426", + "name": "Jet Black Redis" + }, + { + "id": "e2a0d798-5fa4-48a2-a45c-7770d8bb82bf", + "name": "Black Alert Redis" + }, + { + "id": "13f13e3a-0798-44b7-8ae4-b319b227bb19", + "name": "Darth Redis" + } +] +``` + +== Preparing to Build the Application + +To run the code without Spring Boot Docker Compose support, you need a version of Redis running locally to connect to. +To do this, you can use Docker Compose, but you must first make two changes to the `compose.yaml` file. +First, modify the `ports` entry in `compose.yaml` to be `'6379:6379'`. +Second, add a `container_name`. + +The `compose.yaml` should now be: ---- -include::complete/src/main/java/com/example/demo/DemoApplication.java[] +services: + redis: + container_name: 'guide-redis' + image: 'redis:latest' + ports: + - '6379:6379' ---- -include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/spring-boot-application.adoc[] +You can now run `docker compose up` to start the Redis server. +Now you should have an external Redis server that is ready to accept requests. +You can rerun the application and see the same output using your external Redis server. -include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/build_an_executable_jar_subhead.adoc[] +NOTE: No configuration is required in the `application.properties` file because the default values match the Redis server configuration in `compose.yaml`. Specifically, the properties `spring.data.redis.host` and `spring.data.redis.port` default to `localhost` and `6379` respectively. -include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/build_an_executable_jar_with_both.adoc[] +include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/build_and_execute_guide.adoc[] +== Test the Application in Docker -== Test the application +If you ran the application using a Docker instruction (shown earlier), a simple curl command from a terminal or command line will no longer work. +This is because we run our containers in a https://docs.docker.com/compose/networking/[Docker network] that is not accessible from the terminal or command line. To run curl commands, we can start a third container to run our curl commands and attach it to the same network. -Now that the application is running, you can test it by accessing `http://localhost:8080/coffees` from HTTPie, curl, or your favorite browser. +First, obtain an interactive shell to a new container that runs on the same network as the Redis container and the application: +[source, bash] +---- +docker run --rm --network container:guide-redis -it alpine +---- + +Next, from the shell inside the container, install curl: +[source, bash] +---- +apk add curl +---- +Finally, you can run the curl commands as described in <<_test_the_application>>. == Summary diff --git a/complete/build.gradle b/complete/build.gradle index 3fb0c12..ab14919 100644 --- a/complete/build.gradle +++ b/complete/build.gradle @@ -21,8 +21,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' implementation 'org.springframework.boot:spring-boot-starter-webflux' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-docker-compose' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' } diff --git a/complete/compose.yaml b/complete/compose.yaml new file mode 100644 index 0000000..f310cd2 --- /dev/null +++ b/complete/compose.yaml @@ -0,0 +1,5 @@ +services: + redis: + image: 'redis:latest' + ports: + - '6379' \ No newline at end of file diff --git a/complete/pom.xml b/complete/pom.xml index bc2a7e0..8eedd3a 100644 --- a/complete/pom.xml +++ b/complete/pom.xml @@ -25,10 +25,10 @@ org.springframework.boot spring-boot-starter-webflux - - org.projectlombok - lombok + org.springframework.boot + spring-boot-docker-compose + runtime true @@ -45,18 +45,7 @@ - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - + diff --git a/complete/src/main/java/com/example/demo/Coffee.java b/complete/src/main/java/com/example/demo/Coffee.java index fedf616..ea8d020 100644 --- a/complete/src/main/java/com/example/demo/Coffee.java +++ b/complete/src/main/java/com/example/demo/Coffee.java @@ -1,13 +1,4 @@ package com.example.demo; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Coffee { - private String id; - private String name; +public record Coffee(String id, String name) { } \ No newline at end of file diff --git a/complete/src/main/java/com/example/demo/CoffeeLoader.java b/complete/src/main/java/com/example/demo/CoffeeLoader.java index ace4c1a..cea3e38 100644 --- a/complete/src/main/java/com/example/demo/CoffeeLoader.java +++ b/complete/src/main/java/com/example/demo/CoffeeLoader.java @@ -23,7 +23,7 @@ public void loadData() { factory.getReactiveConnection().serverCommands().flushAll().thenMany( Flux.just("Jet Black Redis", "Darth Redis", "Black Alert Redis") .map(name -> new Coffee(UUID.randomUUID().toString(), name)) - .flatMap(coffee -> coffeeOps.opsForValue().set(coffee.getId(), coffee))) + .flatMap(coffee -> coffeeOps.opsForValue().set(coffee.id(), coffee))) .thenMany(coffeeOps.keys("*") .flatMap(coffeeOps.opsForValue()::get)) .subscribe(System.out::println); diff --git a/initial/build.gradle b/initial/build.gradle index 3fb0c12..ab14919 100644 --- a/initial/build.gradle +++ b/initial/build.gradle @@ -21,8 +21,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' implementation 'org.springframework.boot:spring-boot-starter-webflux' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-docker-compose' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' } diff --git a/initial/compose.yaml b/initial/compose.yaml new file mode 100644 index 0000000..f310cd2 --- /dev/null +++ b/initial/compose.yaml @@ -0,0 +1,5 @@ +services: + redis: + image: 'redis:latest' + ports: + - '6379' \ No newline at end of file diff --git a/initial/pom.xml b/initial/pom.xml index bc2a7e0..8eedd3a 100644 --- a/initial/pom.xml +++ b/initial/pom.xml @@ -25,10 +25,10 @@ org.springframework.boot spring-boot-starter-webflux - - org.projectlombok - lombok + org.springframework.boot + spring-boot-docker-compose + runtime true @@ -45,18 +45,7 @@ - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - + diff --git a/initial/src/main/java/com/example/demo/DemoApplication.java b/initial/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..e03ec75 --- /dev/null +++ b/initial/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,12 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/test/run.sh b/test/run.sh deleted file mode 100755 index dff7e36..0000000 --- a/test/run.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -cd $(dirname $0) - -cd ../complete - -./mvnw clean package -ret=$? -if [ $ret -ne 0 ]; then - exit $ret -fi -rm -rf target - -./gradlew build -ret=$? -if [ $ret -ne 0 ]; then - exit $ret -fi -rm -rf build - -cd ../initial - -./mvnw clean compile -ret=$? -if [ $ret -ne 0 ]; then - exit $ret -fi -rm -rf target - -./gradlew compileJava -ret=$? -if [ $ret -ne 0 ]; then - exit $ret -fi -rm -rf build - -exit