Compare commits

...

117 Commits

Author SHA1 Message Date
Tom Hombergs
97bf254d85 update to Java 13, fixing build 2020-05-27 06:33:15 +10:00
Yavuz Tas
381fe85e39 add a sample custom validator for AppProperties and add tests 2020-05-26 08:09:11 +02:00
Yavuz Tas
a418f21ff1 add third-party component properties example and update the tests 2020-05-26 05:50:40 +02:00
Yavuz Tas
735d80d72f Code samples for Validate Spring Boot Configuration Parameters at Startup 2020-05-22 19:45:26 +02:00
Tom Hombergs
3eb97d81ec CloudFormation stacks for creating a Postgres instance, deploying a Spring Boot app and connecting the app to the database. 2020-05-11 17:07:35 +10:00
Tom Hombergs
27e1428488 update readme 2020-05-09 22:00:20 +10:00
Tom Hombergs
7b63198a08 example Docker image requiring a database connection 2020-05-09 08:38:41 +10:00
Tom Hombergs
45c947ddc1 Merge pull request #25 from arkuksin/testcontainers
Testcontainers
2020-05-07 06:47:06 +10:00
akuksin
e41458b7cd Merge branch 'master' into testcontainers 2020-05-05 20:38:44 +02:00
akuksin
03ab12c64b remove @NotNull annotation 2020-05-05 17:37:41 +02:00
akuksin
f183c92661 refactoring 2020-05-05 17:14:24 +02:00
Tom Hombergs
109dacd121 Merge pull request #24 from arkuksin/testcontainers
Testcontainers
2020-05-03 07:12:57 +10:00
Tom Hombergs
0bff861d25 Add CloudFormation templates 2020-05-02 06:39:33 +10:00
akuksin
51be8954a9 Merge branch 'master' into testcontainers 2020-04-29 20:09:38 +02:00
akuksin
4bd0201900 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	build-all.sh
2020-04-29 20:08:59 +02:00
akuksin
074b118bff add comment 2020-04-25 20:17:16 +02:00
akuksin
1a4c9421dc clean up test 2020-04-22 23:56:35 +02:00
akuksin
6168a9a47a delete unnecessary code 2020-04-22 23:11:20 +02:00
akuksin
b6cd9b8138 add testcontainers project to build-all.sh 2020-04-22 22:47:26 +02:00
prabhakar349
cc153bfd06 feat(liquibase)[]: add liquibase code example 2020-04-22 22:42:51 +02:00
thombergs
cf3a36e108 remove unnecessary package check 2020-04-22 22:42:47 +02:00
thombergs
2e6e26193f polishing 2020-04-22 22:42:47 +02:00
Nandan Bn
ae8ec346fd Package visibility changes 2020-04-22 22:42:47 +02:00
Nandan Bn
caf408a550 Fixes and added examples for transactional... 2020-04-22 22:42:47 +02:00
Nandan Bn
4543443a82 Reflect-105 Code example 2020-04-22 22:42:47 +02:00
thombergs
ae813adb18 fixed build 2020-04-22 22:42:47 +02:00
thombergs
6a27f35b2a updated with valueOf() 2020-04-22 22:42:38 +02:00
thombergs
1f9fbc2543 changed class name 2020-04-22 22:42:38 +02:00
thombergs
52f8dfc91c reduce log output in Travis Build 2020-04-22 22:42:35 +02:00
Petros Stergioulas
a5305e266f Implement UserApiDelegate, instead of the UserApi 2020-04-22 22:42:28 +02:00
Petros Stergioulas
507ff901b4 Rename module to spring-boot-openapi, also add maven wrapper in root 2020-04-22 22:42:28 +02:00
Petros Stergioulas
8b5e2ce3a4 Reflect-91 init 2020-04-22 22:42:28 +02:00
thombergs
ebd2362c1c billingmodule -> billing 2020-04-22 22:42:28 +02:00
thombergs
72f40744dd billingmodule -> billing 2020-04-22 22:42:28 +02:00
thombergs
43f9e32282 added missing Gradle files 2020-04-22 22:42:27 +02:00
thombergs
f85a0f36ae example for clean architecture boundaries with Spring Boot 2020-04-22 22:42:27 +02:00
thombergs
1f2b333a8a listContributors -> contributors 2020-04-22 22:42:25 +02:00
akuksin
4f056283b3 replace @Autowired by @Bean annotation for the authentication provider 2020-04-22 22:42:25 +02:00
akuksin
054d177ef2 remove custom code for password migration, use the feature of DelegatingPasswordEncoder instead 2020-04-22 22:42:25 +02:00
akuksin
84f441dbeb rename JdbcUserDetailPasswordService to DatabaseUserDetailPasswordService 2020-04-22 22:42:25 +02:00
akuksin
b3be06ea00 remove @Transactional annotation from RegistrationResource 2020-04-22 22:42:24 +02:00
akuksin
1fa7f545fd rename UserResources to RegistrationResource 2020-04-22 22:42:24 +02:00
akuksin
32f3e4cefa rename daoAuthenticationProvider to provider 2020-04-22 22:42:24 +02:00
akuksin
5109d98167 rename JdbcUserDetailsService to DatabaseUserDetailsService 2020-04-22 22:42:24 +02:00
akuksin
dc12f67ae4 make classes package private, apply google-java-formatter 2020-04-22 22:42:24 +02:00
akuksin
8c34f22b7e remove unnecessary comment 2020-04-22 22:42:24 +02:00
akuksin
ffa9ac6d5e simplify the configuration 2020-04-22 22:42:24 +02:00
akuksin
0b9e63571e reformat code 2020-04-22 22:42:24 +02:00
akuksin
3b69be714e add calculation of bcryt strength with Divide-and-conquer algorithm 2020-04-22 22:42:24 +02:00
akuksin
2ae9c0b2ae add simple examples for password encoders 2020-04-22 22:42:24 +02:00
akuksin
ef9fe2c506 add project spring-boot/password-encoding 2020-04-22 22:42:24 +02:00
thombergs
9653304df2 Repository -> GitRepository 2020-04-22 22:42:24 +02:00
thombergs
b7dc19cb8d argumentresolver example 2020-04-22 22:42:24 +02:00
thombergs
16bdac5240 MethodArgumentResolver example 2020-04-22 22:42:20 +02:00
thombergs
53a7cd866f example of converters in Spring Data JDBC 2020-04-22 22:42:20 +02:00
Petromir Dzhunev
78fd7fb89f REFLECT-2 Add missing bits
Add undo migration, add Jenkins files, add Java-base migration
2020-04-22 22:41:47 +02:00
Petromir Dzhunev
94ebb0bde7 REFLECT-2 Remove redundant parameters to Gradle plugin 2020-04-22 22:41:47 +02:00
Petromir Dzhunev
681d34b775 REFLECT-2 Integrating H2 with Flyway 2020-04-22 22:41:47 +02:00
Petromir Dzhunev
d8ca7d3f04 Initial set-up of Flyway data migration project 2020-04-22 22:41:47 +02:00
thombergs
bf68901255 changes after review 2020-04-22 22:41:47 +02:00
Mukul Sharma
ab949c2a0b Reflect-76 ISP explained added code examples 2020-04-22 22:41:47 +02:00
thombergs
90f7b1ad39 refactored and made the tests work 2020-04-22 22:41:47 +02:00
Vasudha Venkatesan
4b959cb797 Code example for field injection and other changes 2020-04-22 22:41:47 +02:00
Vasudha Venkatesan
670c4c845a Code example changes 2020-04-22 22:41:47 +02:00
Vasudha Venkatesan
531d05de64 Code format changes and Added maven wrapper 2020-04-22 22:41:47 +02:00
Vasudha Venkatesan
3a929134ba Constructor and setter based Dependency injection code example 2020-04-22 22:41:47 +02:00
akuksin
f1aa143b96 initial commit 2020-04-22 21:11:39 +02:00
Tom Hombergs
f05e8f75cd Merge pull request #23 from pdigumarthi/feat-liquibase-example
feat(liquibase)[]: add liquibase code example
2020-04-14 08:33:29 +10:00
prabhakar349
190f2511af feat(liquibase)[]: add liquibase code example 2020-04-12 18:24:09 -04:00
thombergs
4ecc79977a remove unnecessary package check 2020-04-09 06:37:44 +10:00
Tom Hombergs
7e4b6d4cac Merge pull request #17 from Lucaarioo/master
Reflect-105 Code example
2020-03-31 06:10:11 +11:00
thombergs
e023c7c42f polishing 2020-03-31 06:06:50 +11:00
Tom Hombergs
a857ad5455 Merge pull request #18 from vasudhavenkatesan/master
REFLECT -104 : Constructor and setter based Dependency injection code example
2020-03-29 06:46:03 +11:00
thombergs
51460691bb fixed build 2020-03-28 06:34:57 +11:00
thombergs
01b0ef3eb8 Merge branch 'master' into vasudha-master 2020-03-28 06:29:07 +11:00
thombergs
4f006376c1 updated with valueOf() 2020-03-25 06:55:19 +11:00
thombergs
01db1e9805 refactored and made the tests work 2020-03-25 06:06:04 +11:00
thombergs
a989129816 changed class name 2020-03-14 08:58:44 +11:00
thombergs
4ceee9a453 reduce log output in Travis Build 2020-03-12 06:49:07 +11:00
Tom Hombergs
c90578311b Merge pull request #21 from thombergs/openapi
Openapi
2020-03-11 07:05:34 +11:00
thombergs
7cd125ee33 added to build-all.sh 2020-03-11 06:40:03 +11:00
thombergs
76e97842a2 Merge branch 'reflect-91' of https://github.com/Petros0/code-examples into openapi 2020-03-11 06:18:52 +11:00
thombergs
92aef0228e billingmodule -> billing 2020-03-11 05:45:01 +11:00
Vasudha Venkatesan
ca32144e80 Code example for field injection and other changes 2020-03-10 10:55:35 +05:30
thombergs
e8ef69917a billingmodule -> billing 2020-03-10 07:05:08 +11:00
Petros Stergioulas
5da17c5a20 Implement UserApiDelegate, instead of the UserApi 2020-03-09 20:08:40 +01:00
thombergs
8a36085425 added missing Gradle files 2020-03-07 08:42:37 +11:00
thombergs
1916f5b2af example for clean architecture boundaries with Spring Boot 2020-03-07 08:26:15 +11:00
thombergs
c69957d1b8 listContributors -> contributors 2020-03-06 06:40:27 +11:00
thombergs
426d1e77c6 Merge remote-tracking branch 'origin/master' 2020-03-05 06:23:17 +11:00
thombergs
ec681b5abc Repository -> GitRepository 2020-03-05 06:23:01 +11:00
Tom Hombergs
57ff03707c Merge pull request #19 from arkuksin/spring-boot-password-emcoding
add project spring-boot/password-encoding
2020-03-04 05:50:59 +11:00
thombergs
761fdd4d7a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	build-all.sh
2020-03-02 21:17:41 +11:00
thombergs
939e864a4a example of converters in Spring Data JDBC 2020-03-02 21:16:33 +11:00
akuksin
84c593016b replace @Autowired by @Bean annotation for the authentication provider 2020-03-01 11:41:50 +01:00
akuksin
e0a456724d remove custom code for password migration, use the feature of DelegatingPasswordEncoder instead 2020-03-01 10:48:52 +01:00
Tom Hombergs
c8a15d4d4d Merge pull request #20 from thombergs/argumentresolver
Argumentresolver
2020-03-01 09:37:07 +11:00
akuksin
0b9a101b08 rename JdbcUserDetailPasswordService to DatabaseUserDetailPasswordService 2020-02-28 19:32:50 +01:00
akuksin
1b9f2426a4 remove @Transactional annotation from RegistrationResource 2020-02-27 22:42:36 +01:00
akuksin
3ab8517320 rename UserResources to RegistrationResource 2020-02-27 22:40:11 +01:00
akuksin
77151d1752 rename daoAuthenticationProvider to provider 2020-02-27 22:36:06 +01:00
akuksin
b73731159f rename JdbcUserDetailsService to DatabaseUserDetailsService 2020-02-27 22:22:16 +01:00
akuksin
bfd004b73a make classes package private, apply google-java-formatter 2020-02-27 22:17:40 +01:00
Vasudha Venkatesan
1f4484f25f Code example changes 2020-02-26 14:43:33 +05:30
akuksin
ab5143cb2f remove unnecessary comment 2020-02-25 21:58:33 +01:00
akuksin
74f0e28e8e simplify the configuration 2020-02-25 20:12:30 +01:00
Nandan Bn
47baf5ea91 Package visibility changes 2020-02-25 01:08:13 +05:30
Vasudha Venkatesan
61ed2eaaa8 Code format changes and Added maven wrapper 2020-02-24 12:44:47 +05:30
akuksin
d55ee992f3 reformat code 2020-02-22 23:39:48 +01:00
akuksin
9a378b5763 add calculation of bcryt strength with Divide-and-conquer algorithm 2020-02-22 23:35:21 +01:00
Nandan Bn
c4ba025b17 Fixes and added examples for transactional... 2020-02-22 10:22:16 +05:30
akuksin
4ca9b30b94 add simple examples for password encoders 2020-02-20 22:10:17 +01:00
akuksin
0847972b5a add project spring-boot/password-encoding 2020-02-17 22:24:01 +01:00
Vasudha Venkatesan
1e9c30733d Constructor and setter based Dependency injection code example 2020-02-17 22:46:46 +05:30
Nandan Bn
90657e3a6b Reflect-105 Code example 2020-02-17 12:02:27 +05:30
Petros Stergioulas
26739aada2 Rename module to spring-boot-openapi, also add maven wrapper in root 2020-02-11 20:59:00 +01:00
Petros Stergioulas
dabc601384 Reflect-91 init 2020-02-09 22:25:26 +01:00
267 changed files with 12273 additions and 88 deletions

