diff --git a/README.md b/README.md index 0587412..12a05b6 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Key ingredients for coroutines - Insert User ``` -curl -X POST -H "Content-Type: application/json" -d '{"id":null,"username":"JackRabbit","email":"Jack@Rabbit.com","avatarUrl":null}' http://localhost:8080/users/ +curl -X POST -H "Content-Type: application/json" -d '{"id":null,"userName":"JackRabbit","email":"Jack@Rabbit.com","avatarUrl":null}' http://localhost:8080/users/ ``` - Sync avatar @@ -72,3 +72,13 @@ gob -c 100 -n 500 -k http://localhost:8080/blocking/users/3/sync-avatar https://stackoverflow.com/questions/35628764/completablefuture-vs-spring-transactions + +# Coroutines with blocking calls. restTemplate +## case 1 - blocking, spring.threads.virtual.enabled:false +```shell +echo -n '{"id":null,"userName":"JackRabbit","email":"Jack@Rabbit.com","avatarUrl":null}' | http POST http://localhost:8080/users/with-blocking\?delay\=200 --meta +Elapsed time: 0.443338084s +``` + +## case 2 - blocking, spring.threads.virtual.enabled:true +```shell \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ffcc054 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,71 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile + +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.org.jetbrains.kotlin.plugin.spring) // org.jetbrains.kotlin:kotlin-allopen + alias(libs.plugins.org.jetbrains.kotlin.jvm) + alias(libs.plugins.org.jetbrains.kotlin.plugin.jpa) // all open for Entity classes + alias(libs.plugins.org.springframework.boot) + alias(libs.plugins.io.spring.dependency.management) + alias(libs.plugins.org.jetbrains.kotlinx.kover) + alias(libs.plugins.com.linecorp.build.recipe.plugin) +// alias(libs.plugins.org.jlleitschuh.gradle.ktlint) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.spring.io/snapshot") + } + + maven { + url = uri("https://repo.spring.io/milestone") + } + + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + implementation(libs.org.springframework.boot.spring.boot.starter.data.jpa) + implementation(libs.org.springframework.boot.spring.boot.starter.data.r2dbc) + implementation(libs.org.springframework.boot.spring.boot.starter.webflux) + implementation(libs.com.fasterxml.jackson.module.jackson.module.kotlin) + implementation(libs.io.projectreactor.kotlin.reactor.kotlin.extensions) + implementation(libs.org.jetbrains.kotlin.kotlin.reflect) + implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.reactor) + + implementation(libs.io.r2dbc.r2dbc.h2) + implementation(libs.io.r2dbc.r2dbc.spi) + implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.slf4j) + + testImplementation(libs.io.projectreactor.reactor.test) + testImplementation(libs.io.kotlintest.kotlintest.assertions) + testImplementation(libs.org.springframework.boot.spring.boot.starter.test) + testImplementation(libs.org.junit.jupiter.junit.jupiter.api) + testImplementation(libs.org.junit.jupiter.junit.jupiter.engine) + testImplementation(libs.org.jetbrains.kotlin.kotlin.test.junit5) +} + +group = "org.up" +version = "1.0.0" +description = "spring-boot-kotlin" +java.sourceCompatibility = JavaVersion.VERSION_21 + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +tasks { + compileKotlin { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + } + } + test { + useJUnitPlatform() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..cb32b5c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,64 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[plugins] + +org-springframework-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } + +io-spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.4" } + +org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + +org-jetbrains-kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +org-jetbrains-kotlin-plugin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } + +com-linecorp-build-recipe-plugin = { id = "com.linecorp.build-recipe-plugin", version = "1.1.1" } + +org-jetbrains-kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.5" } + +org-jlleitschuh-gradle-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.1.0" } + +[versions] + +com-fasterxml-jackson-module-jackson-module-kotlin = "2.16.1" +io-kotlintest-kotlintest-assertions = "3.4.2" +io-projectreactor-reactor-test = "3.6.2" +io-r2dbc-r2dbc-h2 = "1.0.0.RELEASE" +io-r2dbc-r2dbc-spi = "1.0.0.RELEASE" + +io-projectreactor-kotlin-reactor-kotlin-extensions = "1.2.2" + +spring-boot = "3.2.2" + +kotlin = "1.9.22" + +org-springframework-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } + +io-spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.0" } + +org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + +org-jetbrains-kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } + +[libraries] + +com-fasterxml-jackson-module-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "com-fasterxml-jackson-module-jackson-module-kotlin" } +io-kotlintest-kotlintest-assertions = { module = "io.kotlintest:kotlintest-assertions", version.ref = "io-kotlintest-kotlintest-assertions" } +io-projectreactor-reactor-test = { module = "io.projectreactor:reactor-test", version.ref = "io-projectreactor-reactor-test" } +io-r2dbc-r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "io-r2dbc-r2dbc-h2" } +io-r2dbc-r2dbc-spi = { module = "io.r2dbc:r2dbc-spi", version.ref = "io-r2dbc-r2dbc-spi" } +org-jetbrains-kotlin-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +org-jetbrains-kotlin-kotlin-script-runtime = { module = "org.jetbrains.kotlin:kotlin-script-runtime" } +org-jetbrains-kotlin-kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } +org-jetbrains-kotlinx-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } +org-jetbrains-kotlinx-kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8" } +org-jetbrains-kotlinx-kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor" } +org-jetbrains-kotlinx-kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j" } + +org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" } +org-junit-jupiter-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +org-springframework-boot-spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" } +org-springframework-boot-spring-boot-starter-data-r2dbc = { module = "org.springframework.boot:spring-boot-starter-data-r2dbc" } +org-springframework-boot-spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } +org-springframework-boot-spring-boot-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux" } +io-projectreactor-kotlin-reactor-kotlin-extensions = { module = "io.projectreactor.kotlin:reactor-kotlin-extensions", version.ref = "io-projectreactor-kotlin-reactor-kotlin-extensions" } \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/node-server/Dockerfile b/node-server/Dockerfile index 1222864..bc68ba0 100644 --- a/node-server/Dockerfile +++ b/node-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:7 +FROM node:latest WORKDIR /app RUN apt-get update -yq \ && apt-get install tcpdump -yq diff --git a/node-server/package-lock.json b/node-server/package-lock.json index ae5cac0..60c182c 100644 --- a/node-server/package-lock.json +++ b/node-server/package-lock.json @@ -1,373 +1,665 @@ { "name": "echo-server", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "2.1.20", - "negotiator": "0.6.1" + "packages": { + "": { + "name": "echo-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.18.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/repository/npm-group/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, - "array-flatten": { + "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.3", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.16" + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/repository/npm-group/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/repository/npm-group/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/repository/npm-group/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/repository/npm-group/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "license": "MIT" }, - "debug": { + "node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "resolved": "https://registry.npmjs.org/repository/npm-group/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "license": "MIT", + "dependencies": { "ms": "2.0.0" } }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "ee-first": { + "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "resolved": "https://registry.npmjs.org/repository/npm-group/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, - "encodeurl": { + "node_modules/encodeurl": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "resolved": "https://registry.npmjs.org/repository/npm-group/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "escape-html": { + "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "resolved": "https://registry.npmjs.org/repository/npm-group/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, - "etag": { + "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "1.3.5", + "resolved": "https://registry.npmjs.org/repository/npm-group/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/repository/npm-group/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.2", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.1", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.4", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "1.4.0", - "type-is": "1.6.16", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" } }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", + "dependencies": { "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.4.0", - "unpipe": "1.0.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": "1.4.0" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "media-typer": { + "resolved": "https://registry.npmjs.org/repository/npm-group/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repository/npm-group/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/repository/npm-group/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/repository/npm-group/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "resolved": "https://registry.npmjs.org/repository/npm-group/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "merge-descriptors": { + "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "license": "MIT" }, - "methods": { + "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "requires": { - "mime-db": "1.36.0" + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/repository/npm-group/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "ms": { + "node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { + "resolved": "https://registry.npmjs.org/repository/npm-group/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "path-to-regexp": { + "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/repository/npm-group/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.8.0" + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.4.0" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/repository/npm-group/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/repository/npm-group/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-group/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.3", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/repository/npm-group/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-group/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/repository/npm-group/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { "media-typer": "0.3.0", - "mime-types": "2.1.20" + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "resolved": "https://registry.npmjs.org/repository/npm-group/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } }, - "vary": { + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } } } } diff --git a/node-server/package.json b/node-server/package.json index 0888a40..5fefe7d 100644 --- a/node-server/package.json +++ b/node-server/package.json @@ -9,6 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "express": "^4.16.3" + "express": "^4.18.2" } } diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..e0d958a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,38 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ +import de.fayard.refreshVersions.core.StabilityLevel + + +pluginManagement { + repositories { + maven { url = uri("https://repo.spring.io/milestone") } + maven { url = uri("https://repo.spring.io/snapshot") } + gradlePluginPortal() + } + + plugins { + // See https://jmfayard.github.io/refreshVersions + id("de.fayard.refreshVersions").version("0.60.5") + } +} + +plugins { + id("de.fayard.refreshVersions") +} + +refreshVersions { + // ignore all non-stable releases + rejectVersionIf { + candidate.stabilityLevel != StabilityLevel.Stable + } + + // Or maybe you want to see alpha versions if you are already using an alpha version, otherwise you want to see only stable versions + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = "spring-boot-kotlin" diff --git a/src/main/kotlin/org/up/SpringApplication.kt b/src/main/kotlin/org/up/SpringApplication.kt index 8115879..9f93ad9 100644 --- a/src/main/kotlin/org/up/SpringApplication.kt +++ b/src/main/kotlin/org/up/SpringApplication.kt @@ -2,12 +2,11 @@ package org.up import org.springframework.boot.SpringApplication.run import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories @SpringBootApplication -@EnableR2dbcRepositories(value = arrayOf("org.up.coroutines", "org.up.reactor")) +@EnableR2dbcRepositories(value = ["org.up.coroutines", "org.up.reactor"]) @EnableJpaRepositories("org.up.blocking") class SpringApplication diff --git a/src/main/kotlin/org/up/blocking/BlockingConfig.kt b/src/main/kotlin/org/up/blocking/BlockingConfig.kt index e28d7cb..c7e431d 100644 --- a/src/main/kotlin/org/up/blocking/BlockingConfig.kt +++ b/src/main/kotlin/org/up/blocking/BlockingConfig.kt @@ -1,24 +1,20 @@ package org.up.blocking +import jakarta.persistence.EntityManagerFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import org.springframework.core.env.Environment import org.springframework.jdbc.datasource.DriverManagerDataSource -import org.springframework.orm.hibernate5.HibernateExceptionTranslator import org.springframework.orm.jpa.JpaTransactionManager import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter import org.springframework.transaction.PlatformTransactionManager -import org.springframework.transaction.annotation.EnableTransactionManagement import java.util.* -import javax.persistence.EntityManagerFactory import javax.sql.DataSource - @Configuration class BlockingConfig { - private val PROPERTY_NAME_DATABASE_DRIVER = "spring.datasource.driverClassName" private val PROPERTY_NAME_DATABASE_URL = "spring.datasource.url" private val PROPERTY_NAME_DATABASE_USERNAME = "spring.datasource.username" @@ -26,32 +22,35 @@ class BlockingConfig { private val PROPERTY_NAME_DATABASE_PLATFORM = "spring.jpa.database-platform" @Bean - fun entityManagerFactory(dataSource: DataSource, additionalProperties: Properties): LocalContainerEntityManagerFactoryBean = - LocalContainerEntityManagerFactoryBean().apply { - this.dataSource = dataSource - setPackagesToScan("org.up.blocking") - jpaVendorAdapter = HibernateJpaVendorAdapter() - setJpaProperties(additionalProperties) - } + fun entityManagerFactory( + dataSource: DataSource, + additionalProperties: Properties, + ): LocalContainerEntityManagerFactoryBean = + LocalContainerEntityManagerFactoryBean().apply { + this.dataSource = dataSource + setPackagesToScan("org.up.blocking") + jpaVendorAdapter = HibernateJpaVendorAdapter() + setJpaProperties(additionalProperties) + } @Bean - fun additionalProperties(environment: Environment): Properties = Properties().apply { - setProperty("hibernate.dialect", environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PLATFORM)) - } - + fun additionalProperties(environment: Environment): Properties = + Properties().apply { + setProperty("hibernate.dialect", environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PLATFORM)) + } @Bean fun dataSource(environment: Environment): DataSource = - DriverManagerDataSource().apply { - setDriverClassName(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)) - url = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL) - username = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME) - password = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD) - } + DriverManagerDataSource().apply { + setDriverClassName(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)) + url = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL) + username = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME) + password = environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD) + } @Bean @Primary fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { return JpaTransactionManager(entityManagerFactory) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/blocking/controller/BlockingUserController.kt b/src/main/kotlin/org/up/blocking/controller/BlockingUserController.kt index 417c6d0..4b72428 100644 --- a/src/main/kotlin/org/up/blocking/controller/BlockingUserController.kt +++ b/src/main/kotlin/org/up/blocking/controller/BlockingUserController.kt @@ -1,52 +1,56 @@ package org.up.blocking.controller -import org.slf4j.MDC +import jakarta.transaction.Transactional import org.springframework.web.bind.annotation.* import org.up.blocking.model.UserJpa import org.up.blocking.repository.BlockingAvatarService import org.up.blocking.repository.BlockingEnrollmentService import org.up.blocking.repository.BlockingUserDao -import org.up.coroutines.config.MdcWebFilter import org.up.utils.supplyAsync import org.up.utils.toNullable import java.util.concurrent.CompletableFuture -import javax.transaction.Transactional @RestController class BlockingUserController( - private val blockingUserDao: BlockingUserDao, - private val blockingAvatarService: BlockingAvatarService, - private val blockingEnrollmentService: BlockingEnrollmentService + private val blockingUserDao: BlockingUserDao, + private val blockingAvatarService: BlockingAvatarService, + private val blockingEnrollmentService: BlockingEnrollmentService, ) { - - @GetMapping("/blocking/users/{user-id}") @ResponseBody - fun getUser(@PathVariable("user-id") id: Long = 0): UserJpa? = - blockingUserDao.findById(id).toNullable() + fun getUser( + @PathVariable("user-id") id: Long = 0, + ): UserJpa? = blockingUserDao.findById(id).toNullable() @GetMapping("/blocking/users") @ResponseBody - fun getUsers(): List = - blockingUserDao.findAll() - + fun getUsers(): List = blockingUserDao.findAll() @PostMapping("/blocking/users") @ResponseBody @Transactional - fun storeUser(@RequestBody user: UserJpa, @RequestParam(required = false) delay:Long? = null): UserJpa { + fun storeUser( + @RequestBody user: UserJpa, + @RequestParam(required = false) delay: Long? = null, + ): UserJpa { val emailVerified = blockingEnrollmentService.verifyEmail(user.email, delay) val avatarUrl = user.avatarUrl ?: blockingAvatarService.randomAvatar(delay).url return blockingUserDao.save(user.copy(avatarUrl = avatarUrl, emailVerified = emailVerified)) } - @PostMapping("/futures/users") @ResponseBody - fun storeUserFutures(@RequestBody user: UserJpa, @RequestParam(required = false) delay:Long? = null): UserJpa { + fun storeUserFutures( + @RequestBody user: UserJpa, + @RequestParam(required = false) delay: Long? = null, + ): UserJpa { val emailVerifiedF = CompletableFuture.supplyAsync { blockingEnrollmentService.verifyEmail(user.email, delay) } - val avatuarUrl = if (user.avatarUrl != null) CompletableFuture.completedFuture(user.avatarUrl) else - CompletableFuture.supplyAsync { blockingAvatarService.randomAvatar(delay).url } + val avatuarUrl = + if (user.avatarUrl != null) { + CompletableFuture.completedFuture(user.avatarUrl) + } else { + CompletableFuture.supplyAsync { blockingAvatarService.randomAvatar(delay).url } + } val combinedF = avatuarUrl.thenCombineAsync(emailVerifiedF) { avatuarUrl, emailVerified -> avatuarUrl to emailVerified } val (avatarUrl, emailVerified) = combinedF.join() return blockingUserDao.save(user.copy(avatarUrl = avatarUrl, emailVerified = emailVerified)) @@ -54,21 +58,26 @@ class BlockingUserController( @PostMapping("/futures-mdc/users") @ResponseBody - fun storeUserFuturesMdc(@RequestBody user: UserJpa, @RequestParam(required = false) delay:Long? = null): UserJpa { + fun storeUserFuturesMdc( + @RequestBody user: UserJpa, + @RequestParam(required = false) delay: Long? = null, + ): UserJpa { val avatarUrlF = supplyAsync { blockingAvatarService.randomAvatar(delay).url } - val combinedF = supplyAsync { blockingEnrollmentService.verifyEmail(user.email, delay) } + val combinedF = + supplyAsync { blockingEnrollmentService.verifyEmail(user.email, delay) } .thenCombineAsync(avatarUrlF) { emailVerified, avatarUrl -> avatarUrl to emailVerified } val (avatarUrl, emailVerified) = combinedF.join() return blockingUserDao.save(user.copy(avatarUrl = avatarUrl, emailVerified = emailVerified)) } - @GetMapping("/blocking/users/{user-id}/sync-avatar") @ResponseBody - fun syncAvatar(@PathVariable("user-id") id: Long = 0, @RequestParam(required = false) delay:Long? = null): UserJpa? = - blockingUserDao.findById(id).toNullable()?.let { - val avatar = blockingAvatarService.randomAvatar(delay) - blockingUserDao.save(it.copy(avatarUrl = avatar.url)) - } + fun syncAvatar( + @PathVariable("user-id") id: Long = 0, + @RequestParam(required = false) delay: Long? = null, + ): UserJpa? = + blockingUserDao.findById(id).toNullable()?.let { + val avatar = blockingAvatarService.randomAvatar(delay) + blockingUserDao.save(it.copy(avatarUrl = avatar.url)) + } } - diff --git a/src/main/kotlin/org/up/blocking/model/User.kt b/src/main/kotlin/org/up/blocking/model/User.kt index ce17b7f..2b0cd0c 100644 --- a/src/main/kotlin/org/up/blocking/model/User.kt +++ b/src/main/kotlin/org/up/blocking/model/User.kt @@ -1,21 +1,16 @@ package org.up.blocking.model -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType - +import jakarta.persistence.* @Entity(name = "users") data class UserJpa( - @field: javax.persistence.Id @field: GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, - @field:Column("user_name") - val userName: String, - val email: String, - @field:Column("email_verified") - val emailVerified:Boolean, - @field:Column("avatar_url") - val avatarUrl: String?) { - -} + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + @Column(name = "user_name") + val userName: String, + val email: String, + @Column(name = "email_verified") + val emailVerified: Boolean, + @Column(name = "avatar_url") + val avatarUrl: String?, +) diff --git a/src/main/kotlin/org/up/blocking/repository/BlockingUserDao.kt b/src/main/kotlin/org/up/blocking/repository/BlockingUserDao.kt index da9bd75..72c8f88 100644 --- a/src/main/kotlin/org/up/blocking/repository/BlockingUserDao.kt +++ b/src/main/kotlin/org/up/blocking/repository/BlockingUserDao.kt @@ -9,40 +9,43 @@ import org.springframework.web.client.RestTemplate import org.up.blocking.model.UserJpa import org.up.coroutines.model.AvatarDto - @Repository interface BlockingUserDao : JpaRepository @Component -class BlockingAvatarService(@Value("\${remote.service.delay.ms}")val delayCfg:Long, - @Value("\${remote.service.url}")val baseUrl:String) { - +class BlockingAvatarService( + @Value("\${remote.service.delay.ms}")val delayCfg: Long, + @Value("\${remote.service.url}")val baseUrl: String, +) { val restTemplate = RestTemplate() fun randomAvatar(delay: Long? = null): AvatarDto = - restTemplate.getForEntity("$baseUrl/avatar?delay=${delay ?: delayCfg}", AvatarDto::class.java).body!!.also { - logger.debug("fetch random avatar...") - } + restTemplate.getForEntity("$baseUrl/avatar?delay=${delay ?: delayCfg}", AvatarDto::class.java).body!!.also { + logger.debug("fetch random avatar...") + } companion object { val logger = LoggerFactory.getLogger(BlockingAvatarService::class.java) } - } @Component -class BlockingEnrollmentService(@Value("\${remote.service.delay.ms}")val delayCfg:Long, - @Value("\${remote.service.url}")val baseUrl:String) { - +class BlockingEnrollmentService( + @Value("\${remote.service.delay.ms}")val delayCfg: Long, + @Value("\${remote.service.url}")val baseUrl: String, +) { val restTemplate = RestTemplate() - fun verifyEmail(email:String, delay: Long? = null): Boolean = - restTemplate.getForEntity("$baseUrl/echo?email=$email&value=true&delay=${delay ?: delayCfg}", String::class.java).body!! == "true".also { - logger.debug("verify email $email...") - } + fun verifyEmail( + email: String, + delay: Long? = null, + ): Boolean = + restTemplate.getForEntity("$baseUrl/echo?email=$email&value=true&delay=${delay ?: delayCfg}", String::class.java).body!! == + "true".also { + logger.debug("verify email $email...") + } companion object { val logger = LoggerFactory.getLogger(BlockingEnrollmentService::class.java) } - -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/coroutines/NonBlockingConfig.kt b/src/main/kotlin/org/up/coroutines/NonBlockingConfig.kt index 997c751..b0de004 100644 --- a/src/main/kotlin/org/up/coroutines/NonBlockingConfig.kt +++ b/src/main/kotlin/org/up/coroutines/NonBlockingConfig.kt @@ -1,8 +1,10 @@ -package org.up.coroutines.config +package org.up.coroutines import io.r2dbc.h2.H2ConnectionConfiguration import io.r2dbc.h2.H2ConnectionFactory import io.r2dbc.spi.ConnectionFactory +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy import org.reactivestreams.Subscription import org.slf4j.MDC import org.springframework.beans.factory.annotation.Value @@ -10,9 +12,12 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.ClassPathResource import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration -import org.springframework.data.r2dbc.connectionfactory.init.CompositeDatabasePopulator -import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer -import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator +import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator +// import org.springframework.data.r2dbc.connectionfactory.init.CompositeDatabasePopulator +// import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer +// import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator import org.springframework.stereotype.Component import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.config.WebFluxConfigurer @@ -27,9 +32,6 @@ import reactor.core.publisher.Operators import reactor.util.context.Context import java.util.* import java.util.stream.Collectors -import javax.annotation.PostConstruct -import javax.annotation.PreDestroy - @Configuration class DatastoreConfig : AbstractR2dbcConfiguration() { @@ -45,13 +47,14 @@ class DatastoreConfig : AbstractR2dbcConfiguration() { @Bean override fun connectionFactory(): ConnectionFactory { println("init connectionFactory") - return H2ConnectionFactory(H2ConnectionConfiguration.builder() - - //.inMemory(dbName) + return H2ConnectionFactory( + H2ConnectionConfiguration.builder() + // .inMemory(dbName) .url(url.removePrefix("jdbc:h2:")) .username(userName) .password(password) - .build()) + .build(), + ) } @Bean @@ -64,33 +67,31 @@ class DatastoreConfig : AbstractR2dbcConfiguration() { initializer.setDatabasePopulator(populator) return initializer } - } + @Configuration @EnableWebFlux -class WebFluxConfig : WebFluxConfigurer { - -} - +class WebFluxConfig : WebFluxConfigurer @Component class MdcWebFilter : WebFilter { - override fun filter(serverWebExchange: ServerWebExchange, - webFilterChain: WebFilterChain): Mono { + override fun filter( + serverWebExchange: ServerWebExchange, + webFilterChain: WebFilterChain, + ): Mono { val reqId = UUID.randomUUID().toString().replace("-", "").take(10) MDC.put(MDC_REQUEST_ID, reqId) - //println("set: " + MDC.get(MDC_REQUEST_ID)) - return webFilterChain.filter(serverWebExchange).subscriberContext{it.put(MDC_REQUEST_ID, reqId)} + // println("set: " + MDC.get(MDC_REQUEST_ID)) + return webFilterChain.filter(serverWebExchange).contextWrite { it.put(MDC_REQUEST_ID, reqId) } } + companion object { const val MDC_REQUEST_ID = "req-id" } } - @Configuration class MdcContextLifterConfiguration { - companion object { val MDC_CONTEXT_REACTOR_KEY: String = MdcContextLifterConfiguration::class.java.name } @@ -104,14 +105,12 @@ class MdcContextLifterConfiguration { fun cleanupHook() { Hooks.resetOnEachOperator(MDC_CONTEXT_REACTOR_KEY) } - } /** * Helper that copies the state of Reactor [Context] to MDC on the #onNext function. */ class MdcContextLifter(private val coreSubscriber: CoreSubscriber) : CoreSubscriber { - override fun onNext(t: T) { coreSubscriber.currentContext().copyToMdc() coreSubscriber.onNext(t) @@ -141,7 +140,8 @@ class MdcContextLifter(private val coreSubscriber: CoreSubscriber) : CoreS */ private fun Context.copyToMdc() { if (!this.isEmpty) { - val map: Map = this.stream() + val map: Map = + this.stream() .collect(Collectors.toMap({ e -> e.key.toString() }, { e -> e.value.toString() })) MDC.setContextMap(map) } else { @@ -149,11 +149,8 @@ private fun Context.copyToMdc() { } } - - @Configuration class WebClientConfiguration { - @Bean fun webClient() = WebClient.builder().baseUrl("http://localhost:8080").build() -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/coroutines/controller/ProductController.kt b/src/main/kotlin/org/up/coroutines/controller/ProductController.kt index 68c3b40..04c6491 100644 --- a/src/main/kotlin/org/up/coroutines/controller/ProductController.kt +++ b/src/main/kotlin/org/up/coroutines/controller/ProductController.kt @@ -1,13 +1,13 @@ package org.up.coroutines.controller -import org.up.coroutines.model.Product -import org.up.coroutines.repository.ProductRepository import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType import org.springframework.web.bind.annotation.* import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.bodyToMono +import org.up.coroutines.model.Product import org.up.coroutines.model.ProductStockView +import org.up.coroutines.repository.ProductRepository import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.time.Duration @@ -16,24 +16,30 @@ import java.time.Duration class ProductController { @Autowired lateinit var webClient: WebClient + @Autowired lateinit var productRepository: ProductRepository @GetMapping("/productsf/{id}") - fun findOne(@PathVariable id: Int): Mono { + fun findOne( + @PathVariable id: Int, + ): Mono { return productRepository - .getProductById(id) + .getProductById(id) } @GetMapping("/productsf/{id}/stock") - fun findOneInStock(@PathVariable id: Int): Mono { + fun findOneInStock( + @PathVariable id: Int, + ): Mono { val product = productRepository.getProductById(id) - val stockQuantity = webClient.get() - .uri("/productsf/$id/quantity") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono() + val stockQuantity = + webClient.get() + .uri("/productsf/$id/quantity") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono() return product.zipWith(stockQuantity) { productInStock, stockQty -> ProductStockView(productInStock, stockQty) } @@ -45,8 +51,10 @@ class ProductController { } @GetMapping(value = ["/productsf/sse"], produces = [(MediaType.TEXT_EVENT_STREAM_VALUE)]) - fun allMessages(@RequestParam(defaultValue = "5") size: Int = 1000): Flux { - //return this.messageService.findWithTailableCursorByRoom(room) + fun allMessages( + @RequestParam(defaultValue = "5") size: Int = 1000, + ): Flux { + // return this.messageService.findWithTailableCursorByRoom(room) return Flux.interval(Duration.ofSeconds(1)).map { i -> i.toString() } } @@ -56,7 +64,10 @@ class ProductController { } @PostMapping("/productsf/{name}/{price}") - fun postProduct(@PathVariable("name") name:String, @PathVariable("price") price:Int): Mono { + fun postProduct( + @PathVariable("name") name: String, + @PathVariable("price") price: Int, + ): Mono { return productRepository.addNewProduct(name, price) } } diff --git a/src/main/kotlin/org/up/coroutines/controller/ProductControllerCoroutines.kt b/src/main/kotlin/org/up/coroutines/controller/ProductControllerCoroutines.kt index f85f30d..8828c3f 100644 --- a/src/main/kotlin/org/up/coroutines/controller/ProductControllerCoroutines.kt +++ b/src/main/kotlin/org/up/coroutines/controller/ProductControllerCoroutines.kt @@ -1,7 +1,5 @@ package org.up.coroutines.controller -import org.up.coroutines.model.Product -import org.up.coroutines.repository.ProductRepositoryCoroutines import kotlinx.coroutines.Deferred import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope @@ -17,7 +15,9 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBody import org.springframework.web.reactive.function.client.awaitExchange +import org.up.coroutines.model.Product import org.up.coroutines.model.ProductStockView +import org.up.coroutines.repository.ProductRepositoryCoroutines import reactor.core.publisher.Flux import java.time.Duration @@ -29,33 +29,40 @@ class ProductControllerCoroutines { lateinit var productRepository: ProductRepositoryCoroutines @GetMapping("/products/{id}") - suspend fun findOne(@PathVariable id: Int): Product? { + suspend fun findOne( + @PathVariable id: Int, + ): Product? { return productRepository.getProductById(id) } @GetMapping("/products/{id}/stock") - suspend fun findOneInStock(@PathVariable id: Int): ProductStockView { - val product: Deferred = GlobalScope.async { - productRepository.getProductById(id) - } - val quantity: Deferred = GlobalScope.async { - webClient.get() - .uri("/products/$id/quantity") - .accept(APPLICATION_JSON) - .awaitExchange().awaitBody() - } + suspend fun findOneInStock( + @PathVariable id: Int, + ): ProductStockView { + val product: Deferred = + GlobalScope.async { + productRepository.getProductById(id) + } + val quantity: Deferred = + GlobalScope.async { + webClient.get() + .uri("/products/$id/quantity") + .accept(APPLICATION_JSON) + .awaitExchange().awaitBody() + } return ProductStockView(product.await()!!, quantity.await()) } @GetMapping(value = ["/products/sse"], produces = [(MediaType.TEXT_EVENT_STREAM_VALUE)]) - suspend fun allMessages(@RequestParam(defaultValue = "5") size: Int = 5): Flow { + suspend fun allMessages( + @RequestParam(defaultValue = "5") size: Int = 5, + ): Flow { return Flux.interval(Duration.ofSeconds(1)).map { i -> i.toString() }.asFlow() } - @FlowPreview @GetMapping("/products/") fun findAll(): Flow { return productRepository.getAllProducts() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/coroutines/controller/UserController.kt b/src/main/kotlin/org/up/coroutines/controller/UserController.kt index 80d8b69..b143db0 100644 --- a/src/main/kotlin/org/up/coroutines/controller/UserController.kt +++ b/src/main/kotlin/org/up/coroutines/controller/UserController.kt @@ -1,119 +1,158 @@ package org.up.coroutines.controller -import kotlinx.coroutines.flow.Flow -import org.springframework.web.bind.annotation.* -import org.up.coroutines.model.User -import org.up.coroutines.repository.AvatarService -import org.up.coroutines.repository.EnrollmentService -import org.up.coroutines.repository.UserRepository +import jakarta.transaction.Transactional import kotlinx.coroutines.async import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.slf4j.MDCContext import kotlinx.coroutines.withContext -import org.springframework.http.MediaType -import kotlinx.coroutines.flow.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent +import org.springframework.web.bind.annotation.* import org.springframework.web.server.ResponseStatusException -import javax.transaction.Transactional +import org.up.blocking.repository.BlockingAvatarService +import org.up.blocking.repository.BlockingEnrollmentService +import org.up.coroutines.model.User +import org.up.coroutines.repository.AvatarService +import org.up.coroutines.repository.EnrollmentService +import org.up.coroutines.repository.UserRepository @RestController class UserController( - private val userRepository: UserRepository, - private val avatarService: AvatarService, - private val enrollmentService: EnrollmentService + private val userRepository: UserRepository, + private val avatarService: AvatarService, + private val enrollmentService: EnrollmentService, + private val blockingEnrollmentService: BlockingEnrollmentService, + private val blockingAvatarService: BlockingAvatarService, ) { - - @GetMapping("/users") @ResponseBody - suspend fun getUsers(): Flow = - userRepository.findAll() - + suspend fun getUsers(): Flow = userRepository.findAll() @GetMapping("/users/{user-id}") @ResponseBody - suspend fun getUser(@PathVariable("user-id") id: Long = 0): User? = - userRepository.findById(id) - + suspend fun getUser( + @PathVariable("user-id") id: Long = 0, + ): User? = userRepository.findById(id) @GetMapping("/user") @ResponseBody - suspend fun userByName(@RequestParam("userName") userName: String): User? = - userRepository.findByUserName(userName) + suspend fun userByName( + @RequestParam("userName") userName: String, + ): User? = userRepository.findByUserName(userName) @PostMapping("/users") @ResponseBody @Transactional - suspend fun storeUser(@RequestBody user: User, @RequestParam(required = false) delay:Long? = null): User? = withContext(MDCContext()) { - val emailVerified = async { enrollmentService.verifyEmail(user.email, delay) } - val avatarUrl = async { user.avatarUrl ?: avatarService.randomAvatar(delay).url } - userRepository.save(user.copy(avatarUrl = avatarUrl.await(), emailVerified = emailVerified.await())).also { - channel.send(user.email) + suspend fun storeUser( + @RequestBody user: User, + @RequestParam(required = false) delay: Long? = null, + ): User? = + withContext(MDCContext()) { + val emailVerified = async { enrollmentService.verifyEmail(user.email, delay) } + val avatarUrl = async { user.avatarUrl ?: avatarService.randomAvatar(delay).url } + userRepository.save(user.copy(avatarUrl = avatarUrl.await(), emailVerified = emailVerified.await())).also { + channel.send(user.email) + } } - } + /** + * This method is a blocking version of the storeUser method. + * Because it uses the blockingEnrollmentService and blockingAvatarService. + * Which use restTemplate underlining, and it's blocking. + * But virtual threads are support restTemplate as well. + * So if enable virtual threads, it will be non-blocking. + * Here the response time > 400ms, because of the blockingEnrollmentService and blockingAvatarService. + * But we can use withContext(Dispatchers.IO) to make it non-blocking.(It's will exhausted the thread pool, if the thread pool size is small.) + * Here the response time > 200ms + * And we can use Dispatchers.VT instead of Dispatchers.IO, to make it non-blocking. + * Here the response time > 200ms + */ + @PostMapping("/users/with-blocking") + @ResponseBody + @Transactional + suspend fun storeUserWithBlocking( + @RequestBody user: User, + @RequestParam(required = false) delay: Long? = null, + ): User? = + withContext(MDCContext()) { + val emailVerified = async { blockingEnrollmentService.verifyEmail(user.email, delay) } + val avatarUrl = async { user.avatarUrl ?: blockingAvatarService.randomAvatar(delay).url } + userRepository.save(user.copy(avatarUrl = avatarUrl.await(), emailVerified = emailVerified.await())).also { + channel.send(user.email) + } + } @GetMapping("/users/{user-id}/sync-avatar") @ResponseBody @Transactional - suspend fun syncAvatar(@PathVariable("user-id") id: Long = 0, @RequestParam(required = false) delay:Long? = null): User = - userRepository.findById(id)?.let { - val avatar = avatarService.randomAvatar(delay) - userRepository.save(it.copy(avatarUrl = avatar.url)) - } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Unable to find user with id=$id") - - - + suspend fun syncAvatar( + @PathVariable("user-id") id: Long = 0, + @RequestParam(required = false) delay: Long? = null, + ): User = + userRepository.findById(id)?.let { + val avatar = avatarService.randomAvatar(delay) + userRepository.save(it.copy(avatarUrl = avatar.url)) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Unable to find user with id=$id") @GetMapping("/fibanocci/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) fun fibanocciFlow(): Flow { - val fibonacci: Sequence = generateSequence(Pair(0L, 1L), { Pair(it.second, it.first + it.second) }).map { it.first } + val fibonacci: Sequence = + generateSequence(Pair(0L, 1L), { Pair(it.second, it.first + it.second) }).map { it.first } return flow { - fibonacci.forEach {next -> + fibonacci.forEach { next -> if (next >= 0L) { delay(800) emit(next) - } else return@flow + } else { + return@flow + } } } } - @GetMapping("/infinite", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - fun infiniteFlow(): Flow = flow { - generateSequence(0){it + 1}.forEach { - emit(it.toString() + " \n") + fun infiniteFlow(): Flow = + flow { + generateSequence(0) { it + 1 }.forEach { + emit(it.toString() + " \n") + } } - } @GetMapping("/infinite/sse", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - fun sseFlow(): Flow> = flow { - generateSequence(0){it + 1}.map{it.toString()}.forEach { - emit(ServerSentEvent.builder().id(it).data(it).build()) - delay(it.toLong() % 1000) + fun sseFlow(): Flow> = + flow { + generateSequence(0) { it + 1 }.map { it.toString() }.forEach { + emit(ServerSentEvent.builder().id(it).data(it).build()) + delay(it.toLong() % 1000) + } } - } private val channel = BroadcastChannel(128) @GetMapping("/users/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) @ResponseBody - suspend fun userFlow(@RequestParam("offset") offsetId: Long = 0): Flow { - val userFlow: Flow = flow { - var latestId = offsetId - suspend fun take() = userRepository.findById_GreaterThan(latestId).collect { user -> - emit(user).also { latestId = user.id!! } + suspend fun userFlow( + @RequestParam("offset") offsetId: Long = 0, + ): Flow { + val userFlow: Flow = + flow { + var latestId = offsetId + + suspend fun take() = + userRepository.findById_GreaterThan(latestId).collect { user -> + emit(user).also { latestId = user.id!! } + } + take() + channel.consumeEach { + take() + channel.cancel() + } } - take() - channel.consumeEach { take() - channel.cancel()} - } return userFlow } - - } diff --git a/src/main/kotlin/org/up/coroutines/handlers/ProductsHandler.kt b/src/main/kotlin/org/up/coroutines/handlers/ProductsHandler.kt index 6e2f288..f00ed16 100644 --- a/src/main/kotlin/org/up/coroutines/handlers/ProductsHandler.kt +++ b/src/main/kotlin/org/up/coroutines/handlers/ProductsHandler.kt @@ -1,12 +1,9 @@ package org.up.coroutines.handlers -import org.up.coroutines.model.Product -import org.up.coroutines.repository.ProductRepositoryCoroutines import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.awaitFirst @@ -21,7 +18,9 @@ import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBody import org.springframework.web.reactive.function.client.awaitExchange import org.springframework.web.reactive.function.server.* +import org.up.coroutines.model.Product import org.up.coroutines.model.ProductStockView +import org.up.coroutines.repository.ProductRepositoryCoroutines import reactor.core.publisher.Flux import java.time.Duration import java.util.* @@ -29,25 +28,27 @@ import kotlin.math.absoluteValue @Component class ProductsHandler( - @Autowired var webClient: WebClient, - @Autowired var productRepository: ProductRepositoryCoroutines) { - + @Autowired var webClient: WebClient, + @Autowired var productRepository: ProductRepositoryCoroutines, +) { @FlowPreview suspend fun findAll(request: ServerRequest): ServerResponse = - ServerResponse.ok().json().bodyAndAwait(productRepository.getAllProducts()) + ServerResponse.ok().json().bodyAndAwait(productRepository.getAllProducts()) suspend fun findOneInStock(request: ServerRequest): ServerResponse { val id = request.pathVariable("id").toInt() - val product: Deferred = GlobalScope.async { - productRepository.getProductById(id) - } - val quantity: Deferred = GlobalScope.async { - webClient.get() - .uri("/products/$id/quantity") - .accept(MediaType.APPLICATION_JSON) - .awaitExchange().awaitBody() - } + val product: Deferred = + GlobalScope.async { + productRepository.getProductById(id) + } + val quantity: Deferred = + GlobalScope.async { + webClient.get() + .uri("/products/$id/quantity") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange().awaitBody() + } return ServerResponse.ok().json().bodyValueAndAwait(ProductStockView(product.await()!!, quantity.await())) } @@ -55,134 +56,137 @@ class ProductsHandler( val id = request.pathVariable("id").toInt() return ServerResponse.ok().json().bodyValueAndAwait(productRepository.getProductById(id)!!) } + suspend fun allMessagesFlux(request: ServerRequest): ServerResponse { - val flux = Flux.interval(Duration.ofSeconds(1)).map { i -> i.toString() }//.asFlow() + val flux = Flux.interval(Duration.ofSeconds(1)).map { i -> i.toString() } // .asFlow() return ServerResponse.ok().sse().body(flux).awaitFirst() } suspend fun allMessagesFlow(request: ServerRequest): ServerResponse { val size = request.queryParam("size").map { it.toInt() }.orElse(10) - val f = flow{ - (1..size).forEach{ - delay(1000) - emit(it.toString()) + val f = + flow { + (1..size).forEach { + delay(1000) + emit(it.toString()) + } } - } return ServerResponse.ok().sse().bodyAndAwait(f) } suspend fun forwardingEndpoint(request: ServerRequest): ServerResponse { val size = request.queryParam("size").map { it.toInt() }.orElse(10) val delay = request.queryParam("delay").map { it.toInt() }.orElse(500) - val type: ParameterizedTypeReference> = object : ParameterizedTypeReference>() {} - val stream = webClient.get() - .uri("/products/sse/delayed?size=$size&delay=$delay") - .retrieve() - .bodyToFlux(type) + val type: ParameterizedTypeReference> = + object : ParameterizedTypeReference>() {} + val stream = + webClient.get() + .uri("/products/sse/delayed?size=$size&delay=$delay") + .retrieve() + .bodyToFlux(type) .map { it.data()!! } .asFlow() return ServerResponse.ok().sse().bodyAndAwait(stream) } - suspend fun delayedEndpoint(request: ServerRequest): ServerResponse { val size = request.queryParam("size").map { it.toInt() }.orElse(10) val delay = request.queryParam("delay").map { it.toInt() }.orElse(500) - val f = flow{ - (1..size).forEach{ - delay(delay.toLong()) - emit(it.toString()) + val f = + flow { + (1..size).forEach { + delay(delay.toLong()) + emit(it.toString()) + } } - } return ServerResponse.ok().sse().bodyAndAwait(f) } + private val channel = MutableSharedFlow(replay = 128) - - private val channel = BroadcastChannel(128) suspend fun produceChannel(request: ServerRequest): ServerResponse { val size = request.queryParam("size").map { it.toInt() }.orElse(10) val wait = request.queryParam("delay").map { it.toInt() }.orElse(500) val close = request.queryParam("close").map { it.toBoolean() }.orElse(false) - if(close) channel.close() else - GlobalScope.launch { - (1..size).forEach{ - println("producing values $it") - delay(wait.toLong()) - channel.send("channelmsg=${it.toString()}") + + if (close) { + channel.resetReplayCache() + } else { + GlobalScope.launch { + (1..size).forEach { + println("producing values $it") + delay(wait.toLong()) + channel.emit("channelmsg=$it") + } } } + return ServerResponse.ok().bodyValueAndAwait("feed channel") } suspend fun consumeChannel(request: ServerRequest): ServerResponse { - val flow = channel.asFlow() + val flow = channel.asSharedFlow() return ServerResponse.ok().sse().bodyAndAwait(flow) } - - - - - //================================================= + // ================================================= suspend fun consumeDynamic(request: ServerRequest): ServerResponse { - val seq:Sequence = generateSequence(5){getItem()} - val f:Flow = flow { - suspend fun take() = seq.takeWhile { it != null }.forEach { - println("emitting") - delay(500) - emit(it.toString()) - } - take() - channel.consumeEach { - println("notification") + val seq: Sequence = generateSequence(5) { getItem() } + val f: Flow = + flow { + suspend fun take() = + seq.takeWhile { it != null }.forEach { + println("emitting") + delay(500) + emit(it.toString()) + } take() - } - - } + channel.collect { + println("notification") + take() + } + } return ServerResponse.ok().sse().bodyAndAwait(f) - } - fun getItem():Int? = + fun getItem(): Int? = (Random().nextInt().absoluteValue % 5).run { if (this != 0) this else null } } - @Configuration class RouterConfiguration { - @FlowPreview @Bean - fun productRoutes(productsHandler: ProductsHandler) = coRouter { - GET("/products/", productsHandler::findAll) - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse", productsHandler::allMessagesFlux) - } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse2", productsHandler::allMessagesFlow) - } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse/forwarding", productsHandler::forwardingEndpoint) - } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse/delayed", productsHandler::delayedEndpoint) - } + fun productRoutes(productsHandler: ProductsHandler) = + coRouter { + GET("/products", productsHandler::findAll) + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse", productsHandler::allMessagesFlux) + } + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse2", productsHandler::allMessagesFlow) + } + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse/forwarding", productsHandler::forwardingEndpoint) + } + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse/delayed", productsHandler::delayedEndpoint) + } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse/produce", productsHandler::produceChannel) - } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse/consume", productsHandler::consumeChannel) - } + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse/produce", productsHandler::produceChannel) + } + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse/consume", productsHandler::consumeChannel) + } - accept(MediaType.TEXT_EVENT_STREAM).nest { - GET("/products/sse/consume-dynamic", productsHandler::consumeDynamic) + accept(MediaType.TEXT_EVENT_STREAM).nest { + GET("/products/sse/consume-dynamic", productsHandler::consumeDynamic) + } + GET("/products/{id}", productsHandler::findOne) + GET("/products/{id}/stock", productsHandler::findOneInStock) } - GET("/products/{id}", productsHandler::findOne) - GET("/products/{id}/stock", productsHandler::findOneInStock) - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/coroutines/model/Product.kt b/src/main/kotlin/org/up/coroutines/model/Product.kt index f2d0945..a57f494 100644 --- a/src/main/kotlin/org/up/coroutines/model/Product.kt +++ b/src/main/kotlin/org/up/coroutines/model/Product.kt @@ -5,10 +5,10 @@ import org.springframework.data.relational.core.mapping.Table @Table data class Product( - @Id - var id: Int? = null, - var name: String = "", - var price: Int = 0 + @Id + var id: Int? = null, + var name: String = "", + var price: Int = 0, ) data class ProductStockView(val product: Product, var stockQuantity: Int) { @@ -21,4 +21,4 @@ data class ProductStockView(val product: Product, var stockQuantity: Int) { this.name = product.name this.price = product.price } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/coroutines/model/User.kt b/src/main/kotlin/org/up/coroutines/model/User.kt index 689f07e..36349c3 100644 --- a/src/main/kotlin/org/up/coroutines/model/User.kt +++ b/src/main/kotlin/org/up/coroutines/model/User.kt @@ -3,21 +3,19 @@ package org.up.coroutines.model import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table -import java.net.URL -import java.util.* - @Table("users") -data class User(@Id val id: Long? = null, - val userName: String, - val email: String, - val emailVerified: Boolean = false, - val avatarUrl: String? = null) { - -} - - -data class AvatarDto @JsonCreator constructor(@JsonProperty("url") val url: String) +data class User( + @Id val id: Long? = null, + val userName: String, + val email: String, + val emailVerified: Boolean = false, + val avatarUrl: String? = null, +) +data class AvatarDto +@JsonCreator +constructor( + @JsonProperty("url") val url: String, +) diff --git a/src/main/kotlin/org/up/coroutines/repository/CoroutineBridgeCrudRepository.kt b/src/main/kotlin/org/up/coroutines/repository/CoroutineBridgeCrudRepository.kt index 4ea7f4f..0a065c9 100644 --- a/src/main/kotlin/org/up/coroutines/repository/CoroutineBridgeCrudRepository.kt +++ b/src/main/kotlin/org/up/coroutines/repository/CoroutineBridgeCrudRepository.kt @@ -1,24 +1,16 @@ package org.up.coroutines.repository - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.reactive.asFlow -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.data.repository.reactive.ReactiveCrudRepository - - -//does not work yet as to 2.3.0.RC1 -//@Repository -//interface UserRepository_ : CoroutineCrudRepository { +// does not work yet as to 2.3.0.RC1 +// @Repository +// interface UserRepository_ : CoroutineCrudRepository { // // @Query("SELECT * FROM users WHERE id = ?0") // suspend fun findOne(id: Long): User? // // //fun findByFirstname(firstname: String): Flow -//} +// } -//abstract class CoroutineCrudRepository, T:Any, ID>(val underlying:R) { +// abstract class CoroutineCrudRepository, T:Any, ID>(val underlying:R) { // // suspend fun save(entity: S): S = underlying.save(entity).awaitFirst() // @@ -40,4 +32,4 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository // // suspend fun deleteAll() = underlying.deleteAll().awaitFirst() // -//} +// } diff --git a/src/main/kotlin/org/up/coroutines/repository/ProductRepository.kt b/src/main/kotlin/org/up/coroutines/repository/ProductRepository.kt index 1b43b8c..29aa853 100644 --- a/src/main/kotlin/org/up/coroutines/repository/ProductRepository.kt +++ b/src/main/kotlin/org/up/coroutines/repository/ProductRepository.kt @@ -4,66 +4,77 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.data.r2dbc.core.DatabaseClient +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter +import org.springframework.r2dbc.core.DatabaseClient import org.springframework.stereotype.Repository import org.up.coroutines.model.Product import reactor.core.publisher.Flux import reactor.core.publisher.Mono @Repository -class ProductRepository(private val client: DatabaseClient) { - +class ProductRepository( + private val client: DatabaseClient, + private val converter: MappingR2dbcConverter, +) { fun getProductById(id: Int): Mono { - return client.execute("SELECT * FROM product WHERE id = $1") - .bind(0, id) - .`as`(Product::class.java) - .fetch() - .one() + return client.sql("SELECT * FROM product WHERE id = $1") + .bind(0, id) + .map { row, meta -> + converter.read(Product::class.java, row, meta) + } + .one() } - fun addNewProduct(name: String, price: Int): Mono { - val product = Product(name = name, price = price) - return client.insert() - .into(Product::class.java) - .using(product) - .map{i -> i.get("id", Integer::class.java)} - .first() - .map { id -> product.copy(id = id?.toInt()) } + fun addNewProduct( + name: String, + price: Int, + ): Mono { + val product = Product(name = name, price = price) + return client.sql { "INSERT INTO product (name, price) VALUES($1, $2)" } + .map { i -> i.get("id", Integer::class.java) } + .first() + .map { id -> product.copy(id = id?.toInt()) } } fun getAllProducts(): Flux { - return client.select().from("product") - .`as`(Product::class.java) - .fetch() - .all() + return client.sql("SELECT * FROM product") + .map { row, meta -> + converter.read(Product::class.java, row, meta) + } + .all() } } @Repository -class ProductRepositoryCoroutines(private val client: DatabaseClient) { - +class ProductRepositoryCoroutines( + private val client: DatabaseClient, + private val converter: MappingR2dbcConverter, +) { suspend fun getProductById(id: Int): Product? = - client.execute("SELECT * FROM product WHERE id = $1") - .bind(0, id) - .`as`(Product::class.java) - .fetch() - .one() - .awaitFirstOrNull() + client.sql("SELECT * FROM product WHERE id = $1") + .bind(0, id) + .map { row, meta -> + converter.read(Product::class.java, row, meta) + } + .one() + .awaitFirstOrNull() - suspend fun addNewProduct(name: String, price: Int) = - client.execute("INSERT INTO product (name, price) VALUES($1, $2)") - .bind(0, name) - .bind(1, price) - .then() - .awaitFirstOrNull() + suspend fun addNewProduct( + name: String, + price: Int, + ) = client.sql("INSERT INTO product (name, price) VALUES($1, $2)") + .bind(0, name) + .bind(1, price) + .then() + .awaitFirstOrNull() @FlowPreview fun getAllProducts(): Flow = - client.select() - .from("products") - .`as`(Product::class.java) - .fetch() - .all() - .log() - .asFlow() -} \ No newline at end of file + client.sql("SELECT * FROM product") + .map { row, meta -> + converter.read(Product::class.java, row, meta) + } + .all() + .log() + .asFlow() +} diff --git a/src/main/kotlin/org/up/coroutines/repository/UserRepository.kt b/src/main/kotlin/org/up/coroutines/repository/UserRepository.kt index aab88c8..5a5b285 100644 --- a/src/main/kotlin/org/up/coroutines/repository/UserRepository.kt +++ b/src/main/kotlin/org/up/coroutines/repository/UserRepository.kt @@ -1,75 +1,70 @@ package org.up.coroutines.repository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.reactive.asFlow import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value -import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.kotlin.CoroutineCrudRepository -import org.springframework.data.repository.reactive.ReactiveCrudRepository import org.springframework.stereotype.Component import org.springframework.stereotype.Repository import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBody import org.up.coroutines.model.AvatarDto import org.up.coroutines.model.User -import reactor.core.publisher.Flux - @Repository interface UserRepository : CoroutineCrudRepository { + // @Query("select * from users e where e.id > :id") + fun findById_GreaterThan(id: Long): Flow - //@Query("select * from users e where e.id > :id") - fun findById_GreaterThan(id: Long) : Flow - - suspend fun findByUserName(userName: String) : User? + suspend fun findByUserName(userName: String): User? } - -//@Component -//class UserRepository(val reactiveUserRepository: ReactiveUserRepository) : CoroutineCrudRepository { +// @Component +// class UserRepository(val reactiveUserRepository: ReactiveUserRepository) : CoroutineCrudRepository { // suspend fun findUsersGreatherThan(id:Long) = findUsersGreatherThan(id).asFlow() // -//} +// } @Component -class AvatarService(@Value("\${remote.service.delay.ms}") val delayCfg: Long, - @Value("\${remote.service.url}") val baseUrl: String) { - +class AvatarService( + @Value("\${remote.service.delay.ms}") val delayCfg: Long, + @Value("\${remote.service.url}") val baseUrl: String, +) { private val client by lazy { WebClient.create(baseUrl) } - suspend fun randomAvatar(delay:Long? = null): AvatarDto = - client.get() - .uri("/avatar?delay=${delay ?: delayCfg}") - .retrieve() - .awaitBody().also { - logger.debug("fetch random avatar...") - } + suspend fun randomAvatar(delay: Long? = null): AvatarDto = + client.get() + .uri("/avatar?delay=${delay ?: delayCfg}") + .retrieve() + .awaitBody().also { + logger.debug("fetch random avatar...") + } companion object { val logger = LoggerFactory.getLogger(AvatarService::class.java) } - } @Component -class EnrollmentService(@Value("\${remote.service.delay.ms}") val delayCfg: Long, - @Value("\${remote.service.url}") val baseUrl: String) { - +class EnrollmentService( + @Value("\${remote.service.delay.ms}") val delayCfg: Long, + @Value("\${remote.service.url}") val baseUrl: String, +) { private val client by lazy { WebClient.create(baseUrl) } - suspend fun verifyEmail(email: String, delay:Long? = null): Boolean = - client.get() - .uri("/echo?email=$email&value=true&delay=${delay ?: delayCfg}") - .retrieve() - .awaitBody() - .toBoolean().also { - logger.debug("verify email $email...") - } + suspend fun verifyEmail( + email: String, + delay: Long? = null, + ): Boolean = + client.get() + .uri("/echo?email=$email&value=true&delay=${delay ?: delayCfg}") + .retrieve() + .awaitBody() + .toBoolean().also { + logger.debug("verify email $email...") + } companion object { val logger = LoggerFactory.getLogger(EnrollmentService::class.java) } - - -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/reactor/controller/ReactorUserController.kt b/src/main/kotlin/org/up/reactor/controller/ReactorUserController.kt index 421f801..7b47d2d 100644 --- a/src/main/kotlin/org/up/reactor/controller/ReactorUserController.kt +++ b/src/main/kotlin/org/up/reactor/controller/ReactorUserController.kt @@ -1,7 +1,7 @@ package org.up.reactor.controller +import jakarta.transaction.Transactional import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.web.bind.annotation.* import org.springframework.web.server.ResponseStatusException import org.up.coroutines.model.User @@ -10,37 +10,36 @@ import org.up.reactor.repository.ReactorEnrollmentService import org.up.reactor.repository.ReactorUserRepository import org.up.utils.component1 import org.up.utils.component2 -import reactor.core.publisher.EmitterProcessor import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers -import javax.transaction.Transactional - @RestController -open class ReactorUserController( - private val reactorUserDao: ReactorUserRepository, - private val reactorAvatarService: ReactorAvatarService, - private val reactorEnrollmentService: ReactorEnrollmentService +class ReactorUserController( + private val reactorUserDao: ReactorUserRepository, + private val reactorAvatarService: ReactorAvatarService, + private val reactorEnrollmentService: ReactorEnrollmentService, ) { - @GetMapping("/reactor/users") @ResponseBody - fun getUsers(): Flux = - reactorUserDao.findAll() + fun getUsers(): Flux = reactorUserDao.findAll() @GetMapping("/reactor/{user-id}") @ResponseBody - fun getUser(@PathVariable("user-id") id: Long): Mono { + fun getUser( + @PathVariable("user-id") id: Long, + ): Mono { return reactorUserDao.findById(id) } @PostMapping("/reactor/users") @ResponseBody @Transactional - fun storeUser(@RequestBody user: User, @RequestParam(required = false) delay:Long? = null): Mono { - val avatarM = reactorAvatarService.randomAvatar(delay)//.subscribeOn(Schedulers.elastic()) - val verifyEmailM = reactorEnrollmentService.verifyEmail(user.email, delay)//.subscribeOn(Schedulers.elastic()) + fun storeUser( + @RequestBody user: User, + @RequestParam(required = false) delay: Long? = null, + ): Mono { + val avatarM = reactorAvatarService.randomAvatar(delay) // .subscribeOn(Schedulers.elastic()) + val verifyEmailM = reactorEnrollmentService.verifyEmail(user.email, delay) // .subscribeOn(Schedulers.elastic()) return Mono.zip(avatarM, verifyEmailM).flatMap { (avatar, emailVerified) -> reactorUserDao.save(user.copy(avatarUrl = avatar.url, emailVerified = emailVerified)) } @@ -49,16 +48,18 @@ open class ReactorUserController( @GetMapping("/reactor/{user-id}/sync-avatar") @ResponseBody @Transactional - fun syncAvatar(@PathVariable("user-id") id: Long, @RequestParam(required = false) delay:Long? = null): Flux { + fun syncAvatar( + @PathVariable("user-id") id: Long, + @RequestParam(required = false) delay: Long? = null, + ): Flux { return reactorUserDao.findById(id) - .flatMap { user -> - reactorAvatarService.randomAvatar(delay) - .flatMap { avatar -> - reactorUserDao.save(user.copy(avatarUrl = avatar.url)) - } - }.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND, "Unable to find user with id=$id"))) - .flux() + .flatMap { user -> + reactorAvatarService.randomAvatar(delay) + .flatMap { avatar -> + reactorUserDao.save(user.copy(avatarUrl = avatar.url)) + } + } + .switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND, "Unable to find user with id=$id"))) + .flux() } - } - diff --git a/src/main/kotlin/org/up/reactor/repository/ReactorUserRepository.kt b/src/main/kotlin/org/up/reactor/repository/ReactorUserRepository.kt index 53c0717..6b3d588 100644 --- a/src/main/kotlin/org/up/reactor/repository/ReactorUserRepository.kt +++ b/src/main/kotlin/org/up/reactor/repository/ReactorUserRepository.kt @@ -7,62 +7,58 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository import org.springframework.stereotype.Component import org.springframework.stereotype.Repository import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.awaitBody -import org.up.blocking.repository.BlockingAvatarService -import org.up.blocking.repository.BlockingEnrollmentService import org.up.coroutines.model.AvatarDto import org.up.coroutines.model.User import reactor.core.publisher.Flux import reactor.core.publisher.Mono - @Repository interface ReactorUserRepository : ReactiveCrudRepository { @Query("select * from users e where e.id > :id") fun findUsersGreatherThan(id: Long): Flux - } - @Component -class ReactorAvatarService(@Value("\${remote.service.delay.ms}") val delayCfg: Long, - @Value("\${remote.service.url}") val baseUrl: String) { - +class ReactorAvatarService( + @Value("\${remote.service.delay.ms}") val delayCfg: Long, + @Value("\${remote.service.url}") val baseUrl: String, +) { private val client by lazy { WebClient.create(baseUrl) } fun randomAvatar(delay: Long? = null): Mono = - client.get() - .uri("/avatar?delay=${delay ?: delayCfg}") - .retrieve() - .bodyToMono(AvatarDto::class.java).also { - logger.debug("fetch random avatar...") - } + client.get() + .uri("/avatar?delay=${delay ?: delayCfg}") + .retrieve() + .bodyToMono(AvatarDto::class.java).also { + logger.debug("fetch random avatar...") + } companion object { val logger = LoggerFactory.getLogger(ReactorAvatarService::class.java) } - } @Component -class ReactorEnrollmentService(@Value("\${remote.service.delay.ms}") val delayCfg: Long, - @Value("\${remote.service.url}") val baseUrl: String) { - +class ReactorEnrollmentService( + @Value("\${remote.service.delay.ms}") val delayCfg: Long, + @Value("\${remote.service.url}") val baseUrl: String, +) { private val client by lazy { WebClient.create(baseUrl) } - fun verifyEmail(email: String, delay: Long? = null): Mono = - client.get() - .uri("/echo?email=$email&value=true&delay=${delay ?: delayCfg}") - .retrieve() - .bodyToMono(String::class.java) - .map { it.toBoolean() } - .also { - logger.debug("verify email $email...") - } + fun verifyEmail( + email: String, + delay: Long? = null, + ): Mono = + client.get() + .uri("/echo?email=$email&value=true&delay=${delay ?: delayCfg}") + .retrieve() + .bodyToMono(String::class.java) + .map { it.toBoolean() } + .also { + logger.debug("verify email $email...") + } companion object { val logger = LoggerFactory.getLogger(ReactorEnrollmentService::class.java) } - - -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/up/utils/Utils.kt b/src/main/kotlin/org/up/utils/Utils.kt index 97a90dc..179ea7f 100644 --- a/src/main/kotlin/org/up/utils/Utils.kt +++ b/src/main/kotlin/org/up/utils/Utils.kt @@ -1,14 +1,10 @@ package org.up.utils import org.slf4j.MDC -import org.up.coroutines.config.MdcWebFilter import reactor.util.function.Tuple2 import java.util.* import java.util.concurrent.CompletableFuture - - - fun Optional.toNullable(): T? { return if (this.isPresent) { this.get() @@ -17,11 +13,12 @@ fun Optional.toNullable(): T? { } } -//======Reactor Utils ====== -operator fun Tuple2.component1() = this.t1 -operator fun Tuple2.component2() = this.t2 +// ======Reactor Utils ====== +operator fun Tuple2.component1() = this.t1 + +operator fun Tuple2.component2() = this.t2 -//======CompletableFuture Utils ======= +// ======CompletableFuture Utils ======= fun supplyAsync(supplier: () -> U): CompletableFuture { return CompletableFuture.supplyAsync(SupplierMDC(supplier)) } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 6de8d2f..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,26 +0,0 @@ -logging.level.org.springframework.data.r2dbc=WARN -logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=WARN -#logging.level.org.springframework.data=DEBUG -spring.mvc.log-request-details=true -spring.h2.console.enabled=true -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:./schema.sql' -spring.datasource.jdbc-url=${spring.datasource.url} -spring.datasource.username=sa -spring.datasource.password= -spring.r2dbc.initialization-mode=always -spring.datasource.initialization-mode=always - -#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true -spring.datasource.hikari.maximumPoolSize=100 -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL95Dialect -spring.jpa.generate-ddl=false -spring.jpa.hibernate.ddl-auto=none -spring.jpa.show-sql=false -#logging.level.org.hibernate.sql=debug - - -#application parameters -remote.service.url=http://localhost:8081 -remote.service.delay.ms=300 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..61ec52e --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,47 @@ +logging: + level: + org: + springframework: + data: + r2dbc: WARN + web: + reactive: + function: + client: + ExchangeFunctions: WARN +remote: + service: + delay: + ms: 300 + url: http://localhost:8081 +spring: + datasource: + driverClassName: org.h2.Driver + hikari: + maximumPoolSize: 100 + jdbc-url: ${spring.datasource.url} + password: '' + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:./schema.sql' + username: sa + h2: + console: + enabled: true + jpa: + database-platform: org.hibernate.dialect.H2Dialect + generate-ddl: false + hibernate: + ddl-auto: none + show-sql: false +# mvc: +# log-request-details: true + webflux: + problem-details: + enabled: true +# jackson: +# property-naming-strategy: SNAKE_CASE + sql: + init: + mode: always + threads: + virtual: + enabled: true diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index cb6c59a..738dc0b 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,5 +1,3 @@ -create sequence if NOT EXISTS hibernate_sequence start 1 increment 1 cache 20; - CREATE TABLE IF NOT EXISTS product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR NOT NULL, diff --git a/src/test/kotlin/org/up/coroutines/UserRepositoryTest.kt b/src/test/kotlin/org/up/coroutines/UserRepositoryTest.kt index 9f073da..bc35363 100644 --- a/src/test/kotlin/org/up/coroutines/UserRepositoryTest.kt +++ b/src/test/kotlin/org/up/coroutines/UserRepositoryTest.kt @@ -11,19 +11,19 @@ import org.springframework.transaction.annotation.Transactional import org.up.coroutines.model.User import org.up.coroutines.repository.UserRepository - @SpringBootTest @Transactional @ExtendWith(SpringExtension::class) -class UserRepositoryTest @Autowired constructor( - val userRepository: UserRepository -) { - - @Test - fun `should insert new user and find it by Id`() = runBlocking{ - val newUser = userRepository.save(User(userName = "Joe", email = "joe@home.nl")) - val foundUser = userRepository.findById(newUser.id!!) - foundUser shouldBe newUser - +class UserRepositoryTest + @Autowired + constructor( + val userRepository: UserRepository, + ) { + @Test + fun `should insert new user and find it by Id`() = + runBlocking { + val newUser = userRepository.save(User(userName = "Joe", email = "joe@home.nl")) + val foundUser = userRepository.findById(newUser.id!!) + foundUser shouldBe newUser + } } -} \ No newline at end of file diff --git a/src/test/kotlin/org/up/coroutines/varia/Coroutines.kt b/src/test/kotlin/org/up/coroutines/varia/Coroutines.kt index 28aea2a..bf3951b 100644 --- a/src/test/kotlin/org/up/coroutines/varia/Coroutines.kt +++ b/src/test/kotlin/org/up/coroutines/varia/Coroutines.kt @@ -9,22 +9,23 @@ import kotlinx.coroutines.future.future import java.lang.Thread.sleep import java.util.* import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import kotlin.concurrent.thread import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import kotlin.system.measureTimeMillis -import java.util.concurrent.Executors -import java.util.concurrent.ExecutorService -import kotlin.coroutines.CoroutineContext - data class Product(val id: Int, val name: String) fun findProduct(id: Int): CompletableFuture = - CompletableFuture.supplyAsync { if (id == 9999) throw IllegalArgumentException("not found") else Product(id, "cool gadget") } + CompletableFuture.supplyAsync { if (id == 9999) throw IllegalArgumentException("not found") else Product(id, "cool gadget") } -fun getRating(p: Product): CompletableFuture = CompletableFuture.supplyAsync { if (p.id > 1000) throw IllegalArgumentException("not found") else 10 } +fun getRating(p: Product): CompletableFuture = + CompletableFuture.supplyAsync { + if (p.id > 1000) throw IllegalArgumentException("not found") else 10 + } suspend fun findProductCoroutine(id: Int): Product { delay(1000) @@ -36,7 +37,6 @@ suspend fun getRatingCoroutine(p: Product): Int { return 10 } - suspend fun temperatureIn(city: String): Int { delay(1000) return Random().nextInt(30) @@ -44,113 +44,126 @@ suspend fun temperatureIn(city: String): Int { suspend fun temperatureInException(city: String): Int { delay(1000) - return if (city == "???") + return if (city == "???") { throw IllegalArgumentException("??? does not exist") - else Random().nextInt(30) + } else { + Random().nextInt(30) + } } - fun temperatureInFuture(city: String): CompletableFuture = - CompletableFuture.supplyAsync { - Thread.sleep(1000) - Random().nextInt(30) - } - + CompletableFuture.supplyAsync { + Thread.sleep(1000) + Random().nextInt(30) + } -fun memoryUsageCoroutines() = runBlocking { - val cr = (1..2_400_000).toList().map { - async { - if (it % 100000 == 0) println("$it") - repeat(1000) { - //print(".") - delay(500) +fun memoryUsageCoroutines() = + runBlocking { + val cr = + (1..2_400_000).toList().map { + async { + if (it % 100000 == 0) println("$it") + repeat(1000) { + // print(".") + delay(500) + } + } } - } + cr.forEach { it.await() } + println("\ndone") + Thread.sleep(600000) } - cr.forEach { it.await() } - println("\ndone") - Thread.sleep(600000) -} - - -fun playThread(note: String, elapse: Long = 100, rep: Int = 50) = repeat(rep) { +fun playThread( + note: String, + elapse: Long = 100, + rep: Int = 50, +) = repeat(rep) { sleep(elapse) println("${Thread.currentThread().name}: $note") } -suspend fun play(note: String, elapse: Long = 100, rep: Int = 50) = repeat(rep) { +suspend fun play( + note: String, + elapse: Long = 100, + rep: Int = 50, +) = repeat(rep) { delay(elapse) println("${Thread.currentThread().name}: $note") } fun myFirstThread() { - val thread = thread { - sleep(100) - println("Hi") - } + val thread = + thread { + sleep(100) + println("Hi") + } println("Bonjour") thread.join() } fun mySecondThreads() { - val thread = (1..1_000_000).toList().map { - thread { - if (it % 1000 == 0) println("Created $it threads") - sleep(10000) - print(".") + val thread = + (1..1_000_000).toList().map { + thread { + if (it % 1000 == 0) println("Created $it threads") + sleep(10000) + print(".") + } } - } thread.forEach { it.join() } } suspend fun myFirstCoroutine() { - val job = GlobalScope.launch { - delay(100) - println("Hi") - } + val job = + GlobalScope.launch { + delay(100) + println("Hi") + } println("Bonjour") job.join() } -suspend fun mySecondCoroutine() = coroutineScope { - val jobs = (1..1_000_000).toList().map { - launch { - if (it % 1000 == 0) println("Created $it coroutines") - delay(10000) - print(".") - } +suspend fun mySecondCoroutine() = + coroutineScope { + val jobs = + (1..1_000_000).toList().map { + launch { + if (it % 1000 == 0) println("Created $it coroutines") + delay(10000) + print(".") + } + } + // jobs.forEach { it.join() } } - //jobs.forEach { it.join() } -} - -suspend fun CompletableFuture.await2(): T = suspendCoroutine { cont -> - whenComplete { value, exception -> - when { - exception != null -> cont.resumeWithException(exception) - else -> cont.resume(value) +suspend fun CompletableFuture.await2(): T = + suspendCoroutine { cont -> + whenComplete { value, exception -> + when { + exception != null -> cont.resumeWithException(exception) + else -> cont.resume(value) + } } } -} - fun futureExample() { GlobalScope.launch(Unconfined) { - //coroutineScope { - val future = future { - println("start") - val x = findProduct(10).await2() - println("got '$x'") - val y = getRating(x).await() - println("got '$y' after '$x'") - y - } + // coroutineScope { + val future = + future { + println("start") + val x = findProduct(10).await2() + println("got '$x'") + val y = getRating(x).await() + println("got '$y' after '$x'") + y + } val r = future.await() println("$r") } } -//} +// } fun futureExample2() { runBlocking { @@ -161,199 +174,208 @@ fun futureExample2() { } } - fun futureExampleTemperature() { - - runBlocking { - val time = measureTimeMillis { - val future = GlobalScope.future { - val ams = temperatureInFuture("Amsterdam") - val zrh = temperatureInFuture("Zurich") - "The answer is ams: ${ams.await()} zrh: ${zrh.await()}" - + val time = + measureTimeMillis { + val future = + GlobalScope.future { + val ams = temperatureInFuture("Amsterdam") + val zrh = temperatureInFuture("Zurich") + "The answer is ams: ${ams.await()} zrh: ${zrh.await()}" + } + println(future.await()) } - println(future.await()) - - } println("Completed in $time ms") } } fun futureExampleTemperature2() { runBlocking { - val time = measureTimeMillis { - val ams = temperatureInFuture("Amsterdam") - val zrh = temperatureInFuture("Zurich") - "The answer is ams: ${ams.await()} zrh: ${zrh.await()}" - } + val time = + measureTimeMillis { + val ams = temperatureInFuture("Amsterdam") + val zrh = temperatureInFuture("Zurich") + "The answer is ams: ${ams.await()} zrh: ${zrh.await()}" + } println("Completed in $time ms") } } - fun coRoutineExampleTemperature() { runBlocking { - val time = measureTimeMillis { - val ams = async { temperatureIn("Amsterdam") } - val zrh = async { temperatureIn("Zurich") } - println("The answer is ams: ${ams.await()} zrh: ${zrh.await()}") - - } + val time = + measureTimeMillis { + val ams = async { temperatureIn("Amsterdam") } + val zrh = async { temperatureIn("Zurich") } + println("The answer is ams: ${ams.await()} zrh: ${zrh.await()}") + } println("Completed in $time ms") } } fun coRoutineExampleTemperatureException() { runBlocking { - val time = measureTimeMillis { - try { - val ams = async { temperatureInException("???") } - val zrh = async { temperatureInException("Zurich") } - println("The answer is ams: ${ams.await()} zrh: ${zrh.await()}") - } catch (ex: Exception) { - println("Oeps ${ex.message}") + val time = + measureTimeMillis { + try { + val ams = async { temperatureInException("???") } + val zrh = async { temperatureInException("Zurich") } + println("The answer is ams: ${ams.await()} zrh: ${zrh.await()}") + } catch (ex: Exception) { + println("Oeps ${ex.message}") + } } - - } println("Completed in $time ms") } } - fun coRoutineExample() { - val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> - println(throwable) - } + val exceptionHandler = + CoroutineExceptionHandler { coroutineContext, throwable -> + println(throwable) + } - val job = GlobalScope.launch(Dispatchers.Default + exceptionHandler) { - val time = measureTimeMillis { - val rating = async { - val product = findProductCoroutine(1000) - getRatingCoroutine(product) - } - println("The answer is ${rating.await()}") + val job = + GlobalScope.launch(Dispatchers.Default + exceptionHandler) { + val time = + measureTimeMillis { + val rating = + async { + val product = findProductCoroutine(1000) + getRatingCoroutine(product) + } + println("The answer is ${rating.await()}") + } + println("Completed in $time ms") } - println("Completed in $time ms") - } runBlocking { job.join() } } - fun coRoutineSequentalExample() { - val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> - println(throwable) - } - runBlocking { - val job = launch(context = exceptionHandler) { - val product = findProductCoroutine(1000) - val rating = getRatingCoroutine(product) - println("$product has rating: $rating") + val exceptionHandler = + CoroutineExceptionHandler { coroutineContext, throwable -> + println(throwable) } + runBlocking { + val job = + launch(context = exceptionHandler) { + val product = findProductCoroutine(1000) + val rating = getRatingCoroutine(product) + println("$product has rating: $rating") + } println("done") } } -suspend fun sendReceive(id: Int = 0, channel:Channel):Unit = coroutineScope{ - val msg = "${this.coroutineContext[CoroutineName.Key]} send msg ${id+1}" - channel.send(msg) - println(msg) - delay(500) - val received = channel.receive() - println("${this.coroutineContext[CoroutineName.Key]} received=[$received]") - sendReceive(id + 1, channel) -} +suspend fun sendReceive( + id: Int = 0, + channel: Channel, +): Unit = + coroutineScope { + val msg = "${this.coroutineContext[CoroutineName.Key]} send msg ${id + 1}" + channel.send(msg) + println(msg) + delay(500) + val received = channel.receive() + println("${this.coroutineContext[CoroutineName.Key]} received=[$received]") + sendReceive(id + 1, channel) + } -suspend fun send(channel:Channel, msg:String) = coroutineScope{ +suspend fun send( + channel: Channel, + msg: String, +) = coroutineScope { val msg = "${Thread.currentThread().name} $msg" channel.send(msg) println(msg) delay(500) } +suspend fun receive(channel: Channel) = + coroutineScope { + val received = channel.receive() + println("${Thread.currentThread().name} received=[$received]") + } -suspend fun receive(channel:Channel) = coroutineScope{ - val received = channel.receive() - println("${Thread.currentThread().name} received=[$received]") -} - - - -suspend fun pingPong() = coroutineScope{ - val channel = Channel() +suspend fun pingPong() = + coroutineScope { + val channel = Channel() - launch { - suspend fun pongPing() { - receive(channel) - send(channel, "Pong") + launch { + suspend fun pongPing() { + receive(channel) + send(channel, "Pong") + pongPing() + } pongPing() } - pongPing() - } - val v1 = GlobalScope.launch { - suspend fun pingPong() { - send(channel, "Ping") - receive(channel) - pingPong() - } - pingPong() + val v1 = + GlobalScope.launch { + suspend fun pingPong() { + send(channel, "Ping") + receive(channel) + pingPong() + } + pingPong() + } + // v1.join() } - //v1.join() - - -} -suspend fun simpleChannel() = coroutineScope{ - val channel = Channel() - launch { - println("Send: Rabbit") - channel.send("Rabbit") - delay(500) - println("Send: Bear") - channel.send("Bear") - channel.close() +suspend fun simpleChannel() = + coroutineScope { + val channel = Channel() + launch { + println("Send: Rabbit") + channel.send("Rabbit") + delay(500) + println("Send: Bear") + channel.send("Bear") + channel.close() + } + channel.consumeEach { println("Turtle meets: $it") } } - channel.consumeEach { println("Turtle meets: ${it}") } -} -suspend fun receiveAll() = coroutineScope{ - val channel = Channel() - launch { - channel.send("Rabbit") - delay(500) - channel.send("Bear") - delay(500) - channel.close() - } - channel.consumeEach { println("Turtle meets: ${it}") } +suspend fun receiveAll() = + coroutineScope { + val channel = Channel() + launch { + channel.send("Rabbit") + delay(500) + channel.send("Bear") + delay(500) + channel.close() + } + channel.consumeEach { println("Turtle meets: $it") } // repeat (2) { // println("Turtle meets: ${channel.receive()}") // } -} - -suspend fun broadcast() = coroutineScope{ - val channel = BroadcastChannel(Channel.CONFLATED) - launch { - channel.send("Rabbit") - delay(500) - channel.close() } - launch { - channel.openSubscription().consumeEach { println("Turtle 1 meets: ${it}") } - } - launch { - channel.openSubscription().consumeEach { println("Turtle 2 meets: ${it}") } - } - val args = arrayOf("") - GlobalScope.produce { args.forEach{send(it)} } -} +suspend fun broadcast() = + coroutineScope { + val channel = BroadcastChannel(Channel.CONFLATED) + launch { + channel.send("Rabbit") + delay(500) + channel.close() + } + launch { + channel.openSubscription().consumeEach { println("Turtle 1 meets: $it") } + } + launch { + channel.openSubscription().consumeEach { println("Turtle 2 meets: $it") } + } + val args = arrayOf("") + GlobalScope.produce { args.forEach { send(it) } } + } -fun simpleFlow() = flow{ +fun simpleFlow() = + flow { emit("~ Go with ") delay(1000L) emit("the Flow ~") @@ -362,32 +384,28 @@ fun simpleFlow() = flow{ .onCompletion { println("Flow completed") } .catch { ex -> println("Exception message: ${ex.message}") } +fun main(args: Array): Unit = + runBlocking(Dispatchers.Default) { + (1..10).forEach { println(it) } + simpleFlow().collect { + print(it) + } + // broadcast() + // pingPong() -fun main(args: Array): Unit = runBlocking(Dispatchers.Default) { - (1..10).forEach{println(it)} - simpleFlow().collect{ - print(it) - } - - - //broadcast() - //pingPong() - - - //doIt_A() - // myFirstThread() -//mySecondThreads() - //mySecondCoroutine() + // doIt_A() + // myFirstThread() +// mySecondThreads() + // mySecondCoroutine() - //memoryUsageCoroutines() + // memoryUsageCoroutines() // futureExample2() // coRoutineSequentalExample() // futureExampleTemperature() // coRoutineExampleTemperatureException() - //coRoutineExampleTemperature() - -} + // coRoutineExampleTemperature() + } class CurrencyService(val returnRate: Int, val latency: Long) { suspend fun rateUSD(): Int { @@ -401,70 +419,76 @@ class CurrencyService(val returnRate: Int, val latency: Long) { } } - val serviceBankA = CurrencyService(120, 1000) val serviceBankB = CurrencyService(123, 2000) val serviceBankC = CurrencyService(125, 3000) val servicesBankABC = listOf(serviceBankA, serviceBankB, serviceBankC) - -suspend fun doIt_A() = coroutineScope { - val ctx = newSingleThreadContext("single-threaded-context") - val executorService = ctx.executor as ExecutorService - - //Threads - println("multiple threads threaded") - val r0 = measureTimeMillis { - val threads = servicesBankABC.map { - thread { - it.rateEUR() - println(Thread.currentThread().name) +suspend fun doIt_A() = + coroutineScope { + val ctx = newSingleThreadContext("single-threaded-context") + val executorService = ctx.executor as ExecutorService + + // Threads + println("multiple threads threaded") + val r0 = + measureTimeMillis { + val threads = + servicesBankABC.map { + thread { + it.rateEUR() + println(Thread.currentThread().name) + } + } + threads.map { it.join() }.toSet() } - } - threads.map { it.join() }.toSet() - } - println(r0) - - //Threads - println("single thread threaded") - val r0_ = measureTimeMillis { - val threads = executorService.invokeAll( - servicesBankABC.map { - Executors.callable { - it.rateEUR() - println(Thread.currentThread().name) + println(r0) + + // Threads + println("single thread threaded") + val r0_ = + measureTimeMillis { + val threads = + executorService.invokeAll( + servicesBankABC.map { + Executors.callable { + it.rateEUR() + println(Thread.currentThread().name) + } + }, + ) + threads.map { it.get() }.toSet() + } + println(r0_) + + println("coroutines") + val r1 = + measureTimeMillis { + val jobs = + servicesBankABC.map { + launch(ctx) { + it.rateUSD() + println(Thread.currentThread().name) + } } - }) - threads.map { it.get() }.toSet() - } - println(r0_) - - println("coroutines") - val r1 = measureTimeMillis { - val jobs = servicesBankABC.map { - launch(ctx) { - it.rateUSD() - println(Thread.currentThread().name) + jobs.map { it.join() }.toSet() } - } - jobs.map { it.join() }.toSet() + println(r1) } - println(r1) -} - - -suspend fun doIt(): Unit { +suspend fun doIt() { coroutineScope { // launch(Dispatchers.Default) { - val ms = measureTimeMillis { - val rates = servicesBankABC.map { async { it.rateUSD() } } - val avg = rates.map { - println("${Thread.currentThread().name}") - it.await() - }.sum() / rates.size - println("${Thread.currentThread().name} - avg: $avg") - } + val ms = + measureTimeMillis { + val rates = servicesBankABC.map { async { it.rateUSD() } } + val avg = + rates.map { + println("${Thread.currentThread().name}") + it.await() + }.sum() / rates.size + println("${Thread.currentThread().name} - avg: $avg") + } println(ms) // } } diff --git a/src/test/kotlin/org/up/coroutines/varia/ProductHandlerTest.kt b/src/test/kotlin/org/up/coroutines/varia/ProductHandlerTest.kt index fa53e18..43d00ee 100644 --- a/src/test/kotlin/org/up/coroutines/varia/ProductHandlerTest.kt +++ b/src/test/kotlin/org/up/coroutines/varia/ProductHandlerTest.kt @@ -1,37 +1,37 @@ package org.up.coroutines.varia -import org.up.coroutines.handlers.ProductsHandler -import org.up.coroutines.model.Product -import org.up.coroutines.repository.ProductRepositoryCoroutines import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull -import org.junit.Test -import org.junit.runner.RunWith +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.context.junit4.SpringRunner import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBodyList import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Flux -import org.springframework.test.context.ContextConfiguration +import org.up.coroutines.handlers.ProductsHandler import org.up.coroutines.handlers.RouterConfiguration +import org.up.coroutines.model.Product +import org.up.coroutines.repository.ProductRepositoryCoroutines +import reactor.core.publisher.Flux @WebFluxTest( - excludeAutoConfiguration = [ReactiveUserDetailsServiceAutoConfiguration::class, ReactiveSecurityAutoConfiguration::class] + excludeAutoConfiguration = [ReactiveUserDetailsServiceAutoConfiguration::class, ReactiveSecurityAutoConfiguration::class], ) -@RunWith(SpringRunner::class) +@ExtendWith(SpringExtension::class) @ContextConfiguration(classes = [ProductsHandler::class, RouterConfiguration::class]) class ProductHandlerTest { - @Autowired private lateinit var client: WebTestClient @@ -41,22 +41,22 @@ class ProductHandlerTest { @MockBean private lateinit var productsRepository: ProductRepositoryCoroutines - @FlowPreview @Test public fun `get all products`() { - val productsFlow = Flux.just( + val productsFlow = + Flux.just( Product(1, "product1", 1000), Product(2, "product2", 2000), - Product(3, "product3", 3000) - ).asFlow() + Product(3, "product3", 3000), + ).asFlow() given(productsRepository.getAllProducts()).willReturn(productsFlow) client.get() - .uri("/") - .exchange() - .expectStatus() - .isOk - .expectBodyList() + .uri("/products") + .exchange() + .expectStatus() + .isOk + .expectBodyList() } // @Test @@ -65,16 +65,16 @@ class ProductHandlerTest { // productsRepository.addNewProduct(product.name, product.price) // } - @Test fun `flow sample 1`() { - fun foo(): Flow = flow { - println("Flow started") - for (i in 1..3) { - delay(1000) - emit(i) + fun foo(): Flow = + flow { + println("Flow started") + for (i in 1..3) { + delay(1000) + emit(i) + } } - } suspend fun performRequest(request: Int): String { delay(1000) // imitate long-running asynchronous work @@ -88,18 +88,16 @@ class ProductHandlerTest { println("Calling collect again...") flow.collect { value -> println(value) } - withTimeoutOrNull(250) { // Timeout after 250ms foo().collect { value -> println(value) } } (1..3).asFlow() // a flow of requests - .transform { request -> - emit("abc") - emit(performRequest(request)) } - .collect { response -> println(response) } - + .transform { request -> + emit("abc") + emit(performRequest(request)) + } + .collect { response -> println(response) } } } - } diff --git a/src/test/kotlin/org/up/coroutines/varia/Samples.kt b/src/test/kotlin/org/up/coroutines/varia/Samples.kt index 011ed08..cd6721f 100644 --- a/src/test/kotlin/org/up/coroutines/varia/Samples.kt +++ b/src/test/kotlin/org/up/coroutines/varia/Samples.kt @@ -7,27 +7,27 @@ import org.up.coroutines.model.User import java.util.* import kotlin.math.absoluteValue - data class Users( - val userName: String, - val email: String) -fun main() { + val userName: String, + val email: String, +) +fun main() { val mapper = ObjectMapper() - mapper.registerKotlinModule() - println(mapper.writeValueAsString(User(null, "Jack", "Rabbit"))) -println(mapper.readValue("""{"firstName":"Jack","lastName":"Rabbit"}""")) + mapper.registerKotlinModule() + println(mapper.writeValueAsString(User(null, "Jack", "Rabbit"))) + println(mapper.readValue("""{"firstName":"Jack","lastName":"Rabbit"}""")) - fun getItem():Int? = - (Random().nextInt().absoluteValue % 5).run { - if (this != 0) this else null - } + fun getItem(): Int? = + (Random().nextInt().absoluteValue % 5).run { + if (this != 0) this else null + } - val i:Int? = null - val seq:Sequence = generateSequence(3){getItem()} - fun take() =seq.takeWhile { it != null }.forEach { print(it.toString()) }.also { println("") } + val i: Int? = null + val seq: Sequence = generateSequence(3) { getItem() } + + fun take() = seq.takeWhile { it != null }.forEach { print(it.toString()) }.also { println("") } take() take() } - diff --git a/src/test/kotlin/org/up/reactor/ReactorUserRepositoryTest.kt b/src/test/kotlin/org/up/reactor/ReactorUserRepositoryTest.kt index 82328ea..8737d83 100644 --- a/src/test/kotlin/org/up/reactor/ReactorUserRepositoryTest.kt +++ b/src/test/kotlin/org/up/reactor/ReactorUserRepositoryTest.kt @@ -1,6 +1,6 @@ package org.up.reactor - +import io.kotlintest.shouldBe import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -10,27 +10,26 @@ import org.springframework.transaction.annotation.Transactional import org.up.coroutines.model.User import org.up.reactor.repository.ReactorUserRepository import reactor.test.StepVerifier -import io.kotlintest.shouldBe @SpringBootTest @Transactional @ExtendWith(SpringExtension::class) -class ReactorUserRepositoryTest @Autowired constructor( - val reactorUserRepository: ReactorUserRepository -) { - - @Test - fun `should insert new user and find it by Id`() { - val newUser = reactorUserRepository.save(User(userName = "Joe", email = "joe@home.nl")) - StepVerifier.create(newUser) +class ReactorUserRepositoryTest + @Autowired + constructor( + val reactorUserRepository: ReactorUserRepository, + ) { + @Test + fun `should insert new user and find it by Id`() { + val newUser = reactorUserRepository.save(User(userName = "Joe", email = "joe@home.nl")) + StepVerifier.create(newUser) .expectNextCount(1) .expectComplete() .verify() - val foundUser = reactorUserRepository.findById(newUser.block()?.id!!) - StepVerifier.create(foundUser) + val foundUser = reactorUserRepository.findById(newUser.block()?.id!!) + StepVerifier.create(foundUser) .assertNext { user -> user.userName shouldBe newUser.block()!!.userName } .expectComplete() .verify() + } } - -} \ No newline at end of file diff --git a/versions.properties b/versions.properties new file mode 100644 index 0000000..aab990f --- /dev/null +++ b/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.5 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file.