├── .gitattributes ├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── kotlin │ └── com │ │ └── example │ │ ├── ApplicationConfiguration.kt │ │ ├── BlockingController.kt │ │ ├── CoroutineController.kt │ │ ├── GlobalControllerExceptionHandler.kt │ │ ├── ReactiveController.kt │ │ ├── SpringCoroutinesApplication.kt │ │ └── persistence │ │ ├── Entities.kt │ │ ├── ReactiveRepositories.kt │ │ └── Repositories.kt └── resources │ └── application.properties └── test └── kotlin └── com └── example └── SpringCoroutinesApplicationTests.kt /.gitattributes: -------------------------------------------------------------------------------- 1 | .gradle linguist-vendored 2 | gradle linguist-vendored 3 | gradle* linguist-vendored 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | out/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ 27 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.1.2-2' 4 | springBootVersion = '2.0.0.M4' 5 | kotlinCoroutinesVersion = '0.19.1' 6 | } 7 | repositories { 8 | mavenCentral() 9 | maven { url "https://repo.spring.io/snapshot" } 10 | maven { url "https://repo.spring.io/milestone" } 11 | } 12 | dependencies { 13 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 14 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 15 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 16 | } 17 | } 18 | 19 | plugins { 20 | id "org.jetbrains.kotlin.plugin.noarg" version "1.1.2-2" 21 | } 22 | 23 | noArg { 24 | annotation("org.springframework.data.mongodb.core.mapping.Document") 25 | } 26 | 27 | apply plugin: 'kotlin' 28 | apply plugin: 'kotlin-spring' 29 | apply plugin: 'eclipse' 30 | apply plugin: 'org.springframework.boot' 31 | apply plugin: 'io.spring.dependency-management' 32 | 33 | version = '0.0.1-SNAPSHOT' 34 | sourceCompatibility = 1.8 35 | 36 | kotlin { 37 | experimental { 38 | coroutines 'enable' 39 | } 40 | } 41 | 42 | compileKotlin { 43 | kotlinOptions.jvmTarget = "1.8" 44 | } 45 | compileTestKotlin { 46 | kotlinOptions.jvmTarget = "1.8" 47 | } 48 | 49 | repositories { 50 | mavenCentral() 51 | jcenter() 52 | maven { url "https://repo.spring.io/snapshot" } 53 | maven { url "https://repo.spring.io/milestone" } 54 | } 55 | 56 | dependencies { 57 | compile('org.springframework.boot:spring-boot-starter-webflux') 58 | compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') 59 | compile('org.springframework.boot:spring-boot-devtools') 60 | compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}") 61 | compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") 62 | compile("io.github.microutils:kotlin-logging:1.4.4") 63 | 64 | compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:${kotlinCoroutinesVersion}") 65 | compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:${kotlinCoroutinesVersion}") 66 | compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${kotlinCoroutinesVersion}") 67 | 68 | testCompile('org.springframework.boot:spring-boot-starter-test') 69 | testCompile('io.projectreactor:reactor-test:3.1.0.RELEASE') 70 | testCompile('de.flapdoodle.embed:de.flapdoodle.embed.mongo:2.0.0') 71 | } 72 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alek-sys/spring-coroutines-demo/ae2d72bc33442957bf681c289b2c050ba9250293/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ApplicationConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import java.time.Clock 6 | 7 | @Configuration 8 | class ApplicationConfiguration { 9 | 10 | @Bean 11 | fun clock() = Clock.systemDefaultZone() 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/BlockingController.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.persistence.AuditRepository 4 | import com.example.persistence.MessageRepository 5 | import com.example.persistence.PeopleRepository 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.PathVariable 8 | import org.springframework.web.bind.annotation.RestController 9 | 10 | @RestController 11 | class BlockingController( 12 | val peopleRepository: PeopleRepository, 13 | val auditRepository: AuditRepository, 14 | val messageRepository: MessageRepository) { 15 | 16 | @GetMapping("/blocking/{personId}") 17 | fun getMessagesFor(@PathVariable personId: String): String { 18 | val person = peopleRepository 19 | .findById(personId) 20 | .orElseThrow { NoSuchElementException("Not found") } 21 | 22 | val lastLogin = auditRepository 23 | .findByEmail(person.email).eventDate 24 | 25 | val numberOfMessages = messageRepository 26 | .countByMessageDateGreaterThanAndEmail(lastLogin, person.email) 27 | 28 | return "Hello ${person.name}, you have $numberOfMessages messages since $lastLogin" 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/CoroutineController.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.persistence.ReactiveAuditRepository 4 | import com.example.persistence.ReactiveMessageRepository 5 | import com.example.persistence.ReactivePeopleRepository 6 | import kotlinx.coroutines.experimental.Unconfined 7 | import kotlinx.coroutines.experimental.reactive.awaitSingle 8 | import kotlinx.coroutines.experimental.reactor.mono 9 | import org.springframework.web.bind.annotation.GetMapping 10 | import org.springframework.web.bind.annotation.PathVariable 11 | import org.springframework.web.bind.annotation.RestController 12 | 13 | @RestController 14 | class CoroutineController( 15 | val peopleRepository: ReactivePeopleRepository, 16 | val messageRepository: ReactiveMessageRepository, 17 | val auditRepository: ReactiveAuditRepository) { 18 | 19 | @GetMapping("/coroutine/{personId}") 20 | fun getMessages(@PathVariable personId: String) = mono(Unconfined) { 21 | val person = peopleRepository.findById(personId).awaitSingle() 22 | val lastLogin = auditRepository.findByEmail(person.email).awaitSingle().eventDate 23 | val numberOfMessages = messageRepository.countByMessageDateGreaterThanAndEmail(lastLogin, person.email).awaitSingle() 24 | 25 | val message = "Hello ${person.name}, you have $numberOfMessages messages since $lastLogin" 26 | 27 | message 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/GlobalControllerExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.springframework.http.ResponseEntity 4 | import org.springframework.web.bind.annotation.ControllerAdvice 5 | import org.springframework.web.bind.annotation.ExceptionHandler 6 | 7 | @ControllerAdvice 8 | class GlobalControllerExceptionHandler { 9 | 10 | @ExceptionHandler(NoSuchElementException::class) 11 | fun return404() = ResponseEntity.notFound().build() 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ReactiveController.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.persistence.ReactiveAuditRepository 4 | import com.example.persistence.ReactiveMessageRepository 5 | import com.example.persistence.ReactivePeopleRepository 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.PathVariable 8 | import org.springframework.web.bind.annotation.RestController 9 | import reactor.core.publisher.Mono 10 | 11 | @RestController 12 | class ReactiveController( 13 | val peopleRepository: ReactivePeopleRepository, 14 | val messageRepository: ReactiveMessageRepository, 15 | val auditRepository: ReactiveAuditRepository) { 16 | 17 | @GetMapping("/reactive/{personId}") 18 | fun getMessagesFor(@PathVariable personId: String): Mono { 19 | return peopleRepository.findById(personId) 20 | .switchIfEmpty(Mono.error(NoSuchElementException())) 21 | .flatMap { person -> 22 | auditRepository.findByEmail(person.email) 23 | .flatMap { lastLogin -> 24 | messageRepository.countByMessageDateGreaterThanAndEmail(lastLogin.eventDate, person.email) 25 | .map { numberOfMessages -> 26 | "Hello ${person.name}, you have $numberOfMessages messages since ${lastLogin.eventDate}" 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/SpringCoroutinesApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.persistence.* 4 | import org.springframework.boot.CommandLineRunner 5 | import org.springframework.boot.SpringApplication 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import java.time.Clock 8 | import java.time.LocalDateTime 9 | 10 | @SpringBootApplication 11 | class SpringCoroutinesApplication( 12 | val peopleRepository: PeopleRepository, 13 | val auditRepository: AuditRepository, 14 | val messageRepository: MessageRepository, 15 | val clock: Clock) : CommandLineRunner { 16 | 17 | override fun run(vararg args: String?) { 18 | peopleRepository.save(Person("P1", "Alex", "alex@example.com")) 19 | 20 | auditRepository.save(Audit(LocalDateTime.now(clock).minusDays(2), "alex@example.com")) 21 | 22 | messageRepository.save(Message("Hello", LocalDateTime.now(clock).minusDays(10), "alex@example.com")) 23 | messageRepository.save(Message("How are you", LocalDateTime.now(clock), "alex@example.com")) 24 | } 25 | } 26 | 27 | fun main(args: Array) { 28 | SpringApplication.run(SpringCoroutinesApplication::class.java, *args) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/persistence/Entities.kt: -------------------------------------------------------------------------------- 1 | package com.example.persistence 2 | 3 | import org.springframework.data.annotation.Id 4 | import java.time.LocalDateTime 5 | 6 | data class Person(@Id val id: String, val name: String, val email: String) 7 | data class Audit(val eventDate: LocalDateTime, val email: String) 8 | data class Message(val text: String, val messageDate: LocalDateTime, val email: String) 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/persistence/ReactiveRepositories.kt: -------------------------------------------------------------------------------- 1 | package com.example.persistence 2 | 3 | import org.springframework.data.repository.reactive.ReactiveCrudRepository 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | interface ReactivePeopleRepository: ReactiveCrudRepository 8 | 9 | @Repository 10 | interface ReactiveAuditRepository: ReactiveCrudRepository { 11 | fun findByEmail(email: String): reactor.core.publisher.Mono 12 | } 13 | 14 | @Repository 15 | interface ReactiveMessageRepository: ReactiveCrudRepository { 16 | fun countByMessageDateGreaterThanAndEmail(messageDate: java.time.LocalDateTime, email: String): reactor.core.publisher.Mono 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/persistence/Repositories.kt: -------------------------------------------------------------------------------- 1 | package com.example.persistence 2 | 3 | import org.springframework.data.repository.CrudRepository 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | interface PeopleRepository: CrudRepository 8 | 9 | @Repository 10 | interface AuditRepository: CrudRepository { 11 | fun findByEmail(email: String): Audit 12 | } 13 | 14 | @Repository 15 | interface MessageRepository: CrudRepository { 16 | fun countByMessageDateGreaterThanAndEmail(messageDate: java.time.LocalDateTime, email: String): Long 17 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alek-sys/spring-coroutines-demo/ae2d72bc33442957bf681c289b2c050ba9250293/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/kotlin/com/example/SpringCoroutinesApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient 8 | import org.springframework.boot.test.context.SpringBootTest 9 | import org.springframework.boot.test.context.TestConfiguration 10 | import org.springframework.context.annotation.Bean 11 | import org.springframework.http.HttpStatus 12 | import org.springframework.test.context.junit4.SpringRunner 13 | import org.springframework.test.web.reactive.server.WebTestClient 14 | import reactor.test.StepVerifier 15 | import java.time.Clock 16 | import java.time.Instant 17 | import java.time.ZoneId 18 | 19 | @RunWith(SpringRunner::class) 20 | @SpringBootTest 21 | @AutoConfigureWebTestClient 22 | class SpringCoroutinesApplicationTests { 23 | 24 | @Autowired 25 | lateinit var webTestClient: WebTestClient 26 | 27 | @TestConfiguration 28 | class ClockConfiguration { 29 | @Bean 30 | fun clock() = Clock.fixed(Instant.parse("2017-05-05T11:00:00.00Z"), ZoneId.of("Europe/London")) 31 | } 32 | 33 | @Test 34 | fun `blocking controller returns a message`() { 35 | val message = webTestClient 36 | .get().uri("/blocking/P1").exchange() 37 | .expectBody(String::class.java) 38 | .returnResult().responseBody 39 | 40 | assertThat(message).isEqualTo("Hello Alex, you have 1 messages since 2017-05-03T12:00") 41 | } 42 | 43 | @Test 44 | fun `blocking controller returns 404 for non-existing person`() { 45 | `assert 404 status for url`("/blocking/some-id") 46 | } 47 | 48 | @Test 49 | fun `reactive controller returns a message`() { 50 | val messagePublisher = webTestClient 51 | .get().uri("/reactive/P1").exchange() 52 | .returnResult(String::class.java) 53 | .responseBody 54 | 55 | StepVerifier.create(messagePublisher) 56 | .expectNext("Hello Alex, you have 1 messages since 2017-05-03T12:00") 57 | .expectComplete() 58 | .verify() 59 | } 60 | 61 | @Test 62 | fun `reactive controller returns 404 for non-existing person`() { 63 | `assert 404 status for url`("/reactive/some-id") 64 | } 65 | 66 | @Test 67 | fun `coroutine controller returns a message`() { 68 | val messagePublisher = webTestClient 69 | .get().uri("/coroutine/P1").exchange() 70 | .returnResult(String::class.java) 71 | .responseBody 72 | 73 | StepVerifier.create(messagePublisher) 74 | .expectNext("Hello Alex, you have 1 messages since 2017-05-03T12:00") 75 | .expectComplete() 76 | .verify() 77 | } 78 | 79 | @Test 80 | fun `coroutine controller returns 404 for non-existing person`() { 81 | `assert 404 status for url`("/coroutine/some-id") 82 | } 83 | 84 | private fun `assert 404 status for url`(url: String) { 85 | val status = webTestClient 86 | .get().uri(url).exchange() 87 | .expectBody(String::class.java) 88 | .returnResult().status 89 | 90 | assertThat(status).isEqualTo(HttpStatus.NOT_FOUND) 91 | } 92 | } 93 | --------------------------------------------------------------------------------