32
aws/aws-rds-hello-world/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,5 @@
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

View File

@@ -0,0 +1,25 @@
# RDS Hello World Application
This is a simple Spring Boot application which requires access to a PostgreSQL database.
The application has a single endpoint `/hello` which prints out if the database connection was successful.
Get it in a Docker image via `docker pull reflectoring/aws-rds-hello-world`.
Use the image instead of your real application to test AWS CloudFormation stacks which need access to a database.
## Testing AWS RDS connectivity with this application
1. Create an RDS PostgreSQL database with the AWS console.
2. Note the endpoint of your RDS database in the AWS console.
3. Deploy the Docker container `reflectoring/aws-rds-hello-world` into AWS instead of your real application (this could be via a CloudFormation stack, manually, or however you are deploying your app).
4. Configure your deployment in a way that Docker will pass the coordinates to your RDS database as environment variables, equivalent to this command:
```
docker run \
-e SPRING_DATASOURCE_URL=':'<RDS-ENDPOINT>:5432/postgres \
-e SPRING_DATASOURCE_USERNAME=<USERNAME> \
-e SPRING_DATASOURCE_PASSWORD=<PASSWORD> \
-p 8080:8080 reflectoring/aws-rds-hello-world
```
5. If the Spring Boot application can connect to the database, it will start up sucessfully and serve a message on the endpoint `/hello`.

View File

@@ -0,0 +1,31 @@
plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// database
implementation 'org.flywaydb:flyway-core'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'org.postgresql:postgresql'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,18 @@
version: '3.7'
services:
postgres:
container_name: "rds-hello-world"
image: postgres
volumes:
- rds-hello-world:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_USER=hello
- POSTGRES_PASSWORD=hello
volumes:
rds-hello-world:
driver: local

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
aws/aws-rds-hello-world/gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
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
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
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"
which java >/dev/null 2>&1 || 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
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
aws/aws-rds-hello-world/gradlew.bat vendored Executable file
View File

@@ -0,0 +1,84 @@
@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=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
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 init
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
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
: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 %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'aws-rds-hello-world'

View File

@@ -0,0 +1,13 @@
package io.reflectoring.awshelloworld;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AwsHelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(AwsHelloWorldApplication.class, args);
}
}

View File

@@ -0,0 +1,23 @@
package io.reflectoring.awshelloworld;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class HelloWorldController {
private final UserRepository userRepository;
HelloWorldController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/hello")
String helloWorld(){
Iterable<User> users = userRepository.findAll();
return "Hello AWS! Successfully connected to the database!";
}
}

View File

@@ -0,0 +1,38 @@
package io.reflectoring.awshelloworld;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table("hello_user")
public class User {
@Id
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,6 @@
package io.reflectoring.awshelloworld;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
}

View File

@@ -0,0 +1,5 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/hello
username: hello
password: hello

View File

@@ -0,0 +1,5 @@
create table hello_user (
id varchar(36) not null unique,
name varchar(100) not null,
primary key(id)
);

View File

