diff --git a/2025-09/spring-33-docker/docker-compose-example/.gitignore b/2025-09/spring-33-docker/docker-compose-example/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..2cc7d4a5 Binary files /dev/null and b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar differ diff --git a/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-09/spring-33-docker/docker-compose-example/Dockerfile b/2025-09/spring-33-docker/docker-compose-example/Dockerfile new file mode 100644 index 00000000..c7a02fbc --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/Dockerfile @@ -0,0 +1,4 @@ +FROM bellsoft/liberica-openjdk-alpine-musl:21.0.1 +COPY /target/docker-compose-example.jar /app/app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/2025-09/spring-33-docker/docker-compose-example/docker-compose.yml b/2025-09/spring-33-docker/docker-compose-example/docker-compose.yml new file mode 100644 index 00000000..94916a8e --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/docker-compose.yml @@ -0,0 +1,21 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + # Эти свойства перегружают соответствующие в application.yml + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/db + - SPRING_DATASOURCE_USERNAME=postgres + - SPRING_DATASOURCE_PASSWORD=postgres + postgres: + image: "postgres:17" + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=db + diff --git a/2025-09/spring-33-docker/docker-compose-example/mvnw b/2025-09/spring-33-docker/docker-compose-example/mvnw new file mode 100644 index 00000000..a16b5431 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-09/spring-33-docker/docker-compose-example/mvnw.cmd b/2025-09/spring-33-docker/docker-compose-example/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-09/spring-33-docker/docker-compose-example/pom.xml b/2025-09/spring-33-docker/docker-compose-example/pom.xml new file mode 100644 index 00000000..71423fd4 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + ru.otus.spring + docker-compose-example + 0.0.1-SNAPSHOT + docker-compose-example + Demo project for Spring Boot + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + test + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + docker-compose-example + + diff --git a/2025-09/spring-33-docker/docker-compose-example/readme.txt b/2025-09/spring-33-docker/docker-compose-example/readme.txt new file mode 100644 index 00000000..964c9576 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/readme.txt @@ -0,0 +1,3 @@ +./mvnw package +docker compose up -d +curl http://localhost:8080/api/persons \ No newline at end of file diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java new file mode 100644 index 00000000..76f60e7d --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java @@ -0,0 +1,22 @@ +package ru.otus.spring.docker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import ru.otus.spring.docker.model.Person; +import ru.otus.spring.docker.repository.PersonRepository; + +@SpringBootApplication +@EnableJpaRepositories +public class DockerComposeExampleApplication { + + public static void main(String[] args) { + //Код для примера, делать так конечно нельзя :) + ApplicationContext context = SpringApplication.run(DockerComposeExampleApplication.class, args); + PersonRepository repository = context.getBean(PersonRepository.class); + repository.save(new Person("Ivan", "Ivanov")); + System.out.println(repository.findAll()); + } + +} diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java new file mode 100644 index 00000000..5335b097 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java @@ -0,0 +1,28 @@ +package ru.otus.spring.docker.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@Entity +@AllArgsConstructor +@NoArgsConstructor +public class Person { + + @GeneratedValue(strategy = GenerationType.AUTO) + @Id + private Integer id; + private String name; + private String lastName; + + public Person(String name, String lastName) { + this.name = name; + this.lastName = lastName; + } +} diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java new file mode 100644 index 00000000..11262d78 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java @@ -0,0 +1,7 @@ +package ru.otus.spring.docker.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.otus.spring.docker.model.Person; + +public interface PersonRepository extends JpaRepository { +} diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java new file mode 100644 index 00000000..4cfc6436 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java @@ -0,0 +1,21 @@ +package ru.otus.spring.docker.rest; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.otus.spring.docker.model.Person; +import ru.otus.spring.docker.repository.PersonRepository; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/api/persons") + public List getAllPersons() { + return this.repository.findAll(); + } +} diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/resources/application.yml b/2025-09/spring-33-docker/docker-compose-example/src/main/resources/application.yml new file mode 100644 index 00000000..bf91784a --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + datasource: + # Эти свойства будут перегружены свойствами в docker-compose.yml + url: jdbc:postgresql://localhost:5432/db + username: postgres + password: postgres + jpa: + generate-ddl: true diff --git a/2025-09/spring-33-docker/docker-compose-example/src/main/resources/static/index.html b/2025-09/spring-33-docker/docker-compose-example/src/main/resources/static/index.html new file mode 100644 index 00000000..a80ea187 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + + Главная страницв + + +

Главная страница

+

Список всех лиц доступен по ссылке.

+

Перезапустив приложение можно добавить ещё в БД.

+ + diff --git a/2025-09/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java b/2025-09/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java new file mode 100644 index 00000000..93fe13c4 --- /dev/null +++ b/2025-09/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java @@ -0,0 +1,15 @@ +package ru.otus.spring.docker; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@AutoConfigureTestDatabase +class DockerComposeExampleApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/2025-09/spring-33-docker/helloWorld.txt b/2025-09/spring-33-docker/helloWorld.txt new file mode 100644 index 00000000..f2c3689e --- /dev/null +++ b/2025-09/spring-33-docker/helloWorld.txt @@ -0,0 +1,20 @@ +docker image ls +docker pull hello-world +docker image ls +docker image rm hello-world +docker ps +docker run hello-world +docker ps +docker run hello-world +docker ps -all +docker run --rm hello-world + +docker run -it --name=ubuntu-run ubuntu bash +docker start ubuntu-run +docker exec -it ubuntu-run bash + +docker run -d -p:8080:80 --name=my-nginx nginx +docker ps -a +curl http://localhost:8080 +docker kill my-nginx +docker rm my-nginx diff --git a/2025-09/spring-33-docker/image/Dockerfile b/2025-09/spring-33-docker/image/Dockerfile new file mode 100644 index 00000000..c51e4bde --- /dev/null +++ b/2025-09/spring-33-docker/image/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.11-alpine +COPY index.html /usr/share/nginx/html/index.html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/2025-09/spring-33-docker/image/index.html b/2025-09/spring-33-docker/image/index.html new file mode 100644 index 00000000..f3e333e8 --- /dev/null +++ b/2025-09/spring-33-docker/image/index.html @@ -0,0 +1 @@ +

Hello World

diff --git a/2025-09/spring-33-docker/image/readme.txt b/2025-09/spring-33-docker/image/readme.txt new file mode 100644 index 00000000..9dde7083 --- /dev/null +++ b/2025-09/spring-33-docker/image/readme.txt @@ -0,0 +1,5 @@ +docker build -t my-demo-image:v1 . +docker images | grep my-demo-image +docker run -p:80:80 --rm my-demo-image:v1 +docker ps +curl http://localhost \ No newline at end of file diff --git a/2025-09/spring-34-kuber/.gitignore b/2025-09/spring-34-kuber/.gitignore new file mode 100755 index 00000000..dae2cf99 --- /dev/null +++ b/2025-09/spring-34-kuber/.gitignore @@ -0,0 +1,39 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle +/buildSrc/.gradle/ + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-09/spring-34-kuber/HttpRequests.http b/2025-09/spring-34-kuber/HttpRequests.http new file mode 100755 index 00000000..29913ccf --- /dev/null +++ b/2025-09/spring-34-kuber/HttpRequests.http @@ -0,0 +1,34 @@ +### +GET http://localhost:8080/hi?name=Ivan +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +POST http://localhost:8080/response/Ivan +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +{ + "param1": "pa1", + "param2": "pa2" +} + +### +GET http://localhost:8090/actuator/health +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8090/actuator/health/liveness +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8090/actuator/health/readiness +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-09/spring-34-kuber/LICENSE b/2025-09/spring-34-kuber/LICENSE new file mode 100755 index 00000000..ada0a70c --- /dev/null +++ b/2025-09/spring-34-kuber/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Sergey Petrelevich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2025-09/spring-34-kuber/README.md b/2025-09/spring-34-kuber/README.md new file mode 100755 index 00000000..abe57084 --- /dev/null +++ b/2025-09/spring-34-kuber/README.md @@ -0,0 +1 @@ +# gitlab Hello \ No newline at end of file diff --git a/2025-09/spring-34-kuber/build.gradle.kts b/2025-09/spring-34-kuber/build.gradle.kts new file mode 100755 index 00000000..dff4344e --- /dev/null +++ b/2025-09/spring-34-kuber/build.gradle.kts @@ -0,0 +1,116 @@ +import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension +import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES +import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel +import org.gradle.api.plugins.JavaPluginExtension +import fr.brouillard.oss.gradle.plugins.JGitverPluginExtension +import fr.brouillard.oss.gradle.plugins.JGitverPlugin +import name.remal.gradle_plugins.sonarlint.SonarLintExtension + +plugins { + idea + id("fr.brouillard.oss.gradle.jgitver") + id("io.spring.dependency-management") + id("org.springframework.boot") apply false + id("name.remal.sonarlint") apply false + id("com.diffplug.spotless") apply false +} + + +idea { + project { + languageLevel = IdeaLanguageLevel(21) + } + module { + isDownloadJavadoc = true + isDownloadSources = true + } +} + + +allprojects { + group = "ru.petrelevich" + + repositories { + mavenLocal() + mavenCentral() + } + + apply(plugin = "io.spring.dependency-management") + dependencyManagement { + dependencies { + imports { + mavenBom(BOM_COORDINATES) + } + } + } + + configurations.all { + resolutionStrategy { + failOnVersionConflict() + + force("commons-io:commons-io:2.16.1") + force("org.eclipse.jgit:org.eclipse.jgit:6.9.0.202403050737-r") + force("org.apache.commons:commons-compress:1.26.1") + force("com.google.errorprone:error_prone_annotations:2.28.0") + force("org.jetbrains:annotations:19.0.0") + } + } + + tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:all,-serial,-processing", "-Werror")) + } + + apply() + configure { + nodeJs { + detectNodeJs = false + logNodeJsNotFound = false + } + } + + apply() + configure { + java { + palantirJavaFormat("2.50.0") + } + } + + tasks.withType { + useJUnitPlatform() + testLogging.showExceptions = true + reports { + junitXml.required.set(true) + html.required.set(true) + } + } + + plugins.apply(JavaPlugin::class.java) + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + plugins.apply(JGitverPlugin::class.java) + extensions.configure { + strategy("PATTERN") + nonQualifierBranches("main,master") + tagVersionPattern("\${v}\${() + .managedVersions + .toSortedMap() + .map { "${it.key}:${it.value}" } + .forEach(::println) + } + } +} \ No newline at end of file diff --git a/2025-09/spring-34-kuber/gradle.properties b/2025-09/spring-34-kuber/gradle.properties new file mode 100755 index 00000000..efdab048 --- /dev/null +++ b/2025-09/spring-34-kuber/gradle.properties @@ -0,0 +1,12 @@ +# -------Gradle-------- +org.gradle.jvmargs=-Xmx4g +org.gradle.daemon=true +org.gradle.parallel=true +# -------Plugins--------- +jgitver=0.10.0-rc03 +dependencyManagement=1.1.6 +springframeworkBoot=3.4.0 +jib=3.4.4 +sonarlint=4.3.1 +spotless=6.25.0 +# -------Versions-------- diff --git a/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar b/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..7f93135c Binary files /dev/null and b/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar differ diff --git a/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties b/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..e2847c82 --- /dev/null +++ b/2025-09/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/2025-09/spring-34-kuber/gradlew b/2025-09/spring-34-kuber/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/2025-09/spring-34-kuber/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/2025-09/spring-34-kuber/gradlew.bat b/2025-09/spring-34-kuber/gradlew.bat new file mode 100755 index 00000000..93e3f59f --- /dev/null +++ b/2025-09/spring-34-kuber/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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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/2025-09/spring-34-kuber/kube/config.yaml b/2025-09/spring-34-kuber/kube/config.yaml new file mode 100755 index 00000000..01378922 --- /dev/null +++ b/2025-09/spring-34-kuber/kube/config.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rest-service-config +data: + JAVA_TOOL_OPTIONS: -XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80 \ No newline at end of file diff --git a/2025-09/spring-34-kuber/kube/deployment.yaml b/2025-09/spring-34-kuber/kube/deployment.yaml new file mode 100755 index 00000000..8bbb1d80 --- /dev/null +++ b/2025-09/spring-34-kuber/kube/deployment.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rest-hello-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: rest-hello + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: rest-hello + spec: + containers: + - image: registry.gitlab.com/petrelevich/dockerregistry/rest-hello:2.0.2-1.2d40be46.dirty-SNAPSHOT + name: rest-hello + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: rest-service-config + resources: + requests: + memory: "256M" + limits: + memory: "256M" + readinessProbe: + failureThreshold: 3 + httpGet: + path: /actuator/health/readiness + port: 8090 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /actuator/health/liveness + port: 8090 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + initialDelaySeconds: 10 + imagePullSecrets: + - name: regcred diff --git a/2025-09/spring-34-kuber/kube/ingress.yaml b/2025-09/spring-34-kuber/kube/ingress.yaml new file mode 100755 index 00000000..18e1dc48 --- /dev/null +++ b/2025-09/spring-34-kuber/kube/ingress.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rest-hello + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /appl(/|$)(.*) + pathType: Prefix + backend: + service: + name: rest-hello + port: + number: 80 diff --git a/2025-09/spring-34-kuber/kube/service.yaml b/2025-09/spring-34-kuber/kube/service.yaml new file mode 100755 index 00000000..21b1add5 --- /dev/null +++ b/2025-09/spring-34-kuber/kube/service.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: rest-hello +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: rest-hello + type: ClusterIP diff --git a/2025-09/spring-34-kuber/rest-hello/build.gradle.kts b/2025-09/spring-34-kuber/rest-hello/build.gradle.kts new file mode 100755 index 00000000..4c77e8c9 --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("java") + id("org.springframework.boot") + id("com.google.cloud.tools.jib") + id("fr.brouillard.oss.gradle.jgitver") +} + + +apply(plugin = "com.google.cloud.tools.jib") + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") +} + +tasks { + jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + to { + tags = setOf(project.version.toString()) + image = "registry.gitlab.com/petrelevich/dockerregistry/${project.name}" + auth { + username = System.getenv("GITLAB_USERNAME") + password = System.getenv("GITLAB_PASSWORD") + } + } + } +//docker login registry.gitlab.com +//docker run -p 8080:8080 registry.gitlab.com/petrelevich/dockerregistry/rest-hello + + build { + dependsOn(jib) + } +} \ No newline at end of file diff --git a/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java new file mode 100755 index 00000000..5abbd89c --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java @@ -0,0 +1,23 @@ +package ru.petrelevich; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class Application { + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + com.sun.management.OperatingSystemMXBean os = (com.sun.management.OperatingSystemMXBean) + java.lang.management.ManagementFactory.getOperatingSystemMXBean(); + + logger.info("availableProcessors:{}", Runtime.getRuntime().availableProcessors()); + logger.info("TotalMemorySize, mb:{}", os.getTotalMemorySize() / 1024 / 1024); + logger.info("maxMemory, mb:{}", Runtime.getRuntime().maxMemory() / 1024 / 1024); + logger.info("freeMemory, mb:{}", Runtime.getRuntime().freeMemory() / 1024 / 1024); + + new SpringApplicationBuilder().sources(Application.class).run(args); + } +} diff --git a/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java new file mode 100755 index 00000000..7c1d9701 --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java @@ -0,0 +1,25 @@ +package ru.petrelevich.controller; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IndexController { + + @GetMapping("/hi") + public String hi(@RequestParam(name = "name") String name) throws UnknownHostException { + return String.format( + "Hi, %s. It works, host: %s", name, InetAddress.getLocalHost().getHostName()); + } + + @PostMapping("/response/{name}") + public Response response(@PathVariable("name") String name, @RequestBody Request params) { + return new Response(name, String.format("%s-%s", params.param1(), params.param2())); + } +} diff --git a/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java new file mode 100755 index 00000000..4aba59c4 --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java @@ -0,0 +1,3 @@ +package ru.petrelevich.controller; + +public record Request(String param1, String param2) {} diff --git a/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java new file mode 100755 index 00000000..91b27a4f --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java @@ -0,0 +1,3 @@ +package ru.petrelevich.controller; + +public record Response(String name, String result) {} diff --git a/2025-09/spring-34-kuber/rest-hello/src/main/resources/application.yml b/2025-09/spring-34-kuber/rest-hello/src/main/resources/application.yml new file mode 100755 index 00000000..bb9154dc --- /dev/null +++ b/2025-09/spring-34-kuber/rest-hello/src/main/resources/application.yml @@ -0,0 +1,10 @@ +management: + server: + port: 8090 + endpoints: + enabled-by-default: false + endpoint: + health: + enabled: true + probes: + enabled: true \ No newline at end of file diff --git a/2025-09/spring-34-kuber/settings.gradle.kts b/2025-09/spring-34-kuber/settings.gradle.kts new file mode 100755 index 00000000..a89f82f3 --- /dev/null +++ b/2025-09/spring-34-kuber/settings.gradle.kts @@ -0,0 +1,20 @@ +rootProject.name = "spring-kuber" +include ("rest-hello") + +pluginManagement { + val jgitver: String by settings + val dependencyManagement: String by settings + val springframeworkBoot: String by settings + val jib: String by settings + val sonarlint: String by settings + val spotless: String by settings + + plugins { + id("fr.brouillard.oss.gradle.jgitver") version jgitver + id("io.spring.dependency-management") version dependencyManagement + id("org.springframework.boot") version springframeworkBoot + id("com.google.cloud.tools.jib") version jib + id("name.remal.sonarlint") version sonarlint + id("com.diffplug.spotless") version spotless + } +} \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/.gitignore b/2025-09/spring-39-kafka-webflux/.gitignore new file mode 100755 index 00000000..d8762a8d --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/.gitignore @@ -0,0 +1,35 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-09/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java b/2025-09/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-09/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties b/2025-09/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-09/spring-39-kafka-webflux/client/HttpRequests.http b/2025-09/spring-39-kafka-webflux/client/HttpRequests.http new file mode 100755 index 00000000..3432cafa --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/HttpRequests.http @@ -0,0 +1,11 @@ +### +GET http://localhost:8082/data-mono/16 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8082/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-09/spring-39-kafka-webflux/client/curlLoop.sh b/2025-09/spring-39-kafka-webflux/client/curlLoop.sh new file mode 100755 index 00000000..32d83a38 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/curlLoop.sh @@ -0,0 +1,5 @@ +date +for run in {1..100000}; do + curl -s "http://localhost:8082/data-mono/$run" > /dev/null +done +date \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/client/pom.xml b/2025-09/spring-39-kafka-webflux/client/pom.xml new file mode 100755 index 00000000..b5f78173 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + client + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + io.projectreactor.kafka + reactor-kafka + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.projectreactor + reactor-test + test + + + + com.github.tomakehurst + wiremock + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java new file mode 100755 index 00000000..341f9828 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ClientData { + + public static void main(String[] args) { + SpringApplication.run(ClientData.class, args); + } +} \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java new file mode 100755 index 00000000..1761ca74 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java @@ -0,0 +1,58 @@ +package com.datasrc; + + +import com.datasrc.config.ReactiveSender; +import com.datasrc.model.Request; +import com.datasrc.model.RequestId; +import com.datasrc.model.StreamData; +import com.datasrc.model.StringValue; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +// http://localhost:8082/data/5 +@RestController +public class ClientDataController { + private static final Logger log = LoggerFactory.getLogger(ClientDataController.class); + private final AtomicLong idGenerator = new AtomicLong(0); + + private final WebClient webClient; + private final ReactiveSender requestSender; + + private final StringValueStorage stringValueStorage; + + public ClientDataController(WebClient webClient, ReactiveSender requestSender, StringValueStorage stringValueStorage) { + this.webClient = webClient; + this.requestSender = requestSender; + this.stringValueStorage = stringValueStorage; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for data, seed:{}", seed); + + return webClient.get().uri(String.format("/data/%d", seed)) + .accept(MediaType.APPLICATION_NDJSON) + .retrieve() + .bodyToFlux(StreamData.class) + .doOnNext(val -> log.info("val:{}", val)); + } + + @GetMapping(value = "/data-mono/{seed}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono dataMono(@PathVariable("seed") long seed) { + log.info("request for string data-mono, seed:{}", seed); + var request = new Request(new RequestId(idGenerator.incrementAndGet()), seed); + + return requestSender.send(request, requestSend -> log.info("send ok: {}", requestSend)) + .flatMap(v -> stringValueStorage.get(new RequestId(v.correlationMetadata().id()))) + .doOnNext(stringValue -> log.info("value for client:{}", stringValue)); + } +} diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java new file mode 100755 index 00000000..8ffdd2a9 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java @@ -0,0 +1,47 @@ +package com.datasrc; + +import com.datasrc.model.RequestId; +import com.datasrc.model.StringValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.ConnectableFlux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.Sinks; +import reactor.util.annotation.NonNull; + +public class StringValueStorage implements Sinks.EmitFailureHandler { + + private final Sinks.Many sink; + private final ConnectableFlux sinkConnectable; + private static final Logger log = LoggerFactory.getLogger(StringValueStorage.class); + + public StringValueStorage() { + sink = Sinks.many().multicast().onBackpressureBuffer(); + sinkConnectable = sink.asFlux().publish(); + sinkConnectable.connect(); + } + + public void put(RequestId requestId, StringValue value) { + log.info("put. requestId:{}, value:{}", requestId, value); + sink.emitNext(new ResponseData(requestId, value), this); + } + + public Mono get(RequestId requestId) { + return Mono.from(sinkConnectable + .filter(responseData -> { + log.info("waiting:{}, fact:{}", requestId, responseData.requestId); + return responseData.requestId.id() == requestId.id(); + }) + .map(responseData -> responseData.stringValue)); + } + + + @Override + public boolean onEmitFailure(@NonNull SignalType signalType, @NonNull Sinks.EmitResult emitResult) { + return false; + } + + private record ResponseData(RequestId requestId, StringValue stringValue) { + } +} diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..70edb4aa --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,117 @@ +package com.datasrc.config; + +import com.datasrc.StringValueStorage; +import com.datasrc.model.Request; +import com.datasrc.model.Response; +import io.netty.channel.nio.NioEventLoopGroup; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ReactorResourceFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final int THREAD_POOL_SIZE = 4; + private static final int RESPONSE_RECEIVER_POOL_SIZE = 1; + private static final int KAFKA_POOL_SIZE = 1; + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name ="clientThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup clientThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "client-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactorResourceFactory reactorResourceFactory(@Qualifier("clientThreadEventLoop") NioEventLoopGroup clientThreadEventLoop) { + var resourceFactory = new org.springframework.http.client.ReactorResourceFactory(); + resourceFactory.setLoopResources(b -> clientThreadEventLoop); + resourceFactory.setUseGlobalResources(false); + return resourceFactory; + } + + @Bean + public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory resourceFactory) { + return new ReactorClientHttpConnector(resourceFactory, mapper -> mapper); + } + + @Bean("responseReceiverScheduler") + public Scheduler responseReceiverScheduler() { + return Schedulers.newParallel("response-receiver", RESPONSE_RECEIVER_POOL_SIZE); + } + + @Bean("kafkaScheduler") + public Scheduler kafkaScheduler() { + return Schedulers.newParallel("kafka-scheduler", KAFKA_POOL_SIZE); + } + + @Bean + public WebClient webClient(WebClient.Builder builder, + @Value("${application.processor.url}") String url) { + return builder + .baseUrl(url) + .build(); + } + + @Bean(destroyMethod = "close") + public ReactiveSender requestSender(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-request}") String topicRequest, + @Qualifier("kafkaScheduler") Scheduler kafkaScheduler + ) { + return new ReactiveSender<>(bootstrapServers, kafkaScheduler, topicRequest); + } + + @Bean + public StringValueStorage stringValueStorage() { + return new StringValueStorage(); + } + + @Bean(destroyMethod = "close") + public ReactiveReceiver responseReceiver(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-response}") String topicResponse, + @Value("${application.kafka-group-id}") String groupId, + @Qualifier("responseReceiverScheduler") Scheduler responseReceiverScheduler, + StringValueStorage stringValueStorage) { + return new ReactiveReceiver<>(bootstrapServers, Response.class, topicResponse, responseReceiverScheduler, groupId, + response -> stringValueStorage.put(response.data().requestId(), response.data())); + } + +} diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/resources/application.yml b/2025-09/spring-39-kafka-webflux/client/src/main/resources/application.yml new file mode 100755 index 00000000..29b56723 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8082 + +application: + processor: + url: http://localhost:8081 + kafka-bootstrap-servers: localhost:9092 + kafka-group-id: clientConsumerGroup + topic-request: request + topic-response: response diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/resources/logback.xml b/2025-09/spring-39-kafka-webflux/client/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/index.html b/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/index.html new file mode 100755 index 00000000..d42e4d95 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/index.html @@ -0,0 +1,16 @@ + + + + + + + + Stream Demo + + +

Stream Demo

+
+ + + + \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js b/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js new file mode 100755 index 00000000..9287c9b8 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js @@ -0,0 +1,24 @@ +const streamErr = e => { + console.warn("error"); + console.warn(e); +} + +fetch("http://localhost:8082/data/5").then((response) => { + return can.ndjsonStream(response.body); +}).then(dataStream => { + const reader = dataStream.getReader(); + const read = result => { + if (result.done) { + return; + } + render(result.value); + reader.read().then(read, streamErr); + } + reader.read().then(read, streamErr); +}); + +const render = value => { + const div = document.createElement('div'); + div.append(new Date() + ' stringValue:', JSON.stringify(value)); + document.getElementById('dataBlock').append(div); +}; \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/common/HttpRequests.http b/2025-09/spring-39-kafka-webflux/common/HttpRequests.http new file mode 100755 index 00000000..83afa6d0 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/HttpRequests.http @@ -0,0 +1,11 @@ +### +GET http://localhost:8082/data-mono/13 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8082/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-09/spring-39-kafka-webflux/common/curlLoop.sh b/2025-09/spring-39-kafka-webflux/common/curlLoop.sh new file mode 100755 index 00000000..32d83a38 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/curlLoop.sh @@ -0,0 +1,5 @@ +date +for run in {1..100000}; do + curl -s "http://localhost:8082/data-mono/$run" > /dev/null +done +date \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/common/pom.xml b/2025-09/spring-39-kafka-webflux/common/pom.xml new file mode 100755 index 00000000..0091a0e8 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + common + 1.0 + + + 21 + + + + + ch.qos.logback + logback-classic + provided + + + + io.projectreactor.kafka + reactor-kafka + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java new file mode 100755 index 00000000..1df2c495 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java @@ -0,0 +1,7 @@ +package com.datasrc.config; + +public class ConsumerException extends RuntimeException { + public ConsumerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java new file mode 100755 index 00000000..fa76ab49 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java @@ -0,0 +1,42 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Deserializer; + +public class JsonDeserializer implements Deserializer { + public static final String OBJECT_MAPPER = "objectMapper"; + public static final String TYPE_REFERENCE = "typeReference"; + private final String encoding = StandardCharsets.UTF_8.name(); + private ObjectMapper mapper; + private JavaType javaType; + + @Override + public void configure(Map configs, boolean isKey) { + mapper = (ObjectMapper) configs.get(OBJECT_MAPPER); + if (mapper == null) { + throw new IllegalArgumentException("config property OBJECT_MAPPER was not set"); + } + javaType = (JavaType) configs.get(TYPE_REFERENCE); + if (javaType == null) { + throw new IllegalArgumentException("config property TYPE_REFERENCE was not set"); + } + } + + @Override + public T deserialize(String topic, byte[] data) { + try { + if (data == null) { + return null; + } else { + var valueAsString = new String(data, encoding); + return mapper.readValue(valueAsString, javaType); + } + } catch (Exception e) { + throw new SerializationException("Error when deserializing byte[] to StringValue", e); + } + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java new file mode 100755 index 00000000..84b48d44 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java @@ -0,0 +1,34 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Serializer; + +public class JsonSerializer implements Serializer { + public static final String OBJECT_MAPPER = "objectMapper"; + private final String encoding = StandardCharsets.UTF_8.name(); + private ObjectMapper mapper; + + @Override + public void configure(Map configs, boolean isKey) { + mapper = (ObjectMapper) configs.get(OBJECT_MAPPER); + if (mapper == null) { + throw new IllegalArgumentException("config property OBJECT_MAPPER was not set"); + } + } + + @Override + public byte[] serialize(String topic, T data) { + try { + if (data == null) { + return new byte[]{}; + } else { + return mapper.writeValueAsString(data).getBytes(encoding); + } + } catch (Exception e) { + throw new SerializationException("Error when serializing StringValue to byte[] ", e); + } + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java new file mode 100755 index 00000000..02cd1d35 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java @@ -0,0 +1,127 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.InetAddress; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Properties; +import java.util.Random; +import java.util.concurrent.CancellationException; +import java.util.function.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.serialization.LongDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.scheduler.Scheduler; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.util.retry.Retry; + +import static com.datasrc.config.JsonDeserializer.OBJECT_MAPPER; +import static com.datasrc.config.JsonDeserializer.TYPE_REFERENCE; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.GROUP_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.GROUP_INSTANCE_ID_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_RECORDS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG; + +public class ReactiveReceiver { + private static final Logger log = LoggerFactory.getLogger(ReactiveReceiver.class); + private final Random random = new Random(); + + private final Disposable kafkaSubscriber; + private final Disposable kafkaConnection; + + public static final int MAX_POLL_INTERVAL_MS = 1_000; + + public ReactiveReceiver( + String bootstrapServers, Class valueClass, String topicName, Scheduler schedulerValueReceiver, String groupId, Consumer valueConsumer + ) { + Properties props = new Properties(); + props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(GROUP_ID_CONFIG, groupId); + props.put(GROUP_INSTANCE_ID_CONFIG, makeGroupInstanceIdConfig(groupId)); + props.put(ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(AUTO_COMMIT_INTERVAL_MS_CONFIG, "100"); + props.put(AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(KEY_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); + props.put(VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); + + var objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + props.put(OBJECT_MAPPER, objectMapper); + props.put(TYPE_REFERENCE, objectMapper.getTypeFactory().constructType(valueClass)); + + props.put(MAX_POLL_RECORDS_CONFIG, 3); + props.put(MAX_POLL_INTERVAL_MS_CONFIG, MAX_POLL_INTERVAL_MS); + + ReceiverOptions receiverOptions = + ReceiverOptions.create(props) + .pollTimeout(Duration.ofSeconds(500)) + .schedulerSupplier(() -> schedulerValueReceiver) + .subscription(Collections.singleton(topicName)); + + Flux> inboundFlux = KafkaReceiver.create(receiverOptions) + .receiveAutoAck() + .concatMap( + consumerRecordFlux -> { + log.info("consumerRecordFlux done, commit"); + return consumerRecordFlux; + }) + .retryWhen(Retry.backoff(3, Duration.of(5, ChronoUnit.SECONDS))); + + Hooks.onErrorDropped( + error -> { + if (error instanceof CancellationException) { + log.info("Cancellation event:", error); + } else { + log.error("error:", error); + } + }); + + var kafkaFlow = inboundFlux.doOnCancel(() -> log.info("connection canceled")) + .doOnError(error -> log.error("Consuming error", error)) + .publish(); + + log.info("start consuming"); + kafkaConnection = kafkaFlow.connect(); + kafkaSubscriber = + kafkaFlow.subscribe( + receiverRecord -> { + var key = receiverRecord.key(); + var value = receiverRecord.value(); + log.info("key:{}, value:{}, record:{}", key, value, receiverRecord); + valueConsumer.accept(value); + }); + } + + public void close() { + log.info("stop consuming"); + kafkaSubscriber.dispose(); + kafkaConnection.dispose(); + } + + private String makeGroupInstanceIdConfig(String groupId) { + try { + var hostName = InetAddress.getLocalHost().getHostName(); + return String.join( + "-", + groupId, + hostName, + String.valueOf(random.nextInt(100_999_999))); + } catch (Exception ex) { + throw new ConsumerException("can't make GroupInstanceIdConfig", ex); + } + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java new file mode 100755 index 00000000..362708dd --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java @@ -0,0 +1,92 @@ +package com.datasrc.config; + +import static com.datasrc.config.JsonSerializer.OBJECT_MAPPER; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.RETRIES_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BUFFER_MEMORY_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.MAX_BLOCK_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; + +import com.datasrc.model.DataForSending; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Properties; +import java.util.function.Consumer; +import org.apache.kafka.common.serialization.LongSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.SenderRecord; +import reactor.kafka.sender.SenderResult; + +public class ReactiveSender> { + private static final Logger log = LoggerFactory.getLogger(ReactiveSender.class); + private final KafkaSender sender; + private final String topicName; + + public ReactiveSender(String bootstrapServers, Scheduler schedulerKafka, String topicName) { + this.topicName = topicName; + var props = new Properties(); + props.put(CLIENT_ID_CONFIG, "KafkaReactiveSender"); + props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ACKS_CONFIG, "1"); + props.put(RETRIES_CONFIG, 1); + props.put(BATCH_SIZE_CONFIG, 16384); + props.put(LINGER_MS_CONFIG, 10); + props.put(BUFFER_MEMORY_CONFIG, 26384); // bytes + props.put(MAX_BLOCK_MS_CONFIG, 1_000); // ms + props.put(KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class); + props.put(VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + + var objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + props.put(OBJECT_MAPPER, objectMapper); + + SenderOptions senderOptions = + SenderOptions.create(props) + .maxInFlight(10) + .scheduler(schedulerKafka); + + sender = KafkaSender.create(senderOptions); + + var shutdownHook = + new Thread( + () -> { + log.info("closing kafka sender"); + sender.close(); + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + public Mono> send(T data, Consumer sendAsk) { + return sender.send(Mono.just(SenderRecord.create( + topicName, + null, + null, + data.id(), + data, + data))) + .doOnError(error -> log.error("Send failed", error)) + .doOnNext( + senderResult -> { + log.info( + "message id:{} was sent, offset:{}", + senderResult.correlationMetadata().id(), + senderResult.recordMetadata().offset()); + sendAsk.accept(senderResult.correlationMetadata()); + }).next(); + } + + public void close() { + sender.close(); + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java new file mode 100755 index 00000000..6aa91026 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java @@ -0,0 +1,7 @@ +package com.datasrc.model; + +public interface DataForSending { + long id(); + + T data(); +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java new file mode 100755 index 00000000..00623e66 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java @@ -0,0 +1,34 @@ +package com.datasrc.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Request implements DataForSending { + + private final RequestId id; + private final long seed; + + @JsonCreator + public Request(@JsonProperty("id") RequestId id, @JsonProperty("seed") long seed) { + this.id = id; + this.seed = seed; + } + + @Override + public long id() { + return id.id(); + } + + @Override + public Long data() { + return seed; + } + + @Override + public String toString() { + return "Request{" + + "id=" + id + + ", seed=" + seed + + '}'; + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java new file mode 100755 index 00000000..5a602848 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record RequestId(long id) { +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java new file mode 100755 index 00000000..a224a08d --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java @@ -0,0 +1,33 @@ +package com.datasrc.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Response implements DataForSending { + private final ResponseId id; + private final StringValue stringValue; + + @JsonCreator + public Response(@JsonProperty("id") ResponseId id, @JsonProperty("stringValue") StringValue stringValue) { + this.id = id; + this.stringValue = stringValue; + } + + @Override + public long id() { + return id.id(); + } + + @Override + public StringValue data() { + return stringValue; + } + + @Override + public String toString() { + return "Response{" + + "id=" + id + + ", stringValue=" + stringValue + + '}'; + } +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java new file mode 100755 index 00000000..bf7d7d73 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record ResponseId(long id) { +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java new file mode 100755 index 00000000..cc199ad4 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record StreamData(String value) { +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java new file mode 100755 index 00000000..43568176 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record StringValue(RequestId requestId, String value) { +} diff --git a/2025-09/spring-39-kafka-webflux/common/src/main/resources/logback.xml b/2025-09/spring-39-kafka-webflux/common/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/common/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-09/spring-39-kafka-webflux/docker/docker-compose.yml b/2025-09/spring-39-kafka-webflux/docker/docker-compose.yml new file mode 100755 index 00000000..c264d84e --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/docker/docker-compose.yml @@ -0,0 +1,23 @@ +services: + zookeeper: + image: confluentinc/cp-zookeeper:6.2.0 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + broker: + image: confluentinc/cp-kafka:7.0.0 + container_name: broker + ports: + - "9092:9092" + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/mvnw b/2025-09/spring-39-kafka-webflux/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-09/spring-39-kafka-webflux/mvnw.cmd b/2025-09/spring-39-kafka-webflux/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-09/spring-39-kafka-webflux/pom.xml b/2025-09/spring-39-kafka-webflux/pom.xml new file mode 100755 index 00000000..e24e7f10 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + ru.otus + spring-39-kafka-webflux + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.3 + + + pom + + + client + processor + source + common + + + + UTF-8 + 21 + 21 + 3.0.0-M3 + 3.1.1 + 3.3.9 + 1.0.12.RELEASE + 3.4.3 + 3.0.2 + 3.0.0-beta-10 + 1.3.23 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springframeworkBoot.version} + pom + import + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + + io.projectreactor.kafka + reactor-kafka + ${reactorKafka.version} + + + com.github.tomakehurst + wiremock + ${wiremock.version} + + + + + + + + + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-maven + + enforce + + + + + + ${minimal.maven.version} + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + + + diff --git a/2025-09/spring-39-kafka-webflux/processor/pom.xml b/2025-09/spring-39-kafka-webflux/processor/pom.xml new file mode 100755 index 00000000..d266d2df --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + processor + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + io.projectreactor.kafka + reactor-kafka + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java new file mode 100755 index 00000000..6d911292 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ProcessorData { + + public static void main(String[] args) { + SpringApplication.run(ProcessorData.class, args); + } +} \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java new file mode 100755 index 00000000..0b89514f --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java @@ -0,0 +1,40 @@ +package com.datasrc; + + +import com.datasrc.model.StreamData; +import com.datasrc.processor.DataProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +@RestController +public class ProcessorDataController { + private static final Logger log = LoggerFactory.getLogger(ProcessorDataController.class); + + private final DataProcessor> dataProcessorStringReactorFlux; + private final WebClient client; + + public ProcessorDataController(WebClient client, + @Qualifier("dataProcessorFlux") DataProcessor> dataProcessorFlux) { + this.dataProcessorStringReactorFlux = dataProcessorFlux; + this.client = client; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for data, seed:{}", seed); + + var srcRequest = client.get().uri(String.format("/data/%d", seed)) + .accept(MediaType.APPLICATION_NDJSON) + .retrieve() + .bodyToFlux(StreamData.class); + + return dataProcessorStringReactorFlux.process(srcRequest); + } +} diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..d091bfc7 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,136 @@ +package com.datasrc.config; + +import com.datasrc.model.Request; +import com.datasrc.model.RequestId; +import com.datasrc.model.Response; +import com.datasrc.model.ResponseId; +import com.datasrc.model.StringValue; +import com.datasrc.processor.DataProcessor; +import io.netty.channel.nio.NioEventLoopGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.client.ReactorResourceFactory; + + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final Logger log = LoggerFactory.getLogger(ApplConfig.class); + private static final int THREAD_POOL_SIZE = 2; + private static final int REQUEST_RECEIVER_POOL_SIZE = 1; + private static final int KAFKA_POOL_SIZE = 1; + + private final AtomicLong responseIdGenerator = new AtomicLong(0); + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name ="clientThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup clientThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "client-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactorResourceFactory reactorResourceFactory(@Qualifier("clientThreadEventLoop") NioEventLoopGroup clientThreadEventLoop) { + var resourceFactory = new ReactorResourceFactory(); + resourceFactory.setLoopResources(b -> clientThreadEventLoop); + resourceFactory.setUseGlobalResources(false); + return resourceFactory; + } + + @Bean + public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory resourceFactory) { + return new ReactorClientHttpConnector(resourceFactory, mapper -> mapper); + } + + @Bean + public Scheduler timer() { + return Schedulers.newParallel("processor-thread", 2); + } + + + @Bean("requestReceiverScheduler") + public Scheduler requestReceiverScheduler() { + return Schedulers.newParallel("request-receiver", REQUEST_RECEIVER_POOL_SIZE); + } + + @Bean("kafkaScheduler") + public Scheduler kafkaScheduler() { + return Schedulers.newParallel("kafka-scheduler", KAFKA_POOL_SIZE); + } + + @Bean + public WebClient webClient(WebClient.Builder builder, + @Value("${application.source.url}") String url) { + return builder + .baseUrl(url) + .build(); + } + + @Bean(destroyMethod = "close") + public ReactiveSender responseSender(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-response}") String topicResponse, + @Qualifier("kafkaScheduler") Scheduler kafkaScheduler + ) { + return new ReactiveSender<>(bootstrapServers, kafkaScheduler, topicResponse); + } + + @Bean(destroyMethod = "close") + public ReactiveReceiver requestReceiver(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-request}") String topicRequest, + @Value("${application.kafka-group-id}") String groupId, + @Qualifier("requestReceiverScheduler") Scheduler responseReceiverScheduler, + ReactiveSender responseSender, + @Qualifier("dataProcessorMono") DataProcessor dataProcessor, + WebClient webClient) { + + return new ReactiveReceiver<>(bootstrapServers, Request.class, topicRequest, responseReceiverScheduler, groupId, + request -> webClient.get().uri(String.format("/data-mono/%d", request.data())) + .retrieve() + .bodyToMono(String.class) + .map(dataProcessor::process) + .flatMap(stringValue -> + responseSender.send(new Response(new ResponseId(responseIdGenerator.incrementAndGet()), + new StringValue(new RequestId(request.id()), stringValue)), + stringValueDataForSending -> log.info("response send:{}", stringValueDataForSending))) + .subscribe()); + } +} diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java new file mode 100755 index 00000000..55b9c4cd --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java @@ -0,0 +1,6 @@ +package com.datasrc.processor; + +public interface DataProcessor { + + T process(T data); +} diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java new file mode 100755 index 00000000..c0a5155a --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java @@ -0,0 +1,33 @@ +package com.datasrc.processor; + +import com.datasrc.model.StreamData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; + +import java.time.Duration; + +@Service("dataProcessorFlux") +public class DataProcessorStringReactorFlux implements DataProcessor> { + private static final Logger log = LoggerFactory.getLogger(DataProcessorStringReactorFlux.class); + private final Scheduler timer; + + public DataProcessorStringReactorFlux(Scheduler timer) { + this.timer = timer; + } + + @Override + public Flux process(Flux dataflow) { + log.info("processor"); + var dataSeq = dataflow + .doOnNext(val -> log.info("in val:{}", val)) + .delayElements(Duration.ofSeconds(5), timer) + .map(data -> new StreamData(data.value().toUpperCase())) + .doOnNext(val -> log.info("out val:{}", val)); + + log.info("processor method finished"); + return dataSeq; + } +} diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java new file mode 100755 index 00000000..f102c368 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java @@ -0,0 +1,16 @@ +package com.datasrc.processor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service("dataProcessorMono") +public class DataProcessorStringReactorMono implements DataProcessor { + private static final Logger log = LoggerFactory.getLogger(DataProcessorStringReactorMono.class); + + @Override + public String process(String value) { + log.info("processor"); + return value.toUpperCase(); + } +} diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/resources/application.yml b/2025-09/spring-39-kafka-webflux/processor/src/main/resources/application.yml new file mode 100755 index 00000000..ffa5de55 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8081 + +application: + source: + url: http://localhost:8080 + kafka-bootstrap-servers: localhost:9092 + kafka-group-id: processorConsumerGroup + topic-request: request + topic-response: response diff --git a/2025-09/spring-39-kafka-webflux/processor/src/main/resources/logback.xml b/2025-09/spring-39-kafka-webflux/processor/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/processor/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-09/spring-39-kafka-webflux/source/HttpRequests.http b/2025-09/spring-39-kafka-webflux/source/HttpRequests.http new file mode 100755 index 00000000..be988df3 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/HttpRequests.http @@ -0,0 +1,12 @@ +### +GET http://localhost:8080/data-mono/13 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + + +### +GET http://localhost:8080/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/source/pom.xml b/2025-09/spring-39-kafka-webflux/source/pom.xml new file mode 100755 index 00000000..9e828243 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + source + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java new file mode 100755 index 00000000..a321fbde --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SourceData { + + public static void main(String[] args) { + SpringApplication.run(SourceData.class, args); + } +} \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java new file mode 100755 index 00000000..feb4291a --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java @@ -0,0 +1,54 @@ +package com.datasrc; + + +import com.datasrc.model.StreamData; +import com.datasrc.producer.DataProducer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +public class SourceDataController { + private static final Logger log = LoggerFactory.getLogger(SourceDataController.class); + + private final DataProducer> dataProducerFlux; + private final DataProducer dataProducerStringBlocked; + + private final Executor blockingExecutor; + + public SourceDataController(@Qualifier("dataProducerFlux") DataProducer> dataProducerFlux, + @Qualifier("dataProducerStringBlocked") DataProducer dataProducerStringBlocked, + @Qualifier("blockingExecutor") Executor blockingExecutor) { + this.dataProducerFlux = dataProducerFlux; + this.dataProducerStringBlocked = dataProducerStringBlocked; + this.blockingExecutor = blockingExecutor; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for string data, seed:{}", seed); + var stringData = dataProducerFlux.produce(seed); + + log.info("Method request for string data done"); + return stringData; + } + + @GetMapping(value = "/data-mono/{seed}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono dataMono(@PathVariable("seed") long seed) { + log.info("request for string data-mono, seed:{}", seed); + + var future = CompletableFuture + .supplyAsync(() -> dataProducerStringBlocked.produce(seed), blockingExecutor); + var mono = Mono.fromFuture(future); + log.info("Method request for string data done"); + return mono; + } +} diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..7186ef9a --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,56 @@ +package com.datasrc.config; + +import io.netty.channel.nio.NioEventLoopGroup; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final int THREAD_POOL_SIZE = 2; + private static final int BLOCKING_THREAD_POOL_SIZE = 2; + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name= "blockingExecutor", destroyMethod = "close") + public ExecutorService blockingExecutor() { + var id = new AtomicLong(0); + return Executors.newFixedThreadPool(BLOCKING_THREAD_POOL_SIZE, + task -> new Thread(task, String.format("blocking-thread-%d", id.incrementAndGet()))); + } + + @Bean + public Scheduler timer() { + return Schedulers.newParallel("processor-thread", 2); + } +} diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java new file mode 100755 index 00000000..e205bb5e --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java @@ -0,0 +1,6 @@ +package com.datasrc.producer; + +public interface DataProducer { + + T produce(long seed); +} diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java new file mode 100755 index 00000000..d9cf933b --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java @@ -0,0 +1,26 @@ +package com.datasrc.producer; + +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service("dataProducerStringBlocked") +public class DataProducerStringBlocked implements DataProducer { + private static final Logger log = LoggerFactory.getLogger(DataProducerStringBlocked.class); + + @Override + public String produce(long seed) { + log.info("produce using seed:{}", seed); + sleep(); + return String.format("someDataStr:%s", seed); + } + + private void sleep() { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(10)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java new file mode 100755 index 00000000..b8250eda --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java @@ -0,0 +1,39 @@ +package com.datasrc.producer; + +import com.datasrc.model.StreamData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; +import java.time.Duration; +import java.util.function.BiFunction; +import reactor.core.scheduler.Scheduler; + +@Service("dataProducerFlux") +public class DataProducerStringFlux implements DataProducer> { + private static final Logger log = LoggerFactory.getLogger(DataProducerStringFlux.class); + private final Scheduler timer; + + public DataProducerStringFlux(Scheduler timer) { + this.timer = timer; + } + + @Override + public Flux produce(long seed) { + log.info("produce using seed:{}", seed); + var stringSeed = "someDataStr:"; + var dataSeq = Flux.generate(() -> seed, + (BiFunction, Long>) (prev, sink) -> { + var newValue = prev + 1; + sink.next(newValue); + return newValue; + }) + .delayElements(Duration.ofSeconds(3), timer) + .map(val -> new StreamData(stringSeed + val)) + .doOnNext(val -> log.info("val:{}", val)); + + log.info("produce method finished"); + return dataSeq; + } +} diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/resources/application.yml b/2025-09/spring-39-kafka-webflux/source/src/main/resources/application.yml new file mode 100755 index 00000000..47fbb02d --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 8080 \ No newline at end of file diff --git a/2025-09/spring-39-kafka-webflux/source/src/main/resources/logback.xml b/2025-09/spring-39-kafka-webflux/source/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-09/spring-39-kafka-webflux/source/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-11/spring-23-ACL/ACL/pom.xml b/2025-11/spring-23-ACL/ACL/pom.xml new file mode 100644 index 00000000..2ba139f5 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + ru.otus + spring-framework-26-acl + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.10 + + + + 2.6.11 + 3.0.0 + UTF-8 + UTF-8 + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-acl + + + net.sf.ehcache + ehcache-core + ${ehcache-core.version} + jar + + + org.springframework + spring-context-support + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + + + + org.springdoc + springdoc-openapi-ui + 1.6.9 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..baca49d5 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,14 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java new file mode 100644 index 00000000..e0363d94 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java @@ -0,0 +1,30 @@ +package ru.otus.spring.model; + +import javax.persistence.*; + +@Entity +@Table(name = "system_message") +public class NoticeMessage { + + @Id + @Column + private Integer id; + @Column + private String content; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java new file mode 100644 index 00000000..501d91a2 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java @@ -0,0 +1,21 @@ +package ru.otus.spring.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import ru.otus.spring.model.NoticeMessage; + +import java.util.List; +import java.util.Optional; + +public interface NoticeMessageRepository extends JpaRepository { + + List findAll(); + + Optional findById(Integer id); + + NoticeMessage save(NoticeMessage noticeMessage); + +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java new file mode 100644 index 00000000..01bb4154 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java @@ -0,0 +1,39 @@ +package ru.otus.spring.rest; + +import org.springframework.web.bind.annotation.*; +import ru.otus.spring.model.NoticeMessage; +import ru.otus.spring.repository.NoticeMessageRepository; +import ru.otus.spring.service.NoticeService; + +import java.util.List; + +@RestController +public class NoticeMessageController { + + private final NoticeService noticeService; + + public NoticeMessageController(NoticeService noticeService) { + this.noticeService = noticeService; + } + + @GetMapping("/message") + public List getAll() { + return noticeService.getAll(); + } + + @GetMapping("/message/{id}") + public NoticeMessage get(@PathVariable("id") Integer id) { + var result = noticeService.get( id ); + return result; + } + + @PostMapping("/message") + public NoticeMessage createMessage(@RequestBody NoticeMessage message) { + return noticeService.create(message); + } + + @PutMapping("/message/{id}") + public NoticeMessage updateMessage(@PathVariable("id") Integer id, @RequestBody NoticeMessage message) { + return noticeService.update(message); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java new file mode 100644 index 00000000..d261e2a7 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java @@ -0,0 +1,79 @@ +package ru.otus.spring.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.ehcache.EhCacheFactoryBean; +import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.acls.AclPermissionCacheOptimizer; +import org.springframework.security.acls.AclPermissionEvaluator; +import org.springframework.security.acls.domain.*; +import org.springframework.security.acls.jdbc.BasicLookupStrategy; +import org.springframework.security.acls.jdbc.JdbcMutableAclService; +import org.springframework.security.acls.jdbc.LookupStrategy; +import org.springframework.security.acls.model.PermissionGrantingStrategy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import javax.sql.DataSource; +import java.util.Objects; + +@Configuration +public class AclConfig { + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + private DataSource dataSource; + + @Bean + public EhCacheBasedAclCache aclCache() { + return new EhCacheBasedAclCache( + Objects.requireNonNull(aclEhCacheFactoryBean().getObject()), + permissionGrantingStrategy(), + aclAuthorizationStrategy() + ); + } + + @Bean + public EhCacheFactoryBean aclEhCacheFactoryBean() { + EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); + ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); + ehCacheFactoryBean.setCacheName("aclCache"); + return ehCacheFactoryBean; + } + + @Bean + public EhCacheManagerFactoryBean aclCacheManager() { + return new EhCacheManagerFactoryBean(); + } + + @Bean + public PermissionGrantingStrategy permissionGrantingStrategy() { + return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()); + } + + @Bean + public AclAuthorizationStrategy aclAuthorizationStrategy() { + return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_EDITOR")); + } + + @Bean + public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { + AclMethodSecurityExpressionHandler expressionHandler = new AclMethodSecurityExpressionHandler(); + AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService()); + expressionHandler.setPermissionEvaluator(permissionEvaluator); + expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService())); + return expressionHandler; + } + + @Bean + public LookupStrategy lookupStrategy() { + return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()); + } + + @Bean + public JdbcMutableAclService aclService() { + return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache()); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java new file mode 100644 index 00000000..5fb8a778 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java @@ -0,0 +1,29 @@ +package ru.otus.spring.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.acls.model.AclService; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { + + private final AclService aclService; + + public AclMethodSecurityConfiguration(AclService aclService) { + this.aclService = aclService; + } + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; + + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + return defaultMethodSecurityExpressionHandler; + } + +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java new file mode 100644 index 00000000..ef30e5f3 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java @@ -0,0 +1,23 @@ +package ru.otus.spring.security; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.core.Authentication; + +public class AclMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { + + @Override + protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, + MethodInvocation invocation) { + + AclMethodSecurityExpressionRoot root = new AclMethodSecurityExpressionRoot(authentication); + root.setThis(invocation.getThis()); + root.setPermissionEvaluator(this.getPermissionEvaluator()); + root.setTrustResolver(this.getTrustResolver()); + root.setRoleHierarchy(this.getRoleHierarchy()); + root.setDefaultRolePrefix(this.getDefaultRolePrefix()); + + return root; + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java new file mode 100644 index 00000000..f7885ab0 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java @@ -0,0 +1,12 @@ +package ru.otus.spring.security; + +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; + +public interface AclMethodSecurityExpressionOperations extends MethodSecurityExpressionOperations { + + boolean isAdministrator(Object targetId, Class targetClass); + + boolean isAdministrator(Object target); + + boolean canRead(Object targetId, Class targetClass); +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java new file mode 100644 index 00000000..73cf1068 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java @@ -0,0 +1,69 @@ +package ru.otus.spring.security; + +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.core.Authentication; + +public class AclMethodSecurityExpressionRoot extends SecurityExpressionRoot implements AclMethodSecurityExpressionOperations { + + private Object filterObject; + private Object returnObject; + private Object target; + + public AclMethodSecurityExpressionRoot(Authentication authentication) { + super(authentication); + } + + void setThis(Object target) { + this.target = target; + } + + @Override + public Object getFilterObject() { + return filterObject; + } + + @Override + public void setFilterObject(Object filterObject) { + this.filterObject = filterObject; + } + + @Override + public Object getReturnObject() { + return returnObject; + } + + @Override + public void setReturnObject(Object returnObject) { + this.returnObject = returnObject; + } + + @Override + public Object getThis() { + return this.target; + } + + @Override + public boolean isAdministrator(Object targetId, Class targetClass) { + + return isGranted(targetId, targetClass, admin); + } + + @Override + public boolean isAdministrator(Object target) { + + return hasPermission(target, admin); + } + + @Override + public boolean canRead(Object targetId, Class targetClass) { + + if(isAdministrator(targetId, targetClass)) return true; + + return isGranted(targetId, targetClass, read); + } + + boolean isGranted(Object targetId, Class targetClass, Object permission) { + + return hasPermission(targetId, targetClass.getCanonicalName(), permission); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..0919dad9 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,42 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.ArrayList; + +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf().disable() + .authorizeHttpRequests( ( authorize ) -> authorize + .antMatchers( "/**", "/" ).permitAll() + ) + .httpBasic(); + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + var users = new ArrayList(); + users.add( User + .withDefaultPasswordEncoder().username("admin").password("password").roles("USER") + .build() ); + users.add( User + .withDefaultPasswordEncoder().username("user").password("password").roles("USER") + .build() ); + users.add( User + .withDefaultPasswordEncoder().username("someone").password("password").roles("EDITOR") + .build() ); + return new InMemoryUserDetailsManager( users ); + + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java new file mode 100644 index 00000000..33783ff5 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java @@ -0,0 +1,9 @@ +package ru.otus.spring.service; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.model.Permission; + +public interface AclServiceWrapperService { + + void createPermission(Object object, Permission permission); +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java new file mode 100644 index 00000000..d5fdcd45 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java @@ -0,0 +1,34 @@ +package ru.otus.spring.service; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class AclServiceWrapperServiceImpl implements AclServiceWrapperService { + + private final MutableAclService mutableAclService; + + public AclServiceWrapperServiceImpl(MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + } + + @Override + public void createPermission(Object object, Permission permission) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final Sid owner = new PrincipalSid(authentication); + ObjectIdentity oid = new ObjectIdentityImpl(object); + + final Sid admin = new GrantedAuthoritySid("ROLE_EDITOR"); + + MutableAcl acl = mutableAclService.createAcl(oid); + acl.insertAce(acl.getEntries().size(), permission, owner, true); + acl.insertAce(acl.getEntries().size(), permission, admin, true); + mutableAclService.updateAcl(acl); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java new file mode 100644 index 00000000..efb6a0b2 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java @@ -0,0 +1,16 @@ +package ru.otus.spring.service; + +import ru.otus.spring.model.NoticeMessage; + +import java.util.List; + +public interface NoticeService { + + NoticeMessage create(NoticeMessage message); + + NoticeMessage get(Integer id); + + List getAll(); + + NoticeMessage update(NoticeMessage message); +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java new file mode 100644 index 00000000..e5b43913 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java @@ -0,0 +1,60 @@ +package ru.otus.spring.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.MutableAcl; +import org.springframework.security.acls.model.MutableAclService; +import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.Sid; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.spring.model.NoticeMessage; +import ru.otus.spring.repository.NoticeMessageRepository; + +import java.util.List; + +@Service +public class NoticeServiceImpl implements NoticeService { + + private final AclServiceWrapperService aclServiceWrapperService; + + private final NoticeMessageRepository repository; + + public NoticeServiceImpl(AclServiceWrapperService aclServiceWrapperService, NoticeMessageRepository repository) { + this.aclServiceWrapperService = aclServiceWrapperService; + this.repository = repository; + } + + @Override + @Transactional + public NoticeMessage create(NoticeMessage message) { + NoticeMessage savedMessage = repository.save(message); + aclServiceWrapperService.createPermission(savedMessage, BasePermission.READ); + return savedMessage; + } + + @Override + @PostFilter("hasPermission(filterObject, 'READ')") + public List getAll() { + return repository.findAll(); + } + + @Override + @PreAuthorize("hasPermission(#message, 'WRITE')") + public NoticeMessage update(NoticeMessage message) { + return repository.save(message); + } + + @Override + @PreAuthorize("canRead(#id, T(ru.otus.spring.model.NoticeMessage))") + public NoticeMessage get(Integer id) { + return repository.findById(id).get(); + } +} diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/application.yml b/2025-11/spring-23-ACL/ACL/src/main/resources/application.yml new file mode 100644 index 00000000..02f0a284 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + h2: + console: + enabled: true + jpa: + hibernate: + ddl-auto: none + datasource: + url: jdbc:h2:mem:testdb + +springdoc: + packages-to-scan: ru.otus.spring.rest + paths-to-match: /** \ No newline at end of file diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/data.sql b/2025-11/spring-23-ACL/ACL/src/main/resources/data.sql new file mode 100644 index 00000000..9ae11203 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/data.sql @@ -0,0 +1,28 @@ +INSERT INTO system_message(id,content) VALUES +(1,'First Level Message'), +(2,'Second Level Message'), +(3,'Third Level Message'); + + +INSERT INTO acl_sid (id, principal, sid) VALUES +(1, 1, 'admin'), +(2, 1, 'user'), +(3, 0, 'ROLE_EDITOR'); + +INSERT INTO acl_class (id, class) VALUES +(1, 'ru.otus.spring.model.NoticeMessage'); + +INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES +(1, 1, 1, NULL, 3, 0), +(2, 1, 2, NULL, 3, 0), +(3, 1, 3, NULL, 3, 0); + +INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, + granting, audit_success, audit_failure) VALUES +(1, 1, 1, 1, 1, 1, 1, 1), +(2, 1, 2, 1, 2, 1, 1, 1), +(3, 1, 3, 3, 1, 1, 1, 1), +(4, 2, 1, 2, 1, 1, 1, 1), +(5, 2, 2, 3, 1, 1, 1, 1), +(6, 3, 1, 3, 1, 1, 1, 1), +(7, 3, 2, 3, 2, 1, 1, 1); diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/postman_collection.json b/2025-11/spring-23-ACL/ACL/src/main/resources/postman_collection.json new file mode 100644 index 00000000..50f5f187 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/postman_collection.json @@ -0,0 +1,88 @@ +{ + "info": { + "_postman_id": "00ffe5e9-877c-418a-b088-4409f34756e9", + "name": "New Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "46752532", + "_collection_link": "https://uladzimirmaherau.postman.co/workspace/Uladzimir-Maherau's-Workspace~acb6d39e-e608-4ada-8982-80dabd38178f/collection/46752532-00ffe5e9-877c-418a-b088-4409f34756e9?action=share&source=collection_link&creator=46752532" + }, + "item": [ + { + "name": "http://localhost:8080/message", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "password", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": " {\r\n \"id\": 11,\r\n \"content\": \"11 Level Message\"\r\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/message", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "message" + ] + } + }, + "response": [] + }, + { + "name": "http://localhost:8080/message", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "password", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/message", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "message" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/schema.json b/2025-11/spring-23-ACL/ACL/src/main/resources/schema.json new file mode 100644 index 00000000..f3c13aa2 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/schema.json @@ -0,0 +1,113 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/message": { + "get": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getAll", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + }, + "put": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getById", + "parameters": [ + { + "name": "message", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + }, + "/message/{id}": { + "get": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getById_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NoticeMessage": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/schema.sql b/2025-11/spring-23-ACL/ACL/src/main/resources/schema.sql new file mode 100644 index 00000000..9f740482 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/schema.sql @@ -0,0 +1,58 @@ +create table IF NOT EXISTS system_message (id integer not null, content varchar(255), primary key (id)); + +CREATE TABLE IF NOT EXISTS acl_sid ( + id bigint(20) NOT NULL AUTO_INCREMENT, + principal tinyint(1) NOT NULL, + sid varchar(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_1 (sid,principal) +); + +CREATE TABLE IF NOT EXISTS acl_class ( + id bigint(20) NOT NULL AUTO_INCREMENT, + class varchar(255) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_2 (class) +); + +CREATE TABLE IF NOT EXISTS acl_entry ( + id bigint(20) NOT NULL AUTO_INCREMENT, + acl_object_identity bigint(20) NOT NULL, + ace_order int(11) NOT NULL, + sid bigint(20) NOT NULL, + mask int(11) NOT NULL, + granting tinyint(1) NOT NULL, + audit_success tinyint(1) NOT NULL, + audit_failure tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_4 (acl_object_identity,ace_order) +); + +CREATE TABLE IF NOT EXISTS acl_object_identity ( + id bigint(20) NOT NULL AUTO_INCREMENT, + object_id_class bigint(20) NOT NULL, + object_id_identity bigint(20) NOT NULL, + parent_object bigint(20) DEFAULT NULL, + owner_sid bigint(20) DEFAULT NULL, + entries_inheriting tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_3 (object_id_class,object_id_identity) +); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (sid) REFERENCES acl_sid(id); + +-- +-- Constraints for table acl_object_identity +-- +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (object_id_class) REFERENCES acl_class (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (owner_sid) REFERENCES acl_sid (id); \ No newline at end of file diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/templates/error.html b/2025-11/spring-23-ACL/ACL/src/main/resources/templates/error.html new file mode 100644 index 00000000..f28b51df --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/templates/error.html @@ -0,0 +1,9 @@ + + + + + + +Вам доступ запрещён! + + diff --git a/2025-11/spring-23-ACL/ACL/src/main/resources/templates/index.html b/2025-11/spring-23-ACL/ACL/src/main/resources/templates/index.html new file mode 100644 index 00000000..79347f42 --- /dev/null +++ b/2025-11/spring-23-ACL/ACL/src/main/resources/templates/index.html @@ -0,0 +1,15 @@ + + + + + + +login +
+logout +
+h2-console +
+/swagger + + diff --git a/2025-11/spring-23-ACL/oauth/pom.xml b/2025-11/spring-23-ACL/oauth/pom.xml new file mode 100644 index 00000000..36300726 --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + ru.otus + spring-framework-27-oauth + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.10 + + + + 2.6.11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + + + org.springdoc + springdoc-openapi-ui + 1.6.9 + + + + org.webjars + jquery + 3.4.1 + + + org.webjars + bootstrap + 4.3.1 + + + org.webjars + webjars-locator-core + + + org.webjars + js-cookie + 2.1.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java new file mode 100644 index 00000000..7a69b021 --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java @@ -0,0 +1,13 @@ +package ru.otus.spring.sso; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GithubApplication { + + public static void main(String[] args) { + SpringApplication.run( GithubApplication.class, args); + } + +} \ No newline at end of file diff --git a/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java new file mode 100644 index 00000000..a6e55cd2 --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java @@ -0,0 +1,13 @@ +package ru.otus.spring.sso.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class IndexController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } +} diff --git a/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java new file mode 100644 index 00000000..a99dc54d --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java @@ -0,0 +1,17 @@ +package ru.otus.spring.sso.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Map; + +@RestController +public class UserController { + @GetMapping("/user") + public Map user( @AuthenticationPrincipal OAuth2User principal) { + return Collections.singletonMap("name", principal.getAttribute("name")); + } +} diff --git a/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java new file mode 100644 index 00000000..b357a066 --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java @@ -0,0 +1,26 @@ +package ru.otus.spring.sso.security; + +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; + +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure( HttpSecurity http ) throws Exception { + http + .authorizeRequests( a -> a + .antMatchers( "/", "/error", "/webjars/**" ).permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling( e -> e + .authenticationEntryPoint( new HttpStatusEntryPoint( HttpStatus.UNAUTHORIZED ) ) + ) + .csrf().disable() + .logout( l -> l + .logoutSuccessUrl( "/" ).permitAll() + ) + + .oauth2Login(); + } +} diff --git a/2025-11/spring-23-ACL/oauth/src/main/resources/application.yml b/2025-11/spring-23-ACL/oauth/src/main/resources/application.yml new file mode 100644 index 00000000..c595efd2 --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + security: + oauth2: + client: + registration: + github: + clientId: Ov23liJ3EIv6UfvJ4jPr + clientSecret: 0cb7763869aaa2151a399336c64cdef2618c6332 + +logging: + level: + root: error + org.springframework.security: DEBUG \ No newline at end of file diff --git a/2025-11/spring-23-ACL/oauth/src/main/resources/templates/index.html b/2025-11/spring-23-ACL/oauth/src/main/resources/templates/index.html new file mode 100644 index 00000000..09dae5ba --- /dev/null +++ b/2025-11/spring-23-ACL/oauth/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ + + + + + + Demo + + + + + + + + +

Demo

+
+ + \ No newline at end of file diff --git a/2025-11/spring-24-spring-batch/.gitignore b/2025-11/spring-24-spring-batch/.gitignore new file mode 100644 index 00000000..7622f151 --- /dev/null +++ b/2025-11/spring-24-spring-batch/.gitignore @@ -0,0 +1,9 @@ +.idea/ +*.iml + +target/ + +output.csv +*.log +output*.dat +test-output.dat diff --git a/2025-11/spring-24-spring-batch/entries.csv b/2025-11/spring-24-spring-batch/entries.csv new file mode 100644 index 00000000..bde433ce --- /dev/null +++ b/2025-11/spring-24-spring-batch/entries.csv @@ -0,0 +1,16 @@ +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +Mary,24 +Ivan,23 +John,24 +Sunny,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 diff --git a/2025-11/spring-24-spring-batch/pom.xml b/2025-11/spring-24-spring-batch/pom.xml new file mode 100644 index 00000000..7e088c99 --- /dev/null +++ b/2025-11/spring-24-spring-batch/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + ru.otus.example + spring-batch-demo + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + + 3.5.6 + + + + + 17 + 17 + 17 + 4.3.8 + 4.11.0 + 2.2.220 + 2.0 + 32.1.2-jre + + + + + + org.springframework.boot + spring-boot-starter-batch + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + ${flapdoodle.version} + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + ${flapdoodle.version} + + + + com.github.cloudyrock.mongock + mongock-spring-v5 + ${mongock.version} + + + com.google.guava + guava + + + + + + com.google.guava + guava + ${guava.version} + + + + com.github.cloudyrock.mongock + mongodb-springdata-v3-driver + ${mongock.version} + + + + org.projectlombok + lombok + true + + + + org.springframework.shell + spring-shell-starter + 3.4.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.batch + spring-batch-test + test + + + + + spring-batch-demo + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java new file mode 100644 index 00000000..58b6edf6 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java @@ -0,0 +1,16 @@ +package ru.otus.example.springbatch; + +import com.github.cloudyrock.spring.v5.EnableMongock; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@EnableMongock +@SpringBootApplication +public class Main { + // --spring.shell.interactive.enabled=false --spring.batch.job.enabled=true inputFileName=entries.csv outputFileName=output_new.dat + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} + + diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java new file mode 100644 index 00000000..feb52399 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java @@ -0,0 +1,45 @@ +package ru.otus.example.springbatch.chandgelogs; + +import com.github.cloudyrock.mongock.ChangeLog; +import com.github.cloudyrock.mongock.ChangeSet; +import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate; +import com.mongodb.client.MongoDatabase; +import ru.otus.example.springbatch.model.Person; + +@ChangeLog(order = "001") +public class InitMongoDBDataChangeLog { + + @ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true) + public void dropDB(MongoDatabase database){ + database.drop(); + } + + @ChangeSet(order = "001", id = "initPersons", author = "stvort", runAlways = true) + public void initPersons(MongockTemplate template){ + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java new file mode 100644 index 00000000..5d9deb61 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java @@ -0,0 +1,14 @@ +package ru.otus.example.springbatch.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties("app") +public class AppProps { + private String inputFile; + private String outputFile; + +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java new file mode 100644 index 00000000..276e92b9 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java @@ -0,0 +1,17 @@ +package ru.otus.example.springbatch.config; + +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@SuppressWarnings("unused") +//@Configuration +public class BatchConfig { + @Bean + public JobRegistryBeanPostProcessor postProcessor(JobRegistry jobRegistry) { + var processor = new JobRegistryBeanPostProcessor(); + processor.setJobRegistry(jobRegistry); + return processor; + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java new file mode 100644 index 00000000..2f1a81ed --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java @@ -0,0 +1,188 @@ +package ru.otus.example.springbatch.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.*; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.file.FlatFileItemReader; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; +import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; +import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper; +import org.springframework.batch.item.file.transform.DelimitedLineAggregator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import org.springframework.lang.NonNull; +import org.springframework.transaction.PlatformTransactionManager; +import ru.otus.example.springbatch.model.Person; +import ru.otus.example.springbatch.service.CleanUpService; +import ru.otus.example.springbatch.service.HappyBirthdayService; + +import java.util.List; + + +@SuppressWarnings("unused") +@Configuration +public class JobConfig { + private static final int CHUNK_SIZE = 5; + private final Logger logger = LoggerFactory.getLogger("Batch"); + + public static final String OUTPUT_FILE_NAME = "outputFileName"; + public static final String INPUT_FILE_NAME = "inputFileName"; + public static final String IMPORT_USER_JOB_NAME = "importUserJob"; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private PlatformTransactionManager platformTransactionManager; + + + @Autowired + private CleanUpService cleanUpService; + + @StepScope + @Bean + public FlatFileItemReader reader(@Value("#{jobParameters['" + INPUT_FILE_NAME + "']}") String inputFileName) { + return new FlatFileItemReaderBuilder() + .name("personItemReader") + .resource(new FileSystemResource(inputFileName)) + + .delimited() + .names("name", "age") + .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{ + setTargetType(Person.class); + }}).build(); + } + + @StepScope + @Bean + public ItemProcessor processor(HappyBirthdayService happyBirthdayService) { + return happyBirthdayService::doHappyBirthday; + } + + @StepScope + @Bean + public FlatFileItemWriter writer(@Value("#{jobParameters['" + OUTPUT_FILE_NAME + "']}") String outputFileName) { + return new FlatFileItemWriterBuilder() + .name("personItemWriter") + .resource(new FileSystemResource(outputFileName)) + .lineAggregator(new DelimitedLineAggregator<>()) + .build(); + } + + + @Bean + public MethodInvokingTaskletAdapter cleanUpTasklet() { + MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter(); + + adapter.setTargetObject(cleanUpService); + adapter.setTargetMethod("cleanUp"); + + return adapter; + } + + + @Bean + public Job importUserJob(Step transformPersonsStep, Step cleanUpStep) { + return new JobBuilder(IMPORT_USER_JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .flow(transformPersonsStep) + .next(cleanUpStep) + .end() + .listener(new JobExecutionListener() { + @Override + public void beforeJob(@NonNull JobExecution jobExecution) { + logger.info("Начало job"); + } + + @Override + public void afterJob(@NonNull JobExecution jobExecution) { + logger.info("Конец job"); + } + }) + .build(); + } + + @Bean + public Step transformPersonsStep(ItemReader reader, FlatFileItemWriter writer, + ItemProcessor itemProcessor) { + return new StepBuilder("transformPersonsStep", jobRepository) + .chunk(CHUNK_SIZE, platformTransactionManager) + .reader(reader) + .processor(itemProcessor) + .writer(writer) + .listener(new ItemReadListener<>() { + public void beforeRead() { + logger.info("Начало чтения"); + } + + public void afterRead(@NonNull Person o) { + logger.info("Конец чтения"); + } + + public void onReadError(@NonNull Exception e) { + logger.info("Ошибка чтения"); + } + }) + .listener(new ItemWriteListener() { + public void beforeWrite(@NonNull List list) { + logger.info("Начало записи"); + } + + public void afterWrite(@NonNull List list) { + logger.info("Конец записи"); + } + + public void onWriteError(@NonNull Exception e, @NonNull List list) { + logger.info("Ошибка записи"); + } + }) + .listener(new ItemProcessListener<>() { + public void beforeProcess(@NonNull Person o) { + logger.info("Начало обработки"); + } + + public void afterProcess(@NonNull Person o, Person o2) { + logger.info("Конец обработки"); + } + + public void onProcessError(@NonNull Person o, @NonNull Exception e) { + logger.info("Ошибка обработки"); + } + }) + .listener(new ChunkListener() { + public void beforeChunk(@NonNull ChunkContext chunkContext) { + logger.info("Начало пачки"); + } + + public void afterChunk(@NonNull ChunkContext chunkContext) { + logger.info("Конец пачки"); + } + + public void afterChunkError(@NonNull ChunkContext chunkContext) { + logger.info("Ошибка пачки"); + } + }) +// .taskExecutor(new SimpleAsyncTaskExecutor()) + .build(); + } + + @Bean + public Step cleanUpStep() { + return new StepBuilder("cleanUpStep", jobRepository) + .tasklet(cleanUpTasklet(), platformTransactionManager) + .build(); + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java new file mode 100644 index 00000000..b95cb1ff --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java @@ -0,0 +1,13 @@ +package ru.otus.example.springbatch.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Person { + private String name; + private int age; +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java new file mode 100644 index 00000000..9b88036d --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java @@ -0,0 +1,16 @@ +package ru.otus.example.springbatch.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CleanUpService { + + @SuppressWarnings("unused") + public void cleanUp() throws Exception { + log.info("Выполняю завершающие мероприятия..."); + Thread.sleep(1000); + log.info("Завершающие мероприятия закончены"); + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java new file mode 100644 index 00000000..00ec7287 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java @@ -0,0 +1,13 @@ +package ru.otus.example.springbatch.service; + +import org.springframework.stereotype.Service; +import ru.otus.example.springbatch.model.Person; + +@Service +public class HappyBirthdayService { + + public Person doHappyBirthday(Person person){ + person.setAge(person.getAge() + 1); + return person; + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java new file mode 100644 index 00000000..a8f9db2c --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java @@ -0,0 +1,61 @@ +package ru.otus.example.springbatch.shell; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import ru.otus.example.springbatch.config.AppProps; + +import java.util.Properties; + +import static ru.otus.example.springbatch.config.JobConfig.IMPORT_USER_JOB_NAME; +import static ru.otus.example.springbatch.config.JobConfig.INPUT_FILE_NAME; +import static ru.otus.example.springbatch.config.JobConfig.OUTPUT_FILE_NAME; + +@RequiredArgsConstructor +@ShellComponent +public class BatchCommands { + + private final AppProps appProps; + private final Job importUserJob; + + private final JobLauncher jobLauncher; + private final JobOperator jobOperator; + private final JobExplorer jobExplorer; + + //http://localhost:8080/h2-console/ + + + @SuppressWarnings("unused") + @ShellMethod(value = "startMigrationJobWithJobLauncher", key = "sm-jl") + public void startMigrationJobWithJobLauncher() throws Exception { + JobExecution execution = jobLauncher.run(importUserJob, new JobParametersBuilder() + .addString(INPUT_FILE_NAME, appProps.getInputFile()) + .addString(OUTPUT_FILE_NAME, appProps.getOutputFile()) + .toJobParameters()); + System.out.println(execution); + } + + @SuppressWarnings("unused") + @ShellMethod(value = "startMigrationJobWithJobOperator", key = "sm-jo") + public void startMigrationJobWithJobOperator() throws Exception { + Properties properties = new Properties(); + properties.put(INPUT_FILE_NAME, appProps.getInputFile()); + properties.put(OUTPUT_FILE_NAME, appProps.getOutputFile()); + + Long executionId = jobOperator.start(IMPORT_USER_JOB_NAME, properties); + System.out.println(jobOperator.getSummary(executionId)); + } + + @SuppressWarnings("unused") + @ShellMethod(value = "showInfo", key = "i") + public void showInfo() { + System.out.println(jobExplorer.getJobNames()); + System.out.println(jobExplorer.getLastJobInstance(IMPORT_USER_JOB_NAME)); + } +} diff --git a/2025-11/spring-24-spring-batch/src/main/resources/application.yml b/2025-11/spring-24-spring-batch/src/main/resources/application.yml new file mode 100644 index 00000000..2c5a9894 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/main/resources/application.yml @@ -0,0 +1,60 @@ +spring: + main: + allow-circular-references: true + + batch: + job: + enabled: false + + shell: + interactive: + enabled: true + noninteractive: + enabled: false + + command: + version: + enabled: false + + data: + mongodb: + host: localhost + port: 0 + database: SpringBatchExampleDB + + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + + h2: + console: + enabled: true + path: /h2-console +de: + flapdoodle: + mongodb: + embedded: + version: 6.0.5 + +mongock: + runner-type: "InitializingBean" + change-logs-scan-package: + - ru.otus.example.springbatch.chandgelogs + mongo-db: + write-concern: + journal: false + read-concern: local + +app: + ages-count-to-add: 1 + input-file: entries.csv + output-file: output.dat + +logging: + level: + root: ERROR + Batch: INFO + ru.otus.example.springbatch: INFO + diff --git a/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java b/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java new file mode 100644 index 00000000..3a4441bf --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java @@ -0,0 +1,72 @@ +package ru.otus.example.springbatch.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.JobRepositoryTestUtils; +import org.springframework.batch.test.context.SpringBatchTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.File; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.otus.example.springbatch.config.JobConfig.IMPORT_USER_JOB_NAME; +import static ru.otus.example.springbatch.config.JobConfig.INPUT_FILE_NAME; +import static ru.otus.example.springbatch.config.JobConfig.OUTPUT_FILE_NAME; + +@SpringBootTest +@SpringBatchTest +class ImportUserJobTest { + + private static final String TEST_INPUT_FILE_NAME = "test-entries.csv"; + private static final String EXPECTED_OUTPUT_FILE_NAME = "expected-test-output.dat"; + private static final String EXPECTED_MONGO_OUTPUT_FILE_NAME = "expected-mongo-test-output.dat"; + private static final String TEST_OUTPUT_FILE_NAME = "test-output.dat"; + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Autowired + private JobRepositoryTestUtils jobRepositoryTestUtils; + + @BeforeEach + void clearMetaData() { + jobRepositoryTestUtils.removeJobExecutions(); + } + + @Test + void testJob() throws Exception { + var classLoader = ImportUserJobTest.class.getClassLoader(); + var testInputFileName = URLDecoder.decode( + Objects.requireNonNull(classLoader.getResource(TEST_INPUT_FILE_NAME)).getFile(), + StandardCharsets.UTF_8 + ); + var expectedResultFileName = URLDecoder.decode( + Objects.requireNonNull(classLoader.getResource(EXPECTED_OUTPUT_FILE_NAME)).getFile(), + StandardCharsets.UTF_8 + ); + + Job job = jobLauncherTestUtils.getJob(); + assertThat(job).isNotNull() + .extracting(Job::getName) + .isEqualTo(IMPORT_USER_JOB_NAME); + + JobParameters parameters = new JobParametersBuilder() + .addString(INPUT_FILE_NAME, testInputFileName) + .addString(OUTPUT_FILE_NAME, TEST_OUTPUT_FILE_NAME) + .toJobParameters(); + JobExecution jobExecution = jobLauncherTestUtils.launchJob(parameters); + + assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo("COMPLETED"); + assertThat(new File(TEST_OUTPUT_FILE_NAME)) + .hasSameTextualContentAs(new File(expectedResultFileName), StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java b/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java new file mode 100644 index 00000000..bacb4ee2 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java @@ -0,0 +1,45 @@ +package ru.otus.example.springbatch.testchangelogs; + +import com.github.cloudyrock.mongock.ChangeLog; +import com.github.cloudyrock.mongock.ChangeSet; +import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate; +import com.mongodb.client.MongoDatabase; +import ru.otus.example.springbatch.model.Person; + +@ChangeLog(order = "001") +public class InitMongoDBDataChangeLog { + + @ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true) + public void dropDB(MongoDatabase database){ + database.drop(); + } + + @ChangeSet(order = "001", id = "initPersons", author = "stvort", runAlways = true) + public void initPersons(MongockTemplate template){ + template.save(new Person("Тестовый Джон", 21)); + template.save(new Person("Тестовый Игорь", 32)); + template.save(new Person("Тестовый Дмитрий", 52)); + template.save(new Person("Тестовый Михаил", 22)); + template.save(new Person("Тестовый Герман", 33)); + template.save(new Person("Тестовый Джон", 21)); + template.save(new Person("Тестовый Игорь", 32)); + template.save(new Person("Тестовый Дмитрий", 52)); + template.save(new Person("Тестовый Михаил", 22)); + template.save(new Person("Тестовый Герман", 33)); + template.save(new Person("Тестовый Джон", 21)); + template.save(new Person("Тестовый Игорь", 32)); + template.save(new Person("Тестовый Дмитрий", 52)); + template.save(new Person("Тестовый Михаил", 22)); + template.save(new Person("Тестовый Герман", 33)); + template.save(new Person("Тестовый Джон", 21)); + template.save(new Person("Тестовый Игорь", 32)); + template.save(new Person("Тестовый Дмитрий", 52)); + template.save(new Person("Тестовый Михаил", 22)); + template.save(new Person("Тестовый Герман", 33)); + template.save(new Person("Тестовый Джон", 21)); + template.save(new Person("Тестовый Игорь", 32)); + template.save(new Person("Тестовый Дмитрий", 52)); + template.save(new Person("Тестовый Михаил", 22)); + template.save(new Person("Тестовый Герман", 33)); + } +} diff --git a/2025-11/spring-24-spring-batch/src/test/resources/application.yml b/2025-11/spring-24-spring-batch/src/test/resources/application.yml new file mode 100644 index 00000000..0f1636bc --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/resources/application.yml @@ -0,0 +1,43 @@ +spring: + main: + allow-circular-references: true + + batch: + job: + enabled: false + + shell: + interactive: + enabled: false + command: + version: + enabled: false + + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + + + + data: + mongodb: + host: localhost + port: 0 + database: SpringBatchTestExampleDB + +de: + flapdoodle: + mongodb: + embedded: + version: 6.0.5 + +mongock: + runner-type: "InitializingBean" + change-logs-scan-package: + - ru.otus.example.springbatch.testchangelogs + mongo-db: + write-concern: + journal: false + read-concern: local \ No newline at end of file diff --git a/2025-11/spring-24-spring-batch/src/test/resources/expected-mongo-test-output.dat b/2025-11/spring-24-spring-batch/src/test/resources/expected-mongo-test-output.dat new file mode 100644 index 00000000..adbec3af --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/resources/expected-mongo-test-output.dat @@ -0,0 +1,25 @@ +Person(name=Тестовый Дмитрий, age=53) +Person(name=Тестовый Дмитрий, age=53) +Person(name=Тестовый Дмитрий, age=53) +Person(name=Тестовый Дмитрий, age=53) +Person(name=Тестовый Дмитрий, age=53) +Person(name=Тестовый Герман, age=34) +Person(name=Тестовый Герман, age=34) +Person(name=Тестовый Герман, age=34) +Person(name=Тестовый Герман, age=34) +Person(name=Тестовый Герман, age=34) +Person(name=Тестовый Игорь, age=33) +Person(name=Тестовый Игорь, age=33) +Person(name=Тестовый Игорь, age=33) +Person(name=Тестовый Игорь, age=33) +Person(name=Тестовый Игорь, age=33) +Person(name=Тестовый Михаил, age=23) +Person(name=Тестовый Михаил, age=23) +Person(name=Тестовый Михаил, age=23) +Person(name=Тестовый Михаил, age=23) +Person(name=Тестовый Михаил, age=23) +Person(name=Тестовый Джон, age=22) +Person(name=Тестовый Джон, age=22) +Person(name=Тестовый Джон, age=22) +Person(name=Тестовый Джон, age=22) +Person(name=Тестовый Джон, age=22) diff --git a/2025-11/spring-24-spring-batch/src/test/resources/expected-test-output.dat b/2025-11/spring-24-spring-batch/src/test/resources/expected-test-output.dat new file mode 100644 index 00000000..b4c926b1 --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/resources/expected-test-output.dat @@ -0,0 +1,16 @@ +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=Mary, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Sunny, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) diff --git a/2025-11/spring-24-spring-batch/src/test/resources/test-entries.csv b/2025-11/spring-24-spring-batch/src/test/resources/test-entries.csv new file mode 100644 index 00000000..bde433ce --- /dev/null +++ b/2025-11/spring-24-spring-batch/src/test/resources/test-entries.csv @@ -0,0 +1,16 @@ +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +Mary,24 +Ivan,23 +John,24 +Sunny,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 diff --git a/2025-11/spring-29-integrations-channels/.gitignore b/2025-11/spring-29-integrations-channels/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/pom.xml b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/pom.xml new file mode 100644 index 00000000..f3095768 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + ru.otus + integrations-channels-exercise + 1.0-SNAPSHOT + + + ru.otus + integrations-channels + 1.0 + + + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework + spring-messaging + + + + org.springframework.boot + spring-boot-starter-test + + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/App.java b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/App.java new file mode 100644 index 00000000..d06d4365 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/App.java @@ -0,0 +1,16 @@ +package ru.otus.spring.integration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.integration.annotation.IntegrationComponentScan; + +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} \ No newline at end of file diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/AppRunner.java b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/AppRunner.java new file mode 100644 index 00000000..015c043a --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/AppRunner.java @@ -0,0 +1,62 @@ +package ru.otus.spring.integration; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.stereotype.Component; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.isNull; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AppRunner implements CommandLineRunner { + final PollableChannel queueChannel; + final SubscribableChannel subscribableDirectChannel; + + @Override + public void run(String... args) throws Exception { + log.warn("INIT"); + for (int i = 0; i < 10; i++) { + queueChannel.send(MessageBuilder.withPayload("Start " + i).build()); + } + log.warn("INIT FINISH"); + Thread.sleep(5000); + + log.warn("START"); + + subscribableDirectChannel.subscribe((msg) -> log.warn("Receive msg: {}", msg)); + + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + log.warn("I am here!!!"); + Message receivedMessage = queueChannel.receive(5000); + if (isNull(receivedMessage)) { + return; + } + subscribableDirectChannel.send(receivedMessage); + }, 100, 300, TimeUnit.MILLISECONDS); + log.warn("START FINISH"); + + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello").build()); + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello2").build()); + + Thread.sleep(2_000); + + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello3").build()); + + Thread.sleep(3_000); + executor.shutdown(); + } +} diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/IntegrationConfig.java b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/IntegrationConfig.java new file mode 100644 index 00000000..372c559a --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/java/ru/otus/spring/integration/IntegrationConfig.java @@ -0,0 +1,23 @@ +package ru.otus.spring.integration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.dsl.DirectChannelSpec; +import org.springframework.integration.dsl.MessageChannels; +import org.springframework.messaging.PollableChannel; + +@Configuration +public class IntegrationConfig { + + @Bean + public PollableChannel queueChannel() { + return new QueueChannel(100); + } + + @Bean + public DirectChannelSpec subscribableDirectChannel() { + return MessageChannels.direct("subscribableDirectChannel"); + } + +} diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/resources/application.yml b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..1a9aeb48 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/main/resources/application.yml @@ -0,0 +1,5 @@ +logging: + level: + root: info + + org.springframework.integration: debug \ No newline at end of file diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java new file mode 100644 index 00000000..1541b4ab --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java @@ -0,0 +1,98 @@ +package ru.otus.spring.integration; + + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@SuppressWarnings("all") +public class MessagesTest { + + @Test + public void testCreateSimpleGenericMessage() { + // TODO: Создайте сообщение с payload-ом "Hello" с помощью конструктора + Message message = null; + + assertNotNull(message); + assertEquals(GenericMessage.class, message.getClass()); + assertNotNull(message.getPayload()); + assertEquals("Hello", message.getPayload()); + } + + @Test + public void testCreateGenericMessage() { + // TODO: Создайте сообщение с пользователем с помощью конструктора + Message message = null; + + assertNotNull(message); + assertEquals(GenericMessage.class, message.getClass()); + assertNotNull(message.getPayload()); + assertEquals(new User("John", 23), message.getPayload()); + } + + @Test + public void testGenericMessageWithHeaders() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" + Map headers = null; + Message message = null; + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testGenericMessageWithMessageHeaders() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" + MessageHeaders headers = null; + Message message = null; + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testErrorMessage() { + // TODO: Создайте сообщение об ошибки с объектом NullPointerException внутри + Message errorMessage = null; + + assertNotNull(errorMessage); + assertEquals(ErrorMessage.class, errorMessage.getClass()); + assertEquals(NullPointerException.class, errorMessage.getPayload().getClass()); + } + + @Test + public void testMessageBuilder() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" с помощью MessageBuilder + Message message = null; + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testBuildFromMessage() { + Message original = MessageBuilder + .withPayload(new User("Kate", 30)) + .setHeader("processor", "userService") + .build(); + + // TODO: Создайте новое сообщение с теми же payload и header-ами c помощью MessageBuilder + Message newMessage = null; + + assertNotNull(newMessage); + assertEquals(original.getPayload(), newMessage.getPayload()); + assertEquals(original.getHeaders().get("processor"), newMessage.getHeaders().get("processor")); + } +} diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/User.java b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/User.java new file mode 100644 index 00000000..3c71018d --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-exercise/src/test/java/ru/otus/spring/integration/User.java @@ -0,0 +1,15 @@ +package ru.otus.spring.integration; + +import java.util.Objects; + +public record User(String name, int age) { + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User user)) return false; + return age == user.age && + Objects.equals(name, user.name); + } + +} diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-solution/pom.xml b/2025-11/spring-29-integrations-channels/integrations-channels-solution/pom.xml new file mode 100644 index 00000000..90d25b5e --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-solution/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + ru.otus + integrations-channels-solution + 1.0-SNAPSHOT + + + ru.otus + integrations-channels + 1.0 + + + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework + spring-messaging + + + + org.springframework.boot + spring-boot-starter-test + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/java/ru/otus/spring/integration/App.java b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/java/ru/otus/spring/integration/App.java new file mode 100644 index 00000000..dd7bc06a --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/java/ru/otus/spring/integration/App.java @@ -0,0 +1,79 @@ +package ru.otus.spring.integration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.dsl.DirectChannelSpec; +import org.springframework.integration.dsl.MessageChannels; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.SubscribableChannel; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.isNull; + +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class App { + + public static void main(String[] args) throws InterruptedException { + ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args); + + PollableChannel queueChannel = ctx.getBean("queueChannel", PollableChannel.class); + SubscribableChannel subscribableDirectChannel = ctx.getBean("subscribableDirectChannel", SubscribableChannel.class); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + log.warn("I am here!!!"); + Message receivedMessage = queueChannel.receive(5000); + if (isNull(receivedMessage)) { + return; + } + subscribableDirectChannel.send(receivedMessage); + }, 100, 300, TimeUnit.MILLISECONDS); + subscribableDirectChannel.subscribe((msg) -> log.warn("Receive msg: {}", msg)); + + + log.warn("INIT"); + for (int i = 0; i < 10; i++) { + queueChannel.send(MessageBuilder.withPayload("Start " + i).build()); + } + log.warn("INIT FINISH"); + Thread.sleep(5000); + + log.warn("START"); + + log.warn("START FINISH"); + + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello").build()); + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello2").build()); + + Thread.sleep(2_000); + + log.warn(""); + queueChannel.send(MessageBuilder.withPayload("Hello3").build()); + + Thread.sleep(3_000); + executor.shutdown(); + } + + @Bean + public PollableChannel queueChannel() { + return new QueueChannel(9); + } + + @Bean + public DirectChannelSpec subscribableDirectChannel() { + return MessageChannels.direct("subscribableDirectChannel"); + } +} \ No newline at end of file diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/resources/application.yml b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/resources/application.yml new file mode 100644 index 00000000..1a9aeb48 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/main/resources/application.yml @@ -0,0 +1,5 @@ +logging: + level: + root: info + + org.springframework.integration: debug \ No newline at end of file diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java new file mode 100644 index 00000000..ae2210bf --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java @@ -0,0 +1,101 @@ +package ru.otus.spring.integration; + + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@SuppressWarnings("all") +public class MessagesTest { + + @Test + public void testCreateSimpleGenericMessage() { + // TODO: Создайте сообщение с payload-ом "Hello" с помощью конструктора + Message message = new GenericMessage<>("Hello"); + + assertNotNull(message); + assertEquals(GenericMessage.class, message.getClass()); + assertNotNull(message.getPayload()); + assertEquals("Hello", message.getPayload()); + } + + @Test + public void testCreateGenericMessage() { + // TODO: Создайте сообщение с пользователем с помощью конструктора + Message message = new GenericMessage<>(new User("John", 23)); + + assertNotNull(message); + assertEquals(GenericMessage.class, message.getClass()); + assertNotNull(message.getPayload()); + assertEquals(new User("John", 23), message.getPayload()); + } + + @Test + public void testGenericMessageWithHeaders() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" + Map headers = Collections.singletonMap("to", "World"); + Message message = new GenericMessage<>("Hello", headers); + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testGenericMessageWithMessageHeaders() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" + MessageHeaders headers = new MessageHeaders(Collections.singletonMap("to", "World")); + Message message = new GenericMessage<>("Hello", headers); + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testErrorMessage() { + // TODO: Создайте сообщение об ошибки с объектом NullPointerException внутри + Message errorMessage = new ErrorMessage(new NullPointerException()); + + assertNotNull(errorMessage); + assertEquals(ErrorMessage.class, errorMessage.getClass()); + assertEquals(NullPointerException.class, errorMessage.getPayload().getClass()); + } + + @Test + public void testMessageBuilder() { + // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" с помощью MessageBuilder + Message message = MessageBuilder.withPayload("Hello") + .setHeader("to", "World") + .build(); + + assertNotNull(message); + assertEquals("Hello", message.getPayload()); + assertEquals("World", message.getHeaders().get("to", String.class)); + } + + @Test + public void testBuildFromMessage() { + Message original = MessageBuilder + .withPayload(new User("Kate", 30)) + .setHeader("processor", "userService") + .build(); + + // TODO: Создайте новое сообщение с теми же payload и header-ами c помощью MessageBuilder + Message newMessage = MessageBuilder.fromMessage(original).build(); + + assertNotNull(newMessage); + assertEquals(original.getPayload(), newMessage.getPayload()); + assertEquals(original.getHeaders().get("processor"), newMessage.getHeaders().get("processor")); + } +} diff --git a/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/User.java b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/User.java new file mode 100644 index 00000000..3c71018d --- /dev/null +++ b/2025-11/spring-29-integrations-channels/integrations-channels-solution/src/test/java/ru/otus/spring/integration/User.java @@ -0,0 +1,15 @@ +package ru.otus.spring.integration; + +import java.util.Objects; + +public record User(String name, int age) { + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User user)) return false; + return age == user.age && + Objects.equals(name, user.name); + } + +} diff --git a/2025-11/spring-29-integrations-channels/pom.xml b/2025-11/spring-29-integrations-channels/pom.xml new file mode 100644 index 00000000..a756ed13 --- /dev/null +++ b/2025-11/spring-29-integrations-channels/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.2 + + + ru.otus + integrations-channels + 1.0 + + pom + + + integrations-channels-exercise + integrations-channels-solution + + + + 17 + 17 + UTF-8 + + diff --git a/2025-11/spring-30-endpoints-flow-components/.gitignore b/2025-11/spring-30-endpoints-flow-components/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/pom.xml b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/pom.xml new file mode 100644 index 00000000..6c20857a --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + ru.otus + endpoints-flow-components-exercise + 1.0-SNAPSHOT + + + ru.otus + endpoints-flow-components + 1.0 + + + + + org.springframework.boot + spring-boot-starter-integration + + + + org.apache.commons + commons-lang3 + + + + + org.projectlombok + lombok + provided + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/App.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/App.java new file mode 100644 index 00000000..f4f51ce9 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/App.java @@ -0,0 +1,12 @@ +package ru.otus.spring.integration; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/AppRunner.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/AppRunner.java new file mode 100644 index 00000000..f431ff12 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/AppRunner.java @@ -0,0 +1,19 @@ +package ru.otus.spring.integration.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import ru.otus.spring.integration.services.OrderService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AppRunner implements CommandLineRunner { + final OrderService orderService; + + @Override + public void run(String... args) { + orderService.startGenerateOrdersLoop(); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java new file mode 100644 index 00000000..3abcf8de --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java @@ -0,0 +1,32 @@ +package ru.otus.spring.integration.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.MessageChannelSpec; +import org.springframework.integration.dsl.MessageChannels; + +@SuppressWarnings("unused") +@Configuration +public class IntegrationConfig { + @Bean + public MessageChannelSpec itemsChannel() { + return MessageChannels.queue(10); + } + + @Bean + public MessageChannelSpec foodChannel() { + return MessageChannels.publishSubscribe(); + } + + // TODO: create default poller + + @Bean + public IntegrationFlow cafeFlow() { + return IntegrationFlow.from(itemsChannel()) + // TODO: cook OrderItem in the kitchen + // TODO*: add splitter and aggregator + // TODO: forward it to the publish subscriber channel + .get(); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java new file mode 100644 index 00000000..f27d5047 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java @@ -0,0 +1,6 @@ +package ru.otus.spring.integration.domain; + + +public record Food(String name) { + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java new file mode 100644 index 00000000..69d9da98 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java @@ -0,0 +1,5 @@ +package ru.otus.spring.integration.domain; + +public record OrderItem(String itemName) { + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/CafeGateway.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/CafeGateway.java new file mode 100644 index 00000000..58c24077 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/CafeGateway.java @@ -0,0 +1,12 @@ +package ru.otus.spring.integration.services; + + +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +// TODO: add messaging gateway annotation +public interface CafeGateway { + + // TODO: add gateway annotation with required channels + Food process(OrderItem orderItem); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenService.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenService.java new file mode 100644 index 00000000..2ae92af2 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenService.java @@ -0,0 +1,9 @@ +package ru.otus.spring.integration.services; + +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +public interface KitchenService { + + Food cook(OrderItem orderItem); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java new file mode 100644 index 00000000..08d3ddc2 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java @@ -0,0 +1,28 @@ +package ru.otus.spring.integration.services; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +@Service +@Slf4j +public class KitchenServiceImpl implements KitchenService { + + @Override + public Food cook(OrderItem orderItem) { + log.info("Cooking {}", orderItem.itemName()); + delay(); + log.info("Cooking {} done", orderItem.itemName()); + + return new Food(orderItem.itemName()); + } + + private static void delay() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderService.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderService.java new file mode 100644 index 00000000..c5da0ac9 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderService.java @@ -0,0 +1,5 @@ +package ru.otus.spring.integration.services; + +public interface OrderService { + void startGenerateOrdersLoop(); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java new file mode 100644 index 00000000..dae05628 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java @@ -0,0 +1,43 @@ +package ru.otus.spring.integration.services; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.springframework.stereotype.Service; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +@Service +@Slf4j +public class OrderServiceImpl implements OrderService { + private static final String[] MENU = {"coffee", "tea", "smoothie", "whiskey", "beer", "cola", "water"}; + + private final CafeGateway cafe; + + public OrderServiceImpl(CafeGateway cafe) { + this.cafe = cafe; + } + + @Override + public void startGenerateOrdersLoop() { + for (int i = 0; i < 10; i++) { + OrderItem orderItem = generateOrderItem(); + int num = i + 1; + log.info("{}, New orderItem: {}", num, orderItem.itemName()); + Food food = cafe.process(orderItem); + log.info("{}, Ready food: {}", num, food.name()); + delay(); + } + } + + private static OrderItem generateOrderItem() { + return new OrderItem(MENU[RandomUtils.nextInt(0, MENU.length)]); + } + + private void delay() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/resources/application.yml b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..1a9aeb48 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-exercise/src/main/resources/application.yml @@ -0,0 +1,5 @@ +logging: + level: + root: info + + org.springframework.integration: debug \ No newline at end of file diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/pom.xml b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/pom.xml new file mode 100644 index 00000000..fd184690 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + ru.otus + endpoints-flow-components-solution + 1.0-SNAPSHOT + + + ru.otus + endpoints-flow-components + 1.0 + + + + + org.springframework.boot + spring-boot-starter-integration + + + + org.apache.commons + commons-lang3 + + + + org.projectlombok + lombok + provided + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + ru.otus.spring.integration.App + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + ${checkstyle.config.url} + + + + + check + + + + + + + diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/App.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/App.java new file mode 100644 index 00000000..1b68561b --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/App.java @@ -0,0 +1,14 @@ +package ru.otus.spring.integration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@Slf4j +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/AppRunner.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/AppRunner.java new file mode 100644 index 00000000..f431ff12 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/AppRunner.java @@ -0,0 +1,19 @@ +package ru.otus.spring.integration.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import ru.otus.spring.integration.services.OrderService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AppRunner implements CommandLineRunner { + final OrderService orderService; + + @Override + public void run(String... args) { + orderService.startGenerateOrdersLoop(); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java new file mode 100644 index 00000000..59d9a13b --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/config/IntegrationConfig.java @@ -0,0 +1,38 @@ +package ru.otus.spring.integration.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.dsl.*; +import org.springframework.integration.scheduling.PollerMetadata; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.services.KitchenService; + +@Configuration +public class IntegrationConfig { + + @Bean + public MessageChannelSpec itemsChannel() { + return MessageChannels.queue(10); + } + + @Bean + public MessageChannelSpec foodChannel() { + return MessageChannels.publishSubscribe(); + } + + @Bean(name = PollerMetadata.DEFAULT_POLLER) + public PollerSpec poller() { + return Pollers.fixedRate(100).maxMessagesPerPoll(2); + } + + @Bean + public IntegrationFlow cafeFlow(KitchenService kitchenService) { + return IntegrationFlow.from(itemsChannel()) + .split() + .handle(kitchenService, "cook") + .transform(f -> new Food(f.name().toUpperCase())) + .aggregate() + .channel(foodChannel()) + .get(); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/Food.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/Food.java new file mode 100644 index 00000000..f27d5047 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/Food.java @@ -0,0 +1,6 @@ +package ru.otus.spring.integration.domain; + + +public record Food(String name) { + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java new file mode 100644 index 00000000..69d9da98 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java @@ -0,0 +1,5 @@ +package ru.otus.spring.integration.domain; + +public record OrderItem(String itemName) { + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/CafeGateway.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/CafeGateway.java new file mode 100644 index 00000000..85cae973 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/CafeGateway.java @@ -0,0 +1,16 @@ +package ru.otus.spring.integration.services; + + +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.MessagingGateway; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +import java.util.Collection; + +@MessagingGateway +public interface CafeGateway { + + @Gateway(requestChannel = "itemsChannel", replyChannel = "foodChannel") + Collection process(Collection orderItem); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenService.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenService.java new file mode 100644 index 00000000..2ae92af2 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenService.java @@ -0,0 +1,9 @@ +package ru.otus.spring.integration.services; + +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +public interface KitchenService { + + Food cook(OrderItem orderItem); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java new file mode 100644 index 00000000..8ad7c653 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/KitchenServiceImpl.java @@ -0,0 +1,27 @@ +package ru.otus.spring.integration.services; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +@Service +@Slf4j +public class KitchenServiceImpl implements KitchenService { + + @Override + public Food cook(OrderItem orderItem) { + log.info("Cooking {}", orderItem.itemName()); + delay(); + log.info("Cooking {} done", orderItem.itemName()); + return new Food(orderItem.itemName()); + } + + private static void delay() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderService.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderService.java new file mode 100644 index 00000000..c5da0ac9 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderService.java @@ -0,0 +1,5 @@ +package ru.otus.spring.integration.services; + +public interface OrderService { + void startGenerateOrdersLoop(); +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java new file mode 100644 index 00000000..d3a85e7e --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/integration/services/OrderServiceImpl.java @@ -0,0 +1,65 @@ +package ru.otus.spring.integration.services; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.springframework.stereotype.Service; +import ru.otus.spring.integration.domain.Food; +import ru.otus.spring.integration.domain.OrderItem; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class OrderServiceImpl implements OrderService { + private static final String[] MENU = {"coffee", "tea", "smoothie", "whiskey", "beer", "cola", "water"}; + + private final CafeGateway cafe; + + public OrderServiceImpl(CafeGateway cafe) { + this.cafe = cafe; + } + + @Override + public void startGenerateOrdersLoop() { + ForkJoinPool pool = ForkJoinPool.commonPool(); + for (int i = 0; i < 10; i++) { + int num = i + 1; + pool.execute(() -> { + Collection items = generateOrderItems(); + log.info("{}, New orderItems: {}", num, + items.stream().map(OrderItem::itemName) + .collect(Collectors.joining(","))); + Collection food = cafe.process(items); + log.info("{}, Ready food: {}", num, food.stream() + .map(Food::name) + .collect(Collectors.joining(","))); + }); + delay(); + } + } + + private static OrderItem generateOrderItem() { + return new OrderItem(MENU[RandomUtils.nextInt(0, MENU.length)]); + } + + private static Collection generateOrderItems() { + List items = new ArrayList<>(); + for (int i = 0; i < RandomUtils.nextInt(1, 5); ++i) { + items.add(generateOrderItem()); + } + return items; + } + + private void delay() { + try { + Thread.sleep(7000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/bridge/BridgeApp.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/bridge/BridgeApp.java new file mode 100644 index 00000000..ba8b27d9 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/bridge/BridgeApp.java @@ -0,0 +1,58 @@ +package ru.otus.spring.test.bridge; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.MessageChannels; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused") +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class BridgeApp { + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(BridgeApp.class, args); + Map channels = ctx.getBeansOfType(MessageChannel.class); + log.warn("CHANNELS:"); + int i = 0; + for (Map.Entry entry : channels.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + log.warn("HANDLERS:"); + i = 0; + Map endpoints = ctx.getBeansOfType(MessageHandler.class); + for (Map.Entry entry : endpoints.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + + Bridge bean = ctx.getBean(Bridge.class); + List strings = List.of("TEST1", "end"); + Collection result = bean.send(strings); + log.warn("Bridge send: {}, receive: {}", strings, result); + } + + + @MessagingGateway + public interface Bridge { + @Gateway(requestChannel = "flow.input"/*, replyChannel = "p2pChannel"*/) + Collection send(Collection strings); + } + + @Bean + public IntegrationFlow flow() { + return f -> f + .channel(MessageChannels.queue("p2pChannel", 10)); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/gateway/GatewayApp.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/gateway/GatewayApp.java new file mode 100644 index 00000000..74666244 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/gateway/GatewayApp.java @@ -0,0 +1,76 @@ +package ru.otus.spring.test.gateway; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +@SuppressWarnings("unused") +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class GatewayApp { + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(GatewayApp.class, args); + Map channels = ctx.getBeansOfType(MessageChannel.class); + log.warn("CHANNELS:"); + int i = 0; + for (Map.Entry entry : channels.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + log.warn("HANDLERS:"); + i = 0; + Map endpoints = ctx.getBeansOfType(MessageHandler.class); + for (Map.Entry entry : endpoints.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + Upcase upcase = ctx.getBean(Upcase.class); + Collection result = upcase.upcase(Arrays.asList("test", "new", "last")); + log.warn("Upcase result: {}", result); + + } + + @MessagingGateway + public interface Upcase { + @Gateway(requestChannel = "upcase.input") + Collection upcase(Collection strings); + } + + @Bean + public IntegrationFlow upcase() { + return f -> f//.channel("from-input-to-split") + .split() +// .split(list -> list.getObject().spliterator()) +// .split(getCustomSplitter(), "split") + .channel("from-split-to-transformer") + .transform(String::toUpperCase) + .channel("from-transformer-to-aggregate") + .aggregate(); +// .>filter(source -> source.stream().anyMatch(s -> s.startsWith("a"))) + } + + +// @Bean +// CustomSplitter getCustomSplitter() { +// return new CustomSplitter(); +// } +// +// public static class CustomSplitter { +// public Collection split(Message> message) { +// return message.getPayload().stream().skip(1).collect(Collectors.toList()); +// } +// } + + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/polling/PollingApp.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/polling/PollingApp.java new file mode 100644 index 00000000..8aa04973 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/polling/PollingApp.java @@ -0,0 +1,92 @@ +package ru.otus.spring.test.polling; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.MessageChannelSpec; +import org.springframework.integration.dsl.MessageChannels; +import org.springframework.integration.endpoint.PollingConsumer; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; + +import java.util.Map; + +import static java.lang.Thread.sleep; + +@SuppressWarnings("unused") +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class PollingApp { + public static void main(String[] args) throws InterruptedException { + ConfigurableApplicationContext ctx = SpringApplication.run(PollingApp.class, args); + log.warn("POLLER:"); + Map pollers = ctx.getBeansOfType(PollingConsumer.class); + int i = 0; + for (Map.Entry entry : pollers.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + log.warn("CHANNELS:"); + Map channels = ctx.getBeansOfType(MessageChannel.class); + i = 0; + for (Map.Entry entry : channels.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + log.warn("HANDLERS:"); + i = 0; + Map endpoints = ctx.getBeansOfType(MessageHandler.class); + for (Map.Entry entry : endpoints.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + + Polling polling = ctx.getBean(Polling.class); + String result = polling.send("test"); + log.warn("Polling result: {}", result); + + sleep(5000); + ctx.close(); + } + + @MessagingGateway + public interface Polling { + @Gateway(requestChannel = "flow.input", replyChannel = "pubSub") + String send(String value); + } + +// @Bean(name = PollerMetadata.DEFAULT_POLLER) +// public PollerMetadata defaultPoller() { + + /// / return Pollers.fixedRate(10_000).get(); +// PollerMetadata pollerMetadata = new PollerMetadata(); +// pollerMetadata.setMaxMessagesPerPoll(5); +// pollerMetadata.setTrigger(new PeriodicTrigger(Duration.ofSeconds(3))); +// return pollerMetadata; +// } + @Bean + public IntegrationFlow flow() { + return f -> f + .channel("pubSub") + .channel("p2p"); + } + + @Bean + public MessageChannelSpec p2p() { + return MessageChannels.queue("p2p", 10); + } + + @Bean + public MessageChannelSpec p2p2() { + return MessageChannels.priority("p2p2").capacity(10); + } + + @Bean + public MessageChannelSpec pubSub() { + return MessageChannels.publishSubscribe("pubSub"); + } +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/transform/TransformApp.java b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/transform/TransformApp.java new file mode 100644 index 00000000..c47be87b --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/java/ru/otus/spring/test/transform/TransformApp.java @@ -0,0 +1,91 @@ +package ru.otus.spring.test.transform; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.Transformers; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +@SuppressWarnings("unused") +@SpringBootApplication +@IntegrationComponentScan +@Slf4j +public class TransformApp { + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(TransformApp.class, args); + Map channels = ctx.getBeansOfType(MessageChannel.class); + log.warn("CHANNELS:"); + int i = 0; + for (Map.Entry entry : channels.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + log.warn("HANDLERS:"); + i = 0; + Map endpoints = ctx.getBeansOfType(MessageHandler.class); + for (Map.Entry entry : endpoints.entrySet()) { + log.warn("{}. {}/{} -> {}", ++i, entry.getKey(), entry.getValue().getClass().getSimpleName(), entry.getValue()); + } + Upcase upcase = ctx.getBean(Upcase.class); + Collection result = upcase.upcase(Arrays.asList(new Item("test"), new Item("new"), new Item("last"))); +// Collection result = upcase.upcase(Arrays.asList("test", "new", "last")); + log.warn("Upcase result: {}", result); + + } + + @Getter + public static class Item { + public Item() { + } + + public Item(String value) { + this.value = value; + } + + String value; + String name; + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "Item{" + + "value='" + value + '\'' + + ", name='" + name + '\'' + + '}'; + } + } + + @MessagingGateway + public interface Upcase { + @Gateway(requestChannel = "upcase.input") + Collection upcase(Collection strings); + } + + @Bean + public IntegrationFlow upcase() { + return f -> f + .split() + .transform(Transformers.toMap()) + ., Map>transform(map -> { + map.replaceAll((k, v) -> v != null ? v.toUpperCase() : v); + return map; + }) + .transform(Transformers.fromMap(Item.class)) + .aggregate(); + } + +} diff --git a/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/resources/application.yml b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/resources/application.yml new file mode 100644 index 00000000..1a9aeb48 --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/endpoints-flow-components-solution/src/main/resources/application.yml @@ -0,0 +1,5 @@ +logging: + level: + root: info + + org.springframework.integration: debug \ No newline at end of file diff --git a/2025-11/spring-30-endpoints-flow-components/pom.xml b/2025-11/spring-30-endpoints-flow-components/pom.xml new file mode 100644 index 00000000..d95546ef --- /dev/null +++ b/2025-11/spring-30-endpoints-flow-components/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.2 + + + ru.otus + endpoints-flow-components + 1.0 + + pom + + + endpoints-flow-components-exercise + endpoints-flow-components-solution + + + + 17 + 17 + UTF-8 + UTF-8 + 3.1.2 + 10.9.1 + https://raw.githubusercontent.com/OtusTeam/Spring/master/checkstyle.xml + + + diff --git a/2025-11/spring-31-data-rest/pom.xml b/2025-11/spring-31-data-rest/pom.xml new file mode 100644 index 00000000..4ebf7d7e --- /dev/null +++ b/2025-11/spring-31-data-rest/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + ru.otus + spring-31-data-rest + 1.0 + + pom + + + spring-31-exercise + spring-31-solution + + diff --git a/2025-11/spring-31-data-rest/requests.http b/2025-11/spring-31-data-rest/requests.http new file mode 100644 index 00000000..e6839f0a --- /dev/null +++ b/2025-11/spring-31-data-rest/requests.http @@ -0,0 +1,79 @@ +### "Index page" +GET http://localhost:8080/actuator + +### Список бинов, созданных в приложении +GET http://localhost:8080/actuator/beans + +### Информация о приложении +GET http://localhost:8080/actuator/info + +### Все @ConfigurationProperties +GET http://localhost:8080/actuator/configprops + +### Все перепенные окружения +GET http://localhost:8080/actuator/env + +### Список логгеров +GET http://localhost:8080/actuator/loggers + +### Конфигурация конкретного логгера +GET http://localhost:8080/actuator/loggers/org.springframework + +### Изменение уровня логгирования в runtime +POST http://localhost:8080/actuator/loggers/org.springframework +Content-Type: application/json + +{ + "configuredLevel": "TRACE" +} + +### Healthchecks +GET http://localhost:8080/actuator/health + +### Собственный healthcheck +GET http://localhost:8080/actuator/health/my + +### Список метрик +GET http://localhost:8080/actuator/metrics + +### Состояние подключений к БД +GET http://localhost:8080/actuator/metrics/hikaricp.connections.usage + +### Загрузка CPU приложением +GET http://localhost:8080/actuator/metrics/process.cpu.usage + +### Используемая JVM память +GET http://localhost:8080/actuator/metrics/jvm.memory.used + +### Получение данных о запросах +# Дополнительно можно настроить SLA, персентили и т.д. +# причём для отдельных запросов +GET http://localhost:8080/actuator/metrics/http.server.requests + +### А вот все метрики для Promehteus +GET http://localhost:8080/actuator/prometheus + + +### Spring Data REST - Single entity +GET http://localhost:8080/person/40 + +### Spring Data REST - Collection +GET http://localhost:8080/person + +### Add Alex +POST http://localhost:8080/person +Content-Type: application/json + +{ + "name": "Alex" +} + +### Rename Ivan +PATCH http://localhost:8080/person/1 +Content-Type: application/json + +{ + "name": "Anton" +} + +### diff --git a/2025-11/spring-31-data-rest/spring-31-exercise/pom.xml b/2025-11/spring-31-data-rest/spring-31-exercise/pom.xml new file mode 100644 index 00000000..6b3bfd88 --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-exercise/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + ru.otus + spring-31-exercise + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-data-rest + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + org.springframework.data + spring-data-rest-hal-explorer + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/App.java b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/App.java new file mode 100644 index 00000000..126c5eec --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/App.java @@ -0,0 +1,31 @@ +package ru.otus.spring.microservice; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import jakarta.annotation.PostConstruct; +import ru.otus.spring.microservice.domain.Person; +import ru.otus.spring.microservice.repostory.PersonRepository; + + +@SpringBootApplication +@EnableWebMvc +public class App { + + @Autowired + private PersonRepository repository; + + public static void main(String[] args) { + SpringApplication.run(App.class); + } + + @PostConstruct + public void init() { + for(int i = 0 ; i < 1000; ++i) { + repository.save(new Person("Ivan")); + repository.save(new Person("Maria")); + } + } +} diff --git a/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/domain/Person.java b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/domain/Person.java new file mode 100644 index 00000000..05922c3b --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/domain/Person.java @@ -0,0 +1,38 @@ +package ru.otus.spring.microservice.domain; + + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Person { + + @Id + @GeneratedValue + private int id; + private String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java new file mode 100644 index 00000000..ef89fee3 --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java @@ -0,0 +1,19 @@ +package ru.otus.spring.microservice.repostory; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; + +import ru.otus.spring.microservice.domain.Person; + +@RepositoryRestResource(path = "person") +public interface PersonRepository extends CrudRepository { + + @Override + List findAll(); + + @RestResource(path = "names") + List findByName(String name); +} diff --git a/2025-11/spring-31-data-rest/spring-31-exercise/src/main/resources/application.yml b/2025-11/spring-31-data-rest/spring-31-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b diff --git a/2025-11/spring-31-data-rest/spring-31-solution/pom.xml b/2025-11/spring-31-data-rest/spring-31-solution/pom.xml new file mode 100644 index 00000000..aeee948b --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + ru.otus + spring-31-solution + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-data-rest + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + org.springframework.data + spring-data-rest-hal-explorer + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/App.java b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/App.java new file mode 100644 index 00000000..126c5eec --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/App.java @@ -0,0 +1,31 @@ +package ru.otus.spring.microservice; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import jakarta.annotation.PostConstruct; +import ru.otus.spring.microservice.domain.Person; +import ru.otus.spring.microservice.repostory.PersonRepository; + + +@SpringBootApplication +@EnableWebMvc +public class App { + + @Autowired + private PersonRepository repository; + + public static void main(String[] args) { + SpringApplication.run(App.class); + } + + @PostConstruct + public void init() { + for(int i = 0 ; i < 1000; ++i) { + repository.save(new Person("Ivan")); + repository.save(new Person("Maria")); + } + } +} diff --git a/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/actuators/RandomHealthIndicator.java b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/actuators/RandomHealthIndicator.java new file mode 100644 index 00000000..53bc81a1 --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/actuators/RandomHealthIndicator.java @@ -0,0 +1,27 @@ +package ru.otus.spring.microservice.actuators; + +import java.util.Random; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.stereotype.Component; + +@Component +public class RandomHealthIndicator implements HealthIndicator { + + private final Random random = new Random(); + + @Override + public Health health() { + boolean serverIsDown = random.nextBoolean(); + if (serverIsDown) { + return Health.down() + .status(Status.DOWN) + .withDetail("message", "Караул!") + .build(); + } else { + return Health.up().withDetail("message", "Ура, товарищи!").build(); + } + } +} diff --git a/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/domain/Person.java b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/domain/Person.java new file mode 100644 index 00000000..05922c3b --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/domain/Person.java @@ -0,0 +1,38 @@ +package ru.otus.spring.microservice.domain; + + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Person { + + @Id + @GeneratedValue + private int id; + private String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java new file mode 100644 index 00000000..80888701 --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/src/main/java/ru/otus/spring/microservice/repostory/PersonRepository.java @@ -0,0 +1,19 @@ +package ru.otus.spring.microservice.repostory; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; + +import ru.otus.spring.microservice.domain.Person; + +@RepositoryRestResource(path = "person") +public interface PersonRepository extends CrudRepository { + + @Override + List findAll(); + + @RestResource(path = "names", rel = "names") + List findByName(String name); +} diff --git a/2025-11/spring-31-data-rest/spring-31-solution/src/main/resources/application.yml b/2025-11/spring-31-data-rest/spring-31-solution/src/main/resources/application.yml new file mode 100644 index 00000000..fae6e3e7 --- /dev/null +++ b/2025-11/spring-31-data-rest/spring-31-solution/src/main/resources/application.yml @@ -0,0 +1,14 @@ +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + health: + defaults: + enabled: true +spring: + jmx: + enabled: true \ No newline at end of file diff --git a/2025-11/spring-32-http-client/pom.xml b/2025-11/spring-32-http-client/pom.xml new file mode 100644 index 00000000..51c37b83 --- /dev/null +++ b/2025-11/spring-32-http-client/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + ru.otus + spring-32-http-client + 1.0 + + pom + + + org.springframework.boot + spring-boot-starter-parent + 3.5.2 + + + + + rest-template + soap-client + soap-server + + + + 17 + 17 + UTF-8 + UTF-8 + 3.1.2 + 10.9.1 + https://raw.githubusercontent.com/OtusTeam/Spring/master/checkstyle.xml + + + diff --git a/2025-11/spring-32-http-client/rest-template/pom.xml b/2025-11/spring-32-http-client/rest-template/pom.xml new file mode 100644 index 00000000..311b446e --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + rest-template + 1.0-SNAPSHOT + + + ru.otus + spring-32-http-client + 1.0 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework.retry + spring-retry + + + org.aspectj + aspectjweaver + + + + org.springframework + spring-webflux + + + io.projectreactor.netty + reactor-netty + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.projectlombok + lombok + provided + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2025.0.0 + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/Main.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..c51fb9cd --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,21 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.retry.annotation.EnableRetry; +import ru.otus.spring.config.ClientProperties; + +@EnableCaching +@EnableRetry +@EnableFeignClients +@EnableConfigurationProperties(ClientProperties.class) +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/CacheConfig.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/CacheConfig.java new file mode 100644 index 00000000..cd39f893 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/CacheConfig.java @@ -0,0 +1,15 @@ +package ru.otus.spring.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("countries"); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientConfiguration.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientConfiguration.java new file mode 100644 index 00000000..d7b6a412 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientConfiguration.java @@ -0,0 +1,12 @@ +package ru.otus.spring.config; + +import feign.RequestInterceptor; +import org.springframework.context.annotation.Bean; + +public class ClientConfiguration { + + @Bean + public RequestInterceptor requestInterceptor(ClientProperties properties) { + return requestTemplate -> requestTemplate.query("access_key", properties.getKey()); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientProperties.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientProperties.java new file mode 100644 index 00000000..a683ff88 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/config/ClientProperties.java @@ -0,0 +1,13 @@ +package ru.otus.spring.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "client") +public class ClientProperties { + private String url; + private String key; +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/dto/Country.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/dto/Country.java new file mode 100644 index 00000000..5d0f66e4 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/dto/Country.java @@ -0,0 +1,19 @@ +package ru.otus.spring.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@NoArgsConstructor +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class Country { + private String name; + // private String capital; +// private String region; + private String alpha3Code; +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/CountryService.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/CountryService.java new file mode 100644 index 00000000..dbaa7878 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/CountryService.java @@ -0,0 +1,12 @@ +package ru.otus.spring.service; + +import ru.otus.spring.dto.Country; + +import java.util.List; + +public interface CountryService { + + Country findByCode(String id); + + List getAll(); +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/FeignCountryService.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/FeignCountryService.java new file mode 100644 index 00000000..2a87c1e7 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/FeignCountryService.java @@ -0,0 +1,25 @@ +package ru.otus.spring.service; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import ru.otus.spring.config.ClientConfiguration; +import ru.otus.spring.dto.Country; + +import java.util.List; + +@FeignClient(value = "countrylayer", configuration = ClientConfiguration.class) +@Cacheable("countries") +@Retryable(retryFor = Exception.class, maxAttempts = 2, backoff = @Backoff(delay = 2000)) +public interface FeignCountryService { + @RequestMapping(method = RequestMethod.GET, value = "/alpha/{code}", produces = "application/json") + Country findByCode(@PathVariable("code") String code); + + @RequestMapping(method = RequestMethod.GET, value = "/all", produces = "application/json") + List getAll(); + +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/MainServiceImpl.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/MainServiceImpl.java new file mode 100644 index 00000000..9ae99afb --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/MainServiceImpl.java @@ -0,0 +1,29 @@ +package ru.otus.spring.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Service; +import ru.otus.spring.dto.Country; + +@Slf4j +@RequiredArgsConstructor +@Service +public class MainServiceImpl implements CommandLineRunner { + // private final TemplateCountryService countryService; +// private final WebCountryService countryService; +// private final RestCountryService countryService; + private final FeignCountryService countryService; + + @Override + public void run(String... args) { +// List countries = countryService.getAll(); +// log.info("Countries: {}", countries); + Country country = countryService.findByCode("col"); + log.info("Find {}", country); +// country = countryService.findByCode("col"); +// log.info("Find {}", country); +// country = countryService.findByCode("rus"); +// log.info("Find {}", country); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/RestCountryService.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/RestCountryService.java new file mode 100644 index 00000000..c4792623 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/RestCountryService.java @@ -0,0 +1,56 @@ +package ru.otus.spring.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import ru.otus.spring.config.ClientProperties; +import ru.otus.spring.dto.Country; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class RestCountryService implements CountryService { + final ClientProperties properties; + + final RestClient restClient; + + public RestCountryService(ClientProperties properties) { + this.properties = properties; + this.restClient = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()) +// .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) + .baseUrl(properties.getUrl()) + .defaultUriVariables(Map.of("key", properties.getKey())) +// .defaultHeader("Header", "") +// .requestInterceptor(myCustomInterceptor) +// .requestInitializer(myCustomInitializer) + .build(); + } + + @Override + public Country findByCode(String id) { + log.info("Rest client Request findByCode"); + return restClient.get() + .uri(uriBuilder -> uriBuilder.path("/alpha/{id}") + .queryParam("access_key", properties.getKey()) + .build(Map.of("id", id))) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .body(Country.class); + } + + @Override + public List getAll() { + log.info("Web client Request findByCode"); + return restClient.get() + .uri("/all?access_key={key}") + .retrieve() + .body(new ParameterizedTypeReference<>() { + }); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/TemplateCountryService.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/TemplateCountryService.java new file mode 100644 index 00000000..34b0a482 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/TemplateCountryService.java @@ -0,0 +1,48 @@ +package ru.otus.spring.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import ru.otus.spring.config.ClientProperties; +import ru.otus.spring.dto.Country; + +import java.net.URI; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class TemplateCountryService implements CountryService { + final ClientProperties properties; + + private final RestOperations rest = new RestTemplate(); + + @Override + public Country findByCode(String id) { + log.info("RestTemplate Request findByCode"); + return rest.getForObject("http://api.countrylayer.com/v2/alpha/" + id + "?access_key=" + properties.getKey(), + Country.class); + } + + @Override + public List getAll() { + log.info("RestTemplate Request getAll"); + URI uri = UriComponentsBuilder.fromHttpUrl(properties.getUrl()) + .path("/all") + .queryParam("access_key", properties.getKey()) + .build().toUri(); + RequestEntity> requestEntity = new RequestEntity<>(HttpMethod.GET, + uri); +// List countries = rest.getForObject(uri, List.class); + ResponseEntity> exchange = rest.exchange(requestEntity, new ParameterizedTypeReference<>() { + }); + return exchange.getBody(); + } +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/WebCountryService.java b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/WebCountryService.java new file mode 100644 index 00000000..891a74fd --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/java/ru/otus/spring/service/WebCountryService.java @@ -0,0 +1,66 @@ +package ru.otus.spring.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import ru.otus.spring.config.ClientProperties; +import ru.otus.spring.dto.Country; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class WebCountryService implements CountryService { + final ClientProperties properties; + + final WebClient webClient; + + public WebCountryService(ClientProperties properties) { + this.properties = properties; + this.webClient = WebClient.builder() + .baseUrl(properties.getUrl()) + .defaultUriVariables(Map.of("key", properties.getKey())) + .filter(errorHandler()) + .build(); + } + + @Override + public Country findByCode(String id) { + log.info("Web client Request findByCode"); + return webClient.get() + .uri("/alpha/{id}?access_key={key}", Map.of("id", id)) + .retrieve() + .bodyToMono(Country.class) + .block(); + } + + @Override + public List getAll() { + log.info("Web client Request findByCode"); + return webClient.get() + .uri("/all?access_key={key}") + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(); + } + + private static ExchangeFilterFunction errorHandler() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if (clientResponse.statusCode().is5xxServerError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new RuntimeException(errorBody))); + } else if (clientResponse.statusCode().is4xxClientError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new RuntimeException(errorBody))); + } else { + return Mono.just(clientResponse); + } + }); + } + +} diff --git a/2025-11/spring-32-http-client/rest-template/src/main/resources/application.yml b/2025-11/spring-32-http-client/rest-template/src/main/resources/application.yml new file mode 100644 index 00000000..4ac51230 --- /dev/null +++ b/2025-11/spring-32-http-client/rest-template/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring.main.web-application-type: none + +spring: + cloud: + openfeign: + client: + config: + countrylayer: + url: http://api.countrylayer.com/v2 + +client: + url: http://api.countrylayer.com/v2 + key: [ !!!Your key!!! ] + +logging: + level: + org: + springframework: + web: debug diff --git a/2025-11/spring-32-http-client/soap-client/pom.xml b/2025-11/spring-32-http-client/soap-client/pom.xml new file mode 100644 index 00000000..dc7feaa7 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + soap-client + 1.0 + + + ru.otus + spring-32-http-client + 1.0 + + + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.sun.xml.ws + jaxws-maven-plugin + 3.0.0 + + + + wsimport + + + + + hello.wsdl + + src/main/resources/countries.wsdl + + + true + + + + + + diff --git a/2025-11/spring-32-http-client/soap-client/src/main/java/hello/ClientApp.java b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/ClientApp.java new file mode 100644 index 00000000..185dff2d --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/ClientApp.java @@ -0,0 +1,39 @@ +package hello; + +import hello.wsdl.Country; +import hello.wsdl.GetCountryResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@Slf4j +@SpringBootApplication +public class ClientApp { + + public static void main(String[] args) { + SpringApplication.run(ClientApp.class, args); + } + + @Bean + CommandLineRunner lookup(CountryClient quoteClient) { + return args -> { + String country = "Spain"; + + if (args.length > 0) { + country = args[0]; + } + GetCountryResponse response = quoteClient.getCountry(country); + if (response.getCountry() != null) { + Country countryDetails = response.getCountry(); + log.warn("GetCountry {} : Country[name={}, population={}, capital={}, currency={}]", + country, countryDetails.getName(), countryDetails.getPopulation(), + countryDetails.getCapital(), countryDetails.getCurrency()); + } else { + log.warn("GetCountry {} : no country found in response", country); + } + }; + } + +} diff --git a/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryClient.java b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryClient.java new file mode 100644 index 00000000..eec56348 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryClient.java @@ -0,0 +1,24 @@ +package hello; + +import hello.wsdl.GetCountryRequest; +import hello.wsdl.GetCountryResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ws.client.core.support.WebServiceGatewaySupport; + +@Slf4j +public class CountryClient extends WebServiceGatewaySupport { + + public GetCountryResponse getCountry(String country) { + + GetCountryRequest request = new GetCountryRequest(); + request.setName(country); + + log.info("Requesting location for {}", country); + + GetCountryResponse response = (GetCountryResponse) getWebServiceTemplate() + .marshalSendAndReceive("http://localhost:8080/ws", request); + + return response; + } + +} diff --git a/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryConfiguration.java b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryConfiguration.java new file mode 100644 index 00000000..edd3a4a4 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/src/main/java/hello/CountryConfiguration.java @@ -0,0 +1,28 @@ +package hello; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; + +@Configuration +public class CountryConfiguration { + + @Bean + public Jaxb2Marshaller marshaller() { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + // this package must match the package in the specified in + // pom.xml + marshaller.setContextPath("hello.wsdl"); + return marshaller; + } + + @Bean + public CountryClient countryClient(Jaxb2Marshaller marshaller) { + CountryClient client = new CountryClient(); + client.setDefaultUri("http://localhost:8080/ws"); + client.setMarshaller(marshaller); + client.setUnmarshaller(marshaller); + return client; + } + +} diff --git a/2025-11/spring-32-http-client/soap-client/src/main/resources/application.yml b/2025-11/spring-32-http-client/soap-client/src/main/resources/application.yml new file mode 100644 index 00000000..1aa84da6 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/src/main/resources/application.yml @@ -0,0 +1,6 @@ +logging: + level: + hello: INFO + org: + springframework: + ws: DEBUG diff --git a/2025-11/spring-32-http-client/soap-client/src/main/resources/countries.wsdl b/2025-11/spring-32-http-client/soap-client/src/main/resources/countries.wsdl new file mode 100644 index 00000000..061e18f5 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-client/src/main/resources/countries.wsdl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2025-11/spring-32-http-client/soap-server/pom.xml b/2025-11/spring-32-http-client/soap-server/pom.xml new file mode 100644 index 00000000..7b02d5b8 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + soap-server + 1.0 + + + ru.otus + spring-32-http-client + 1.0 + + + + + + org.springframework.boot + spring-boot-starter-web-services + + + wsdl4j + wsdl4j + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.codehaus.mojo + jaxb2-maven-plugin + 3.1.0 + + + xjc + + xjc + + + + + + ${project.basedir}/src/main/resources/countries.xsd + + + ru.otus.hello.web_service + + + + + + diff --git a/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryEndpoint.java b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryEndpoint.java new file mode 100644 index 00000000..b0c6f9f7 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryEndpoint.java @@ -0,0 +1,30 @@ +package hello; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ws.server.endpoint.annotation.Endpoint; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; +import org.springframework.ws.server.endpoint.annotation.RequestPayload; +import org.springframework.ws.server.endpoint.annotation.ResponsePayload; +import ru.otus.hello.web_service.GetCountryRequest; +import ru.otus.hello.web_service.GetCountryResponse; + +@Endpoint +public class CountryEndpoint { + private static final String NAMESPACE_URI = "http://otus.ru/hello/web-service"; + + private final CountryRepository countryRepository; + + @Autowired + public CountryEndpoint(CountryRepository countryRepository) { + this.countryRepository = countryRepository; + } + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getCountryRequest") + @ResponsePayload + public GetCountryResponse getCountry(@RequestPayload GetCountryRequest request) { + GetCountryResponse response = new GetCountryResponse(); + response.setCountry(countryRepository.findCountry(request.getName())); + + return response; + } +} diff --git a/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryRepository.java b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryRepository.java new file mode 100644 index 00000000..be6d81dc --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/CountryRepository.java @@ -0,0 +1,47 @@ +package hello; + +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import ru.otus.hello.web_service.Country; +import ru.otus.hello.web_service.Currency; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class CountryRepository { + private static final Map countries = new HashMap<>(); + + @PostConstruct + public void initData() { + Country spain = new Country(); + spain.setName("Spain"); + spain.setCapital("Madrid"); + spain.setCurrency(Currency.EUR); + spain.setPopulation(46704314); + + countries.put(spain.getName(), spain); + + Country poland = new Country(); + poland.setName("Poland"); + poland.setCapital("Warsaw"); + poland.setCurrency(Currency.PLN); + poland.setPopulation(38186860); + + countries.put(poland.getName(), poland); + + Country uk = new Country(); + uk.setName("United Kingdom"); + uk.setCapital("London"); + uk.setCurrency(Currency.GBP); + uk.setPopulation(63705000); + + countries.put(uk.getName(), uk); + } + + public Country findCountry(String name) { + Assert.notNull(name, "The country's name must not be null"); + return countries.get(name); + } +} diff --git a/2025-11/spring-32-http-client/soap-server/src/main/java/hello/ServerApp.java b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/ServerApp.java new file mode 100644 index 00000000..489836fb --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/ServerApp.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ServerApp { + + public static void main(String[] args) { + SpringApplication.run(ServerApp.class, args); + } +} diff --git a/2025-11/spring-32-http-client/soap-server/src/main/java/hello/WebServiceConfig.java b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/WebServiceConfig.java new file mode 100644 index 00000000..79455f80 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/java/hello/WebServiceConfig.java @@ -0,0 +1,40 @@ +package hello; + +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.ws.config.annotation.EnableWs; +import org.springframework.ws.config.annotation.WsConfigurer; +import org.springframework.ws.transport.http.MessageDispatcherServlet; +import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition; +import org.springframework.xml.xsd.SimpleXsdSchema; +import org.springframework.xml.xsd.XsdSchema; + +@EnableWs +@Configuration +public class WebServiceConfig implements WsConfigurer { + @Bean + public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) { + MessageDispatcherServlet servlet = new MessageDispatcherServlet(); + servlet.setApplicationContext(applicationContext); + servlet.setTransformWsdlLocations(true); + return new ServletRegistrationBean<>(servlet, "/ws/*"); + } + + @Bean(name = "countries") + public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) { + DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition(); + wsdl11Definition.setPortTypeName("CountriesPort"); + wsdl11Definition.setLocationUri("/ws"); + wsdl11Definition.setTargetNamespace("http://otus.ru/hello/web-service"); + wsdl11Definition.setSchema(countriesSchema); + return wsdl11Definition; + } + + @Bean + public XsdSchema countriesSchema() { + return new SimpleXsdSchema(new ClassPathResource("countries.xsd")); + } +} diff --git a/2025-11/spring-32-http-client/soap-server/src/main/resources/application.yml b/2025-11/spring-32-http-client/soap-server/src/main/resources/application.yml new file mode 100644 index 00000000..4b4d02dc --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/resources/application.yml @@ -0,0 +1,3 @@ +logging: + level: + root: debug \ No newline at end of file diff --git a/2025-11/spring-32-http-client/soap-server/src/main/resources/countries.xsd b/2025-11/spring-32-http-client/soap-server/src/main/resources/countries.xsd new file mode 100644 index 00000000..806a4136 --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/main/resources/countries.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2025-11/spring-32-http-client/soap-server/src/test/java/hello/ApplicationIntegrationTests.java b/2025-11/spring-32-http-client/soap-server/src/test/java/hello/ApplicationIntegrationTests.java new file mode 100644 index 00000000..0bc377ea --- /dev/null +++ b/2025-11/spring-32-http-client/soap-server/src/test/java/hello/ApplicationIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2015 the original author or 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 + * + * http://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. + */ + +package hello; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.util.ClassUtils; +import org.springframework.ws.client.core.WebServiceTemplate; +import ru.otus.hello.web_service.GetCountryRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class ApplicationIntegrationTests { + + private final Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + + @LocalServerPort + private int port = 0; + + @BeforeEach + public void init() throws Exception { + marshaller.setPackagesToScan(ClassUtils.getPackageName(GetCountryRequest.class)); + marshaller.afterPropertiesSet(); + } + + @Test + public void testSendAndReceive() { + WebServiceTemplate ws = new WebServiceTemplate(marshaller); + GetCountryRequest request = new GetCountryRequest(); + request.setName("Spain"); + + assertThat(ws.marshalSendAndReceive("http://localhost:" + port + "/ws", request)).isNotNull(); + } +} \ No newline at end of file diff --git a/2025-11/spring-33-docker/docker-compose-example/.gitignore b/2025-11/spring-33-docker/docker-compose-example/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..2cc7d4a5 Binary files /dev/null and b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.jar differ diff --git a/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-11/spring-33-docker/docker-compose-example/Dockerfile b/2025-11/spring-33-docker/docker-compose-example/Dockerfile new file mode 100644 index 00000000..c7a02fbc --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/Dockerfile @@ -0,0 +1,4 @@ +FROM bellsoft/liberica-openjdk-alpine-musl:21.0.1 +COPY /target/docker-compose-example.jar /app/app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/2025-11/spring-33-docker/docker-compose-example/docker-compose.yml b/2025-11/spring-33-docker/docker-compose-example/docker-compose.yml new file mode 100644 index 00000000..94916a8e --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/docker-compose.yml @@ -0,0 +1,21 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + # Эти свойства перегружают соответствующие в application.yml + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/db + - SPRING_DATASOURCE_USERNAME=postgres + - SPRING_DATASOURCE_PASSWORD=postgres + postgres: + image: "postgres:17" + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=db + diff --git a/2025-11/spring-33-docker/docker-compose-example/mvnw b/2025-11/spring-33-docker/docker-compose-example/mvnw new file mode 100644 index 00000000..a16b5431 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-11/spring-33-docker/docker-compose-example/mvnw.cmd b/2025-11/spring-33-docker/docker-compose-example/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-11/spring-33-docker/docker-compose-example/pom.xml b/2025-11/spring-33-docker/docker-compose-example/pom.xml new file mode 100644 index 00000000..71423fd4 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + ru.otus.spring + docker-compose-example + 0.0.1-SNAPSHOT + docker-compose-example + Demo project for Spring Boot + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + test + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + docker-compose-example + + diff --git a/2025-11/spring-33-docker/docker-compose-example/readme.txt b/2025-11/spring-33-docker/docker-compose-example/readme.txt new file mode 100644 index 00000000..964c9576 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/readme.txt @@ -0,0 +1,3 @@ +./mvnw package +docker compose up -d +curl http://localhost:8080/api/persons \ No newline at end of file diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java new file mode 100644 index 00000000..76f60e7d --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/DockerComposeExampleApplication.java @@ -0,0 +1,22 @@ +package ru.otus.spring.docker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import ru.otus.spring.docker.model.Person; +import ru.otus.spring.docker.repository.PersonRepository; + +@SpringBootApplication +@EnableJpaRepositories +public class DockerComposeExampleApplication { + + public static void main(String[] args) { + //Код для примера, делать так конечно нельзя :) + ApplicationContext context = SpringApplication.run(DockerComposeExampleApplication.class, args); + PersonRepository repository = context.getBean(PersonRepository.class); + repository.save(new Person("Ivan", "Ivanov")); + System.out.println(repository.findAll()); + } + +} diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java new file mode 100644 index 00000000..5335b097 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/model/Person.java @@ -0,0 +1,28 @@ +package ru.otus.spring.docker.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@Entity +@AllArgsConstructor +@NoArgsConstructor +public class Person { + + @GeneratedValue(strategy = GenerationType.AUTO) + @Id + private Integer id; + private String name; + private String lastName; + + public Person(String name, String lastName) { + this.name = name; + this.lastName = lastName; + } +} diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java new file mode 100644 index 00000000..11262d78 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/repository/PersonRepository.java @@ -0,0 +1,7 @@ +package ru.otus.spring.docker.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.otus.spring.docker.model.Person; + +public interface PersonRepository extends JpaRepository { +} diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java new file mode 100644 index 00000000..4cfc6436 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/java/ru/otus/spring/docker/rest/PersonController.java @@ -0,0 +1,21 @@ +package ru.otus.spring.docker.rest; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.otus.spring.docker.model.Person; +import ru.otus.spring.docker.repository.PersonRepository; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/api/persons") + public List getAllPersons() { + return this.repository.findAll(); + } +} diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml new file mode 100644 index 00000000..bf91784a --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + datasource: + # Эти свойства будут перегружены свойствами в docker-compose.yml + url: jdbc:postgresql://localhost:5432/db + username: postgres + password: postgres + jpa: + generate-ddl: true diff --git a/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html new file mode 100644 index 00000000..a80ea187 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + + Главная страницв + + +

Главная страница

+

Список всех лиц доступен по ссылке.

+

Перезапустив приложение можно добавить ещё в БД.

+ + diff --git a/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java b/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java new file mode 100644 index 00000000..93fe13c4 --- /dev/null +++ b/2025-11/spring-33-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java @@ -0,0 +1,15 @@ +package ru.otus.spring.docker; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@AutoConfigureTestDatabase +class DockerComposeExampleApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/2025-11/spring-33-docker/helloWorld.txt b/2025-11/spring-33-docker/helloWorld.txt new file mode 100644 index 00000000..f2c3689e --- /dev/null +++ b/2025-11/spring-33-docker/helloWorld.txt @@ -0,0 +1,20 @@ +docker image ls +docker pull hello-world +docker image ls +docker image rm hello-world +docker ps +docker run hello-world +docker ps +docker run hello-world +docker ps -all +docker run --rm hello-world + +docker run -it --name=ubuntu-run ubuntu bash +docker start ubuntu-run +docker exec -it ubuntu-run bash + +docker run -d -p:8080:80 --name=my-nginx nginx +docker ps -a +curl http://localhost:8080 +docker kill my-nginx +docker rm my-nginx diff --git a/2025-11/spring-33-docker/image/Dockerfile b/2025-11/spring-33-docker/image/Dockerfile new file mode 100644 index 00000000..c51e4bde --- /dev/null +++ b/2025-11/spring-33-docker/image/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.11-alpine +COPY index.html /usr/share/nginx/html/index.html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/2025-11/spring-33-docker/image/index.html b/2025-11/spring-33-docker/image/index.html new file mode 100644 index 00000000..f3e333e8 --- /dev/null +++ b/2025-11/spring-33-docker/image/index.html @@ -0,0 +1 @@ +

Hello World

diff --git a/2025-11/spring-33-docker/image/readme.txt b/2025-11/spring-33-docker/image/readme.txt new file mode 100644 index 00000000..9dde7083 --- /dev/null +++ b/2025-11/spring-33-docker/image/readme.txt @@ -0,0 +1,5 @@ +docker build -t my-demo-image:v1 . +docker images | grep my-demo-image +docker run -p:80:80 --rm my-demo-image:v1 +docker ps +curl http://localhost \ No newline at end of file diff --git a/2025-11/spring-34-kuber/.gitignore b/2025-11/spring-34-kuber/.gitignore new file mode 100755 index 00000000..dae2cf99 --- /dev/null +++ b/2025-11/spring-34-kuber/.gitignore @@ -0,0 +1,39 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle +/buildSrc/.gradle/ + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-11/spring-34-kuber/HttpRequests.http b/2025-11/spring-34-kuber/HttpRequests.http new file mode 100755 index 00000000..29913ccf --- /dev/null +++ b/2025-11/spring-34-kuber/HttpRequests.http @@ -0,0 +1,34 @@ +### +GET http://localhost:8080/hi?name=Ivan +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +POST http://localhost:8080/response/Ivan +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +{ + "param1": "pa1", + "param2": "pa2" +} + +### +GET http://localhost:8090/actuator/health +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8090/actuator/health/liveness +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8090/actuator/health/readiness +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-11/spring-34-kuber/LICENSE b/2025-11/spring-34-kuber/LICENSE new file mode 100755 index 00000000..ada0a70c --- /dev/null +++ b/2025-11/spring-34-kuber/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Sergey Petrelevich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2025-11/spring-34-kuber/README.md b/2025-11/spring-34-kuber/README.md new file mode 100755 index 00000000..abe57084 --- /dev/null +++ b/2025-11/spring-34-kuber/README.md @@ -0,0 +1 @@ +# gitlab Hello \ No newline at end of file diff --git a/2025-11/spring-34-kuber/build.gradle.kts b/2025-11/spring-34-kuber/build.gradle.kts new file mode 100755 index 00000000..dff4344e --- /dev/null +++ b/2025-11/spring-34-kuber/build.gradle.kts @@ -0,0 +1,116 @@ +import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension +import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES +import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel +import org.gradle.api.plugins.JavaPluginExtension +import fr.brouillard.oss.gradle.plugins.JGitverPluginExtension +import fr.brouillard.oss.gradle.plugins.JGitverPlugin +import name.remal.gradle_plugins.sonarlint.SonarLintExtension + +plugins { + idea + id("fr.brouillard.oss.gradle.jgitver") + id("io.spring.dependency-management") + id("org.springframework.boot") apply false + id("name.remal.sonarlint") apply false + id("com.diffplug.spotless") apply false +} + + +idea { + project { + languageLevel = IdeaLanguageLevel(21) + } + module { + isDownloadJavadoc = true + isDownloadSources = true + } +} + + +allprojects { + group = "ru.petrelevich" + + repositories { + mavenLocal() + mavenCentral() + } + + apply(plugin = "io.spring.dependency-management") + dependencyManagement { + dependencies { + imports { + mavenBom(BOM_COORDINATES) + } + } + } + + configurations.all { + resolutionStrategy { + failOnVersionConflict() + + force("commons-io:commons-io:2.16.1") + force("org.eclipse.jgit:org.eclipse.jgit:6.9.0.202403050737-r") + force("org.apache.commons:commons-compress:1.26.1") + force("com.google.errorprone:error_prone_annotations:2.28.0") + force("org.jetbrains:annotations:19.0.0") + } + } + + tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:all,-serial,-processing", "-Werror")) + } + + apply() + configure { + nodeJs { + detectNodeJs = false + logNodeJsNotFound = false + } + } + + apply() + configure { + java { + palantirJavaFormat("2.50.0") + } + } + + tasks.withType { + useJUnitPlatform() + testLogging.showExceptions = true + reports { + junitXml.required.set(true) + html.required.set(true) + } + } + + plugins.apply(JavaPlugin::class.java) + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + plugins.apply(JGitverPlugin::class.java) + extensions.configure { + strategy("PATTERN") + nonQualifierBranches("main,master") + tagVersionPattern("\${v}\${() + .managedVersions + .toSortedMap() + .map { "${it.key}:${it.value}" } + .forEach(::println) + } + } +} \ No newline at end of file diff --git a/2025-11/spring-34-kuber/gradle.properties b/2025-11/spring-34-kuber/gradle.properties new file mode 100755 index 00000000..efdab048 --- /dev/null +++ b/2025-11/spring-34-kuber/gradle.properties @@ -0,0 +1,12 @@ +# -------Gradle-------- +org.gradle.jvmargs=-Xmx4g +org.gradle.daemon=true +org.gradle.parallel=true +# -------Plugins--------- +jgitver=0.10.0-rc03 +dependencyManagement=1.1.6 +springframeworkBoot=3.4.0 +jib=3.4.4 +sonarlint=4.3.1 +spotless=6.25.0 +# -------Versions-------- diff --git a/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..7f93135c Binary files /dev/null and b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.jar differ diff --git a/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..e2847c82 --- /dev/null +++ b/2025-11/spring-34-kuber/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/2025-11/spring-34-kuber/gradlew b/2025-11/spring-34-kuber/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/2025-11/spring-34-kuber/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/2025-11/spring-34-kuber/gradlew.bat b/2025-11/spring-34-kuber/gradlew.bat new file mode 100755 index 00000000..93e3f59f --- /dev/null +++ b/2025-11/spring-34-kuber/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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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/2025-11/spring-34-kuber/kube/config.yaml b/2025-11/spring-34-kuber/kube/config.yaml new file mode 100755 index 00000000..01378922 --- /dev/null +++ b/2025-11/spring-34-kuber/kube/config.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rest-service-config +data: + JAVA_TOOL_OPTIONS: -XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80 \ No newline at end of file diff --git a/2025-11/spring-34-kuber/kube/deployment.yaml b/2025-11/spring-34-kuber/kube/deployment.yaml new file mode 100755 index 00000000..8bbb1d80 --- /dev/null +++ b/2025-11/spring-34-kuber/kube/deployment.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rest-hello-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: rest-hello + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: rest-hello + spec: + containers: + - image: registry.gitlab.com/petrelevich/dockerregistry/rest-hello:2.0.2-1.2d40be46.dirty-SNAPSHOT + name: rest-hello + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: rest-service-config + resources: + requests: + memory: "256M" + limits: + memory: "256M" + readinessProbe: + failureThreshold: 3 + httpGet: + path: /actuator/health/readiness + port: 8090 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /actuator/health/liveness + port: 8090 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + initialDelaySeconds: 10 + imagePullSecrets: + - name: regcred diff --git a/2025-11/spring-34-kuber/kube/ingress.yaml b/2025-11/spring-34-kuber/kube/ingress.yaml new file mode 100755 index 00000000..ea92a13f --- /dev/null +++ b/2025-11/spring-34-kuber/kube/ingress.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rest-hello +spec: + ingressClassName: traefik + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: rest-hello + port: + number: 80 diff --git a/2025-11/spring-34-kuber/kube/service.yaml b/2025-11/spring-34-kuber/kube/service.yaml new file mode 100755 index 00000000..21b1add5 --- /dev/null +++ b/2025-11/spring-34-kuber/kube/service.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: rest-hello +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: rest-hello + type: ClusterIP diff --git a/2025-11/spring-34-kuber/rest-hello/build.gradle.kts b/2025-11/spring-34-kuber/rest-hello/build.gradle.kts new file mode 100755 index 00000000..4c77e8c9 --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("java") + id("org.springframework.boot") + id("com.google.cloud.tools.jib") + id("fr.brouillard.oss.gradle.jgitver") +} + + +apply(plugin = "com.google.cloud.tools.jib") + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") +} + +tasks { + jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + to { + tags = setOf(project.version.toString()) + image = "registry.gitlab.com/petrelevich/dockerregistry/${project.name}" + auth { + username = System.getenv("GITLAB_USERNAME") + password = System.getenv("GITLAB_PASSWORD") + } + } + } +//docker login registry.gitlab.com +//docker run -p 8080:8080 registry.gitlab.com/petrelevich/dockerregistry/rest-hello + + build { + dependsOn(jib) + } +} \ No newline at end of file diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java new file mode 100755 index 00000000..5abbd89c --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/Application.java @@ -0,0 +1,23 @@ +package ru.petrelevich; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class Application { + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + com.sun.management.OperatingSystemMXBean os = (com.sun.management.OperatingSystemMXBean) + java.lang.management.ManagementFactory.getOperatingSystemMXBean(); + + logger.info("availableProcessors:{}", Runtime.getRuntime().availableProcessors()); + logger.info("TotalMemorySize, mb:{}", os.getTotalMemorySize() / 1024 / 1024); + logger.info("maxMemory, mb:{}", Runtime.getRuntime().maxMemory() / 1024 / 1024); + logger.info("freeMemory, mb:{}", Runtime.getRuntime().freeMemory() / 1024 / 1024); + + new SpringApplicationBuilder().sources(Application.class).run(args); + } +} diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java new file mode 100755 index 00000000..7c1d9701 --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/IndexController.java @@ -0,0 +1,25 @@ +package ru.petrelevich.controller; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IndexController { + + @GetMapping("/hi") + public String hi(@RequestParam(name = "name") String name) throws UnknownHostException { + return String.format( + "Hi, %s. It works, host: %s", name, InetAddress.getLocalHost().getHostName()); + } + + @PostMapping("/response/{name}") + public Response response(@PathVariable("name") String name, @RequestBody Request params) { + return new Response(name, String.format("%s-%s", params.param1(), params.param2())); + } +} diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java new file mode 100755 index 00000000..4aba59c4 --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Request.java @@ -0,0 +1,3 @@ +package ru.petrelevich.controller; + +public record Request(String param1, String param2) {} diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java new file mode 100755 index 00000000..91b27a4f --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/src/main/java/ru/petrelevich/controller/Response.java @@ -0,0 +1,3 @@ +package ru.petrelevich.controller; + +public record Response(String name, String result) {} diff --git a/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml b/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml new file mode 100755 index 00000000..bb9154dc --- /dev/null +++ b/2025-11/spring-34-kuber/rest-hello/src/main/resources/application.yml @@ -0,0 +1,10 @@ +management: + server: + port: 8090 + endpoints: + enabled-by-default: false + endpoint: + health: + enabled: true + probes: + enabled: true \ No newline at end of file diff --git a/2025-11/spring-34-kuber/settings.gradle.kts b/2025-11/spring-34-kuber/settings.gradle.kts new file mode 100755 index 00000000..a89f82f3 --- /dev/null +++ b/2025-11/spring-34-kuber/settings.gradle.kts @@ -0,0 +1,20 @@ +rootProject.name = "spring-kuber" +include ("rest-hello") + +pluginManagement { + val jgitver: String by settings + val dependencyManagement: String by settings + val springframeworkBoot: String by settings + val jib: String by settings + val sonarlint: String by settings + val spotless: String by settings + + plugins { + id("fr.brouillard.oss.gradle.jgitver") version jgitver + id("io.spring.dependency-management") version dependencyManagement + id("org.springframework.boot") version springframeworkBoot + id("com.google.cloud.tools.jib") version jib + id("name.remal.sonarlint") version sonarlint + id("com.diffplug.spotless") version spotless + } +} \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/.gitignore b/2025-11/spring-35-36-spring-cloud/.gitignore new file mode 100755 index 00000000..dae2cf99 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/.gitignore @@ -0,0 +1,39 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle +/buildSrc/.gradle/ + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-11/spring-35-36-spring-cloud/README.md b/2025-11/spring-35-36-spring-cloud/README.md new file mode 100755 index 00000000..387c879c --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/README.md @@ -0,0 +1 @@ +# Spring Cloud example diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts new file mode 100755 index 00000000..bd891347 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + id("com.google.cloud.tools.jib") +} + +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + implementation("ch.qos.logback:logback-classic") + + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.cloud:spring-cloud-starter-gateway") + implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. +} + +jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + + to { + image = "localrun/api-gateway" + tags = setOf(project.version.toString()) + } +} + +tasks { + build { + dependsOn(spotlessApply) + dependsOn(jibBuildTar) + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/hey b/2025-11/spring-35-36-spring-cloud/api-gateway/hey new file mode 100755 index 00000000..27aec446 Binary files /dev/null and b/2025-11/spring-35-36-spring-cloud/api-gateway/hey differ diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh b/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh new file mode 100755 index 00000000..45350fd6 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/runApiGateway.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +../gradlew :api-gateway:build + +docker load --input build/jib-image.tar + +docker stop api-gateway + +docker run --rm -d --name api-gateway \ +--memory=512m \ +--cpus 1 \ +--network="host" \ +-v $HOME/.ssh:/root/.ssh \ +-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \ +localrun/api-gateway:latest + diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh b/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh new file mode 100755 index 00000000..7a0a9b20 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/runLoad.sh @@ -0,0 +1,3 @@ +#!/bin/bash +./hey -n=1000000 -c=1 -m GET http://localhost:7777/client/info?name=testClientName + diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java new file mode 100755 index 00000000..6c3bc85a --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/ApiGateway.java @@ -0,0 +1,22 @@ +package ru.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiGateway { + private static final Logger log = LoggerFactory.getLogger(ApiGateway.class); + + /* + curl -v http://localhost:7777/client/info?name=testClientName + curl -v http://localhost:7777/order/info?id=testOrderId + + */ + + public static void main(String[] args) { + SpringApplication.run(ApiGateway.class, args); + log.info("SrvApiGateway started"); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java new file mode 100755 index 00000000..c9526422 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiConfig.java @@ -0,0 +1,34 @@ +package ru.demo.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.demo.controller.XrequestFilter; + +@Configuration +@EnableConfigurationProperties(ApplConfigProperties.class) +@EnableDiscoveryClient +public class ApiConfig { + + @Bean + public XrequestFilter xrequestFilter() { + return new XrequestFilter(); + } + + @Bean + RouteLocator gateway( + RouteLocatorBuilder rlb, ApplConfigProperties applConfigProperties, XrequestFilter xrequestFilter) { + var routesBuilder = rlb.routes(); + for (var route : applConfigProperties.getApiRoutes()) { + routesBuilder.route(route.id(), routeSpec -> routeSpec + .path(String.format("/%s/**", route.from())) + .filters(fs -> fs.filters(xrequestFilter) + .rewritePath(String.format("/%s/(?.*)", route.from()), "/${segment}")) + .uri(String.format("%s/@", route.to()))); + } + return routesBuilder.build(); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java new file mode 100755 index 00000000..85e84ccf --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApiRoute.java @@ -0,0 +1,3 @@ +package ru.demo.config; + +public record ApiRoute(String id, String from, String to) {} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java new file mode 100755 index 00000000..47cc8501 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/config/ApplConfigProperties.java @@ -0,0 +1,30 @@ +package ru.demo.config; + +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +@ConfigurationProperties(prefix = "application") +public class ApplConfigProperties { + private final String authHost; + private final List apiRoutes; + + @ConstructorBinding + public ApplConfigProperties(String authHost, List apiRoutes) { + this.authHost = authHost; + this.apiRoutes = apiRoutes; + } + + public String getAuthHost() { + return authHost; + } + + public List getApiRoutes() { + return apiRoutes; + } + + @Override + public String toString() { + return "ApplConfigProperties{" + "authHost='" + authHost + '\'' + ", apiRoutes=" + apiRoutes + '}'; + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java new file mode 100755 index 00000000..79f779be --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/ErrorHandler.java @@ -0,0 +1,38 @@ +package ru.demo.controller; + +import java.net.ConnectException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.util.annotation.NonNull; + +@Order(-2) +@Component +public class ErrorHandler implements ErrorWebExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class); + + @Override + public @NonNull Mono handle(@NonNull ServerWebExchange serverWebExchange, @NonNull Throwable thr) { + var bufferFactory = serverWebExchange.getResponse().bufferFactory(); + var response = serverWebExchange.getResponse(); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + if (thr instanceof ConnectException) { + log.error("Target host connection error", thr); + response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE); + var dataBuffer = bufferFactory.wrap("Target host connection error".getBytes()); + return response.writeWith(Mono.just(dataBuffer)); + } else { + log.error("Unhandled exception", thr); + response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + var dataBuffer = bufferFactory.wrap("Unhandled error".getBytes()); + return response.writeWith(Mono.just(dataBuffer)); + } + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java new file mode 100755 index 00000000..0b8a5176 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/java/ru/demo/controller/XrequestFilter.java @@ -0,0 +1,24 @@ +package ru.demo.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class XrequestFilter implements GatewayFilter { + private static final String HEADER_X_REQUEST_ID = "X-Request-Id"; + private static final Logger log = LoggerFactory.getLogger(XrequestFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + var guid = java.util.UUID.randomUUID().toString(); + log.info("requestId:{}", guid); + + var request = + exchange.getRequest().mutate().header(HEADER_X_REQUEST_ID, guid).build(); + + return chain.filter(exchange.mutate().request(request).build()); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml new file mode 100755 index 00000000..fdd7a1ff --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + application: + name: api-gateway + cloud: + config: + fail-fast: true + retry: + initial-interval: 5000 + max-attempts: 10 + max-interval: 5000 + multiplier: 1.2 + config: + import: optional:configserver:http://localhost:8888 + codec: + max-in-memory-size: 10MB \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml new file mode 100755 index 00000000..c20eb884 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/api-gateway/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"api-gateway"} + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/build.gradle.kts new file mode 100755 index 00000000..f9c9d78a --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/build.gradle.kts @@ -0,0 +1,121 @@ +import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension +import name.remal.gradle_plugins.sonarlint.SonarLintExtension +import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES +import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + idea + id("fr.brouillard.oss.gradle.jgitver") + id("io.spring.dependency-management") + id("org.springframework.boot") apply false + id("name.remal.sonarlint") apply false + id("com.diffplug.spotless") apply false +} + +allprojects { + group = "ru.demo" + + repositories { + mavenLocal() + mavenCentral() + } + + val springCloudVersion: String by project + val logbackEncoder: String by project + + apply(plugin = "io.spring.dependency-management") + dependencyManagement { + dependencies { + imports { + mavenBom(BOM_COORDINATES) + mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion") + } + dependency("net.logstash.logback:logstash-logback-encoder:$logbackEncoder") + } + } + + configurations.all { + resolutionStrategy { + failOnVersionConflict() + + force("org.glassfish.hk2.external:aopalliance-repackaged:3.1.1") + force("org.glassfish.hk2:hk2-utils:3.1.1") + force("commons-logging:commons-logging:1.3.1") + force("com.fasterxml.woodstox:woodstox-core:6.6.2") + force("org.glassfish.hk2:hk2-api:3.1.1") + force("org.apache.httpcomponents:httpclient:4.5.14") + force("org.sonarsource.analyzer-commons:sonar-analyzer-commons:2.11.0.2861") + force("org.sonarsource.analyzer-commons:sonar-xml-parsing:2.11.0.2861") + force("org.sonarsource.sslr:sslr-core:1.24.0.633") + force("org.sonarsource.analyzer-commons:sonar-analyzer-recognizers:2.11.0.2861") + force("commons-io:commons-io:2.15.1") + force("com.google.guava:guava:32.1.3-jre") + force("com.google.code.findbugs:jsr305:3.0.2") + force("org.codehaus.woodstox:stax2-api:4.2.2") + force("io.opentelemetry:opentelemetry-api-incubator:1.38.0-alpha") + } + } +} + +subprojects { + plugins.apply(SpringBootPlugin::class.java) + plugins.apply(JavaPlugin::class.java) + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:all,-serial,-processing")) + + dependsOn("spotlessApply") + } + + apply() + configure { + nodeJs { + detectNodeJs = false + logNodeJsNotFound = false + } + } + apply() + configure { + java { + palantirJavaFormat("2.38.0") + } + } + + plugins.apply(fr.brouillard.oss.gradle.plugins.JGitverPlugin::class.java) + extensions.configure { + strategy("PATTERN") + nonQualifierBranches("main,master") + tagVersionPattern("\${v}\${ { + useJUnitPlatform() + testLogging.showExceptions = true + reports { + junitXml.required.set(true) + html.required.set(true) + } + } +} + +tasks { + val managedVersions by registering { + doLast { + project.extensions.getByType() + .managedVersions + .toSortedMap() + .map { "${it.key}:${it.value}" } + .forEach(::println) + } + } +} \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts new file mode 100755 index 00000000..6d4b0399 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/config-server/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("com.google.cloud.tools.jib") +} + +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation ("org.springframework.cloud:spring-cloud-config-server") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. +} + +jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + + to { + image = "localrun/config-server" + tags = setOf(project.version.toString()) + } +} + +tasks { + build { + dependsOn(spotlessApply) + dependsOn(jibBuildTar) + } +} diff --git a/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh b/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh new file mode 100755 index 00000000..5cb30633 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/config-server/runConfigServer.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +../gradlew :config-server:build + +docker load --input build/jib-image.tar + +docker stop config-server + +docker run --rm -d --name config-server \ +--memory=1024m \ +--cpus 1 \ +--network="host" \ +-v $HOME/.ssh:/root/.ssh \ +-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \ +localrun/config-server:latest + diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java b/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java new file mode 100755 index 00000000..bbbb0a42 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/java/ru/demo/ConfigServer.java @@ -0,0 +1,19 @@ +package ru.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +@SpringBootApplication +@EnableConfigServer +public class ConfigServer { + private static final Logger log = LoggerFactory.getLogger(ConfigServer.class); + + public static void main(String[] args) { + + SpringApplication.run(ConfigServer.class, args); + log.info("ConfigServer started"); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml new file mode 100755 index 00000000..2c0a5866 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/application.yml @@ -0,0 +1,40 @@ +server: + port: 8888 + +spring: + application: + name: config-server + cloud: + config: + server: + git: + uri: "git@github.com:OtusTeam/Spring.git" + search-paths: "2025-11/spring-35-36-spring-cloud/git-config" + ignoreLocalSshSettings: false + strictHostKeyChecking: false + defaultLabel: "master" + +management: + tracing: + sampling: + probability: 1.0 + endpoint: + prometheus: + enabled: true + metrics: + enabled: true + health: + enabled: true + probes: + enabled: true + refresh: + enabled: true + endpoints: + web: + exposure: + include: + - prometheus + - health + - metrics + - refresh + enabled-by-default: false diff --git a/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml new file mode 100755 index 00000000..85e2f041 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/config-server/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"config-server"} + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json b/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json new file mode 100644 index 00000000..b0f42ea7 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/elk/JVM (Micrometer)-1733760120676.json @@ -0,0 +1,3880 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.4.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "enable": true, + "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0", + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "name": "Restart Detection", + "showIn": 0, + "step": "1m", + "tagKeys": "restart-tag", + "textFormat": "uptime reset", + "titleFormat": "Restart" + } + ] + }, + "description": "Dashboard for Micrometer instrumented applications (Java, Spring Boot, Micronaut)", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 148, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(request_counter_total{application=\"service-client\"}[1m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{application}}", + "range": true, + "refId": "A", + "useBackend": false, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + } + } + ], + "title": "Счетчик запросов", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 149, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration{application=\"service-client\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{application}}", + "range": true, + "refId": "A", + "useBackend": false, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + } + } + ], + "title": "Время выполнения запроса", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 139, + "panels": [], + "title": "Quick Facts", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 63, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_uptime_seconds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dateTimeAsIso" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 92, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_start_time_seconds{application=\"$application\", instance=\"$instance\"}*1000", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "title": "Start time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 70 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 65, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "title": "Heap used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + }, + { + "options": { + "from": -1e+32, + "result": { + "text": "N/A" + }, + "to": 0 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 70 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 75, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "title": "Non-Heap used", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 140, + "panels": [], + "title": "I/O Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 13 + }, + "id": 111, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP", + "refId": "A" + } + ], + "title": "Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "HTTP" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890f02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP - 5xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bf1b00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890f02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP - 5xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bf1b00", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 13 + }, + "id": 112, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status=~\"5..\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "title": "Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 13 + }, + "id": 113, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(http_server_requests_seconds_sum{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))/sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - AVG", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "max(http_server_requests_seconds_max{application=\"$application\", instance=\"$instance\", status!~\"5..\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - MAX", + "refId": "B" + } + ], + "title": "Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 119, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "tomcat_threads_busy_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "TOMCAT - BSY", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "tomcat_threads_current_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "TOMCAT - CUR", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "tomcat_threads_config_max_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "TOMCAT - MAX", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jetty_threads_busy{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "JETTY - BSY", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jetty_threads_current{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "JETTY - CUR", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jetty_threads_config_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "JETTY - MAX", + "refId": "F" + } + ], + "title": "Utilisation", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 141, + "panels": [], + "title": "JVM Memory", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 21 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "title": "JVM Heap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 21 + }, + "id": 25, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "title": "JVM Non-Heap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 21 + }, + "id": 26, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "title": "JVM Total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 21 + }, + "id": 86, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": true, + "intervalFactor": 2, + "legendFormat": "vss", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "rss", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swap", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "total", + "refId": "D" + } + ], + "title": "JVM Process Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 142, + "panels": [], + "title": "JVM Misc", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 1, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 29 + }, + "id": 106, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "system_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "system", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "avg_over_time(process_cpu_usage{application=\"$application\", instance=\"$instance\"}[15m])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process-15m", + "refId": "C" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 1, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 29 + }, + "id": 93, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "system_load_average_1m{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "system-1m", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "system_cpu_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "cpus", + "refId": "B" + } + ], + "title": "Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 29 + }, + "id": 32, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_threads_live_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "live", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_threads_daemon_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "daemon", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_threads_peak_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "peak", + "refId": "C", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "process", + "refId": "D", + "step": 2400 + } + ], + "title": "Threads", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "blocked" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bf1b00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "new" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#fce2de", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "runnable" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7eb26d", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "terminated" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "timed-waiting" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#c15c17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "waiting" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#eab839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "blocked" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bf1b00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "new" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#fce2de", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "runnable" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7eb26d", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "terminated" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "timed-waiting" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#c15c17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "waiting" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#eab839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 29 + }, + "id": 124, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_threads_states_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "title": "Thread States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The percent of time spent on Garbage Collection over all CPUs assigned to the JVM process.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 1, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 36 + }, + "id": 138, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])) by (application, instance) / on(application, instance) system_cpu_count", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "CPU time spent on GC", + "refId": "A" + } + ], + "title": "GC Pressure", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "opm" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "debug" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#1F78C1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "info" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trace" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warn" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 6, + "y": 36 + }, + "id": 91, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "increase(logback_events_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{level}}", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "title": "Log Events", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 36 + }, + "id": 61, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_files_open_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "open", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "process_files_max_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "title": "File Descriptors", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 143, + "panels": [], + "repeat": "persistence_counts", + "title": "JVM Memory Pools (Heap)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 3, + "maxPerRow": 3, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "repeat": "jvm_memory_pool_heap", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "title": "$jvm_memory_pool_heap", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 144, + "panels": [], + "title": "JVM Memory Pools (Non-Heap)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 78, + "maxPerRow": 3, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "repeat": "jvm_memory_pool_nonheap", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "title": "$jvm_memory_pool_nonheap", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 145, + "panels": [], + "title": "Garbage Collection", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 67 + }, + "id": 98, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{action}} ({{cause}})", + "refId": "A" + } + ], + "title": "Collections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 67 + }, + "id": 101, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "avg {{action}} ({{cause}})", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_gc_pause_seconds_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "max {{action}} ({{cause}})", + "refId": "B" + } + ], + "title": "Pause Durations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 67 + }, + "id": 99, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(jvm_gc_memory_allocated_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "allocated", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(jvm_gc_memory_promoted_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "promoted", + "refId": "B" + } + ], + "title": "Allocated/Promoted", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 74 + }, + "id": 146, + "panels": [], + "title": "Classloading", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 75 + }, + "id": 37, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_classes_loaded_classes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "loaded", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "title": "Classes loaded", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 75 + }, + "id": 38, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "delta(jvm_classes_loaded_classes{application=\"$application\",instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "delta-1m", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "title": "Class delta", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 82 + }, + "id": 147, + "panels": [], + "title": "Buffer Pools", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "count" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "buffers" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "count" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "buffers" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "count" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "buffers" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "count" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "buffers" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 83 + }, + "id": 131, + "maxPerRow": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "repeat": "jvm_buffer_pool", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=~\"$jvm_buffer_pool\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "buffers", + "refId": "C" + } + ], + "title": "$jvm_buffer_pool", + "type": "timeseries" + } + ], + "refresh": "5s", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "includeAll": false, + "label": "Application", + "name": "application", + "options": [], + "query": "label_values(application)", + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "includeAll": false, + "label": "Instance", + "name": "instance", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\"}, instance)", + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "hide": 2, + "includeAll": true, + "label": "JVM Memory Pools Heap", + "name": "jvm_memory_pool_heap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)", + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "hide": 2, + "includeAll": true, + "label": "JVM Memory Pools Non-Heap", + "name": "jvm_memory_pool_nonheap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)", + "refresh": 1, + "regex": "", + "sort": 2, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "hide": 2, + "includeAll": true, + "label": "JVM Buffer Pools", + "name": "jvm_buffer_pool", + "options": [], + "query": "label_values(jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\"},id)", + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "JVM (Micrometer)", + "uid": "fe6atqkegecqoe", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml b/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml new file mode 100755 index 00000000..e2131c28 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/elk/docker-compose.yml @@ -0,0 +1,149 @@ + +services: + opensearch-node1: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node1 + environment: + - cluster.name=opensearch-cluster # Name the cluster + - node.name=opensearch-node1 # Name the node that will run in this container + - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager + - bootstrap.memory_lock=true # Disable JVM heap memory swapping + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM + - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch + - "DISABLE_SECURITY_PLUGIN=true" # Disables Security plugin + ulimits: + memlock: + soft: -1 # Set memlock to unlimited (no soft or hard limit) + hard: -1 + nofile: + soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536 + hard: 65536 + volumes: + - opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container + ports: + - 9200:9200 # REST API + - 9600:9600 # Performance Analyzer + networks: + - opensearch-net # All of the containers will join the same Docker bridge network + opensearch-node2: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node2 + environment: + - cluster.name=opensearch-cluster # Name the cluster + - node.name=opensearch-node2 # Name the node that will run in this container + - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager + - bootstrap.memory_lock=true # Disable JVM heap memory swapping + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM + - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch + - "DISABLE_SECURITY_PLUGIN=true" # Disables Security plugin + ulimits: + memlock: + soft: -1 # Set memlock to unlimited (no soft or hard limit) + hard: -1 + nofile: + soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536 + hard: 65536 + volumes: + - opensearch-data2:/usr/share/opensearch/data # Creates volume called opensearch-data2 and mounts it to the container + networks: + - opensearch-net # All of the containers will join the same Docker bridge network + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.18.0 + container_name: opensearch-dashboards + ports: + - 5601:5601 # Map host port 5601 to container port 5601 + expose: + - "5601" # Expose port 5601 for web access to OpenSearch Dashboards + environment: + - 'OPENSEARCH_HOSTS=["http://opensearch-node1:9200","http://opensearch-node2:9200"]' + - "DISABLE_SECURITY_DASHBOARDS_PLUGIN=true" # disables security dashboards plugin in OpenSearch Dashboards + networks: + - opensearch-net + +#logstash + + logstash: + image: opensearchproject/logstash-oss-with-opensearch-output-plugin:7.16.2 + container_name: logstash + volumes: + - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf + networks: + - opensearch-net + + zookeeper: + image: confluentinc/cp-zookeeper:6.2.0 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + networks: + - opensearch-net + + broker: + image: confluentinc/cp-kafka:7.0.0 + container_name: broker + ports: + - "9092:9092" + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + networks: + - opensearch-net + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + network_mode: "host" + environment: + DYNAMIC_CONFIG_ENABLED: 'true' + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: localhost:9092 + deploy: + resources: + limits: + memory: "256M" + + zipkin: + container_name: zipkin + image: openzipkin/zipkin:3 + network_mode: "host" + expose: + - "9411" + + prometheus: + image: bitnami/prometheus:3.0.1 + container_name: prometheus + network_mode: "host" + expose: + - "9090" + volumes: + - type: bind + source: ./prometheus.yml + target: /etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana-enterprise:11.4.0 + container_name: grafana + network_mode: "host" + expose: + - "3000" + environment: + GF_SECURITY_ADMIN_USER: "grafana" + GF_SECURITY_ADMIN_PASSWORD: "grafana" + + +volumes: + opensearch-data1: + opensearch-data2: + +networks: + opensearch-net: \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/elk/logstash.conf b/2025-11/spring-35-36-spring-cloud/elk/logstash.conf new file mode 100644 index 00000000..35bb470e --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/elk/logstash.conf @@ -0,0 +1,24 @@ +input { + http { + id => "test_http_plugin_id" + host => "127.0.0.1" + port => 8080 + } + + kafka { + bootstrap_servers => "broker:29092" + topics => "applLogs" + codec => "json" + } +} + +output { + opensearch { + hosts => "http://opensearch-node1:9200" + user => "admin" + password => "admin" + index => "logstash-logs-%{+YYYY.MM.dd}" + ssl_certificate_verification => false + ssl => false + } +} \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml b/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml new file mode 100755 index 00000000..efdaf62d --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/elk/prometheus.yml @@ -0,0 +1,11 @@ +# JVM (Micrometer) Grafana dashboard: 4701 +global: + scrape_interval: 10s + evaluation_interval: 15s + +scrape_configs: + - job_name: service-client + metrics_path: '/actuator/prometheus' + scrape_interval: 5s + static_configs: + - targets: ['localhost:8081'] diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts new file mode 100755 index 00000000..f7b03f13 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/eureka-server/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.google.cloud.tools.jib") +} + +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation ("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. +} + +jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + + to { + image = "localrun/eureka-server" + tags = setOf(project.version.toString()) + } +} + +tasks { + build { + dependsOn(spotlessApply) + dependsOn(jibBuildTar) + } +} diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh b/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh new file mode 100755 index 00000000..5eedff0e --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/eureka-server/runEurekaServer.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +../gradlew :eureka-server:build + +docker load --input build/jib-image.tar + +docker stop eureka-server + + +docker run --rm -d --name eureka-server \ +--memory=512m \ +--cpus 1 \ +--network="host" \ +-v $HOME/.ssh:/root/.ssh \ +-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \ +localrun/eureka-server:latest + + diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java new file mode 100755 index 00000000..34063ca5 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/java/ru/demo/EurekaServer.java @@ -0,0 +1,19 @@ +package ru.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaServer { + private static final Logger log = LoggerFactory.getLogger(EurekaServer.class); + + public static void main(String[] args) { + + SpringApplication.run(EurekaServer.class, args); + log.info("EurekaServer started"); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml new file mode 100755 index 00000000..6d04a554 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + application: + name: eureka-server + cloud: + config: + fail-fast: true + retry: + initial-interval: 5000 + max-attempts: 10 + max-interval: 5000 + multiplier: 1.2 + config: + import: optional:configserver:http://localhost:8888 + codec: + max-in-memory-size: 10MB + + diff --git a/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml new file mode 100755 index 00000000..01075dfc --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/eureka-server/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"eureka-server"} + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml b/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml new file mode 100755 index 00000000..8a75eba0 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/api-gateway.yml @@ -0,0 +1,16 @@ +server: + port: 7777 + +application: + api-routes: + - id: "service-client" + from: "client" + to: "lb://SERVICE-CLIENT" + - id: "service-order" + from: "order" + to: "lb://SERVICE-ORDER" + +eureka: + client: + registerWithEureka: false + fetchRegistry: true diff --git a/2025-11/spring-35-36-spring-cloud/git-config/application.yml b/2025-11/spring-35-36-spring-cloud/git-config/application.yml new file mode 100755 index 00000000..d9b9ee4e --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/application.yml @@ -0,0 +1,36 @@ +management: + tracing: + sampling: + probability: 1.0 + endpoint: + prometheus: + enabled: true + metrics: + enabled: true + health: + enabled: true + probes: + enabled: true + refresh: + enabled: true + endpoints: + web: + exposure: + include: + - prometheus + - health + - metrics + - refresh + enabled-by-default: false + metrics: + tags: + application: ${spring.application.name} + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:9999/eureka/ + instance: + lease-renewal-interval-in-seconds: 30 + metadataMap: + zone: zone1 \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml b/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml new file mode 100755 index 00000000..fdc99a79 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/eureka-server.yml @@ -0,0 +1,14 @@ +server: + port: 9999 + +eureka: + server: + renewal-percent-threshold: 0.5 + instance: + hostname: localhost + lease-expiration-duration-in-seconds: 90 + client: + registerWithEureka: false + fetchRegistry: false + serviceUrl: + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml new file mode 100755 index 00000000..1e3605aa --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/service-client-info.yml @@ -0,0 +1,6 @@ +server: + port: 8083 + +eureka: + client: + fetchRegistry: false \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml new file mode 100755 index 00000000..1b028c1d --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/service-client.yml @@ -0,0 +1,6 @@ +server: + port: 8081 + +eureka: + client: + fetchRegistry: true \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml b/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml new file mode 100755 index 00000000..3b80dc61 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/git-config/service-order.yml @@ -0,0 +1,8 @@ +server: + port: 8082 + +eureka: + client: + fetchRegistry: false + instance: + instance-id: service-order:${spring.application.instance_id:0} \ No newline at end of file diff --git a/2025-11/spring-35-36-spring-cloud/gradle.properties b/2025-11/spring-35-36-spring-cloud/gradle.properties new file mode 100755 index 00000000..157d79a8 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/gradle.properties @@ -0,0 +1,15 @@ +# -------Gradle-------- +org.gradle.jvmargs=-Xmx4g +org.gradle.daemon=true +org.gradle.parallel=true +# -------Plugins--------- +jgitver=0.10.0-rc03 +dependencyManagement=1.1.5 +springframeworkBoot=3.3.1 +springCloudVersion=2023.0.3 + +sonarlint=4.2.4 +spotless=6.25.0 +jib=3.4.5 +# -------Versions-------- +logbackEncoder=8.0 diff --git a/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..d64cd491 Binary files /dev/null and b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.jar differ diff --git a/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..df97d72b --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/2025-11/spring-35-36-spring-cloud/gradlew b/2025-11/spring-35-36-spring-cloud/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/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/2025-11/spring-35-36-spring-cloud/gradlew.bat b/2025-11/spring-35-36-spring-cloud/gradlew.bat new file mode 100755 index 00000000..93e3f59f --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts new file mode 100755 index 00000000..93ab7ab5 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + implementation("ch.qos.logback:logback-classic") + implementation ("org.apache.kafka:kafka-clients") +} + +tasks { + bootJar { + enabled = false + } +} diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java new file mode 100755 index 00000000..d57ddd0f --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/AppenderException.java @@ -0,0 +1,8 @@ +package ru.appender.kafka; + +public class AppenderException extends RuntimeException { + + public AppenderException(String message) { + super(message); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java new file mode 100644 index 00000000..f7dbb53e --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/ErrorMsgConsumer.java @@ -0,0 +1,5 @@ +package ru.appender.kafka; + +import java.util.function.Consumer; + +public interface ErrorMsgConsumer extends Consumer {} diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java new file mode 100755 index 00000000..a0e30079 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogAppender.java @@ -0,0 +1,84 @@ +package ru.appender.kafka; + +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.UnsynchronizedAppenderBase; +import ch.qos.logback.core.encoder.Encoder; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +public class LogAppender extends UnsynchronizedAppenderBase { + private static final String MESSAGE_TEMPLATE = "[Kafka appender] %s"; + + private String bootstrapServers; + private String topicName; + + private final Queue eventsQueue = new ArrayBlockingQueue<>(1000); + + private Thread senderThread; + private Encoder encoder; + + private final ErrorMsgConsumer errorMsgConsumer = error -> addError(String.format(MESSAGE_TEMPLATE, error)); + + public void setEncoder(Encoder encoder) { + this.encoder = encoder; + } + + public void setBootstrapServers(String bootstrapServers) { + this.bootstrapServers = bootstrapServers; + addInfo(String.format(MESSAGE_TEMPLATE, "set bootstrapServers:" + bootstrapServers)); + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + addInfo(String.format(MESSAGE_TEMPLATE, "set topicName:" + topicName)); + } + + @Override + public void start() { + if (bootstrapServers == null) { + addError(String.format(MESSAGE_TEMPLATE, "bootstrapServers is null")); + return; + } + + if (topicName == null) { + addError(String.format(MESSAGE_TEMPLATE, "topicName is null")); + return; + } + var logProducer = new LogProducer(bootstrapServers, topicName); + + senderThread = Thread.ofVirtual().name("senderThread").start(() -> sendMessages(logProducer)); + super.start(); + addInfo(String.format(MESSAGE_TEMPLATE, "started")); + } + + @Override + public void stop() { + super.stop(); + if (senderThread != null) { + senderThread.interrupt(); + } + addInfo(String.format(MESSAGE_TEMPLATE, "stopped")); + } + + @Override + public void append(LoggingEvent eventObject) { + var result = eventsQueue.offer(eventObject); + if (!result) { + addWarn(String.format(MESSAGE_TEMPLATE, "eventsQueue is full")); + } + } + + private void sendMessages(LogProducer logProducer) { + while (!Thread.currentThread().isInterrupted()) { + var event = eventsQueue.poll(); + if (event != null) { + try { + var messageAsText = new String(encoder.encode(event)); + logProducer.send(messageAsText, errorMsgConsumer); + } catch (Exception ex) { + addError(String.format(MESSAGE_TEMPLATE, ex.getMessage())); + } + } + } + } +} diff --git a/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java new file mode 100755 index 00000000..72c03095 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/kafka-log-appender/src/main/java/ru/appender/kafka/LogProducer.java @@ -0,0 +1,56 @@ +package ru.appender.kafka; + +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.RETRIES_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BUFFER_MEMORY_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.MAX_BLOCK_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; + +import java.util.Properties; +import java.util.function.Consumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; + +public class LogProducer { + private final KafkaProducer kafkaProducer; + private final String topicName; + private long lastSendKey = System.currentTimeMillis(); + + public LogProducer(String bootstrapServers, String topicName) { + this.topicName = topicName; + Properties props = new Properties(); + props.put(CLIENT_ID_CONFIG, "myKafkaProducer"); + props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ACKS_CONFIG, "1"); + props.put(RETRIES_CONFIG, 1); + props.put(BATCH_SIZE_CONFIG, 16384); + props.put(LINGER_MS_CONFIG, 10); + props.put(BUFFER_MEMORY_CONFIG, 33_554_432); // bytes + props.put(MAX_BLOCK_MS_CONFIG, 1_000); // ms + props.put(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + + kafkaProducer = new KafkaProducer<>(props); + + Runtime.getRuntime().addShutdownHook(new Thread(this::close)); + } + + public void send(String value, Consumer errorCallback) { + var key = lastSendKey++; + kafkaProducer.send(new ProducerRecord<>(topicName, String.valueOf(key), value), (metadata, exception) -> { + if (exception != null) { + errorCallback.accept(String.format(exception.getMessage())); + } + }); + } + + public void close() { + kafkaProducer.close(); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts new file mode 100755 index 00000000..d931e6d2 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/build.gradle.kts @@ -0,0 +1,14 @@ +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + + implementation ("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java new file mode 100755 index 00000000..c5ce2a91 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/ServiceClientInfo.java @@ -0,0 +1,11 @@ +package ru.demo; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class ServiceClientInfo { + public static void main(String[] args) { + new SpringApplicationBuilder().sources(ServiceClientInfo.class).run(args); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java new file mode 100755 index 00000000..be04c7bd --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/config/ApplConf.java @@ -0,0 +1,19 @@ +package ru.demo.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.OncePerRequestFilter; +import ru.demo.filter.MdcFilter; + +@Configuration +public class ApplConf { + + @Bean + public FilterRegistrationBean mdcFilterRegistrationBean() { + var registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(new MdcFilter()); + registrationBean.setOrder(1); + return registrationBean; + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java new file mode 100755 index 00000000..273a35f1 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/controller/ClientInfoController.java @@ -0,0 +1,30 @@ +package ru.demo.controller; + +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.demo.model.ClientData; + +@RestController +public class ClientInfoController { + private static final Logger logger = LoggerFactory.getLogger(ClientInfoController.class); + + // curl -v http://localhost:8083/additional-info?name="testClient" + + // docker run -d --rm -p 9411:9411 openzipkin/zipkin + // http://localhost:9411 + + @GetMapping(value = "/additional-info") + public ClientData info(@RequestParam(name = "name") String name) throws InterruptedException { + logger.info("request. name:{}", name); + doJob(); + return new ClientData(String.format("additional ClientInfo name:%s", name)); + } + + private void doJob() throws InterruptedException { + Thread.sleep(Duration.ofMillis(100)); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java new file mode 100755 index 00000000..5b134b08 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/filter/MdcFilter.java @@ -0,0 +1,39 @@ +package ru.demo.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +public class MdcFilter extends OncePerRequestFilter { + private final Logger log = LoggerFactory.getLogger(MdcFilter.class); + private static final String HEADER_X_REQUEST_ID = "X-Request-Id"; + private static final String MDC_REQUEST_ID = "requestId"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + var xRequestId = request.getHeader(HEADER_X_REQUEST_ID); + log.info("method:{}, xRequestId:{}", request.getMethod(), xRequestId); + if (xRequestId != null) { + MDC.put(MDC_REQUEST_ID, xRequestId); + } + + var headerIterator = request.getHeaderNames().asIterator(); + var headers = new ArrayList(); + while (headerIterator.hasNext()) { + headers.add(headerIterator.next()); + } + log.info("request headers:{}", headers); + + response.addHeader(HEADER_X_REQUEST_ID, xRequestId); + filterChain.doFilter(request, response); + MDC.remove(MDC_REQUEST_ID); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java new file mode 100755 index 00000000..75a423ed --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/java/ru/demo/model/ClientData.java @@ -0,0 +1,3 @@ +package ru.demo.model; + +public record ClientData(String data) {} diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml new file mode 100755 index 00000000..ca36cd5c --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + application: + name: service-client-info + cloud: + config: + fail-fast: true + retry: + initial-interval: 5000 + max-attempts: 10 + max-interval: 5000 + multiplier: 1.2 + config: + import: optional:configserver:http://localhost:8888 + codec: + max-in-memory-size: 10MB + diff --git a/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml new file mode 100755 index 00000000..5507f6db --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client-info/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"service-client-info"} + requestId + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts new file mode 100755 index 00000000..3b63f3e7 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/build.gradle.kts @@ -0,0 +1,19 @@ +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation("org.springframework.boot:spring-boot-starter-web") + + implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. + implementation("io.github.openfeign:feign-micrometer") + + implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j") +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java new file mode 100755 index 00000000..98e9c7a0 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/ServiceClient.java @@ -0,0 +1,12 @@ +package ru.demo; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class ServiceClient { + public static void main(String[] args) { + + new SpringApplicationBuilder().sources(ServiceClient.class).run(args); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java new file mode 100755 index 00000000..58e6e120 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/RequestEncoder.java @@ -0,0 +1,23 @@ +package ru.demo.config; + +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import java.lang.reflect.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RequestEncoder implements Encoder { + private static final Logger log = LoggerFactory.getLogger(RequestEncoder.class); + private final Encoder defaultEncoder; + + public RequestEncoder(Encoder defaultEncoder) { + this.defaultEncoder = defaultEncoder; + } + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + log.info("encode value:{}", object); + defaultEncoder.encode(object, bodyType, template); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java new file mode 100755 index 00000000..1ae51572 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ResponseDecoder.java @@ -0,0 +1,30 @@ +package ru.demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import feign.Response; +import feign.codec.Decoder; +import java.io.IOException; +import java.lang.reflect.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.demo.model.ClientData; + +public class ResponseDecoder implements Decoder { + private static final Logger log = LoggerFactory.getLogger(ResponseDecoder.class); + + private final Decoder defaultDecoder; + private final ObjectMapper mapper; + + public ResponseDecoder(Decoder defaultDecoder, ObjectMapper mapper) { + this.defaultDecoder = defaultDecoder; + this.mapper = mapper; + } + + @Override + public ClientData decode(Response response, Type type) throws IOException, FeignException { + var responseAsString = (String) defaultDecoder.decode(response, String.class); + log.info("response:{}", responseAsString); + return mapper.readValue(responseAsString, ClientData.class); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java new file mode 100755 index 00000000..4078e0a2 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/config/ServiceClientApplConf.java @@ -0,0 +1,106 @@ +package ru.demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import feign.Contract; +import feign.Feign; +import feign.Logger; +import feign.Retryer; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.micrometer.MicrometerCapability; +import feign.micrometer.MicrometerObservationCapability; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import java.time.Duration; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.Customizer; +import org.springframework.cloud.openfeign.FeignClientsConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.filter.OncePerRequestFilter; +import ru.demo.controller.ClientAdditionalInfoClient; +import ru.demo.filter.MdcFilter; +import ru.demo.metrics.MetricsManager; +import ru.demo.metrics.MicrometerMetricsManager; + +@Configuration +@Import(FeignClientsConfiguration.class) +public class ServiceClientApplConf { + + @Bean + public RateLimiterConfig rateLimiterConfig() { + return RateLimiterConfig.custom() + .timeoutDuration(Duration.ofMillis(100)) + .limitRefreshPeriod(Duration.ofSeconds(1)) + .limitForPeriod(1000) + .build(); + } + + @Bean + public RateLimiter rateLimiter(RateLimiterConfig config) { + return RateLimiter.of("defaultRateLimiter", config); + } + + @Bean + public Customizer defaultCustomizer() { + return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) + .timeLimiterConfig(TimeLimiterConfig.custom() + .timeoutDuration(Duration.ofSeconds(5)) + .build()) + .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) + .build()); + } + + @Bean + public CircuitBreaker circuitBreaker(CircuitBreakerFactory circuitBreakerFactory) { + return circuitBreakerFactory.create("defaultCircuitBreaker"); + } + + @Bean + public FilterRegistrationBean mdcFilterRegistrationBean() { + var registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(new MdcFilter()); + registrationBean.setOrder(1); + return registrationBean; + } + + @Bean + public ObjectMapper objectMapper() { + return JsonMapper.builder().build(); + } + + @Bean + public ClientAdditionalInfoClient clientAdditionalInfoClient( + Decoder decoder, + Encoder encoder, + Contract contract, + ObjectMapper mapper, + MeterRegistry meterRegistry, + ObservationRegistry observationRegistry) { + + return Feign.builder() + .encoder(new RequestEncoder(encoder)) + .decoder(new ResponseDecoder(decoder, mapper)) + .contract(contract) + .logLevel(Logger.Level.FULL) + .addCapability(new MicrometerObservationCapability(observationRegistry)) // <-- THIS IS NEW + .addCapability(new MicrometerCapability(meterRegistry)) // <-- THIS IS NEW + .retryer(new Retryer.Default(500, 5_000, 10)) + .target(ClientAdditionalInfoClient.class, "http"); + } + + @Bean + public MetricsManager micrometerMetricsManager(MeterRegistry meterRegistry) { + return new MicrometerMetricsManager(meterRegistry); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java new file mode 100755 index 00000000..e9b1470a --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientAdditionalInfoClient.java @@ -0,0 +1,16 @@ +package ru.demo.controller; + +import static ru.demo.filter.MdcFilter.HEADER_X_REQUEST_ID; + +import java.net.URI; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import ru.demo.model.ClientData; + +public interface ClientAdditionalInfoClient { + + @GetMapping(value = "/additional-info", consumes = "application/json") + ClientData additionalInfo( + @RequestHeader(HEADER_X_REQUEST_ID) String xRequestId, URI baseUri, @RequestParam("name") String nameVal); +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java new file mode 100755 index 00000000..e3d996ca --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/controller/ClientController.java @@ -0,0 +1,90 @@ +package ru.demo.controller; + +import static ru.demo.filter.MdcFilter.MDC_REQUEST_ID; +import static ru.demo.metrics.Meter.REQUEST_COUNTER; + +import com.netflix.discovery.EurekaClient; +import io.github.resilience4j.core.functions.CheckedFunction; +import io.github.resilience4j.ratelimiter.RateLimiter; +import java.net.URI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.demo.metrics.Meter; +import ru.demo.metrics.MetricsManager; +import ru.demo.model.RequestForData; + +@RestController +public class ClientController { + private static final Logger log = LoggerFactory.getLogger(ClientController.class); + + private final MetricsManager metricsManager; + private final ClientAdditionalInfoClient clientAdditionalInfoClient; + private final EurekaClient discoveryClient; + private final CheckedFunction getAdditionalInfoFunction; + + // curl -v -H "X-Request-Id: 123" http://localhost:8081/info?name="testClient" + + public ClientController( + MetricsManager metricsManager, + ClientAdditionalInfoClient clientAdditionalInfoClient, + EurekaClient discoveryClient, + CircuitBreaker circuitBreaker, + RateLimiter rateLimiter) { + this.metricsManager = metricsManager; + this.clientAdditionalInfoClient = clientAdditionalInfoClient; + this.discoveryClient = discoveryClient; + + this.getAdditionalInfoFunction = RateLimiter.decorateCheckedFunction( + rateLimiter, + requestForData -> circuitBreaker.run(() -> doRequest(requestForData), t -> { + log.error("delay call failed error:{}", t.getMessage()); + return "unknown info"; + })); + } + + @GetMapping(value = "/info") + public String info(@RequestParam(name = "name") String name) { + var startTime = System.currentTimeMillis(); + metricsManager.incrementValue(REQUEST_COUNTER); + log.info("request. name:{}", name); + String additionalInfo = null; + try { + additionalInfo = getAdditionalInfoFunction.apply(new RequestForData(name, MDC.get(MDC_REQUEST_ID))); + } catch (Throwable ex) { + log.error("can't execute additional info, name:{}, error:{}", name, ex.getMessage()); + } + var requestResult = String.format("ClientInfo name:%s, additional:%s", name, additionalInfo); + + var duration = System.currentTimeMillis() - startTime; + metricsManager.putValue(Meter.REQUEST_DURATION, duration); + return requestResult; + } + + private String doRequest(RequestForData requestForData) { + try { + MDC.put(MDC_REQUEST_ID, requestForData.requestId()); + return getAdditionalInfo(requestForData.name()); + } finally { + MDC.remove(MDC_REQUEST_ID); + } + } + + private String getAdditionalInfo(String name) { + try { + var clientInfo = discoveryClient.getNextServerFromEureka("SERVICE-CLIENT-INFO", false); + log.info("clientInfo from Eureka:{}", clientInfo); + var additionalInfo = clientAdditionalInfoClient.additionalInfo( + MDC.get(MDC_REQUEST_ID), new URI(clientInfo.getHomePageUrl()), name); + log.info("additionalInfo:{}", additionalInfo); + return additionalInfo.data(); + } catch (Exception ex) { + log.error("can't get additional info, name:{}, error:{}", name, ex.getMessage()); + return null; + } + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java new file mode 100755 index 00000000..ebd5fcff --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/filter/MdcFilter.java @@ -0,0 +1,38 @@ +package ru.demo.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +public class MdcFilter extends OncePerRequestFilter { + public static final String HEADER_X_REQUEST_ID = "X-Request-Id"; + public static final String MDC_REQUEST_ID = "requestId"; + private final Logger log = LoggerFactory.getLogger(MdcFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + var xRequestId = request.getHeader(HEADER_X_REQUEST_ID); + log.debug("xRequestId:{}", xRequestId); + if (xRequestId != null) { + MDC.put(MDC_REQUEST_ID, xRequestId); + } + var headerIterator = request.getHeaderNames().asIterator(); + var headers = new ArrayList(); + while (headerIterator.hasNext()) { + headers.add(headerIterator.next()); + } + log.debug("request headers:{}", headers); + response.addHeader(HEADER_X_REQUEST_ID, xRequestId); + filterChain.doFilter(request, response); + MDC.remove(MDC_REQUEST_ID); + log.debug("response headers:{}", response.getHeaderNames()); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java new file mode 100644 index 00000000..57a79577 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/Meter.java @@ -0,0 +1,16 @@ +package ru.demo.metrics; + +public enum Meter { + REQUEST_COUNTER("request_counter"), + REQUEST_DURATION("request_duration"); + + private final String meterName; + + Meter(String meterName) { + this.meterName = meterName; + } + + public String getMeterName() { + return meterName; + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java new file mode 100644 index 00000000..67365084 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MetricsManager.java @@ -0,0 +1,11 @@ +package ru.demo.metrics; + +import java.util.function.Supplier; + +public interface MetricsManager { + void putValue(Meter meterName, long value); + + void incrementValue(Meter meterName); + + void registerGauge(Meter gaugeName, Supplier gaugeGetter); +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java new file mode 100644 index 00000000..e15242f5 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/metrics/MicrometerMetricsManager.java @@ -0,0 +1,52 @@ +package ru.demo.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class MicrometerMetricsManager implements MetricsManager { + private final MeterRegistry meterRegistry; + private final Map gauges = new ConcurrentHashMap<>(); + private final Map counters = new ConcurrentHashMap<>(); + + public MicrometerMetricsManager(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public void putValue(Meter meterName, long value) { + var gaugeName = makeMeterName(meterName); + var gauge = gauges.computeIfAbsent(gaugeName, key -> { + var newGauge = new AtomicLong(); + registerGauge(meterName, newGauge::get); + return newGauge; + }); + gauge.set(value); + } + + @Override + public void registerGauge(Meter gaugeName, Supplier gaugeGetter) { + var builder = Gauge.builder(gaugeName.getMeterName(), gaugeGetter); + builder.register(meterRegistry); + } + + @Override + public void incrementValue(Meter meterName) { + var counterName = makeMeterName(meterName); + var counter = counters.computeIfAbsent(counterName, key -> makeCounter(meterName)); + counter.increment(); + } + + private Counter makeCounter(Meter meterName) { + var builder = Counter.builder(meterName.getMeterName()); + return builder.register(meterRegistry); + } + + private String makeMeterName(Meter meterName) { + return String.format("%s--", meterName.getMeterName()); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java new file mode 100755 index 00000000..75a423ed --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/ClientData.java @@ -0,0 +1,3 @@ +package ru.demo.model; + +public record ClientData(String data) {} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java new file mode 100644 index 00000000..68ba5ff2 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/java/ru/demo/model/RequestForData.java @@ -0,0 +1,3 @@ +package ru.demo.model; + +public record RequestForData(String name, String requestId) {} diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml new file mode 100755 index 00000000..c40d2f5c --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + application: + name: service-client + cloud: + config: + fail-fast: true + retry: + initial-interval: 5000 + max-attempts: 10 + max-interval: 5000 + multiplier: 1.2 + config: + import: optional:configserver:http://localhost:8888 + codec: + max-in-memory-size: 10MB + + + diff --git a/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml new file mode 100755 index 00000000..d20f5eca --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-client/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"service-client"} + requestId + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts b/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts new file mode 100755 index 00000000..f3da6edd --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("com.google.cloud.tools.jib") +} + +dependencies { + implementation(project(":kafka-log-appender")) + implementation("net.logstash.logback:logstash-logback-encoder") + + implementation ("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") + + implementation("io.micrometer:micrometer-tracing-bridge-otel") // bridges the Micrometer Observation API to OpenTelemetry. + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") // reports traces to Zipkin. +} + +jib { + container { + creationTime.set("USE_CURRENT_TIMESTAMP") + } + from { + image = "bellsoft/liberica-openjdk-alpine-musl:21.0.1" + } + + to { + image = "localrun/service-order" + tags = setOf(project.version.toString()) + } +} + +tasks { + build { + dependsOn(spotlessApply) + dependsOn(jibBuildTar) + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh b/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh new file mode 100755 index 00000000..b6f76415 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/runServiceOrder.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +../gradlew :service-order:build + +docker load --input build/jib-image.tar + +docker stop service-order-1 +docker stop service-order-2 + +docker run --rm -d --name service-order-1 \ +--memory=256m \ +--cpus 1 \ +--network="host" \ +-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \ +-e SPRING_APPLICATION_INSTANCE_ID="i1" \ +-e SERVER_PORT=8091 \ +localrun/service-order:latest + +docker run --rm -d --name service-order-2 \ +--memory=256m \ +--cpus 1 \ +--network="host" \ +-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=80 -XX:MaxRAMPercentage=80" \ +-e SPRING_APPLICATION_INSTANCE_ID="i2" \ +-e SERVER_PORT=8092 \ +localrun/service-order:latest + diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java new file mode 100755 index 00000000..ac7f5a38 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/Config.java @@ -0,0 +1,15 @@ +package ru.demo; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.demo.controller.InstanceId; + +@Configuration +public class Config { + + @Bean + InstanceId instanceId(@Value("${spring.application.instance_id}") String id) { + return new InstanceId(id); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java new file mode 100755 index 00000000..106c40b3 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/ServiceOrder.java @@ -0,0 +1,11 @@ +package ru.demo; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class ServiceOrder { + public static void main(String[] args) { + new SpringApplicationBuilder().sources(ServiceOrder.class).run(args); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java new file mode 100755 index 00000000..73d8c740 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/InstanceId.java @@ -0,0 +1,3 @@ +package ru.demo.controller; + +public record InstanceId(String name) {} diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java new file mode 100755 index 00000000..33307da2 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/java/ru/demo/controller/OrderController.java @@ -0,0 +1,28 @@ +package ru.demo.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class OrderController { + private static final Logger logger = LoggerFactory.getLogger(OrderController.class); + private final InstanceId instanceId; + + public OrderController(InstanceId instanceId) { + this.instanceId = instanceId; + } + + // curl -v http://localhost:8082/info?id="idClient" + + // curl -v http://localhost:8091/info?id="idClient" + // curl -v http://localhost:8092/info?id="idClient" + + @GetMapping(value = "/info") + public String info(@RequestParam(name = "id") String id) { + logger.info("instanceId:{}, request. id:{}", instanceId.name(), id); + return String.format("%s, Order id:%s", instanceId.name(), id); + } +} diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml new file mode 100755 index 00000000..472355c8 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + application: + name: service-order + instance_id: i0 + cloud: + config: + fail-fast: true + retry: + initial-interval: 5000 + max-attempts: 10 + max-interval: 5000 + multiplier: 1.2 + config: + import: optional:configserver:http://localhost:8888 + codec: + max-in-memory-size: 10MB + + diff --git a/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml new file mode 100755 index 00000000..c74d4279 --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/service-order/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] %msg%n + + + + + localhost:9092 + applLogs + + + [ignore] + + {"appname":"service-order"} + requestId + + + + + + + + diff --git a/2025-11/spring-35-36-spring-cloud/settings.gradle.kts b/2025-11/spring-35-36-spring-cloud/settings.gradle.kts new file mode 100755 index 00000000..4934408f --- /dev/null +++ b/2025-11/spring-35-36-spring-cloud/settings.gradle.kts @@ -0,0 +1,27 @@ +pluginManagement { + val jgitver: String by settings + val dependencyManagement: String by settings + val springframeworkBoot: String by settings + val sonarlint: String by settings + val spotless: String by settings + val jib: String by settings + + plugins { + id("fr.brouillard.oss.gradle.jgitver") version jgitver + id("io.spring.dependency-management") version dependencyManagement + id("org.springframework.boot") version springframeworkBoot + id("name.remal.sonarlint") version sonarlint + id("com.diffplug.spotless") version spotless + id("com.google.cloud.tools.jib") version jib + } +} +rootProject.name = "spring-cloud" + +include("api-gateway") +include("config-server") +include("eureka-server") +include("service-client") +include("service-client-info") +include("service-order") + +include("kafka-log-appender") diff --git a/2025-11/spring-37-rabbit/README.md b/2025-11/spring-37-rabbit/README.md new file mode 100644 index 00000000..9ce1271d --- /dev/null +++ b/2025-11/spring-37-rabbit/README.md @@ -0,0 +1,41 @@ +## Пример взаимодействия приложений через RabbitMQ + +В примере демонстрируется: + +* *использование `@Scheduled` таймера для имитации пользовательской активности* +* *оправка и прием сообщений между двумя приложениями через RabbitMQ* +* *создание и отправка email сообщений средствами SpringMail* +* *аггрегация данных с помощью SpringData/JPQL* +* *создание кастомного endpoint-а actuator-а для вывода статистики* + +Описание примера: + +user-activity-emitter-microservice: + +* *по таймеру `UserActivityEmitterService` достает из БД случайный тип активности и пользователя* +* *после чего формирует из них объект активности (`UserActivity`) и отправляет в очередь сообщений RabbitMQ с + помощью `RabbitTemplate` + настроенный на работу с "main-exchange"* +* *большее число активностей имеют `routingKey` = "user.activity.message.simple" * +* *активности, у которых в названии типа есть вхождение "Вредн" имееют свой `routingKey` (" + user.activity.message.important") * +* *так же приложение по таймеру, с помощью `ActivityStatCalculationEmitterSerivce` инициирует подсчет статистики с + помощью отправки + сообщения в очередь RabbitMQ c `routingKey` = "user.activity.stat"* + +user-activity-processor-microservice: + +* *в приложении для обработки сообщений есть несколько очередей, куда попадают сообщения в зависимости от `routingKey`* +* *их прослушивает компонент `RabbitMqListener`, который содержит по методу на каждую очередь"* +* *в "all-activity-queue" попадают все активности. На выходе из данной очереди активности сохраняются в БД* +* *в "important-activity-queue" попадают важные активности. На выходе из данной очереди активности преобразуются в + письма и отправляются на + почту администратору* +* *в "stat-calc-commands-queue" попадают команды, которые инициируют расчет статистики. * +* *в методе-обработчике сообщений данной очереди происходит удаление старых статистических данных, а так же подсчет и + сохранение в БД новых* +* *за вывод статистических данных отвечает кастомный endpoint actuator-а `ActivityStatEndpoint`* + +Для работы приложений требуется работающий RabbitMQ. За его запуск отвечает docker-compose.yml +Адрес консоли RabbitMQ: http://localhost:15672/ +Логин и пароль: guest \ No newline at end of file diff --git a/2025-11/spring-37-rabbit/docker-compose.yml b/2025-11/spring-37-rabbit/docker-compose.yml new file mode 100644 index 00000000..2bdf971a --- /dev/null +++ b/2025-11/spring-37-rabbit/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' +services: + rabbitmq: + image: rabbitmq:management + ports: + - "5672:5672" + - "15672:15672" \ No newline at end of file diff --git a/2025-11/spring-37-rabbit/pom.xml b/2025-11/spring-37-rabbit/pom.xml new file mode 100644 index 00000000..55178e58 --- /dev/null +++ b/2025-11/spring-37-rabbit/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.2 + + + + ru.otus.example + spring-mail-rabbitmq-demo + 1.0 + + pom + + + user-activity-models + user-activity-emitter-microservice + user-activity-processor-microservice + + + + 17 + 17 + + + + + org.projectlombok + lombok + provided + + + diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml new file mode 100644 index 00000000..98ca0cdf --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + ru.otus.example + spring-mail-rabbitmq-demo + 1.0 + + + user-activity-emitter-microservice + 0.0.1-SNAPSHOT + user-activity-emitter-microservice + User activity emitter microservice + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-amqp + + + + ru.otus.example + user-activity-models + 0.0.1-SNAPSHOT + + + + com.h2database + h2 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java new file mode 100644 index 00000000..fc68506f --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/EmitterMicroServiceApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.rabbitmq; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@EntityScan("ru.otus.example.useractivitymodels") +@SpringBootApplication +public class EmitterMicroServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(EmitterMicroServiceApplication.class, args); + } + +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java new file mode 100644 index 00000000..e6c6124c --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java @@ -0,0 +1,34 @@ +package ru.otus.example.rabbitmq.rabbitmq; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMqConfig { + + private static final String MAIN_EXCHANGE_NAME = "main-exchange"; + + @Bean + public Jackson2JsonMessageConverter jsonConverter(ObjectMapper objectMapper) { + return new Jackson2JsonMessageConverter(objectMapper); + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, + Jackson2JsonMessageConverter jsonConverter) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setExchange(MAIN_EXCHANGE_NAME); + rabbitTemplate.setMessageConverter(jsonConverter); + return rabbitTemplate; + } + + @Bean + public TopicExchange topicExchange() { + return new TopicExchange(MAIN_EXCHANGE_NAME); + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java new file mode 100644 index 00000000..ec1a77a6 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.rabbitmq.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.useractivitymodels.UserActivity; + +@Transactional +public interface ActivityRepository extends JpaRepository { +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java new file mode 100644 index 00000000..b1ab8572 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityTypeRepository.java @@ -0,0 +1,7 @@ +package ru.otus.example.rabbitmq.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.otus.example.useractivitymodels.ActivityType; + +public interface ActivityTypeRepository extends JpaRepository { +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java new file mode 100644 index 00000000..cb45abc1 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/AppUserRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.rabbitmq.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.useractivitymodels.AppUser; + +@Transactional +public interface AppUserRepository extends JpaRepository { +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java new file mode 100644 index 00000000..0de464c1 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/ActivityStatCalculationEmitterSerivce.java @@ -0,0 +1,24 @@ +package ru.otus.example.rabbitmq.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Slf4j +public class ActivityStatCalculationEmitterSerivce { + private static final String USER_ACTIVITY_STAT_ROUTING_KEY = "user.activity.stat"; + private static final String CALC_STAT_COMMAND = "{\"command\": \"calc stat now\"}"; + + private final RabbitTemplate rabbitTemplate; + + @Scheduled(initialDelay = 3000, fixedRate = 10000) + public void emitAppUserActivityStatCalculation() { +// log.warn("Stat send!!!"); + rabbitTemplate.convertAndSend(USER_ACTIVITY_STAT_ROUTING_KEY, CALC_STAT_COMMAND); + log.warn("Stat calculated!!!"); + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java new file mode 100644 index 00000000..8e6b6986 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityEmitterService.java @@ -0,0 +1,40 @@ +package ru.otus.example.rabbitmq.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import ru.otus.example.rabbitmq.repositories.ActivityTypeRepository; +import ru.otus.example.rabbitmq.repositories.AppUserRepository; +import ru.otus.example.useractivitymodels.UserActivity; + +import java.util.Random; + +@RequiredArgsConstructor +@Service +@Slf4j +public class UserActivityEmitterService { + private final ActivityTypeRepository activityTypeRepository; + private final AppUserRepository appUserRepository; + private final RabbitTemplate rabbitTemplate; + + @SuppressWarnings("unused") + @Scheduled(initialDelay = 2000, fixedRate = 3000) + public void emitAppUserActivity() { + val random = new Random(); + val activityTypes = activityTypeRepository.findAll(); + val appUsers = appUserRepository.findAll(); + + val activityType = activityTypes.get(random.nextInt(activityTypes.size())); + val appUser = appUsers.get(random.nextInt(appUsers.size())); + val appUserActivity = new UserActivity(activityType, appUser); + val isImportant = activityType.getName().contains("Вредн"); + + val routingKey = String.format("user.activity.message.%s", isImportant ? "important" : "simple"); + rabbitTemplate.convertAndSend(routingKey, appUserActivity); + log.info("Send activity: {}", appUserActivity); + + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml new file mode 100644 index 00000000..c9f02077 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server: + port: 9090 +spring: + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + show-sql: false + + rabbitmq: + addresses: "localhost" + sql: + init: + mode: always \ No newline at end of file diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql new file mode 100644 index 00000000..e15ece69 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/data.sql @@ -0,0 +1,44 @@ +insert into app_users(name, email) +values ('Рафаель Губерманович Тыгыдым', 'test@mail.ru'); + +insert into app_users(name, email) +values ('Артем Демосфенович Шмяк', 'test@mail.ru'); + +insert into app_users(name, email) +values ('Ифигения Бореславовна Фуфелшмерц', 'test@mail.ru'); + + +insert into activity_types(name) +values ('Очень полезное дело №4'); + +insert into activity_types(name) +values ('Очень полезное дело №13'); + +insert into activity_types(name) +values ('Очень полезное дело №34'); + +insert into activity_types(name) +values ('Очень полезное дело №48'); + +insert into activity_types(name) +values ('Очень полезное дело №53'); + +insert into activity_types(name) +values ('Вредное дело №11'); + +insert into activity_types(name) +values ('Вредное дело №12'); + +insert into activity_types(name) +values ('Вредное дело №13'); + +insert into activity_types(name) +values ('Вредное дело №14'); + +insert into activity_types(name) +values ('Вредное дело №15'); + +insert into activity_types(name) +values ('Вредное дело №16'); + + diff --git a/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql new file mode 100644 index 00000000..03bcef37 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-emitter-microservice/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table activity_types +( + id bigint auto_increment, + name varchar(255), + primary key (id) +); + +create table app_users +( + id bigint auto_increment, + email varchar(255), + name varchar(255), + primary key (id) +); + + +create table app_users_activity +( + id bigint auto_increment, + activity_time timestamp, + app_user_id bigint, + activity_type_id bigint, + primary key (id) +); + +alter table app_users_activity + add constraint app_users_activity_user_id_fk foreign key (app_user_id) references app_users; + +alter table app_users_activity + add constraint app_users_activity_activity_type_id_fk foreign key (activity_type_id) references activity_types; + diff --git a/2025-11/spring-37-rabbit/user-activity-models/.gitignore b/2025-11/spring-37-rabbit/user-activity-models/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/2025-11/spring-37-rabbit/user-activity-models/pom.xml b/2025-11/spring-37-rabbit/user-activity-models/pom.xml new file mode 100644 index 00000000..eae783db --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + ru.otus.example + spring-mail-rabbitmq-demo + 1.0 + + + user-activity-models + 0.0.1-SNAPSHOT + user-activity-models + Models for spring mail rabbitmq demo project + + + + + com.fasterxml.jackson.core + jackson-annotations + + + + jakarta.persistence + jakarta.persistence-api + + + + + + diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java new file mode 100644 index 00000000..4cc6e62f --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityStatElem.java @@ -0,0 +1,37 @@ +package ru.otus.example.useractivitymodels; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "activity_stat") +public class ActivityStatElem { + + @JsonIgnore + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @JsonProperty("Имя пользователя") + @Column(name = "app_user_name") + private String appUserName; + + @JsonProperty("Тип активности") + @Column(name = "activity_type") + private String activityType; + + @JsonProperty("Количество") + @Column(name = "activities_count") + private long activitiesCount; + + public ActivityStatElem(String appUserName, String activityType, long activitiesCount) { + this.appUserName = appUserName; + this.activityType = activityType; + this.activitiesCount = activitiesCount; + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java new file mode 100644 index 00000000..2ffc1322 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/ActivityType.java @@ -0,0 +1,21 @@ +package ru.otus.example.useractivitymodels; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "activity_types") +public class ActivityType { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java new file mode 100644 index 00000000..84936afd --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/AppUser.java @@ -0,0 +1,24 @@ +package ru.otus.example.useractivitymodels; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "app_users") +public class AppUser { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "email") + private String email; + + @Column(name = "name") + private String name; +} diff --git a/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java new file mode 100644 index 00000000..b318688d --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-models/src/main/java/ru/otus/example/useractivitymodels/UserActivity.java @@ -0,0 +1,38 @@ +package ru.otus.example.useractivitymodels; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "app_users_activity") +public class UserActivity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "activity_time") + private LocalDateTime activityTime; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "activity_type_id") + private ActivityType type; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "app_user_id") + private AppUser appUser; + + public UserActivity(ActivityType type, AppUser appUser) { + this.id = id; + this.type = type; + this.appUser = appUser; + this.activityTime = LocalDateTime.now(); + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore b/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml b/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml new file mode 100644 index 00000000..513df465 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + ru.otus.example + spring-mail-rabbitmq-demo + 1.0 + + + user-activity-processor-microservice + 0.0.1-SNAPSHOT + user-activity-processor-microservice + User activity processor microservice + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-amqp + + + + org.springframework.boot + spring-boot-starter-mail + + + + ru.otus.example + user-activity-models + 0.0.1-SNAPSHOT + + + + com.h2database + h2 + runtime + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java new file mode 100644 index 00000000..a7b6eda5 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/ProcessorMicroServiceApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.rabbitmq; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@EntityScan("ru.otus.example.useractivitymodels") +@SpringBootApplication +public class ProcessorMicroServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ProcessorMicroServiceApplication.class, args); + } + +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java new file mode 100644 index 00000000..a6170e07 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/actuator/ActivityStatEndpoint.java @@ -0,0 +1,23 @@ +package ru.otus.example.rabbitmq.actuator; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.stereotype.Component; +import ru.otus.example.rabbitmq.repositories.ActivityStatRepository; +import ru.otus.example.useractivitymodels.ActivityStatElem; + +import java.util.List; + +@RequiredArgsConstructor +@Component +@Endpoint(id = "activity-stat") +public class ActivityStatEndpoint { + + private final ActivityStatRepository activityStatRepository; + + @ReadOperation + public List getAppUsersActivityStat() { + return activityStatRepository.findAll(); + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java new file mode 100644 index 00000000..473d175e --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/config/AppProps.java @@ -0,0 +1,13 @@ +package ru.otus.example.rabbitmq.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties("app") +public class AppProps { + private String serverEmail; + private String adminEmail; +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java new file mode 100644 index 00000000..caf54bcc --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqConfig.java @@ -0,0 +1,75 @@ +package ru.otus.example.rabbitmq.rabbitmq; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.amqp.core.*; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMqConfig { + @Bean + public Jackson2JsonMessageConverter jsonConverter(ObjectMapper objectMapper) { + return new Jackson2JsonMessageConverter(objectMapper); + } + + @Bean + public Queue allActivityQueue() { + return new Queue("all-activity-queue"); + } + + @Bean + public Queue importantActivityQueue() { + return new Queue("important-activity-queue"); + } + + @Bean + public Queue statCalcCommandsQueue() { + return QueueBuilder.durable("stat-calc-commands-queue") + .maxLength(5) + .deadLetterExchange("dead-letter-exchange") + .build(); + } + + @Bean + public Queue deadLetterQueue() { + return new Queue("dead-letter-queue"); + } + + @Bean + public TopicExchange topicExchange() { + return new TopicExchange("main-exchange"); + } + + @Bean + public FanoutExchange deadLetterExchange() { + return new FanoutExchange("dead-letter-exchange"); + } + + @Bean + public Binding allActivityBinding() { + return BindingBuilder.bind(allActivityQueue()) + .to(topicExchange()) + .with("user.activity.message.*"); + } + + @Bean + public Binding importantActivityBinding() { + return BindingBuilder.bind(importantActivityQueue()) + .to(topicExchange()) + .with("user.activity.message.important"); + } + + @Bean + public Binding statCalcCommandsBinding() { + return BindingBuilder.bind(statCalcCommandsQueue()) + .to(topicExchange()) + .with("user.activity.stat"); + } + + @Bean + public Binding deadLetterBinding() { + return BindingBuilder.bind(deadLetterQueue()) + .to(deadLetterExchange()); + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java new file mode 100644 index 00000000..ba6a63e5 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/rabbitmq/RabbitMqListener.java @@ -0,0 +1,70 @@ +package ru.otus.example.rabbitmq.rabbitmq; + +import com.rabbitmq.client.Channel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Service; +import ru.otus.example.rabbitmq.repositories.ActivityRepository; +import ru.otus.example.rabbitmq.repositories.ActivityStatRepository; +import ru.otus.example.rabbitmq.services.UserActivityToEmailTransformer; +import ru.otus.example.useractivitymodels.UserActivity; + +@RequiredArgsConstructor +@Service +@Slf4j +public class RabbitMqListener { + + private final ActivityRepository activityRepository; + private final ActivityStatRepository activityStatRepository; + private final UserActivityToEmailTransformer messageTransformer; + private final JavaMailSender mailSender; + + @RabbitListener(queues = "important-activity-queue") + public void processImportantMessages(UserActivity message) { + log.info("RECEIVED FROM important-activity-queue: " + message); + + try { + val mailMessage = messageTransformer.transform(message); + log.info("Как будто посылаем письмо: " + mailMessage); + //mailSender.send(mailMessage); + } catch (Exception e) { + throw new AmqpRejectAndDontRequeueException("Ooops"); + } + } + + @RabbitListener(queues = "all-activity-queue") + public void processAllMessages(UserActivity message) { + log.info("RECEIVED FROM all-activity-queue: " + message); + try { + activityRepository.save(message); + } catch (Exception e) { + throw new AmqpRejectAndDontRequeueException("Ooops"); + } + } + + @RabbitListener(queues = "stat-calc-commands-queue", ackMode = "MANUAL") + public void processStatCalcCommandsMessages(String message, + Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception { + log.warn("RECEIVED FROM stat-calc-commands-queue: " + message); + + activityStatRepository.deleteAll(); + val activityStat = activityStatRepository.calcActivityStat(); + activityStatRepository.saveAll(activityStat); +// sleep(5000); + channel.basicAck(tag, false); + + // Для ackMode = "MANUAL" и перехода в dead letter exchange + //channel.basicNack(tag, false, false); + + // Для ackMode = "AUTO" и перехода в dead letter exchange + //throw new AmqpRejectAndDontRequeueException("Ooops"); + + } +} \ No newline at end of file diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java new file mode 100644 index 00000000..ec1a77a6 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.rabbitmq.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.useractivitymodels.UserActivity; + +@Transactional +public interface ActivityRepository extends JpaRepository { +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java new file mode 100644 index 00000000..cde54ceb --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/repositories/ActivityStatRepository.java @@ -0,0 +1,21 @@ +package ru.otus.example.rabbitmq.repositories; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.useractivitymodels.ActivityStatElem; + +import java.util.List; + +@Transactional +public interface ActivityStatRepository extends JpaRepository { + + @Transactional(readOnly = true) + @Query("select new ru.otus.example.useractivitymodels.ActivityStatElem(u.name, t.name, count(a)) " + + "from UserActivity a left join a.appUser u left join a.type t " + + "group by u.name, t.name " + + "order by count(a) desc, u.name, t.name") + List calcActivityStat(); + +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java new file mode 100644 index 00000000..8a61b36f --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformer.java @@ -0,0 +1,8 @@ +package ru.otus.example.rabbitmq.services; + +import org.springframework.mail.SimpleMailMessage; +import ru.otus.example.useractivitymodels.UserActivity; + +public interface UserActivityToEmailTransformer { + SimpleMailMessage transform(UserActivity activity); +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java new file mode 100644 index 00000000..d5dc0c08 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/java/ru/otus/example/rabbitmq/services/UserActivityToEmailTransformerImpl.java @@ -0,0 +1,25 @@ +package ru.otus.example.rabbitmq.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.stereotype.Service; +import ru.otus.example.rabbitmq.config.AppProps; +import ru.otus.example.useractivitymodels.UserActivity; + +@RequiredArgsConstructor +@Service +public class UserActivityToEmailTransformerImpl implements UserActivityToEmailTransformer { + + private final AppProps appProps; + + @Override + public SimpleMailMessage transform(UserActivity activity) { + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setTo(appProps.getAdminEmail()); + mailMessage.setFrom(appProps.getServerEmail()); + mailMessage.setSubject("Обнаружена вредная активность"); + mailMessage.setText(String.format("Внимание!!! Обнаружена вредная активность! Время: %s, пользователь: %s, тип активности: %s", + activity.getActivityTime(), activity.getAppUser().getName(), activity.getType().getName())); + return mailMessage; + } +} diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml new file mode 100644 index 00000000..e0fa092d --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/application.yml @@ -0,0 +1,40 @@ +app: + # адрес почты, через которую сервер отправляет письма + server-email: ${server.email} + # адрес почты администратора, на которую сервер отправляет письма + admin-email: ${admin.email} + +spring: + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + show-sql: true + + rabbitmq: + addresses: "localhost" + + mail: + host: smtp.mail.ru + port: 465 + # логин и пароль для почты, через которую сервер отправляет письма + username: ${email.server.user} + password: ${email.server.password} + protocol: smtps + properties: + mail: + smtp: + auth: true + starttls.enable: true + sql: + init: + mode: + +management: + endpoints: + web: + exposure: + include: health, info, activity-stat + endpoint: + health: + show-details: always \ No newline at end of file diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql new file mode 100644 index 00000000..e15ece69 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/data.sql @@ -0,0 +1,44 @@ +insert into app_users(name, email) +values ('Рафаель Губерманович Тыгыдым', 'test@mail.ru'); + +insert into app_users(name, email) +values ('Артем Демосфенович Шмяк', 'test@mail.ru'); + +insert into app_users(name, email) +values ('Ифигения Бореславовна Фуфелшмерц', 'test@mail.ru'); + + +insert into activity_types(name) +values ('Очень полезное дело №4'); + +insert into activity_types(name) +values ('Очень полезное дело №13'); + +insert into activity_types(name) +values ('Очень полезное дело №34'); + +insert into activity_types(name) +values ('Очень полезное дело №48'); + +insert into activity_types(name) +values ('Очень полезное дело №53'); + +insert into activity_types(name) +values ('Вредное дело №11'); + +insert into activity_types(name) +values ('Вредное дело №12'); + +insert into activity_types(name) +values ('Вредное дело №13'); + +insert into activity_types(name) +values ('Вредное дело №14'); + +insert into activity_types(name) +values ('Вредное дело №15'); + +insert into activity_types(name) +values ('Вредное дело №16'); + + diff --git a/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql new file mode 100644 index 00000000..0f67d6e4 --- /dev/null +++ b/2025-11/spring-37-rabbit/user-activity-processor-microservice/src/main/resources/schema.sql @@ -0,0 +1,41 @@ +create table activity_types +( + id bigint auto_increment, + name varchar(255), + primary key (id) +); + +create table app_users +( + id bigint auto_increment, + email varchar(255), + name varchar(255), + primary key (id) +); + + +create table app_users_activity +( + id bigint auto_increment, + activity_time timestamp, + app_user_id bigint, + activity_type_id bigint, + primary key (id) +); + +alter table app_users_activity + add constraint app_users_activity_user_id_fk foreign key (app_user_id) references app_users; + +alter table app_users_activity + add constraint app_users_activity_activity_type_id_fk foreign key (activity_type_id) references activity_types; + + +create table activity_stat +( + id bigint auto_increment, + app_user_name varchar(255), + activity_type varchar(255), + activities_count bigint, + primary key (id) +); + diff --git a/2025-11/spring-38-kafka/.gitignore b/2025-11/spring-38-kafka/.gitignore new file mode 100644 index 00000000..dae2cf99 --- /dev/null +++ b/2025-11/spring-38-kafka/.gitignore @@ -0,0 +1,39 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle +/buildSrc/.gradle/ + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-11/spring-38-kafka/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-11/spring-38-kafka/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-11/spring-38-kafka/consumer/mvnw b/2025-11/spring-38-kafka/consumer/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-11/spring-38-kafka/consumer/mvnw.cmd b/2025-11/spring-38-kafka/consumer/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-11/spring-38-kafka/consumer/pom.xml b/2025-11/spring-38-kafka/consumer/pom.xml new file mode 100644 index 00000000..f270f281 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + ru.otus + spring-38-kafka + 1.0 + + + consumer + 1.0 + + + 21 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.kafka + spring-kafka + + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.findbugs + jsr305 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java new file mode 100644 index 00000000..a9e6aa58 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/ConsumerApp.java @@ -0,0 +1,11 @@ +package ru.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ConsumerApp { + public static void main(String[] args) { + SpringApplication.run(ConsumerApp.class, args); + } +} diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java new file mode 100644 index 00000000..a44ba11f --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/config/ApplicationConfig.java @@ -0,0 +1,112 @@ +package ru.demo.config; + +import static org.springframework.kafka.support.serializer.JsonDeserializer.TYPE_MAPPINGS; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.kafka.KafkaProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.config.KafkaListenerContainerFactory; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; +import org.springframework.kafka.support.JacksonUtils; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; +import ru.demo.model.StringValue; +import ru.demo.service.StringValueConsumer; +import ru.demo.service.StringValueConsumerLogger; + +@Configuration +public class ApplicationConfig { + private static final Logger log = LoggerFactory.getLogger(ApplicationConfig.class); + public final String topicName; + + public ApplicationConfig(@Value("${application.kafka.topic}") String topicName) { + this.topicName = topicName; + } + + @Bean + public ObjectMapper objectMapper() { + return JacksonUtils.enhancedObjectMapper(); + } + + @Bean + public ConsumerFactory consumerFactory( + KafkaProperties kafkaProperties, ObjectMapper mapper) { + var props = kafkaProperties.buildConsumerProperties(); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); + props.put(TYPE_MAPPINGS, "ru.demo.model.StringValue:ru.demo.model.StringValue"); + // Максимальное количество записей, возвращаемых за один вызов poll() + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 3); + // Максимальный интервал в миллисекундах между вызовами poll() (после превышения consumer считается неактивным) + props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 3_000); + + var kafkaConsumerFactory = new DefaultKafkaConsumerFactory(props); + kafkaConsumerFactory.setValueDeserializer(new JsonDeserializer<>(mapper)); + return kafkaConsumerFactory; + } + + @Bean("listenerContainerFactory") + public KafkaListenerContainerFactory> + listenerContainerFactory(ConsumerFactory consumerFactory) { + var factory = new ConcurrentKafkaListenerContainerFactory(); + factory.setConsumerFactory(consumerFactory); + factory.setBatchListener(true); + factory.setConcurrency(1); + // Время в миллисекундах между вызовами poll() к Kafka брокеру + factory.getContainerProperties().setIdleBetweenPolls(1_000); + // Таймаут в миллисекундах для операции poll() (время ожидания новых сообщений) + factory.getContainerProperties().setPollTimeout(1_000); + + var executor = new SimpleAsyncTaskExecutor("k-consumer-"); + executor.setConcurrencyLimit(10); + var listenerTaskExecutor = new ConcurrentTaskExecutor(executor); + factory.getContainerProperties().setListenerTaskExecutor(listenerTaskExecutor); + return factory; + } + + @Bean + public NewTopic topic() { + return TopicBuilder.name(topicName).partitions(1).replicas(1).build(); + } + + @Bean + public StringValueConsumer stringValueConsumerLogger() { + return new StringValueConsumerLogger(); + } + + @Bean + public KafkaClient stringValueConsumer(StringValueConsumer stringValueConsumer) { + return new KafkaClient(stringValueConsumer); + } + + public static class KafkaClient { + private final StringValueConsumer stringValueConsumer; + + public KafkaClient(StringValueConsumer stringValueConsumer) { + this.stringValueConsumer = stringValueConsumer; + } + + @KafkaListener( + topics = "${application.kafka.topic}", + containerFactory = "listenerContainerFactory") + public void listen(@Payload List values) { + log.info("values, values.size:{}", values.size()); + stringValueConsumer.accept(values); + } + } +} diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java new file mode 100644 index 00000000..213f1309 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/model/StringValue.java @@ -0,0 +1,3 @@ +package ru.demo.model; + +public record StringValue(long id, String value) {} diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java new file mode 100644 index 00000000..ff5cd296 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumer.java @@ -0,0 +1,9 @@ +package ru.demo.service; + +import java.util.List; +import ru.demo.model.StringValue; + +public interface StringValueConsumer { + + void accept(List value); +} diff --git a/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java new file mode 100644 index 00000000..c791cbea --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/java/ru/demo/service/StringValueConsumerLogger.java @@ -0,0 +1,17 @@ +package ru.demo.service; + +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.demo.model.StringValue; + +public class StringValueConsumerLogger implements StringValueConsumer { + private static final Logger log = LoggerFactory.getLogger(StringValueConsumerLogger.class); + + @Override + public void accept(List values) { + for (var value : values) { + log.info("log:{}", value); + } + } +} diff --git a/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml b/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml new file mode 100644 index 00000000..23adc138 --- /dev/null +++ b/2025-11/spring-38-kafka/consumer/src/main/resources/application.yml @@ -0,0 +1,13 @@ +application: + kafka: + topic: "demo-topic" + + +spring: + kafka: + consumer: + group-id: "test-group" + bootstrap-servers: "localhost:9092" + client-id: "demo-consumer" + # auto-offset-reset - стратегия чтения при отсутствии сохраненного offset: earliest (с начала), latest (с конца) + auto-offset-reset: earliest diff --git a/2025-11/spring-38-kafka/docker/docker-compose.yml b/2025-11/spring-38-kafka/docker/docker-compose.yml new file mode 100644 index 00000000..c36a5e37 --- /dev/null +++ b/2025-11/spring-38-kafka/docker/docker-compose.yml @@ -0,0 +1,35 @@ + +services: + zookeeper: + image: confluentinc/cp-zookeeper:6.2.0 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + broker: + image: confluentinc/cp-kafka:7.0.0 + container_name: broker + ports: + - "9092:9092" + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + + kafdrop: + image: obsidiandynamics/kafdrop:4.0.1 + container_name: kafdrop + ports: + - "9000:9000" + depends_on: + - broker + environment: + KAFKA_BROKERCONNECT: "broker:29092" + JVM_OPTS: "-Xms32M -Xmx64M" \ No newline at end of file diff --git a/2025-11/spring-38-kafka/mvnw b/2025-11/spring-38-kafka/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2025-11/spring-38-kafka/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-11/spring-38-kafka/mvnw.cmd b/2025-11/spring-38-kafka/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-11/spring-38-kafka/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-11/spring-38-kafka/pom.xml b/2025-11/spring-38-kafka/pom.xml new file mode 100644 index 00000000..f761fa7a --- /dev/null +++ b/2025-11/spring-38-kafka/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + ru.otus + spring-38-kafka + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + pom + + + consumer + producer + + + + UTF-8 + 21 + 21 + 3.0.0-M3 + 3.1.1 + 3.3.9 + 1.0.12.RELEASE + 3.4.5 + 3.0.2 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springframeworkBoot.version} + pom + import + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + + + + + + + + + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-maven + + enforce + + + + + + ${minimal.maven.version} + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + + + diff --git a/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-11/spring-38-kafka/producer/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-11/spring-38-kafka/producer/mvnw b/2025-11/spring-38-kafka/producer/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-11/spring-38-kafka/producer/mvnw.cmd b/2025-11/spring-38-kafka/producer/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-11/spring-38-kafka/producer/pom.xml b/2025-11/spring-38-kafka/producer/pom.xml new file mode 100644 index 00000000..c3598163 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + ru.otus + spring-38-kafka + 1.0 + + + producer + 1.0 + + + 21 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.kafka + spring-kafka + + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java new file mode 100644 index 00000000..e9d022bb --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/ProducerApp.java @@ -0,0 +1,11 @@ +package ru.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ProducerApp { + public static void main(String[] args) { + SpringApplication.run(ProducerApp.class, args); + } +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java new file mode 100644 index 00000000..5e60699e --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/config/ApplicationConfig.java @@ -0,0 +1,82 @@ +package ru.demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.kafka.KafkaProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.JacksonUtils; +import org.springframework.kafka.support.serializer.JsonSerializer; +import ru.demo.model.StringValue; +import ru.demo.service.DataSender; +import ru.demo.service.DataSenderKafka; +import ru.demo.service.StringValueSource; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Configuration +public class ApplicationConfig { + private static final Logger log = LoggerFactory.getLogger(ApplicationConfig.class); + public final String topicName; + + public ApplicationConfig(@Value("${application.kafka.topic}") String topicName) { + this.topicName = topicName; + } + + @Bean + public ObjectMapper objectMapper() { + return JacksonUtils.enhancedObjectMapper(); + } + + @Bean + public ProducerFactory producerFactory( + KafkaProperties kafkaProperties, ObjectMapper mapper) { + var props = kafkaProperties.buildProducerProperties(); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + + var kafkaProducerFactory = new DefaultKafkaProducerFactory(props); + kafkaProducerFactory.setValueSerializer(new JsonSerializer<>(mapper)); + return kafkaProducerFactory; + } + + @Bean + public KafkaTemplate kafkaTemplate( + ProducerFactory producerFactory) { + return new KafkaTemplate<>(producerFactory); + } + + @Bean + public NewTopic topic() { + // Создание топика с указанным количеством партиций и реплик + return TopicBuilder.name(topicName).partitions(1).replicas(1).build(); + } + + @Bean + public DataSender dataSender(NewTopic topic, KafkaTemplate kafkaTemplate) { + return new DataSenderKafka( + topic.name(), + kafkaTemplate, + stringValue -> log.info("asked, value:{}", stringValue)); + } + + @Bean(value = "sourceExecutor", destroyMethod = "close") + public ScheduledExecutorService sourceExecutor() { + return Executors.newScheduledThreadPool(1); + } + + @Bean + public StringValueSource stringValueSource(DataSender dataSender, ScheduledExecutorService sourceExecutor) { + return new StringValueSource(dataSender, sourceExecutor); + } +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java new file mode 100644 index 00000000..213f1309 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/model/StringValue.java @@ -0,0 +1,3 @@ +package ru.demo.model; + +public record StringValue(long id, String value) {} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java new file mode 100644 index 00000000..321e3f5f --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSender.java @@ -0,0 +1,7 @@ +package ru.demo.service; + +import ru.demo.model.StringValue; + +public interface DataSender { + void send(StringValue value); +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java new file mode 100644 index 00000000..e3494ece --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/DataSenderKafka.java @@ -0,0 +1,49 @@ +package ru.demo.service; + +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import ru.demo.model.StringValue; + +public class DataSenderKafka implements DataSender { + private static final Logger log = LoggerFactory.getLogger(DataSenderKafka.class); + + private final KafkaTemplate template; + + private final Consumer sendAsk; + + private final String topic; + + public DataSenderKafka( + String topic, + KafkaTemplate template, + Consumer sendAsk) { + this.topic = topic; + this.template = template; + this.sendAsk = sendAsk; + } + + @Override + public void send(StringValue value) { + try { + log.info("value:{}", value); + var key = String.valueOf(value.id()); + template.send(topic, key, value) + .whenComplete( + (result, ex) -> { + if (ex == null) { + log.info( + "message id:{} was sent, offset:{}", + value.id(), + result.getRecordMetadata().offset()); + sendAsk.accept(value); + } else { + log.error("message id:{} was not sent", value.id(), ex); + } + }); + } catch (Exception ex) { + log.error("send error, value:{}", value, ex); + } + } +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java new file mode 100644 index 00000000..16c25602 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/Runner.java @@ -0,0 +1,18 @@ +package ru.demo.service; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Service; + +@Service +public class Runner implements CommandLineRunner { + private final ValueSource valueSource; + + public Runner(ValueSource valueSource) { + this.valueSource = valueSource; + } + + @Override + public void run(String... args) { + valueSource.generate(); + } +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java new file mode 100644 index 00000000..4c474f4c --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/StringValueSource.java @@ -0,0 +1,33 @@ +package ru.demo.service; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.demo.model.StringValue; + +public class StringValueSource implements ValueSource { + private static final Logger log = LoggerFactory.getLogger(StringValueSource.class); + private final AtomicLong nextValue = new AtomicLong(1); + private final DataSender valueConsumer; + private final ScheduledExecutorService executor; + + + public StringValueSource(DataSender dataSender, ScheduledExecutorService executor) { + this.valueConsumer = dataSender; + this.executor = executor; + } + + @Override + public void generate() { + executor.scheduleAtFixedRate(() -> valueConsumer.send(makeValue()), 0, 1, TimeUnit.SECONDS); + log.info("generation started"); + } + + private StringValue makeValue() { + var id = nextValue.getAndIncrement(); + return new StringValue(id, "stVal:" + id); + } +} diff --git a/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java new file mode 100644 index 00000000..1204f5e6 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/java/ru/demo/service/ValueSource.java @@ -0,0 +1,5 @@ +package ru.demo.service; + +public interface ValueSource { + void generate(); +} diff --git a/2025-11/spring-38-kafka/producer/src/main/resources/application.yml b/2025-11/spring-38-kafka/producer/src/main/resources/application.yml new file mode 100644 index 00000000..72fa1f46 --- /dev/null +++ b/2025-11/spring-38-kafka/producer/src/main/resources/application.yml @@ -0,0 +1,11 @@ +application: + kafka: + topic: "demo-topic" + +spring: + kafka: + producer: + bootstrap-servers: "127.0.0.1:9092" + client-id: "demo-producer" + + diff --git a/2025-11/spring-39-kafka-webflux/.gitignore b/2025-11/spring-39-kafka-webflux/.gitignore new file mode 100755 index 00000000..d8762a8d --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/.gitignore @@ -0,0 +1,35 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle build output directory +build +out + +#Idea +*.iml +*.iws +*.ipr +*.idea + diff --git a/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2025-11/spring-39-kafka-webflux/client/HttpRequests.http b/2025-11/spring-39-kafka-webflux/client/HttpRequests.http new file mode 100755 index 00000000..3432cafa --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/HttpRequests.http @@ -0,0 +1,11 @@ +### +GET http://localhost:8082/data-mono/16 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8082/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-11/spring-39-kafka-webflux/client/curlLoop.sh b/2025-11/spring-39-kafka-webflux/client/curlLoop.sh new file mode 100755 index 00000000..32d83a38 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/curlLoop.sh @@ -0,0 +1,5 @@ +date +for run in {1..100000}; do + curl -s "http://localhost:8082/data-mono/$run" > /dev/null +done +date \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/client/pom.xml b/2025-11/spring-39-kafka-webflux/client/pom.xml new file mode 100755 index 00000000..b5f78173 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + client + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + io.projectreactor.kafka + reactor-kafka + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.projectreactor + reactor-test + test + + + + com.github.tomakehurst + wiremock + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java new file mode 100755 index 00000000..341f9828 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ClientData { + + public static void main(String[] args) { + SpringApplication.run(ClientData.class, args); + } +} \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java new file mode 100755 index 00000000..1761ca74 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/ClientDataController.java @@ -0,0 +1,58 @@ +package com.datasrc; + + +import com.datasrc.config.ReactiveSender; +import com.datasrc.model.Request; +import com.datasrc.model.RequestId; +import com.datasrc.model.StreamData; +import com.datasrc.model.StringValue; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +// http://localhost:8082/data/5 +@RestController +public class ClientDataController { + private static final Logger log = LoggerFactory.getLogger(ClientDataController.class); + private final AtomicLong idGenerator = new AtomicLong(0); + + private final WebClient webClient; + private final ReactiveSender requestSender; + + private final StringValueStorage stringValueStorage; + + public ClientDataController(WebClient webClient, ReactiveSender requestSender, StringValueStorage stringValueStorage) { + this.webClient = webClient; + this.requestSender = requestSender; + this.stringValueStorage = stringValueStorage; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for data, seed:{}", seed); + + return webClient.get().uri(String.format("/data/%d", seed)) + .accept(MediaType.APPLICATION_NDJSON) + .retrieve() + .bodyToFlux(StreamData.class) + .doOnNext(val -> log.info("val:{}", val)); + } + + @GetMapping(value = "/data-mono/{seed}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono dataMono(@PathVariable("seed") long seed) { + log.info("request for string data-mono, seed:{}", seed); + var request = new Request(new RequestId(idGenerator.incrementAndGet()), seed); + + return requestSender.send(request, requestSend -> log.info("send ok: {}", requestSend)) + .flatMap(v -> stringValueStorage.get(new RequestId(v.correlationMetadata().id()))) + .doOnNext(stringValue -> log.info("value for client:{}", stringValue)); + } +} diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java new file mode 100755 index 00000000..8ffdd2a9 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/StringValueStorage.java @@ -0,0 +1,47 @@ +package com.datasrc; + +import com.datasrc.model.RequestId; +import com.datasrc.model.StringValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.ConnectableFlux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.Sinks; +import reactor.util.annotation.NonNull; + +public class StringValueStorage implements Sinks.EmitFailureHandler { + + private final Sinks.Many sink; + private final ConnectableFlux sinkConnectable; + private static final Logger log = LoggerFactory.getLogger(StringValueStorage.class); + + public StringValueStorage() { + sink = Sinks.many().multicast().onBackpressureBuffer(); + sinkConnectable = sink.asFlux().publish(); + sinkConnectable.connect(); + } + + public void put(RequestId requestId, StringValue value) { + log.info("put. requestId:{}, value:{}", requestId, value); + sink.emitNext(new ResponseData(requestId, value), this); + } + + public Mono get(RequestId requestId) { + return Mono.from(sinkConnectable + .filter(responseData -> { + log.info("waiting:{}, fact:{}", requestId, responseData.requestId); + return responseData.requestId.id() == requestId.id(); + }) + .map(responseData -> responseData.stringValue)); + } + + + @Override + public boolean onEmitFailure(@NonNull SignalType signalType, @NonNull Sinks.EmitResult emitResult) { + return false; + } + + private record ResponseData(RequestId requestId, StringValue stringValue) { + } +} diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..70edb4aa --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,117 @@ +package com.datasrc.config; + +import com.datasrc.StringValueStorage; +import com.datasrc.model.Request; +import com.datasrc.model.Response; +import io.netty.channel.nio.NioEventLoopGroup; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ReactorResourceFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final int THREAD_POOL_SIZE = 4; + private static final int RESPONSE_RECEIVER_POOL_SIZE = 1; + private static final int KAFKA_POOL_SIZE = 1; + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name ="clientThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup clientThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "client-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactorResourceFactory reactorResourceFactory(@Qualifier("clientThreadEventLoop") NioEventLoopGroup clientThreadEventLoop) { + var resourceFactory = new org.springframework.http.client.ReactorResourceFactory(); + resourceFactory.setLoopResources(b -> clientThreadEventLoop); + resourceFactory.setUseGlobalResources(false); + return resourceFactory; + } + + @Bean + public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory resourceFactory) { + return new ReactorClientHttpConnector(resourceFactory, mapper -> mapper); + } + + @Bean("responseReceiverScheduler") + public Scheduler responseReceiverScheduler() { + return Schedulers.newParallel("response-receiver", RESPONSE_RECEIVER_POOL_SIZE); + } + + @Bean("kafkaScheduler") + public Scheduler kafkaScheduler() { + return Schedulers.newParallel("kafka-scheduler", KAFKA_POOL_SIZE); + } + + @Bean + public WebClient webClient(WebClient.Builder builder, + @Value("${application.processor.url}") String url) { + return builder + .baseUrl(url) + .build(); + } + + @Bean(destroyMethod = "close") + public ReactiveSender requestSender(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-request}") String topicRequest, + @Qualifier("kafkaScheduler") Scheduler kafkaScheduler + ) { + return new ReactiveSender<>(bootstrapServers, kafkaScheduler, topicRequest); + } + + @Bean + public StringValueStorage stringValueStorage() { + return new StringValueStorage(); + } + + @Bean(destroyMethod = "close") + public ReactiveReceiver responseReceiver(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-response}") String topicResponse, + @Value("${application.kafka-group-id}") String groupId, + @Qualifier("responseReceiverScheduler") Scheduler responseReceiverScheduler, + StringValueStorage stringValueStorage) { + return new ReactiveReceiver<>(bootstrapServers, Response.class, topicResponse, responseReceiverScheduler, groupId, + response -> stringValueStorage.put(response.data().requestId(), response.data())); + } + +} diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml b/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml new file mode 100755 index 00000000..29b56723 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8082 + +application: + processor: + url: http://localhost:8081 + kafka-bootstrap-servers: localhost:9092 + kafka-group-id: clientConsumerGroup + topic-request: request + topic-response: response diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml b/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html new file mode 100755 index 00000000..d42e4d95 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/index.html @@ -0,0 +1,16 @@ + + + + + + + + Stream Demo + + +

Stream Demo

+
+ + + + \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js new file mode 100755 index 00000000..9287c9b8 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/client/src/main/resources/static/webclient.js @@ -0,0 +1,24 @@ +const streamErr = e => { + console.warn("error"); + console.warn(e); +} + +fetch("http://localhost:8082/data/5").then((response) => { + return can.ndjsonStream(response.body); +}).then(dataStream => { + const reader = dataStream.getReader(); + const read = result => { + if (result.done) { + return; + } + render(result.value); + reader.read().then(read, streamErr); + } + reader.read().then(read, streamErr); +}); + +const render = value => { + const div = document.createElement('div'); + div.append(new Date() + ' stringValue:', JSON.stringify(value)); + document.getElementById('dataBlock').append(div); +}; \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/common/HttpRequests.http b/2025-11/spring-39-kafka-webflux/common/HttpRequests.http new file mode 100755 index 00000000..83afa6d0 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/HttpRequests.http @@ -0,0 +1,11 @@ +### +GET http://localhost:8082/data-mono/13 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8082/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2025-11/spring-39-kafka-webflux/common/curlLoop.sh b/2025-11/spring-39-kafka-webflux/common/curlLoop.sh new file mode 100755 index 00000000..32d83a38 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/curlLoop.sh @@ -0,0 +1,5 @@ +date +for run in {1..100000}; do + curl -s "http://localhost:8082/data-mono/$run" > /dev/null +done +date \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/common/pom.xml b/2025-11/spring-39-kafka-webflux/common/pom.xml new file mode 100755 index 00000000..0091a0e8 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + common + 1.0 + + + 21 + + + + + ch.qos.logback + logback-classic + provided + + + + io.projectreactor.kafka + reactor-kafka + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java new file mode 100755 index 00000000..1df2c495 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ConsumerException.java @@ -0,0 +1,7 @@ +package com.datasrc.config; + +public class ConsumerException extends RuntimeException { + public ConsumerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java new file mode 100755 index 00000000..fa76ab49 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonDeserializer.java @@ -0,0 +1,42 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Deserializer; + +public class JsonDeserializer implements Deserializer { + public static final String OBJECT_MAPPER = "objectMapper"; + public static final String TYPE_REFERENCE = "typeReference"; + private final String encoding = StandardCharsets.UTF_8.name(); + private ObjectMapper mapper; + private JavaType javaType; + + @Override + public void configure(Map configs, boolean isKey) { + mapper = (ObjectMapper) configs.get(OBJECT_MAPPER); + if (mapper == null) { + throw new IllegalArgumentException("config property OBJECT_MAPPER was not set"); + } + javaType = (JavaType) configs.get(TYPE_REFERENCE); + if (javaType == null) { + throw new IllegalArgumentException("config property TYPE_REFERENCE was not set"); + } + } + + @Override + public T deserialize(String topic, byte[] data) { + try { + if (data == null) { + return null; + } else { + var valueAsString = new String(data, encoding); + return mapper.readValue(valueAsString, javaType); + } + } catch (Exception e) { + throw new SerializationException("Error when deserializing byte[] to StringValue", e); + } + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java new file mode 100755 index 00000000..84b48d44 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/JsonSerializer.java @@ -0,0 +1,34 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Serializer; + +public class JsonSerializer implements Serializer { + public static final String OBJECT_MAPPER = "objectMapper"; + private final String encoding = StandardCharsets.UTF_8.name(); + private ObjectMapper mapper; + + @Override + public void configure(Map configs, boolean isKey) { + mapper = (ObjectMapper) configs.get(OBJECT_MAPPER); + if (mapper == null) { + throw new IllegalArgumentException("config property OBJECT_MAPPER was not set"); + } + } + + @Override + public byte[] serialize(String topic, T data) { + try { + if (data == null) { + return new byte[]{}; + } else { + return mapper.writeValueAsString(data).getBytes(encoding); + } + } catch (Exception e) { + throw new SerializationException("Error when serializing StringValue to byte[] ", e); + } + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java new file mode 100755 index 00000000..02cd1d35 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveReceiver.java @@ -0,0 +1,127 @@ +package com.datasrc.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.InetAddress; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Properties; +import java.util.Random; +import java.util.concurrent.CancellationException; +import java.util.function.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.serialization.LongDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.scheduler.Scheduler; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.util.retry.Retry; + +import static com.datasrc.config.JsonDeserializer.OBJECT_MAPPER; +import static com.datasrc.config.JsonDeserializer.TYPE_REFERENCE; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.GROUP_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.GROUP_INSTANCE_ID_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.MAX_POLL_RECORDS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG; + +public class ReactiveReceiver { + private static final Logger log = LoggerFactory.getLogger(ReactiveReceiver.class); + private final Random random = new Random(); + + private final Disposable kafkaSubscriber; + private final Disposable kafkaConnection; + + public static final int MAX_POLL_INTERVAL_MS = 1_000; + + public ReactiveReceiver( + String bootstrapServers, Class valueClass, String topicName, Scheduler schedulerValueReceiver, String groupId, Consumer valueConsumer + ) { + Properties props = new Properties(); + props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(GROUP_ID_CONFIG, groupId); + props.put(GROUP_INSTANCE_ID_CONFIG, makeGroupInstanceIdConfig(groupId)); + props.put(ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(AUTO_COMMIT_INTERVAL_MS_CONFIG, "100"); + props.put(AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(KEY_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); + props.put(VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); + + var objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + props.put(OBJECT_MAPPER, objectMapper); + props.put(TYPE_REFERENCE, objectMapper.getTypeFactory().constructType(valueClass)); + + props.put(MAX_POLL_RECORDS_CONFIG, 3); + props.put(MAX_POLL_INTERVAL_MS_CONFIG, MAX_POLL_INTERVAL_MS); + + ReceiverOptions receiverOptions = + ReceiverOptions.create(props) + .pollTimeout(Duration.ofSeconds(500)) + .schedulerSupplier(() -> schedulerValueReceiver) + .subscription(Collections.singleton(topicName)); + + Flux> inboundFlux = KafkaReceiver.create(receiverOptions) + .receiveAutoAck() + .concatMap( + consumerRecordFlux -> { + log.info("consumerRecordFlux done, commit"); + return consumerRecordFlux; + }) + .retryWhen(Retry.backoff(3, Duration.of(5, ChronoUnit.SECONDS))); + + Hooks.onErrorDropped( + error -> { + if (error instanceof CancellationException) { + log.info("Cancellation event:", error); + } else { + log.error("error:", error); + } + }); + + var kafkaFlow = inboundFlux.doOnCancel(() -> log.info("connection canceled")) + .doOnError(error -> log.error("Consuming error", error)) + .publish(); + + log.info("start consuming"); + kafkaConnection = kafkaFlow.connect(); + kafkaSubscriber = + kafkaFlow.subscribe( + receiverRecord -> { + var key = receiverRecord.key(); + var value = receiverRecord.value(); + log.info("key:{}, value:{}, record:{}", key, value, receiverRecord); + valueConsumer.accept(value); + }); + } + + public void close() { + log.info("stop consuming"); + kafkaSubscriber.dispose(); + kafkaConnection.dispose(); + } + + private String makeGroupInstanceIdConfig(String groupId) { + try { + var hostName = InetAddress.getLocalHost().getHostName(); + return String.join( + "-", + groupId, + hostName, + String.valueOf(random.nextInt(100_999_999))); + } catch (Exception ex) { + throw new ConsumerException("can't make GroupInstanceIdConfig", ex); + } + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java new file mode 100755 index 00000000..362708dd --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/config/ReactiveSender.java @@ -0,0 +1,92 @@ +package com.datasrc.config; + +import static com.datasrc.config.JsonSerializer.OBJECT_MAPPER; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.RETRIES_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.BUFFER_MEMORY_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.MAX_BLOCK_MS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; + +import com.datasrc.model.DataForSending; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Properties; +import java.util.function.Consumer; +import org.apache.kafka.common.serialization.LongSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.SenderRecord; +import reactor.kafka.sender.SenderResult; + +public class ReactiveSender> { + private static final Logger log = LoggerFactory.getLogger(ReactiveSender.class); + private final KafkaSender sender; + private final String topicName; + + public ReactiveSender(String bootstrapServers, Scheduler schedulerKafka, String topicName) { + this.topicName = topicName; + var props = new Properties(); + props.put(CLIENT_ID_CONFIG, "KafkaReactiveSender"); + props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ACKS_CONFIG, "1"); + props.put(RETRIES_CONFIG, 1); + props.put(BATCH_SIZE_CONFIG, 16384); + props.put(LINGER_MS_CONFIG, 10); + props.put(BUFFER_MEMORY_CONFIG, 26384); // bytes + props.put(MAX_BLOCK_MS_CONFIG, 1_000); // ms + props.put(KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class); + props.put(VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + + var objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + props.put(OBJECT_MAPPER, objectMapper); + + SenderOptions senderOptions = + SenderOptions.create(props) + .maxInFlight(10) + .scheduler(schedulerKafka); + + sender = KafkaSender.create(senderOptions); + + var shutdownHook = + new Thread( + () -> { + log.info("closing kafka sender"); + sender.close(); + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + public Mono> send(T data, Consumer sendAsk) { + return sender.send(Mono.just(SenderRecord.create( + topicName, + null, + null, + data.id(), + data, + data))) + .doOnError(error -> log.error("Send failed", error)) + .doOnNext( + senderResult -> { + log.info( + "message id:{} was sent, offset:{}", + senderResult.correlationMetadata().id(), + senderResult.recordMetadata().offset()); + sendAsk.accept(senderResult.correlationMetadata()); + }).next(); + } + + public void close() { + sender.close(); + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java new file mode 100755 index 00000000..6aa91026 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/DataForSending.java @@ -0,0 +1,7 @@ +package com.datasrc.model; + +public interface DataForSending { + long id(); + + T data(); +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java new file mode 100755 index 00000000..00623e66 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Request.java @@ -0,0 +1,34 @@ +package com.datasrc.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Request implements DataForSending { + + private final RequestId id; + private final long seed; + + @JsonCreator + public Request(@JsonProperty("id") RequestId id, @JsonProperty("seed") long seed) { + this.id = id; + this.seed = seed; + } + + @Override + public long id() { + return id.id(); + } + + @Override + public Long data() { + return seed; + } + + @Override + public String toString() { + return "Request{" + + "id=" + id + + ", seed=" + seed + + '}'; + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java new file mode 100755 index 00000000..5a602848 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/RequestId.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record RequestId(long id) { +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java new file mode 100755 index 00000000..a224a08d --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/Response.java @@ -0,0 +1,33 @@ +package com.datasrc.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Response implements DataForSending { + private final ResponseId id; + private final StringValue stringValue; + + @JsonCreator + public Response(@JsonProperty("id") ResponseId id, @JsonProperty("stringValue") StringValue stringValue) { + this.id = id; + this.stringValue = stringValue; + } + + @Override + public long id() { + return id.id(); + } + + @Override + public StringValue data() { + return stringValue; + } + + @Override + public String toString() { + return "Response{" + + "id=" + id + + ", stringValue=" + stringValue + + '}'; + } +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java new file mode 100755 index 00000000..bf7d7d73 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/ResponseId.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record ResponseId(long id) { +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java new file mode 100755 index 00000000..cc199ad4 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StreamData.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record StreamData(String value) { +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java new file mode 100755 index 00000000..43568176 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/java/com/datasrc/model/StringValue.java @@ -0,0 +1,4 @@ +package com.datasrc.model; + +public record StringValue(RequestId requestId, String value) { +} diff --git a/2025-11/spring-39-kafka-webflux/common/src/main/resources/logback.xml b/2025-11/spring-39-kafka-webflux/common/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/common/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-11/spring-39-kafka-webflux/docker/docker-compose.yml b/2025-11/spring-39-kafka-webflux/docker/docker-compose.yml new file mode 100755 index 00000000..c264d84e --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/docker/docker-compose.yml @@ -0,0 +1,23 @@ +services: + zookeeper: + image: confluentinc/cp-zookeeper:6.2.0 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + broker: + image: confluentinc/cp-kafka:7.0.0 + container_name: broker + ports: + - "9092:9092" + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/mvnw b/2025-11/spring-39-kafka-webflux/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2025-11/spring-39-kafka-webflux/mvnw.cmd b/2025-11/spring-39-kafka-webflux/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2025-11/spring-39-kafka-webflux/pom.xml b/2025-11/spring-39-kafka-webflux/pom.xml new file mode 100755 index 00000000..e24e7f10 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + ru.otus + spring-39-kafka-webflux + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.3 + + + pom + + + client + processor + source + common + + + + UTF-8 + 21 + 21 + 3.0.0-M3 + 3.1.1 + 3.3.9 + 1.0.12.RELEASE + 3.4.3 + 3.0.2 + 3.0.0-beta-10 + 1.3.23 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springframeworkBoot.version} + pom + import + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + + io.projectreactor.kafka + reactor-kafka + ${reactorKafka.version} + + + com.github.tomakehurst + wiremock + ${wiremock.version} + + + + + + + + + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-maven + + enforce + + + + + + ${minimal.maven.version} + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + + + diff --git a/2025-11/spring-39-kafka-webflux/processor/pom.xml b/2025-11/spring-39-kafka-webflux/processor/pom.xml new file mode 100755 index 00000000..d266d2df --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + processor + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + io.projectreactor.kafka + reactor-kafka + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java new file mode 100755 index 00000000..6d911292 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ProcessorData { + + public static void main(String[] args) { + SpringApplication.run(ProcessorData.class, args); + } +} \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java new file mode 100755 index 00000000..0b89514f --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/ProcessorDataController.java @@ -0,0 +1,40 @@ +package com.datasrc; + + +import com.datasrc.model.StreamData; +import com.datasrc.processor.DataProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +@RestController +public class ProcessorDataController { + private static final Logger log = LoggerFactory.getLogger(ProcessorDataController.class); + + private final DataProcessor> dataProcessorStringReactorFlux; + private final WebClient client; + + public ProcessorDataController(WebClient client, + @Qualifier("dataProcessorFlux") DataProcessor> dataProcessorFlux) { + this.dataProcessorStringReactorFlux = dataProcessorFlux; + this.client = client; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for data, seed:{}", seed); + + var srcRequest = client.get().uri(String.format("/data/%d", seed)) + .accept(MediaType.APPLICATION_NDJSON) + .retrieve() + .bodyToFlux(StreamData.class); + + return dataProcessorStringReactorFlux.process(srcRequest); + } +} diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..d091bfc7 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,136 @@ +package com.datasrc.config; + +import com.datasrc.model.Request; +import com.datasrc.model.RequestId; +import com.datasrc.model.Response; +import com.datasrc.model.ResponseId; +import com.datasrc.model.StringValue; +import com.datasrc.processor.DataProcessor; +import io.netty.channel.nio.NioEventLoopGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.client.ReactorResourceFactory; + + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final Logger log = LoggerFactory.getLogger(ApplConfig.class); + private static final int THREAD_POOL_SIZE = 2; + private static final int REQUEST_RECEIVER_POOL_SIZE = 1; + private static final int KAFKA_POOL_SIZE = 1; + + private final AtomicLong responseIdGenerator = new AtomicLong(0); + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name ="clientThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup clientThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "client-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactorResourceFactory reactorResourceFactory(@Qualifier("clientThreadEventLoop") NioEventLoopGroup clientThreadEventLoop) { + var resourceFactory = new ReactorResourceFactory(); + resourceFactory.setLoopResources(b -> clientThreadEventLoop); + resourceFactory.setUseGlobalResources(false); + return resourceFactory; + } + + @Bean + public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory resourceFactory) { + return new ReactorClientHttpConnector(resourceFactory, mapper -> mapper); + } + + @Bean + public Scheduler timer() { + return Schedulers.newParallel("processor-thread", 2); + } + + + @Bean("requestReceiverScheduler") + public Scheduler requestReceiverScheduler() { + return Schedulers.newParallel("request-receiver", REQUEST_RECEIVER_POOL_SIZE); + } + + @Bean("kafkaScheduler") + public Scheduler kafkaScheduler() { + return Schedulers.newParallel("kafka-scheduler", KAFKA_POOL_SIZE); + } + + @Bean + public WebClient webClient(WebClient.Builder builder, + @Value("${application.source.url}") String url) { + return builder + .baseUrl(url) + .build(); + } + + @Bean(destroyMethod = "close") + public ReactiveSender responseSender(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-response}") String topicResponse, + @Qualifier("kafkaScheduler") Scheduler kafkaScheduler + ) { + return new ReactiveSender<>(bootstrapServers, kafkaScheduler, topicResponse); + } + + @Bean(destroyMethod = "close") + public ReactiveReceiver requestReceiver(@Value("${application.kafka-bootstrap-servers}") String bootstrapServers, + @Value("${application.topic-request}") String topicRequest, + @Value("${application.kafka-group-id}") String groupId, + @Qualifier("requestReceiverScheduler") Scheduler responseReceiverScheduler, + ReactiveSender responseSender, + @Qualifier("dataProcessorMono") DataProcessor dataProcessor, + WebClient webClient) { + + return new ReactiveReceiver<>(bootstrapServers, Request.class, topicRequest, responseReceiverScheduler, groupId, + request -> webClient.get().uri(String.format("/data-mono/%d", request.data())) + .retrieve() + .bodyToMono(String.class) + .map(dataProcessor::process) + .flatMap(stringValue -> + responseSender.send(new Response(new ResponseId(responseIdGenerator.incrementAndGet()), + new StringValue(new RequestId(request.id()), stringValue)), + stringValueDataForSending -> log.info("response send:{}", stringValueDataForSending))) + .subscribe()); + } +} diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java new file mode 100755 index 00000000..55b9c4cd --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessor.java @@ -0,0 +1,6 @@ +package com.datasrc.processor; + +public interface DataProcessor { + + T process(T data); +} diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java new file mode 100755 index 00000000..c0a5155a --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorFlux.java @@ -0,0 +1,33 @@ +package com.datasrc.processor; + +import com.datasrc.model.StreamData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; + +import java.time.Duration; + +@Service("dataProcessorFlux") +public class DataProcessorStringReactorFlux implements DataProcessor> { + private static final Logger log = LoggerFactory.getLogger(DataProcessorStringReactorFlux.class); + private final Scheduler timer; + + public DataProcessorStringReactorFlux(Scheduler timer) { + this.timer = timer; + } + + @Override + public Flux process(Flux dataflow) { + log.info("processor"); + var dataSeq = dataflow + .doOnNext(val -> log.info("in val:{}", val)) + .delayElements(Duration.ofSeconds(5), timer) + .map(data -> new StreamData(data.value().toUpperCase())) + .doOnNext(val -> log.info("out val:{}", val)); + + log.info("processor method finished"); + return dataSeq; + } +} diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java new file mode 100755 index 00000000..f102c368 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/java/com/datasrc/processor/DataProcessorStringReactorMono.java @@ -0,0 +1,16 @@ +package com.datasrc.processor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service("dataProcessorMono") +public class DataProcessorStringReactorMono implements DataProcessor { + private static final Logger log = LoggerFactory.getLogger(DataProcessorStringReactorMono.class); + + @Override + public String process(String value) { + log.info("processor"); + return value.toUpperCase(); + } +} diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/resources/application.yml b/2025-11/spring-39-kafka-webflux/processor/src/main/resources/application.yml new file mode 100755 index 00000000..ffa5de55 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8081 + +application: + source: + url: http://localhost:8080 + kafka-bootstrap-servers: localhost:9092 + kafka-group-id: processorConsumerGroup + topic-request: request + topic-response: response diff --git a/2025-11/spring-39-kafka-webflux/processor/src/main/resources/logback.xml b/2025-11/spring-39-kafka-webflux/processor/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/processor/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2025-11/spring-39-kafka-webflux/source/HttpRequests.http b/2025-11/spring-39-kafka-webflux/source/HttpRequests.http new file mode 100755 index 00000000..be988df3 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/HttpRequests.http @@ -0,0 +1,12 @@ +### +GET http://localhost:8080/data-mono/13 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + + +### +GET http://localhost:8080/data/5 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/source/pom.xml b/2025-11/spring-39-kafka-webflux/source/pom.xml new file mode 100755 index 00000000..9e828243 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + ru.otus + spring-39-kafka-webflux + 1.0 + + + source + 1.0 + + + 21 + + + + + ru.otus + common + 1.0 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java new file mode 100755 index 00000000..a321fbde --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceData.java @@ -0,0 +1,12 @@ +package com.datasrc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SourceData { + + public static void main(String[] args) { + SpringApplication.run(SourceData.class, args); + } +} \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java new file mode 100755 index 00000000..feb4291a --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/SourceDataController.java @@ -0,0 +1,54 @@ +package com.datasrc; + + +import com.datasrc.model.StreamData; +import com.datasrc.producer.DataProducer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +public class SourceDataController { + private static final Logger log = LoggerFactory.getLogger(SourceDataController.class); + + private final DataProducer> dataProducerFlux; + private final DataProducer dataProducerStringBlocked; + + private final Executor blockingExecutor; + + public SourceDataController(@Qualifier("dataProducerFlux") DataProducer> dataProducerFlux, + @Qualifier("dataProducerStringBlocked") DataProducer dataProducerStringBlocked, + @Qualifier("blockingExecutor") Executor blockingExecutor) { + this.dataProducerFlux = dataProducerFlux; + this.dataProducerStringBlocked = dataProducerStringBlocked; + this.blockingExecutor = blockingExecutor; + } + + @GetMapping(value = "/data/{seed}", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux data(@PathVariable("seed") long seed) { + log.info("request for string data, seed:{}", seed); + var stringData = dataProducerFlux.produce(seed); + + log.info("Method request for string data done"); + return stringData; + } + + @GetMapping(value = "/data-mono/{seed}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono dataMono(@PathVariable("seed") long seed) { + log.info("request for string data-mono, seed:{}", seed); + + var future = CompletableFuture + .supplyAsync(() -> dataProducerStringBlocked.produce(seed), blockingExecutor); + var mono = Mono.fromFuture(future); + log.info("Method request for string data done"); + return mono; + } +} diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java new file mode 100755 index 00000000..7186ef9a --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/config/ApplConfig.java @@ -0,0 +1,56 @@ +package com.datasrc.config; + +import io.netty.channel.nio.NioEventLoopGroup; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +@SuppressWarnings("java:S2095") +public class ApplConfig { + private static final int THREAD_POOL_SIZE = 2; + private static final int BLOCKING_THREAD_POOL_SIZE = 2; + + @Bean(name ="serverThreadEventLoop", destroyMethod = "close") + public NioEventLoopGroup serverThreadEventLoop() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(@Qualifier("serverThreadEventLoop") NioEventLoopGroup serverThreadEventLoop) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(serverThreadEventLoop)); + return factory; + } + + @Bean(name= "blockingExecutor", destroyMethod = "close") + public ExecutorService blockingExecutor() { + var id = new AtomicLong(0); + return Executors.newFixedThreadPool(BLOCKING_THREAD_POOL_SIZE, + task -> new Thread(task, String.format("blocking-thread-%d", id.incrementAndGet()))); + } + + @Bean + public Scheduler timer() { + return Schedulers.newParallel("processor-thread", 2); + } +} diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java new file mode 100755 index 00000000..e205bb5e --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducer.java @@ -0,0 +1,6 @@ +package com.datasrc.producer; + +public interface DataProducer { + + T produce(long seed); +} diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java new file mode 100755 index 00000000..d9cf933b --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringBlocked.java @@ -0,0 +1,26 @@ +package com.datasrc.producer; + +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service("dataProducerStringBlocked") +public class DataProducerStringBlocked implements DataProducer { + private static final Logger log = LoggerFactory.getLogger(DataProducerStringBlocked.class); + + @Override + public String produce(long seed) { + log.info("produce using seed:{}", seed); + sleep(); + return String.format("someDataStr:%s", seed); + } + + private void sleep() { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(10)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java new file mode 100755 index 00000000..b8250eda --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/java/com/datasrc/producer/DataProducerStringFlux.java @@ -0,0 +1,39 @@ +package com.datasrc.producer; + +import com.datasrc.model.StreamData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; +import java.time.Duration; +import java.util.function.BiFunction; +import reactor.core.scheduler.Scheduler; + +@Service("dataProducerFlux") +public class DataProducerStringFlux implements DataProducer> { + private static final Logger log = LoggerFactory.getLogger(DataProducerStringFlux.class); + private final Scheduler timer; + + public DataProducerStringFlux(Scheduler timer) { + this.timer = timer; + } + + @Override + public Flux produce(long seed) { + log.info("produce using seed:{}", seed); + var stringSeed = "someDataStr:"; + var dataSeq = Flux.generate(() -> seed, + (BiFunction, Long>) (prev, sink) -> { + var newValue = prev + 1; + sink.next(newValue); + return newValue; + }) + .delayElements(Duration.ofSeconds(3), timer) + .map(val -> new StreamData(stringSeed + val)) + .doOnNext(val -> log.info("val:{}", val)); + + log.info("produce method finished"); + return dataSeq; + } +} diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/resources/application.yml b/2025-11/spring-39-kafka-webflux/source/src/main/resources/application.yml new file mode 100755 index 00000000..47fbb02d --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 8080 \ No newline at end of file diff --git a/2025-11/spring-39-kafka-webflux/source/src/main/resources/logback.xml b/2025-11/spring-39-kafka-webflux/source/src/main/resources/logback.xml new file mode 100755 index 00000000..b1f9bfe2 --- /dev/null +++ b/2025-11/spring-39-kafka-webflux/source/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/pom.xml b/2026-01/spring-09-jdbc/jdbc-demo-exercise/pom.xml new file mode 100644 index 00000000..16b94a8f --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + ru.otus + jdbc-class-work + 1.0 + + + jdbc-demo-exercise + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + ${h2.version} + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..8916eb04 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,20 @@ +package ru.otus.spring; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.dao.PersonDao; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws Exception { + + ApplicationContext context = SpringApplication.run(Main.class); + + PersonDao dao = context.getBean(PersonDao.class); + + Console.main(args); + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java new file mode 100644 index 00000000..d5df8878 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java @@ -0,0 +1,41 @@ +package ru.otus.spring.dao; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +@Repository +public class JdbcPersonDao implements PersonDao { + private final JdbcOperations jdbc; + + public JdbcPersonDao(JdbcOperations jdbcOperations) { + this.jdbc = jdbcOperations; + } + + @Override + public int count() { + return 0; + } + + @Override + public void insert(Person person) { + + } + + @Override + public Person getById(long id) { + return null; + } + + @Override + public List getAll() { + return null; + } + + @Override + public void deleteById(long id) { + + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/PersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/PersonDao.java new file mode 100644 index 00000000..9fa01df3 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/dao/PersonDao.java @@ -0,0 +1,17 @@ +package ru.otus.spring.dao; + +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonDao { + int count(); + + void insert(Person person); + + Person getById(long id); + + List getAll(); + + void deleteById(long id); +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..92553c2d --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,4 @@ +package ru.otus.spring.domain; + +public record Person(long id, String name) { +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..8f40ea4e --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #schema: schema.sql + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql + h2: + console: + path: /h2-console + settings: + web-allow-others: true \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/data.sql new file mode 100644 index 00000000..9de9daab --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'masha'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/schema.sql b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/schema.sql new file mode 100644 index 00000000..583bbae0 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS PERSONS; +CREATE TABLE PERSONS +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(255) +); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java new file mode 100644 index 00000000..45721e7c --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java @@ -0,0 +1,92 @@ +package ru.otus.spring.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import ru.otus.spring.domain.Person; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Dao для работы с пёрсонами должно") +@JdbcTest +@Import(JdbcPersonDao.class) +class JdbcPersonDaoTest { + + private static final int EXPECTED_PERSONS_COUNT = 1; + private static final int EXISTING_PERSON_ID = 1; + private static final String EXISTING_PERSON_NAME = "Ivan"; + + @Autowired + private JdbcPersonDao personDao; + + @DisplayName("возвращать ожидаемое количество пёрсонов в БД") + @Test + void shouldReturnExpectedPersonCount() { + int actualPersonsCount = personDao.count(); + assertThat(actualPersonsCount).isEqualTo(EXPECTED_PERSONS_COUNT); + } + + @DisplayName("добавлять пёрсона в БД") + @Test + void shouldInsertPerson() { + int countBeforeInsert = personDao.count(); + assertThat(countBeforeInsert).isEqualTo(EXPECTED_PERSONS_COUNT); + + Person expectedPerson = new Person(2, "Igor"); + personDao.insert(expectedPerson); + + // Ошибка! Сейчас так проверяем т.к. больше нет других способов, + // когда появится getById, будем использовать его + int countAfterInsert = personDao.count(); + assertThat(countAfterInsert).isEqualTo(countBeforeInsert + 1); +/* + Person actualPerson = personDao.getById(expectedPerson.getId()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); +*/ + } + + @DisplayName("возвращать ожидаемого пёрсона по его id") + @Test + void shouldReturnExpectedPersonById() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + Person actualPerson = personDao.getById(expectedPerson.id()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); + } + + @DisplayName("удалять заданного пёрсона по его id") + @Test + void shouldCorrectDeletePersonById() { + // Ошибка! Сейчас так проверяем т.к. больше нет других способов, + // когда появится getById, тест будет выглядеть, как закомментированный блок ниже + int countBeforeDelete = personDao.count(); + assertThat(countBeforeDelete).isEqualTo(EXPECTED_PERSONS_COUNT); + + personDao.deleteById(EXISTING_PERSON_ID); + + int countAfterDelete = personDao.count(); + assertThat(countAfterDelete).isEqualTo(countBeforeDelete - 1); + + /* + assertThatCode(() -> personDao.getById(EXISTING_PERSON_ID)) + .doesNotThrowAnyException(); + + personDao.deleteById(EXISTING_PERSON_ID); + + assertThatThrownBy(() -> personDao.getById(EXISTING_PERSON_ID)) + .isInstanceOf(EmptyResultDataAccessException.class); + */ + } + + @DisplayName("возвращать ожидаемый список пёрсонов") + @Test + void shouldReturnExpectedPersonsList() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + List actualPersonList = personDao.getAll(); + assertThat(actualPersonList) + .containsExactlyInAnyOrder(expectedPerson); + } +} \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/application.yml new file mode 100644 index 00000000..f7de231a --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + #schema-locations: schema.sql diff --git a/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/data.sql new file mode 100644 index 00000000..e58b8502 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-exercise/src/test/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'Ivan'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/pom.xml b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/pom.xml new file mode 100644 index 00000000..fa78daa1 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + ru.otus + jdbc-class-work + 1.0 + + + jdbc-demo-solution-3 + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + ${h2.version} + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..7ec142b6 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,22 @@ +package ru.otus.spring; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.dao.PersonDao; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws Exception { + + ApplicationContext context = SpringApplication.run(Main.class); + + PersonDao dao = context.getBean(PersonDao.class); + + System.out.println("All count " + dao.count()); + + Console.main(args); + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java new file mode 100644 index 00000000..ae91105b --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java @@ -0,0 +1,44 @@ +package ru.otus.spring.dao; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +@Repository +public class JdbcPersonDao implements PersonDao { + + private final JdbcOperations jdbc; + + public JdbcPersonDao(JdbcOperations jdbcOperations) { + this.jdbc = jdbcOperations; + } + + @Override + public int count() { + Integer count = jdbc.queryForObject("select count(*) from persons", Integer.class); + return count == null ? 0 : count; + } + + @Override + public void insert(Person person) { + + } + + @Override + public Person getById(long id) { + return null; + } + + @Override + public List getAll() { + return null; + } + + @Override + public void deleteById(long id) { + + } + +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/PersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/PersonDao.java new file mode 100644 index 00000000..f2c30f7c --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/dao/PersonDao.java @@ -0,0 +1,18 @@ +package ru.otus.spring.dao; + +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonDao { + + int count(); + + void insert(Person person); + + Person getById(long id); + + List getAll(); + + void deleteById(long id); +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..92553c2d --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,4 @@ +package ru.otus.spring.domain; + +public record Person(long id, String name) { +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/application.yml new file mode 100644 index 00000000..8f40ea4e --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #schema: schema.sql + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql + h2: + console: + path: /h2-console + settings: + web-allow-others: true \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/data.sql new file mode 100644 index 00000000..9de9daab --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'masha'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/schema.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/schema.sql new file mode 100644 index 00000000..583bbae0 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS PERSONS; +CREATE TABLE PERSONS +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(255) +); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java new file mode 100644 index 00000000..b756d4c5 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java @@ -0,0 +1,27 @@ +package ru.otus.spring.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Dao для работы с пёрсонами должно") +@JdbcTest +@Import(JdbcPersonDao.class) +class JdbcPersonDaoTest { + + private static final int EXPECTED_PERSONS_COUNT = 1; + + @Autowired + private JdbcPersonDao personDao; + + @DisplayName("возвращать ожидаемое количество пёрсонов в БД") + @Test + void shouldReturnExpectedPersonCount() { + int actualPersonsCount = personDao.count(); + assertThat(actualPersonsCount).isEqualTo(EXPECTED_PERSONS_COUNT); + } +} \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/application.yml new file mode 100644 index 00000000..f7de231a --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + #schema-locations: schema.sql diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/data.sql new file mode 100644 index 00000000..e58b8502 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-3/src/test/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'Ivan'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/pom.xml b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/pom.xml new file mode 100644 index 00000000..7abe25e4 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + ru.otus + jdbc-class-work + 1.0 + + + jdbc-demo-solution-4 + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + ${h2.version} + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..2bc8e898 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,27 @@ +package ru.otus.spring; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.dao.PersonDao; +import ru.otus.spring.domain.Person; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws Exception { + + ApplicationContext context = SpringApplication.run(Main.class); + + PersonDao dao = context.getBean(PersonDao.class); + + System.out.println("All count " + dao.count()); + + dao.insert(new Person(2, "ivan")); + + System.out.println("All count " + dao.count()); + + Console.main(args); + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java new file mode 100644 index 00000000..ccaf3644 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java @@ -0,0 +1,45 @@ +package ru.otus.spring.dao; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Person; + +import java.util.List; + + +@Repository +public class JdbcPersonDao implements PersonDao { + + private final JdbcOperations jdbc; + + public JdbcPersonDao(JdbcOperations jdbcOperations) { + this.jdbc = jdbcOperations; + } + + @Override + public int count() { + Integer count = jdbc.queryForObject("select count(*) from persons", Integer.class); + return count == null ? 0 : count; + } + + @Override + public void insert(Person person) { + jdbc.update("insert into persons (id, name) values (?, ?)", person.id(), person.name()); + } + + @Override + public Person getById(long id) { + return null; + } + + @Override + public List getAll() { + return null; + } + + @Override + public void deleteById(long id) { + jdbc.update("delete from persons where id = ?", id); + } + +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/PersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/PersonDao.java new file mode 100644 index 00000000..f2c30f7c --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/dao/PersonDao.java @@ -0,0 +1,18 @@ +package ru.otus.spring.dao; + +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonDao { + + int count(); + + void insert(Person person); + + Person getById(long id); + + List getAll(); + + void deleteById(long id); +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..92553c2d --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,4 @@ +package ru.otus.spring.domain; + +public record Person(long id, String name) { +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/application.yml new file mode 100644 index 00000000..8f40ea4e --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #schema: schema.sql + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql + h2: + console: + path: /h2-console + settings: + web-allow-others: true \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/data.sql new file mode 100644 index 00000000..9de9daab --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'masha'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/schema.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/schema.sql new file mode 100644 index 00000000..583bbae0 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS PERSONS; +CREATE TABLE PERSONS +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(255) +); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java new file mode 100644 index 00000000..7841a4d8 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java @@ -0,0 +1,73 @@ +package ru.otus.spring.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import ru.otus.spring.domain.Person; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Dao для работы с пёрсонами должно") +@JdbcTest +@Import(JdbcPersonDao.class) +class JdbcPersonDaoTest { + + private static final int EXPECTED_PERSONS_COUNT = 1; + private static final int EXISTING_PERSON_ID = 1; + private static final String EXISTING_PERSON_NAME = "Ivan"; + + @Autowired + private JdbcPersonDao personDao; + + @DisplayName("возвращать ожидаемое количество пёрсонов в БД") + @Test + void shouldReturnExpectedPersonCount() { + int actualPersonsCount = personDao.count(); + assertThat(actualPersonsCount).isEqualTo(EXPECTED_PERSONS_COUNT); + } + + @DisplayName("добавлять пёрсона в БД") + @Test + void shouldInsertPerson() { + int countBeforeInsert = personDao.count(); + assertThat(countBeforeInsert).isEqualTo(EXPECTED_PERSONS_COUNT); + + Person expectedPerson = new Person(2, "Igor"); + personDao.insert(expectedPerson); + + // Ошибка! Сейчас так проверяем т.к. больше нет других способов, + // когда появится getById, будем использовать его + int countAfterInsert = personDao.count(); + assertThat(countAfterInsert).isEqualTo(countBeforeInsert + 1); +/* + Person actualPerson = personDao.getById(expectedPerson.getId()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); +*/ + } + + @DisplayName("удалять заданного пёрсона по его id") + @Test + void shouldCorrectDeletePersonById() { + // Ошибка! Сейчас так проверяем т.к. больше нет других способов, + // когда появится getById, тест будет выглядеть, как закомментированный блок ниже + int countBeforeDelete = personDao.count(); + assertThat(countBeforeDelete).isEqualTo(EXPECTED_PERSONS_COUNT); + + personDao.deleteById(EXISTING_PERSON_ID); + + int countAfterDelete = personDao.count(); + assertThat(countAfterDelete).isEqualTo(countBeforeDelete - 1); + + /* + assertThatCode(() -> personDao.getById(EXISTING_PERSON_ID)) + .doesNotThrowAnyException(); + + personDao.deleteById(EXISTING_PERSON_ID); + + assertThatThrownBy(() -> personDao.getById(EXISTING_PERSON_ID)) + .isInstanceOf(EmptyResultDataAccessException.class); + */ + } +} \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/application.yml new file mode 100644 index 00000000..f7de231a --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + #schema-locations: schema.sql diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/data.sql new file mode 100644 index 00000000..e58b8502 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-4/src/test/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'Ivan'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/pom.xml b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/pom.xml new file mode 100644 index 00000000..227c0edd --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + ru.otus + jdbc-class-work + 1.0 + + + jdbc-demo-solution-5 + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + ${h2.version} + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..be086b88 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,31 @@ +package ru.otus.spring; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.dao.PersonDao; +import ru.otus.spring.domain.Person; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws Exception { + + ApplicationContext context = SpringApplication.run(Main.class); + + PersonDao dao = context.getBean(PersonDao.class); + + System.out.println("All count " + dao.count()); + + dao.insert(new Person(2, "ivan")); + + System.out.println("All count " + dao.count()); + + Person ivan = dao.getById(2); + + System.out.println("Ivan id: " + ivan.id() + " name: " + ivan.name()); + + Console.main(args); + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java new file mode 100644 index 00000000..5d054ce5 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java @@ -0,0 +1,57 @@ +package ru.otus.spring.dao; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Person; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +public class JdbcPersonDao implements PersonDao { + + private final JdbcOperations jdbc; + + public JdbcPersonDao(JdbcOperations jdbcOperations) { + this.jdbc = jdbcOperations; + } + + @Override + public int count() { + Integer count = jdbc.queryForObject("select count(*) from persons", Integer.class); + return count == null ? 0 : count; + } + + @Override + public void insert(Person person) { + jdbc.update("insert into persons (id, name) values (?, ?)", person.id(), person.name()); + } + + @Override + public Person getById(long id) { + return jdbc.queryForObject("select id, name from persons where id = ?", new PersonMapper(), id); + } + + @Override + public List getAll() { + return jdbc.query("select id, name from persons", new PersonMapper()); + } + + @Override + public void deleteById(long id) { + jdbc.update("delete from persons where id = ?", id); + } + + + private static class PersonMapper implements RowMapper { + + @Override + public Person mapRow(ResultSet resultSet, int i) throws SQLException { + long id = resultSet.getLong("id"); + String name = resultSet.getString("name"); + return new Person(id, name); + } + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/PersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/PersonDao.java new file mode 100644 index 00000000..9fa01df3 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/dao/PersonDao.java @@ -0,0 +1,17 @@ +package ru.otus.spring.dao; + +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonDao { + int count(); + + void insert(Person person); + + Person getById(long id); + + List getAll(); + + void deleteById(long id); +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..92553c2d --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,4 @@ +package ru.otus.spring.domain; + +public record Person(long id, String name) { +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/application.yml new file mode 100644 index 00000000..8f40ea4e --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #schema: schema.sql + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql + h2: + console: + path: /h2-console + settings: + web-allow-others: true \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/data.sql new file mode 100644 index 00000000..9de9daab --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'masha'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/schema.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/schema.sql new file mode 100644 index 00000000..583bbae0 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS PERSONS; +CREATE TABLE PERSONS +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(255) +); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java new file mode 100644 index 00000000..42d4ead9 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java @@ -0,0 +1,71 @@ +package ru.otus.spring.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.EmptyResultDataAccessException; +import ru.otus.spring.domain.Person; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@DisplayName("Dao для работы с пёрсонами должно") +@JdbcTest +@Import(JdbcPersonDao.class) +class JdbcPersonDaoTest { + + private static final int EXPECTED_PERSONS_COUNT = 1; + private static final int EXISTING_PERSON_ID = 1; + private static final String EXISTING_PERSON_NAME = "Ivan"; + + @Autowired + private JdbcPersonDao personDao; + + @DisplayName("возвращать ожидаемое количество пёрсонов в БД") + @Test + void shouldReturnExpectedPersonCount() { + int actualPersonsCount = personDao.count(); + assertThat(actualPersonsCount).isEqualTo(EXPECTED_PERSONS_COUNT); + } + + @DisplayName("добавлять пёрсона в БД") + @Test + void shouldInsertPerson() { + Person expectedPerson = new Person(2, "Igor"); + personDao.insert(expectedPerson); + Person actualPerson = personDao.getById(expectedPerson.id()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); + } + + @DisplayName("возвращать ожидаемого пёрсона по его id") + @Test + void shouldReturnExpectedPersonById() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + Person actualPerson = personDao.getById(expectedPerson.id()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); + } + + @DisplayName("удалять заданного пёрсона по его id") + @Test + void shouldCorrectDeletePersonById() { + assertThatCode(() -> personDao.getById(EXISTING_PERSON_ID)) + .doesNotThrowAnyException(); + + personDao.deleteById(EXISTING_PERSON_ID); + + assertThatThrownBy(() -> personDao.getById(EXISTING_PERSON_ID)) + .isInstanceOf(EmptyResultDataAccessException.class); + } + + @DisplayName("возвращать ожидаемый список пёрсонов") + @Test + void shouldReturnExpectedPersonsList() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + List actualPersonList = personDao.getAll(); + assertThat(actualPersonList) + .containsExactlyInAnyOrder(expectedPerson); + } +} \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/application.yml new file mode 100644 index 00000000..f7de231a --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + #schema-locations: schema.sql diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/data.sql new file mode 100644 index 00000000..e58b8502 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-5/src/test/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'Ivan'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/pom.xml b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/pom.xml new file mode 100644 index 00000000..5949aef0 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + ru.otus + jdbc-class-work + 1.0 + + + jdbc-demo-solution-final + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + ${h2.version} + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..ab054919 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,33 @@ +package ru.otus.spring; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.dao.PersonDao; +import ru.otus.spring.domain.Person; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws Exception { + + ApplicationContext context = SpringApplication.run(Main.class); + + PersonDao dao = context.getBean(PersonDao.class); + + System.out.println("All count " + dao.count()); + + dao.insert(new Person(2, "ivan")); + + System.out.println("All count " + dao.count()); + + Person ivan = dao.getById(2); + + System.out.println("Ivan id: " + ivan.id() + " name: " + ivan.name()); + + System.out.println(dao.getAll()); + + Console.main(args); + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java new file mode 100644 index 00000000..5093b542 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/JdbcPersonDao.java @@ -0,0 +1,70 @@ +package ru.otus.spring.dao; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Person; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Repository +public class JdbcPersonDao implements PersonDao { + + private final JdbcOperations jdbc; + private final NamedParameterJdbcOperations namedParameterJdbcOperations; + + public JdbcPersonDao(NamedParameterJdbcOperations namedParameterJdbcOperations) { + // Это просто оставили, чтобы не переписывать код + // В идеале всё должно быть на NamedParameterJdbcOperations + this.jdbc = namedParameterJdbcOperations.getJdbcOperations(); + this.namedParameterJdbcOperations = namedParameterJdbcOperations; + } + + @Override + public int count() { + Integer count = jdbc.queryForObject("select count(*) from persons", Integer.class); + return count == null ? 0 : count; + } + + @Override + public void insert(Person person) { + namedParameterJdbcOperations.update("insert into persons (id, name) values (:id, :name)", + Map.of("id", person.id(), "name", person.name())); + } + + @Override + public Person getById(long id) { + Map params = Collections.singletonMap("id", id); + return namedParameterJdbcOperations.queryForObject( + "select id, name from persons where id = :id", params, new PersonMapper() + ); + } + + @Override + public List getAll() { + return jdbc.query("select id, name from persons", new PersonMapper()); + } + + @Override + public void deleteById(long id) { + Map params = Collections.singletonMap("id", id); + namedParameterJdbcOperations.update( + "delete from persons where id = :id", params + ); + } + + private static class PersonMapper implements RowMapper { + + @Override + public Person mapRow(ResultSet resultSet, int i) throws SQLException { + long id = resultSet.getLong("id"); + String name = resultSet.getString("name"); + return new Person(id, name); + } + } +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/PersonDao.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/PersonDao.java new file mode 100644 index 00000000..f2c30f7c --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/dao/PersonDao.java @@ -0,0 +1,18 @@ +package ru.otus.spring.dao; + +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonDao { + + int count(); + + void insert(Person person); + + Person getById(long id); + + List getAll(); + + void deleteById(long id); +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..92553c2d --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,4 @@ +package ru.otus.spring.domain; + +public record Person(long id, String name) { +} diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/application.yml new file mode 100644 index 00000000..13c3ec8f --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + # initialization-mode: always + # schema: schema.sql + # data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql + h2: + console: + path: /h2-console + settings: + web-allow-others: true + diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/data.sql new file mode 100644 index 00000000..9de9daab --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'masha'); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/schema.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/schema.sql new file mode 100644 index 00000000..bd6f5353 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE PERSONS +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(255) +); diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java new file mode 100644 index 00000000..8573aecd --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/java/ru/otus/spring/dao/JdbcPersonDaoTest.java @@ -0,0 +1,86 @@ +package ru.otus.spring.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import ru.otus.spring.domain.Person; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@DisplayName("Dao для работы с пёрсонами должно") +@JdbcTest +@Import(JdbcPersonDao.class) +//@Transactional(propagation = Propagation.NOT_SUPPORTED) +class JdbcPersonDaoTest { + + private static final int EXPECTED_PERSONS_COUNT = 1; + private static final int EXISTING_PERSON_ID = 1; + private static final String EXISTING_PERSON_NAME = "Ivan"; + + @Autowired + private JdbcPersonDao personDao; + + @BeforeTransaction + void beforeTransaction() { + System.out.println("beforeTransaction"); + } + + @AfterTransaction + void afterTransaction() { + System.out.println("afterTransaction"); + } + + @DisplayName("возвращать ожидаемое количество пёрсонов в БД") + @Test + void shouldReturnExpectedPersonCount() { + int actualPersonsCount = personDao.count(); + assertThat(actualPersonsCount).isEqualTo(EXPECTED_PERSONS_COUNT); + } + + //@Rollback(value = false) + //@Commit + @DisplayName("добавлять пёрсона в БД") + @Test + void shouldInsertPerson() { + Person expectedPerson = new Person(2, "Igor"); + personDao.insert(expectedPerson); + Person actualPerson = personDao.getById(expectedPerson.id()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); + } + + @DisplayName("возвращать ожидаемого пёрсона по его id") + @Test + void shouldReturnExpectedPersonById() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + Person actualPerson = personDao.getById(expectedPerson.id()); + assertThat(actualPerson).usingRecursiveComparison().isEqualTo(expectedPerson); + } + + @DisplayName("удалять заданного пёрсона по его id") + @Test + void shouldCorrectDeletePersonById() { + assertThatCode(() -> personDao.getById(EXISTING_PERSON_ID)) + .doesNotThrowAnyException(); + + personDao.deleteById(EXISTING_PERSON_ID); + + assertThatThrownBy(() -> personDao.getById(EXISTING_PERSON_ID)) + .isInstanceOf(EmptyResultDataAccessException.class); + } + + @DisplayName("возвращать ожидаемый список пёрсонов") + @Test + void shouldReturnExpectedPersonsList() { + Person expectedPerson = new Person(EXISTING_PERSON_ID, EXISTING_PERSON_NAME); + List actualPersonList = personDao.getAll(); + assertThat(actualPersonList) + .containsExactlyInAnyOrder(expectedPerson); + } +} \ No newline at end of file diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/application.yml b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/application.yml new file mode 100644 index 00000000..2694a3e5 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + #initialization-mode: always + #data: data.sql + sql: + init: + mode: always + data-locations: data.sql + schema-locations: schema.sql diff --git a/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/data.sql b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/data.sql new file mode 100644 index 00000000..e58b8502 --- /dev/null +++ b/2026-01/spring-09-jdbc/jdbc-demo-solution-final/src/test/resources/data.sql @@ -0,0 +1,2 @@ +insert into persons (id, name) +values (1, 'Ivan'); diff --git a/2026-01/spring-09-jdbc/pom.xml b/2026-01/spring-09-jdbc/pom.xml new file mode 100644 index 00000000..548ba351 --- /dev/null +++ b/2026-01/spring-09-jdbc/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + + + ru.otus + jdbc-class-work + 1.0 + + pom + + + jdbc-demo-exercise + jdbc-demo-solution-3 + jdbc-demo-solution-4 + jdbc-demo-solution-5 + jdbc-demo-solution-final + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + ${h2.version} + + + + + diff --git a/2026-01/spring-10-orm/demo-projects/.gitignore b/2026-01/spring-10-orm/demo-projects/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/.gitignore b/2026-01/spring-10-orm/demo-projects/mybatis-demo/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/README.md b/2026-01/spring-10-orm/demo-projects/mybatis-demo/README.md new file mode 100644 index 00000000..44635273 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/README.md @@ -0,0 +1,2 @@ +# mybatis-demo +Пример работы с БД через MyBatis \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/pom.xml b/2026-01/spring-10-orm/demo-projects/mybatis-demo/pom.xml new file mode 100644 index 00000000..09d25661 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + mybatis-demo + 0.0.1-SNAPSHOT + mybatis-demo + MyBatis demo + + + 17 + 17 + 17 + 3.0.3 + 2.2.220 + 2.0 + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java new file mode 100644 index 00000000..ab5f7513 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java @@ -0,0 +1,14 @@ +package ru.otus.example.mybatisdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +@SpringBootApplication +public class MyBatisDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(MyBatisDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java new file mode 100644 index 00000000..5c8f4728 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java @@ -0,0 +1,13 @@ +package ru.otus.example.mybatisdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Avatar { + private long id; + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java new file mode 100644 index 00000000..6aa8cff7 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java @@ -0,0 +1,13 @@ +package ru.otus.example.mybatisdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Course { + private long id; + private String name; +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java new file mode 100644 index 00000000..8fa43ebd --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java @@ -0,0 +1,13 @@ +package ru.otus.example.mybatisdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EMail { + private long id; + private String email; +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java new file mode 100644 index 00000000..afb324fb --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java @@ -0,0 +1,18 @@ +package ru.otus.example.mybatisdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OtusStudent { + private long id; + private String name; + private Avatar avatar; + private List emails; + private List courses; +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java new file mode 100644 index 00000000..634d1aba --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.mybatisdemo.repositories; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; +import org.apache.ibatis.annotations.Select; +import ru.otus.example.mybatisdemo.models.Avatar; + +@Mapper +public interface AvatarRepository { + @Select("select * from avatars where id = #{id}") + @Results(value = { + @Result(property = "id", column = "id"), + @Result(property = "photoUrl", column = "photo_url") + }) + Avatar getAvatarById(long id); + +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java new file mode 100644 index 00000000..20271981 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java @@ -0,0 +1,17 @@ +package ru.otus.example.mybatisdemo.repositories; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import ru.otus.example.mybatisdemo.models.Course; + +import java.util.List; + +@Mapper +public interface CourseRepository { + + @Select("select * " + + "from student_courses sc left join courses c on sc.course_id = c.id " + + "where sc.student_id = #{studentId}") + List getCoursesByStudentId(long studentId); + +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java new file mode 100644 index 00000000..989b681d --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java @@ -0,0 +1,14 @@ +package ru.otus.example.mybatisdemo.repositories; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import ru.otus.example.mybatisdemo.models.EMail; + +import java.util.List; + +@Mapper +public interface EmailRepository { + + @Select("select * from emails where student_id = #{studentId}") + List getEmailsByStudentId(long studentId); +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..82a8260b --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,42 @@ +package ru.otus.example.mybatisdemo.repositories; + +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.mapping.FetchType; +import ru.otus.example.mybatisdemo.models.Avatar; +import ru.otus.example.mybatisdemo.models.OtusStudent; + +import java.util.List; + +@Mapper +public interface OtusStudentRepository { + + @Select("select * from otus_students") + @Results(id = "studentAllMap", value = { + @Result(property = "id", column = "id"), + @Result(property = "name", column = "name"), + @Result(property = "avatar", column = "avatar_id", javaType = Avatar.class, + one = @One(select = "ru.otus.example.mybatisdemo.repositories.AvatarRepository.getAvatarById", fetchType = FetchType.EAGER)), + @Result(property = "emails", column = "id", javaType = List.class, + many = @Many(select = "ru.otus.example.mybatisdemo.repositories.EmailRepository.getEmailsByStudentId", fetchType = FetchType.EAGER)), + @Result(property = "courses", column = "id", javaType = List.class, + many = @Many(select = "ru.otus.example.mybatisdemo.repositories.CourseRepository.getCoursesByStudentId", fetchType = FetchType.EAGER)) + }) + List findAllWithAllInfo(); + + @Select("select * from otus_students where id = #{id}") + @ResultMap("studentAllMap") + OtusStudent findById(long id); + + @Select("select count(*) as students_count from otus_students") + long getStudentsCount(); + + @Insert("insert into otus_students(name, avatar_id) values (#{name}, #{avatar.id})") + void insert(OtusStudent student); + + @Update("update otus_students set name = #{name} where id = #{id}") + void updateName(OtusStudent student); + + @Delete("delete from otus_students where id = #{id}") + void deleteById(long id); + +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java new file mode 100644 index 00000000..d9a24bce --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java @@ -0,0 +1,110 @@ +package ru.otus.example.mybatisdemo.repositories; + +import lombok.val; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.mybatisdemo.models.Avatar; +import ru.otus.example.mybatisdemo.models.OtusStudent; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе MyBatis для работы со студентами ") +@SpringBootTest +@Transactional +public class OtusStudentRepositoryTest { + + private static final String FIELD_ID = "id"; + private static final String FIELD_PHOTO_URL = "photoUrl"; + private static final String FIELD_NAME = "name"; + + private static final long FIRST_STUDENT_ID = 1L; + private static final long FIRST_AVATAR_ID = 1L; + private static final String FIRST_STUDENT_NAME = "student_01"; + private static final String FIRST_AVATAR_URL = "photoUrl_01"; + private static final String STUDENT_NEW_NAME = "Висусуалий"; + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long INSERTED_STUDENT_ID = 11L; + private static final int EXPECTED_EMAILS_COUNT = 2; + private static final int EXPECTED_COURSES_COUNT = 3; + + @Autowired + private OtusStudentRepository studentRepositoryMyBatis; + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + val students = studentRepositoryMyBatis.findAllWithAllInfo(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + } + + @DisplayName("должен загружать число студентов в БД") + @Test + void shouldReturnCorrectStudentsCount() { + long studentsCount = studentRepositoryMyBatis.getStudentsCount(); + assertThat(studentsCount).isEqualTo(EXPECTED_NUMBER_OF_STUDENTS); + } + + @DisplayName(" должен загружать информацию о нужном студенте") + @Test + void shouldFindExpectedStudentById(){ + val actualStudent = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID); + + assertThat(actualStudent).isNotNull(); + assertThat(actualStudent.getName()).isEqualTo(FIRST_STUDENT_NAME); + assertThat(actualStudent.getAvatar()).isNotNull() + .hasFieldOrPropertyWithValue(FIELD_ID, FIRST_STUDENT_ID) + .hasFieldOrPropertyWithValue(FIELD_PHOTO_URL, FIRST_AVATAR_URL); + assertThat(actualStudent.getEmails()).isNotNull().hasSize(EXPECTED_EMAILS_COUNT); + assertThat(actualStudent.getCourses()).isNotNull().hasSize(EXPECTED_COURSES_COUNT); + } + + @DisplayName(" должен сохранить, а потом загрузить информацию о нужном студенте") + @Test + void shouldSaveAndLoadCorrectStudent() { + val expectedStudent = new OtusStudent(0, STUDENT_NEW_NAME, + new Avatar(FIRST_AVATAR_ID, FIRST_AVATAR_URL), List.of(), List.of()); + studentRepositoryMyBatis.insert(expectedStudent); + val actualStudent = studentRepositoryMyBatis.findById(INSERTED_STUDENT_ID); + + assertThat(actualStudent) + .isNotNull() + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withIgnoredFields(FIELD_ID).build()) + .isEqualTo(expectedStudent); + } + + + @DisplayName(" должен обновлять имя студента в БД") + @Test + void shouldUpdateStudentName() { + val student = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID); + student.setName(STUDENT_NEW_NAME); + studentRepositoryMyBatis.updateName(student); + val actualStudent = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID); + + assertThat(actualStudent).isNotNull().hasFieldOrPropertyWithValue(FIELD_NAME, student.getName()); + } + + @DisplayName("должен удалять студента из БД по id") + @Test + void shouldDeleteStudentFromDbById() { + val studentsCountBefore = studentRepositoryMyBatis.getStudentsCount(); + studentRepositoryMyBatis.deleteById(FIRST_STUDENT_ID); + val studentsCountAfter = studentRepositoryMyBatis.getStudentsCount(); + + assertThat(studentsCountBefore - studentsCountAfter).isEqualTo(1); + } + +} diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml new file mode 100644 index 00000000..dc237b00 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: always + +logging: + level: + ru.otus.example.mybatisdemo.repositories: TRACE \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-10-orm/demo-projects/pom.xml b/2026-01/spring-10-orm/demo-projects/pom.xml new file mode 100644 index 00000000..95c77325 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + ru.otus + demo-projects + 1.0 + + pom + + + spring-jdbc-demo + spring-jpa-ineritance-demo + mybatis-demo + + diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/README.md b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/README.md new file mode 100644 index 00000000..462216c3 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/README.md @@ -0,0 +1,2 @@ +# spring-jdbc-demo +Пример работы с БД через jdbc \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml new file mode 100644 index 00000000..26af5118 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + spring-jdbc-demo + 0.0.1-SNAPSHOT + spring-jdbc-demo + Spring jdbc demo + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java new file mode 100644 index 00000000..28ebfec4 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.springjdbcdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringJdbcDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringJdbcDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java new file mode 100644 index 00000000..a1963ea4 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java @@ -0,0 +1,13 @@ +package ru.otus.example.springjdbcdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Avatar { + private long id; + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java new file mode 100644 index 00000000..07b6e2c2 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java @@ -0,0 +1,13 @@ +package ru.otus.example.springjdbcdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Course { + private long id; + private String name; +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java new file mode 100644 index 00000000..16985ad5 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java @@ -0,0 +1,13 @@ +package ru.otus.example.springjdbcdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EMail { + private long id; + private String email; +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java new file mode 100644 index 00000000..5ae9167c --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java @@ -0,0 +1,18 @@ +package ru.otus.example.springjdbcdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OtusStudent { + private long id; + private String name; + private Avatar avatar; + private List emails; + private List courses; +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepository.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepository.java new file mode 100644 index 00000000..e594ee5c --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.springjdbcdemo.repositories; + +import ru.otus.example.springjdbcdemo.models.Course; + +import java.util.List; + +public interface CourseRepository { + List findAllUsed(); +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java new file mode 100644 index 00000000..10526ed5 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java @@ -0,0 +1,35 @@ +package ru.otus.example.springjdbcdemo.repositories; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.otus.example.springjdbcdemo.models.Course; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class CourseRepositoryJdbc implements CourseRepository { + + private final JdbcOperations op; + + @Override + public List findAllUsed() { + return op.query("select c.id, c.name " + + "from courses c inner join student_courses sc on c.id = sc.course_id " + + "group by c.id, c.name " + + "order by c.name", new CourseRowMapper()); + } + + private static class CourseRowMapper implements RowMapper { + @Override + public Course mapRow(ResultSet rs, int i) throws SQLException { + return new Course(rs.getLong(1), rs.getString(2)); + } + } + +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepository.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..0012af3d --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.springjdbcdemo.repositories; + +import ru.otus.example.springjdbcdemo.models.OtusStudent; + +import java.util.List; + +public interface OtusStudentRepository { + List findAllWithAllInfo(); +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java new file mode 100644 index 00000000..5618b797 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java @@ -0,0 +1,52 @@ +package ru.otus.example.springjdbcdemo.repositories; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; +import ru.otus.example.springjdbcdemo.models.Course; +import ru.otus.example.springjdbcdemo.models.OtusStudent; +import ru.otus.example.springjdbcdemo.repositories.ext.OtusStudentResultSetExtractor; +import ru.otus.example.springjdbcdemo.repositories.ext.StudentCourseRelation; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class OtusStudentRepositoryJdbc implements OtusStudentRepository { + + private final CourseRepository courseRepository; + private final JdbcOperations op; + + @Override + public List findAllWithAllInfo() { + List courses = courseRepository.findAllUsed(); + List relations = getAllRelations(); + Map students = + op.query("select os.id, os.name, a.id avatar_id, a.photo_url, e.id email_id, e.email " + + "from (otus_students os left join avatars a on " + + "os.avatar_id = a.id) left join emails e on os.id = e.student_id", + new OtusStudentResultSetExtractor()); + + mergeStudentsInfo(students, courses, relations); + return new ArrayList<>(Objects.requireNonNull(students).values()); + } + + private List getAllRelations() { + return op.query("select student_id, course_id from student_courses sc order by student_id, course_id", + (rs, i) -> new StudentCourseRelation(rs.getLong(1), rs.getLong(2))); + } + + private void mergeStudentsInfo(Map students, List courses, + List relations) { + Map coursesMap = courses.stream().collect(Collectors.toMap(Course::getId, Function.identity())); + relations.forEach(r -> { + if (students.containsKey(r.getStudentId()) && coursesMap.containsKey(r.getCourseId())) { + students.get(r.getStudentId()).getCourses().add(coursesMap.get(r.getCourseId())); + } + }); + } + + +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java new file mode 100644 index 00000000..778a46b7 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java @@ -0,0 +1,37 @@ +package ru.otus.example.springjdbcdemo.repositories.ext; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.ResultSetExtractor; +import ru.otus.example.springjdbcdemo.models.Avatar; +import ru.otus.example.springjdbcdemo.models.EMail; +import ru.otus.example.springjdbcdemo.models.OtusStudent; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class OtusStudentResultSetExtractor implements + ResultSetExtractor> { + @Override + public Map extractData(ResultSet rs) throws SQLException, + DataAccessException { + + Map students = new HashMap<>(); + while (rs.next()) { + long id = rs.getLong("id"); + OtusStudent student = students.get(id); + if (student == null) { + student = new OtusStudent(id, rs.getString("name"), + new Avatar(rs.getLong("avatar_id"), rs.getString("photo_url")), + new ArrayList<>(), new ArrayList<>()); + students.put(student.getId(), student); + } + + student.getEmails().add(new EMail(rs.getLong("email_id"), + rs.getString("email"))); + } + return students; + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/StudentCourseRelation.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/StudentCourseRelation.java new file mode 100644 index 00000000..408c97d2 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/StudentCourseRelation.java @@ -0,0 +1,11 @@ +package ru.otus.example.springjdbcdemo.repositories.ext; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class StudentCourseRelation { + private final long studentId; + private final long courseId; +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/resources/schema.sql b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcTest.java b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcTest.java new file mode 100644 index 00000000..bf2a8197 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcTest.java @@ -0,0 +1,35 @@ +package ru.otus.example.springjdbcdemo.repositories; + +import lombok.val; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; + + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jdbc для работы со студентами ") +@JdbcTest +@Import({OtusStudentRepositoryJdbc.class, CourseRepositoryJdbc.class}) +class OtusStudentRepositoryJdbcTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + + @Autowired + private OtusStudentRepositoryJdbc repositoryJdbc; + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + val students = repositoryJdbc.findAllWithAllInfo(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + students.forEach(System.out::println); + + } +} \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/application.yml b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/application.yml new file mode 100644 index 00000000..e1e538a4 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/application.yml @@ -0,0 +1,4 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: always \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/data.sql b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jdbc-demo/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/.gitignore b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/README.md b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/README.md new file mode 100644 index 00000000..462216c3 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/README.md @@ -0,0 +1,2 @@ +# spring-jdbc-demo +Пример работы с БД через jdbc \ No newline at end of file diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/pom.xml b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/pom.xml new file mode 100644 index 00000000..713d3543 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + spring-jpa-ineritance-demo + 0.0.1-SNAPSHOT + spring-jpa-demo + Spring jpa demo + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/InheritanceDemo.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/InheritanceDemo.java new file mode 100644 index 00000000..e8a725ee --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/InheritanceDemo.java @@ -0,0 +1,43 @@ +package ru.otus.example.ineritancedemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import ru.otus.example.ineritancedemo.model.A; +import ru.otus.example.ineritancedemo.model.C; +import ru.otus.example.ineritancedemo.model.B; +import ru.otus.example.ineritancedemo.repository.ARepository; + +import java.sql.SQLException; +import java.util.List; + +@SpringBootApplication +public class InheritanceDemo { + + public static void main(String[] args) throws SQLException { + ConfigurableApplicationContext ctx = SpringApplication.run(InheritanceDemo.class, args); + ARepository aRepository = ctx.getBean(ARepository.class); + + System.out.println("\n\n-------------------------------------------\n\n"); + System.out.println("Начинаем вставку сущностей A/B/C: "); + + var a = new A(0, "aaaaaa1"); + var b = new B(0, "aaaaaa2", "bbbbbbb"); + var c = new C(0, "aaaaaa3", "ccccccc"); + aRepository.save(a); + aRepository.save(b); + aRepository.save(c); + + System.out.println("\n\n-------------------------------------------\n\n"); + System.out.println("Загружаем все сущности A (в т.ч. наследников):"); + + List resultList = aRepository.findAll(); + + System.out.println("\n\nРезультат:"); + System.out.println(resultList); + + + Console.main(); + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/A.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/A.java new file mode 100644 index 00000000..bae6ecf1 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/A.java @@ -0,0 +1,45 @@ +package ru.otus.example.ineritancedemo.model; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +@Entity +@DiscriminatorColumn(name = "discriminator") +@DiscriminatorValue("RootA") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +//@Inheritance(strategy = InheritanceType.JOINED) +//@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +@Table(name = "A") +public class A { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + // Не получится использовать при InheritanceType.TABLE_PER_CLASS + //@GeneratedValue(strategy = GenerationType.IDENTITY) + protected long id; + + protected String a; + + public A() { + } + + public A(long id, String a) { + this.id = id; + this.a = a; + } + + @Override + public String toString() { + return "A{" + + "id=" + id + + ", a='" + a + '\'' + + '}'; + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/B.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/B.java new file mode 100644 index 00000000..d18ad82c --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/B.java @@ -0,0 +1,28 @@ +package ru.otus.example.ineritancedemo.model; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("LeafB") +public class B extends A { + private String b; + + public B() { + super(); + } + + public B(long id, String a, String b) { + super(id, a); + this.b = b; + } + + @Override + public String toString() { + return "B{" + + "id=" + id + + ", a='" + a + '\'' + + ", b='" + b + '\'' + + '}'; + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/C.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/C.java new file mode 100644 index 00000000..aa4c148c --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/model/C.java @@ -0,0 +1,31 @@ +package ru.otus.example.ineritancedemo.model; + + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + + +@Entity +@DiscriminatorValue("LeafC") +public class C extends A{ + private String c; + + public C() { + super(); + } + + + public C(long id, String a, String c) { + super(id, a); + this.c = c; + } + + @Override + public String toString() { + return "C{" + + "id=" + id + + ", a='" + a + '\'' + + ", c='" + c + '\'' + + '}'; + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepository.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepository.java new file mode 100644 index 00000000..8d787e92 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepository.java @@ -0,0 +1,10 @@ +package ru.otus.example.ineritancedemo.repository; + +import ru.otus.example.ineritancedemo.model.A; + +import java.util.List; + +public interface ARepository { + List findAll(); + void save(A a); +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepositoryJpa.java b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepositoryJpa.java new file mode 100644 index 00000000..8f580eef --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/java/ru/otus/example/ineritancedemo/repository/ARepositoryJpa.java @@ -0,0 +1,32 @@ +package ru.otus.example.ineritancedemo.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ineritancedemo.model.A; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ARepositoryJpa implements ARepository { + + @PersistenceContext + private final EntityManager em; + + @Transactional(readOnly = true) // Только для примера. Лучше вешать на методы сервисов + @Override + public List findAll() { + TypedQuery query = em.createQuery("select a from A a", A.class); + return query.getResultList(); + } + + @Transactional // Только для примера. Лучше вешать на методы сервисов + @Override + public void save(A a) { + em.persist(a); + } +} diff --git a/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/resources/application.yml b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/demo-projects/spring-jpa-ineritance-demo/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-exercise/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-exercise/pom.xml new file mode 100644 index 00000000..8eb18b0b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-exercise + orm-exercise + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..cbf71df2 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.sql.SQLException; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) throws SQLException { + SpringApplication.run(OrmDemoApplication.class, args); + Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..27ace01b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Avatar { + private long id; + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..b6fbaf37 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Course { + private long id; + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..d0f35463 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EMail { + private long id; + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..7479eb7d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.models; + +import lombok.*; + +import jakarta.persistence.*; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OtusStudent { + private long id; + private String name; + + //private Avatar avatar; + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-exercise/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/pom.xml new file mode 100644 index 00000000..1d9007b2 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-01 + orm-solution-01 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..f79957fe --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.sql.SQLException; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) throws SQLException { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..f07b0b43 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Avatar { + @Id + private long id; + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..d13d262d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Course { + @Id + private long id; + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..576ba63f --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class EMail { + @Id + private long id; + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..3173d4a3 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,23 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + private long id; + private String name; + + //private Avatar avatar; + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-01/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/pom.xml new file mode 100644 index 00000000..ff1921f4 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-02 + orm-solution-02 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..f79957fe --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.sql.SQLException; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) throws SQLException { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..e8707692 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,22 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + private long id; + + @Column(name = "photo_url") + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..e4aa8680 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,22 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..0d5171f7 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,23 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..5494d703 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,27 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name") + private String name; + + //private Avatar avatar; + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-02/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/pom.xml new file mode 100644 index 00000000..bbe14934 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-03 + orm-solution-03 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..f79957fe --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.sql.SQLException; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) throws SQLException { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..86e9900d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..b68f4f09 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,32 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Позволяет указать стратегию генерации id + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name") + private String name; + + //private Avatar avatar; + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-03/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/pom.xml new file mode 100644 index 00000000..ecd1211b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-04 + orm-solution-04 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..f79957fe --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,17 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.sql.SQLException; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) throws SQLException { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..86e9900d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..fae7b5f3 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,41 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-04/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/pom.xml new file mode 100644 index 00000000..b0763e4f --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-05 + orm-solution-04 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..2d29dd3c --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,14 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..9e2efa43 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,30 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; + + @OneToOne(fetch = FetchType.LAZY) + private OtusStudent student; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..38b93c58 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,38 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private Avatar avatar; + + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-05/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/pom.xml new file mode 100644 index 00000000..2cbaf7b2 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-06 + orm-solution-04 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..ae44965b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..9e2efa43 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,30 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; + + @OneToOne(fetch = FetchType.LAZY) + private OtusStudent student; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..b08ab739 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,38 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "student", orphanRemoval = true) + private Avatar avatar; + + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-06/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/pom.xml new file mode 100644 index 00000000..e6fe66a3 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-07 + orm-solution-04 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..ae44965b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..928ac1f6 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,29 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; + + @OneToOne(mappedBy = "avatar") + private OtusStudent student; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..38b93c58 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,38 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private Avatar avatar; + + //private List emails; + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-07/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/pom.xml new file mode 100644 index 00000000..02cd3042 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-08 + orm-solution-05 + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..ae44965b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..86e9900d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..b95aa319 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,46 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + @JoinColumn(name = "student_id") + private List emails; + + //private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-08/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/.gitignore b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/pom.xml b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/pom.xml new file mode 100644 index 00000000..38b60d53 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + ru.otus.example + orm-solution-final + orm-solution-final + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + ${h2.version} + + + + org.projectlombok + lombok + true + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..ae44965b --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,15 @@ +package ru.otus.example.ormdemo; + +import org.h2.tools.Console; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + //Console.main(args); + } + +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..86e9900d --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url") + private String photoUrl; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..dad16a14 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..92880532 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email") + private String email; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..4f39147c --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,53 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(fetch = FetchType.LAZY /*, cascade = CascadeType.PERSIST*/) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "students_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/resources/application.yml b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/resources/application.yml new file mode 100644 index 00000000..80d05f29 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/orm-solution-final/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: true + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-10-orm/orm-class-work/pom.xml b/2026-01/spring-10-orm/orm-class-work/pom.xml new file mode 100644 index 00000000..2d21fb30 --- /dev/null +++ b/2026-01/spring-10-orm/orm-class-work/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + ru.otus + orm-class-work + 1.0 + + pom + + + orm-exercise + orm-solution-01 + orm-solution-02 + orm-solution-03 + orm-solution-04 + orm-solution-05 + orm-solution-06 + orm-solution-07 + orm-solution-08 + orm-solution-final + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml new file mode 100644 index 00000000..b7a3fe11 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + jpql-exercise + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..71e85478 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,53 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..f279aa39 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.repositories; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + return null; + } + + @Override + public Optional findById(long id) { + return Optional.empty(); + } + + @Override + public List findAll() { + return Collections.emptyList(); + } + + @Override + public List findByName(String name) { + return Collections.emptyList(); + } + + @Override + public void updateNameById(long id, String name) { + + } + + @Override + public void deleteById(long id) { + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..f41c7dd5 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,126 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; +import ru.otus.example.ormdemo.models.Avatar; +import ru.otus.example.ormdemo.models.Course; +import ru.otus.example.ormdemo.models.EMail; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + private static final String FIRST_STUDENT_NAME = "student_01"; + + private static final int EXPECTED_QUERIES_COUNT = 31; + + private static final String STUDENT_AVATAR_URL = "где-то там"; + private static final String STUDENT_EMAIL = "any@mail.com"; + private static final String COURSE_NAME = "Spring"; + private static final String STUDENT_NAME = "Вася"; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } + + @DisplayName(" должен корректно сохранять всю информацию о студенте") + @Test + void shouldSaveAllStudentInfo() { + val avatar = new Avatar(0, STUDENT_AVATAR_URL); + val email = new EMail(0, STUDENT_EMAIL); + val emails = Collections.singletonList(email); + + val course = new Course(0, COURSE_NAME); + val courses = Collections.singletonList(course); + + + val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses); + repositoryJpa.save(vasya); + assertThat(vasya.getId()).isGreaterThan(0); + + val actualStudent = em.find(OtusStudent.class, vasya.getId()); + assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals("")) + .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0) + .matches(s -> s.getAvatar() != null) + .matches(s -> s.getEmails() != null && s.getEmails().size() > 0); + } + + @DisplayName(" должен загружать информацию о нужном студенте по его имени") + @Test + void shouldFindExpectedStudentByName() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + List students = repositoryJpa.findByName(FIRST_STUDENT_NAME); + assertThat(students).containsOnlyOnce(firstStudent); + } + + @DisplayName(" должен изменять имя заданного студента по его id") + @Test + void shouldUpdateStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + String oldName = firstStudent.getName(); + em.detach(firstStudent); + + repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME); + val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME); + } + + @DisplayName(" должен удалять заданного студента по его id") + @Test + void shouldDeleteStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(firstStudent).isNotNull(); + em.detach(firstStudent); + + repositoryJpa.deleteById(FIRST_STUDENT_ID); + val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(deletedStudent).isNull(); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml new file mode 100644 index 00000000..7d216f89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml new file mode 100644 index 00000000..e4c9abc7 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + jpql-solution-01 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..5ee3b725 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..71e85478 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,53 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..8b8b767f --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,64 @@ +package ru.otus.example.ormdemo.repositories; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + return Collections.emptyList(); + } + + @Override + public List findByName(String name) { + return Collections.emptyList(); + } + + @Override + public void updateNameById(long id, String name) { + + } + + @Override + public void deleteById(long id) { + + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..f41c7dd5 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,126 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; +import ru.otus.example.ormdemo.models.Avatar; +import ru.otus.example.ormdemo.models.Course; +import ru.otus.example.ormdemo.models.EMail; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + private static final String FIRST_STUDENT_NAME = "student_01"; + + private static final int EXPECTED_QUERIES_COUNT = 31; + + private static final String STUDENT_AVATAR_URL = "где-то там"; + private static final String STUDENT_EMAIL = "any@mail.com"; + private static final String COURSE_NAME = "Spring"; + private static final String STUDENT_NAME = "Вася"; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } + + @DisplayName(" должен корректно сохранять всю информацию о студенте") + @Test + void shouldSaveAllStudentInfo() { + val avatar = new Avatar(0, STUDENT_AVATAR_URL); + val email = new EMail(0, STUDENT_EMAIL); + val emails = Collections.singletonList(email); + + val course = new Course(0, COURSE_NAME); + val courses = Collections.singletonList(course); + + + val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses); + repositoryJpa.save(vasya); + assertThat(vasya.getId()).isGreaterThan(0); + + val actualStudent = em.find(OtusStudent.class, vasya.getId()); + assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals("")) + .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0) + .matches(s -> s.getAvatar() != null) + .matches(s -> s.getEmails() != null && s.getEmails().size() > 0); + } + + @DisplayName(" должен загружать информацию о нужном студенте по его имени") + @Test + void shouldFindExpectedStudentByName() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + List students = repositoryJpa.findByName(FIRST_STUDENT_NAME); + assertThat(students).containsOnlyOnce(firstStudent); + } + + @DisplayName(" должен изменять имя заданного студента по его id") + @Test + void shouldUpdateStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + String oldName = firstStudent.getName(); + em.detach(firstStudent); + + repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME); + val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME); + } + + @DisplayName(" должен удалять заданного студента по его id") + @Test + void shouldDeleteStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(firstStudent).isNotNull(); + em.detach(firstStudent); + + repositoryJpa.deleteById(FIRST_STUDENT_ID); + val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(deletedStudent).isNull(); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml new file mode 100644 index 00000000..7d216f89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml new file mode 100644 index 00000000..d3099a08 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + jpql-solution-02 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..71e85478 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,53 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..182ed8c1 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,69 @@ +package ru.otus.example.ormdemo.repositories; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + return em.createQuery("select s from OtusStudent s", OtusStudent.class) + .getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public void updateNameById(long id, String name) { + + } + + @Override + public void deleteById(long id) { + + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..f41c7dd5 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,126 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; +import ru.otus.example.ormdemo.models.Avatar; +import ru.otus.example.ormdemo.models.Course; +import ru.otus.example.ormdemo.models.EMail; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + private static final String FIRST_STUDENT_NAME = "student_01"; + + private static final int EXPECTED_QUERIES_COUNT = 31; + + private static final String STUDENT_AVATAR_URL = "где-то там"; + private static final String STUDENT_EMAIL = "any@mail.com"; + private static final String COURSE_NAME = "Spring"; + private static final String STUDENT_NAME = "Вася"; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } + + @DisplayName(" должен корректно сохранять всю информацию о студенте") + @Test + void shouldSaveAllStudentInfo() { + val avatar = new Avatar(0, STUDENT_AVATAR_URL); + val email = new EMail(0, STUDENT_EMAIL); + val emails = Collections.singletonList(email); + + val course = new Course(0, COURSE_NAME); + val courses = Collections.singletonList(course); + + + val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses); + repositoryJpa.save(vasya); + assertThat(vasya.getId()).isGreaterThan(0); + + val actualStudent = em.find(OtusStudent.class, vasya.getId()); + assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals("")) + .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0) + .matches(s -> s.getAvatar() != null) + .matches(s -> s.getEmails() != null && s.getEmails().size() > 0); + } + + @DisplayName(" должен загружать информацию о нужном студенте по его имени") + @Test + void shouldFindExpectedStudentByName() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + List students = repositoryJpa.findByName(FIRST_STUDENT_NAME); + assertThat(students).containsOnlyOnce(firstStudent); + } + + @DisplayName(" должен изменять имя заданного студента по его id") + @Test + void shouldUpdateStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + String oldName = firstStudent.getName(); + em.detach(firstStudent); + + repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME); + val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME); + } + + @DisplayName(" должен удалять заданного студента по его id") + @Test + void shouldDeleteStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(firstStudent).isNotNull(); + em.detach(firstStudent); + + repositoryJpa.deleteById(FIRST_STUDENT_ID); + val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(deletedStudent).isNull(); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml new file mode 100644 index 00000000..7d216f89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml new file mode 100644 index 00000000..12e3d813 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + ru.otus + jpql-solution-03 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..71e85478 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,53 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..df02c2d9 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,77 @@ +package ru.otus.example.ormdemo.repositories; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private EntityManager em; + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + return em.createQuery("select s from OtusStudent s", OtusStudent.class) + .getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void updateNameById(long id, String name) { + Query query = em.createQuery("update OtusStudent s " + + "set s.name = :name " + + "where s.id = :id"); + query.setParameter("name", name); + query.setParameter("id", id); + query.executeUpdate(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void deleteById(long id) { + Query query = em.createQuery("delete " + + "from OtusStudent s " + + "where s.id = :id"); + query.setParameter("id", id); + query.executeUpdate(); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..89482782 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,127 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; +import ru.otus.example.ormdemo.models.Avatar; +import ru.otus.example.ormdemo.models.Course; +import ru.otus.example.ormdemo.models.EMail; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + private static final String FIRST_STUDENT_NAME = "student_01"; + + private static final int EXPECTED_QUERIES_COUNT = 31; + + private static final String STUDENT_AVATAR_URL = "где-то там"; + private static final String STUDENT_EMAIL = "any@mail.com"; + private static final String COURSE_NAME = "Spring"; + private static final String STUDENT_NAME = "Вася"; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } + + @DisplayName(" должен корректно сохранять всю информацию о студенте") + @Test + void shouldSaveAllStudentInfo() { + val avatar = new Avatar(0, STUDENT_AVATAR_URL); + val email = new EMail(0, STUDENT_EMAIL); + val emails = Collections.singletonList(email); + + val course = new Course(0, COURSE_NAME); + val courses = Collections.singletonList(course); + + + val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses); + repositoryJpa.save(vasya); + assertThat(vasya.getId()).isGreaterThan(0); + + val actualStudent = em.find(OtusStudent.class, vasya.getId()); + assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals("")) + .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0) + .matches(s -> s.getAvatar() != null) + .matches(s -> s.getEmails() != null && s.getEmails().size() > 0); + } + + @DisplayName(" должен загружать информацию о нужном студенте по его имени") + @Test + void shouldFindExpectedStudentByName() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + List students = repositoryJpa.findByName(FIRST_STUDENT_NAME); + assertThat(students).containsOnlyOnce(firstStudent); + } + + @DisplayName(" должен изменять имя заданного студента по его id") + @Test + void shouldUpdateStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + String oldName = firstStudent.getName(); + em.detach(firstStudent); + + repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME); + val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + + assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME); + } + + @DisplayName(" должен удалять заданного студента по его id") + @Test + void shouldDeleteStudentNameById() { + val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(firstStudent).isNotNull(); + em.detach(firstStudent); + + repositoryJpa.deleteById(FIRST_STUDENT_ID); + val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + + assertThat(deletedStudent).isNull(); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml new file mode 100644 index 00000000..7d216f89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml new file mode 100644 index 00000000..ca70e8e3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + jpql-solution-04 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..357c2224 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе +@NamedEntityGraph(name = "otus-student-avatars-entity-graph", + attributeNodes = {@NamedAttributeNode("avatar")}) +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..868da17b --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,86 @@ +package ru.otus.example.ormdemo.repositories; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + EntityGraph entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph"); + TypedQuery query = em.createQuery("select s from OtusStudent s", OtusStudent.class); + query.setHint(FETCH.getKey(), entityGraph); + return query.getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void updateNameById(long id, String name) { + Query query = em.createQuery("update OtusStudent s " + + "set s.name = :name " + + "where s.id = :id"); + query.setParameter("name", name); + query.setParameter("id", id); + query.executeUpdate(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void deleteById(long id) { + Query query = em.createQuery("delete " + + "from OtusStudent s " + + "where s.id = :id"); + query.setParameter("id", id); + query.executeUpdate(); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..c935924c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + + private static final int EXPECTED_QUERIES_COUNT = 21; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml new file mode 100644 index 00000000..7d216f89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml new file mode 100644 index 00000000..12b3b9df --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + ru.otus + jpql-solution-05 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..357c2224 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе +@NamedEntityGraph(name = "otus-student-avatars-entity-graph", + attributeNodes = {@NamedAttributeNode("avatar")}) +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..b76135a6 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,87 @@ +package ru.otus.example.ormdemo.repositories; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + EntityGraph entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph"); + TypedQuery query = em.createQuery("select distinct s from OtusStudent s " + + "left join fetch s.emails", OtusStudent.class); + query.setHint(FETCH.getKey(), entityGraph); + return query.getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void updateNameById(long id, String name) { + Query query = em.createQuery("update OtusStudent s " + + "set s.name = :name " + + "where s.id = :id"); + query.setParameter("name", name); + query.setParameter("id", id); + query.executeUpdate(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void deleteById(long id) { + Query query = em.createQuery("delete " + + "from OtusStudent s " + + "where s.id = :id"); + query.setParameter("id", id); + query.executeUpdate(); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml new file mode 100644 index 00000000..8d633961 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: always + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..557be9da --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + + private static final int EXPECTED_QUERIES_COUNT = 11; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml new file mode 100644 index 00000000..d2b811a4 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: always + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml new file mode 100644 index 00000000..ba799ea1 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + ru.otus + jpql-solution-06 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..c28f2655 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,62 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе +@NamedEntityGraph(name = "otus-student-avatars-entity-graph", + attributeNodes = {@NamedAttributeNode("avatar")}) +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + // Все данные талицы будут загружены в память отдельным запросом и соединены с родительской сущностью + @Fetch(FetchMode.SUBSELECT) + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..b76135a6 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,87 @@ +package ru.otus.example.ormdemo.repositories; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + EntityGraph entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph"); + TypedQuery query = em.createQuery("select distinct s from OtusStudent s " + + "left join fetch s.emails", OtusStudent.class); + query.setHint(FETCH.getKey(), entityGraph); + return query.getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void updateNameById(long id, String name) { + Query query = em.createQuery("update OtusStudent s " + + "set s.name = :name " + + "where s.id = :id"); + query.setParameter("name", name); + query.setParameter("id", id); + query.executeUpdate(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void deleteById(long id) { + Query query = em.createQuery("delete " + + "from OtusStudent s " + + "where s.id = :id"); + query.setParameter("id", id); + query.executeUpdate(); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..84ddb33b --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,58 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.ormdemo.models.OtusStudent; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + + private static final int EXPECTED_QUERIES_COUNT = 2; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml new file mode 100644 index 00000000..02f7edee --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml @@ -0,0 +1,24 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + + properties: + hibernate: + format_sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml new file mode 100644 index 00000000..52097596 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + ru.otus + jpql-solution-final + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java new file mode 100644 index 00000000..8d4b413c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.ormdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OrmDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(OrmDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java new file mode 100644 index 00000000..c382bc41 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "avatars") +public class Avatar { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "photo_url", nullable = false, unique = true) + private String photoUrl; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java new file mode 100644 index 00000000..6c9c819a --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java @@ -0,0 +1,25 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "courses") +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name", nullable = false, unique = true) + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java new file mode 100644 index 00000000..69a92ff3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java @@ -0,0 +1,26 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "emails") +public class EMail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "email", nullable = false, unique = true) + private String email; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java new file mode 100644 index 00000000..2d74413e --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java @@ -0,0 +1,62 @@ +package ru.otus.example.ormdemo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity // Указывает, что данный класс является сущностью +@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность +@NamedEntityGraph(name = "otus-student-avatars-entity-graph", + attributeNodes = {@NamedAttributeNode("avatar")}) +public class OtusStudent { + @Id // Позволяет указать какое поле является идентификатором + @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов + private long id; + + // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Указывает на связь между таблицами "один к одному" + @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL) + // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности + @JoinColumn(name = "avatar_id") + private Avatar avatar; + + // Указывает на связь между таблицами "один ко многим" + @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private List emails; + + @Fetch(FetchMode.SELECT) + @BatchSize(size = 5) + // Указывает на связь между таблицами "многие ко многим" + @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью + @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id")) + private List courses; +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java new file mode 100644 index 00000000..d2ab60e1 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java @@ -0,0 +1,86 @@ +package ru.otus.example.ormdemo.repositories; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +// @Transactional должна стоять на методе сервиса. +// Причем, если метод не подразумевает изменения данных в БД то категорически желательно +// выставить у аннотации параметр readOnly в true. +// Но это только упражнение и транзакции мы пока не проходили. +// Поэтому, для упрощения, пока вешаем над классом репозитория +@Transactional +@Repository +public class JpaOtusStudentRepository implements OtusStudentRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaOtusStudentRepository(EntityManager em) { + this.em = em; + } + + @Override + public OtusStudent save(OtusStudent student) { + if (student.getId() == 0) { + em.persist(student); + return student; + } + return em.merge(student); + } + + @Override + public Optional findById(long id) { + return Optional.ofNullable(em.find(OtusStudent.class, id)); + } + + @Override + public List findAll() { + EntityGraph entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph"); + TypedQuery query = em.createQuery("select distinct s from OtusStudent s left join fetch s.emails", OtusStudent.class); + query.setHint(FETCH.getKey(), entityGraph); + return query.getResultList(); + } + + @Override + public List findByName(String name) { + TypedQuery query = em.createQuery("select s " + + "from OtusStudent s " + + "where s.name = :name", + OtusStudent.class); + query.setParameter("name", name); + return query.getResultList(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void updateNameById(long id, String name) { + Query query = em.createQuery("update OtusStudent s " + + "set s.name = :name " + + "where s.id = :id"); + query.setParameter("name", name); + query.setParameter("id", id); + query.executeUpdate(); + } + + // Только для примера, в реальности JPQL лучше использовать только для массовых операций + @Override + public void deleteById(long id) { + Query query = em.createQuery("delete " + + "from OtusStudent s " + + "where s.id = :id"); + query.setParameter("id", id); + query.executeUpdate(); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java new file mode 100644 index 00000000..44cc6a58 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java @@ -0,0 +1,18 @@ +package ru.otus.example.ormdemo.repositories; + + +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.util.List; +import java.util.Optional; + +public interface OtusStudentRepository { + OtusStudent save(OtusStudent student); + Optional findById(long id); + + List findAll(); + List findByName(String name); + + void updateNameById(long id, String name); + void deleteById(long id); +} diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml new file mode 100644 index 00000000..ab48d518 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql new file mode 100644 index 00000000..43a684bb --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +create table avatars( + id bigserial, + photo_url varchar(8000), + primary key (id) +); + +create table courses( + id bigserial, + name varchar(255), + primary key (id) +); + +create table otus_students( + id bigserial, + name varchar(255), + avatar_id bigint references avatars (id), + primary key (id) +); + +create table emails( + id bigserial, + student_id bigint references otus_students(id) on delete cascade, + email varchar(255), + primary key (id) +); + +create table student_courses( + student_id bigint references otus_students(id) on delete cascade, + course_id bigint references courses(id), + primary key (student_id, course_id) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java new file mode 100644 index 00000000..ea8b4d65 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java @@ -0,0 +1,113 @@ +package ru.otus.example.ormdemo.repositories; + +import lombok.val; +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.jdbc.spi.SqlStatementLogger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import ru.otus.example.ormdemo.models.OtusStudent; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Репозиторий на основе Jpa для работы со студентами ") +@DataJpaTest +@Import(JpaOtusStudentRepository.class) +class JpaOtusStudentRepositoryTest { + + private static final int EXPECTED_NUMBER_OF_STUDENTS = 10; + private static final long FIRST_STUDENT_ID = 1L; + + private static final int EXPECTED_QUERIES_COUNT = 3; + + @Autowired + private JpaOtusStudentRepository repositoryJpa; + + @Autowired + private TestEntityManager em; + + @DisplayName(" должен загружать информацию о нужном студенте по его id") + @Test + void shouldFindExpectedStudentById() { + val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID); + val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID); + assertThat(optionalActualStudent).isPresent().get() + .usingRecursiveComparison().isEqualTo(expectedStudent); + } + + + @DisplayName("должен загружать список всех студентов с полной информацией о них") + @Test + void shouldReturnCorrectStudentsListWithAllInfo() { + SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class); + sessionFactory.getStatistics().setStatisticsEnabled(true); + + + System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------"); + val students = repositoryJpa.findAll(); + assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS) + .allMatch(s -> !s.getName().equals("")) + .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0) + .allMatch(s -> s.getAvatar().getPhotoUrl() != null) + .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0); + System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n"); + assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT); + } + + @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + @DisplayName("должен загружать ожидаемый список студентов по номеру страницы") + @Test + void shouldReturnCorrectStudentsListByPage() { + AtomicInteger studentsSelectionsCount = new AtomicInteger(0); + applyCustomSqlStatementLogger(new SqlStatementLogger(true, false, false, 0) { + @Override + public void logStatement(String statement) { + super.logStatement(statement); + if (!statement.contains("count") && statement.contains("from otus_students")) { + studentsSelectionsCount.incrementAndGet(); + assertThat(statement).contains("offset").contains("rows only"); + } + } + }); + + + var studentsCount = em.getEntityManager() + .createQuery("select count(s) from OtusStudent s", Long.class).getSingleResult(); + var pageNum = 2; + var pageSize = 3; + var pagesCount = (long) Math.ceil(studentsCount * 1d / pageSize); + + var query = em.getEntityManager().createQuery("select s from OtusStudent s ", OtusStudent.class); + //var query = em.getEntityManager().createQuery("select distinct s from OtusStudent s " + + // "left join fetch s.courses c", OtusStudent.class); + var students = query.setFirstResult(pageNum * pageSize).setMaxResults(pageSize).getResultList(); + + assertThat(pagesCount).isEqualTo(4); + assertThat(students).isNotNull().hasSize(pageSize); + assertThat(studentsSelectionsCount.get()).isEqualTo(1); + } + + private void applyCustomSqlStatementLogger(SqlStatementLogger customSqlStatementLogger) { + StandardServiceRegistry serviceRegistry = em.getEntityManager().getEntityManagerFactory() + .unwrap(SessionFactory.class).getSessionFactoryOptions().getServiceRegistry(); + var jdbcServices = serviceRegistry.getService(JdbcServices.class); + try { + Field field = jdbcServices.getClass().getDeclaredField("sqlStatementLogger"); + field.setAccessible(true); + field.set(jdbcServices, customSqlStatementLogger); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml new file mode 100644 index 00000000..9ba1bec9 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + jpa: + generate-ddl: false + #generate-ddl: true + hibernate: + ddl-auto: none + #ddl-auto: create-drop + + #show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql new file mode 100644 index 00000000..a8db6b85 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql @@ -0,0 +1,29 @@ +insert into avatars(photo_url) +values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'), + ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10'); + +insert into courses(name) +values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'), + ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11'); + +insert into otus_students(name, avatar_id) +values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5), + ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10); + + +insert into emails(email, student_id) +values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4), + ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10); + + +insert into student_courses(student_id, course_id) +values (1, 1), (1, 2), (1, 3), + (2, 2), (2, 4), (2, 5), + (3, 3), (3, 6), (3, 7), + (4, 4), (4, 8), (4, 9), + (5, 5), (5, 10), (5, 1), + (6, 6), (6, 2), (6, 3), + (7, 7), (7, 4), (7, 5), + (8, 8), (8, 6), (8, 7), + (9, 9), (9, 8), (9, 10), + (10, 10), (10, 1), (10, 2); diff --git a/2026-01/spring-11-jpql/jpql-class-work/pom.xml b/2026-01/spring-11-jpql/jpql-class-work/pom.xml new file mode 100644 index 00000000..6f781fad --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-class-work/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + ru.otus + jpql-class-work + 1.0 + + pom + + + jpql-exercise + jpql-solution-01 + jpql-solution-02 + jpql-solution-03 + jpql-solution-04 + jpql-solution-05 + jpql-solution-06 + jpql-solution-final + + diff --git a/2026-01/spring-11-jpql/jpql-demo/.gitignore b/2026-01/spring-11-jpql/jpql-demo/.gitignore new file mode 100644 index 00000000..153c9335 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/2026-01/spring-11-jpql/jpql-demo/README.md b/2026-01/spring-11-jpql/jpql-demo/README.md new file mode 100644 index 00000000..67632929 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/README.md @@ -0,0 +1,6 @@ +## Пример работы с JPQL + +В примере демонстрируется: +* *репозитории на Spring ORM с использованием JPA и JPQL* +* *использование JPQL для написания разного рода запросов (в т.ч. для выборки, агрегации, изменения и удаления данных)* +* *тестирование репозиториев на Spring ORM с использованием @DataJpaTest* diff --git a/2026-01/spring-11-jpql/jpql-demo/pom.xml b/2026-01/spring-11-jpql/jpql-demo/pom.xml new file mode 100644 index 00000000..f580cd89 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + ru.otus.example + jpql-demo + 0.0.1-SNAPSHOT + jpql-demo + Demo project for Spring Boot + + + 17 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java new file mode 100644 index 00000000..5588b288 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java @@ -0,0 +1,13 @@ +package ru.otus.example.jpql_demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JpqlDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(JpqlDemoApplication.class, args); + } + +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java new file mode 100644 index 00000000..29078c2f --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java @@ -0,0 +1,15 @@ +package ru.otus.example.jpql_demo.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CitySalary { + private String city; + private Double salary; + +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java new file mode 100644 index 00000000..7f73ba5c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java @@ -0,0 +1,16 @@ +package ru.otus.example.jpql_demo.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.otus.example.jpql_demo.models.Employee; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EmployeeProjects { + private Employee employee; + private long projectsCount; + +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java new file mode 100644 index 00000000..438f8efe --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java @@ -0,0 +1,25 @@ +package ru.otus.example.jpql_demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "addresses") +public class Address { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "city") + private String city; +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java new file mode 100644 index 00000000..4b342522 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java @@ -0,0 +1,29 @@ +package ru.otus.example.jpql_demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "categories") +@Entity +public class Category { + @Id + private long id; + + @Column(name = "name") + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_category_id") + private Category parent; +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java new file mode 100644 index 00000000..27d90c76 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java @@ -0,0 +1,25 @@ +package ru.otus.example.jpql_demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "departments") +public class Department { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java new file mode 100644 index 00000000..dd62e1f1 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java @@ -0,0 +1,60 @@ +package ru.otus.example.jpql_demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "employees") +public class Employee { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "salary") + private BigDecimal salary; + + @ManyToOne + @JoinColumn(name = "department_id", referencedColumnName = "id") + private Department department; + + @ManyToOne + @JoinColumn(name = "address_id", referencedColumnName = "id") + private Address address; + + @BatchSize(size = 100) + @ManyToMany + @JoinTable(name = "employees_projects", + joinColumns = @JoinColumn(name = "employee_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "project_id", referencedColumnName = "id")) + private List projects; + + + public Employee(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java new file mode 100644 index 00000000..474ac321 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java @@ -0,0 +1,25 @@ +package ru.otus.example.jpql_demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "projects") +public class Project { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java new file mode 100644 index 00000000..385cc6a4 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java @@ -0,0 +1,9 @@ +package ru.otus.example.jpql_demo.repositories; + +import ru.otus.example.jpql_demo.models.Category; + +import java.util.List; + +public interface CategoryRepository { + List findAll(); +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java new file mode 100644 index 00000000..3c00881c --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java @@ -0,0 +1,49 @@ +package ru.otus.example.jpql_demo.repositories; + +import ru.otus.example.jpql_demo.dto.CitySalary; +import ru.otus.example.jpql_demo.dto.EmployeeProjects; +import ru.otus.example.jpql_demo.models.Employee; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +public interface EmployeeRepository { + + List findAll(); + Optional findEmployeeById(long id); + List findAllEmployeesWithSalaryOver100000(); + List findEmployeesFirstNames(); + List findEmployeesFirstAndLastNames(); + long calcEmployeesCount(); + BigDecimal findMaxEmployeeSalary(); + Double calcAvgEmployeeSalary(); + + + List calcAvgSalaryByCities(); + List calcAvgSalaryByCitiesSorted(); + List calcAvgSalaryByCitiesHavingValueOver100000(); + + + List findEmployeesWithGivenProjects(String p1Name, String p2Name); + List findEmployeesProjectsCount(); + + + List findEmployeesWithGivenFirstNames(String name1, String name2); + List findEmployeesWithFirstNamesFromGivenList(List names); + List findEmployeeNameSakes(Employee employee); + + + Employee findEmployeeNameSake(Employee employee); + List findEmployeesWithSalaryLessThanGivenEmployee(Employee employee); + List findEmployeeWithNameMatchingAnyOtherEmployeesNames(); + List findEmployeesWithSalaryLessThanAllEmployees(); + + + int updateEmployeesSalary(BigDecimal oldSalary, BigDecimal newSalary); + int doubleEmployeesSalary(BigDecimal oldSalary); + int deleteEmployeesWithoutDepartment(); + + + +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepository.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepository.java new file mode 100644 index 00000000..5f2b8d21 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepository.java @@ -0,0 +1,24 @@ +package ru.otus.example.jpql_demo.repositories; + +import ru.otus.example.jpql_demo.models.Category; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; + +public class JpaCategoryRepository implements CategoryRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaCategoryRepository(EntityManager em) { + this.em = em; + } + + @Override + public List findAll() { + var query = em.createQuery("select c from Category c", Category.class); + //var query = em.createQuery("select c from Category c where c.parent is not null", Category.class); + return query.getResultList(); + } +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepository.java b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepository.java new file mode 100644 index 00000000..918024a3 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepository.java @@ -0,0 +1,245 @@ +package ru.otus.example.jpql_demo.repositories; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; +import ru.otus.example.jpql_demo.dto.CitySalary; +import ru.otus.example.jpql_demo.dto.EmployeeProjects; +import ru.otus.example.jpql_demo.models.Employee; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +@Repository +public class JpaEmployeeRepository implements EmployeeRepository { + + @PersistenceContext + private final EntityManager em; + + public JpaEmployeeRepository(EntityManager em) { + this.em = em; + } + + @Override + public List findAll() { + return em.createQuery("select e from Employee e", Employee.class).getResultList(); + } + + @Override + public Optional findEmployeeById(long id) { + TypedQuery query = em.createQuery( + "select e from Employee e where e.id = :id" + , Employee.class); + query.setParameter("id", id); + try { + return Optional.of(query.getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + public List findAllEmployeesWithSalaryOver100000() { + return em.createQuery( + "select e from Employee e where e.salary > 100000" + , Employee.class).getResultList(); + } + + @Override + public List findEmployeesFirstNames() { + return em.createQuery( + "select e.firstName from Employee e" + , String.class).getResultList(); + } + + @SuppressWarnings("unchecked") + @Override + public List findEmployeesFirstAndLastNames() { + return em.createQuery( + "select e.firstName, e.lastName from Employee e" + ).getResultList(); + } + + @Override + public long calcEmployeesCount() { + return em.createQuery( + "select count(e) from Employee e" + , Long.class).getSingleResult(); + } + + @Override + public BigDecimal findMaxEmployeeSalary() { + return em.createQuery( + "select max(e.salary) from Employee e" + , BigDecimal.class).getSingleResult(); + } + + @Override + public Double calcAvgEmployeeSalary() { + return em.createQuery( + "select avg(e.salary) from Employee e" + , Double.class).getSingleResult(); + } + + //------------------------------------------------------------------------------------------------------- + + @Override + public List calcAvgSalaryByCities() { + return em.createQuery( + "select new ru.otus.example.jpql_demo.dto.CitySalary(e.address.city, avg(e.salary)) " + + "from Employee e " + + "group by e.address.city" + , CitySalary.class).getResultList(); + } + + @Override + public List calcAvgSalaryByCitiesSorted() { + return em.createQuery( + "select new ru.otus.example.jpql_demo.dto.CitySalary(e.address.city, avg(e.salary)) " + + "from Employee e " + + "group by e.address.city " + + "order by avg(e.salary)" + , CitySalary.class).getResultList(); + } + + @Override + public List calcAvgSalaryByCitiesHavingValueOver100000() { + return em.createQuery( + "select new ru.otus.example.jpql_demo.dto.CitySalary(e.address.city, avg(e.salary)) " + + "from Employee e " + + "group by e.address.city " + + "having avg(e.salary) > 100000" + + "order by avg(e.salary) " + , CitySalary.class).getResultList(); + } + + //------------------------------------------------------------------------------------------------------- + + @Override + public List findEmployeesWithGivenProjects(String p1Name, String p2Name) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e join e.projects p1 join e.projects p2 " + + "where p1.name = :p1 and p2.name = :p2" + , Employee.class); + query.setParameter("p1", p1Name); + query.setParameter("p2", p2Name); + return query.getResultList(); + } + + @Override + public List findEmployeesProjectsCount() { + return em.createQuery( + "select new ru.otus.example.jpql_demo.dto.EmployeeProjects(e, count(p)) " + + "from Employee e left join e.projects p " + + "group by e " + + "order by count(p) desc " + , EmployeeProjects.class).getResultList(); + } + + //------------------------------------------------------------------------------------------------------- + + @Override + public List findEmployeesWithGivenFirstNames(String name1, String name2) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e " + + "where e.firstName in (:name1, :name2) " + , Employee.class); + query.setParameter("name1", name1); + query.setParameter("name2", name2); + return query.getResultList(); + } + + @Override + public List findEmployeesWithFirstNamesFromGivenList(List names) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e " + + "where e.firstName in :names " + , Employee.class); + query.setParameter("names", names); + return query.getResultList(); + } + + @Override + public List findEmployeeNameSakes(Employee employee) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e " + + "where e.firstName in (select e2.firstName from Employee e2 where e2.lastName = :lastName and e2.id <> :id) " + , Employee.class); + query.setParameter("lastName", employee.getLastName()); + query.setParameter("id", employee.getId()); + return query.getResultList(); + } + + //------------------------------------------------------------------------------------------------------- + + @Override + public Employee findEmployeeNameSake(Employee employee) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e " + + "where e.firstName = (select e2.firstName from Employee e2 where e2.id = :id) and e.id <> :id " + , Employee.class); + query.setParameter("id", employee.getId()); + return query.getSingleResult(); + } + + @Override + public List findEmployeesWithSalaryLessThanGivenEmployee(Employee employee) { + TypedQuery query = em.createQuery( + "select e " + + "from Employee e " + + "where e.salary < (select e2.salary from Employee e2 where e2.id = :id) " + , Employee.class); + query.setParameter("id", employee.getId()); + return query.getResultList(); + } + + @Override + public List findEmployeeWithNameMatchingAnyOtherEmployeesNames() { + return em.createQuery( + "select e " + + "from Employee e " + + "where e.firstName = any(select e2.firstName from Employee e2 where e2.id <> e.id) " + , Employee.class).getResultList(); + } + + @Override + public List findEmployeesWithSalaryLessThanAllEmployees() { + return em.createQuery( + "select e " + + "from Employee e " + + "where e.salary <= all(select e2.salary from Employee e2) " + , Employee.class).getResultList(); + } + + //------------------------------------------------------------------------------------------------------- + + @Override + public int updateEmployeesSalary(BigDecimal oldSalary, BigDecimal newSalary) { + Query query = em.createQuery("update Employee e set e.salary = :newSalary where e.salary = :oldSalary"); + query.setParameter("newSalary", newSalary); + query.setParameter("oldSalary", oldSalary); + return query.executeUpdate(); + } + + @Override + public int doubleEmployeesSalary(BigDecimal oldSalary) { + Query query = em.createQuery("update Employee e set e.salary = e.salary * 2 where e.salary = :oldSalary"); + query.setParameter("oldSalary", oldSalary); + return query.executeUpdate(); + } + + @Override + public int deleteEmployeesWithoutDepartment() { + return em.createQuery("delete from Employee e where e.department is null") + .executeUpdate(); + } +} diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/resources/application.yml b/2026-01/spring-11-jpql/jpql-demo/src/main/resources/application.yml new file mode 100644 index 00000000..b08f33b5 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + properties: + hibernate: + #format_sql: true + diff --git a/2026-01/spring-11-jpql/jpql-demo/src/main/resources/schema.sql b/2026-01/spring-11-jpql/jpql-demo/src/main/resources/schema.sql new file mode 100644 index 00000000..d9269b08 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/main/resources/schema.sql @@ -0,0 +1,46 @@ +DROP TABLE IF EXISTS employees_projects; +DROP TABLE IF EXISTS addresses; +DROP TABLE IF EXISTS departments; +DROP TABLE IF EXISTS projects; +DROP TABLE IF EXISTS employees; + +CREATE TABLE addresses ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + city VARCHAR(255) +); + +CREATE TABLE departments ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) +); + +CREATE TABLE projects ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) +); + + +CREATE TABLE employees ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(255), + last_name VARCHAR(255), + salary BIGINT, + address_id BIGINT, + department_id BIGINT, + FOREIGN KEY(address_id) REFERENCES addresses(id) ON DELETE CASCADE, + FOREIGN KEY(department_id) REFERENCES departments(id) ON DELETE CASCADE +); + +CREATE TABLE employees_projects ( + employee_id BIGINT, + project_id BIGINT, + FOREIGN KEY(employee_id) REFERENCES employees(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE +); + + +CREATE TABLE categories ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + parent_category_id BIGINT REFERENCES categories(id) ON DELETE CASCADE, + name VARCHAR(255) +); \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepositoryTest.java b/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepositoryTest.java new file mode 100644 index 00000000..7b1f6b27 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaCategoryRepositoryTest.java @@ -0,0 +1,34 @@ +package ru.otus.example.jpql_demo.repositories; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.example.jpql_demo.models.Category; + +import java.util.List; + +import static java.util.Objects.isNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.util.StringUtils.hasLength; + +@DisplayName("Репозиторий для Category должен") +@DataJpaTest +@Import(JpaCategoryRepository.class) +@Transactional(propagation = Propagation.NEVER) +class JpaCategoryRepositoryTest { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @DisplayName("возвращать список всех категорий") + @Test + void shouldFindAllCategories() { + List categories = categoryRepository.findAll(); + assertThat(categories) + .allMatch(category -> isNull(category.getParent()) || hasLength(category.getParent().getName())); + } +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepositoryTest.java b/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepositoryTest.java new file mode 100644 index 00000000..335b285b --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/test/java/ru/otus/example/jpql_demo/repositories/JpaEmployeeRepositoryTest.java @@ -0,0 +1,282 @@ +package ru.otus.example.jpql_demo.repositories; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import ru.otus.example.jpql_demo.dto.CitySalary; +import ru.otus.example.jpql_demo.dto.EmployeeProjects; +import ru.otus.example.jpql_demo.models.Employee; + +import jakarta.persistence.NonUniqueResultException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.*; + +@DisplayName("Репозиторий Employee должен") +@DataJpaTest +@Import(JpaEmployeeRepository.class) +class JpaEmployeeRepositoryTest { + + private static final long FIRST_EMPLOYEE_ID = 1L; + private static final long SECOND_EMPLOYEE_ID = 2L; + private static final long THIRD_EMPLOYEE_ID = 3L; + private static final long FOURTH_EMPLOYEE_ID = 4L; + private static final long SEVENTH_EMPLOYEE_ID = 7L; + private static final long EIGTH_EMPLOYEE_ID = 8L; + + private static final int EMPLOYEES_COUNT = 8; + private static final String FIRST_EMPLOYEE_FIRST_NAME = "fn1"; + + private static final String PROJECT_3 = "Project #3"; + private static final String PROJECT_4 = "Project #4"; + + private static final CitySalary SARATOV_SALARY = new CitySalary("Saratov", 66666.0); + private static final CitySalary OMSK_SALARY = new CitySalary("Omsk", 170000.0); + private static final CitySalary MOSCOW_SALARY = new CitySalary("Moscow", 330100.0); + + private static final int MAX_SALARY = 1000000; + private static final double AVG_SALARY = 211299.75d; + private static final int EMPLOYEES_WITH_SALARY_OVER_100000_COUNT = 4; + private static final int FOURTH_EMPLOYEE_PROJECTS_COUNT = 4; + private static final String NAME_SAKE_NAME_1 = "NameSake1"; + private static final String NAME_SAKE_NAME_2 = "NameSake2"; + + @Autowired + private TestEntityManager em; + + @Autowired + private JpaEmployeeRepository employeeRepository; + + + @DisplayName("возвращать список всех сотрудников") + @Test + void shouldFindAllEmployees() { + List employees = employeeRepository.findAll(); + assertThat(employees).hasSize(EMPLOYEES_COUNT); + } + + @DisplayName("возвращать сотрудника по его id") + @Test + void shouldFindEmployeeById() { + Optional employee = employeeRepository.findEmployeeById(FIRST_EMPLOYEE_ID); + assertThat(employee).isNotEmpty().get() + .hasFieldOrPropertyWithValue("firstName", FIRST_EMPLOYEE_FIRST_NAME); + } + + @DisplayName("возвращать список всех сотрудников c окладом более 100000") + @Test + void shouldFindAllEmployeesWithSalaryOver100000() { + List allEmployeesWithSalaryOver100000 = employeeRepository.findAllEmployeesWithSalaryOver100000(); + assertThat(allEmployeesWithSalaryOver100000).size().isEqualTo(EMPLOYEES_WITH_SALARY_OVER_100000_COUNT); + } + + @DisplayName("возвращать список имен всех сотрудников") + @Test + void shouldFindEmployeesFirstNames() { + List employeesFirstNames = employeeRepository.findEmployeesFirstNames(); + assertThat(employeesFirstNames) + .containsExactlyInAnyOrder("fn1", "fn2", "fn3", "fn4", "fn5", "fn6", "fn7", "fn8"); + } + + @DisplayName("возвращать список имен и фамилий всех сотрудников") + @Test + void shouldFindEmployeesFirstAndLastNames() { + List employeesFirstAndLastNames = employeeRepository.findEmployeesFirstAndLastNames(); + String[][] expectedFirstAndLastNames = new String[EMPLOYEES_COUNT][2]; + IntStream.range(1, EMPLOYEES_COUNT + 1) + .forEachOrdered(i -> expectedFirstAndLastNames[i - 1] = new String[]{"fn" + i, "ln" + i}); + assertThat(employeesFirstAndLastNames).containsExactlyInAnyOrder(expectedFirstAndLastNames); + } + + @DisplayName("считать общее количество сотрудников") + @Test + void shouldCalcEmployeesCount() { + long employeesCount = employeeRepository.calcEmployeesCount(); + assertThat(employeesCount).isEqualTo(EMPLOYEES_COUNT); + } + + @DisplayName("находить максимальный оклад сотрудников") + @Test + void shouldFindMaxEmployeeSalary() { + BigDecimal maxSalary = employeeRepository.findMaxEmployeeSalary(); + assertThat(maxSalary).isEqualTo(new BigDecimal(MAX_SALARY)); + } + + @DisplayName("считать средний оклад всех сотрудников") + @Test + void shouldCalcAvgEmployeeSalary() { + Double avgSalary = employeeRepository.calcAvgEmployeeSalary(); + assertThat(avgSalary).isEqualTo(AVG_SALARY, offset(0.01d)); + } + + //------------------------------------------------------------------------------------------------------- + + @DisplayName("возвращать список окладов по городам") + @Test + void shouldCalcAvgEmployeeSalaryByCities() { + List avgSalaryByCities = employeeRepository.calcAvgSalaryByCities(); + assertThat(avgSalaryByCities) + .containsExactlyInAnyOrder(SARATOV_SALARY, MOSCOW_SALARY, OMSK_SALARY); + } + + @DisplayName("возвращать сортированный список окладов по городам") + @Test + void shouldCalcAvgEmployeeSalaryByCitiesSorted() { + List avgSalaryByCities = employeeRepository.calcAvgSalaryByCitiesSorted(); + assertThat(avgSalaryByCities) + .containsExactly(SARATOV_SALARY, OMSK_SALARY, MOSCOW_SALARY); + } + + @DisplayName("возвращать список окладов по городам, где средний доход сотрудников более 100000") + @Test + void shouldCalcAvgEmployeeSalaryByCitiesHavingValueOver100000() { + List avgSalaryByCities = employeeRepository.calcAvgSalaryByCitiesHavingValueOver100000(); + assertThat(avgSalaryByCities).containsExactly(OMSK_SALARY, MOSCOW_SALARY); + } + + //------------------------------------------------------------------------------------------------------- + + @DisplayName("возвращать список всех сотрудников работающих над заданными проектами") + @Test + void shouldFindEmployeesWithGivenProjects() { + Employee employee2 = em.find(Employee.class, SECOND_EMPLOYEE_ID); + Employee employee4 = em.find(Employee.class, FOURTH_EMPLOYEE_ID); + List employees = employeeRepository.findEmployeesWithGivenProjects(PROJECT_3, PROJECT_4); + assertThat(employees).hasSize(2).containsExactlyInAnyOrder(employee2, employee4); + } + + @DisplayName("возвращать количество проектов по сотрудникам") + @Test + void shouldFindEmployeesProjectsCount() { + Employee employee4 = em.find(Employee.class, FOURTH_EMPLOYEE_ID); + List employeeProjects = employeeRepository.findEmployeesProjectsCount(); + assertThat(employeeProjects).hasSize(EMPLOYEES_COUNT) + .contains(new EmployeeProjects(employee4, FOURTH_EMPLOYEE_PROJECTS_COUNT)); + } + + //------------------------------------------------------------------------------------------------------- + + @DisplayName("возвращать список всех сотрудников имеющих одно из двух заданных имен") + @Test + void shouldFindEmployeesWithGivenFirstNames() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee7 = em.find(Employee.class, SEVENTH_EMPLOYEE_ID); + List employees = employeeRepository.findEmployeesWithGivenFirstNames("fn1", "fn7"); + assertThat(employees).hasSize(2).containsExactlyInAnyOrder(employee1, employee7); + } + + @DisplayName("возвращать список всех сотрудников имеющих имя, совпадающее с одним из заданного списка") + @Test + void shouldFindEmployeesWithFirstNamesFromGivenList() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee7 = em.find(Employee.class, SEVENTH_EMPLOYEE_ID); + List employees = employeeRepository.findEmployeesWithFirstNamesFromGivenList(List.of("fn1", "fn7")); + assertThat(employees).hasSize(2).containsExactlyInAnyOrder(employee1, employee7); + } + + @DisplayName("возвращать список всех однофамильцев заданного сотрудника") + @Test + void shouldFindEmployeesNameSakes() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee9 = em.persistAndFlush(new Employee(NAME_SAKE_NAME_1, employee1.getLastName())); + Employee employee10 = em.persistAndFlush(new Employee(NAME_SAKE_NAME_2, employee1.getLastName())); + List nameSakes = employeeRepository.findEmployeeNameSakes(employee1); + assertThat(nameSakes).hasSize(2).containsExactlyInAnyOrder(employee9, employee10); + } + + //------------------------------------------------------------------------------------------------------- + + @DisplayName("возвращать список всех тезок заданного сотрудника") + @Test + void shouldFindEmployeeNameSake() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee9 = em.persistAndFlush(new Employee(employee1.getFirstName(), NAME_SAKE_NAME_1)); + Employee nameSake = employeeRepository.findEmployeeNameSake(employee1); + assertThat(nameSake).usingRecursiveComparison().isEqualTo(employee9); + + em.persistAndFlush(new Employee(employee1.getFirstName(), NAME_SAKE_NAME_2)); + assertThatCode(() -> employeeRepository.findEmployeeNameSake(employee1)) + .isInstanceOf(NonUniqueResultException.class); + } + + @DisplayName("возвращать список всех сотрудников имеющих оклад меньше, чем у заданного сотрудника") + @Test + void shouldFindEmployeesWithSalaryLessThanGivenEmployee() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee2 = em.find(Employee.class, SECOND_EMPLOYEE_ID); + Employee employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + Employee employee7 = em.find(Employee.class, SEVENTH_EMPLOYEE_ID); + List employees = employeeRepository.findEmployeesWithSalaryLessThanGivenEmployee(employee7); + assertThat(employees).hasSize(3).containsExactlyInAnyOrder(employee1, employee2, employee3); + } + + @DisplayName("возвращать сотрудника, являющегося тезкой любому другому сотруднику") + @Test + void shouldFindEmployeeWithNameMatchingAnyOtherEmployeesNames() { + Employee employee1 = em.find(Employee.class, FIRST_EMPLOYEE_ID); + Employee employee9 = em.persistAndFlush(new Employee(employee1.getFirstName(), NAME_SAKE_NAME_1)); + List nameSakes = employeeRepository.findEmployeeWithNameMatchingAnyOtherEmployeesNames(); + assertThat(nameSakes).hasSize(2).containsExactlyInAnyOrder(employee9, employee1); + } + + @DisplayName("возвращать сотрудника имеющго оклад меньше, чем у всех") + @Test + void shouldFindEmployeesWithSalaryLessThanAllEmployees() { + Employee employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + List employees = employeeRepository.findEmployeesWithSalaryLessThanAllEmployees(); + assertThat(employees).hasSize(1).containsOnly(employee3); + + } + + //------------------------------------------------------------------------------------------------------- + + @DisplayName("изменять значение оклада сотрудника имеющего заданный оклад") + @Test + void shouldUpdateEmployeesSalary() { + Employee employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + BigDecimal oldSalary = employee3.getSalary(); + BigDecimal newSalary = oldSalary.multiply(new BigDecimal(2)); + em.detach(employee3); + employeeRepository.updateEmployeesSalary(oldSalary, newSalary); + + employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + assertThat(employee3.getSalary()).isEqualTo(newSalary); + } + + @DisplayName("изменять значение оклада в два раза, у сотрудника имеющего заданный оклад") + @Test + void shouldDoubleEmployeesSalary() { + Employee employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + BigDecimal oldSalary = employee3.getSalary(); + BigDecimal newSalary = oldSalary.multiply(new BigDecimal(2)); + em.detach(employee3); + employeeRepository.doubleEmployeesSalary(oldSalary); + + employee3 = em.find(Employee.class, THIRD_EMPLOYEE_ID); + assertThat(employee3.getSalary()).isEqualTo(newSalary); + } + + @DisplayName("удалять сотрудников не относящихся ни к одному отделу") + @Test + void shouldDeleteEmployeesWithoutDepartment() { + Employee employee2 = em.find(Employee.class, SECOND_EMPLOYEE_ID); + Employee employee8 = em.find(Employee.class, EIGTH_EMPLOYEE_ID); + assertThat(employee2).isNotNull(); + assertThat(employee8).isNotNull(); + employeeRepository.deleteEmployeesWithoutDepartment(); + + em.clear(); + + employee2 = em.find(Employee.class, SECOND_EMPLOYEE_ID); + employee8 = em.find(Employee.class, EIGTH_EMPLOYEE_ID); + assertThat(employee2).isNull(); + assertThat(employee8).isNull(); + } + +} \ No newline at end of file diff --git a/2026-01/spring-11-jpql/jpql-demo/src/test/resources/application.yml b/2026-01/spring-11-jpql/jpql-demo/src/test/resources/application.yml new file mode 100644 index 00000000..88225c76 --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/test/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + data-locations: test-data.sql + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + properties: + hibernate: + #format_sql: true + diff --git a/2026-01/spring-11-jpql/jpql-demo/src/test/resources/test-data.sql b/2026-01/spring-11-jpql/jpql-demo/src/test/resources/test-data.sql new file mode 100644 index 00000000..c89ebc5f --- /dev/null +++ b/2026-01/spring-11-jpql/jpql-demo/src/test/resources/test-data.sql @@ -0,0 +1,26 @@ +INSERT INTO addresses (city) VALUES ('Saratov'), ('Omsk'), ('Moscow'); +INSERT INTO departments (name) VALUES ('IT'), ('AHO'); +INSERT INTO projects (name) VALUES ('Project #1'), ('Project #2'), ('Project #3'), ('Project #4'); + +INSERT INTO employees (first_name, last_name, salary, address_id, department_id) +VALUES ('fn1', 'ln1', 70000, 1, 1), + ('fn2', 'ln2', 99998, 1, null), + ('fn3', 'ln3', 30000, 1, 2), + + ('fn4', 'ln4', 170000, 2, 1), + + ('fn5', 'ln5', 120000, 3, 1), + ('fn6', 'ln6', 100400, 3, 1), + ('fn7', 'ln7', 100000, 3, 1), + ('fn8', 'ln8', 1000000, 3, null); + + +INSERT INTO employees_projects (employee_id, project_id) +VALUES (1, 1), (1, 2), (1, 3), + (2, 3), (2, 4), + (4, 1), (4, 2), (4, 3), (4, 4); + + +INSERT INTO categories (parent_category_id, name) +VALUES (null, 'Parent category #1'), (null, 'Parent category #2'), (null, 'Parent category #3'), + (1, 'Child category #1'), (2, 'Child category #2'), (3, 'Child category #3'); \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/.gitignore b/2026-01/spring-13-data-jpa/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/demo/.gitignore b/2026-01/spring-13-data-jpa/demo/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/demo/pom.xml b/2026-01/spring-13-data-jpa/demo/pom.xml new file mode 100644 index 00000000..5126e974 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + demo + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..bd056210 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,75 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.jpa.domain.Specification; +import ru.otus.springdata.domain.Email; +import ru.otus.springdata.domain.Person; +import ru.otus.springdata.repository.EmailRepository; +import ru.otus.springdata.repository.PersonRepository; + +import java.util.Objects; +import java.util.stream.Collectors; + +import static ru.otus.springdata.repository.PersonSpecification.emailAddressLike; +import static ru.otus.springdata.repository.PersonSpecification.nameLike; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository personRepository = context.getBean(PersonRepository.class); + EmailRepository emailRepository = context.getBean(EmailRepository.class); + + var pushkin = new Person("Александр Сергеевич Пушкин", new Email("alex.pushkin@mail.ru")); + var block = new Person("Александр Александрович Блок", new Email("alex.block@mail.ru")); + var lermontov = new Person("Михаил Юрьевич Лермонтов", new Email("michail.lermontov@bk.ru")); + var gorbachev = new Person("Михаил Сергеевич Горбачев", new Email("gorbachev@mail.ru")); + var bulgakov = new Person("Михаил Афанасьевич Булгаков", new Email("bulgakov@mail.ru")); + + emailRepository.save(pushkin.getEmail()); + emailRepository.save(block.getEmail()); + emailRepository.save(lermontov.getEmail()); + emailRepository.save(gorbachev.getEmail()); + emailRepository.save(bulgakov.getEmail()); + + personRepository.save(pushkin); + personRepository.save(block); + personRepository.save(lermontov); + personRepository.save(gorbachev); + personRepository.save(bulgakov); + + System.out.println("\n\nИщем почту Горбачева по его id"); + emailRepository.findByPersonId(gorbachev.getId()) + .ifPresent(System.out::println); + + + System.out.println("\n\nС помощью Example ищем всех пёрсонов с именем \"Михаил\" и почтой на \"mail.ru\""); + ExampleMatcher ignoringExampleMatcher = ExampleMatcher.matchingAll() + .withMatcher("email.address", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()) + .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()) + .withIgnorePaths("id", "email.id"); + + Example example = Example.of(new Person("Михаил", new Email(0, "mail.ru")), ignoringExampleMatcher); + + System.out.println(personRepository.findAll(example).stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + + System.out.println("\n\nС помощью Specification ищем всех пёрсонов с именем \"Александр\" или с почтой на \"bk.ru\""); + + Specification specification = Specification.where(nameLike("Александр")) + .or(emailAddressLike("bk.ru")); + + System.out.println(personRepository.findAll(specification).stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\n"); + + } +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..bf40bee2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,26 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Email { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..499cd632 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,34 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + @OneToOne(orphanRemoval = true) + @JoinColumn(name = "email_id") + private Email email; + + public Person(String name, Email email) { + this.name = name; + this.email = email; + } + +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..a76a43ff --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,21 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.springdata.domain.Email; + +import java.util.Optional; + +public interface EmailRepository extends JpaRepository, EmailRepositoryCustom { + + @Query("select e from Email e where e.address = :address") + Optional findByEmailAddress(@Param("address") String email); + + @Modifying + @Transactional + @Query("update Email e set e.address = :address where e.id = :id") + void updateEmailById(@Param("id") long id, @Param("address") String address); +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustom.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustom.java new file mode 100644 index 00000000..db7112e1 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustom.java @@ -0,0 +1,9 @@ +package ru.otus.springdata.repository; + +import ru.otus.springdata.domain.Email; + +import java.util.Optional; + +public interface EmailRepositoryCustom { + Optional findByPersonId(long personId); +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustomImpl.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustomImpl.java new file mode 100644 index 00000000..18f9ac33 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/EmailRepositoryCustomImpl.java @@ -0,0 +1,20 @@ +package ru.otus.springdata.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import ru.otus.springdata.domain.Email; +import ru.otus.springdata.domain.Person; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class EmailRepositoryCustomImpl implements EmailRepositoryCustom { + + private final PersonRepository personRepository; + + @Override + public Optional findByPersonId(long personId) { + return personRepository.findById(personId).map(Person::getEmail); + } +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..7bb5c825 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,20 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; +import ru.otus.springdata.domain.Person; + +import java.util.List; +import java.util.Optional; + +public interface PersonRepository extends JpaRepository, JpaSpecificationExecutor { + + @EntityGraph(attributePaths = "email") + List findAll(); + + Optional findByName(String s); + + Optional findByEmailAddress(String email); +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonSpecification.java b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonSpecification.java new file mode 100644 index 00000000..3f8c67b3 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/java/ru/otus/springdata/repository/PersonSpecification.java @@ -0,0 +1,21 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.domain.Specification; +import ru.otus.springdata.domain.Person; + +public class PersonSpecification { + + public static Specification nameLike(String name) { + if (name == null) { + return null; + } + return (root, query, cb) -> cb.like(root.get("name"), "%" + name + "%"); + } + + public static Specification emailAddressLike(String address) { + if (address == null) { + return null; + } + return (root, query, cb) -> cb.like(root.join("email").get("address"), "%" + address + "%"); + } +} diff --git a/2026-01/spring-13-data-jpa/demo/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/demo/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/demo/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/exercise/.gitignore b/2026-01/spring-13-data-jpa/exercise/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/exercise/pom.xml b/2026-01/spring-13-data-jpa/exercise/pom.xml new file mode 100644 index 00000000..18c8aa98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + ru.otus + exercise + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..dbbe3598 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,23 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + + + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + //PersonRepository personRepository = context.getBean(PersonRepository.class); + //EmailRepository emailRepository = context.getBean(EmailRepository.class); + + // personRepository.save(new Person("Александр Сергеевич Пушкин")); + // personRepository.save(new Person("Михаил Юрьевич Лермонтов")); + // personRepository.save(new Person("Михаил Сергеевич Горбачев")); + } + + +} diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..5cb61fa0 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,21 @@ +package ru.otus.springdata.domain; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Email { + + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } + +} diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..b087adca --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,25 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + public Person(String name) { + this.name = name; + } +} diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..7a446b95 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,4 @@ +package ru.otus.springdata.repository; + +public interface EmailRepository { +} diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..f8e5fc8a --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,4 @@ +package ru.otus.springdata.repository; + +public interface PersonRepository { +} diff --git a/2026-01/spring-13-data-jpa/exercise/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/exercise/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/exercise/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/pom.xml b/2026-01/spring-13-data-jpa/pom.xml new file mode 100644 index 00000000..922fc48a --- /dev/null +++ b/2026-01/spring-13-data-jpa/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + ru.otus + spring-11-data-jpa + 1.0 + + pom + + + exercise + solution-01 + solution-02 + solution-03 + solution-04 + demo + + diff --git a/2026-01/spring-13-data-jpa/solution-01/.gitignore b/2026-01/spring-13-data-jpa/solution-01/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/solution-01/pom.xml b/2026-01/spring-13-data-jpa/solution-01/pom.xml new file mode 100644 index 00000000..269c2fab --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + solution-01 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..4961485f --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,37 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import ru.otus.springdata.domain.Person; +import ru.otus.springdata.repository.PersonRepository; + +import java.util.Objects; +import java.util.stream.Collectors; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository personRepository = context.getBean(PersonRepository.class); + + + personRepository.save(new Person("Александр Сергеевич Пушкин")); + personRepository.save(new Person("Михаил Юрьевич Лермонтов")); + personRepository.save(new Person("Михаил Сергеевич Горбачев")); + + System.out.println("\n\nИщем всех пёрсонов"); + System.out.println(personRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина"); + personRepository.findByName("Александр Сергеевич Пушкин") + .ifPresent(System.out::println); + + + System.out.println("\n\n"); + + } +} diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..918f052a --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,24 @@ +package ru.otus.springdata.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Email { + + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } +} diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..e5905f85 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,27 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + public Person(String name) { + this.name = name; + } + +} diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..50b72f9b --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,12 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.otus.springdata.domain.Email; + +import java.util.List; + +public interface EmailRepository { + + //@Override + List findAll(); +} diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..012ded26 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,15 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.springdata.domain.Person; + +import java.util.List; +import java.util.Optional; + +public interface PersonRepository extends CrudRepository { + + @Override + List findAll(); + + Optional findByName(String s); +} diff --git a/2026-01/spring-13-data-jpa/solution-01/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/solution-01/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-01/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/solution-02/.gitignore b/2026-01/spring-13-data-jpa/solution-02/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/solution-02/pom.xml b/2026-01/spring-13-data-jpa/solution-02/pom.xml new file mode 100644 index 00000000..8fe1219e --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + solution-02 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..d25a29d2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,53 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import ru.otus.springdata.domain.Email; +import ru.otus.springdata.domain.Person; +import ru.otus.springdata.repository.EmailRepository; +import ru.otus.springdata.repository.PersonRepository; + +import java.util.Objects; +import java.util.stream.Collectors; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + PersonRepository personRepository = context.getBean(PersonRepository.class); + EmailRepository emailRepository = context.getBean(EmailRepository.class); + + var pushkinEmail = new Email("alex.pushkin@mail.ru"); + var lermontovEmail = new Email("michail.lermontov@mail.ru"); + var gorbachevEmail = new Email("gorbachev@mail.ru"); + + var pushkin = new Person("Александр Сергеевич Пушкин"); + var lermontov = new Person("Михаил Юрьевич Лермонтов"); + var gorbachev = new Person("Михаил Сергеевич Горбачев"); + + emailRepository.save(pushkinEmail); + emailRepository.save(lermontovEmail); + emailRepository.save(gorbachevEmail); + + personRepository.save(pushkin); + personRepository.save(lermontov); + personRepository.save(gorbachev); + + System.out.println("\n\nИщем всех пёрсонов"); + System.out.println(personRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина"); + personRepository.findByName("Александр Сергеевич Пушкин") + .ifPresent(System.out::println); + + System.out.println("\n\nИщем все почты"); + System.out.println(emailRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\n"); + } +} diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..bea8c7a5 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,27 @@ +package ru.otus.springdata.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Email { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } +} diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..e5905f85 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,27 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + public Person(String name) { + this.name = name; + } + +} diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..3d5c3152 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,13 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.springdata.domain.Email; + +import java.util.Optional; + +public interface EmailRepository extends JpaRepository{ +} diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..cd5bb3f0 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,14 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.springdata.domain.Person; + +import java.util.List; +import java.util.Optional; + +public interface PersonRepository extends CrudRepository { + + List findAll(); + + Optional findByName(String s); +} diff --git a/2026-01/spring-13-data-jpa/solution-02/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/solution-02/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-02/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/solution-03/.gitignore b/2026-01/spring-13-data-jpa/solution-03/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/solution-03/pom.xml b/2026-01/spring-13-data-jpa/solution-03/pom.xml new file mode 100644 index 00000000..9d6ceaf5 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + solution-03 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..a02e3e25 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,55 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import ru.otus.springdata.domain.Email; +import ru.otus.springdata.domain.Person; +import ru.otus.springdata.repository.EmailRepository; +import ru.otus.springdata.repository.PersonRepository; + +import java.util.Objects; +import java.util.stream.Collectors; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository personRepository = context.getBean(PersonRepository.class); + EmailRepository emailRepository = context.getBean(EmailRepository.class); + + var pushkin = new Person("Александр Сергеевич Пушкин", new Email("alex.pushkin@mail.ru")); + var lermontov = new Person("Михаил Юрьевич Лермонтов", new Email("michail.lermontov@mail.ru")); + var gorbachev = new Person("Михаил Сергеевич Горбачев", new Email("gorbachev@mail.ru")); + + emailRepository.save(pushkin.getEmail()); + emailRepository.save(lermontov.getEmail()); + emailRepository.save(gorbachev.getEmail()); + + personRepository.save(pushkin); + personRepository.save(lermontov); + personRepository.save(gorbachev); + + System.out.println("\n\nИщем всех пёрсонов"); + System.out.println(personRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина"); + personRepository.findByName("Александр Сергеевич Пушкин") + .ifPresent(System.out::println); + + System.out.println("\n\nИщем все почты"); + System.out.println(emailRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина по его почте"); + personRepository.findByEmailAddress("alex.pushkin@mail.ru") + .ifPresent(System.out::println); + + System.out.println("\n\n"); + } +} diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..bf40bee2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,26 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Email { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } +} diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..499cd632 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,34 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + @OneToOne(orphanRemoval = true) + @JoinColumn(name = "email_id") + private Email email; + + public Person(String name, Email email) { + this.name = name; + this.email = email; + } + +} diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..16f1d0c9 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,13 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.springdata.domain.Email; + +import java.util.Optional; + +public interface EmailRepository extends JpaRepository { +} diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..a6aa8dd3 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,18 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.repository.CrudRepository; +import ru.otus.springdata.domain.Person; + +import java.util.List; +import java.util.Optional; + +public interface PersonRepository extends CrudRepository { + + @EntityGraph(attributePaths = "email") + List findAll(); + + Optional findByName(String s); + + Optional findByEmailAddress(String email); +} diff --git a/2026-01/spring-13-data-jpa/solution-03/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/solution-03/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-03/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-13-data-jpa/solution-04/.gitignore b/2026-01/spring-13-data-jpa/solution-04/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-13-data-jpa/solution-04/pom.xml b/2026-01/spring-13-data-jpa/solution-04/pom.xml new file mode 100644 index 00000000..3872ab8b --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.otus + solution-04 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/Main.java b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/Main.java new file mode 100644 index 00000000..4574b783 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/Main.java @@ -0,0 +1,64 @@ +package ru.otus.springdata; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import ru.otus.springdata.domain.Email; +import ru.otus.springdata.domain.Person; +import ru.otus.springdata.repository.EmailRepository; +import ru.otus.springdata.repository.PersonRepository; + +import java.util.Objects; +import java.util.stream.Collectors; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository personRepository = context.getBean(PersonRepository.class); + EmailRepository emailRepository = context.getBean(EmailRepository.class); + + var pushkin = new Person("Александр Сергеевич Пушкин", new Email("alex.pushkin@mail.ru")); + var lermontov = new Person("Михаил Юрьевич Лермонтов", new Email("michail.lermontov@mail.ru")); + var gorbachev = new Person("Михаил Сергеевич Горбачев", new Email("gorbachev@mail.ru")); + + emailRepository.save(pushkin.getEmail()); + emailRepository.save(lermontov.getEmail()); + emailRepository.save(gorbachev.getEmail()); + + personRepository.save(pushkin); + personRepository.save(lermontov); + personRepository.save(gorbachev); + + System.out.println("\n\nИщем всех пёрсонов"); + System.out.println(personRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина"); + personRepository.findByName("Александр Сергеевич Пушкин") + .ifPresent(System.out::println); + + System.out.println("\n\nИщем все почты"); + System.out.println(emailRepository.findAll().stream().map(Objects::toString) + .collect(Collectors.joining("\n"))); + + System.out.println("\n\nИщем Пушкина по его почте"); + personRepository.findByEmailAddress("alex.pushkin@mail.ru") + .ifPresent(System.out::println); + + System.out.println("\n\nОбновляем почту Лермонтову"); + System.out.println("До обновления: " + lermontov.getEmail()); + emailRepository.updateEmailById(lermontov.getId(), "michail1984@lermontov.ru"); + + System.out.println("\n\nИщем почту Лермонтова по новому адресу"); + emailRepository.findByEmailAddress("michail1984@lermontov.ru") + .ifPresent(e -> System.out.println("После обновления: " + e)); + + System.out.println("\n\n"); + + } +} diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Email.java b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Email.java new file mode 100644 index 00000000..bf40bee2 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Email.java @@ -0,0 +1,26 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Email { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String address; + + public Email(String address) { + this.address = address; + } +} diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Person.java b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Person.java new file mode 100644 index 00000000..499cd632 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/domain/Person.java @@ -0,0 +1,34 @@ +package ru.otus.springdata.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String name; + + @OneToOne(orphanRemoval = true) + @JoinColumn(name = "email_id") + private Email email; + + public Person(String name, Email email) { + this.name = name; + this.email = email; + } + +} diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/EmailRepository.java b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/EmailRepository.java new file mode 100644 index 00000000..66b4da80 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/EmailRepository.java @@ -0,0 +1,21 @@ +package ru.otus.springdata.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.springdata.domain.Email; + +import java.util.Optional; + +public interface EmailRepository extends JpaRepository { + + @Query("select e from Email e where e.address = :address") + Optional findByEmailAddress(@Param("address") String email); + + @Modifying + @Transactional + @Query("update Email e set e.address = :address where e.id = :id") + void updateEmailById(@Param("id") long id, @Param("address") String address); +} diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/PersonRepository.java b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/PersonRepository.java new file mode 100644 index 00000000..64579e9b --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/java/ru/otus/springdata/repository/PersonRepository.java @@ -0,0 +1,20 @@ +package ru.otus.springdata.repository; + +import jakarta.annotation.Nonnull; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.repository.CrudRepository; +import ru.otus.springdata.domain.Person; + +import java.util.List; +import java.util.Optional; + +public interface PersonRepository extends CrudRepository { + + @Nonnull + @EntityGraph(attributePaths = "email") + List findAll(); + + Optional findByName(String s); + + Optional findByEmailAddress(String email); +} diff --git a/2026-01/spring-13-data-jpa/solution-04/src/main/resources/application.yml b/2026-01/spring-13-data-jpa/solution-04/src/main/resources/application.yml new file mode 100644 index 00000000..bd5b0f98 --- /dev/null +++ b/2026-01/spring-13-data-jpa/solution-04/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: never + + jpa: + generate-ddl: true + hibernate: + ddl-auto: create + + properties: + hibernate: + format_sql: false + + show-sql: true + + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/pom.xml b/2026-01/spring-15/spring-data-keyvalue-class-work/pom.xml new file mode 100644 index 00000000..b2fbb1c3 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + ru.otus + spring-data-keyvalue-class-work + 1.0 + + pom + + + spring-data-keyvalue-exercise + spring-data-keyvalue-solution + + diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/pom.xml b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/pom.xml new file mode 100644 index 00000000..169f8ccf --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + ru.otus + spring-data-keyvalue-exercise + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + + 17 + 17 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.data + spring-data-keyvalue + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..a7535258 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,27 @@ +package ru.otus.spring; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +@SpringBootApplication +public class Main { + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + private PersonRepository repository; + + @PostConstruct + public void init() { + repository.save(new Person(1, "Pushkin")); + + repository.findAll(); + } + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Email.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Email.java new file mode 100644 index 00000000..c1e24a7d --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Email.java @@ -0,0 +1,25 @@ +package ru.otus.spring.domain; + +public class Email { + + private int id; + + private String email; + + public Email(String email) { + this.email = email; + } + + public Email(int id, String email) { + this.id = id; + this.email = email; + } + + public int getId() { + return id; + } + + public String getEmail() { + return email; + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..f9b8f0ac --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,32 @@ +package ru.otus.spring.domain; + +public class Person { + + private int id; + private String name; + + public Person(String name) { + this.name = name; + } + + public Person(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4b20e5b7 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/pom.xml b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/pom.xml new file mode 100644 index 00000000..310cb3e7 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + ru.otus + spring-data-keyvalue-solution + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + + 17 + 17 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.data + spring-data-keyvalue + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..8bedd3c6 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,38 @@ +package ru.otus.spring; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.map.repository.config.EnableMapRepositories; +import ru.otus.spring.domain.Email; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.EmailRepository; +import ru.otus.spring.repostory.PersonRepository; + +@SpringBootApplication +@EnableMapRepositories +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + private PersonRepository repository; + + @Autowired + private EmailRepository emailRepository; + + @PostConstruct + public void init() { + repository.save(new Person(1, "Pushkin")); + repository.save(new Person(2, "Lermontov")); + System.out.println(repository.findAll()); + + emailRepository.save(new Email(1, "alex@pushkin.com")); + emailRepository.save(new Email(2, "micha@pushkin.com")); + System.out.println(emailRepository.findAll()); + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Email.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Email.java new file mode 100644 index 00000000..86ddb0c1 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Email.java @@ -0,0 +1,37 @@ +package ru.otus.spring.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.annotation.KeySpace; + +@KeySpace("email") +public class Email { + @Id + private int id; + + private String email; + + public Email(int id, String email) { + this.id = id; + this.email = email; + } + + public Email(String email) { + this.email = email; + } + + public int getId() { + return id; + } + + public String getEmail() { + return email; + } + + @Override + public String toString() { + return "Email{" + + "id=" + id + + ", email='" + email + '\'' + + '}'; + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..b0181bd1 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,45 @@ +package ru.otus.spring.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.annotation.KeySpace; + +@KeySpace("person") +public class Person { + + @Id + private int id; + private String name; + + public Person(int id, String name) { + this.id = id; + this.name = name; + } + + public Person(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepository.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepository.java new file mode 100644 index 00000000..e563fc74 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepository.java @@ -0,0 +1,12 @@ +package ru.otus.spring.repostory; + +import ru.otus.spring.domain.Email; + +import java.util.List; + +public interface EmailRepository { + + List findAll(); + + Email save(Email email); +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepositoryImpl.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepositoryImpl.java new file mode 100644 index 00000000..1bc381d4 --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/EmailRepositoryImpl.java @@ -0,0 +1,29 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.stereotype.Repository; +import ru.otus.spring.domain.Email; + +import java.util.List; +import java.util.stream.StreamSupport; + +@Repository +public class EmailRepositoryImpl implements EmailRepository { + + final private KeyValueOperations keyValueTemplate; + + public EmailRepositoryImpl(KeyValueOperations keyValueTemplate) { + this.keyValueTemplate = keyValueTemplate; + } + + @Override + public List findAll() { + return StreamSupport.stream(keyValueTemplate.findAll(Email.class).spliterator(), false) + .toList(); + } + + @Override + public Email save(Email email) { + return keyValueTemplate.insert(email); + } +} diff --git a/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..8e0760fb --- /dev/null +++ b/2026-01/spring-15/spring-data-keyvalue-class-work/spring-data-keyvalue-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,13 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.keyvalue.repository.KeyValueRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends KeyValueRepository { + + Person save(Person person); + + List findAll(); +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/pom.xml b/2026-01/spring-15/spring-data-mongo-class-work/pom.xml new file mode 100644 index 00000000..a13c15b7 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + ru.otus + spring-data-mongo-class-work + 1.0 + + pom + + + spring-data-mongo-exercise + spring-data-mongo-solution + + diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/pom.xml b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/pom.xml new file mode 100644 index 00000000..6a29b310 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + ru.otus + spring-data-mongo-exercise + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.0.4 + + + + + 17 + 17 + 17 + 4.3.8 + 4.6.1 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + ${flapdoodle.version} + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + ${flapdoodle.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..c0233527 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,26 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws InterruptedException { + ApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository repository = context.getBean(PersonRepository.class); + + repository.save(new Person("Dostoevsky")); + + Thread.sleep(3000); + + System.out.println("\n\n\n----------------------------------------------\n\n"); + System.out.println("Авторы в БД:"); + repository.findAll().forEach(p -> System.out.println(p.getName())); + System.out.println("\n\n----------------------------------------------\n\n\n"); + } +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..2bdc3894 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,27 @@ +package ru.otus.spring.domain; + +public class Person { + + private String id; + private String name; + + public Person(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..763a2288 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,12 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/resources/application.yml b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..d2122b82 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-exercise/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + data: + mongodb: + port: 0 # when flapdoodle using + database: company + +de: + flapdoodle: + mongodb: + embedded: + version: 4.0.2 \ No newline at end of file diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/pom.xml b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/pom.xml new file mode 100644 index 00000000..1cbec2d3 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + ru.otus + spring-data-mongo-solution + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.0.4 + + + + + 17 + 17 + 17 + 4.3.8 + 4.6.1 + + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + ${flapdoodle.version} + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + ${flapdoodle.version} + + + + com.github.cloudyrock.mongock + mongock-spring-v5 + ${mongock.version} + + + + com.github.cloudyrock.mongock + mongodb-springdata-v3-driver + ${mongock.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..3c9e618d --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,30 @@ +package ru.otus.spring; + +import com.github.cloudyrock.spring.v5.EnableMongock; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +@EnableMongock +@EnableMongoRepositories +@SpringBootApplication +public class Main { + + public static void main(String[] args) throws InterruptedException { + ApplicationContext context = SpringApplication.run(Main.class); + + PersonRepository repository = context.getBean(PersonRepository.class); + + repository.save(new Person("Dostoevsky")); + + Thread.sleep(3000); + + System.out.println("\n\n\n----------------------------------------------\n\n"); + System.out.println("Авторы в БД:"); + repository.findAll().forEach(p -> System.out.println(p.getName())); + System.out.println("\n\n----------------------------------------------\n\n\n"); + } +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..12cf6355 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,32 @@ +package ru.otus.spring.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "persons") +public class Person { + + @Id + private String id; + private String name; + + public Person(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/mongock/changelog/DatabaseChangelog.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/mongock/changelog/DatabaseChangelog.java new file mode 100644 index 00000000..f9226e50 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/mongock/changelog/DatabaseChangelog.java @@ -0,0 +1,30 @@ +package ru.otus.spring.mongock.changelog; + +import com.github.cloudyrock.mongock.ChangeLog; +import com.github.cloudyrock.mongock.ChangeSet; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +@ChangeLog +public class DatabaseChangelog { + + @ChangeSet(order = "001", id = "dropDb", author = "stvort", runAlways = true) + public void dropDb(MongoDatabase db) { + db.drop(); + } + + @ChangeSet(order = "002", id = "insertLermontov", author = "ydvorzhetskiy") + public void insertLermontov(MongoDatabase db) { + MongoCollection myCollection = db.getCollection("persons"); + var doc = new Document().append("name", "Lermontov"); + myCollection.insertOne(doc); + } + + @ChangeSet(order = "003", id = "insertPushkin", author = "stvort") + public void insertPushkin(PersonRepository repository) { + repository.save(new Person("Pushkin")); + } +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..763a2288 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,12 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/resources/application.yml b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/resources/application.yml new file mode 100644 index 00000000..39177887 --- /dev/null +++ b/2026-01/spring-15/spring-data-mongo-class-work/spring-data-mongo-solution/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + data: + mongodb: + port: 0 # when flapdoodle using + database: company + +de: + flapdoodle: + mongodb: + embedded: + version: 4.0.2 + +mongock: + runner-type: "ApplicationRunner" # default + #runner-type: "InitializingBean" + change-logs-scan-package: + - ru.otus.spring.mongock.changelog + mongo-db: + write-concern: + journal: false + read-concern: local diff --git a/2026-01/spring-16-view/.gitignore b/2026-01/spring-16-view/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/pom.xml b/2026-01/spring-16-view/pom.xml new file mode 100644 index 00000000..f2ad9bdf --- /dev/null +++ b/2026-01/spring-16-view/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view + 1.0 + + pom + + + spring-mvc-view-exercise + spring-mvc-view-demo + spring-mvc-view-solution1 + spring-mvc-view-solution2 + spring-mvc-view-solution3 + spring-mvc-view-solution4 + + diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/.gitignore b/2026-01/spring-16-view/spring-mvc-view-demo/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/pom.xml b/2026-01/spring-16-view/spring-mvc-view-demo/pom.xml new file mode 100644 index 00000000..f0ec2ddd --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-demo + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..6ec3d275 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,14 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + System.out.printf("Чтобы перейти на страницу сайта открывай: %n%s%n", + "http://localhost:8080"); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/config/LocalizationConfig.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/config/LocalizationConfig.java new file mode 100644 index 00000000..fa2743ac --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/config/LocalizationConfig.java @@ -0,0 +1,34 @@ +package ru.otus.spring.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +import java.util.Locale; + +@Configuration +public class LocalizationConfig implements WebMvcConfigurer { + + @Bean(name = "localeResolver") + public LocaleResolver localeResolver() { + var resolver = new CookieLocaleResolver("locale"); + resolver.setDefaultLocale(new Locale("en")); + return resolver; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + var localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("lang"); + return localeChangeInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java new file mode 100644 index 00000000..028238f5 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; + +@RequiredArgsConstructor +@ControllerAdvice +public class GlobalExceptionHandler { + + private final MessageSource messageSource; + + @ExceptionHandler(NotFoundException.class) + public ModelAndView handeNotFoundException(NotFoundException ex) { + String errorText = messageSource.getMessage("person-not-found-error", null, + LocaleContextHolder.getLocale()); + return new ModelAndView("customError", "errorText", errorText); + } + +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..843693e3 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,8 @@ +package ru.otus.spring.controller; + +public class NotFoundException extends RuntimeException{ + + NotFoundException() { + super("Person not found"); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..4d43c693 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,56 @@ +package ru.otus.spring.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.dto.PersonDto; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll().stream() + .map(PersonDto::fromDomainObject).toList(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + PersonDto person = repository.findById(id) + .map(PersonDto::fromDomainObject) + .orElseThrow(NotFoundException::new); + model.addAttribute("person", person); + return "edit"; + } + + @PostMapping("/edit") + public String savePerson(@Valid @ModelAttribute("person") PersonDto person, + BindingResult bindingResult, + @RequestParam(value = "hobby", defaultValue = "") List hobby) { + if (bindingResult.hasErrors()) { + return "edit"; + } + + log.debug("Hobby from plain RequestParam: {}", String.join(", ", hobby)); + log.debug("Hobby from DTO: {}", person.hobbyAsString()); + + repository.save(person.toDomainObject()); + return "redirect:/"; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..0252c11f --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,29 @@ +package ru.otus.spring.domain; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.FetchType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; + + @ElementCollection(fetch = FetchType.EAGER) + private List hobby; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/dto/PersonDto.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/dto/PersonDto.java new file mode 100644 index 00000000..cc84bf38 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/dto/PersonDto.java @@ -0,0 +1,38 @@ +package ru.otus.spring.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import ru.otus.spring.domain.Person; + +import java.util.List; + +import static org.springframework.util.CollectionUtils.isEmpty; + +@Data +@AllArgsConstructor +public class PersonDto { + + private long id; + + @NotBlank(message = "{name-field-should-not-be-blank}") + @Size(min = 2, max = 10, message = "{name-field-should-has-expected-size}") + private String name; + private List hobby; + + public String hobbyAsString() { + if (isEmpty(hobby)){ + return ""; + } + return String.join(", ", hobby); + } + + public Person toDomainObject(){ + return new Person(id, name, hobby); + } + + public static PersonDto fromDomainObject(Person person) { + return new PersonDto(person.getId(), person.getName(), person.getHobby()); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/application.yml new file mode 100644 index 00000000..6c79a7c5 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/application.yml @@ -0,0 +1,23 @@ +spring: + messages: + encoding: UTF-8 + + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR + ru.otus.spring.controller: DEBUG \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/data.sql new file mode 100644 index 00000000..e3bcb10d --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/data.sql @@ -0,0 +1,10 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); + +insert into person_hobby (person_id, hobby) +values + (1, 'Fishing'), + (1, 'Poetry'), + (2, 'Traveling'), + (2, 'Poetry') +; \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages.properties b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages.properties new file mode 100644 index 00000000..372c1b19 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages.properties @@ -0,0 +1,16 @@ +lang-switcher-header=Select language +en-lang-switch-button-caption=Language - EN +ru-lang-switch-button-caption=Language - RU +persons-table-header=Persons: +persons-table-column-action=Action +person-field-id=ID +person-field-name=Name +person-field-hobby=Hobby +edit-button-caption=Edit +person-form-header=Person Info: +save-button-caption=Save +cancel-button-caption=Cancel +name-field-should-not-be-blank=Name field should not be blank +name-field-should-has-expected-size=Name field should be between 2 and 10 characters +error-text-header=Error +person-not-found-error=Person not found \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_en.properties b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_en.properties new file mode 100644 index 00000000..b156844e --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_en.properties @@ -0,0 +1,16 @@ +lang-switcher-header=Select language +en-lang-switch-button-caption=Language - EN +ru-lang-switch-button-caption=Language - RU +persons-table-header=Persons: +persons-table-column-action=Action +person-field-column-id=ID +person-field-name=Name +person-field-hobby=Hobby +edit-button-caption=Edit +person-form-header=Person Info: +save-button-caption=Save +cancel-button-caption=Cancel +name-field-should-not-be-blank=Name field should not be blank +name-field-should-has-expected-size=Name field should be between 2 and 10 characters +error-text-header=Error +person-not-found-error=Person not found \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_ru.properties b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_ru.properties new file mode 100644 index 00000000..a9739884 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/messages_ru.properties @@ -0,0 +1,16 @@ +lang-switcher-header=\u0412\u044B\u0431\u043E\u0440 \u044F\u0437\u044B\u043A\u0430 +en-lang-switch-button-caption=\u042F\u0437\u044B\u043A - EN +ru-lang-switch-button-caption=\u042F\u0437\u044B\u043A - RU +persons-table-header=\u041F\u0451\u0440\u0441\u043E\u043D\u044B: +persons-table-column-action=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 +person-field-id=\u0410\u0439\u0414\u0438 +person-field-name=\u0418\u043C\u044F +person-field-hobby=\u0425\u043E\u0431\u0431\u0438 +edit-button-caption=\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C +person-form-header=\u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F \u043E \u043F\u0451\u0440\u0441\u043E\u043D\u0435: +save-button-caption=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C +cancel-button-caption=\u041E\u0442\u043C\u0435\u043D\u0430 +name-field-should-not-be-blank=\u0418\u043C\u044F \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +name-field-should-has-expected-size=\u0414\u043B\u0438\u043D\u0430 \u0438\u043C\u0435\u043D\u0438 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043E\u0442 2 \u0434\u043E 10 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 +error-text-header=\u041E\u0448\u0438\u0431\u043A\u0430 +person-not-found-error=\u041F\u0451\u0440\u0441\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/schema.sql new file mode 100644 index 00000000..0482e3d4 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/schema.sql @@ -0,0 +1,11 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), + hobby varchar(500), + primary key (id) +); + +create table person_hobby ( + person_id integer references person(id), + hobby varchar(255) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/static/listmark.png b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/static/listmark.png new file mode 100644 index 00000000..f8eb391b Binary files /dev/null and b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/static/listmark.png differ diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/customError.html b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/customError.html new file mode 100644 index 00000000..b54ef571 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/customError.html @@ -0,0 +1,34 @@ + + + + + List of all persons + + + + + +

Error text

+ Error text + + diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/edit.html new file mode 100644 index 00000000..ff0abf12 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/edit.html @@ -0,0 +1,78 @@ + + + + + Edit person + + + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
Wrong person name error
+
+ +
+ + +
+ +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/list.html new file mode 100644 index 00000000..6acdb815 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/main/resources/templates/list.html @@ -0,0 +1,76 @@ + + + + + List of all persons + + + + + + +

Select language

+ + +

Persons:

+ + + + + + + + + + + + + + + + + + +
IDNameHobbyAction
1John DoeHobby1, hobby2 + Edit +
+ + diff --git a/2026-01/spring-16-view/spring-mvc-view-demo/src/test/java/ru/otus/spring/controller/PersonControllerTest.java b/2026-01/spring-16-view/spring-mvc-view-demo/src/test/java/ru/otus/spring/controller/PersonControllerTest.java new file mode 100644 index 00000000..fcb8dd5e --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-demo/src/test/java/ru/otus/spring/controller/PersonControllerTest.java @@ -0,0 +1,69 @@ +package ru.otus.spring.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import ru.otus.spring.domain.Person; +import ru.otus.spring.dto.PersonDto; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@WebMvcTest(PersonController.class) +class PersonControllerTest { + + @Autowired + private MockMvc mvc; + + @MockitoBean + private PersonRepository personRepository; + + private List persons = List.of(new Person(1L, "Vasya", List.of()), + new Person(2L, "Dima", List.of())); + + @Test + void shouldRenderListPageWithCorrectViewAndModelAttributes() throws Exception { + when(personRepository.findAll()).thenReturn(persons); + List expectedPersons = persons.stream() + .map(PersonDto::fromDomainObject).toList(); + mvc.perform(get("/")) + .andExpect(view().name("list")) + .andExpect(model().attribute("persons", expectedPersons)); + } + + @Test + void shouldRenderEditPageWithCorrectViewAndModelAttributes() throws Exception { + when(personRepository.findById(1L)).thenReturn(Optional.of(persons.get(0))); + PersonDto expectedPerson = PersonDto.fromDomainObject(persons.get(0)); + mvc.perform(get("/edit").param("id", "1")) + .andExpect(view().name("edit")) + .andExpect(model().attribute("person", expectedPerson)); + } + + @Test + void shouldRenderErrorPageWhenPersonNotFound() throws Exception { + when(personRepository.findById(1L)).thenThrow(new NotFoundException()); + mvc.perform(get("/edit").param("id", "1")) + .andExpect(view().name("customError")); + } + + @Test + void shouldSavePersonAndRedirectToContextPath() throws Exception { + when(personRepository.findById(1L)).thenReturn(Optional.of(persons.get(0))); + mvc.perform(post("/edit").param("id", "3").param("name", "Olya")) + .andExpect(view().name("redirect:/")); + verify(personRepository, times(1)).save(any(Person.class)); + } +} \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/.gitignore b/2026-01/spring-16-view/spring-mvc-view-exercise/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/pom.xml b/2026-01/spring-16-view/spring-mvc-view-exercise/pom.xml new file mode 100644 index 00000000..6db51f02 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-exercise + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..09283e91 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,14 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + System.out.printf("Чтобы проверить себя открывай: %n%s%n%s%n", + "http://localhost:8080", "http://localhost:8080/edit?id=1"); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..41b48826 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.otus.spring.controller; + +class NotFoundException extends RuntimeException{ + + NotFoundException() { + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..5a345bbb --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,30 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + return null; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..e40fd3f9 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,22 @@ +package ru.otus.spring.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/application.yml new file mode 100644 index 00000000..c1f5d333 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/data.sql new file mode 100644 index 00000000..7fa18c96 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/schema.sql new file mode 100644 index 00000000..f36202f2 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), primary key (id) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/edit.html new file mode 100644 index 00000000..87d8c8f1 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/edit.html @@ -0,0 +1,49 @@ + + + + + Edit person + + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/list.html new file mode 100644 index 00000000..a5b52a8d --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-exercise/src/main/resources/templates/list.html @@ -0,0 +1,49 @@ + + + + + List of all persons + + + +

Persons:

+ + + + + + + + + + + + + + + + +
IDNameAction
1John Doe + Edit +
+ + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/.gitignore b/2026-01/spring-16-view/spring-mvc-view-solution1/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/pom.xml b/2026-01/spring-16-view/spring-mvc-view-solution1/pom.xml new file mode 100644 index 00000000..06c5d8fe --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-solution1 + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..c94edb10 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +//http://localhost:8080/edit?id=1 +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..41b48826 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.otus.spring.controller; + +class NotFoundException extends RuntimeException{ + + NotFoundException() { + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..2953c703 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,33 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + Person person = repository.findById(id) + .orElseThrow(NotFoundException::new); + model.addAttribute("person", person); + return "edit"; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..e40fd3f9 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,22 @@ +package ru.otus.spring.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/application.yml new file mode 100644 index 00000000..c1f5d333 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/data.sql new file mode 100644 index 00000000..7fa18c96 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/schema.sql new file mode 100644 index 00000000..f36202f2 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), primary key (id) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/edit.html new file mode 100644 index 00000000..7b3f5d6a --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/edit.html @@ -0,0 +1,48 @@ + + + + + Edit person + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/list.html new file mode 100644 index 00000000..0d59a759 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution1/src/main/resources/templates/list.html @@ -0,0 +1,50 @@ + + + + + List of all persons + + + + +

Persons:

+ + + + + + + + + + + + + + + + +
IDNameAction
1John Doe + Edit +
+ + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/.gitignore b/2026-01/spring-16-view/spring-mvc-view-solution2/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/pom.xml b/2026-01/spring-16-view/spring-mvc-view-solution2/pom.xml new file mode 100644 index 00000000..cee45f24 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-solution2 + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..c94edb10 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +//http://localhost:8080/edit?id=1 +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..41b48826 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.otus.spring.controller; + +class NotFoundException extends RuntimeException{ + + NotFoundException() { + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..ad22dbe8 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,32 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + Person person = repository.findById(id).orElseThrow(NotFoundException::new); + model.addAttribute("person", person); + return "edit"; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..e40fd3f9 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,22 @@ +package ru.otus.spring.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/application.yml new file mode 100644 index 00000000..c1f5d333 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/data.sql new file mode 100644 index 00000000..7fa18c96 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/schema.sql new file mode 100644 index 00000000..f36202f2 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), primary key (id) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/edit.html new file mode 100644 index 00000000..ef643cff --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/edit.html @@ -0,0 +1,48 @@ + + + + + Edit person + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/list.html new file mode 100644 index 00000000..a5b52a8d --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution2/src/main/resources/templates/list.html @@ -0,0 +1,49 @@ + + + + + List of all persons + + + +

Persons:

+ + + + + + + + + + + + + + + + +
IDNameAction
1John Doe + Edit +
+ + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/.gitignore b/2026-01/spring-16-view/spring-mvc-view-solution3/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/pom.xml b/2026-01/spring-16-view/spring-mvc-view-solution3/pom.xml new file mode 100644 index 00000000..2ac5e364 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-solution3 + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..418ecb9b --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,14 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +//http://localhost:8080 +//http://localhost:8080/edit?id=1 +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..41b48826 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.otus.spring.controller; + +class NotFoundException extends RuntimeException{ + + NotFoundException() { + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..ad22dbe8 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,32 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + Person person = repository.findById(id).orElseThrow(NotFoundException::new); + model.addAttribute("person", person); + return "edit"; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..e40fd3f9 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,22 @@ +package ru.otus.spring.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/application.yml new file mode 100644 index 00000000..c1f5d333 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/data.sql new file mode 100644 index 00000000..7fa18c96 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/schema.sql new file mode 100644 index 00000000..f36202f2 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), primary key (id) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/static/listmark.png b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/static/listmark.png new file mode 100644 index 00000000..f8eb391b Binary files /dev/null and b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/static/listmark.png differ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/edit.html new file mode 100644 index 00000000..ef643cff --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/edit.html @@ -0,0 +1,48 @@ + + + + + Edit person + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/list.html new file mode 100644 index 00000000..e7983e80 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution3/src/main/resources/templates/list.html @@ -0,0 +1,49 @@ + + + + + List of all persons + + + +

Persons:

+ + + + + + + + + + + + + + + + +
IDNameAction
1John Doe + Edit +
+ + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/.gitignore b/2026-01/spring-16-view/spring-mvc-view-solution4/.gitignore new file mode 100644 index 00000000..4ea52072 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/.gitignore @@ -0,0 +1,24 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/pom.xml b/2026-01/spring-16-view/spring-mvc-view-solution4/pom.xml new file mode 100644 index 00000000..52bec85b --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + ru.otus + spring-mvc-view-solution4 + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + + 17 + 17 + 2.2.220 + 2.0 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.h2database + h2 + runtime + ${h2.version} + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..0335e618 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,15 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +//http://localhost:8080 +//http://localhost:8080/edit?id=1 +//http://localhost:8080/edit?id=111 +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java new file mode 100644 index 00000000..f700c5e6 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; + +@RequiredArgsConstructor +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + public ModelAndView handeNotFoundException(NotFoundException ex) { + return new ModelAndView("customError", + "errorText", "Person not found"); + } + +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/NotFoundException.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/NotFoundException.java new file mode 100644 index 00000000..35b57190 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.otus.spring.controller; + +public class NotFoundException extends RuntimeException{ + + NotFoundException() { + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/PersonController.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/PersonController.java new file mode 100644 index 00000000..971bf323 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/controller/PersonController.java @@ -0,0 +1,39 @@ +package ru.otus.spring.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repostory.PersonRepository; + +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class PersonController { + + private final PersonRepository repository; + + @GetMapping("/") + public String listPage(Model model) { + List persons = repository.findAll(); + model.addAttribute("persons", persons); + return "list"; + } + + @GetMapping("/edit") + public String editPage(@RequestParam("id") long id, Model model) { + Person person = repository.findById(id).orElseThrow(NotFoundException::new); + model.addAttribute("person", person); + return "edit"; + } + + @PostMapping("/edit") + public String savePerson(Person person) { + repository.save(person); + return "redirect:/"; + } +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..e40fd3f9 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,22 @@ +package ru.otus.spring.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4fb88650 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/application.yml b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/application.yml new file mode 100644 index 00000000..c1f5d333 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + sql: + init: + mode: always + + + jpa: + open-in-view: false + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true + +logging: + level: + ROOT: ERROR \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/data.sql b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/data.sql new file mode 100644 index 00000000..7fa18c96 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into person (id, name) values (1, 'Pushkin'); +insert into person (id, name) values (2, 'Lermontov'); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/schema.sql b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/schema.sql new file mode 100644 index 00000000..f36202f2 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table person ( + id integer generated by default as identity, + name varchar(255), primary key (id) +); \ No newline at end of file diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/static/listmark.png b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/static/listmark.png new file mode 100644 index 00000000..f8eb391b Binary files /dev/null and b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/static/listmark.png differ diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/customError.html b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/customError.html new file mode 100644 index 00000000..a4550575 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/customError.html @@ -0,0 +1,34 @@ + + + + + List of all persons + + + + + +

Error:

+ Error text + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/edit.html b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/edit.html new file mode 100644 index 00000000..b5165f40 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/edit.html @@ -0,0 +1,49 @@ + + + + + Edit person + + + + + +
+

Person Info:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/list.html b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/list.html new file mode 100644 index 00000000..e93f2235 --- /dev/null +++ b/2026-01/spring-16-view/spring-mvc-view-solution4/src/main/resources/templates/list.html @@ -0,0 +1,49 @@ + + + + + List of all persons + + + +

Persons:

+ + + + + + + + + + + + + + + + +
IDNameAction
1John Doe + Edit +
+ + diff --git a/2026-01/spring-17-ajax/ajax-demo.html b/2026-01/spring-17-ajax/ajax-demo.html new file mode 100644 index 00000000..7a58829b --- /dev/null +++ b/2026-01/spring-17-ajax/ajax-demo.html @@ -0,0 +1,87 @@ + + + + Технологии JS для отправки запросов + + + + + + + + + + + + + + + + +

+

+

+

+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/2026-01/spring-17-ajax/pom.xml b/2026-01/spring-17-ajax/pom.xml
new file mode 100644
index 00000000..0a47af1c
--- /dev/null
+++ b/2026-01/spring-17-ajax/pom.xml
@@ -0,0 +1,17 @@
+
+
+    4.0.0
+
+    ru.otus
+    spring-mvc-ajax
+    1.0
+
+    pom
+
+    
+        spring-ajax-demo
+        spring-boot-and-react-demo
+    
+
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/pom.xml b/2026-01/spring-17-ajax/spring-ajax-demo/pom.xml
new file mode 100644
index 00000000..360ef7b5
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/pom.xml
@@ -0,0 +1,85 @@
+
+
+    4.0.0
+
+    ru.otus
+    spring-ajax-demo
+    1.0
+
+    
+        org.springframework.boot
+        spring-boot-starter-parent
+        3.5.6
+        
+    
+
+    
+        17
+        17
+        1.18.32
+    
+
+    
+        
+            org.springframework.boot
+            spring-boot-starter
+        
+
+        
+            org.springframework.boot
+            spring-boot-starter-web
+        
+
+        
+            org.springframework.boot
+            spring-boot-starter-thymeleaf
+        
+
+        
+            org.webjars
+            jquery
+            3.7.1
+        
+
+
+        
+            com.h2database
+            h2
+            runtime
+        
+
+        
+            org.springframework.boot
+            spring-boot-starter-data-jpa
+        
+
+        
+            org.projectlombok
+            lombok
+            ${lombok.version}
+            provided
+        
+
+        
+            org.springframework.boot
+            spring-boot-starter-validation
+        
+
+        
+            org.springframework.boot
+            spring-boot-starter-test
+        
+
+    
+
+    
+        
+            
+                org.springframework.boot
+                spring-boot-maven-plugin
+            
+        
+    
+
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..725af191
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,15 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Main {
+    // http://localhost:8080/
+    // http://localhost:8080/api/persons
+    // http://localhost:8080/server/system/info
+    public static void main(String[] args) {
+        SpringApplication.run(Main.class);
+    }
+
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java
new file mode 100644
index 00000000..e201fdcf
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java
@@ -0,0 +1,21 @@
+package ru.otus.spring.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import ru.otus.spring.rest.resolvers.SystemInfoMethodArgumentResolver;
+
+import java.util.List;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private SystemInfoMethodArgumentResolver systemInfoMethodArgumentResolver;
+
+    @Override
+    public void addArgumentResolvers(List resolvers) {
+        resolvers.add(systemInfoMethodArgumentResolver);
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..4c7af816
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,26 @@
+package ru.otus.spring.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "persons")
+public class Person {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private long id;
+
+    @Column(name = "name")
+    private String name;
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java
new file mode 100644
index 00000000..c16a8a78
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java
@@ -0,0 +1,31 @@
+package ru.otus.spring.domain;
+
+public class SystemInfo {
+    private final String osName;
+    private final String timeZone;
+    private final String osArch;
+    private final int processorsCount;
+
+    public SystemInfo(String osName, String timeZone, String osArch, int processorsCount) {
+        this.osName = osName;
+        this.timeZone = timeZone;
+        this.osArch = osArch;
+        this.processorsCount = processorsCount;
+    }
+
+    public String getOsName() {
+        return osName;
+    }
+
+    public String getTimeZone() {
+        return timeZone;
+    }
+
+    public String getOsArch() {
+        return osArch;
+    }
+
+    public int getProcessorsCount() {
+        return processorsCount;
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java
new file mode 100644
index 00000000..5e59b329
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.page;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class PersonPagesController {
+
+    @GetMapping("/")
+    public String listPersonsPage(Model model) {
+        model.addAttribute("keywords", "list users in Omsk, omsk, list users, list users free");
+        return "list";
+    }
+
+    @GetMapping("/add")
+    public String addPersonPage() {
+        return "add";
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
new file mode 100644
index 00000000..5fbe33cc
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
@@ -0,0 +1,7 @@
+package ru.otus.spring.repostory;
+
+import org.springframework.data.repository.ListCrudRepository;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository extends ListCrudRepository {
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java
new file mode 100644
index 00000000..f622165a
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.rest;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import ru.otus.spring.rest.exceptions.NotFoundException;
+
+@RequiredArgsConstructor
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+    public static final String ERROR_STRING = "Тут пёрсонов нет(";
+
+    @ExceptionHandler(NotFoundException.class)
+    public ResponseEntity handeNotFoundException(NotFoundException ex) {
+        return ResponseEntity.status(404).body(ERROR_STRING);
+    }
+
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..d437f390
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,38 @@
+package ru.otus.spring.rest;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import ru.otus.spring.rest.dto.PersonDto;
+import ru.otus.spring.rest.exceptions.NotFoundException;
+import ru.otus.spring.service.PersonService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+@RestController
+public class PersonController {
+
+    private final PersonService personService;
+
+    @GetMapping("/api/persons")
+    public List getAllPersons() {
+        List persons = personService.findAll().stream().map(PersonDto::toDto)
+                .collect(Collectors.toList());
+        if (persons.isEmpty()) {
+            throw new NotFoundException("Persons not found!");
+        }
+        return persons;
+    }
+
+    @PostMapping("/api/persons")
+    public ResponseEntity addPerson(@Valid @RequestBody PersonDto personDto) {
+        var savedPerson = personService.save(personDto.toDomainObject());
+        return ResponseEntity.ok(PersonDto.toDto(savedPerson));
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java
new file mode 100644
index 00000000..c255342f
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.otus.spring.domain.SystemInfo;
+
+@RestController
+public class SystemInfoController {
+
+    @GetMapping("api/server/system/info")
+    public ResponseEntity getServerSystemInfo(SystemInfo systemInfo) {
+        return ResponseEntity.ok(systemInfo);
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
new file mode 100644
index 00000000..d8f03542
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
@@ -0,0 +1,23 @@
+package ru.otus.spring.rest.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+import ru.otus.spring.domain.Person;
+
+@Data
+@AllArgsConstructor
+public class PersonDto {
+
+    private long id;
+    @Length(min = 3)
+    private String name;
+
+    public static PersonDto toDto(Person person) {
+        return new PersonDto(person.getId(), person.getName());
+    }
+
+    public Person toDomainObject() {
+        return new Person(id, name);
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java
new file mode 100644
index 00000000..36593145
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java
@@ -0,0 +1,8 @@
+package ru.otus.spring.rest.exceptions;
+
+public class NotFoundException extends RuntimeException{
+
+    public NotFoundException(String message) {
+        super(message);
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java
new file mode 100644
index 00000000..51f517a7
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java
@@ -0,0 +1,33 @@
+package ru.otus.spring.rest.resolvers;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+import ru.otus.spring.domain.SystemInfo;
+import ru.otus.spring.service.SystemInfoService;
+
+@Component
+public class SystemInfoMethodArgumentResolver implements HandlerMethodArgumentResolver {
+
+    private final SystemInfoService systemInfoService;
+
+    public SystemInfoMethodArgumentResolver(SystemInfoService systemInfoService) {
+        this.systemInfoService = systemInfoService;
+    }
+
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return parameter.getParameterType().equals(SystemInfo.class);
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter,
+                                  ModelAndViewContainer mavContainer,
+                                  NativeWebRequest webRequest,
+                                  WebDataBinderFactory binderFactory) throws Exception {
+        return systemInfoService.getSystemInfo();
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java
new file mode 100644
index 00000000..3604f7df
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java
@@ -0,0 +1,11 @@
+package ru.otus.spring.service;
+
+import ru.otus.spring.domain.Person;
+
+import java.util.List;
+
+public interface PersonService  {
+
+    List findAll();
+    Person save(Person person);
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java
new file mode 100644
index 00000000..22d99a0b
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java
@@ -0,0 +1,27 @@
+package ru.otus.spring.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repostory.PersonRepository;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class PersonServiceImpl implements PersonService {
+
+    private final PersonRepository personRepository;
+
+    @Override
+    public List findAll() {
+        return personRepository.findAll();
+    }
+
+    @Transactional
+    @Override
+    public Person save(Person person) {
+        return personRepository.save(person);
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java
new file mode 100644
index 00000000..26fb34d2
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java
@@ -0,0 +1,17 @@
+package ru.otus.spring.service;
+
+import org.springframework.stereotype.Service;
+import ru.otus.spring.domain.SystemInfo;
+
+@Service
+public class SystemInfoService {
+
+    public SystemInfo getSystemInfo(){
+        String osName = System.getProperty("os.name");
+        String timeZone = System.getProperty("user.timezone");
+        String osArch = System.getProperty("os.arch");
+        int processorsCount = Runtime.getRuntime().availableProcessors();
+        return new SystemInfo(osName, timeZone, osArch, processorsCount);
+
+    }
+}
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml
new file mode 100644
index 00000000..02e01231
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml
@@ -0,0 +1,11 @@
+spring:
+  datasource:
+    url: jdbc:h2:mem:testdb
+    initialization-mode: always
+
+  jpa:
+    generate-ddl: false
+    hibernate:
+      ddl-auto: none
+
+    show-sql: true
\ No newline at end of file
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql
new file mode 100644
index 00000000..b840826c
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql
@@ -0,0 +1,2 @@
+INSERT INTO persons (name) VALUES ('Pushkin'), ('Lermontov');
+select * from persons;
\ No newline at end of file
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql
new file mode 100644
index 00000000..2190e858
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql
@@ -0,0 +1,8 @@
+DROP TABLE IF EXISTS persons;
+
+CREATE TABLE persons (
+    id BIGSERIAL,
+    name VARCHAR(250),
+
+    PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png
new file mode 100644
index 00000000..f8eb391b
Binary files /dev/null and b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png differ
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html
new file mode 100644
index 00000000..11d14e0f
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html
@@ -0,0 +1,75 @@
+
+
+
+    
+    Edit person
+    
+    
+    
+
+
+
+

Form for new person creation:

+
+
+ + +
+ +
+ + +
+
+ +

Saved person:

+

+
+
+
diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html
new file mode 100644
index 00000000..26a0c8a0
--- /dev/null
+++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html
@@ -0,0 +1,88 @@
+
+
+
+    
+    
+    List of all persons
+    
+
+    
+    
+
+
+

System Info:

+

+
+

Persons:

+ +Add new + + + + + + + + + +
IDName
+ + + + + diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java new file mode 100644 index 00000000..5dc2e9d4 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java @@ -0,0 +1,74 @@ +package ru.otus.spring.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import ru.otus.spring.domain.Person; +import ru.otus.spring.rest.dto.PersonDto; +import ru.otus.spring.service.PersonService; +import ru.otus.spring.service.SystemInfoService; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.otus.spring.rest.GlobalExceptionHandler.ERROR_STRING; + +@WebMvcTest(PersonController.class) +class PersonControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper mapper; + + @MockitoBean + private PersonService personService; + + @MockitoBean + private SystemInfoService systemInfoService; + + @Test + void shouldReturnCorrectPersonsList() throws Exception { + List persons = List.of(new Person(1, "Person1"), new Person(2, "Person2")); + given(personService.findAll()).willReturn(persons); + + List expectedResult = persons.stream() + .map(PersonDto::toDto).collect(Collectors.toList()); + + mvc.perform(get("/api/persons")) + .andExpect(status().isOk()) + .andExpect(content().json(mapper.writeValueAsString(expectedResult))); + } + + @Test + void shouldReturnExpectedErrorWhenPersonsNotFound() throws Exception { + given(personService.findAll()).willReturn(List.of()); + + mvc.perform(get("/api/persons")) + .andExpect(status().isNotFound()) + .andExpect(content().string(ERROR_STRING)); + } + + @Test + void shouldCorrectSaveNewPerson() throws Exception { + Person person = new Person(1, "Person1"); + given(personService.save(any())).willReturn(person); + String expectedResult = mapper.writeValueAsString(PersonDto.toDto(person)); + + mvc.perform(post("/api/persons").contentType(APPLICATION_JSON) + .content(expectedResult)) + .andExpect(status().isOk()) + .andExpect(content().json(expectedResult)); + } +} \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java b/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java new file mode 100644 index 00000000..2911919a --- /dev/null +++ b/2026-01/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java @@ -0,0 +1,39 @@ +package ru.otus.spring.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import ru.otus.spring.domain.SystemInfo; +import ru.otus.spring.repostory.PersonRepository; +import ru.otus.spring.service.SystemInfoService; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +@WebMvcTest(SystemInfoController.class) +@Import(SystemInfoService.class) +class SystemInfoControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper mapper; + + @MockitoBean + private PersonRepository repository; + + @Autowired + private SystemInfoService systemInfoService; + + @Test + void shouldReturnCorrectServerSystemInfo() throws Exception { + SystemInfo expectedSystemInfo = systemInfoService.getSystemInfo(); + mvc.perform(get("/api/server/system/info")) + .andExpect(content().json(mapper.writeValueAsString(expectedSystemInfo))); + } +} \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/.gitignore b/2026-01/spring-17-ajax/spring-boot-and-react-demo/.gitignore new file mode 100644 index 00000000..b11e399f --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/.gitignore @@ -0,0 +1,30 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + + +node/ +/node_modules +/output +package-lock.json diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/package.json b/2026-01/spring-17-ajax/spring-boot-and-react-demo/package.json new file mode 100644 index 00000000..22d1433b --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/package.json @@ -0,0 +1,28 @@ +{ + "name": "client", + "version": "1.0.0", + "description": "Simple react demo", + "main": "index.js", + "scripts": { + "dev": "webpack-dev-server --config webpack.dev.config.js", + "build": "webpack" + }, + "author": "", + "license": "ISC", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@babel/core": "^7.19.1", + "babel-loader": "^8.2.5", + "@babel/preset-env": "^7.19.1", + "@babel/preset-react": "^7.18.6", + "react-css-modules": "^4.7.11", + "html-webpack-plugin": "^5.5.0", + "terser-webpack-plugin": "^5.3.6", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.1" + } +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/pom.xml b/2026-01/spring-17-ajax/spring-boot-and-react-demo/pom.xml new file mode 100644 index 00000000..4eb4ea41 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + ru.otus + spring-boot-and-react-demo + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.github.eirslett + frontend-maven-plugin + 1.12.1 + + + install node and npm + + install-node-and-npm + + + v16.13.2 + 8.3.2 + + + + npm install + + npm + + generate-resources + + install + + + + npm run build + + npm + + generate-resources + + run build + + + + + + + diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..9c34a6f3 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,15 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + // http://localhost:8080 + // http://localhost:8080/api/persons + public static void main(String[] args) { + SpringApplication.run(Main.class); + } + +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..81dae69e --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,40 @@ +package ru.otus.spring.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "persons") +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + private String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java new file mode 100644 index 00000000..4b20e5b7 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.repostory; + +import org.springframework.data.repository.CrudRepository; +import ru.otus.spring.domain.Person; + +import java.util.List; + +public interface PersonRepository extends CrudRepository { + + List findAll(); +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java new file mode 100644 index 00000000..47c36b18 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java @@ -0,0 +1,25 @@ +package ru.otus.spring.rest; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.otus.spring.repostory.PersonRepository; +import ru.otus.spring.rest.dto.PersonDto; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +public class PersonController { + + private final PersonRepository repository; + + public PersonController(PersonRepository repository) { + this.repository = repository; + } + + @GetMapping("/api/persons") + public List getAllPersons() { + return repository.findAll().stream().map(PersonDto::toDto) + .collect(Collectors.toList()); + } +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java new file mode 100644 index 00000000..03918447 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Russian Post + * + * This source code is Russian Post Confidential Proprietary. + * This software is protected by copyright. All rights and titles are reserved. + * You shall not use, copy, distribute, modify, decompile, disassemble or reverse engineer the software. + * Otherwise this violation would be treated by law and would be subject to legal prosecution. + * Legal use of the software provides receipt of a license from the right name only. + */ +package ru.otus.spring.rest.dto; + +import ru.otus.spring.domain.Person; + +/** + * DTO that represents Account + */ +@SuppressWarnings("all") +public class PersonDto { + + private int id = -1; + private String name; + + public PersonDto() { + } + + public PersonDto(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static PersonDto toDto(Person person) { + return new PersonDto(person.getId(), person.getName()); + } +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml new file mode 100644 index 00000000..02e01231 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + initialization-mode: always + + jpa: + generate-ddl: false + hibernate: + ddl-auto: none + + show-sql: true \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql new file mode 100644 index 00000000..e60c79ca --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql @@ -0,0 +1 @@ +INSERT INTO persons (name) VALUES ('Pushkin'), ('Lermontov') \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql new file mode 100644 index 00000000..2190e858 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS persons; + +CREATE TABLE persons ( + id BIGSERIAL, + name VARCHAR(250), + + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js new file mode 100644 index 00000000..4a638db3 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js @@ -0,0 +1,58 @@ +import React from 'react' + +const styles = { + personsTable: { + border: "1px solid steelblue", + width: "300px", + borderCollapse: "collapse", + }, + + personsTableItem: { + padding: "5px", + border: "1px solid steelblue" + } +} + +const Header = (props) => ( +

{props.title}

+); + +export default class App extends React.Component { + + constructor() { + super(); + this.state = {persons: []}; + } + + componentDidMount() { + fetch('/api/persons') + .then(response => response.json()) + .then(persons => this.setState({persons})); + } + + render() { + return ( + +
+ + + + + + + + + { + this.state.persons.map((person, i) => ( + + + + + )) + } + +
IDName
{person.id}{person.name}
+ + ) + } +}; diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html new file mode 100644 index 00000000..86fc7182 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html @@ -0,0 +1,15 @@ + + + + Minimal React Boilerplate + + + + +
+ + + + diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js new file mode 100644 index 00000000..b054022e --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js @@ -0,0 +1,8 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './components/App' + +ReactDOM.render( + , + document.getElementById('root') +) \ No newline at end of file diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js b/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js new file mode 100644 index 00000000..a1a380c5 --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js @@ -0,0 +1,51 @@ +const TerserPlugin = require("terser-webpack-plugin"); +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: './src/ui/index.js', + mode: "production", + output: { + path: path.resolve(__dirname, 'target/classes/public/'), + filename: 'bundle.min.js', + libraryTarget: 'umd' + }, + + module: { + rules: [ + { + test: /\.js$/, + exclude: /(node_modules|bower_components|build)/, + use: { + loader: 'babel-loader', + options: { + presets: ["@babel/preset-env", '@babel/preset-react'] + } + } + } + ] + }, + + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + extractComments: true, + }), + ], + }, + + plugins: [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: JSON.stringify("production") + } + }), + + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'src/ui/index.html' + }) + ] +} diff --git a/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js b/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js new file mode 100644 index 00000000..af675edd --- /dev/null +++ b/2026-01/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js @@ -0,0 +1,63 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require('path'); + +module.exports = { + entry: './src/ui/index.js', + devtool: 'inline-source-map', + mode: 'development', + output: { + path: path.resolve(__dirname), + filename: 'bundle.js', + libraryTarget: 'umd' + }, + + devServer: { + static: path.resolve(__dirname) + '/src/ui', + compress: true, + port: 9000, + host: 'localhost', + open: true, +/* + setupMiddlewares: (middlewares, devServer) => { + middlewares.unshift({ + name: 'inital-data-mw', + path: '/api/persons', + middleware: (req, res) => res.send([ + {id: '1', name: 'Привяу'} + ]) + }); + return middlewares; + }, +*/ + proxy: { + '*': { + target: 'http://localhost:8080', + secure: false, + changeOrigin: true + } + } + + + }, + + module: { + rules: [ + { + test: /\.js$/, + exclude: /(node_modules|bower_components|build)/, + use: { + loader: 'babel-loader', + options: { + presets: ["@babel/preset-env", '@babel/preset-react'] + } + } + } + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'src/ui/index.html' + }) + ] +} diff --git a/2026-01/spring-18-reactor/.gitignore b/2026-01/spring-18-reactor/.gitignore new file mode 100644 index 00000000..fbe7a1ed --- /dev/null +++ b/2026-01/spring-18-reactor/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.iml + +target/ + +/node_modules +/output diff --git a/2026-01/spring-18-reactor/.mvn/wrapper/MavenWrapperDownloader.java b/2026-01/spring-18-reactor/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2026-01/spring-18-reactor/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.jar b/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..2cc7d4a5 Binary files /dev/null and b/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.jar differ diff --git a/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.properties b/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2026-01/spring-18-reactor/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2026-01/spring-18-reactor/mvnw b/2026-01/spring-18-reactor/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2026-01/spring-18-reactor/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2026-01/spring-18-reactor/mvnw.cmd b/2026-01/spring-18-reactor/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2026-01/spring-18-reactor/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2026-01/spring-18-reactor/pom.xml b/2026-01/spring-18-reactor/pom.xml new file mode 100644 index 00000000..3a28237f --- /dev/null +++ b/2026-01/spring-18-reactor/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + ru.otus + spring-18-reactor + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.5.3 + + + + 17 + 17 + + + + + + io.projectreactor + reactor-bom + 2024.0.7 + pom + import + + + org.springframework.boot + spring-boot-dependencies + 3.5.3 + pom + import + + + + + + + + io.projectreactor + reactor-core + + + + ch.qos.logback + logback-classic + + + + io.projectreactor + reactor-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + diff --git a/2026-01/spring-18-reactor/src/main/java/ru/otus/CreateExample.java b/2026-01/spring-18-reactor/src/main/java/ru/otus/CreateExample.java new file mode 100644 index 00000000..cdd96103 --- /dev/null +++ b/2026-01/spring-18-reactor/src/main/java/ru/otus/CreateExample.java @@ -0,0 +1,74 @@ +package ru.otus; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +public class CreateExample { + private static final Logger logger = LoggerFactory.getLogger(CreateExample.class); + + public static void main(String[] args) { + onEachNext(); + lazyObservable(); + creatorExample(); + } + + private static void onEachNext() { + Flux obs = Flux.just("one1", "two1", "three1"); + obs.doFirst(() -> logger.info("Starting:")) + .doOnComplete(() -> logger.info("The end!")) + .doOnEach(item -> logger.info("1 item_1:{}", item.get())) + .subscribe(); + + logger.info("-----"); + + obs.doOnNext(item -> logger.info("2 item_2:{}", item)) + .map(String::length) + .doOnNext(item -> logger.info("length_2:{}", item)) + .subscribe(); + } + + private static void lazyObservable() { + Flux obs = Flux.defer(() -> { + logger.info("creating new Observable"); + return Flux.just("one", "two", "three"); + }); + + obs.doOnNext(item -> logger.info("item_1:{}", item)) + .subscribe(); + + logger.info("----------------"); + + obs.doOnNext(item -> logger.info("item_2:{}", item)) + .subscribe(); + } + + private static void creatorExample() { + Flux obs = Flux.create(emitter -> { + emitter.next("one"); + emitter.next("two"); + + emitter.error(new RuntimeException("Error!")); + + emitter.next("three"); + emitter.complete(); + }); + + obs.onErrorResume(e -> { + logger.error("error:{}", e.getMessage(), e); + return Flux.just("r1", "r2", "r3"); + }) + .doOnNext(item -> logger.info("item__1:{}", item)) + .subscribe(); + + logger.info("---------------"); + + Disposable disposable = obs.doOnNext(item -> logger.info("item__2:{}", item)) + .subscribe(next -> logger.info("next:{}", next), + error -> logger.info("error:{}", error.getMessage()), + () -> logger.info("onComplete")); + + logger.info("isDisposed:{}", disposable.isDisposed()); + } +} diff --git a/2026-01/spring-18-reactor/src/main/java/ru/otus/OperatorsExample.java b/2026-01/spring-18-reactor/src/main/java/ru/otus/OperatorsExample.java new file mode 100644 index 00000000..c36f4d4a --- /dev/null +++ b/2026-01/spring-18-reactor/src/main/java/ru/otus/OperatorsExample.java @@ -0,0 +1,51 @@ +package ru.otus; + +import java.time.LocalDate; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + + +public class OperatorsExample { + private static final Logger logger = LoggerFactory.getLogger(OperatorsExample.class); + + public static void main(String[] args) { + merge(); + fromList(); + } + + private static void merge() { + var listFlux1 = Flux.fromIterable(List.of( + new Person("John", "Dow", "male", LocalDate.of(1992, 3, 12)), + new Person("Jane", "Dow", "female", LocalDate.of(2001, 6, 23)))); + + var listFlux2 = Flux.fromIterable(List.of( + new Person("Howard", "Lovecraft", "male", LocalDate.of(1890, 8, 20)), + new Person("Joanne", "Rowling", "female", LocalDate.of(1965, 6, 30)))); + + var listFlux3 = Flux.fromIterable(List.of( + new Person("Ivan", "Petrov", "male", LocalDate.of(1890, 2, 10)), + new Person("Joanne", "Stuard", "female", LocalDate.of(1965, 1, 3)))); + + Flux.merge(listFlux1, listFlux2, listFlux3) + .subscribe(person -> logger.info("person:{}", person)); + + } + + private static void fromList() { + var persons = List.of( + new Person("John", "Dow", "male", LocalDate.of(1992, 3, 12)), + new Person("Jane", "Dow", "female", LocalDate.of(2001, 6, 23)), + new Person("Howard", "Lovecraft", "male", LocalDate.of(1890, 8, 20)), + new Person("Joanne", "Rowling", "female", LocalDate.of(1965, 6, 30))); + + var disposable = Flux.fromIterable(persons) + .filter(person -> person.birth().isAfter(LocalDate.of(1990, 1, 1))) + .map(p -> p.firstName() + " " + p.lastName()) + .collectList() + .subscribe(item -> logger.info("item: {}", item)); + + logger.info("disposable.isDisposed:{}", disposable.isDisposed()); + } +} diff --git a/2026-01/spring-18-reactor/src/main/java/ru/otus/Person.java b/2026-01/spring-18-reactor/src/main/java/ru/otus/Person.java new file mode 100644 index 00000000..ddbe5422 --- /dev/null +++ b/2026-01/spring-18-reactor/src/main/java/ru/otus/Person.java @@ -0,0 +1,6 @@ +package ru.otus; + +import java.time.LocalDate; + +public record Person(String firstName, String lastName, String gender, LocalDate birth) { +} diff --git a/2026-01/spring-18-reactor/src/main/java/ru/otus/PublisherExample.java b/2026-01/spring-18-reactor/src/main/java/ru/otus/PublisherExample.java new file mode 100644 index 00000000..aaab7853 --- /dev/null +++ b/2026-01/spring-18-reactor/src/main/java/ru/otus/PublisherExample.java @@ -0,0 +1,56 @@ +package ru.otus; + +import java.time.Duration; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.ConnectableFlux; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; +import reactor.core.scheduler.Schedulers; + +public class PublisherExample { + private static final Logger logger = LoggerFactory.getLogger(PublisherExample.class); + + public static void main(String[] args) throws Exception { + publisherExample(); + } + + public static void publisherExample() throws InterruptedException { + Flux ob = magicPublisher(); + Thread.sleep(5000); + logger.info("First subscribed"); + var disposable1 = ob.subscribe(item -> logger.info("item: {}", item)); + + logger.info("disposable1.isDisposed:{}", disposable1.isDisposed()); + Thread.sleep(5000); + + logger.info("Second subscribed"); + var disposable2 = ob.subscribe(item -> logger.info("item second: {}", item)); + + + logger.info("disposable2.isDisposed():{}", disposable2.isDisposed()); + + Thread.sleep(60_000); + } + + public static Flux magicPublisher() { + var schedulerGenerator = Schedulers.newParallel("generator", 1); + var generator = Flux.generate( + () -> 0L, + (BiFunction, Long>) + (prev, sink) -> { + var newValue = prev + 1; + sink.next(newValue); + logger.info("newValue:{}", newValue); + return newValue; + }) + .delayElements(Duration.ofSeconds(5), schedulerGenerator) + .map(id -> "new id:" + id) + .doOnNext(val -> logger.info("val:{}", val)); + + ConnectableFlux generatorConnectable = generator.publish(); + + return generatorConnectable.autoConnect(0); + } +} diff --git a/2026-01/spring-19-webflux/.gitignore b/2026-01/spring-19-webflux/.gitignore new file mode 100644 index 00000000..e62c33c2 --- /dev/null +++ b/2026-01/spring-19-webflux/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml + +target/ diff --git a/2026-01/spring-19-webflux/.mvn/wrapper/MavenWrapperDownloader.java b/2026-01/spring-19-webflux/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..e76d1f32 --- /dev/null +++ b/2026-01/spring-19-webflux/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.jar b/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..2cc7d4a5 Binary files /dev/null and b/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.jar differ diff --git a/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.properties b/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/2026-01/spring-19-webflux/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/2026-01/spring-19-webflux/HttpRequests.http b/2026-01/spring-19-webflux/HttpRequests.http new file mode 100644 index 00000000..1da581ec --- /dev/null +++ b/2026-01/spring-19-webflux/HttpRequests.http @@ -0,0 +1,41 @@ +### +GET http://localhost:8080/flux/one +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/flux/ten +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/stream +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/person +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/func/person?name=Lermontov +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/func/person?age=22 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache + +### +GET http://localhost:8080/func/person/1 +Accept: */* +Content-Type: application/json +Cache-Control: no-cache diff --git a/2026-01/spring-19-webflux/mvnw b/2026-01/spring-19-webflux/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/2026-01/spring-19-webflux/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/2026-01/spring-19-webflux/mvnw.cmd b/2026-01/spring-19-webflux/mvnw.cmd new file mode 100644 index 00000000..c8d43372 --- /dev/null +++ b/2026-01/spring-19-webflux/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/2026-01/spring-19-webflux/pom.xml b/2026-01/spring-19-webflux/pom.xml new file mode 100644 index 00000000..06f4c951 --- /dev/null +++ b/2026-01/spring-19-webflux/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + ru.otus + spring-19-webflux + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.2 + + + + 17 + 17 + 24.0.1 + + + + + + io.projectreactor + reactor-bom + 2024.0.7 + pom + import + + + org.springframework.boot + spring-boot-dependencies + 3.5.3 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + + org.jetbrains + annotations + ${annotations.version} + + + + io.r2dbc + r2dbc-h2 + runtime + + + + + io.projectreactor + reactor-test + test + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/DataFiller.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/DataFiller.java new file mode 100644 index 00000000..6fd45d59 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/DataFiller.java @@ -0,0 +1,52 @@ +package ru.otus.spring; + +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import reactor.core.scheduler.Scheduler; +import ru.otus.spring.domain.Notes; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repository.NotesRepository; +import ru.otus.spring.repository.PersonRepository; +import ru.otus.spring.repository.PersonRepositoryCustom; + +@Component +public class DataFiller implements ApplicationRunner { + private static final Logger logger = LoggerFactory.getLogger(DataFiller.class); + + private final PersonRepository personRepository; + private final NotesRepository notesRepository; + private final PersonRepositoryCustom personRepositoryCustom; + private final Scheduler workerPool; + + public DataFiller(PersonRepository personRepository, NotesRepository notesRepository, PersonRepositoryCustom personRepositoryCustom, Scheduler workerPool) { + this.personRepository = personRepository; + this.notesRepository = notesRepository; + this.workerPool = workerPool; + this.personRepositoryCustom = personRepositoryCustom; + } + + @Override + public void run(ApplicationArguments args) { + personRepository.saveAll(Arrays.asList( + new Person("Pushkin", 22), + new Person("Lermontov", 22), + new Person("Tolstoy", 60) + )).publishOn(workerPool) + .subscribe(savedPerson -> { + logger.info("saved person:{}", savedPerson); + notesRepository.saveAll(Arrays.asList( + new Notes(null, "txt_1_" + savedPerson.getId(), savedPerson.getId()), + new Notes(null, "txt_2_" + savedPerson.getId(), savedPerson.getId()))) + .publishOn(workerPool) + .subscribe(savedNotes -> logger.info("saved notes:{}", savedNotes)); + }); + + personRepositoryCustom.findAll() + .publishOn(workerPool) + .subscribe(personDto -> logger.info("personDto:{}", personDto)); + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java new file mode 100644 index 00000000..a9d317da --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java @@ -0,0 +1,67 @@ +package ru.otus.spring; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import ru.otus.spring.domain.Person; +import ru.otus.spring.repository.PersonRepository; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.BodyInserters.fromValue; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; +import static org.springframework.web.reactive.function.server.ServerResponse.badRequest; +import static org.springframework.web.reactive.function.server.ServerResponse.notFound; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +@Configuration +public class FunctionalEndpointsConfig { + @Bean + public RouterFunction composedRoutes(PersonRepository repository) { + return route() + // эта функция должна стоять раньше findAll - порядок следования роутов - важен + .GET("/func/person", + queryParam("name", param -> param != null && !param.isEmpty()), + request -> request.queryParam("name") + .map(name -> ok().body(repository.findAllByLastName(name), Person.class)) + .orElse(badRequest().build()) + ) + // пример другой реализации - начиная с запроса репозитория + .GET("/func/person", queryParam("age", param -> param != null && !param.isEmpty()), + request -> + ok() + .contentType(MediaType.APPLICATION_JSON) + .body(repository.findAllByAge(request.queryParam("age") + .map(Integer::parseInt) + .orElseThrow(IllegalArgumentException::new)), Person.class) + ) + // Обратите внимание на использование хэндлера + .GET("/func/person", accept(APPLICATION_JSON), new PersonHandler(repository)::list) + // Обратите внимание на использование pathVariable + .GET("/func/person/{id}", accept(APPLICATION_JSON), + request -> repository.findById(Long.parseLong(request.pathVariable("id"))) + .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person))) + .switchIfEmpty(notFound().build()) + ).build(); + } + + // Это пример хэндлера, который даже не бин + static class PersonHandler { + + private final PersonRepository repository; + + PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + Mono list(ServerRequest request) { + // Обратите внимание на пример другого порядка создания response от Flux + return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class); + } + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java new file mode 100644 index 00000000..2eb244c2 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java @@ -0,0 +1,16 @@ +package ru.otus.spring; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebfluxDemo { + + + public static void main(String[] args) { + SpringApplication.run(WebfluxDemo.class); + // org.h2.tools.Console.Console.main(args); + } +} + diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java new file mode 100644 index 00000000..a6d841f9 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java @@ -0,0 +1,41 @@ +package ru.otus.spring.config; + +import io.netty.channel.nio.NioEventLoopGroup; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.NonNull; + +@Configuration +public class ApplConfig { + private static final int THREAD_POOL_SIZE = 2; + + @Bean(destroyMethod = "close") + public NioEventLoopGroup eventLoopGroup() { + return new NioEventLoopGroup(THREAD_POOL_SIZE, + new ThreadFactory() { + private final AtomicLong threadIdGenerator = new AtomicLong(0); + @Override + public Thread newThread(@NonNull Runnable task) { + return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet()); + } + }); + } + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory(NioEventLoopGroup eventLoopGroup) { + var factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(builder -> builder.runOn(eventLoopGroup)); + return factory; + } + + @Bean + public Scheduler workerPool() { + return Schedulers.newParallel("worker-thread", THREAD_POOL_SIZE); + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Notes.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Notes.java new file mode 100644 index 00000000..037cefa4 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Notes.java @@ -0,0 +1,49 @@ +package ru.otus.spring.domain; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.relational.core.mapping.Table; + +@Table("notes") +public class Notes { + + @Id + private final Long id; + + @NotNull + private final String noteText; + + @NotNull + private final Long personId; + + @PersistenceCreator + public Notes(Long id, @NotNull String noteText, @NotNull Long personId) { + this.id = id; + this.noteText = noteText; + this.personId = personId; + } + + public Long getId() { + return id; + } + + @NotNull + public String getNoteText() { + return noteText; + } + + @NotNull + public Long getPersonId() { + return personId; + } + + @Override + public String toString() { + return "Notes{" + + "id=" + id + + ", noteText='" + noteText + '\'' + + ", personId=" + personId + + '}'; + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Person.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Person.java new file mode 100644 index 00000000..9721a730 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/Person.java @@ -0,0 +1,53 @@ +package ru.otus.spring.domain; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.relational.core.mapping.Table; + +@Table("person") +public class Person { + + @Id + private final Long id; + + @NotNull + private final String lastName; + + private final int age; + + + @PersistenceCreator + private Person(Long id, @NotNull String lastName, int age) { + this.id = id; + this.lastName = lastName; + this.age = age; + } + + public Person(String lastName, int age) { + this(null, lastName, age); + } + + public Long getId() { + return id; + } + + + public @NotNull String getLastName() { + return lastName; + } + + + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", lastName='" + lastName + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java new file mode 100644 index 00000000..9e782abb --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java @@ -0,0 +1,8 @@ +package ru.otus.spring.domain; + + +import java.util.List; + +public record PersonDto(Long id, String name, Integer age, List notes) { + +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/NotesRepository.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/NotesRepository.java new file mode 100644 index 00000000..38145043 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/NotesRepository.java @@ -0,0 +1,12 @@ +package ru.otus.spring.repository; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Flux; +import ru.otus.spring.domain.Notes; + + +public interface NotesRepository extends ReactiveCrudRepository { + + Flux findByPersonId(@NotNull Long personId); +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java new file mode 100644 index 00000000..d420f092 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java @@ -0,0 +1,18 @@ +package ru.otus.spring.repository; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import ru.otus.spring.domain.Person; + +public interface PersonRepository extends ReactiveCrudRepository { + + @NotNull Mono findById(@NotNull Long id); + + Mono save(Mono person); + + Flux findAllByLastName(String lastName); + + Flux findAllByAge(int age); +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepositoryCustom.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepositoryCustom.java new file mode 100644 index 00000000..859d5cae --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/repository/PersonRepositoryCustom.java @@ -0,0 +1,52 @@ +package ru.otus.spring.repository; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.r2dbc.spi.Readable; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import ru.otus.spring.domain.PersonDto; + +import java.util.List; + + +@Repository +public class PersonRepositoryCustom { + + private final R2dbcEntityTemplate template; + private final ObjectMapper objectMapper; + + private static final String SQL_ALL = """ + select json_array(select note_text from notes n where n.person_id = p.id) as notes, p.id, + p.last_name, p.age + from person p + """; + + public PersonRepositoryCustom(R2dbcEntityTemplate template, ObjectMapper objectMapper) { + this.template = template; + this.objectMapper = objectMapper; + } + + public Flux findAll() { + return template.getDatabaseClient().inConnectionMany(connection -> + Flux.from(connection.createStatement(SQL_ALL) + .execute()) + .flatMap(result -> result.map(this::mapper))); + } + + private PersonDto mapper(Readable selectedRecord) { + var notesAsText = selectedRecord.get("notes", String.class); + try { + List notes = objectMapper.readValue(notesAsText, new TypeReference<>() { + }); + return new PersonDto(selectedRecord.get("id", Long.class), + selectedRecord.get("last_name", String.class), + selectedRecord.get("age", Integer.class), + notes); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("notes:" + notesAsText + " parsing error:" + e); + } + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java new file mode 100644 index 00000000..b37dcd86 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java @@ -0,0 +1,49 @@ +package ru.otus.spring.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import reactor.core.scheduler.Scheduler; + + +@RestController +public class AnnotatedController { + private static final Logger logger = LoggerFactory.getLogger(AnnotatedController.class); + + private final Scheduler workerPool; + + public AnnotatedController(Scheduler workerPool) { + this.workerPool = workerPool; + } + + @GetMapping("/flux/one") + public Mono one() { + return Mono.just("one"); + } + + @GetMapping(path ="/flux/ten", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux list() { + logger.info("list request"); + return Flux.range(1, 10) + .delayElements(Duration.ofSeconds(1), workerPool) + .doOnNext(val -> logger.info("value:{}", val)); + } + + @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux stream() { + logger.info("stream"); + return Flux.generate(() -> 0, (state, emitter) -> { + emitter.next(state); + return state + 1; + }) + .delayElements(Duration.ofSeconds(1L)) + .map(Object::toString) + .map(val -> String.format("valStr:%s", val)); + } +} diff --git a/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/PersonController.java b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/PersonController.java new file mode 100644 index 00000000..3de17a9a --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/java/ru/otus/spring/rest/PersonController.java @@ -0,0 +1,57 @@ +package ru.otus.spring.rest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import ru.otus.spring.domain.Notes; +import ru.otus.spring.domain.Person; +import ru.otus.spring.domain.PersonDto; +import ru.otus.spring.repository.NotesRepository; +import ru.otus.spring.repository.PersonRepository; +import ru.otus.spring.repository.PersonRepositoryCustom; + +import java.util.List; + +@RestController +public class PersonController { + + private final PersonRepository personRepository; + private final NotesRepository notesRepository; + + private final PersonRepositoryCustom personRepositoryCustom; + + public PersonController(PersonRepository personRepository, NotesRepository notesRepository, PersonRepositoryCustom personRepositoryCustom) { + this.personRepository = personRepository; + this.notesRepository = notesRepository; + this.personRepositoryCustom = personRepositoryCustom; + } + + @GetMapping("/person") + public Flux all() { + return personRepositoryCustom.findAll(); + } + + @GetMapping("/person/{id}") + public Mono> byId(@PathVariable("id") Long id) { + return personRepository.findById(id) + .flatMap(person -> notesRepository.findByPersonId(person.getId()).map(Notes::getNoteText).collectList() + .map(notes -> toDto(person, notes))) + .map(ResponseEntity::ok) + .switchIfEmpty(Mono.fromCallable(() -> ResponseEntity.notFound().build())); + } + + @PostMapping("/person") + public Mono save(@RequestBody Mono dto) { + return personRepository.save(dto); + } + + @GetMapping("/person/find") + public Flux byName(@RequestParam("name") String name) { + return personRepository.findAllByLastName(name); + } + + private PersonDto toDto(Person person, List notes) { + return new PersonDto(person.getId(), person.getLastName(), person.getAge(), notes); + } +} diff --git a/2026-01/spring-19-webflux/src/main/resources/application.yml b/2026-01/spring-19-webflux/src/main/resources/application.yml new file mode 100644 index 00000000..c550c54a --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/resources/application.yml @@ -0,0 +1,17 @@ +server: + port: 8080 + +spring: + r2dbc: + url: r2dbc:h2:mem:///testdb + username: sa + password: password + pool: + enabled: true + sql: + init: + mode: always + +logging: + level: + org.springframework.jdbc.core.JdbcTemplate: TRACE diff --git a/2026-01/spring-19-webflux/src/main/resources/logback.xml b/2026-01/spring-19-webflux/src/main/resources/logback.xml new file mode 100644 index 00000000..b1f9bfe2 --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/2026-01/spring-19-webflux/src/main/resources/schema.sql b/2026-01/spring-19-webflux/src/main/resources/schema.sql new file mode 100644 index 00000000..b8827aeb --- /dev/null +++ b/2026-01/spring-19-webflux/src/main/resources/schema.sql @@ -0,0 +1,14 @@ +create table if not exists person +( + id bigint primary key auto_increment, + last_name varchar(50) not null, + age int not null +); + +create table if not exists notes +( + id bigint primary key auto_increment, + note_text varchar(250) not null, + person_id bigint not null references person (id) +); +create index if not exists idx_notes_person_id on notes (person_id); \ No newline at end of file diff --git a/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java new file mode 100644 index 00000000..67bc6c01 --- /dev/null +++ b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java @@ -0,0 +1,28 @@ +package ru.otus.spring.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import ru.otus.spring.domain.Person; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +class PersonRepositoryTest { + + @Autowired + private PersonRepository repository; + + @Test + void shouldSetIdOnSave() { + Mono personMono = repository.save(new Person("Bill", 12)); + + StepVerifier + .create(personMono) + .assertNext(person -> assertNotNull(person.getId())) + .expectComplete() + .verify(); + } +} diff --git a/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java new file mode 100644 index 00000000..ea416812 --- /dev/null +++ b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java @@ -0,0 +1,95 @@ +package ru.otus.spring.rest; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class AnnotatedControllerTest { + + @Autowired + private WebTestClient webTestClient; + @LocalServerPort + private int port; + + + @Test + void oneTest() { + //given + var client = WebClient.create(String.format("http://localhost:%d", port)); + + //when + var result = client + .get().uri("/flux/one") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(3)) + .block(); + + //then + assertThat(result).isEqualTo("one"); + } + + @Test + void streamTest() { + //given + var client = WebClient.create(String.format("http://localhost:%d", port)); + var expectedSize = 5; + + //when + List result = client + .get().uri("/stream") + .accept(MediaType.TEXT_EVENT_STREAM) + .retrieve() + .bodyToFlux(String.class) + .take(expectedSize) + .timeout(Duration.ofSeconds(3)) + .collectList() + .block(); + + //then + assertThat(result).hasSize(expectedSize) + .contains(String.format("valStr:%s", 0), + String.format("valStr:%s", 1), + String.format("valStr:%s", 2), + String.format("valStr:%s", 3), + String.format("valStr:%s", 4)); + } + + @Test + void dataTest() { + //given + var webTestClientForTest = webTestClient.mutate() + .responseTimeout(Duration.ofSeconds(20)) + .build(); + + //when + var result = webTestClientForTest + .get().uri("/flux/ten") + .accept(MediaType.TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .returnResult(Integer.class) + .getResponseBody(); + + //then + var step = StepVerifier.create(result); + StepVerifier.Step stepResult = null; + for (var idx = 1; idx <= 10; idx++) { + stepResult = step.expectNext(idx); + } + stepResult.verifyComplete(); + } + +} \ No newline at end of file diff --git a/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java new file mode 100644 index 00000000..986eb0b0 --- /dev/null +++ b/2026-01/spring-19-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java @@ -0,0 +1,29 @@ +package ru.otus.spring.rest; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + + +@SpringBootTest +class PersonControllerTest { + + @Autowired + private RouterFunction route; + + @Test + void testRoute() { + WebTestClient client = WebTestClient + .bindToRouterFunction(route) + .build(); + + client.get() + .uri("/func/person") + .exchange() + .expectStatus() + .isOk(); + } +} diff --git a/2026-01/spring-20-SS-start/2.7-style/pom.xml b/2026-01/spring-20-SS-start/2.7-style/pom.xml new file mode 100644 index 00000000..25baa882 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + ru.otus + spring-framework-23-ss-start + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.7.8 + + + + 17 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..95fa70ed --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,12 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main( String[] args ) { + SpringApplication.run( Main.class ); + } +} diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/rest/PagesController.java b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/rest/PagesController.java new file mode 100644 index 00000000..47321812 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/rest/PagesController.java @@ -0,0 +1,28 @@ +package ru.otus.spring.rest; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PagesController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage() { + return "public"; + } + + @GetMapping("/authenticated") + public String authenticatedPage() { + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } +} diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..a708cc22 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,49 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf().disable() + .authorizeHttpRequests( ( authorize ) -> authorize + .antMatchers( "/public" ).permitAll() + .antMatchers( "/authenticated" ).authenticated() + .anyRequest().permitAll() + ) + .httpBasic(); + return http.build(); + } + + + @Bean + public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(10); + return NoOpPasswordEncoder.getInstance(); + + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User + .builder() + .username( "user" ) + .password( "password" ) + .roles( "USER" ) + .build(); + return new InMemoryUserDetailsManager( user ); + } +} diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/resources/application.yml b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/authenticated.html b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..9f8b0d7e --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для авторизованных + + diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/index.html b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/index.html new file mode 100644 index 00000000..f4d11090 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/index.html @@ -0,0 +1,11 @@ + + + + + + +/public +
+/authenticated + + diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/public.html b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/success.html b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/success.html new file mode 100644 index 00000000..89db5f22 --- /dev/null +++ b/2026-01/spring-20-SS-start/2.7-style/src/main/resources/templates/success.html @@ -0,0 +1,10 @@ + + + + + Вы успешно вошли + + +Вы успешно вошли + + diff --git a/2026-01/spring-20-SS-start/3.x-style/pom.xml b/2026-01/spring-20-SS-start/3.x-style/pom.xml new file mode 100644 index 00000000..376b0077 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + ru.otus + spring-framework-23-ss-start + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + 17 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..3a3edb94 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main( String[] args ) { + SpringApplication.run( Main.class ); + // http://localhost:8080/ + } +} diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/rest/PagesController.java b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/rest/PagesController.java new file mode 100644 index 00000000..47321812 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/rest/PagesController.java @@ -0,0 +1,28 @@ +package ru.otus.spring.rest; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PagesController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage() { + return "public"; + } + + @GetMapping("/authenticated") + public String authenticatedPage() { + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } +} diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..f5150102 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,51 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( ( authorize ) -> authorize + .requestMatchers( "/public" ).permitAll() + .requestMatchers( "/authenticated" ).authenticated() + .anyRequest().permitAll() + ) + .httpBasic(Customizer.withDefaults()); + return http.build(); + } + + + @Bean + public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(10); + return NoOpPasswordEncoder.getInstance(); + + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User + .builder() + .username("user") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } +} diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/resources/application.yml b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/authenticated.html b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..9f8b0d7e --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для авторизованных + + diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/index.html b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/index.html new file mode 100644 index 00000000..f4d11090 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/index.html @@ -0,0 +1,11 @@ + + + + + + +/public +
+/authenticated + + diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/public.html b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/success.html b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/success.html new file mode 100644 index 00000000..89db5f22 --- /dev/null +++ b/2026-01/spring-20-SS-start/3.x-style/src/main/resources/templates/success.html @@ -0,0 +1,10 @@ + + + + + Вы успешно вошли + + +Вы успешно вошли + + diff --git a/2026-01/spring-20-SS-start/old-style/pom.xml b/2026-01/spring-20-SS-start/old-style/pom.xml new file mode 100644 index 00000000..d0b19bbc --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + ru.otus + spring-framework-23-spring-security-3x-start + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.7.8 + + + + 17 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..95fa70ed --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,12 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main( String[] args ) { + SpringApplication.run( Main.class ); + } +} diff --git a/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/rest/PagesController.java b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/rest/PagesController.java new file mode 100644 index 00000000..47321812 --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/rest/PagesController.java @@ -0,0 +1,28 @@ +package ru.otus.spring.rest; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PagesController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage() { + return "public"; + } + + @GetMapping("/authenticated") + public String authenticatedPage() { + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } +} diff --git a/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..89ef82be --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,70 @@ +package ru.otus.spring.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + public void configure( WebSecurity web ) { + web.ignoring() + .antMatchers( "/" ) + .antMatchers( "/static/**" ); // You are asking Spring Security to ignore Ant [pattern='/static/**']. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead. + } + + @Override + public void configure( HttpSecurity http ) throws Exception { + http.csrf().disable() + // По умолчанию SecurityContext хранится в сессии. Эта часть вырубает и каждый запросом приходитТ +// .sessionManagement() +// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) +// .and() + .authorizeRequests() + .antMatchers( "/public/" ).anonymous() + .and() + .authorizeRequests() + .antMatchers( "/authenticated" ).authenticated() +// .and() +// .authorizeRequests().antMatchers("/public").authenticated() + .and() + .formLogin() + .and() + .anonymous() + .principal( "anonymous" ) + .and() + .rememberMe().key( "Some secret" ) + ; + } + + @Bean + public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(10); + return NoOpPasswordEncoder.getInstance(); +// return new PasswordEncoder() { +// @Override +// public String encode(CharSequence charSequence) { +// return charSequence.toString(); +// } +// +// @Override +// public boolean matches(CharSequence charSequence, String s) { +// return charSequence.toString().equals(s); +// } +// }; + } + + @Autowired + public void configure( AuthenticationManagerBuilder auth ) throws Exception { + auth.inMemoryAuthentication() + .withUser( "admin" ).password( "password" ).roles( "ADMIN" ) + ; + } +} diff --git a/2026-01/spring-20-SS-start/old-style/src/main/resources/application.yml b/2026-01/spring-20-SS-start/old-style/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b diff --git a/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/authenticated.html b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..9f8b0d7e --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для авторизованных + + diff --git a/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/index.html b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/index.html new file mode 100644 index 00000000..f4d11090 --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/index.html @@ -0,0 +1,11 @@ + + + + + + +/public +
+/authenticated + + diff --git a/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/public.html b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/success.html b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/success.html new file mode 100644 index 00000000..89db5f22 --- /dev/null +++ b/2026-01/spring-20-SS-start/old-style/src/main/resources/templates/success.html @@ -0,0 +1,10 @@ + + + + + Вы успешно вошли + + +Вы успешно вошли + + diff --git a/2026-01/spring-21-SS-auth/pom.xml b/2026-01/spring-21-SS-auth/pom.xml new file mode 100644 index 00000000..5d73fcd5 --- /dev/null +++ b/2026-01/spring-21-SS-auth/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + ru.otus + classwork + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + UTF-8 + UTF-8 + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-test + + + + org.springframework.security + spring-security-test + ${spring-security.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..e8fdc91d --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + // http://localhost:8080/ + } +} diff --git a/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/controller/PagesController.java b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/controller/PagesController.java new file mode 100644 index 00000000..e7530392 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/controller/PagesController.java @@ -0,0 +1,54 @@ +package ru.otus.spring.controller; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class PagesController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage(@RequestParam(name = "SpecialValue") String specialValue, Model model) { + model.addAttribute("secret", specialValue); + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + System.out.println(authentication.getPrincipal()); + return "public"; + } + + @GetMapping("/authenticated") + public String authenticatedPage(Model model) { + SecurityContext securityContext = SecurityContextHolder.getContext(); + User user = (User) securityContext.getAuthentication().getPrincipal(); + model.addAttribute("userName", user.getUsername()); + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } + + @GetMapping("/error") + public String errorPage(Model model) { + model.addAttribute("source", "errorPage"); + return "error"; + } + + @PostMapping("/fail") + public String failPage(Model model) { + model.addAttribute("source", "failPage"); + return "error"; + } +} diff --git a/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/AnonimusUD.java b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/AnonimusUD.java new file mode 100644 index 00000000..bf2a2210 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/AnonimusUD.java @@ -0,0 +1,45 @@ +package ru.otus.spring.security; + +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@Data +public class AnonimusUD implements UserDetails { + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return "anonymous"; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..84478483 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,66 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import ru.otus.spring.security.filter.MyOwnFilter; + +import java.util.ArrayList; + +@EnableWebSecurity +@Configuration +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) + throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/").permitAll() + .requestMatchers("/public").permitAll() + .requestMatchers("/authenticated", "/success").authenticated() + .anyRequest().authenticated() + ) +// .anonymous(a -> a.principal(new AnonimusUD()).authorities("ROLE_ANONYMOUS")) + .addFilterAfter(new MyOwnFilter(), AuthorizationFilter.class) +// .httpBasic(Customizer.withDefaults()) +// .formLogin(Customizer.withDefaults()) + .formLogin(fm ->fm.failureForwardUrl("/fail")) +/* .rememberMe(rm -> rm.key("AnyKey") + .tokenValiditySeconds(600))*/ + ; + return http.build(); + } + + @SuppressWarnings("deprecation") + @Bean + public PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User + .builder() + .username("user") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + + } +} diff --git a/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java new file mode 100644 index 00000000..2b81609f --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java @@ -0,0 +1,30 @@ +package ru.otus.spring.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +public class MyOwnFilter extends GenericFilterBean { + @Override + public void doFilter(ServletRequest servletRequest, + ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + var requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) servletRequest) { + @Override + public String[] getParameterValues(String name) { + if ("SpecialValue".equals(name)) { + return new String[]{"My dirty secret"}; + } + return super.getParameterValues(name); + } + }; + + filterChain.doFilter(requestWrapper, servletResponse); + } +} diff --git a/2026-01/spring-21-SS-auth/src/main/resources/application.yml b/2026-01/spring-21-SS-auth/src/main/resources/application.yml new file mode 100644 index 00000000..d07e4971 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/application.yml @@ -0,0 +1,4 @@ +logging: + level: + root: error + org.springframework: info \ No newline at end of file diff --git a/2026-01/spring-21-SS-auth/src/main/resources/templates/authenticated.html b/2026-01/spring-21-SS-auth/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..62f1eb0d --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/templates/authenticated.html @@ -0,0 +1,10 @@ + + + + + Только для авторизованных + + +Только для авторизованных. Вы как раз такой) + + diff --git a/2026-01/spring-21-SS-auth/src/main/resources/templates/error.html b/2026-01/spring-21-SS-auth/src/main/resources/templates/error.html new file mode 100644 index 00000000..41a3f8e2 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/templates/error.html @@ -0,0 +1,11 @@ + + + + + Упс... + + +Что-то пошло не так. Печалька
+Источник: Неизвестен + + diff --git a/2026-01/spring-21-SS-auth/src/main/resources/templates/index.html b/2026-01/spring-21-SS-auth/src/main/resources/templates/index.html new file mode 100644 index 00000000..f2d1d1ae --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/templates/index.html @@ -0,0 +1,12 @@ + + + + + Главная страница + + +/public +
+/authenticated + + diff --git a/2026-01/spring-21-SS-auth/src/main/resources/templates/public.html b/2026-01/spring-21-SS-auth/src/main/resources/templates/public.html new file mode 100644 index 00000000..58fca813 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/templates/public.html @@ -0,0 +1,10 @@ + + + + + Доступен всем + + +Доступен всем, но есть секрет: Нет секрета + + diff --git a/2026-01/spring-21-SS-auth/src/main/resources/templates/success.html b/2026-01/spring-21-SS-auth/src/main/resources/templates/success.html new file mode 100644 index 00000000..58414c01 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/main/resources/templates/success.html @@ -0,0 +1,10 @@ + + + + + Вы успешно вошли ! + + +Вы успешно вошли ! + + diff --git a/2026-01/spring-21-SS-auth/src/test/java/ru/otus/spring/controller/PagesControllerTest.java b/2026-01/spring-21-SS-auth/src/test/java/ru/otus/spring/controller/PagesControllerTest.java new file mode 100644 index 00000000..847927b3 --- /dev/null +++ b/2026-01/spring-21-SS-auth/src/test/java/ru/otus/spring/controller/PagesControllerTest.java @@ -0,0 +1,33 @@ +package ru.otus.spring.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import ru.otus.spring.security.SecurityConfiguration; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(PagesController.class) +@Import(SecurityConfiguration.class) +public class PagesControllerTest { + + @Autowired + private MockMvc mockMvc; + +/* @WithMockUser( + username = "admin", + authorities = {"ROLE_ADMIN"} + )*/ + @Test + public void testAuthenticatedOnAdmin() throws Exception { + mockMvc.perform(get("/authenticated") + .with(user("admin").authorities(new SimpleGrantedAuthority("ROLE_ADMIN")))) + .andExpect(status().isOk()); + } +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/pom.xml b/2026-01/spring-22-SS-auth/WebFlux/pom.xml new file mode 100644 index 00000000..3eda6d55 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + ru.otus + spring-framework-27-webflux + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-web + + + + org.springframework.boot + spring-boot-starter-data-mongodb-reactive + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 4.6.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/WebFluxStarter.java b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/WebFluxStarter.java new file mode 100644 index 00000000..7a06898e --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/WebFluxStarter.java @@ -0,0 +1,22 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +@SpringBootApplication +public class WebFluxStarter { + + public static void main( String[] args ) { + SpringApplication.run( WebFluxStarter.class ); + } + + @Bean + RouterFunction staticResourceRouter() { + return RouterFunctions.resources( "/**.html", new ClassPathResource( "static/" ) ); + } +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/Person.java b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/Person.java new file mode 100644 index 00000000..bedf08b0 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/Person.java @@ -0,0 +1,41 @@ +package ru.otus.spring.data; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +public class Person { + + @Id + private String id; + + private String name; + + public Person() { + } + + public Person( String name ) { + this.name = name; + } + + public Person( String id, String name ) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId( String id ) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/PersonRepository.java b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/PersonRepository.java new file mode 100644 index 00000000..8f5f3d67 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/data/PersonRepository.java @@ -0,0 +1,11 @@ +package ru.otus.spring.data; + +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +@Repository +public interface PersonRepository extends ReactiveMongoRepository { + + Mono findByName( String string ); +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/rest/PersonController.java b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/rest/PersonController.java new file mode 100644 index 00000000..7af4b0a9 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/rest/PersonController.java @@ -0,0 +1,34 @@ +package ru.otus.spring.rest; + +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import ru.otus.spring.data.Person; +import ru.otus.spring.data.PersonRepository; + +@RestController +public class PersonController { + + private final PersonRepository personRepository; + + public PersonController( PersonRepository personRepository ) { + this.personRepository = personRepository; + } + + @GetMapping("/person") + public Flux getAll() { + return personRepository.findAll(); + } + + @GetMapping("/person/find") + public Mono find( @RequestParam("name") String name ) { + return personRepository.findByName( name ) + .cache(); + } + + + @PostMapping("/person") + public Mono savePerson( @RequestBody Person person ) { + return personRepository.save( person ); + } +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..5dfc9dc7 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,49 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@EnableWebFluxSecurity +@Configuration +public class SecurityConfiguration { + + @Bean + public SecurityWebFilterChain springWebFilterChain( ServerHttpSecurity http ) { + return http + .authorizeExchange((exchanges)->exchanges + .pathMatchers( HttpMethod.GET, "/authenticated.html" ).authenticated() + .pathMatchers( "/person" ).hasAnyRole( "USER" ) + .anyExchange().permitAll() + ) + .formLogin( Customizer.withDefaults()) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + + @Bean + public ReactiveUserDetailsService userDetailsService() { + UserDetails user = User + .withUsername( "user" ) + .password( "password" ) + .roles( "USER" ) + .build(); + return new MapReactiveUserDetailsService( user ); + } +} diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/application.yml b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/application.yml new file mode 100644 index 00000000..add8d9b3 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/application.yml @@ -0,0 +1 @@ +spring.mongodb.embedded.version: 4.6.1 \ No newline at end of file diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/authenticated.html b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/authenticated.html new file mode 100644 index 00000000..9f8b0d7e --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для авторизованных + + diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/index.html b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/index.html new file mode 100644 index 00000000..a89ba331 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/index.html @@ -0,0 +1,11 @@ + + + + + + +/public.html +
+/authenticated.html + + diff --git a/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/public.html b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-22-SS-auth/WebFlux/src/main/resources/static/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-22-SS-auth/classwork/pom.xml b/2026-01/spring-22-SS-auth/classwork/pom.xml new file mode 100644 index 00000000..747bcc45 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + ru.otus + classwork + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + + UTF-8 + UTF-8 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java new file mode 100644 index 00000000..6b3bf909 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringSecurityAuthorization { + + public static void main(String[] args) { + SpringApplication.run(SpringSecurityAuthorization.class); + } + +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/PagesController.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/PagesController.java new file mode 100644 index 00000000..80a1674b --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/PagesController.java @@ -0,0 +1,60 @@ +package ru.otus.spring.controller; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import ru.otus.spring.service.MyService; + +@Controller +public class PagesController { + + private final MyService myService; + + public PagesController(MyService myService) { + this.myService = myService; + } + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage() { + return "public"; + } + + @GetMapping("/user") + public String userPage() { + myService.onlyUser(); + return "user"; + } + + @GetMapping("/manager") + public String managerPage() { + return "manager"; + } + + @GetMapping("/admin") + @Secured("ROLE_ADMIN") + public String adminPage() { + //myService.onlyUser(); + myService.onlyAdmin(); + return "admin"; + } + + @GetMapping("/authenticated") + public String authenticatedPage() { + UserDetails userDetails = (UserDetails) SecurityContextHolder + .getContext().getAuthentication().getPrincipal(); + System.out.println(userDetails.getUsername()); + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/SecurityControllerAdvice.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/SecurityControllerAdvice.java new file mode 100644 index 00000000..bdbb5ea3 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/controller/SecurityControllerAdvice.java @@ -0,0 +1,16 @@ +package ru.otus.spring.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.Optional; + +@ControllerAdvice +public class SecurityControllerAdvice { + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity accessError() { + return ResponseEntity.of(Optional.of("Неудачник")); + } +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java new file mode 100644 index 00000000..286fb5fe --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java @@ -0,0 +1,12 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +@EnableGlobalMethodSecurity( + securedEnabled = true, + prePostEnabled = true +) +@Configuration +public class MethodSecurityConfiguration { +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..084ae162 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,57 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.ArrayList; + +@EnableWebSecurity +@Configuration +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf( AbstractHttpConfigurer::disable ) + .authorizeHttpRequests( ( authorize ) -> authorize + .requestMatchers( "/public", "/" ).permitAll() + .requestMatchers( "/authenticated", "/success", "/admin" ).authenticated() + .requestMatchers( "/manager/**" ).hasAnyRole( "MANAGER", "ADMIN" ) + .requestMatchers( "/user" ).hasAnyRole( "ADMIN", "USER" ) + .anyRequest().permitAll() + ) + .formLogin( Customizer.withDefaults() ) + + ; + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + var users = new ArrayList(); + users.add( User + .withUsername("admin").password("password").roles("ADMIN") + .build() ); + users.add( User + .withUsername("user").password("password").roles("USER") + .build() ); + return new InMemoryUserDetailsManager(users); + + } +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/service/MyService.java b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/service/MyService.java new file mode 100644 index 00000000..29a9db38 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/java/ru/otus/spring/service/MyService.java @@ -0,0 +1,18 @@ +package ru.otus.spring.service; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +@Service +public class MyService { + + @PreAuthorize("hasRole('ROLE_USER') && {new java.util.Random().nextInt()%2 == 0}") + public String onlyUser() { + return "My love"; + } + + @Secured("ROLE_ADMIN") + public void onlyAdmin() { + } +} diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/application.yml b/2026-01/spring-22-SS-auth/classwork/src/main/resources/application.yml new file mode 100644 index 00000000..85cef7cd --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port: 8080 \ No newline at end of file diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/admin.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/admin.html new file mode 100644 index 00000000..aa1c9563 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/admin.html @@ -0,0 +1,9 @@ + + + + + + +Страница с доступом только админу + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/authenticated.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..e4756c01 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для аторизованных + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/error.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/error.html new file mode 100644 index 00000000..f28b51df --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/error.html @@ -0,0 +1,9 @@ + + + + + + +Вам доступ запрещён! + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/index.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/index.html new file mode 100644 index 00000000..d4c54ecd --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/index.html @@ -0,0 +1,15 @@ + + + + + + +/public +
+/authenticated +
+/user +
+/admin + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/manager.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/manager.html new file mode 100644 index 00000000..dd4a77af --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/manager.html @@ -0,0 +1,9 @@ + + + + + + +Доступ к MANAGER + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/public.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/success.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/success.html new file mode 100644 index 00000000..4e2a37cd --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/success.html @@ -0,0 +1,9 @@ + + + + + + +Вы успешно вошли ! + + diff --git a/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/user.html b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/user.html new file mode 100644 index 00000000..a794bc2d --- /dev/null +++ b/2026-01/spring-22-SS-auth/classwork/src/main/resources/templates/user.html @@ -0,0 +1,9 @@ + + + + + + +Доступ к USER + + diff --git a/2026-01/spring-22-SS-auth/jwt/pom.xml b/2026-01/spring-22-SS-auth/jwt/pom.xml new file mode 100644 index 00000000..87fbb4a2 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + ru.otus + spring-framework-27-jwt + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + 2.6.11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springdoc + springdoc-openapi-ui + 1.6.9 + + + + org.webjars + jquery + 3.4.1 + + + org.webjars + bootstrap + 4.3.1 + + + org.webjars + webjars-locator-core + + + org.webjars + js-cookie + 2.1.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/JwtStarter.java b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/JwtStarter.java new file mode 100644 index 00000000..79f664b0 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/JwtStarter.java @@ -0,0 +1,12 @@ +package ru.otus.security.jwt; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JwtStarter { + public static void main(String[] args){ + SpringApplication.run( JwtStarter.class, args ); + } +} diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/config/SecurityConfig.java b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/config/SecurityConfig.java new file mode 100644 index 00000000..32d6b974 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/config/SecurityConfig.java @@ -0,0 +1,97 @@ +package ru.otus.security.jwt.config; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +@Configuration +public class SecurityConfig { + + @Value("${jwt.public.key}") + RSAPublicKey key; + + @Value("${jwt.private.key}") + RSAPrivateKey priv; + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + + http + .authorizeHttpRequests( ( authorize ) -> authorize + .anyRequest().authenticated() + ) + .csrf( AbstractHttpConfigurer::disable ) + //.httpBasic( Customizer.withDefaults() ) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + .sessionManagement( ( session ) -> session.sessionCreationPolicy( SessionCreationPolicy.STATELESS ) ) + .exceptionHandling( ( exceptions ) -> exceptions + .authenticationEntryPoint( new BearerTokenAuthenticationEntryPoint() ) + .accessDeniedHandler( new BearerTokenAccessDeniedHandler() ) + ); + + return http.build(); + } + + @Bean + UserDetailsService users() { + + return new InMemoryUserDetailsManager( + User.withUsername( "user" ) + .password( "password" ) + .authorities( "app" ) + .build() + ); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new PasswordEncoder() { + @Override + public String encode( CharSequence charSequence ) { + return charSequence.toString(); + } + + @Override + public boolean matches( CharSequence charSequence, String s ) { + return charSequence.toString().equals( s ); + } + }; + } + + @Bean + JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey( this.key ).build(); + } + + @Bean + JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder( this.key ).privateKey( this.priv ).build(); + JWKSource jwks = new ImmutableJWKSet<>( new JWKSet( jwk ) ); + return new NimbusJwtEncoder( jwks ); + } +} diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/HelloController.java b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/HelloController.java new file mode 100644 index 00000000..4ff42a49 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/HelloController.java @@ -0,0 +1,14 @@ +package ru.otus.security.jwt.controller; + +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping("/hello") + public String hello( Authentication authentication ) { + return "Hello, " + authentication.getName() + "!"; + } +} diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/TokenController.java b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/TokenController.java new file mode 100644 index 00000000..02ddac7b --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/java/ru/otus/security/jwt/controller/TokenController.java @@ -0,0 +1,40 @@ +package ru.otus.security.jwt.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Instant; +import java.util.stream.Collectors; + +@RestController +public class TokenController { + + @Autowired + JwtEncoder encoder; + + @PostMapping("/token") + public String token( Authentication authentication) { + Instant now = Instant.now(); + long expiry = 36000L; + // @formatter:off + String scope = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect( Collectors.joining(",")); + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer("self") + .issuedAt(now) + .expiresAt(now.plusSeconds(expiry)) + .subject(authentication.getName()) + .claim("scope", scope) + .claim("claimTest", "123") + .build(); + // @formatter:on + return this.encoder.encode( JwtEncoderParameters.from(claims)).getTokenValue(); + } +} diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.key b/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.key new file mode 100644 index 00000000..53510079 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA +iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM +g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK +LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF +oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc +3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn ++jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE +E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek +lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG +mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 +62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 +bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA ++Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH +Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA +8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd +I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY +QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d +rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk +HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA +Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN +HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a +FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF +snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H +c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM +TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR +47jndeyIaMTNETEmOnms+as17g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.pub b/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.pub new file mode 100644 index 00000000..0b2ee7b3 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd +7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv +c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 +iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 +kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o +RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj +KwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/resources/application.yml b/2026-01/spring-22-SS-auth/jwt/src/main/resources/application.yml new file mode 100644 index 00000000..ad5d3850 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/resources/application.yml @@ -0,0 +1,10 @@ +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO +# org.springframework.boot.autoconfigure: DEBUG + +jwt: + private.key: classpath:app.key + public.key: classpath:app.pub \ No newline at end of file diff --git a/2026-01/spring-22-SS-auth/jwt/src/main/resources/templates/index.html b/2026-01/spring-22-SS-auth/jwt/src/main/resources/templates/index.html new file mode 100644 index 00000000..fa9cb9f6 --- /dev/null +++ b/2026-01/spring-22-SS-auth/jwt/src/main/resources/templates/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+/swagger + + diff --git a/2026-01/spring-22-SS-auth/solution2/pom.xml b/2026-01/spring-22-SS-auth/solution2/pom.xml new file mode 100644 index 00000000..a2c07373 --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + ru.otus + solution2 + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.7.8 + + + + + UTF-8 + UTF-8 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java new file mode 100644 index 00000000..6b3bf909 --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/SpringSecurityAuthorization.java @@ -0,0 +1,13 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringSecurityAuthorization { + + public static void main(String[] args) { + SpringApplication.run(SpringSecurityAuthorization.class); + } + +} diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/controller/PagesController.java b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/controller/PagesController.java new file mode 100644 index 00000000..b367fe8d --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/controller/PagesController.java @@ -0,0 +1,58 @@ +package ru.otus.spring.controller; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import ru.otus.spring.service.MyService; + +@Controller +public class PagesController { + + private final MyService myService; + + public PagesController(MyService myService) { + this.myService = myService; + } + + @GetMapping("/") + public String indexPage() { + return "index"; + } + + @GetMapping("/public") + public String publicPage() { + return "public"; + } + + @GetMapping("/user") + public String userPage() { + return "user"; + } + + @GetMapping("/manager") + public String managerPage() { + myService.onlyAdmin(); + return "manager"; + } + + @GetMapping("/admin") + @Secured("ROLE_ADMIN") + public String adminPage() { + return "admin"; + } + + @GetMapping("/authenticated") + public String authenticatedPage() { + UserDetails userDetails = (UserDetails) SecurityContextHolder + .getContext().getAuthentication().getPrincipal(); + System.out.println(userDetails.getUsername()); + return "authenticated"; + } + + @GetMapping("/success") + public String successPage() { + return "success"; + } +} diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java new file mode 100644 index 00000000..7add513b --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/MethodSecurityConfiguration.java @@ -0,0 +1,7 @@ +package ru.otus.spring.security; + +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class MethodSecurityConfiguration { +} diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..64e5216b --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,50 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.ArrayList; + +@EnableWebSecurity +public class SecurityConfiguration { + + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf().disable() + .authorizeHttpRequests( ( authorize ) -> authorize + .antMatchers( "/public", "/" ).permitAll() + .antMatchers( "/authenticated", "/success" ).authenticated() + .antMatchers( "/manager" ).hasRole("MANAGER") + .antMatchers( "/user/**" ).hasAnyRole( "USER", "MANAGER" ) + // /user/add /user/delete /user/delete/update + .anyRequest().denyAll() + ) + .formLogin() + + ; + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + var users = new ArrayList(); + users.add( User + .withDefaultPasswordEncoder().username( "admin" ).password( "password" ).roles( "ADMIN" ) + .build() ); + users.add( User + .withDefaultPasswordEncoder().username( "user" ).password( "password" ).roles( "USER" ) + .build() ); + users.add( User + .withDefaultPasswordEncoder().username( "manager" ).password( "password" ).roles( "MANAGER" ) + .build() ); + return new InMemoryUserDetailsManager( users ); + + } +} diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/service/MyService.java b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/service/MyService.java new file mode 100644 index 00000000..0adcbd3b --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/java/ru/otus/spring/service/MyService.java @@ -0,0 +1,13 @@ +package ru.otus.spring.service; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +@Service +public class MyService { + + @PreAuthorize("hasRole('ROLE_MANAGER') && {new java.util.Random().nextInt()%2 == 0}") + public void onlyAdmin() { + } +} diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/admin.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/admin.html new file mode 100644 index 00000000..aa1c9563 --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/admin.html @@ -0,0 +1,9 @@ + + + + + + +Страница с доступом только админу + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/authenticated.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/authenticated.html new file mode 100644 index 00000000..9f8b0d7e --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/authenticated.html @@ -0,0 +1,9 @@ + + + + + + +Только для авторизованных + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/error.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/error.html new file mode 100644 index 00000000..f28b51df --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/error.html @@ -0,0 +1,9 @@ + + + + + + +Вам доступ запрещён! + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/index.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/index.html new file mode 100644 index 00000000..9a8b9382 --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/index.html @@ -0,0 +1,17 @@ + + + + + + +/public +
+/authenticated +
+/user +
+/manager +
+/admin + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/manager.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/manager.html new file mode 100644 index 00000000..dd4a77af --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/manager.html @@ -0,0 +1,9 @@ + + + + + + +Доступ к MANAGER + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/public.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/public.html new file mode 100644 index 00000000..77188469 --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/public.html @@ -0,0 +1,9 @@ + + + + + + +Доступен всем + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/success.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/success.html new file mode 100644 index 00000000..4e2a37cd --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/success.html @@ -0,0 +1,9 @@ + + + + + + +Вы успешно вошли ! + + diff --git a/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/user.html b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/user.html new file mode 100644 index 00000000..a794bc2d --- /dev/null +++ b/2026-01/spring-22-SS-auth/solution2/src/main/resources/templates/user.html @@ -0,0 +1,9 @@ + + + + + + +Доступ к USER + + diff --git a/2026-01/spring-23-ACL/ACL/pom.xml b/2026-01/spring-23-ACL/ACL/pom.xml new file mode 100644 index 00000000..2ba139f5 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + ru.otus + spring-framework-26-acl + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.10 + + + + 2.6.11 + 3.0.0 + UTF-8 + UTF-8 + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-acl + + + net.sf.ehcache + ehcache-core + ${ehcache-core.version} + jar + + + org.springframework + spring-context-support + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + + + + org.springdoc + springdoc-openapi-ui + 1.6.9 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java new file mode 100644 index 00000000..baca49d5 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/Main.java @@ -0,0 +1,14 @@ +package ru.otus.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java new file mode 100644 index 00000000..e0363d94 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java @@ -0,0 +1,30 @@ +package ru.otus.spring.model; + +import javax.persistence.*; + +@Entity +@Table(name = "system_message") +public class NoticeMessage { + + @Id + @Column + private Integer id; + @Column + private String content; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java new file mode 100644 index 00000000..501d91a2 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/repository/NoticeMessageRepository.java @@ -0,0 +1,21 @@ +package ru.otus.spring.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import ru.otus.spring.model.NoticeMessage; + +import java.util.List; +import java.util.Optional; + +public interface NoticeMessageRepository extends JpaRepository { + + List findAll(); + + Optional findById(Integer id); + + NoticeMessage save(NoticeMessage noticeMessage); + +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java new file mode 100644 index 00000000..01bb4154 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/rest/NoticeMessageController.java @@ -0,0 +1,39 @@ +package ru.otus.spring.rest; + +import org.springframework.web.bind.annotation.*; +import ru.otus.spring.model.NoticeMessage; +import ru.otus.spring.repository.NoticeMessageRepository; +import ru.otus.spring.service.NoticeService; + +import java.util.List; + +@RestController +public class NoticeMessageController { + + private final NoticeService noticeService; + + public NoticeMessageController(NoticeService noticeService) { + this.noticeService = noticeService; + } + + @GetMapping("/message") + public List getAll() { + return noticeService.getAll(); + } + + @GetMapping("/message/{id}") + public NoticeMessage get(@PathVariable("id") Integer id) { + var result = noticeService.get( id ); + return result; + } + + @PostMapping("/message") + public NoticeMessage createMessage(@RequestBody NoticeMessage message) { + return noticeService.create(message); + } + + @PutMapping("/message/{id}") + public NoticeMessage updateMessage(@PathVariable("id") Integer id, @RequestBody NoticeMessage message) { + return noticeService.update(message); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java new file mode 100644 index 00000000..d261e2a7 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclConfig.java @@ -0,0 +1,79 @@ +package ru.otus.spring.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.ehcache.EhCacheFactoryBean; +import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.acls.AclPermissionCacheOptimizer; +import org.springframework.security.acls.AclPermissionEvaluator; +import org.springframework.security.acls.domain.*; +import org.springframework.security.acls.jdbc.BasicLookupStrategy; +import org.springframework.security.acls.jdbc.JdbcMutableAclService; +import org.springframework.security.acls.jdbc.LookupStrategy; +import org.springframework.security.acls.model.PermissionGrantingStrategy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import javax.sql.DataSource; +import java.util.Objects; + +@Configuration +public class AclConfig { + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + private DataSource dataSource; + + @Bean + public EhCacheBasedAclCache aclCache() { + return new EhCacheBasedAclCache( + Objects.requireNonNull(aclEhCacheFactoryBean().getObject()), + permissionGrantingStrategy(), + aclAuthorizationStrategy() + ); + } + + @Bean + public EhCacheFactoryBean aclEhCacheFactoryBean() { + EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); + ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); + ehCacheFactoryBean.setCacheName("aclCache"); + return ehCacheFactoryBean; + } + + @Bean + public EhCacheManagerFactoryBean aclCacheManager() { + return new EhCacheManagerFactoryBean(); + } + + @Bean + public PermissionGrantingStrategy permissionGrantingStrategy() { + return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()); + } + + @Bean + public AclAuthorizationStrategy aclAuthorizationStrategy() { + return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_EDITOR")); + } + + @Bean + public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { + AclMethodSecurityExpressionHandler expressionHandler = new AclMethodSecurityExpressionHandler(); + AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService()); + expressionHandler.setPermissionEvaluator(permissionEvaluator); + expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService())); + return expressionHandler; + } + + @Bean + public LookupStrategy lookupStrategy() { + return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()); + } + + @Bean + public JdbcMutableAclService aclService() { + return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache()); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java new file mode 100644 index 00000000..5fb8a778 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityConfiguration.java @@ -0,0 +1,29 @@ +package ru.otus.spring.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.acls.model.AclService; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { + + private final AclService aclService; + + public AclMethodSecurityConfiguration(AclService aclService) { + this.aclService = aclService; + } + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired + MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; + + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + return defaultMethodSecurityExpressionHandler; + } + +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java new file mode 100644 index 00000000..ef30e5f3 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionHandler.java @@ -0,0 +1,23 @@ +package ru.otus.spring.security; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.core.Authentication; + +public class AclMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { + + @Override + protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, + MethodInvocation invocation) { + + AclMethodSecurityExpressionRoot root = new AclMethodSecurityExpressionRoot(authentication); + root.setThis(invocation.getThis()); + root.setPermissionEvaluator(this.getPermissionEvaluator()); + root.setTrustResolver(this.getTrustResolver()); + root.setRoleHierarchy(this.getRoleHierarchy()); + root.setDefaultRolePrefix(this.getDefaultRolePrefix()); + + return root; + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java new file mode 100644 index 00000000..f7885ab0 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionOperations.java @@ -0,0 +1,12 @@ +package ru.otus.spring.security; + +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; + +public interface AclMethodSecurityExpressionOperations extends MethodSecurityExpressionOperations { + + boolean isAdministrator(Object targetId, Class targetClass); + + boolean isAdministrator(Object target); + + boolean canRead(Object targetId, Class targetClass); +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java new file mode 100644 index 00000000..73cf1068 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/AclMethodSecurityExpressionRoot.java @@ -0,0 +1,69 @@ +package ru.otus.spring.security; + +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.core.Authentication; + +public class AclMethodSecurityExpressionRoot extends SecurityExpressionRoot implements AclMethodSecurityExpressionOperations { + + private Object filterObject; + private Object returnObject; + private Object target; + + public AclMethodSecurityExpressionRoot(Authentication authentication) { + super(authentication); + } + + void setThis(Object target) { + this.target = target; + } + + @Override + public Object getFilterObject() { + return filterObject; + } + + @Override + public void setFilterObject(Object filterObject) { + this.filterObject = filterObject; + } + + @Override + public Object getReturnObject() { + return returnObject; + } + + @Override + public void setReturnObject(Object returnObject) { + this.returnObject = returnObject; + } + + @Override + public Object getThis() { + return this.target; + } + + @Override + public boolean isAdministrator(Object targetId, Class targetClass) { + + return isGranted(targetId, targetClass, admin); + } + + @Override + public boolean isAdministrator(Object target) { + + return hasPermission(target, admin); + } + + @Override + public boolean canRead(Object targetId, Class targetClass) { + + if(isAdministrator(targetId, targetClass)) return true; + + return isGranted(targetId, targetClass, read); + } + + boolean isGranted(Object targetId, Class targetClass, Object permission) { + + return hasPermission(targetId, targetClass.getCanonicalName(), permission); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java new file mode 100644 index 00000000..0919dad9 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/security/SecurityConfiguration.java @@ -0,0 +1,42 @@ +package ru.otus.spring.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.ArrayList; + +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { + http + .csrf().disable() + .authorizeHttpRequests( ( authorize ) -> authorize + .antMatchers( "/**", "/" ).permitAll() + ) + .httpBasic(); + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + var users = new ArrayList(); + users.add( User + .withDefaultPasswordEncoder().username("admin").password("password").roles("USER") + .build() ); + users.add( User + .withDefaultPasswordEncoder().username("user").password("password").roles("USER") + .build() ); + users.add( User + .withDefaultPasswordEncoder().username("someone").password("password").roles("EDITOR") + .build() ); + return new InMemoryUserDetailsManager( users ); + + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java new file mode 100644 index 00000000..33783ff5 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperService.java @@ -0,0 +1,9 @@ +package ru.otus.spring.service; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.model.Permission; + +public interface AclServiceWrapperService { + + void createPermission(Object object, Permission permission); +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java new file mode 100644 index 00000000..d5fdcd45 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/AclServiceWrapperServiceImpl.java @@ -0,0 +1,34 @@ +package ru.otus.spring.service; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class AclServiceWrapperServiceImpl implements AclServiceWrapperService { + + private final MutableAclService mutableAclService; + + public AclServiceWrapperServiceImpl(MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + } + + @Override + public void createPermission(Object object, Permission permission) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final Sid owner = new PrincipalSid(authentication); + ObjectIdentity oid = new ObjectIdentityImpl(object); + + final Sid admin = new GrantedAuthoritySid("ROLE_EDITOR"); + + MutableAcl acl = mutableAclService.createAcl(oid); + acl.insertAce(acl.getEntries().size(), permission, owner, true); + acl.insertAce(acl.getEntries().size(), permission, admin, true); + mutableAclService.updateAcl(acl); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java new file mode 100644 index 00000000..efb6a0b2 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeService.java @@ -0,0 +1,16 @@ +package ru.otus.spring.service; + +import ru.otus.spring.model.NoticeMessage; + +import java.util.List; + +public interface NoticeService { + + NoticeMessage create(NoticeMessage message); + + NoticeMessage get(Integer id); + + List getAll(); + + NoticeMessage update(NoticeMessage message); +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java new file mode 100644 index 00000000..e5b43913 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/java/ru/otus/spring/service/NoticeServiceImpl.java @@ -0,0 +1,60 @@ +package ru.otus.spring.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.MutableAcl; +import org.springframework.security.acls.model.MutableAclService; +import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.Sid; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.otus.spring.model.NoticeMessage; +import ru.otus.spring.repository.NoticeMessageRepository; + +import java.util.List; + +@Service +public class NoticeServiceImpl implements NoticeService { + + private final AclServiceWrapperService aclServiceWrapperService; + + private final NoticeMessageRepository repository; + + public NoticeServiceImpl(AclServiceWrapperService aclServiceWrapperService, NoticeMessageRepository repository) { + this.aclServiceWrapperService = aclServiceWrapperService; + this.repository = repository; + } + + @Override + @Transactional + public NoticeMessage create(NoticeMessage message) { + NoticeMessage savedMessage = repository.save(message); + aclServiceWrapperService.createPermission(savedMessage, BasePermission.READ); + return savedMessage; + } + + @Override + @PostFilter("hasPermission(filterObject, 'READ')") + public List getAll() { + return repository.findAll(); + } + + @Override + @PreAuthorize("hasPermission(#message, 'WRITE')") + public NoticeMessage update(NoticeMessage message) { + return repository.save(message); + } + + @Override + @PreAuthorize("canRead(#id, T(ru.otus.spring.model.NoticeMessage))") + public NoticeMessage get(Integer id) { + return repository.findById(id).get(); + } +} diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/application.yml b/2026-01/spring-23-ACL/ACL/src/main/resources/application.yml new file mode 100644 index 00000000..02f0a284 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + h2: + console: + enabled: true + jpa: + hibernate: + ddl-auto: none + datasource: + url: jdbc:h2:mem:testdb + +springdoc: + packages-to-scan: ru.otus.spring.rest + paths-to-match: /** \ No newline at end of file diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/data.sql b/2026-01/spring-23-ACL/ACL/src/main/resources/data.sql new file mode 100644 index 00000000..9ae11203 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/data.sql @@ -0,0 +1,28 @@ +INSERT INTO system_message(id,content) VALUES +(1,'First Level Message'), +(2,'Second Level Message'), +(3,'Third Level Message'); + + +INSERT INTO acl_sid (id, principal, sid) VALUES +(1, 1, 'admin'), +(2, 1, 'user'), +(3, 0, 'ROLE_EDITOR'); + +INSERT INTO acl_class (id, class) VALUES +(1, 'ru.otus.spring.model.NoticeMessage'); + +INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES +(1, 1, 1, NULL, 3, 0), +(2, 1, 2, NULL, 3, 0), +(3, 1, 3, NULL, 3, 0); + +INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, + granting, audit_success, audit_failure) VALUES +(1, 1, 1, 1, 1, 1, 1, 1), +(2, 1, 2, 1, 2, 1, 1, 1), +(3, 1, 3, 3, 1, 1, 1, 1), +(4, 2, 1, 2, 1, 1, 1, 1), +(5, 2, 2, 3, 1, 1, 1, 1), +(6, 3, 1, 3, 1, 1, 1, 1), +(7, 3, 2, 3, 2, 1, 1, 1); diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/postman_collection.json b/2026-01/spring-23-ACL/ACL/src/main/resources/postman_collection.json new file mode 100644 index 00000000..50f5f187 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/postman_collection.json @@ -0,0 +1,88 @@ +{ + "info": { + "_postman_id": "00ffe5e9-877c-418a-b088-4409f34756e9", + "name": "New Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "46752532", + "_collection_link": "https://uladzimirmaherau.postman.co/workspace/Uladzimir-Maherau's-Workspace~acb6d39e-e608-4ada-8982-80dabd38178f/collection/46752532-00ffe5e9-877c-418a-b088-4409f34756e9?action=share&source=collection_link&creator=46752532" + }, + "item": [ + { + "name": "http://localhost:8080/message", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "password", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": " {\r\n \"id\": 11,\r\n \"content\": \"11 Level Message\"\r\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/message", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "message" + ] + } + }, + "response": [] + }, + { + "name": "http://localhost:8080/message", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "password", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/message", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "message" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/schema.json b/2026-01/spring-23-ACL/ACL/src/main/resources/schema.json new file mode 100644 index 00000000..f3c13aa2 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/schema.json @@ -0,0 +1,113 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/message": { + "get": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getAll", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + }, + "put": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getById", + "parameters": [ + { + "name": "message", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + }, + "/message/{id}": { + "get": { + "tags": [ + "notice-message-controller" + ], + "operationId": "getById_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/NoticeMessage" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NoticeMessage": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/schema.sql b/2026-01/spring-23-ACL/ACL/src/main/resources/schema.sql new file mode 100644 index 00000000..9f740482 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/schema.sql @@ -0,0 +1,58 @@ +create table IF NOT EXISTS system_message (id integer not null, content varchar(255), primary key (id)); + +CREATE TABLE IF NOT EXISTS acl_sid ( + id bigint(20) NOT NULL AUTO_INCREMENT, + principal tinyint(1) NOT NULL, + sid varchar(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_1 (sid,principal) +); + +CREATE TABLE IF NOT EXISTS acl_class ( + id bigint(20) NOT NULL AUTO_INCREMENT, + class varchar(255) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_2 (class) +); + +CREATE TABLE IF NOT EXISTS acl_entry ( + id bigint(20) NOT NULL AUTO_INCREMENT, + acl_object_identity bigint(20) NOT NULL, + ace_order int(11) NOT NULL, + sid bigint(20) NOT NULL, + mask int(11) NOT NULL, + granting tinyint(1) NOT NULL, + audit_success tinyint(1) NOT NULL, + audit_failure tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_4 (acl_object_identity,ace_order) +); + +CREATE TABLE IF NOT EXISTS acl_object_identity ( + id bigint(20) NOT NULL AUTO_INCREMENT, + object_id_class bigint(20) NOT NULL, + object_id_identity bigint(20) NOT NULL, + parent_object bigint(20) DEFAULT NULL, + owner_sid bigint(20) DEFAULT NULL, + entries_inheriting tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_3 (object_id_class,object_id_identity) +); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (sid) REFERENCES acl_sid(id); + +-- +-- Constraints for table acl_object_identity +-- +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (object_id_class) REFERENCES acl_class (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (owner_sid) REFERENCES acl_sid (id); \ No newline at end of file diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/templates/error.html b/2026-01/spring-23-ACL/ACL/src/main/resources/templates/error.html new file mode 100644 index 00000000..f28b51df --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/templates/error.html @@ -0,0 +1,9 @@ + + + + + + +Вам доступ запрещён! + + diff --git a/2026-01/spring-23-ACL/ACL/src/main/resources/templates/index.html b/2026-01/spring-23-ACL/ACL/src/main/resources/templates/index.html new file mode 100644 index 00000000..79347f42 --- /dev/null +++ b/2026-01/spring-23-ACL/ACL/src/main/resources/templates/index.html @@ -0,0 +1,15 @@ + + + + + + +login +
+logout +
+h2-console +
+/swagger + + diff --git a/2026-01/spring-23-ACL/oauth/pom.xml b/2026-01/spring-23-ACL/oauth/pom.xml new file mode 100644 index 00000000..36300726 --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + ru.otus + spring-framework-27-oauth + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.10 + + + + 2.6.11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + + + org.springdoc + springdoc-openapi-ui + 1.6.9 + + + + org.webjars + jquery + 3.4.1 + + + org.webjars + bootstrap + 4.3.1 + + + org.webjars + webjars-locator-core + + + org.webjars + js-cookie + 2.1.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java new file mode 100644 index 00000000..7a69b021 --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/GithubApplication.java @@ -0,0 +1,13 @@ +package ru.otus.spring.sso; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GithubApplication { + + public static void main(String[] args) { + SpringApplication.run( GithubApplication.class, args); + } + +} \ No newline at end of file diff --git a/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java new file mode 100644 index 00000000..a6e55cd2 --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/IndexController.java @@ -0,0 +1,13 @@ +package ru.otus.spring.sso.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class IndexController { + + @GetMapping("/") + public String indexPage() { + return "index"; + } +} diff --git a/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java new file mode 100644 index 00000000..a99dc54d --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/controller/UserController.java @@ -0,0 +1,17 @@ +package ru.otus.spring.sso.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Map; + +@RestController +public class UserController { + @GetMapping("/user") + public Map user( @AuthenticationPrincipal OAuth2User principal) { + return Collections.singletonMap("name", principal.getAttribute("name")); + } +} diff --git a/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java new file mode 100644 index 00000000..b357a066 --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/java/ru/otus/spring/sso/security/SecurityConfig.java @@ -0,0 +1,26 @@ +package ru.otus.spring.sso.security; + +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; + +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure( HttpSecurity http ) throws Exception { + http + .authorizeRequests( a -> a + .antMatchers( "/", "/error", "/webjars/**" ).permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling( e -> e + .authenticationEntryPoint( new HttpStatusEntryPoint( HttpStatus.UNAUTHORIZED ) ) + ) + .csrf().disable() + .logout( l -> l + .logoutSuccessUrl( "/" ).permitAll() + ) + + .oauth2Login(); + } +} diff --git a/2026-01/spring-23-ACL/oauth/src/main/resources/application.yml b/2026-01/spring-23-ACL/oauth/src/main/resources/application.yml new file mode 100644 index 00000000..c595efd2 --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + security: + oauth2: + client: + registration: + github: + clientId: Ov23liJ3EIv6UfvJ4jPr + clientSecret: 0cb7763869aaa2151a399336c64cdef2618c6332 + +logging: + level: + root: error + org.springframework.security: DEBUG \ No newline at end of file diff --git a/2026-01/spring-23-ACL/oauth/src/main/resources/templates/index.html b/2026-01/spring-23-ACL/oauth/src/main/resources/templates/index.html new file mode 100644 index 00000000..09dae5ba --- /dev/null +++ b/2026-01/spring-23-ACL/oauth/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ + + + + + + Demo + + + + + + + + +

Demo

+
+ + \ No newline at end of file