├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── common ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── example │ └── demo │ ├── configuration │ └── AppConfigurationException.kt │ └── logging │ └── AppLogger.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── rest-service-classic ├── README.md ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── com │ │ └── example │ │ └── demo │ │ └── restservice │ │ ├── DemoRestApplication.kt │ │ ├── DemoRestApplicationProperties.kt │ │ ├── api │ │ ├── RestApiController.kt │ │ └── handler │ │ │ └── TweetHandler.kt │ │ ├── configuration │ │ └── Swagger.kt │ │ ├── domain │ │ └── Tweets.kt │ │ └── util │ │ └── Extensions.kt │ └── resources │ ├── _log4j.properties │ ├── _logback.xml │ └── application.yml ├── rest-service ├── README.md ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── com │ │ └── example │ │ └── demo │ │ └── restservice │ │ ├── DemoRestApplication.kt │ │ ├── DemoRestApplicationProperties.kt │ │ ├── api │ │ ├── handler │ │ │ └── TweetHandler.kt │ │ └── routes │ │ │ ├── ApiRoutes.kt │ │ │ └── RedirectRoutes.kt │ │ ├── configuration │ │ └── Swagger.kt │ │ ├── domain │ │ └── Tweets.kt │ │ └── util │ │ └── Extensions.kt │ └── resources │ ├── _log4j.properties │ ├── _logback.xml │ └── application.yml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #/build/ 2 | 3 | .gradle 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | .DS_Store 7 | .idea 8 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Seb Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webflux playground 2 | 3 | Note: this is just quick and dirty playground 4 | 5 | - https://speakerdeck.com/sdeleuze/functional-web-applications-with-kotlin-and-spring-5 6 | - https://www.infoq.com/articles/reactor-by-example 7 | 8 | 9 | ## findings 10 | - springfox swagger currently does not work with webflux, since it has dependencies to mvc 11 | 12 | Caused by: java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter 13 | at springfox.documentation.spring.web.ObjectMapperConfigurer.postProcessBeforeInitialization(ObjectMapperConfigurer.java:45) ~[springfox-spring-web-2.6.1.jar:2.6.1] 14 | 15 | it requires: import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 16 | 17 | 18 | todo: try to include mvc, but factory spring boot with webflux ???? 19 | 20 | - I created a feature request: https://github.com/springfox/springfox/issues/1809 21 | 22 | ## gradle modules 23 | 24 | - common: some shared code (example) 25 | - rest-service-classic: (reactive) mvc-style restcontroller example 26 | - rest-service: (reactive) functional-style router example 27 | 28 | ## project flavours 29 | 30 | This project has been implemented in different flavours: 31 | 32 | ### kotlin 33 | - spring boot 1.5.* & spring-mvc: https://github.com/bastman/skeleton-springboot-kotlin 34 | - spring boot 2.0.* & spring-webflux: https://github.com/bastman/skeleton-springboot-webflux-kotlin 35 | 36 | ### java 37 | - spring boot 1.5.* & spring-mvc: https://github.com/bastman/skeleton-springboot-java -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | 4 | repositories { 5 | maven { url "https://plugins.gradle.org/m2/" } 6 | mavenCentral() 7 | jcenter() 8 | maven { url 'https://jitpack.io' } 9 | } 10 | 11 | dependencies { 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 13 | } 14 | } 15 | 16 | 17 | subprojects { 18 | apply plugin: 'kotlin' 19 | //apply plugin: 'org.jetbrains.kotlin.plugin.noarg' 20 | 21 | compileKotlin { 22 | kotlinOptions { 23 | // suppressWarnings = true 24 | jvmTarget = "1.8" 25 | } 26 | } 27 | 28 | buildscript { 29 | repositories { 30 | maven { url "https://plugins.gradle.org/m2/" } 31 | mavenCentral() 32 | jcenter() 33 | maven { url 'https://jitpack.io' } 34 | } 35 | } 36 | 37 | repositories { 38 | mavenCentral() 39 | jcenter() 40 | maven { url 'https://jitpack.io' } 41 | } 42 | 43 | dependencies { 44 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 45 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 46 | } 47 | 48 | kotlin { 49 | experimental { coroutines 'enable' } 50 | } 51 | 52 | } 53 | 54 | task wrapper(type: Wrapper) { 55 | gradleVersion = '3.5' 56 | } -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | // kotlin coroutines 4 | compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion" 5 | compile "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinxCoroutinesVersion" 6 | compile "org.jetbrains.kotlinx:kotlinx-coroutines-nio:$kotlinxCoroutinesVersion" 7 | compile "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$kotlinxCoroutinesVersion" 8 | 9 | // logging 10 | //compile("net.logstash.logback:logstash-logback-encoder:$logstashLogbackEncoderVersion") 11 | //compile("org.slf4j:slf4j-api:$slfApiVersion") 12 | 13 | // jackson 14 | compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") 15 | compile("com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonVersion") 16 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion") 17 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") 18 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr353:$jacksonVersion") 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/example/demo/configuration/AppConfigurationException.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.configuration 2 | 3 | class AppConfigurationException( 4 | message: String? = null, 5 | cause: Throwable? = null 6 | ) : RuntimeException(message, cause) -------------------------------------------------------------------------------- /common/src/main/kotlin/com/example/demo/logging/AppLogger.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.logging 2 | 3 | /* 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | 7 | object AppLogger { 8 | fun get(clazz: Class<*>): Logger = LoggerFactory.getLogger(clazz) 9 | } 10 | 11 | */ 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group = com.example.demo 2 | version = 1.0-SNAPSHOT 3 | 4 | ## kotlin 5 | kotlin_version = 1.1.1 6 | kotlinxCoroutinesVersion=0.14 7 | 8 | ## jackson 9 | jacksonVersion = 2.8.8 10 | 11 | ## logging 12 | #slfApiVersion=1.7.25 13 | #logstashLogbackEncoderVersion=4.8 14 | 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastman/skeleton-springboot-webflux-kotlin/7d41287207d84be3f3b38836644f26224cad823f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 05 09:33:20 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /rest-service-classic/README.md: -------------------------------------------------------------------------------- 1 | ## classic-mvc-style RestController, but reactive 2 | 3 | - https://www.infoq.com/articles/reactor-by-example 4 | 5 | ## Api 6 | 7 | - GET http://localhost:8080/api/tweets 8 | - GET http://localhost:8080/api/tweet/{id} 9 | - POST http://localhost:8080/api/tweet/submit 10 | 11 | Example: 12 | 13 | $ curl -H 'Host: localhost:8080' -H 'Acceptost:8080' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' -H 'Content-Type: application/json' -H 'Referer: http://localhost:8080/swagger-ui.html' -H 'Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4' --data-binary '{ 14 | "author": "seb", 15 | "message": "Hello World." 16 | }' --compressed 'http://localhost:8080/api/tweet/submit' -v 17 | 18 | -------------------------------------------------------------------------------- /rest-service-classic/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { url "https://repo.spring.io/snapshot" } 5 | maven { url "https://repo.spring.io/milestone" } 6 | } 7 | dependencies { 8 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 9 | } 10 | } 11 | 12 | apply plugin: 'org.springframework.boot' 13 | apply plugin: 'io.spring.dependency-management' 14 | 15 | 16 | 17 | repositories { 18 | mavenCentral() 19 | maven { url "https://repo.spring.io/snapshot" } 20 | maven { url "https://repo.spring.io/milestone" } 21 | } 22 | 23 | dependencies { 24 | compile project(':common') 25 | compile('org.springframework.boot:spring-boot-starter-webflux') { 26 | exclude module: "hibernate-validator" 27 | } 28 | 29 | // cache: caffeine 30 | compile("com.github.ben-manes.caffeine:caffeine") 31 | 32 | compile("io.projectreactor:reactor-kotlin-extensions:1.0.0.BUILD-SNAPSHOT") 33 | testCompile("io.projectreactor.addons:reactor-test") 34 | 35 | 36 | testCompile('org.springframework.boot:spring-boot-starter-test') 37 | } 38 | -------------------------------------------------------------------------------- /rest-service-classic/gradle.properties: -------------------------------------------------------------------------------- 1 | springBootVersion=2.0.0.BUILD-SNAPSHOT 2 | swaggerVersion=2.6.1 3 | #caffeineVersion=2.3.5 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/DemoRestApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice 2 | 3 | import com.example.demo.restservice.domain.Tweet 4 | import com.example.demo.restservice.domain.TweetService 5 | import com.example.demo.restservice.util.runWebflux 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.boot.CommandLineRunner 8 | import org.springframework.boot.autoconfigure.SpringBootApplication 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties 10 | import org.springframework.context.ConfigurableApplicationContext 11 | import org.springframework.context.annotation.Bean 12 | import java.time.Instant 13 | 14 | 15 | //typealias WebMvcRequestMappingInfoHandlerMapping = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping 16 | //typealias WebMvcRequestMappingInfo = org.springframework.web.servlet.mvc.method.RequestMappingInfo 17 | //typealias WebMvcRequestMappingHandlerMapping = org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 18 | 19 | typealias WebFluxRequestMappingInfoHandlerMapping = org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping 20 | typealias WebFluxRequestMappingInfo = org.springframework.web.reactive.result.method.RequestMappingInfo 21 | typealias WebFluxRequestMappingHandlerMapping = org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping 22 | 23 | @SpringBootApplication 24 | @EnableConfigurationProperties(DemoRestApplicationProperties::class) 25 | open class DemoRestApplication { 26 | @Bean 27 | open fun init( 28 | ctx: ConfigurableApplicationContext, 29 | @Value("\${app.appName}") appName: String, 30 | tweetService: TweetService 31 | ) = CommandLineRunner { 32 | println("=== init $appName - $ctx =====") 33 | 34 | listOf( 35 | Tweet( 36 | id = "1", 37 | createdAt = Instant.now(), 38 | author = "seb", 39 | message = "msg1" 40 | ), 41 | Tweet( 42 | id = "2", 43 | createdAt = Instant.now(), 44 | author = "seb", 45 | message = "msg2" 46 | ) 47 | ).forEach { 48 | tweetService.submit(it) 49 | } 50 | 51 | } 52 | 53 | // No qualifying bean of type 'java.util.List' 54 | /* 55 | @Bean 56 | open fun webmvcRequestMappingHandlerMapping( 57 | webfluxMapping: WebFluxRequestMappingHandlerMapping 58 | ): WebMvcRequestMappingHandlerMapping { 59 | val mapping = WebMvcRequestMappingHandlerMapping() 60 | // add properties here 61 | return mapping 62 | } 63 | */ 64 | /* 65 | @Bean open fun foo(): WebMvcRequestMappingInfoHandlerMapping { 66 | WebMvcRequestMappingInfo() 67 | return WebMvcRequestMappingInfoHandlerMapping() 68 | } 69 | */ 70 | } 71 | 72 | fun main(args: Array) { 73 | runWebflux(DemoRestApplication::class, *args) 74 | } -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/DemoRestApplicationProperties.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties("app") 6 | class DemoRestApplicationProperties { 7 | var appName: String? = null 8 | } -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/api/RestApiController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.api 2 | 3 | import com.example.demo.restservice.api.handler.TweetHandler 4 | import org.springframework.web.bind.annotation.* 5 | import reactor.core.publisher.Mono 6 | 7 | @RestController 8 | class RestApiController(private val tweetHandler: TweetHandler) { 9 | 10 | @GetMapping("/api/tweets") 11 | fun findAllTweets(): Mono 12 | = tweetHandler.findAll() 13 | 14 | @GetMapping("/api/tweet/{id}") 15 | fun getTweetById(@PathVariable id: String) 16 | = tweetHandler.findOne(id = id) 17 | 18 | @PostMapping("/api/tweet/submit") 19 | fun submitTweet( 20 | @RequestBody body: Mono 21 | ): Mono 22 | = tweetHandler.submitOne(body) 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/api/handler/TweetHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.api.handler 2 | 3 | import com.example.demo.restservice.domain.Tweet 4 | import com.example.demo.restservice.domain.TweetService 5 | import org.springframework.stereotype.Component 6 | import org.springframework.web.bind.annotation.PathVariable 7 | import reactor.core.publisher.Mono 8 | import kotlin.streams.toList 9 | 10 | @Component 11 | class TweetHandler(private val tweetService: TweetService) { 12 | 13 | data class SubmitRequest(val author: String, val message: String) 14 | data class SubmitResponse(val id: String) 15 | data class TweetsCollectionResponse(val tweets: List) 16 | 17 | fun submitOne(body: Mono): Mono { 18 | return body.flatMap { 19 | val tweet = tweetService.create(it.author, it.message) 20 | tweetService.submit(tweet) 21 | 22 | Mono.just(SubmitResponse(id = tweet.id)) 23 | } 24 | } 25 | 26 | fun findOne(@PathVariable id: String): Mono { 27 | return tweetService.get(tweetId = id) 28 | } 29 | 30 | fun findAll(): Mono { 31 | val tweets = tweetService.getAll() 32 | .toStream() 33 | .toList() 34 | 35 | return Mono.just( 36 | TweetsCollectionResponse(tweets = tweets) 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/configuration/Swagger.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.configuration 2 | 3 | /* 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import springfox.documentation.builders.ApiInfoBuilder 8 | import springfox.documentation.builders.RequestHandlerSelectors 9 | import springfox.documentation.service.BasicAuth 10 | import springfox.documentation.spi.DocumentationType 11 | import springfox.documentation.spring.web.plugins.Docket 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2 13 | 14 | @Configuration 15 | @EnableSwagger2() 16 | open class SwaggerConfiguration( 17 | @Value("\${app.appName}") appName: String 18 | ) { 19 | val API_ROUTE_SWAGGER_UI = "/swagger-ui.html" 20 | 21 | private val apiInfo by lazy { 22 | ApiInfoBuilder() 23 | .title(appName) 24 | .description("This is an example microservice application.") 25 | .build() 26 | } 27 | 28 | @Bean 29 | open fun demoApiDocket(): Docket { 30 | val groupName = "demo-api" 31 | val basePackage = "com.example.demo.restservice.api" 32 | 33 | return Docket(DocumentationType.SWAGGER_2) 34 | .groupName(groupName) 35 | .apiInfo(apiInfo) 36 | .select() 37 | .apis(RequestHandlerSelectors.basePackage(basePackage)) 38 | .build() 39 | .securitySchemes( 40 | arrayListOf( 41 | BasicAuth("basic-auth-realm") 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | */ -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/domain/Tweets.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.domain 2 | 3 | //import com.example.demo.logging.AppLogger 4 | import com.github.benmanes.caffeine.cache.Cache 5 | import com.github.benmanes.caffeine.cache.Caffeine 6 | import org.springframework.stereotype.Component 7 | import reactor.core.publisher.Flux 8 | import reactor.core.publisher.Mono 9 | import java.time.Duration 10 | import java.time.Instant 11 | import java.util.* 12 | import java.util.concurrent.TimeUnit 13 | 14 | data class Tweet( 15 | val id: String, 16 | val createdAt: Instant, 17 | val author: String, 18 | val message: String 19 | ) 20 | 21 | @Component 22 | class TweetService(private val repository: TweetRepository) { 23 | 24 | fun create(author: String, message: String) = Tweet( 25 | author = author, 26 | message = message, 27 | createdAt = Instant.now(), 28 | id = "${System.nanoTime()}-${UUID.randomUUID()}" 29 | ) 30 | 31 | fun submit(tweet: Tweet) = repository.add(tweet) 32 | fun get(tweetId: String) = repository.getOrNull(tweetId) 33 | fun getAll() = repository.getItems() 34 | fun findByAuthor(author: String) = getAll().filter { it.author == author } 35 | } 36 | 37 | typealias TweetRepositoryCache = Cache 38 | @Component 39 | class TweetRepository() { 40 | // private val LOGGER = AppLogger.get(javaClass) 41 | 42 | private val cache: TweetRepositoryCache by lazy { 43 | val expiry = Duration.ofDays(3) 44 | 45 | Caffeine 46 | .newBuilder() 47 | .maximumSize(1_000_000) 48 | .expireAfterWrite(expiry.seconds, TimeUnit.SECONDS) 49 | .build() 50 | } 51 | 52 | fun add(item: Tweet) { 53 | cache.put(item.id, item) 54 | println("$this : add item to repository. itemId=${item.id}") 55 | // LOGGER.info("add item to repository. itemId=${item.id}") 56 | } 57 | 58 | fun getOrNull(itemId: String): Mono = Mono.justOrEmpty(cache.getIfPresent(itemId)) 59 | 60 | fun getItems(): Flux { 61 | val items = cache.asMap().values 62 | return Flux.fromIterable(cache.asMap().values) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rest-service-classic/src/main/kotlin/com/example/demo/restservice/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.util 2 | 3 | 4 | import org.springframework.boot.SpringApplication 5 | import org.springframework.boot.WebApplicationType 6 | import org.springframework.boot.builder.SpringApplicationBuilder 7 | import org.springframework.http.MediaType.* 8 | import org.springframework.web.reactive.function.server.ServerResponse 9 | import org.springframework.web.reactive.function.server.ServerResponse.permanentRedirect 10 | import org.springframework.web.reactive.function.server.ServerResponse.seeOther 11 | import java.net.URI 12 | import kotlin.reflect.KClass 13 | 14 | // borrowed from: https://github.com/mixitconf/mixit/blob/master/src/main/kotlin/mixit/util/Extensions.kt 15 | 16 | // ---------------------- 17 | // Spring Boot extensions 18 | // ---------------------- 19 | 20 | fun run(type: KClass<*>, vararg args: String) = SpringApplication.run(type.java, *args) 21 | fun runWebflux(type: KClass<*>, vararg args: String) = SpringApplicationBuilder() 22 | .sources(type.java) 23 | .web(WebApplicationType.REACTIVE) 24 | .run(*args) 25 | 26 | // ------------------------- 27 | // Spring WebFlux extensions 28 | // ------------------------- 29 | 30 | 31 | fun ServerResponse.BodyBuilder.json() = contentType(APPLICATION_JSON_UTF8) 32 | 33 | fun ServerResponse.BodyBuilder.xml() = contentType(APPLICATION_XML) 34 | 35 | fun ServerResponse.BodyBuilder.html() = contentType(TEXT_HTML) 36 | 37 | fun permanentRedirect(uri: String) = permanentRedirect(URI(uri)).build() 38 | 39 | fun seeOther(uri: String) = seeOther(URI(uri)).build() -------------------------------------------------------------------------------- /rest-service-classic/src/main/resources/_log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | # Direct log messages to stdout 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target=System.out 6 | # Logstash layout 7 | #log4j.appender.stdout.layout=net.logstash.log4j.JSONEventLayoutV1 8 | # "Readable" layout for local debugging 9 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 10 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m %X{traceId}%n 11 | 12 | -------------------------------------------------------------------------------- /rest-service-classic/src/main/resources/_logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /rest-service-classic/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main.banner-mode: 'OFF' 3 | 4 | app: 5 | appName: 'Demo RestService Application' 6 | 7 | -------------------------------------------------------------------------------- /rest-service/README.md: -------------------------------------------------------------------------------- 1 | # functional style - api router 2 | 3 | - https://speakerdeck.com/sdeleuze/functional-web-applications-with-kotlin-and-spring-5 4 | 5 | ## Api 6 | 7 | - GET http://localhost:8080/api/tweets 8 | - GET http://localhost:8080/api/tweet/{id} 9 | - POST http://localhost:8080/api/tweet/submit 10 | 11 | Example: 12 | 13 | $ curl -H 'Host: localhost:8080' -H 'Acceptost:8080' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' -H 'Content-Type: application/json' -H 'Referer: http://localhost:8080/swagger-ui.html' -H 'Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4' --data-binary '{ 14 | "author": "seb", 15 | "message": "Hello World." 16 | }' --compressed 'http://localhost:8080/api/tweet/submit' -v -------------------------------------------------------------------------------- /rest-service/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { url "https://repo.spring.io/snapshot" } 5 | maven { url "https://repo.spring.io/milestone" } 6 | } 7 | dependencies { 8 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 9 | } 10 | } 11 | 12 | apply plugin: 'org.springframework.boot' 13 | apply plugin: 'io.spring.dependency-management' 14 | 15 | 16 | 17 | repositories { 18 | mavenCentral() 19 | maven { url "https://repo.spring.io/snapshot" } 20 | maven { url "https://repo.spring.io/milestone" } 21 | } 22 | 23 | dependencies { 24 | compile project(':common') 25 | compile('org.springframework.boot:spring-boot-starter-webflux') { 26 | exclude module: "hibernate-validator" 27 | } 28 | 29 | // swagger requires it :( 30 | //compile("org.springframework.boot:spring-boot-starter-web") { 31 | // exclude module: "org.springframework.boot:spring-boot-starter-tomcat" 32 | //} 33 | //compile("org.springframework.boot:spring-boot-starter-undertow") 34 | 35 | // spring-boot: swagger 36 | //compile("io.springfox:springfox-swagger2:$swaggerVersion") 37 | //compile("io.springfox:springfox-swagger-ui:$swaggerVersion") 38 | 39 | // cache: caffeine 40 | compile("com.github.ben-manes.caffeine:caffeine") 41 | 42 | 43 | 44 | compile("io.projectreactor:reactor-kotlin-extensions:1.0.0.BUILD-SNAPSHOT") 45 | testCompile("io.projectreactor.addons:reactor-test") 46 | 47 | 48 | testCompile('org.springframework.boot:spring-boot-starter-test') 49 | } 50 | -------------------------------------------------------------------------------- /rest-service/gradle.properties: -------------------------------------------------------------------------------- 1 | springBootVersion=2.0.0.BUILD-SNAPSHOT 2 | swaggerVersion=2.6.1 3 | #caffeineVersion=2.3.5 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/DemoRestApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice 2 | 3 | import com.example.demo.restservice.domain.Tweet 4 | import com.example.demo.restservice.domain.TweetService 5 | import com.example.demo.restservice.util.runWebflux 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.boot.CommandLineRunner 8 | import org.springframework.boot.autoconfigure.SpringBootApplication 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties 10 | import org.springframework.context.ConfigurableApplicationContext 11 | import org.springframework.context.annotation.Bean 12 | import java.time.Instant 13 | 14 | 15 | //typealias WebMvcRequestMappingInfoHandlerMapping = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping 16 | //typealias WebMvcRequestMappingInfo = org.springframework.web.servlet.mvc.method.RequestMappingInfo 17 | //typealias WebMvcRequestMappingHandlerMapping = org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 18 | 19 | typealias WebFluxRequestMappingInfoHandlerMapping = org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping 20 | typealias WebFluxRequestMappingInfo = org.springframework.web.reactive.result.method.RequestMappingInfo 21 | typealias WebFluxRequestMappingHandlerMapping = org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping 22 | 23 | @SpringBootApplication 24 | @EnableConfigurationProperties(DemoRestApplicationProperties::class) 25 | open class DemoRestApplication { 26 | @Bean 27 | open fun init( 28 | ctx: ConfigurableApplicationContext, 29 | @Value("\${app.appName}") appName: String, 30 | tweetService: TweetService 31 | ) = CommandLineRunner { 32 | println("=== init $appName - $ctx =====") 33 | 34 | listOf( 35 | Tweet( 36 | id = "1", 37 | createdAt = Instant.now(), 38 | author = "seb", 39 | message = "msg1" 40 | ), 41 | Tweet( 42 | id = "2", 43 | createdAt = Instant.now(), 44 | author = "seb", 45 | message = "msg2" 46 | ) 47 | ).forEach { 48 | tweetService.submit(it) 49 | } 50 | 51 | } 52 | 53 | // No qualifying bean of type 'java.util.List' 54 | /* 55 | @Bean 56 | open fun webmvcRequestMappingHandlerMapping( 57 | webfluxMapping: WebFluxRequestMappingHandlerMapping 58 | ): WebMvcRequestMappingHandlerMapping { 59 | val mapping = WebMvcRequestMappingHandlerMapping() 60 | // add properties here 61 | return mapping 62 | } 63 | */ 64 | /* 65 | @Bean open fun foo(): WebMvcRequestMappingInfoHandlerMapping { 66 | WebMvcRequestMappingInfo() 67 | return WebMvcRequestMappingInfoHandlerMapping() 68 | } 69 | */ 70 | } 71 | 72 | fun main(args: Array) { 73 | runWebflux(DemoRestApplication::class, *args) 74 | } -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/DemoRestApplicationProperties.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties("app") 6 | class DemoRestApplicationProperties { 7 | var appName: String? = null 8 | } -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/api/handler/TweetHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.api.handler 2 | 3 | import com.example.demo.restservice.domain.Tweet 4 | import com.example.demo.restservice.domain.TweetService 5 | import com.example.demo.restservice.util.json 6 | import org.springframework.stereotype.Component 7 | import org.springframework.web.reactive.function.server.ServerRequest 8 | import org.springframework.web.reactive.function.server.ServerResponse 9 | import org.springframework.web.reactive.function.server.body 10 | import reactor.core.publisher.Mono 11 | import kotlin.streams.toList 12 | 13 | @Component 14 | class TweetHandler(private val tweetService: TweetService) { 15 | 16 | data class SubmitRequest(val author: String, val message: String) 17 | data class SubmitResponse(val id: String) 18 | data class TweetsCollectionResponse(val tweets: List) 19 | 20 | fun submitOne(req: ServerRequest): Mono { 21 | val submitRequest: Mono = 22 | req.bodyToMono(SubmitRequest::class.java) 23 | 24 | val submitResponse: Mono = submitRequest 25 | .flatMap { 26 | val tweet = tweetService 27 | .create(author = it.author, message = it.message) 28 | tweetService.submit(tweet) 29 | 30 | Mono.just(SubmitResponse(id = tweet.id)) 31 | } 32 | 33 | return ServerResponse 34 | .ok() 35 | .json() 36 | .body(submitResponse) 37 | } 38 | 39 | fun findAll(req: ServerRequest): Mono { 40 | val tweetsResponse = TweetsCollectionResponse( 41 | tweets = tweetService 42 | .getAll() 43 | .toStream() 44 | .toList() 45 | ) 46 | 47 | return ServerResponse 48 | .ok() 49 | .json() 50 | .body(Mono.just(tweetsResponse)) 51 | } 52 | 53 | fun findOne(req: ServerRequest): Mono { 54 | val tweet = tweetService.get(req.pathVariable("id")) 55 | 56 | return ServerResponse.ok() 57 | .json() 58 | .body(tweet) 59 | } 60 | } -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/api/routes/ApiRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.api.routes 2 | 3 | import com.example.demo.restservice.api.handler.TweetHandler 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.http.MediaType.APPLICATION_JSON 7 | import org.springframework.web.reactive.function.server.router 8 | 9 | 10 | @Configuration 11 | open class ApiRoutes(private val tweetHandler: TweetHandler) { 12 | 13 | @Bean 14 | open fun apiRouter() = router { 15 | (accept(APPLICATION_JSON) and "/api").nest { 16 | "/tweet".nest { 17 | POST("/submit", tweetHandler::submitOne) 18 | GET("/", tweetHandler::findAll) 19 | GET("/{id}", tweetHandler::findOne) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/api/routes/RedirectRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.api.routes 2 | 3 | import com.example.demo.restservice.api.handler.TweetHandler 4 | import com.example.demo.restservice.util.permanentRedirect 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.http.MediaType.TEXT_HTML 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.reactive.function.server.router 9 | 10 | @Component 11 | class RedirectRoutes(private val tweetHandler: TweetHandler) { 12 | 13 | @Bean 14 | fun redirectRouter() = router { 15 | accept(TEXT_HTML).nest { 16 | "/tweet".nest { 17 | GET("/") { permanentRedirect("/api/tweet") } 18 | (GET("/{id}") or GET("/{id}/")).invoke(tweetHandler::findOne) 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/configuration/Swagger.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.configuration 2 | 3 | /* 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import springfox.documentation.builders.ApiInfoBuilder 8 | import springfox.documentation.builders.RequestHandlerSelectors 9 | import springfox.documentation.service.BasicAuth 10 | import springfox.documentation.spi.DocumentationType 11 | import springfox.documentation.spring.web.plugins.Docket 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2 13 | 14 | @Configuration 15 | @EnableSwagger2() 16 | open class SwaggerConfiguration( 17 | @Value("\${app.appName}") appName: String 18 | ) { 19 | val API_ROUTE_SWAGGER_UI = "/swagger-ui.html" 20 | 21 | private val apiInfo by lazy { 22 | ApiInfoBuilder() 23 | .title(appName) 24 | .description("This is an example microservice application.") 25 | .build() 26 | } 27 | 28 | @Bean 29 | open fun demoApiDocket(): Docket { 30 | val groupName = "demo-api" 31 | val basePackage = "com.example.demo.restservice.api" 32 | 33 | return Docket(DocumentationType.SWAGGER_2) 34 | .groupName(groupName) 35 | .apiInfo(apiInfo) 36 | .select() 37 | .apis(RequestHandlerSelectors.basePackage(basePackage)) 38 | .build() 39 | .securitySchemes( 40 | arrayListOf( 41 | BasicAuth("basic-auth-realm") 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | */ -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/domain/Tweets.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.domain 2 | 3 | //import com.example.demo.logging.AppLogger 4 | import com.github.benmanes.caffeine.cache.Cache 5 | import com.github.benmanes.caffeine.cache.Caffeine 6 | import org.springframework.stereotype.Component 7 | import reactor.core.publisher.Flux 8 | import reactor.core.publisher.Mono 9 | import java.time.Duration 10 | import java.time.Instant 11 | import java.util.* 12 | import java.util.concurrent.TimeUnit 13 | 14 | data class Tweet( 15 | val id: String, 16 | val createdAt: Instant, 17 | val author: String, 18 | val message: String 19 | ) 20 | 21 | @Component 22 | class TweetService(private val repository: TweetRepository) { 23 | 24 | fun create(author: String, message: String) = Tweet( 25 | author = author, 26 | message = message, 27 | createdAt = Instant.now(), 28 | id = "${System.nanoTime()}-${UUID.randomUUID()}" 29 | ) 30 | 31 | fun submit(tweet: Tweet) = repository.add(tweet) 32 | fun get(tweetId: String) = repository.getOrNull(tweetId) 33 | fun getAll() = repository.getItems() 34 | fun findByAuthor(author: String) = getAll().filter { it.author == author } 35 | } 36 | 37 | typealias TweetRepositoryCache = Cache 38 | @Component 39 | class TweetRepository() { 40 | // private val LOGGER = AppLogger.get(javaClass) 41 | 42 | private val cache: TweetRepositoryCache by lazy { 43 | val expiry = Duration.ofDays(3) 44 | 45 | Caffeine 46 | .newBuilder() 47 | .maximumSize(1_000_000) 48 | .expireAfterWrite(expiry.seconds, TimeUnit.SECONDS) 49 | .build() 50 | } 51 | 52 | fun add(item: Tweet) { 53 | cache.put(item.id, item) 54 | // LOGGER.info("add item to repository. itemId=${item.id}") 55 | } 56 | 57 | fun getOrNull(itemId: String): Mono = Mono.justOrEmpty(cache.getIfPresent(itemId)) 58 | 59 | fun getItems(): Flux = Flux.fromIterable(cache.asMap().values) 60 | } 61 | -------------------------------------------------------------------------------- /rest-service/src/main/kotlin/com/example/demo/restservice/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.restservice.util 2 | 3 | 4 | import org.springframework.boot.SpringApplication 5 | import org.springframework.boot.WebApplicationType 6 | import org.springframework.boot.builder.SpringApplicationBuilder 7 | import org.springframework.http.MediaType.* 8 | import org.springframework.web.reactive.function.server.ServerResponse 9 | import org.springframework.web.reactive.function.server.ServerResponse.permanentRedirect 10 | import org.springframework.web.reactive.function.server.ServerResponse.seeOther 11 | import java.net.URI 12 | import kotlin.reflect.KClass 13 | 14 | // borrowed from: https://github.com/mixitconf/mixit/blob/master/src/main/kotlin/mixit/util/Extensions.kt 15 | 16 | // ---------------------- 17 | // Spring Boot extensions 18 | // ---------------------- 19 | 20 | fun run(type: KClass<*>, vararg args: String) = SpringApplication.run(type.java, *args) 21 | fun runWebflux(type: KClass<*>, vararg args: String) = SpringApplicationBuilder() 22 | .sources(type.java) 23 | .web(WebApplicationType.REACTIVE) 24 | .run(*args) 25 | 26 | // ------------------------- 27 | // Spring WebFlux extensions 28 | // ------------------------- 29 | 30 | 31 | fun ServerResponse.BodyBuilder.json() = contentType(APPLICATION_JSON_UTF8) 32 | 33 | fun ServerResponse.BodyBuilder.xml() = contentType(APPLICATION_XML) 34 | 35 | fun ServerResponse.BodyBuilder.html() = contentType(TEXT_HTML) 36 | 37 | fun permanentRedirect(uri: String) = permanentRedirect(URI(uri)).build() 38 | 39 | fun seeOther(uri: String) = seeOther(URI(uri)).build() -------------------------------------------------------------------------------- /rest-service/src/main/resources/_log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | # Direct log messages to stdout 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target=System.out 6 | # Logstash layout 7 | #log4j.appender.stdout.layout=net.logstash.log4j.JSONEventLayoutV1 8 | # "Readable" layout for local debugging 9 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 10 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m %X{traceId}%n 11 | 12 | -------------------------------------------------------------------------------- /rest-service/src/main/resources/_logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /rest-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main.banner-mode: 'OFF' 3 | 4 | app: 5 | appName: 'Demo RestService Application' 6 | 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'common' 2 | include 'rest-service' 3 | include 'rest-service-classic' 4 | 5 | --------------------------------------------------------------------------------