@@ -0,0 +1,8 @@
# Overview
![ECS in two public subnets](ecs-in-two-public-subnets.svg)
# Companion Blog Post
[The AWS Journey Part 2: Deploying a Docker image from the Command Line with CloudFormation](https://reflectoring.io/aws-cloudformation-deploy-docker-image/)

View File

@@ -0,0 +1 @@
<mxfile modified="2020-04-30T21:28:18.347Z" host="app.diagrams.net" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" etag="X9Eef2gSXNvxkCnmY-_0" version="13.0.4" type="device"><diagram id="Ht1M8jgEwFfnCIfOTk4-" name="Page-1">7Vpbb+o4EP41PC7K/fLIrWcr9UiVWO05+4RM4garIc4aU6C/fseJc7ND6ekB9nQXVKmZsTMez3yf7TEM7Ml6/4WhfPWVxjgdWEa8H9jTgWWZphXCP6E5lBrPcEpFwkgsOzWKOXnFUmlI7ZbEeNPpyClNOcm7yohmGY54R4cYo7tutyeadkfNUYI1xTxCqa79RmK+qublhU3D75gkKzl0YPllwxpVneVMNisU011LZc8G9oRRysun9X6CUxG8Ki7le3dHWmvHGM74e15Y4cjav97736avy2fP+Rr/vVj+Vjn3gtKtnPGfjxPpMD9UUcgpyXgRSXcMf/DSxBi40DIR0tByFYUq+12FqUvCRlehyn5XYarmTWV8U3WwpdCkjnlDGd9oOQh/9phueUoyPKkxZ4AyYSgmkIsJTSkDXUYziN54xdcpSCY87laE43mOIhHVHfAFdE804xL1plXJMvDiHUBNLp7X+0QQbIh2G2eYMLrNiyHvAfe9rYuXPBKvc0afceXSwLItJwhMRwxE0lRx9QUzTgD6o5QkwiqnYhAkpRQ/cWER/CdZ8lBIU9uQPreGGI3G/jgAfYw2KxzL8EicwRB4fxTBZs0LWFAwXWPODtClesGTaJVrSQXeXUNMO5S6VYuTjiGVSC4GSW264Qs8SMr8AH3MUKPP43aZkkjQZ7vMML9R6fNTaYOjLSP8sGg6zwteSXffRzLQz8I7e+adj2n1OOdmmm0pTLPCoR1obKsZ2Gab6ZkFSC5COPvGtxvf/vt8c4Buftj6uL8A90xfI99sMgfFHLMXAshQqddzxNBS5468SeC1Q2oezZcKMCU7talzJMTvJsTvXf9MuycHVnip04arxX9geamA7hIeEvEwynNYDREnwK2qjVWNDxTFuhZogLIIs6oFXKvNaSntXT46bKhS94CWOH2kG1L4Yk+XlHO6PsnFCBIEvnQWn56FxB6iZqaLFCa2WFbz0JaBOzdwbef4OncGvMAZs4MX0w+GgQ6Yqmps48W3LgSX4DRc7kWwi13TQ2uRhmy5yYuIqCj5gjjeocMnAwmR81sk0v1+bHgXxUa9SrSw4esL+lWxodct5VL+B9o8vy+f6tZoW3difC2OIsZ+MDPa/JsSBobKlGeUiQioeZka7gQ2nJ4d46n4XAFRp843aJOX4Xgie+FH/4GH4Q3dsgiXx50xiH0HHxxtzgO2utatwGb07lx+z8blX6xKdm5o+3+gzXL/fbRZeo2oYQzHCa6CKxBBE5qhdNZo25nBWTwSN8kivymNnoUqXRZyleYCOYjxqp/cPODNO5LWO5R29nXcsSeQCmVOFtfggtizw/e28JcQoKqT4nTfbpwe2tIjZgTiKNDVKk7EfN9OMYSnAM5bgZWbBkw0wfzUwUPHTPvgXOGG4RSOcC9d5/ogIc09imq+wV+owM9Uap9yTvKl9t24YsdU6i9bvTEs56wZKgBaT/EnMOvdMPsRzJ6Eons1KFrqUvhhLAYKFs0rY1Ev8z8ZFveEf6/GhucWEkFqgCiEQ0u44NLpvhOu5UnpKng1TfXKyfgYXq1Tho7gFXIt6sm6m7yoPeqw7fYT7JhfWv/QUehSenBe8vQV3TfyXIU84dW4Y1ff+9VfjzjDIDDqj2LwvUxyHLdr1hOn6aNmfxFemc41eKVfWLx1mfUp76isH7+dqn9T0nwtUv1KxT7TxaaS7MDvu7vyw6FUtks81zlOrCMlHojNL2JK8DS/K7Jn/wA=</diagram></mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,247 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A network stack for deploying containers in AWS ECS.
This stack creates a VPC with two public subnets and a loadbalancer to balance traffic between those subnets.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
PublicSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.1.0/24'
MapPublicIpOnLaunch: true
PublicSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.2.0/24'
MapPublicIpOnLaunch: true
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachement:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref 'VPC'
InternetGatewayId: !Ref 'InternetGateway'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref 'VPC'
PublicSubnetOneRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetOne
RouteTableId: !Ref PublicRouteTable
PublicSubnetTwoRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetTwo
RouteTableId: !Ref PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachement
Properties:
RouteTableId: !Ref 'PublicRouteTable'
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref 'InternetGateway'
PublicLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'VPC'
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
SecurityGroups: [!Ref 'PublicLoadBalancerSecurityGroup']
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref 'VPC'
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- PublicLoadBalancer
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS containers
VpcId: !Ref 'VPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
ECSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Rules which allow ECS to attach network interfaces to instances
# on your behalf in order for awsvpc networking mode to work right
- 'ec2:AttachNetworkInterface'
- 'ec2:CreateNetworkInterface'
- 'ec2:CreateNetworkInterfacePermission'
- 'ec2:DeleteNetworkInterface'
- 'ec2:DeleteNetworkInterfacePermission'
- 'ec2:Describe*'
- 'ec2:DetachNetworkInterface'
# Rules which allow ECS to update load balancers on your behalf
# with the information sabout how to send traffic to your containers
- 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
- 'elasticloadbalancing:DeregisterTargets'
- 'elasticloadbalancing:Describe*'
- 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
- 'elasticloadbalancing:RegisterTargets'
Resource: '*'
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: AmazonECSTaskExecutionRolePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Allow the ECS Tasks to download images from ECR
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
# Allow the ECS tasks to upload logs to CloudWatch
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
Outputs:
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
ExternalUrl:
Description: The url of the external load balancer
Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']]
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ]
ECSRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSRole' ] ]
ECSTaskExecutionRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSTaskExecutionRole' ] ]
PublicListener:
Description: The ARN of the public load balancer's Listener
Value: !Ref PublicLoadBalancerListener
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicListener' ] ]
VPCId:
Description: The ID of the VPC that this stack is deployed in
Value: !Ref 'VPC'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ]
PublicSubnetOne:
Description: Public subnet one
Value: !Ref 'PublicSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ]
PublicSubnetTwo:
Description: Public subnet two
Value: !Ref 'PublicSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetTwo' ] ]
ECSSecurityGroup:
Description: A security group used to allow ECS containers to receive traffic
Value: !Ref 'ECSSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSSecurityGroup' ] ]

View File

@@ -0,0 +1,139 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a service on AWS Fargate, hosted in two public subnets and accessible via a public load balancer.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Parameters:
StackName:
Type: String
Description: The name of the networking stack that
these resources are put into.
ServiceName:
Type: String
Description: A human-readable name for the service.
HealthCheckPath:
Type: String
Default: /health
Description: Path to perform the healthcheck on each instance.
HealthCheckIntervalSeconds:
Type: Number
Default: 5
Description: Number of seconds to wait between each health check.
ImageUrl:
Type: String
Description: The url of a docker image that will handle incoming traffic.
ContainerPort:
Type: Number
Default: 80
Description: The port number the application inside the docker container
is binding to.
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU.
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container.
Path:
Type: String
Default: "*"
Description: A path on the public load balancer that this service
should be connected to.
DesiredCount:
Type: Number
Default: 2
Description: How many copies of the service task to run.
Resources:
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: !Ref 'HealthCheckIntervalSeconds'
HealthCheckPath: !Ref 'HealthCheckPath'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: !Ref 'ServiceName'
Port: !Ref 'ContainerPort'
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'VPCId']]
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values: [!Ref 'Path']
ListenerArn:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicListener']]
Priority: 1
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref 'ServiceName'
RetentionInDays: 1
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ECSTaskExecutionRole']]
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
Image: !Ref 'ImageUrl'
PortMappings:
- ContainerPort: !Ref 'ContainerPort'
LogConfiguration:
LogDriver: 'awslogs'
Options:
awslogs-group: !Ref 'ServiceName'
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref 'ServiceName'
Service:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
ServiceName: !Ref 'ServiceName'
Cluster:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ClusterName']]
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DesiredCount: !Ref 'DesiredCount'
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ECSSecurityGroup']]
Subnets:
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicSubnetTwo']]
TaskDefinition: !Ref 'TaskDefinition'
LoadBalancers:
- ContainerName: !Ref 'ServiceName'
ContainerPort: !Ref 'ContainerPort'
TargetGroupArn: !Ref 'TargetGroup'

View File

@@ -0,0 +1,7 @@
# Overview
![RDS in private subnet](rds-in-private-subnet.svg)
# Companion Blog Post
TO DO

View File

@@ -0,0 +1,88 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A stack that creates an RDS instance and places it into two subnets
Parameters:
NetworkStackName:
Type: String
Description: The name of the networking stack that this stack will build upon.
DBInstanceClass:
Type: String
Description: The ID of the second subnet to place the RDS instance into.
Default: 'db.t2.micro'
DBName:
Type: String
Description: The name of the database that is created within the PostgreSQL instance.
DBUsername:
Type: String
Description: The master user name for the PostgreSQL instance.
Resources:
Secret:
Type: "AWS::SecretsManager::Secret"
Properties:
Name: !Ref 'DBUsername'
GenerateSecretString:
# This will generate a JSON object with the keys "username" and password.
SecretStringTemplate: !Join ['', ['{"username": "', !Ref 'DBUsername' ,'"}']]
GenerateStringKey: "password"
PasswordLength: 32
ExcludeCharacters: '"@/\'
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for the RDS instance
DBSubnetGroupName: DBSubnetGroup
SubnetIds:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PrivateSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PrivateSubnetTwo']]
PostgresInstance:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 20
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
DBInstanceClass: !Ref 'DBInstanceClass'
DBName: !Ref 'DBName'
DBSubnetGroupName: !Ref 'DBSubnetGroup'
Engine: postgres
EngineVersion: 11.5
MasterUsername: !Ref 'DBUsername'
MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref Secret, ':SecretString:password}}' ]]
PubliclyAccessible: false
VPCSecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'DBSecurityGroupId']]
SecretRDSInstanceAttachment:
Type: "AWS::SecretsManager::SecretTargetAttachment"
Properties:
SecretId: !Ref Secret
TargetId: !Ref PostgresInstance
TargetType: AWS::RDS::DBInstance
Outputs:
EndpointAddress:
Description: Address of the RDS endpoint.
Value: !GetAtt 'PostgresInstance.Endpoint.Address'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EndpointAddress' ] ]
EndpointPort:
Description: Port of the RDS endpoint.
Value: !GetAtt 'PostgresInstance.Endpoint.Port'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EndpointPort' ] ]
DBName:
Description: The name of the database that is created within the PostgreSQL instance.
Value: !Ref DBName
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'DBName' ] ]
Secret:
Description: Reference to the secret containing the password to the database.
Value: !Ref 'Secret'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Secret' ] ]

View File

@@ -0,0 +1,298 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A network stack for deploying containers in AWS ECS.
This stack creates a VPC with two public subnets and a loadbalancer to balance traffic between those subnets.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
PublicSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.1.0/24'
MapPublicIpOnLaunch: true
PublicSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.2.0/24'
MapPublicIpOnLaunch: true
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachement:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref 'VPC'
InternetGatewayId: !Ref 'InternetGateway'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref 'VPC'
PublicSubnetOneRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetOne
RouteTableId: !Ref PublicRouteTable
PublicSubnetTwoRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetTwo
RouteTableId: !Ref PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachement
Properties:
RouteTableId: !Ref 'PublicRouteTable'
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref 'InternetGateway'
PublicLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'VPC'
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
SecurityGroups: [!Ref 'PublicLoadBalancerSecurityGroup']
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref 'VPC'
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- PublicLoadBalancer
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS containers
VpcId: !Ref 'VPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
ECSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Rules which allow ECS to attach network interfaces to instances
# on your behalf in order for awsvpc networking mode to work right
- 'ec2:AttachNetworkInterface'
- 'ec2:CreateNetworkInterface'
- 'ec2:CreateNetworkInterfacePermission'
- 'ec2:DeleteNetworkInterface'
- 'ec2:DeleteNetworkInterfacePermission'
- 'ec2:Describe*'
- 'ec2:DetachNetworkInterface'
# Rules which allow ECS to update load balancers on your behalf
# with the information sabout how to send traffic to your containers
- 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
- 'elasticloadbalancing:DeregisterTargets'
- 'elasticloadbalancing:Describe*'
- 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
- 'elasticloadbalancing:RegisterTargets'
Resource: '*'
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: AmazonECSTaskExecutionRolePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Allow the ECS Tasks to download images from ECR
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
# Allow the ECS tasks to upload logs to CloudWatch
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
PrivateSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.101.0/24'
MapPublicIpOnLaunch: false
PrivateSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.102.0/24'
MapPublicIpOnLaunch: false
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the RDS instance
VpcId: !Ref 'VPC'
DBSecurityGroupIngressFromECS:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the ECS containers to the RDS instance
GroupId: !Ref 'DBSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
Outputs:
PrivateSubnetOne:
Description: Private subnet one
Value: !Ref 'PrivateSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetOne' ] ]
PrivateSubnetTwo:
Description: Private subnet two
Value: !Ref 'PrivateSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetTwo' ] ]
DBSecurityGroupId:
Description: ID of the security group that an RDS instance can be placed into.
Value: !Ref 'DBSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'DBSecurityGroupId' ] ]
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
ExternalUrl:
Description: The url of the external load balancer
Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']]
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ]
ECSRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSRole' ] ]
ECSTaskExecutionRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSTaskExecutionRole' ] ]
PublicListener:
Description: The ARN of the public load balancer's Listener
Value: !Ref PublicLoadBalancerListener
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicListener' ] ]
VPCId:
Description: The ID of the VPC that this stack is deployed in
Value: !Ref 'VPC'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ]
PublicSubnetOne:
Description: Public subnet one
Value: !Ref 'PublicSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ]
PublicSubnetTwo:
Description: Public subnet two
Value: !Ref 'PublicSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetTwo' ] ]
ECSSecurityGroup:
Description: A security group used to allow ECS containers to receive traffic
Value: !Ref 'ECSSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSSecurityGroup' ] ]

View File

@@ -0,0 +1 @@
<mxfile modified="2020-05-11T21:18:00.281Z" host="app.diagrams.net" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" etag="d22WT4Cb11cg7E6o-Mdr" version="13.0.9" type="device"><diagram id="Ht1M8jgEwFfnCIfOTk4-" name="Page-1">7VvZcuI4FP0aHpuyJK+PYUk6VemaTNHTyxNlsGI8MRYtiy1fP5KRwZbEkkBI00OKqljXspZ7z7mLDA3UHi/uaDgZfSERThvQihYN1GlACJDj8H9CslxJPM9eCWKaRLLTRtBLXrAUWlI6TSKc1zoyQlKWTOrCIckyPGQ1WUgpmde7PZG0PuskjLEm6A3DVJd+TyI2klLgBpsbn3ESj+TUPvRWN8Zh2VnuJB+FEZlXRKjbQG1KCFtdjRdtnArllXpZPXe75e56YRRn7JAHHuJp6+UXX9i3PB/cv7TDr5/+/SRHmYXpVG74cTpIk6FY73SQYSbXzpalQiYkyVihVKfFP3zOttVw+J22aDWhowjUtlcXAL0lxqgL1LZXFwB1eKDMD9QFVgRaqza8pcxvVRbIP6hFpixNMtxew8/iwpiGUcLN0iYpoVyWkYxrrzVi45S3AL+cjxKGe5NwKLQ659ThsieSMUkAAMu2VLx4hgNoIq7Hi1hwrRnOc7sZUzKdFFPecwoY7/ZzPJzShC37m849RskzlsvNi0a52AZE0PZ9YIslJGlakXeDW9R1uXyGKUs4SW7SJBaTMiKGDWUrxU9MDMu3l2TxQ9HqIEtuyTRPFOYjHMnlSEjyKfBiK9bBmkHc9WAyxowueRf5AHIk6aTXQaU7mVc47EvZqEJf4IICIYX7kK4jXg+/YRe/kAQzk22Eh3Dxcu9977wMnl37S/SrP/hUeoIK2749tq8Mu3yGzSbDg3kkl3oiCt3ctLyWb6aQxhcDq7ZSCLh1CgFLp5BrGyiEAut4/hiDla0HK5rMQoav0eoP4tL/L1o5wQdHq0U6ve3jv2af7eXPX2zwzwu+m5pSwyvbrmz7Hdh2VGBT2QbgmdlmjG2OxrYO36zVk0Sz7grzqIQz5BWawZwbt+27VUWCrVZSYaXYZD2UYs0T+EAXKlZxDFYptV9LN8AJ0g1juo6utfHV//15/k+tjc/u/4xkA57Gtm67JzwgprOEQ+G3dn3HGcQ/wPUhU6X1bq5Pj0YN6KZivwN+EYuLm8mE+8KQJZxZ5T1a3nwgYaRLue7CbIhpeYcvbT2cZl+j86hxobTjQzjA6SPJk2ItqDMgjJHxXiYOucX4Wmqux+BGUDPc7LSf8o31B+U+NCdw6/gOsrd7uROgBdp1tEDXagKkl+aBjhcPvhNc/P1wuRfKLmKmG46FGbJBPik0oqLkjpcY83B5YSBJ5P76sVy+GRvuu2Jj7SU22PhYYARbnPrXMH8+zJhqVETwVsyvKVEo2PO7VpV8nYTygVb2zggVGlCN0rGcNg89htjxVPydAU77Upswn6zU8ZQsxDrMuQ7FOZnSIV5lOi3eNOU8eJifBmk2UGKWryPNM4Qs770iFtSzdQ1gOIpxqVkBBxKTLEy7G2nVLDiLbsT7Q2HclAyfhSgdFO3SxgVsQsrKfpLI/MnbJF17Cy0psZ2WK2DKE84sWiOL24Iuf1QbP0WD59ey2VlUb3aW1dYjpgnXo4DW7jRxBZJdepSU5fuKMdvv84VOdwKmmsSUR8gUpzyczuqvWE2QkMM9irpqAz6lgIeBq+Wlq33K56ovRZWhgFJ2Il/PcVeq0MYqYLre6BHIda/IPQC5ewHpfBwgoXVCRCrVAArOj0hTPndJiHw7iIKPAxGyAhVEb0OQXX7npkSQbTWtyh84CEzcECIJX3eTZ1vblw81DuxcpdYfOAqWVys4LbL1hHRXpXKRBQh8femx/mrS5sSr/LITOlHVqhYmgZ4uBk3D8a5jH58wGl9vwf1I4NZiMcW9vx94x/uM+6dMHERdFCxolOt4gF0PFV8aeL9S1LXtmsWBZ6/DWNXoUDe5v8N1HmXySy8Rtpplb1ZvSOp3keJDwh9Qwh+0m77/tgjoAnNsOXXMc3bHsNf2PzrmGd8s6qi/nsH8LmcwR70yVc9gkGs3TXH1nY5hjGC7+Fp2kbAf5dz8ulLJ8tamkBWNZaVhLmMVP/o23+3ovns7zz/EVSPrrZWK6g8PLHTPXZvY1hn8tP4C9MKoc/pjoNfyZ1f8Oyz3OQt/7KBekEOVBE0L2bYPPcd3oeW9MQdybGVYHh38wHUc4PHQgdR3PqdKidz6bhTq7O2/L4VS64ojUyje3PwCZ9V98zsm1P0P</diagram></mxfile>

View File

@@ -0,0 +1,164 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a service on AWS Fargate, hosted in two public subnets and accessible via a public load balancer.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Parameters:
NetworkStackName:
Type: String
Description: The name of the networking stack that
these resources are put into.
DatabaseStackName:
Type: String
Description: The name of the database stack with the database this service should connect to.
ServiceName:
Type: String
Description: A human-readable name for the service.
HealthCheckPath:
Type: String
Default: /health
Description: Path to perform the healthcheck on each instance.
HealthCheckIntervalSeconds:
Type: Number
Default: 5
Description: Number of seconds to wait between each health check.
ImageUrl:
Type: String
Description: The url of a docker image that will handle incoming traffic.
ContainerPort:
Type: Number
Default: 80
Description: The port number the application inside the docker container
is binding to.
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU.
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container.
Path:
Type: String
Default: "*"
Description: A path on the public load balancer that this service
should be connected to.
DesiredCount:
Type: Number
Default: 2
Description: How many copies of the service task to run.
Resources:
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: !Ref 'HealthCheckIntervalSeconds'
HealthCheckPath: !Ref 'HealthCheckPath'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: !Ref 'ServiceName'
Port: !Ref 'ContainerPort'
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'VPCId']]
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values: [!Ref 'Path']
ListenerArn:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicListener']]
Priority: 1
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref 'ServiceName'
RetentionInDays: 1
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ECSTaskExecutionRole']]
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
Image: !Ref 'ImageUrl'
Environment:
- Name: SPRING_DATASOURCE_URL
Value: !Join
- ''
- - 'jdbc:postgresql://'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'EndpointAddress']]
- ':'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'EndpointPort']]
- '/'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'DBName']]
- Name: SPRING_DATASOURCE_USERNAME
Value: !Join
- ''
- - '{{resolve:secretsmanager:'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'Secret']]
- ':SecretString:username}}'
- Name: SPRING_DATASOURCE_PASSWORD
Value: !Join
- ''
- - '{{resolve:secretsmanager:'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'Secret']]
- ':SecretString:password}}'
PortMappings:
- ContainerPort: !Ref 'ContainerPort'
LogConfiguration:
LogDriver: 'awslogs'
Options:
awslogs-group: !Ref 'ServiceName'
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref 'ServiceName'
Service:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
ServiceName: !Ref 'ServiceName'
Cluster:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ClusterName']]
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DesiredCount: !Ref 'DesiredCount'
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ECSSecurityGroup']]
Subnets:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicSubnetTwo']]
TaskDefinition: !Ref 'TaskDefinition'
LoadBalancers:
- ContainerName: !Ref 'ServiceName'
ContainerPort: !Ref 'ContainerPort'
TargetGroupArn: !Ref 'TargetGroup'

View File

@@ -10,7 +10,7 @@ build_gradle_module() {
echo "+++"
cd $MODULE_PATH && {
chmod +x gradlew
./gradlew clean build --info --stacktrace
./gradlew clean build
if [ $? -ne 0 ]
then
echo ""
@@ -28,9 +28,38 @@ build_gradle_module() {
}
}
chmod +x gradlew
build_maven_module() {
MODULE_PATH=$1
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH"
echo "+++"
cd $MODULE_PATH && {
chmod +x mvnw
./mvnw clean package
if [ $? -ne 0 ]
then
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH FAILED"
echo "+++"
exit 1
else
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH SUCCESSFUL"
echo "+++"
fi
cd $MAIN_DIR
}
}
build_maven_module "spring-boot/dependency-injection"
build_maven_module "spring-boot/spring-boot-openapi"
build_maven_module "spring-boot/data-migration/liquibase"
build_gradle_module "spring-boot/boundaries"
build_gradle_module "spring-boot/argumentresolver"
build_gradle_module "spring-data/spring-data-jdbc-converter"
build_gradle_module "solid"
build_gradle_module "spring-boot/data-migration/flyway"
build_gradle_module "reactive"
@@ -55,6 +84,8 @@ build_gradle_module "spring-boot/startup"
build_gradle_module "spring-boot/static"
build_gradle_module "spring-boot/validation"
build_gradle_module "spring-boot/profiles"
build_gradle_module "spring-boot/password-encoding"
build_gradle_module "spring-boot/testcontainers"
build_gradle_module "spring-cloud/feign-with-spring-data-rest"
build_gradle_module "spring-cloud/sleuth-downstream-service"
build_gradle_module "spring-cloud/sleuth-upstream-service"

View File

@@ -0,0 +1,13 @@
package io.reflectoring.solid.isp;
class AdaptedBurgerOrder implements IAdapterOrderForBurger {
private final IOrder burgerOrder;
public AdaptedBurgerOrder(IOrder burgerOrder){
this.burgerOrder = burgerOrder;
}
@Override
public void orderBurger(int quantity) {
burgerOrder.orderBurger(quantity);
}
}

View File

@@ -0,0 +1,13 @@
package io.reflectoring.solid.isp;
class AdaptedFriesOrder implements IAdapterOrderForFries {
private final IOrder friesOrder;
public AdaptedFriesOrder(IOrder friesOrder){
this.friesOrder = friesOrder;
}
@Override
public void orderFries(int quantity) {
friesOrder.orderFries(quantity);
}
}

View File

@@ -0,0 +1,13 @@
package io.reflectoring.solid.isp;
class BurgerOrder implements IOrder {
@Override
public void orderBurger(int quantity) {
}
@Override
public void orderFries(int fries) {
throw new UnsupportedOperationException("No fries in Burger only order");
}
}

View File

@@ -0,0 +1,5 @@
package io.reflectoring.solid.isp;
interface IAdapterOrderForBurger {
void orderBurger(int quantity);
}

View File

@@ -0,0 +1,5 @@
package io.reflectoring.solid.isp;
interface IAdapterOrderForFries {
void orderFries(int quantity);
}

View File

@@ -0,0 +1,5 @@
package io.reflectoring.solid.isp;
interface IBurgerOrder{
void orderBurger(int quantity);
}

View File

@@ -0,0 +1,5 @@
package io.reflectoring.solid.isp;
interface IFriesOrder {
void orderFries(int quantity);
}

View File

@@ -0,0 +1,6 @@
package io.reflectoring.solid.isp;
interface IOrder {
void orderBurger(int quantity);
void orderFries(int fries);
}

View File

@@ -4,7 +4,7 @@ package io.reflectoring.argumentresolver;
import lombok.Value;
@Value
class Repository {
class GitRepository {
private final Long id;
private final String slug;

View File

@@ -1,7 +1,6 @@
package io.reflectoring.argumentresolver;
import java.util.Optional;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -11,13 +10,13 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@RequiredArgsConstructor
class RepositoryArgumentResolver implements HandlerMethodArgumentResolver {
class GitRepositoryArgumentResolver implements HandlerMethodArgumentResolver {
private final RepositoryFinder repositoryFinder;
private final GitRepositoryFinder gitRepositoryFinder;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameter().getType() == Repository.class;
return parameter.getParameter().getType() == GitRepository.class;
}
@Override
@@ -33,12 +32,7 @@ class RepositoryArgumentResolver implements HandlerMethodArgumentResolver {
.substring(0, requestPath.indexOf("/", 1))
.replaceAll("^/", "");
Optional<Repository> repository = repositoryFinder.findBySlug(slug);
if (repository.isEmpty()) {
throw new NotFoundException();
}
return repository.get();
return gitRepositoryFinder.findBySlug(slug)
.orElseThrow(NotFoundException::new);
}
}

View File

@@ -8,13 +8,13 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Component
@RequiredArgsConstructor
class RepositoryArgumentResolverConfiguration implements WebMvcConfigurer {
class GitRepositoryArgumentResolverConfiguration implements WebMvcConfigurer {
private final RepositoryFinder repositoryFinder;
private final GitRepositoryFinder gitRepositoryFinder;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new RepositoryArgumentResolver(repositoryFinder));
resolvers.add(new GitRepositoryArgumentResolver(gitRepositoryFinder));
}
}

View File

@@ -0,0 +1,9 @@
package io.reflectoring.argumentresolver;
import java.util.Optional;
public interface GitRepositoryFinder {
Optional<GitRepository> findBySlug(String slug);
}

View File

@@ -3,7 +3,7 @@ package io.reflectoring.argumentresolver;
import lombok.Value;
@Value
class RepositoryId {
class GitRepositoryId {
private final long value;

View File

@@ -0,0 +1,13 @@
package io.reflectoring.argumentresolver;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
class GitRepositoryIdConverter implements Converter<String, GitRepositoryId> {
@Override
public GitRepositoryId convert(String source) {
return new GitRepositoryId(Long.parseLong(source));
}
}

View File

@@ -0,0 +1,17 @@
package io.reflectoring.argumentresolver;
import lombok.Value;
@Value
class GitRepositoryIdWithValueOf {
private final long value;
/**
* If we have a static valueOf(String) method, we don't need a Converter!
*/
public static GitRepositoryIdWithValueOf valueOf(String value){
return new GitRepositoryIdWithValueOf(Long.parseLong(value));
}
}

View File

@@ -1,9 +0,0 @@
package io.reflectoring.argumentresolver;
import java.util.Optional;
public interface RepositoryFinder {
Optional<Repository> findBySlug(String slug);
}

View File

@@ -1,13 +0,0 @@
package io.reflectoring.argumentresolver;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
class RepositoryIdConverter implements Converter<String, RepositoryId> {
@Override
public RepositoryId convert(String source) {
return new RepositoryId(Long.parseLong(source));
}
}

View File

@@ -11,32 +11,32 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(controllers = RepositoryArgumentResolverTestController.class)
class RepositoryArgumentResolverTest {
@WebMvcTest(controllers = GitRepositoryArgumentResolverTestController.class)
class GitRepositoryArgumentResolverTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private RepositoryFinder repositoryFinder;
private GitRepositoryFinder gitRepositoryFinder;
@Test
void resolvesSiteSuccessfully() throws Exception {
given(repositoryFinder.findBySlug("my-repo"))
.willReturn(Optional.of(new Repository(1L, "my-repo")));
given(gitRepositoryFinder.findBySlug("my-repo"))
.willReturn(Optional.of(new GitRepository(1L, "my-repo")));
mockMvc.perform(get("/my-repo/listContributors"))
mockMvc.perform(get("/my-repo/contributors"))
.andExpect(status().isOk());
}
@Test
void notFoundOnUnknownSlug() throws Exception {
given(repositoryFinder.findBySlug("unknownSlug"))
given(gitRepositoryFinder.findBySlug("unknownSlug"))
.willReturn(Optional.empty());
mockMvc.perform(get("/unknownSlug/listContributors"))
mockMvc.perform(get("/unknownSlug/contributors"))
.andExpect(status().isNotFound());
}

View File

@@ -8,11 +8,11 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/{repositorySlug}")
class RepositoryArgumentResolverTestController {
class GitRepositoryArgumentResolverTestController {
@GetMapping("/listContributors")
String listContributors(Repository repository) {
assertThat(repository.getId()).isEqualTo(1L);
@GetMapping("/contributors")
String listContributors(GitRepository gitRepository) {
assertThat(gitRepository.getId()).isEqualTo(1L);
return "test";
}

View File

@@ -9,14 +9,14 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(controllers = RepositoryIdConverterTestController.class)
class RepositoryIdConverterTest {
@WebMvcTest(controllers = GitRepositoryIdConverterTestController.class)
class GitRepositoryIdConverterTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private RepositoryFinder repositoryFinder;
private GitRepositoryFinder gitRepositoryFinder;
@Test
void resolvesRepositoryId() throws Exception {

View File

@@ -7,11 +7,11 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RepositoryIdConverterTestController {
class GitRepositoryIdConverterTestController {
@GetMapping("/repositories/{repositoryId}")
String getRepository(@PathVariable("repositoryId") RepositoryId repositoryId) {
assertThat(repositoryId).isNotNull();
String getRepository(@PathVariable("repositoryId") GitRepositoryId gitRepositoryId) {
assertThat(gitRepositoryId).isNotNull();
return "test";
}

View File

@@ -0,0 +1,27 @@
package io.reflectoring.argumentresolver;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(controllers = GitRepositoryIdWithValueOfTestController.class)
class GitRepositoryIdWithValueOfTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private GitRepositoryFinder gitRepositoryFinder;
@Test
void resolvesRepositoryId() throws Exception {
mockMvc.perform(get("/repositories/42"))
.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,18 @@
package io.reflectoring.argumentresolver;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
class GitRepositoryIdWithValueOfTestController {
@GetMapping("/repositories/{repositoryId}")
String getRepository(@PathVariable("repositoryId") GitRepositoryIdWithValueOf gitRepositoryId) {
assertThat(gitRepositoryId).isNotNull();
return "test";
}
}

32
spring-boot/boundaries/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,30 @@
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// ArchUnit
testImplementation 'com.tngtech.archunit:archunit-junit5:0.13.1'
testImplementation 'org.reflections:reflections:0.9.10'
}
test {
useJUnitPlatform()
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
spring-boot/boundaries/gradlew vendored Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
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
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
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"
which java >/dev/null 2>&1 || 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
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
spring-boot/boundaries/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@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=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
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 init
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
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
: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 %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'boundaries'

View File

@@ -0,0 +1,13 @@
package io.reflectoring.boundaries;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BoundariesApplication {
public static void main(String[] args) {
SpringApplication.run(BoundariesApplication.class, args);
}
}

View File

@@ -0,0 +1,18 @@
package io.reflectoring.boundaries;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotate a package-info.java file with this annotation to mark it as internal, i.e. no
* classes outside of that package may depend on any classes within that package.
*/
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalPackage {
}

View File

@@ -0,0 +1,11 @@
package io.reflectoring.boundaries.billing.api;
import lombok.Value;
@Value
public class Invoice {
private Long userId;
private double amount;
}

View File

@@ -0,0 +1,9 @@
package io.reflectoring.boundaries.billing.api;
import java.time.LocalDate;
public interface InvoiceCalculator {
Invoice calculateInvoice(Long userId, LocalDate fromDate, LocalDate toDate);
}

View File

@@ -0,0 +1,10 @@
package io.reflectoring.boundaries.billing.internal;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
class BillingConfiguration {
}

View File

@@ -0,0 +1,27 @@
package io.reflectoring.boundaries.billing.internal;
import io.reflectoring.boundaries.billing.api.Invoice;
import io.reflectoring.boundaries.billing.api.InvoiceCalculator;
import io.reflectoring.boundaries.billing.internal.database.api.LineItem;
import io.reflectoring.boundaries.billing.internal.database.api.ReadLineItems;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
class BillingService implements InvoiceCalculator {
private final ReadLineItems readLineItems;
@Override
public Invoice calculateInvoice(Long userId, LocalDate fromDate, LocalDate toDate) {
List<LineItem> items = readLineItems.getLineItemsForUser(userId, fromDate, toDate);
double sum = items.stream()
.mapToDouble(LineItem::getAmount)
.sum();
return new Invoice(userId, sum);
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.boundaries.billing.internal.batchjob.internal;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
@ComponentScan
class BillingBatchJobConfiguration {
}

View File

@@ -0,0 +1,42 @@
package io.reflectoring.boundaries.billing.internal.batchjob.internal;
import io.reflectoring.boundaries.billing.internal.database.api.WriteLineItems;
import io.reflectoring.boundaries.billing.internal.database.api.LineItem;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
class LoadInvoiceDataBatchJob {
private static final Logger logger = LoggerFactory.getLogger(LoadInvoiceDataBatchJob.class);
private static final Random random = new Random();
private final WriteLineItems writeLineItems;
@Scheduled(fixedRate = 5000)
void loadDataFromBillingSystem() {
List<LineItem> items = getLineItemsFromBillingSystem();
writeLineItems.saveLineItems(items);
}
private List<LineItem> getLineItemsFromBillingSystem() {
// imagine this list is loaded from a 3rd party system
return Arrays.asList(
lineItem("bread"),
lineItem("butter"),
lineItem("toilet paper")
);
}
private LineItem lineItem(String itemName) {
return new LineItem(42L, itemName, 42.42d, LocalDate.now().minusDays(random.nextInt(10)));
}
}

View File

@@ -0,0 +1,4 @@
@InternalPackage
package io.reflectoring.boundaries.billing.internal.batchjob.internal;
import io.reflectoring.boundaries.InternalPackage;

View File

@@ -0,0 +1,14 @@
package io.reflectoring.boundaries.billing.internal.database.api;
import java.time.LocalDate;
import lombok.Value;
@Value
public class LineItem {
private final Long userId;
private final String name;
private final double amount;
private final LocalDate date;
}

View File

@@ -0,0 +1,10 @@
package io.reflectoring.boundaries.billing.internal.database.api;
import java.time.LocalDate;
import java.util.List;
public interface ReadLineItems {
List<LineItem> getLineItemsForUser(Long userId, LocalDate startDate, LocalDate endDate);
}

View File

@@ -0,0 +1,9 @@
package io.reflectoring.boundaries.billing.internal.database.api;
import java.util.List;
public interface WriteLineItems {
void saveLineItems(List<LineItem> lineItems);
}

View File

@@ -0,0 +1,31 @@
package io.reflectoring.boundaries.billing.internal.database.internal;
import io.reflectoring.boundaries.billing.internal.database.api.LineItem;
import io.reflectoring.boundaries.billing.internal.database.api.ReadLineItems;
import io.reflectoring.boundaries.billing.internal.database.api.WriteLineItems;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
class BillingDatabase implements WriteLineItems, ReadLineItems {
private final LineItemRepository lineItemRepository;
@Override
public void saveLineItems(List<LineItem> lineItems) {
for (LineItem lineItem : lineItems) {
lineItemRepository.save(LineItemJpaEntity.fromDomainObject(lineItem));
}
}
@Override
public List<LineItem> getLineItemsForUser(Long userId, LocalDate startDate, LocalDate endDate) {
return lineItemRepository.findByUserIdAndDateBetween(userId, startDate, endDate).stream()
.map(LineItemJpaEntity::toDomainObject)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.boundaries.billing.internal.database.internal;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories
@ComponentScan
class BillingDatabaseConfiguration {
}

View File

@@ -0,0 +1,43 @@
package io.reflectoring.boundaries.billing.internal.database.internal;
import io.reflectoring.boundaries.billing.internal.database.api.LineItem;
import java.time.LocalDate;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
class LineItemJpaEntity {
@Id
@GeneratedValue
private Long userId;
private String name;
private Double amount;
private LocalDate date;
static LineItemJpaEntity fromDomainObject(LineItem lineItem) {
return new LineItemJpaEntity(
lineItem.getUserId(),
lineItem.getName(),
lineItem.getAmount(),
lineItem.getDate()
);
}
LineItem toDomainObject(){
return new LineItem(
this.userId,
this.name,
this.amount,
this.date
);
}
}

View File

@@ -0,0 +1,11 @@
package io.reflectoring.boundaries.billing.internal.database.internal;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
interface LineItemRepository extends CrudRepository<LineItemJpaEntity, Long> {
List<LineItemJpaEntity> findByUserIdAndDateBetween(Long userId, LocalDate startDate, LocalDate endDate);
}

View File

@@ -0,0 +1,4 @@
@InternalPackage
package io.reflectoring.boundaries.billing.internal.database.internal;
import io.reflectoring.boundaries.InternalPackage;

View File

@@ -0,0 +1,4 @@
@InternalPackage
package io.reflectoring.boundaries.billing.internal;
import io.reflectoring.boundaries.InternalPackage;

View File

@@ -0,0 +1,13 @@
package io.reflectoring.boundaries;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class BoundariesApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,63 @@
package io.reflectoring.boundaries;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.assertj.core.api.Assertions.assertThat;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.reflections.Reflections;
/**
* Evaluates {@link InternalPackage} annotations and checks that those packages are not accessed from the outside.
*/
class InternalPackageTests {
private static final String BASE_PACKAGE = "io.reflectoring";
private final JavaClasses analyzedClasses = new ClassFileImporter().importPackages(BASE_PACKAGE);
@Test
void internalPackagesAreNotAccessedFromOutside() throws IOException {
// so that the test will break when the base package is re-named
assertPackageExists(BASE_PACKAGE);
List<String> internalPackages = internalPackages(BASE_PACKAGE);
for (String internalPackage : internalPackages) {
assertPackageIsNotAccessedFromOutside(internalPackage);
}
}
/**
* Finds all packages annotated with @{@link InternalPackage}.
*/
private List<String> internalPackages(String basePackage) {
Reflections reflections = new Reflections(basePackage);
return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()
.map(c -> c.getPackage().getName())
.collect(Collectors.toList());
}
void assertPackageIsNotAccessedFromOutside(String internalPackage) {
noClasses().that().resideOutsideOfPackage(packageMatcher(internalPackage))
.should().dependOnClassesThat().resideInAPackage(packageMatcher(internalPackage))
.check(analyzedClasses);
}
void assertPackageExists(String packageName) {
assertThat(analyzedClasses.containPackage(packageName))
.as("package %s exists", packageName)
.isTrue();
}
private String packageMatcher(String fullyQualifiedPackage) {
return fullyQualifiedPackage + "..";
}
}

View File

@@ -1,5 +1,5 @@
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'java'
}
@@ -7,7 +7,7 @@ apply plugin: 'io.spring.dependency-management'
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
sourceCompatibility = '13'
repositories {
mavenCentral()
@@ -23,6 +23,7 @@ dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.4.0')
testImplementation('org.springframework.boot:spring-boot-starter-test'){
exclude group: 'junit', module: 'junit'
exclude group: 'org.junit.vintage'
}
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
spring-boot/configuration/gradlew vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,14 @@
package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,26 @@
package io.reflectoring.validation;
import io.reflectoring.validation.thirdparty.ThirdPartyComponentProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
@Configuration
@EnableConfigurationProperties(AppProperties.class)
class AppConfiguration {
@Bean
public static ReportEmailAddressValidator configurationPropertiesValidator() {
return new ReportEmailAddressValidator();
}
@Bean
@Validated
@ConfigurationProperties(prefix = "app.third-party.properties")
public ThirdPartyComponentProperties thirdPartyComponentProperties() {
return new ThirdPartyComponentProperties();
}
}

View File

@@ -0,0 +1,53 @@
package io.reflectoring.validation;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
@Validated
@ConfigurationProperties(prefix = "app.properties")
class AppProperties implements Validator {
@NotBlank
private String name;
@Valid
private ReportProperties report;
private static final String APP_BASE_NAME = "Application";
public boolean supports(Class clazz) {
return AppProperties.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
AppProperties appProperties = (AppProperties) target;
if (!appProperties.getName().endsWith(APP_BASE_NAME)) {
errors.rejectValue("name", "field.name.malformed",
new Object[]{APP_BASE_NAME},
"The application name must contain [" + APP_BASE_NAME + "] base name");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ReportProperties getReport() {
return report;
}
public void setReport(ReportProperties report) {
this.report = report;
}
}

View File

@@ -0,0 +1,27 @@
package io.reflectoring.validation;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
class ReportEmailAddressValidator implements Validator {
private static final String EMAIL_DOMAIN = "@analysisapp.com";
public boolean supports(Class clazz) {
return ReportProperties.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "emailAddress", "field.required");
ReportProperties reportProperties = (ReportProperties) target;
if (!reportProperties.getEmailAddress().endsWith(EMAIL_DOMAIN)) {
errors.rejectValue("emailAddress", "field.domain.required",
new Object[]{EMAIL_DOMAIN},
"The email address must contain [" + EMAIL_DOMAIN + "] domain");
}
}
}

View File

@@ -0,0 +1,51 @@
package io.reflectoring.validation;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
class ReportProperties {
private Boolean sendEmails = Boolean.FALSE;
private ReportType type = ReportType.HTML;
@Min(value = 7)
@Max(value = 30)
private Integer intervalInDays;
@Email
private String emailAddress;
public Boolean getSendEmails() {
return sendEmails;
}
public void setSendEmails(Boolean sendEmails) {
this.sendEmails = sendEmails;
}
public ReportType getType() {
return type;
}
public void setType(ReportType type) {
this.type = type;
}
public Integer getIntervalInDays() {
return intervalInDays;
}
public void setIntervalInDays(Integer intervalInDays) {
this.intervalInDays = intervalInDays;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
}

View File

@@ -0,0 +1,5 @@
package io.reflectoring.validation;
enum ReportType {
HTML, PLAIN_TEXT
}

View File

@@ -0,0 +1,21 @@
package io.reflectoring.validation.thirdparty;
import javax.validation.constraints.NotBlank;
/**
* We assume that this bean comes from another jar file
*/
public class ThirdPartyComponentProperties {
@NotBlank
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,7 @@
app.properties.name=Analysis Application
app.properties.report.send-emails=true
app.properties.report.type=PLAIN_TEXT
app.properties.report.interval-in-days=14
app.properties.report.email-address=manager@analysisapp.com
# third-party component properties
app.third-party.properties.name=Third Party!

View File

@@ -1,5 +1,4 @@
myapp.mail.enabled=true
myapp
myapp.mail.pauseBetweenMails=5s
myapp.mail.maxAttachmentSize=1MB
myapp.mail.smtpServers[0]=server1

View File

@@ -1,37 +1,36 @@
package io.reflectoring.configuration.mail;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.unit.DataSize;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = {
"myapp.mail.enabled=asd",
"myapp.mail.defaultSubject=hello",
"myapp.mail.pauseBetweenMails=5s",
"myapp.mail.maxAttachmentSize=1MB",
"myapp.mail.smtpServers[0]=server1",
"myapp.mail.smtpServers[1]=server2",
"myapp.mail.maxAttachmentWeight=5kg"
"myapp.mail.enabled=asd",
"myapp.mail.defaultSubject=hello",
"myapp.mail.pauseBetweenMails=5s",
"myapp.mail.maxAttachmentSize=1MB",
"myapp.mail.smtpServers[0]=server1",
"myapp.mail.smtpServers[1]=server2",
"myapp.mail.maxAttachmentWeight=5kg"
})
class MailModuleTestWithAllProperties {
@Autowired(required = false)
private MailModuleProperties mailModuleProperties;
@Autowired(required = false)
private MailModuleProperties mailModuleProperties;
@Test
void propertiesAreLoaded() {
assertThat(mailModuleProperties).isNotNull();
assertThat(mailModuleProperties.getDefaultSubject()).isEqualTo("hello");
assertThat(mailModuleProperties.getEnabled()).isTrue();
assertThat(mailModuleProperties.getPauseBetweenMails()).isEqualByComparingTo(Duration.ofSeconds(5));
assertThat(mailModuleProperties.getMaxAttachmentSize()).isEqualByComparingTo(DataSize.ofMegabytes(1));
assertThat(mailModuleProperties.getSmtpServers()).hasSize(2);
assertThat(mailModuleProperties.getMaxAttachmentWeight().getGrams()).isEqualTo(5000L);
}
@Test
void propertiesAreLoaded() {
assertThat(mailModuleProperties).isNotNull();
assertThat(mailModuleProperties.getDefaultSubject()).isEqualTo("hello");
assertThat(mailModuleProperties.getEnabled()).isTrue();
assertThat(mailModuleProperties.getPauseBetweenMails()).isEqualByComparingTo(Duration.ofSeconds(5));
assertThat(mailModuleProperties.getMaxAttachmentSize()).isEqualByComparingTo(DataSize.ofMegabytes(1));
assertThat(mailModuleProperties.getSmtpServers()).hasSize(2);
assertThat(mailModuleProperties.getMaxAttachmentWeight().getGrams()).isEqualTo(5000L);
}
}

View File

@@ -0,0 +1,127 @@
package io.reflectoring.validation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
import org.springframework.boot.context.properties.bind.validation.BindValidationException;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.StandardEnvironment;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* We create Spring Application dynamically to catch and test application context startup exceptions
*/
class PropertiesInvalidInputTest {
SpringApplication application;
Properties properties;
@BeforeEach
void setup() {
// create Spring Application dynamically
application = new SpringApplication(ValidationApplication.class);
// setting test properties for our Spring Application
properties = new Properties();
ConfigurableEnvironment environment = new StandardEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new PropertiesPropertySource("application-test", properties));
application.setEnvironment(environment);
}
@Test
void whenGivenNameEmpty_thenNotBlankValidationFails() {
properties.put("app.properties.name", "");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("Field error in object 'app.properties' on field 'name'")
.hasStackTraceContaining("[must not be blank]");
}
@Test
void whenGivenNameDoesNotContainBaseName_thenCustomAppPropertiesValidatorFails() {
properties.put("app.properties.name", "My App");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("Field error in object 'app.properties' on field 'name'")
.hasStackTraceContaining("[The application name must contain [Application] base name]");
}
@Test
void whenGivenReportIntervalInDaysMoreThan30_thenMaxValidationFails() {
properties.put("app.properties.report.interval-in-days", "31");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("rejected value [31]");
}
@Test
void whenGivenReportIntervalInDaysLessThan7_thenMinValidationFails() {
properties.put("app.properties.report.interval-in-days", "6");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("rejected value [6]");
}
@Test
void whenGivenReportEmailAddressIsNotWellFormed_thenEmailValidationFails() {
properties.put("app.properties.report.email-address", "manager.analysisapp.com");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("rejected value [manager.analysisapp.com]");
}
@Test
void whenGivenReportEmailAddressDoesNotContainAnalysisappDomain_thenCustomEmailValidatorFails() {
properties.put("app.properties.report.email-address", "manager@notanalysisapp.com");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("Field error in object 'app.properties.report' on field 'emailAddress'")
.hasStackTraceContaining("[The email address must contain [@analysisapp.com] domain]");
}
@Test
void whenGivenThirdPartyComponentNameIsEmpty_thenNotBlankValidationFails() {
properties.put("app.third-party.properties.name", "");
assertThatThrownBy(application::run)
.isInstanceOf(ConfigurationPropertiesBindException.class)
.hasRootCauseInstanceOf(BindValidationException.class)
.hasStackTraceContaining("Field error in object 'app.third-party.properties' on field 'name'")
.hasStackTraceContaining("[must not be blank]");
}
}

View File

@@ -0,0 +1,43 @@
package io.reflectoring.validation;
import io.reflectoring.validation.thirdparty.ThirdPartyComponentProperties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = {
"app.properties.name=My Test Application",
"app.properties.report.send-emails=true",
"app.properties.report.type=PLAIN_TEXT",
"app.properties.report.interval-in-days=14",
"app.properties.report.email-address=manager@analysisapp.com",
"app.third-party.properties.name=Third Party!"
}, classes = {AppConfiguration.class})
class PropertiesValidInputTest {
@Autowired
AppProperties appProperties;
@Autowired
ThirdPartyComponentProperties thirdPartyComponentProperties;
@Test
void appPropertiesAreLoaded() {
assertThat(appProperties).isNotNull();
assertThat(appProperties.getName()).isEqualTo("My Test Application");
assertThat(appProperties.getReport()).isNotNull();
assertThat(appProperties.getReport().getSendEmails()).isTrue();
assertThat(appProperties.getReport().getType()).isEqualTo(ReportType.PLAIN_TEXT);
assertThat(appProperties.getReport().getIntervalInDays()).isEqualTo(14);
assertThat(appProperties.getReport().getEmailAddress()).isEqualTo("manager@analysisapp.com");
}
@Test
void thirdPartyComponentPropertiesAreLoaded() {
assertThat(thirdPartyComponentProperties).isNotNull();
assertThat(thirdPartyComponentProperties.getName()).isEqualTo("Third Party!");
}
}

View File

@@ -0,0 +1,15 @@
package io.reflectoring.validation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication
@PropertySource("classpath:application-validation.properties")
public class ValidationApplication {
public static void main(String[] args) {
SpringApplication.run(ValidationApplication.class, args);
}
}

View File

@@ -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/

View File

@@ -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();
}
}

Binary file not shown.

View File

@@ -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

View File

@@ -0,0 +1,122 @@
# liquibase-demo
This project demonstrates on how to set-up liquibase in a Spring Boot application.
When this project is run, it executes the changelogs and creates a `user_details` table and populates it with some test data.
It also creates an index and couple of constraints on `user_details` table.
The project also demonstrates the following.
* Organising liquibase change logs [using master changelog](src/main/resources/db/changelog/db.changelog-master.yaml)
* It demonstrates how to write a [change log in YAML format](src/main/resources/db/changelog/db.changelog-yaml-example.yaml)
* It demonstrates how to write a [change log in JSON format](src/main/resources/db/changelog/db.changelog-json-example.json)
* It demonstrates how to write a [change log in XML format](src/main/resources/db/changelog/db.changelog-xml-example.xml)
* It demonstrates how to write a [change log in SQL format](src/main/resources/db/changelog/db.changelog-sql-example.sql)
* Enable `INFO` [logs for liquibase](src/main/resources/application.yaml#L10-L12).
* It demonstrates testing of changes done by liquibase to the [schema using Junit test](src/test/java/io/reflectoring/liquibase/adapter/datastore/UserRepositoryDockerProfileTest.java)
* It demonstrates use of liquibase [change log parameters](src/main/resources/db/changelog/db.changelog-yaml-example.yaml#L21-L33)
* IT demonstrates use of liquibase [context](src/main/resources/db/changelog/db.changelog-xml-example.xml#L5)
## How to Run the project and tests
### Have Docker and Docker Compose in your machine?
#### Run application
- Clone this repository
```
git clone https://github.com/thombergs/code-examples/spring-boot/data-migration/liquibase
```
- Move to the directory `code-examples/spring-boot/data-migration/liquibase`
- Run the docker compose file `infra-local.yaml` in `src/docker`. This starts the postgres database needed to run the application.
```
docker-compose -f ./src/main/docker/infra-local.yaml up -d
```
- Now run the spring boot application as follows.
```
mvnw spring-boot:run -Dspring-boot.run.profiles=docker
```
The application should start and listening on port `8080`.
Open the url (http://localhost:8080/liquibase/users/100000000) in the browser. You should see a response as below
```json
{
"id": 100000000,
"userName": "testUser",
"firstName": "testFirstName",
"lastName": "testLastName"
}
```
You can login to the database and check the creation of `user_details` table along with some test data in it.
```yaml
#Use below credentials to login to the database
databaseHost: localhost
jdbcUrl: jdbc:postgresql://localhost:5432/liquibasedemo?current_schema=public
port: 5432
username: demouser
password: demopassword
```
Also, have a look at databasechangelog and databasechangeloglock tables.
#### Run Test
To run integration test, you don't need to run the docker compose file. Just run the test as follows:
```yaml
mvn test -Dspring.profiles.active=docker
```
The test uses [TestContainers](https://www.testcontainers.org/) to spin a postgres database, which is used during the integration test.
### Don't have docker?
You can use h2 profile to run the app and use h2 database.
- Clone this repository
```
git clone https://github.com/thombergs/code-examples/spring-boot/data-migration/liquibase
```
- Move to the directory `code-examples/spring-boot/data-migration/liquibase`
- Now run the spring boot application as follows.
```
mvnw spring-boot:run -Dspring-boot.run.profiles=h2
```
The application should start and listening on port `8080`.
Open the url (http://localhost:8080/liquibase/users/100000000) in the browser. You should see a response as below
```json
{
"id": 100000000,
"userName": "testUser",
"firstName": "testFirstName",
"lastName": "testLastName"
}
```
Login to h2 database console using [url](http://localhost:8080/liquibase/h2-console)
```yaml
JDBC URL: jdbc:h2:mem:liquibasedemo
username: demouser
password: demopassword
```
Check the creation of `user_details` table along with some test data in it.
Also, have a look at databasechangelog and databasechangeloglock tables.
#### Run Test
To run integration test
```yaml
mvn test -Dspring.profiles.active=h2
```

View File

@@ -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 "$@"

Some files were not shown because too many files have changed in this diff Show More