├── .gitignore ├── .whitesource ├── ExploringMapStruct ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Exploring-Mapstruct.postman_collection.json ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── beyondjava │ │ │ ├── ExploringMapStructApplication.java │ │ │ ├── business │ │ │ ├── employees │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeController.java │ │ │ │ ├── EmployeeDto.java │ │ │ │ ├── EmployeeMapper.java │ │ │ │ ├── EmployeeRepository.java │ │ │ │ └── EmployeeService.java │ │ │ └── projects │ │ │ │ ├── Project.java │ │ │ │ ├── ProjectController.java │ │ │ │ ├── ProjectDto.java │ │ │ │ ├── ProjectMapper.java │ │ │ │ ├── ProjectRepository.java │ │ │ │ └── ProjectService.java │ │ │ └── tech │ │ │ ├── errorHandlers │ │ │ ├── GlobalMethodArgumentNotValidExceptionHandler.java │ │ │ ├── GlobalMethodNoSuchElementFoundExceptionHandler.java │ │ │ ├── ValidationError.java │ │ │ └── ValidationErrorList.java │ │ │ └── startup │ │ │ └── Startup.java │ ├── resources │ │ ├── application.properties │ │ └── data.sql │ └── typescript │ │ ├── package-lock.json │ │ ├── package.json │ │ └── sql-generator.ts │ └── test │ └── java │ └── de │ └── beyondjava │ ├── employees │ └── EmployeeTest.java │ └── exploringmapstruct │ └── ExploringMapStructApplicationTests.java ├── ExploringSpringBootAndFargate ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Exploring-Mapstruct.postman_collection.json ├── README.md ├── _buildDocker.sh ├── build-and-deploy.sh ├── cdk │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ │ └── cdk.ts │ ├── cdk.context.json │ ├── cdk.json │ ├── generated-cloudformation.yaml │ ├── jest.config.js │ ├── lib │ │ └── cdk-stack.ts │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── Dockerfile │ │ └── index.js │ ├── test │ │ └── cdk.test.ts │ └── tsconfig.json ├── data │ └── myDB.mv.db ├── mvnw ├── mvnw.cmd ├── pom.xml ├── runDocker.sh └── src │ ├── main │ ├── docker │ │ └── Dockerfile │ ├── java │ │ └── de │ │ │ └── beyondjava │ │ │ ├── ExploringMapStructApplication.java │ │ │ ├── business │ │ │ ├── HomeController.java │ │ │ ├── employees │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeController.java │ │ │ │ ├── EmployeeDto.java │ │ │ │ ├── EmployeeMapper.java │ │ │ │ ├── EmployeeRepository.java │ │ │ │ └── EmployeeService.java │ │ │ └── projects │ │ │ │ ├── Project.java │ │ │ │ ├── ProjectController.java │ │ │ │ ├── ProjectDto.java │ │ │ │ ├── ProjectMapper.java │ │ │ │ ├── ProjectRepository.java │ │ │ │ └── ProjectService.java │ │ │ └── tech │ │ │ ├── errorHandlers │ │ │ ├── GlobalMethodArgumentNotValidExceptionHandler.java │ │ │ ├── GlobalMethodNoSuchElementFoundExceptionHandler.java │ │ │ ├── ValidationError.java │ │ │ └── ValidationErrorList.java │ │ │ └── startup │ │ │ └── Startup.java │ ├── resources │ │ ├── application.properties │ │ └── data.sql │ └── typescript │ │ ├── package-lock.json │ │ ├── package.json │ │ └── sql-generator.ts │ └── test │ └── java │ └── de │ └── beyondjava │ ├── employees │ └── EmployeeTest.java │ └── exploringmapstruct │ └── ExploringMapStructApplicationTests.java ├── LICENSE ├── README.md ├── reactive-programming ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── reactive-programming.postman_collection.json └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── beyondjava │ │ │ ├── ReactiveProgrammingApplication.java │ │ │ ├── business │ │ │ └── employees │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeController.java │ │ │ │ └── EmployeeRepository.java │ │ │ └── tech │ │ │ └── errorHandlers │ │ │ ├── GlobalMethodArgumentNotValidExceptionHandler.java │ │ │ ├── GlobalMethodNoSuchElementFoundExceptionHandler.java │ │ │ ├── ValidationError.java │ │ │ └── ValidationErrorList.java │ ├── resources │ │ ├── application.properties │ │ ├── data.sql │ │ └── schema.sql │ └── typescript │ │ ├── package-lock.json │ │ ├── package.json │ │ └── sql-generator.ts │ └── test │ └── java │ └── de │ └── beyondjava │ └── exploringmapstruct │ └── ReactiveProgrammingApplicationTests.java ├── snake_case_parameters ├── .gitignore ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── snake_case.postman_collection.json └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── beyondjava │ │ │ ├── SnakeCaseApplication.java │ │ │ ├── business │ │ │ ├── employees │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeController.java │ │ │ │ ├── EmployeeDto.java │ │ │ │ ├── EmployeeMapper.java │ │ │ │ ├── EmployeeRepository.java │ │ │ │ └── EmployeeService.java │ │ │ └── projects │ │ │ │ ├── Project.java │ │ │ │ ├── ProjectController.java │ │ │ │ ├── ProjectDto.java │ │ │ │ ├── ProjectMapper.java │ │ │ │ ├── ProjectRepository.java │ │ │ │ └── ProjectService.java │ │ │ └── tech │ │ │ ├── SnakeCaseApplicationConfiguration.java │ │ │ ├── errorHandlers │ │ │ ├── GlobalMethodArgumentNotValidExceptionHandler.java │ │ │ ├── GlobalMethodNoSuchElementFoundExceptionHandler.java │ │ │ ├── ValidationError.java │ │ │ └── ValidationErrorList.java │ │ │ └── startup │ │ │ └── Startup.java │ └── resources │ │ ├── application.properties │ │ └── data.sql │ └── test │ └── java │ └── de │ └── beyondjava │ ├── employees │ └── EmployeeTest.java │ └── exploringmapstruct │ └── ExploringMapStructApplicationTests.java ├── spring-cloudwatch-http-observer ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ └── maven-wrapper.properties ├── .vscode │ └── launch.json ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── pratik │ │ └── metricscapture │ │ ├── AppConfig.java │ │ ├── MetricPublisher.java │ │ ├── MetricscaptureApplication.java │ │ ├── PricingEngine.java │ │ ├── ProductController.java │ │ ├── ProductInventoryController.java │ │ └── models │ │ ├── MetricTag.java │ │ ├── Order.java │ │ └── Product.java │ └── resources │ └── application.properties └── springcloudwatch ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── .vscode └── launch.json ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main ├── java └── io │ └── pratik │ └── metricscapture │ ├── AppConfig.java │ ├── MetricPublisher.java │ ├── MetricscaptureApplication.java │ ├── PricingEngine.java │ ├── ProductController.java │ └── models │ ├── MetricTag.java │ ├── Order.java │ └── Product.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | **/*.iml 25 | **/.idea 26 | **/target 27 | /.DS_Store 28 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW", 11 | "issueType": "DEPENDENCY" 12 | } 13 | } -------------------------------------------------------------------------------- /ExploringMapStruct/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### embedded H2 database 36 | /data/ 37 | 38 | ### OSX 39 | .DS_Store -------------------------------------------------------------------------------- /ExploringMapStruct/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanrauh/SpringBoot/894e2204af7a851053387b6b5d923b8b1cf3d88b/ExploringMapStruct/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /ExploringMapStruct/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /ExploringMapStruct/Exploring-Mapstruct.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "5b622f93-dc31-4430-90e7-adb6f4e68a89", 4 | "name": "Spring-data-rest", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "get employees", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "localhost:8080/employees/", 15 | "host": [ 16 | "localhost" 17 | ], 18 | "port": "8080", 19 | "path": [ 20 | "employees", 21 | "" 22 | ] 23 | } 24 | }, 25 | "response": [] 26 | }, 27 | { 28 | "name": "get employee 1", 29 | "request": { 30 | "method": "GET", 31 | "header": [], 32 | "url": { 33 | "raw": "localhost:8080/authors", 34 | "host": [ 35 | "localhost" 36 | ], 37 | "port": "8080", 38 | "path": [ 39 | "authors" 40 | ] 41 | } 42 | }, 43 | "response": [] 44 | }, 45 | { 46 | "name": "post employee", 47 | "request": { 48 | "method": "POST", 49 | "header": [], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Riccia\"\n}", 53 | "options": { 54 | "raw": { 55 | "language": "json" 56 | } 57 | } 58 | }, 59 | "url": { 60 | "raw": "localhost:8080/employees/", 61 | "host": [ 62 | "localhost" 63 | ], 64 | "port": "8080", 65 | "path": [ 66 | "employees", 67 | "" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "post employee error message", 75 | "request": { 76 | "method": "POST", 77 | "header": [], 78 | "body": { 79 | "mode": "raw", 80 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Ty\"\n}", 81 | "options": { 82 | "raw": { 83 | "language": "json" 84 | } 85 | } 86 | }, 87 | "url": { 88 | "raw": "localhost:8080/employees/", 89 | "host": [ 90 | "localhost" 91 | ], 92 | "port": "8080", 93 | "path": [ 94 | "employees", 95 | "" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | }, 101 | { 102 | "name": "post employee another error message", 103 | "request": { 104 | "method": "POST", 105 | "header": [], 106 | "body": { 107 | "mode": "raw", 108 | "raw": "{\n \"firstName\": \"Pat\",\n \"surname\": \"Riccia\"\n}", 109 | "options": { 110 | "raw": { 111 | "language": "json" 112 | } 113 | } 114 | }, 115 | "url": { 116 | "raw": "localhost:8080/employees/", 117 | "host": [ 118 | "localhost" 119 | ], 120 | "port": "8080", 121 | "path": [ 122 | "employees", 123 | "" 124 | ] 125 | } 126 | }, 127 | "response": [] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /ExploringMapStruct/README.md: -------------------------------------------------------------------------------- 1 | # Goals of this project 2 | 3 | This is a minimal Spring Boot project demonstrating how to use Lombok along with MapStruct, Java Bean Validation, 4 | and domain-oriented layering. 5 | 6 | ## Domain-oriented layering 7 | Traditionally, Java developers use horzontal layering. There's a controller layer, a service layer, and entity layer, 8 | and possibly several others. This project does it the other way round. Technology has evolved to a point we don't 9 | need these horizontal layers any more. Instead, the challenges are in the business domain, so it makes sense to put 10 | the classes which belong together in a common package. 11 | 12 | ## Mapstruct 13 | Mapstruct takes the string out of mapping entities to DTO. The reason for adding DTOs in this project is avoiding 14 | technical informations, such as primary keys. 15 | 16 | ## Lombok 17 | Lombok seems to be a first-class citizen of Spring Boot nowadays, so let's use it to reduce the amount of boiler-plate code. 18 | 19 | I'm also experimenting the Lombok's `val` keyword. It's a pity that `val` didn't make it to the Java language, 20 | but it seems `val` is a useful workaround. There's even IntelliJ support for it. 21 | 22 | ## Using records instead of Lombok 23 | By definition, DTOs are immutable data classes, so it sounds like a good idea to implement the DTO as a record. Mapstruct 24 | generates the ugly parts of the code (i.e. using the constructur which may have dozens of parameters) 25 | and hides it from you. So you're working with code that looks clean enough. 26 | 27 | The only challenge is you need to convince your code formatter to format the record definition nicely. By default, 28 | the entire record definition is single line, which looks a bit weird for a data class. 29 | 30 | ## Creating records directly from a JPA query in a repository 31 | You can also return records from a repository: 32 | ```java 33 | @Query(""" 34 | SELECT new de.beyondjava.business.projects.ProjectDto( 35 | p.name, p.description) 36 | FROM Project p 37 | WHERE LOWER(p.name) = LOWER(:projectName) 38 | """) 39 | List findProjectDtosByProjectName( 40 | @Param("projectName") String projectName); 41 | ``` 42 | 43 | ## Java Bean Validation 44 | Controllers, DTOs, and Entities make use of the bean validation annotations. There's also a global error handler 45 | converting the technical messages Spring generates to more user-friendly messages. 46 | 47 | ## Global and local error handlers 48 | The project has two global error handlers, one of them catching validation errors, the other catching the 49 | "ElementNotFoundException" thrown by Optional.orElseThrow(). 50 | 51 | For the sake of demonstration, there's also an exception handler in a controller. 52 | 53 | ## Getting the application up and running 54 | It's a simple Maven project running on Java 17. You can start it 55 | - by running the commmand mvn spring-boot:run 56 | - or by starting the class de.beyondjava.ExploringMapStructApplication. 57 | 58 | There's also a small Postman collection of the API calls. 59 | 60 | ## Creating test data 61 | The `src/main/typescript` folder contains a small script generating test data. Just open a terminal in this folder and run 62 | 63 | ``` 64 | npm run run 65 | ``` 66 | 67 | (no typo - I've really called the script "run" because it types fast). -------------------------------------------------------------------------------- /ExploringMapStruct/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /ExploringMapStruct/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.2 9 | 10 | 11 | de.beyondjava 12 | ExploringMapStruct 13 | 0.0.1-SNAPSHOT 14 | ExploringMapStruct 15 | ExploringMapStruct 16 | 17 | 17 18 | 1.5.1.Final 19 | 1.18.24 20 | 0.2.0 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-validation 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-devtools 41 | runtime 42 | true 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | 52 | 53 | org.mapstruct 54 | mapstruct 55 | ${org.mapstruct.version} 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | ${org.projectlombok.version} 61 | 62 | 63 | com.h2database 64 | h2 65 | runtime 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-compiler-plugin 74 | 3.8.1 75 | 76 | 17 77 | 17 78 | 79 | 80 | org.projectlombok 81 | lombok 82 | ${org.projectlombok.version} 83 | 84 | 85 | 86 | org.projectlombok 87 | lombok-mapstruct-binding 88 | 0.2.0 89 | 90 | 91 | org.mapstruct 92 | mapstruct-processor 93 | ${org.mapstruct.version} 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-maven-plugin 102 | 103 | 104 | 105 | org.projectlombok 106 | lombok 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/ExploringMapStructApplication.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava; 2 | 3 | import de.beyondjava.tech.startup.Startup; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | public class ExploringMapStructApplication { 10 | 11 | @Autowired 12 | private Startup startup; 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(ExploringMapStructApplication.class, args); 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/Employee.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import de.beyondjava.business.projects.Project; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | import javax.persistence.*; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.Size; 12 | import java.util.List; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Employee { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "You do have a first name, don't you?") 25 | @Size(min = 3, max = 20) 26 | private String firstName; 27 | 28 | @NonNull 29 | @NotBlank(message = "Don't be shy and tell us your last name.") 30 | @Size(min = 3, max = 20) 31 | private String lastName; 32 | 33 | @NonNull 34 | @OneToMany(cascade = CascadeType.ALL) 35 | private List projects; 36 | } 37 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.validation.Valid; 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping("/employees") 13 | @Data 14 | @AllArgsConstructor 15 | public class EmployeeController { 16 | 17 | private EmployeeService employeeService; 18 | 19 | @PostMapping("/") 20 | @ResponseStatus(HttpStatus.CREATED) 21 | public void save(@Valid @RequestBody EmployeeDto dto) { 22 | employeeService.save(dto); 23 | } 24 | 25 | @GetMapping("/") 26 | @ResponseStatus(HttpStatus.OK) 27 | public List findAll() { 28 | return employeeService.findAll(); 29 | } 30 | 31 | @GetMapping("/{id}") 32 | @ResponseStatus(HttpStatus.OK) 33 | public EmployeeDto findOne(@PathVariable("id") int id) { 34 | return employeeService.findById(id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/EmployeeDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.Size; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @RequiredArgsConstructor 14 | public class EmployeeDto { 15 | @NonNull 16 | @NotBlank(message = "You do have a first name, don't you?") 17 | @Size(min = 3, max = 20) 18 | private String firstName; 19 | 20 | @NonNull 21 | @NotBlank(message = "Don't be shy and tell us your last name.") 22 | @Size(min = 3, max = 20) 23 | private String lastName; 24 | } 25 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/EmployeeMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface EmployeeMapper { 9 | EmployeeDto entityToDTO(Employee employee); 10 | 11 | List entityToDTO(Iterable employee); 12 | 13 | Employee dtoToEntity(EmployeeDto employee); 14 | 15 | List dtoToEntity(Iterable employees); 16 | 17 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import java.util.List; 6 | 7 | public interface EmployeeRepository extends CrudRepository { 8 | List findEmployeeByFirstNameContaining(String start); 9 | } 10 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/employees/EmployeeService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class EmployeeService { 14 | private EmployeeMapper employeeMapper; 15 | 16 | private EmployeeRepository employeeRepository; 17 | 18 | public void save(EmployeeDto employeeDto) { 19 | val employee = employeeMapper.dtoToEntity(employeeDto); 20 | employeeRepository.save(employee); 21 | } 22 | 23 | public List findAll() { 24 | val employees = employeeRepository.findAll(); 25 | return employeeMapper.entityToDTO(employees); 26 | } 27 | 28 | public EmployeeDto findById(long id) { 29 | val employee = employeeRepository.findById(id).orElseThrow(); 30 | return employeeMapper.entityToDTO(employee); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/Project.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.Id; 11 | import javax.validation.constraints.NotBlank; 12 | import javax.validation.constraints.Size; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Project { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "Your project needs a name.") 25 | @Size(min = 3, max = 30) 26 | private String name; 27 | 28 | @NonNull 29 | @NotBlank(message = "Tell others about your project.") 30 | @Size(min = 1, max = 2000) 31 | private String description; 32 | } 33 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/ProjectController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | @RestController 15 | @RequestMapping("/projects") 16 | @Data 17 | @AllArgsConstructor 18 | public class ProjectController { 19 | 20 | private ProjectService projectService; 21 | 22 | @PostMapping("/") 23 | @ResponseStatus(HttpStatus.CREATED) 24 | public void save(@Valid @RequestBody ProjectDto dto) { 25 | projectService.save(dto); 26 | } 27 | 28 | @GetMapping("/") 29 | @ResponseStatus(HttpStatus.OK) 30 | public List findAll() { 31 | return projectService.findAll(); 32 | } 33 | 34 | @GetMapping("/find") 35 | @ResponseStatus(HttpStatus.OK) 36 | public List findProjectsByProjectName(@RequestParam String projectName) { 37 | return projectService.findProjectsByProjectName(projectName); 38 | } 39 | 40 | @GetMapping("/{id}") 41 | @ResponseStatus(HttpStatus.OK) 42 | public ProjectDto findOne(@PathVariable("id") int id) { 43 | return projectService.findById(id); 44 | } 45 | 46 | /** 47 | * This is an error handler specific to this controller. It overrides the global error handler. 48 | * In a real-world application, this particular combination of error handlers is almost 49 | * certainly undesirable: the global error handler returns a complex JSON object, while the 50 | * local error handler returns a string. 51 | * 52 | * @param ex 53 | * @return 54 | */ 55 | @ResponseStatus(HttpStatus.BAD_REQUEST) 56 | @ExceptionHandler(MethodArgumentNotValidException.class) 57 | public String handleValidationExceptions( 58 | MethodArgumentNotValidException ex) { 59 | var result = "There's a validation error in the project."; 60 | var errors = ex.getBindingResult().getAllErrors(); 61 | var details = errors.stream().map(error -> { 62 | String fieldName = ((FieldError) error).getField(); 63 | String errorMessage = error.getDefaultMessage(); 64 | return "\n" + fieldName + ":" + errorMessage; 65 | }).collect(Collectors.joining()); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/ProjectDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.Size; 6 | 7 | public record ProjectDto( 8 | @NotBlank(message = "Your project needs a name.") 9 | @Size(min = 3, max = 30) 10 | String name, 11 | 12 | @NotBlank(message = "Tell others about your project.") 13 | @Size(min = 1, max = 2000) 14 | String description) { 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/ProjectMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface ProjectMapper { 9 | ProjectDto entityToDTO(Project project); 10 | 11 | List entityToDTO(Iterable project); 12 | 13 | Project dtoToEntity(ProjectDto project); 14 | 15 | List dtoToEntity(Iterable projects); 16 | 17 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | import java.util.List; 8 | 9 | public interface ProjectRepository extends CrudRepository { 10 | @Query(""" 11 | SELECT new de.beyondjava.business.projects.ProjectDto(p.name, p.description) 12 | FROM Project p 13 | WHERE LOWER(p.name) = LOWER(:projectName) 14 | """) 15 | List findProjectDtosByProjectName(@Param("projectName") String projectName); 16 | } 17 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/business/projects/ProjectService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class ProjectService { 14 | private ProjectMapper projectMapper; 15 | 16 | private ProjectRepository projectRepository; 17 | 18 | public void save(ProjectDto projectDto) { 19 | val project = projectMapper.dtoToEntity(projectDto); 20 | projectRepository.save(project); 21 | } 22 | 23 | public List findAll() { 24 | val projects = projectRepository.findAll(); 25 | return projectMapper.entityToDTO(projects); 26 | } 27 | 28 | public List findProjectsByProjectName(String projectName) { 29 | return projectRepository.findProjectDtosByProjectName(projectName); 30 | } 31 | 32 | public ProjectDto findById(long id) { 33 | val project = projectRepository.findById(id); 34 | return projectMapper.entityToDTO(project.orElseThrow()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.val; 4 | import org.springframework.context.MessageSourceResolvable; 5 | import org.springframework.core.Ordered; 6 | import org.springframework.core.annotation.Order; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.method.annotation.HandlerMethodValidationException; 12 | 13 | import java.util.List; 14 | 15 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 16 | 17 | /** 18 | * Kudos https://stackoverflow.com/questions/33663801/how-do-i-customize-default-error-message-from-spring-valid-validation 19 | * Kudos http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ 20 | */ 21 | @Order(Ordered.HIGHEST_PRECEDENCE) 22 | @ControllerAdvice 23 | public class GlobalMethodArgumentNotValidExceptionHandler { 24 | 25 | @ResponseBody 26 | @ExceptionHandler(HandlerMethodValidationException.class) 27 | public ValidationErrorList handleValidationException(HandlerMethodValidationException e) { 28 | var error = new ValidationErrorList(BAD_REQUEST.value(), e.getAllValidationResults().size() + " validation error(s) found."); 29 | for (var fieldError : e.getAllValidationResults()) { 30 | var message = ""; 31 | for (MessageSourceResolvable messageSourceResolvable : fieldError.getResolvableErrors()) { 32 | message += messageSourceResolvable.getDefaultMessage(); 33 | } 34 | error.getFieldErrors().add(new ValidationError("", 35 | fieldError.getMethodParameter().getParameterName(), 36 | message)); 37 | } 38 | return error; 39 | } 40 | 41 | 42 | @ResponseBody 43 | @ExceptionHandler(MethodArgumentNotValidException.class) 44 | public ValidationErrorList methodArgumentNotValidException(MethodArgumentNotValidException ex) { 45 | var result = ex.getBindingResult(); 46 | var fieldErrors = result.getFieldErrors(); 47 | return processFieldErrors(fieldErrors); 48 | } 49 | 50 | private ValidationErrorList processFieldErrors(List fieldErrors) { 51 | val error = new ValidationErrorList(BAD_REQUEST.value(), fieldErrors.size() + " validation error(s) found."); 52 | for (var fieldError : fieldErrors) { 53 | error.getFieldErrors().add(new ValidationError(fieldError.getObjectName(), fieldError.getField(), 54 | fieldError.getDefaultMessage())); 55 | } 56 | return error; 57 | } 58 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodNoSuchElementFoundExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import java.util.NoSuchElementException; 11 | 12 | import static org.springframework.http.HttpStatus.NOT_FOUND; 13 | 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | @ControllerAdvice 16 | public class GlobalMethodNoSuchElementFoundExceptionHandler { 17 | @ExceptionHandler(NoSuchElementException.class) 18 | public ResponseEntity noSuchElementException(NoSuchElementException ex) { 19 | return new ResponseEntity("Couldn't find the resource", NOT_FOUND); 20 | } 21 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/tech/errorHandlers/ValidationError.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @ToString 10 | public class ValidationError { 11 | private String object; 12 | private String field; 13 | private String message; 14 | } 15 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/tech/errorHandlers/ValidationErrorList.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | @ToString 13 | public class ValidationErrorList { 14 | private final int status; 15 | private final String message; 16 | private final List fieldErrors = new ArrayList<>(); 17 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/java/de/beyondjava/tech/startup/Startup.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.startup; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import de.beyondjava.business.employees.EmployeeMapper; 5 | import de.beyondjava.business.employees.EmployeeRepository; 6 | import de.beyondjava.business.projects.Project; 7 | import lombok.AllArgsConstructor; 8 | import lombok.val; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | @Configuration 17 | @AllArgsConstructor 18 | public class Startup { 19 | private EmployeeRepository employeeRepository; 20 | 21 | private EmployeeMapper employeeMapper; 22 | 23 | @Bean 24 | public CommandLineRunner run(EmployeeRepository repository) { 25 | return (args) -> { 26 | this.insertFourEmployees(repository); 27 | var employees = repository.findAll(); 28 | var employeeDTOs = employeeMapper.entityToDTO(employees); 29 | System.out.println(employeeDTOs); 30 | }; 31 | } 32 | 33 | public void insertFourEmployees(EmployeeRepository repository) { 34 | val cafelito = new Project("Cafelito", """ 35 | Demo project used at conferences, 36 | see http://trishagee.github.io/presentation/angularjs_html5_groovy_java_mongodb_wcpgw/ 37 | """); 38 | repository.save(new Employee("Dalia", "Abo Sheasha", Collections.emptyList())); 39 | repository.save(new Employee("Trisha", "Gee", List.of(cafelito))); 40 | repository.save(new Employee("Helen", "Scott", Collections.emptyList())); 41 | repository.save(new Employee("Mala", "Gupta", Collections.emptyList())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:file:./data/myDB 2 | spring.datasource.driver-class-name=org.h2.Driver 3 | spring.datasource.username=sa 4 | spring.datasource.password= 5 | spring.jpa.hibernate.ddl-auto=create-drop 6 | spring.sql.init.mode=ALWAYS 7 | spring.jpa.defer-datasource-initialization=true 8 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM EMPLOYEE_PROJECTS; 2 | DELETE FROM EMPLOYEE; 3 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-1, 'Anna', 'Konda'); 4 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-2, 'Anno', 'Nymous'); 5 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-3, 'Beyond', 'Java'); 6 | 7 | DELETE FROM PROJECT; 8 | INSERT INTO PROJECT(id, name, description) VALUES (-1, 'ngx-extended-pdf-viewer', 'A cute little PDF viewer for Angular.'); 9 | 10 | INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES(-3, -1); -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sql-generator", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | }, 15 | "node_modules/@types/node": { 16 | "version": "18.0.0", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 18 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "@types/node": { 24 | "version": "18.0.0", 25 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 26 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sql-generator.ts", 6 | "scripts": { 7 | "run": "npx ts-node sql-generator.ts" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/main/typescript/sql-generator.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const TEST_DATA_SIZE = 100_000; 4 | 5 | const base = `DELETE FROM EMPLOYEE_PROJECTS; 6 | DELETE FROM EMPLOYEE; 7 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-1, 'Anna', 'Konda'); 8 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-2, 'Anno', 'Nymous'); 9 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-3, 'Beyond', 'Java'); 10 | 11 | DELETE FROM PROJECT; 12 | INSERT INTO PROJECT(id, name, description) VALUES (-1, 'ngx-extended-pdf-viewer', 'A cute little PDF viewer for Angular.'); 13 | 14 | INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES(-3, -1);` 15 | 16 | let result = [base]; 17 | 18 | for (let id = -4; id > -TEST_DATA_SIZE; id--) { 19 | let sql = `INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (${id}, 'Em Ploy', 'Yee${id}');`; 20 | result.push(sql); 21 | } 22 | /* 23 | for (let id = 0; id > -1_000_00; id--) { 24 | let sql = `INSERT INTO PROJECT(id, name, description) VALUES (${id}, 'ngx-extended-pdf-viewer${id}', 'A cute little PDF viewer for Angular.${id}');`; 25 | result.push(sql); 26 | } 27 | for (let id = -1; id > -1_000_00; id--) { 28 | let sql = `INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES (${id%100_00}, ${id});`; 29 | result.push(sql); 30 | } 31 | */ 32 | fs.writeFileSync('../resources/data.sql', result.join("\n")); 33 | -------------------------------------------------------------------------------- /ExploringMapStruct/src/test/java/de/beyondjava/employees/EmployeeTest.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.employees; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | class EmployeeTest { 7 | @BeforeEach 8 | public void init() { 9 | var employee = new Employee(); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /ExploringMapStruct/src/test/java/de/beyondjava/exploringmapstruct/ExploringMapStructApplicationTests.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.exploringmapstruct; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ExploringMapStructApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### embedded H2 database 36 | /data/ 37 | 38 | ### OSX 39 | .DS_Store -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanrauh/SpringBoot/894e2204af7a851053387b6b5d923b8b1cf3d88b/ExploringSpringBootAndFargate/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/Exploring-Mapstruct.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "5b622f93-dc31-4430-90e7-adb6f4e68a89", 4 | "name": "Spring-data-rest", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "get employees", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "localhost:8080/employees/", 15 | "host": [ 16 | "localhost" 17 | ], 18 | "port": "8080", 19 | "path": [ 20 | "employees", 21 | "" 22 | ] 23 | } 24 | }, 25 | "response": [] 26 | }, 27 | { 28 | "name": "get employee 1", 29 | "request": { 30 | "method": "GET", 31 | "header": [], 32 | "url": { 33 | "raw": "localhost:8080/authors", 34 | "host": [ 35 | "localhost" 36 | ], 37 | "port": "8080", 38 | "path": [ 39 | "authors" 40 | ] 41 | } 42 | }, 43 | "response": [] 44 | }, 45 | { 46 | "name": "post employee", 47 | "request": { 48 | "method": "POST", 49 | "header": [], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Riccia\"\n}", 53 | "options": { 54 | "raw": { 55 | "language": "json" 56 | } 57 | } 58 | }, 59 | "url": { 60 | "raw": "localhost:8080/employees/", 61 | "host": [ 62 | "localhost" 63 | ], 64 | "port": "8080", 65 | "path": [ 66 | "employees", 67 | "" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "post employee error message", 75 | "request": { 76 | "method": "POST", 77 | "header": [], 78 | "body": { 79 | "mode": "raw", 80 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Ty\"\n}", 81 | "options": { 82 | "raw": { 83 | "language": "json" 84 | } 85 | } 86 | }, 87 | "url": { 88 | "raw": "localhost:8080/employees/", 89 | "host": [ 90 | "localhost" 91 | ], 92 | "port": "8080", 93 | "path": [ 94 | "employees", 95 | "" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | }, 101 | { 102 | "name": "post employee another error message", 103 | "request": { 104 | "method": "POST", 105 | "header": [], 106 | "body": { 107 | "mode": "raw", 108 | "raw": "{\n \"firstName\": \"Pat\",\n \"surname\": \"Riccia\"\n}", 109 | "options": { 110 | "raw": { 111 | "language": "json" 112 | } 113 | } 114 | }, 115 | "url": { 116 | "raw": "localhost:8080/employees/", 117 | "host": [ 118 | "localhost" 119 | ], 120 | "port": "8080", 121 | "path": [ 122 | "employees", 123 | "" 124 | ] 125 | } 126 | }, 127 | "response": [] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/README.md: -------------------------------------------------------------------------------- 1 | # Goals of this project 2 | 3 | This is a minimal Spring Boot project running on AWS Fargate. It's deployed by a small AWS CDK script. 4 | 5 | ## Getting the application up and running 6 | It's a simple Maven project running on Java 17. You can start it 7 | - by running the commmand mvn spring-boot:run, 8 | - or by starting the class de.beyondjava.ExploringMapStructApplication, 9 | - Running the script `runDocker.sh` runs the application in a local Docker container. 10 | - Finally, `build-and-deploy.sh` compiles the application, packages it as a Docker container, and deploys it in the AWS. This takes roughly five Minutes. A few lines above the "Total time" output, the console window shows an URL. You can use this URL to test the application. 11 | 12 | The project also contains a small Postman collection for local testing. If you want to test the application in the AWS, you'll want to edit the URLs of the Postman collection. 13 | 14 | ## Getting rid of the application 15 | Don't forget to remove your test application because it costs money! You achieve this by running the command 16 | 17 | ``` 18 | cd cdk 19 | npm run cdk destroy 20 | ``` -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/_buildDocker.sh: -------------------------------------------------------------------------------- 1 | cp -f ./target/ExploringSpringBootAndFargate-0.0.1-SNAPSHOT.jar ./src/main/docker/app.jar 2 | cd src/main/docker 3 | 4 | docker build -t beyondjava/spring-boot-on-docker . -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/build-and-deploy.sh: -------------------------------------------------------------------------------- 1 | # export DOCKER_DEFAULT_PLATFORM='linux/amd64' 2 | # DOCKER_DEFAULT_PLATFORM='linux/amd64' ./mvnw compile jib:dockerBuild 3 | # ./mvnw compile jib:dockerBuild 4 | ./mvnw clean package 5 | ./_buildDocker.sh 6 | cd cdk 7 | npm install 8 | # DOCKER_DEFAULT_PLATFORM='linux/amd64' npm run cdk deploy 9 | npm run cdk deploy -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { CdkStack } from '../lib/cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | new CdkStack(app, 'SpringBootOnFargate', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=290944405340:region=eu-central-1": [ 3 | "eu-central-1a", 4 | "eu-central-1b", 5 | "eu-central-1c" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:checkSecretUsage": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 36 | "@aws-cdk/core:enablePartitionLiterals": true, 37 | "@aws-cdk/core:target-partitions": [ 38 | "aws", 39 | "aws-cn" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/lib/cdk-stack.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_ec2 as ec2, 3 | aws_ecs as ecs, 4 | aws_ecs_patterns as ecs_patterns, 5 | Duration, 6 | Stack, 7 | StackProps 8 | } from 'aws-cdk-lib'; 9 | import {Construct} from 'constructs'; 10 | import * as path from "path"; 11 | import {Protocol} from "aws-cdk-lib/aws-elasticloadbalancingv2"; 12 | 13 | export class CdkStack extends Stack { 14 | constructor(scope: Construct, id: string, props?: StackProps) { 15 | super(scope, id, props); 16 | 17 | console.log(process.env.DOCKER_DEFAULT_PLATFORM); 18 | 19 | const vpc = new ec2.Vpc(this, 'SpringBootOnFargateVpc', { 20 | maxAzs: 3, 21 | }); 22 | 23 | const cluster = new ecs.Cluster(this, 'SpringBootOnFargateCluster', { 24 | vpc: vpc 25 | }); 26 | 27 | const fargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'SpringBootOnFargateFargateService', { 28 | cluster: cluster, 29 | cpu: 512, 30 | memoryLimitMiB: 2048, 31 | desiredCount: 1, 32 | taskImageOptions: { 33 | image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../../src/main/docker/')), 34 | // image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../src/')), 35 | environment: { 36 | "name": "Spring Boot on Fargate", 37 | }, 38 | }, 39 | }); 40 | /* 41 | fargate.targetGroup.configureHealthCheck({ 42 | path: "/", 43 | port: "80", 44 | healthyHttpCodes: "200", 45 | interval: Duration.seconds(30), 46 | protocol: Protocol.HTTP 47 | 48 | }) 49 | */ 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "10.17.27", 16 | "@types/prettier": "2.6.0", 17 | "jest": "^27.5.1", 18 | "ts-jest": "^27.1.4", 19 | "aws-cdk": "2.43.1", 20 | "ts-node": "^10.9.1", 21 | "typescript": "~3.9.7" 22 | }, 23 | "dependencies": { 24 | "aws-cdk-lib": "2.43.1", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | WORKDIR /usr/src/app 3 | COPY index.js ./ 4 | EXPOSE 80 5 | CMD ["node", "index.js"] -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/src/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const hostname = '0.0.0.0'; 4 | const port = 80; 5 | 6 | const server = http.createServer((req, res) => { 7 | res.statusCode = 200; 8 | res.setHeader('Content-Type', 'text/html'); 9 | res.end(`Hello World ${process.env.name}`); 10 | }); 11 | 12 | server.listen(port, hostname, () => { 13 | console.log(`Server running at http://${hostname}:${port}/`); 14 | }); -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as Cdk from '../lib/cdk-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/cdk-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new Cdk.CdkStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/data/myDB.mv.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanrauh/SpringBoot/894e2204af7a851053387b6b5d923b8b1cf3d88b/ExploringSpringBootAndFargate/data/myDB.mv.db -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.0 9 | 10 | 11 | de.beyondjava 12 | ExploringSpringBootAndFargate 13 | 0.0.1-SNAPSHOT 14 | ExploringSpringBootAndFargate 15 | Exploring howo to dockerize and Spring Boot application with Jib and how to deploy it to AWS Fargate 16 | 17 | 18 | 17 19 | 1.5.1.Final 20 | 1.18.24 21 | 0.2.0 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-data-jpa 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-validation 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-devtools 42 | runtime 43 | true 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-test 49 | test 50 | 51 | 52 | 53 | 54 | org.mapstruct 55 | mapstruct 56 | ${org.mapstruct.version} 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | ${org.projectlombok.version} 62 | 63 | 64 | com.h2database 65 | h2 66 | runtime 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 3.8.1 76 | 77 | 17 78 | 17 79 | 80 | 81 | org.projectlombok 82 | lombok 83 | ${org.projectlombok.version} 84 | 85 | 86 | 87 | org.projectlombok 88 | lombok-mapstruct-binding 89 | 0.2.0 90 | 91 | 92 | org.mapstruct 93 | mapstruct-processor 94 | ${org.mapstruct.version} 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-maven-plugin 103 | 104 | 105 | 106 | org.projectlombok 107 | lombok 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/runDocker.sh: -------------------------------------------------------------------------------- 1 | ./mvnw clean package 2 | ./_buildDocker.sh 3 | docker run -p 80:80 beyondjava/spring-boot-on-docker 4 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 openjdk:17-jdk-alpine 2 | COPY ./app.jar app.jar 3 | EXPOSE 80 4 | ENTRYPOINT ["java","-jar","/app.jar"] 5 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/ExploringMapStructApplication.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava; 2 | 3 | import de.beyondjava.tech.startup.Startup; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | public class ExploringMapStructApplication { 10 | 11 | @Autowired 12 | private Startup startup; 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(ExploringMapStructApplication.class, args); 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/HomeController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.validation.Valid; 10 | import java.util.List; 11 | 12 | @RestController 13 | @Data 14 | @AllArgsConstructor 15 | public class HomeController { 16 | 17 | @GetMapping("/") 18 | public String home(HttpServletRequest request 19 | ) { 20 | var root = request.getRequestURI(); 21 | var response = """ 22 |

Greetings!

23 |

This is Spring Boot running on AWS Fargate, deployed by an AWS CDK script.

24 |

Check out these URLs:

25 | 33 | """; 34 | return response; 35 | } 36 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/Employee.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import de.beyondjava.business.projects.Project; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | import javax.persistence.*; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.Size; 12 | import java.util.List; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Employee { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "You do have a first name, don't you?") 25 | @Size(min = 3, max = 20) 26 | private String firstName; 27 | 28 | @NonNull 29 | @NotBlank(message = "Don't be shy and tell us your last name.") 30 | @Size(min = 3, max = 20) 31 | private String lastName; 32 | 33 | @NonNull 34 | @OneToMany(cascade = CascadeType.ALL) 35 | private List projects; 36 | } 37 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.validation.Valid; 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping("/employees") 13 | @Data 14 | @AllArgsConstructor 15 | public class EmployeeController { 16 | 17 | private EmployeeService employeeService; 18 | 19 | @PostMapping("/") 20 | @ResponseStatus(HttpStatus.CREATED) 21 | public void save(@Valid @RequestBody EmployeeDto dto) { 22 | employeeService.save(dto); 23 | } 24 | 25 | @GetMapping("/") 26 | @ResponseStatus(HttpStatus.OK) 27 | public List findAll() { 28 | return employeeService.findAll(); 29 | } 30 | 31 | @GetMapping("/{id}") 32 | @ResponseStatus(HttpStatus.OK) 33 | public EmployeeDto findOne(@PathVariable("id") int id) { 34 | return employeeService.findById(id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/EmployeeDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.Size; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @RequiredArgsConstructor 14 | public class EmployeeDto { 15 | @NonNull 16 | @NotBlank(message = "You do have a first name, don't you?") 17 | @Size(min = 3, max = 20) 18 | private String firstName; 19 | 20 | @NonNull 21 | @NotBlank(message = "Don't be shy and tell us your last name.") 22 | @Size(min = 3, max = 20) 23 | private String lastName; 24 | } 25 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/EmployeeMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface EmployeeMapper { 9 | EmployeeDto entityToDTO(Employee employee); 10 | 11 | List entityToDTO(Iterable employee); 12 | 13 | Employee dtoToEntity(EmployeeDto employee); 14 | 15 | List dtoToEntity(Iterable employees); 16 | 17 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import java.util.List; 6 | 7 | public interface EmployeeRepository extends CrudRepository { 8 | List findEmployeeByFirstNameContaining(String start); 9 | } 10 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/employees/EmployeeService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class EmployeeService { 14 | private EmployeeMapper employeeMapper; 15 | 16 | private EmployeeRepository employeeRepository; 17 | 18 | public void save(EmployeeDto employeeDto) { 19 | val employee = employeeMapper.dtoToEntity(employeeDto); 20 | employeeRepository.save(employee); 21 | } 22 | 23 | public List findAll() { 24 | val employees = employeeRepository.findAll(); 25 | return employeeMapper.entityToDTO(employees); 26 | } 27 | 28 | public EmployeeDto findById(long id) { 29 | val employee = employeeRepository.findById(id).orElseThrow(); 30 | return employeeMapper.entityToDTO(employee); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/Project.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.Id; 11 | import javax.validation.constraints.NotBlank; 12 | import javax.validation.constraints.Size; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Project { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "Your project needs a name.") 25 | @Size(min = 3, max = 30) 26 | private String name; 27 | 28 | @NonNull 29 | @NotBlank(message = "Tell others about your project.") 30 | @Size(min = 1, max = 2000) 31 | private String description; 32 | } 33 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/ProjectController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | @RestController 15 | @RequestMapping("/projects") 16 | @Data 17 | @AllArgsConstructor 18 | public class ProjectController { 19 | 20 | private ProjectService projectService; 21 | 22 | @PostMapping("/") 23 | @ResponseStatus(HttpStatus.CREATED) 24 | public void save(@Valid @RequestBody ProjectDto dto) { 25 | projectService.save(dto); 26 | } 27 | 28 | @GetMapping("/") 29 | @ResponseStatus(HttpStatus.OK) 30 | public List findAll() { 31 | return projectService.findAll(); 32 | } 33 | 34 | @GetMapping("/find") 35 | @ResponseStatus(HttpStatus.OK) 36 | public List findProjectsByProjectName(@RequestParam String projectName) { 37 | return projectService.findProjectsByProjectName(projectName); 38 | } 39 | 40 | @GetMapping("/{id}") 41 | @ResponseStatus(HttpStatus.OK) 42 | public ProjectDto findOne(@PathVariable("id") int id) { 43 | return projectService.findById(id); 44 | } 45 | 46 | /** 47 | * This is an error handler specific to this controller. It overrides the global error handler. 48 | * In a real-world application, this particular combination of error handlers is almost 49 | * certainly undesirable: the global error handler returns a complex JSON object, while the 50 | * local error handler returns a string. 51 | * 52 | * @param ex 53 | * @return 54 | */ 55 | @ResponseStatus(HttpStatus.BAD_REQUEST) 56 | @ExceptionHandler(MethodArgumentNotValidException.class) 57 | public String handleValidationExceptions( 58 | MethodArgumentNotValidException ex) { 59 | var result = "There's a validation error in the project."; 60 | var errors = ex.getBindingResult().getAllErrors(); 61 | var details = errors.stream().map(error -> { 62 | String fieldName = ((FieldError) error).getField(); 63 | String errorMessage = error.getDefaultMessage(); 64 | return "\n" + fieldName + ":" + errorMessage; 65 | }).collect(Collectors.joining()); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/ProjectDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.Size; 6 | 7 | public record ProjectDto( 8 | @NotBlank(message = "Your project needs a name.") 9 | @Size(min = 3, max = 30) 10 | String name, 11 | 12 | @NotBlank(message = "Tell others about your project.") 13 | @Size(min = 1, max = 2000) 14 | String description) { 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/ProjectMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface ProjectMapper { 9 | ProjectDto entityToDTO(Project project); 10 | 11 | List entityToDTO(Iterable project); 12 | 13 | Project dtoToEntity(ProjectDto project); 14 | 15 | List dtoToEntity(Iterable projects); 16 | 17 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | import java.util.List; 8 | 9 | public interface ProjectRepository extends CrudRepository { 10 | @Query(""" 11 | SELECT new de.beyondjava.business.projects.ProjectDto(p.name, p.description) 12 | FROM Project p 13 | WHERE LOWER(p.name) = LOWER(:projectName) 14 | """) 15 | List findProjectDtosByProjectName(@Param("projectName") String projectName); 16 | } 17 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/business/projects/ProjectService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class ProjectService { 14 | private ProjectMapper projectMapper; 15 | 16 | private ProjectRepository projectRepository; 17 | 18 | public void save(ProjectDto projectDto) { 19 | val project = projectMapper.dtoToEntity(projectDto); 20 | projectRepository.save(project); 21 | } 22 | 23 | public List findAll() { 24 | val projects = projectRepository.findAll(); 25 | return projectMapper.entityToDTO(projects); 26 | } 27 | 28 | public List findProjectsByProjectName(String projectName) { 29 | return projectRepository.findProjectDtosByProjectName(projectName); 30 | } 31 | 32 | public ProjectDto findById(long id) { 33 | val project = projectRepository.findById(id); 34 | return projectMapper.entityToDTO(project.orElseThrow()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.val; 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.web.bind.MethodArgumentNotValidException; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | 11 | import java.util.List; 12 | 13 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 14 | 15 | /** 16 | * Kudos https://stackoverflow.com/questions/33663801/how-do-i-customize-default-error-message-from-spring-valid-validation 17 | * Kudos http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ 18 | */ 19 | @Order(Ordered.HIGHEST_PRECEDENCE) 20 | @ControllerAdvice 21 | public class GlobalMethodArgumentNotValidExceptionHandler { 22 | 23 | // @ResponseStatus(BAD_REQUEST) 24 | @ResponseBody 25 | @ExceptionHandler(MethodArgumentNotValidException.class) 26 | public ValidationErrorList methodArgumentNotValidException(MethodArgumentNotValidException ex) { 27 | var result = ex.getBindingResult(); 28 | var fieldErrors = result.getFieldErrors(); 29 | return processFieldErrors(fieldErrors); 30 | } 31 | 32 | private ValidationErrorList processFieldErrors(List fieldErrors) { 33 | val error = new ValidationErrorList(BAD_REQUEST.value(), fieldErrors.size() + " validation error(s) found."); 34 | for (var fieldError : fieldErrors) { 35 | error.getFieldErrors().add(new ValidationError(fieldError.getObjectName(), fieldError.getField(), 36 | fieldError.getDefaultMessage())); 37 | } 38 | return error; 39 | } 40 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodNoSuchElementFoundExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import java.util.NoSuchElementException; 11 | 12 | import static org.springframework.http.HttpStatus.NOT_FOUND; 13 | 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | @ControllerAdvice 16 | public class GlobalMethodNoSuchElementFoundExceptionHandler { 17 | @ExceptionHandler(NoSuchElementException.class) 18 | public ResponseEntity noSuchElementException(NoSuchElementException ex) { 19 | return new ResponseEntity("Couldn't find the resource", NOT_FOUND); 20 | } 21 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/tech/errorHandlers/ValidationError.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @ToString 10 | public class ValidationError { 11 | private String object; 12 | private String field; 13 | private String message; 14 | } 15 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/tech/errorHandlers/ValidationErrorList.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | @ToString 13 | public class ValidationErrorList { 14 | private final int status; 15 | private final String message; 16 | private final List fieldErrors = new ArrayList<>(); 17 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/java/de/beyondjava/tech/startup/Startup.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.startup; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import de.beyondjava.business.employees.EmployeeMapper; 5 | import de.beyondjava.business.employees.EmployeeRepository; 6 | import de.beyondjava.business.projects.Project; 7 | import lombok.AllArgsConstructor; 8 | import lombok.val; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | @Configuration 17 | @AllArgsConstructor 18 | public class Startup { 19 | private EmployeeRepository employeeRepository; 20 | 21 | private EmployeeMapper employeeMapper; 22 | 23 | @Bean 24 | public CommandLineRunner run(EmployeeRepository repository) { 25 | return (args) -> { 26 | this.insertFourEmployees(repository); 27 | var employees = repository.findAll(); 28 | var employeeDTOs = employeeMapper.entityToDTO(employees); 29 | System.out.println(employeeDTOs); 30 | }; 31 | } 32 | 33 | public void insertFourEmployees(EmployeeRepository repository) { 34 | val cafelito = new Project("Cafelito", """ 35 | Demo project used at conferences, 36 | see http://trishagee.github.io/presentation/angularjs_html5_groovy_java_mongodb_wcpgw/ 37 | """); 38 | repository.save(new Employee("Dalia", "Abo Sheasha", Collections.emptyList())); 39 | repository.save(new Employee("Trisha", "Gee", List.of(cafelito))); 40 | repository.save(new Employee("Helen", "Scott", Collections.emptyList())); 41 | repository.save(new Employee("Mala", "Gupta", Collections.emptyList())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:file:./data/myDB 2 | spring.datasource.driver-class-name=org.h2.Driver 3 | spring.datasource.username=sa 4 | spring.datasource.password= 5 | spring.jpa.hibernate.ddl-auto=create-drop 6 | spring.sql.init.mode=ALWAYS 7 | spring.jpa.defer-datasource-initialization=true 8 | server.port=80 9 | logging.level.root=INFO -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM EMPLOYEE_PROJECTS; 2 | DELETE FROM EMPLOYEE; 3 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-1, 'Anna', 'Konda'); 4 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-2, 'Anno', 'Nymous'); 5 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-3, 'Beyond', 'Java'); 6 | 7 | DELETE FROM PROJECT; 8 | INSERT INTO PROJECT(id, name, description) VALUES (-1, 'ngx-extended-pdf-viewer', 'A cute little PDF viewer for Angular.'); 9 | 10 | INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES(-3, -1); -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sql-generator", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | }, 15 | "node_modules/@types/node": { 16 | "version": "18.0.0", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 18 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "@types/node": { 24 | "version": "18.0.0", 25 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 26 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sql-generator.ts", 6 | "scripts": { 7 | "run": "npx ts-node sql-generator.ts" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/main/typescript/sql-generator.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const TEST_DATA_SIZE = 100_000; 4 | 5 | const base = `DELETE FROM EMPLOYEE_PROJECTS; 6 | DELETE FROM EMPLOYEE; 7 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-1, 'Anna', 'Konda'); 8 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-2, 'Anno', 'Nymous'); 9 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-3, 'Beyond', 'Java'); 10 | 11 | DELETE FROM PROJECT; 12 | INSERT INTO PROJECT(id, name, description) VALUES (-1, 'ngx-extended-pdf-viewer', 'A cute little PDF viewer for Angular.'); 13 | 14 | INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES(-3, -1);` 15 | 16 | let result = [base]; 17 | 18 | for (let id = -4; id > -TEST_DATA_SIZE; id--) { 19 | let sql = `INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (${id}, 'Em Ploy', 'Yee${id}');`; 20 | result.push(sql); 21 | } 22 | /* 23 | for (let id = 0; id > -1_000_00; id--) { 24 | let sql = `INSERT INTO PROJECT(id, name, description) VALUES (${id}, 'ngx-extended-pdf-viewer${id}', 'A cute little PDF viewer for Angular.${id}');`; 25 | result.push(sql); 26 | } 27 | for (let id = -1; id > -1_000_00; id--) { 28 | let sql = `INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES (${id%100_00}, ${id});`; 29 | result.push(sql); 30 | } 31 | */ 32 | fs.writeFileSync('../resources/data.sql', result.join("\n")); 33 | -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/test/java/de/beyondjava/employees/EmployeeTest.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.employees; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | class EmployeeTest { 7 | @BeforeEach 8 | public void init() { 9 | var employee = new Employee(); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /ExploringSpringBootAndFargate/src/test/java/de/beyondjava/exploringmapstruct/ExploringMapStructApplicationTests.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.exploringmapstruct; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ExploringMapStructApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringBoot -------------------------------------------------------------------------------- /reactive-programming/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### embedded H2 database 36 | /data/ 37 | 38 | ### OSX 39 | .DS_Store -------------------------------------------------------------------------------- /reactive-programming/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanrauh/SpringBoot/894e2204af7a851053387b6b5d923b8b1cf3d88b/reactive-programming/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /reactive-programming/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /reactive-programming/README.md: -------------------------------------------------------------------------------- 1 | # Goals of this project 2 | 3 | This is a minimal Spring Boot project demonstrating reactive programming with Flux and Mono. 4 | 5 | 6 | ## Getting the application up and running 7 | You need a Postgres database. By default, the database name is "reactive". You have to create this database: 8 | 9 | ``` 10 | CREATE DATABASE reactive; 11 | ``` 12 | 13 | After that, the application automatedly creates the database table "EMPLOYEE". 14 | 15 | The Java code is a simple Maven project running on Java 17. You can start it 16 | - by running the commmand mvn spring-boot:run 17 | - or by starting the class de.beyondjava.ReactiveProgrammingApplication. 18 | 19 | There's also a small Postman collection of the API calls. 20 | 21 | ## Creating test data 22 | The `src/main/typescript` folder contains a small script generating test data. Just open a terminal in this folder and run 23 | 24 | ``` 25 | npm run run 26 | ``` 27 | 28 | (no typo - I've really called the script "run" because it types fast). -------------------------------------------------------------------------------- /reactive-programming/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.0 9 | 10 | 11 | de.beyondjava 12 | ReactiveProgramming 13 | 0.0.1-SNAPSHOT 14 | ReactiveProgramming 15 | ReactiveProgramming 16 | 17 | 17 18 | 1.5.1.Final 19 | 1.18.24 20 | 0.2.0 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-r2dbc 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-webflux 30 | 31 | 32 | 33 | org.postgresql 34 | postgresql 35 | runtime 36 | 37 | 38 | org.postgresql 39 | r2dbc-postgresql 40 | runtime 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | io.projectreactor 49 | reactor-test 50 | test 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-validation 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-devtools 61 | runtime 62 | true 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-test 68 | test 69 | 70 | 71 | 72 | 73 | org.mapstruct 74 | mapstruct 75 | ${org.mapstruct.version} 76 | 77 | 78 | org.projectlombok 79 | lombok 80 | ${org.projectlombok.version} 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-compiler-plugin 89 | 3.8.1 90 | 91 | 17 92 | 17 93 | 94 | 95 | org.projectlombok 96 | lombok 97 | ${org.projectlombok.version} 98 | 99 | 100 | 101 | org.projectlombok 102 | lombok-mapstruct-binding 103 | 0.2.0 104 | 105 | 106 | org.mapstruct 107 | mapstruct-processor 108 | ${org.mapstruct.version} 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-maven-plugin 117 | 118 | 119 | 120 | org.projectlombok 121 | lombok 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /reactive-programming/reactive-programming.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "4ddb8515-4e00-4ab7-abd9-e9fb57ea307e", 4 | "name": "Reactive Programming with Spring", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "get employees", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "localhost:8080/employees/", 15 | "host": [ 16 | "localhost" 17 | ], 18 | "port": "8080", 19 | "path": [ 20 | "employees", 21 | "" 22 | ] 23 | } 24 | }, 25 | "response": [] 26 | }, 27 | { 28 | "name": "get one employee", 29 | "request": { 30 | "method": "GET", 31 | "header": [], 32 | "url": { 33 | "raw": "localhost:8080/authors", 34 | "host": [ 35 | "localhost" 36 | ], 37 | "port": "8080", 38 | "path": [ 39 | "authors" 40 | ] 41 | } 42 | }, 43 | "response": [] 44 | }, 45 | { 46 | "name": "post employee", 47 | "request": { 48 | "method": "POST", 49 | "header": [], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Riccia\"\n}", 53 | "options": { 54 | "raw": { 55 | "language": "json" 56 | } 57 | } 58 | }, 59 | "url": { 60 | "raw": "localhost:8080/employees/", 61 | "host": [ 62 | "localhost" 63 | ], 64 | "port": "8080", 65 | "path": [ 66 | "employees", 67 | "" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "post another employee", 75 | "request": { 76 | "method": "POST", 77 | "header": [], 78 | "body": { 79 | "mode": "raw", 80 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Ty\"\n}", 81 | "options": { 82 | "raw": { 83 | "language": "json" 84 | } 85 | } 86 | }, 87 | "url": { 88 | "raw": "localhost:8080/employees/", 89 | "host": [ 90 | "localhost" 91 | ], 92 | "port": "8080", 93 | "path": [ 94 | "employees", 95 | "" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | }, 101 | { 102 | "name": "post employer error message", 103 | "request": { 104 | "method": "POST", 105 | "header": [], 106 | "body": { 107 | "mode": "raw", 108 | "raw": "{\n \"firstName\": \"Pat\",\n \"surname\": \"Riccia\"\n}", 109 | "options": { 110 | "raw": { 111 | "language": "json" 112 | } 113 | } 114 | }, 115 | "url": { 116 | "raw": "localhost:8080/employees/", 117 | "host": [ 118 | "localhost" 119 | ], 120 | "port": "8080", 121 | "path": [ 122 | "employees", 123 | "" 124 | ] 125 | } 126 | }, 127 | "response": [] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/ReactiveProgrammingApplication.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.core.io.ClassPathResource; 9 | import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; 10 | import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; 11 | 12 | @SpringBootApplication 13 | public class ReactiveProgrammingApplication { 14 | public static void main(String[] args) { 15 | SpringApplication.run(ReactiveProgrammingApplication.class, args); 16 | } 17 | 18 | @Bean 19 | ConnectionFactoryInitializer initializer(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) { 20 | ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); 21 | initializer.setConnectionFactory(connectionFactory); 22 | initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"), new ClassPathResource("data.sql"))); 23 | 24 | return initializer; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/business/employees/Employee.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.*; 4 | import org.springframework.data.annotation.Id; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.Size; 8 | 9 | @Value 10 | @Builder 11 | @RequiredArgsConstructor 12 | public class Employee { 13 | @Id 14 | // @GeneratedValue 15 | private Long id; 16 | 17 | @NonNull 18 | @NotBlank(message = "You do have a first name, don't you?") 19 | @Size(min = 3, max = 20) 20 | private String firstName; 21 | 22 | @NonNull 23 | @NotBlank(message = "Don't be shy and tell us your last name.") 24 | @Size(min = 3, max = 20) 25 | private String lastName; 26 | 27 | // @NonNull 28 | // @OneToMany(cascade = CascadeType.ALL) 29 | // private List projects; 30 | } 31 | -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/business/employees/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | import javax.validation.Valid; 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/employees") 15 | @AllArgsConstructor 16 | public class EmployeeController { 17 | 18 | private EmployeeRepository employeeRepository; 19 | 20 | @PostMapping 21 | public Mono createEmployee(@RequestBody Employee employee) { 22 | return employeeRepository.save(employee); 23 | } 24 | 25 | @PutMapping 26 | public Mono updateEmployee(@RequestBody Employee employee) { 27 | return employeeRepository 28 | .findByFirstName(employee.getFirstName()) 29 | .flatMap(employeeResult -> employeeRepository.save(employee)); 30 | } 31 | 32 | @DeleteMapping 33 | public Mono deleteEmployee(@RequestBody Employee employee) { 34 | return employeeRepository.deleteById(employee.getId()); 35 | } 36 | 37 | @GetMapping("/") 38 | @ResponseStatus(HttpStatus.OK) 39 | public Flux findAll() { 40 | return employeeRepository.findAll(); 41 | } 42 | 43 | @GetMapping("/{id}") 44 | @ResponseStatus(HttpStatus.OK) 45 | public Mono findOne(@PathVariable("id") long id) { 46 | return employeeRepository.findById(id); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/business/employees/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.springframework.data.r2dbc.repository.R2dbcRepository; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface EmployeeRepository extends R2dbcRepository { 7 | Mono findByFirstName(String name); 8 | } 9 | -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.val; 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.web.bind.MethodArgumentNotValidException; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | 11 | import java.util.List; 12 | 13 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 14 | 15 | /** 16 | * Kudos https://stackoverflow.com/questions/33663801/how-do-i-customize-default-error-message-from-spring-valid-validation 17 | * Kudos http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ 18 | */ 19 | @Order(Ordered.HIGHEST_PRECEDENCE) 20 | @ControllerAdvice 21 | public class GlobalMethodArgumentNotValidExceptionHandler { 22 | 23 | // @ResponseStatus(BAD_REQUEST) 24 | @ResponseBody 25 | @ExceptionHandler(MethodArgumentNotValidException.class) 26 | public ValidationErrorList methodArgumentNotValidException(MethodArgumentNotValidException ex) { 27 | var result = ex.getBindingResult(); 28 | var fieldErrors = result.getFieldErrors(); 29 | return processFieldErrors(fieldErrors); 30 | } 31 | 32 | private ValidationErrorList processFieldErrors(List fieldErrors) { 33 | val error = new ValidationErrorList(BAD_REQUEST.value(), fieldErrors.size() + " validation error(s) found."); 34 | for (var fieldError : fieldErrors) { 35 | error.getFieldErrors().add(new ValidationError(fieldError.getObjectName(), fieldError.getField(), 36 | fieldError.getDefaultMessage())); 37 | } 38 | return error; 39 | } 40 | } -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodNoSuchElementFoundExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import java.util.NoSuchElementException; 11 | 12 | import static org.springframework.http.HttpStatus.NOT_FOUND; 13 | 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | @ControllerAdvice 16 | public class GlobalMethodNoSuchElementFoundExceptionHandler { 17 | @ExceptionHandler(NoSuchElementException.class) 18 | public ResponseEntity noSuchElementException(NoSuchElementException ex) { 19 | return new ResponseEntity("Couldn't find the resource", NOT_FOUND); 20 | } 21 | } -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/tech/errorHandlers/ValidationError.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @ToString 10 | public class ValidationError { 11 | private String object; 12 | private String field; 13 | private String message; 14 | } 15 | -------------------------------------------------------------------------------- /reactive-programming/src/main/java/de/beyondjava/tech/errorHandlers/ValidationErrorList.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | @ToString 13 | public class ValidationErrorList { 14 | private final int status; 15 | private final String message; 16 | private final List fieldErrors = new ArrayList<>(); 17 | } -------------------------------------------------------------------------------- /reactive-programming/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # spring.datasource.url=jdbc:h2:file:./data/myDB 2 | # spring.datasource.driver-class-name=org.h2.Driver 3 | # spring.datasource.username=sa 4 | # spring.datasource.password= 5 | # spring.jpa.hibernate.ddl-auto=create-drop 6 | # spring.sql.init.mode=ALWAYS 7 | # spring.jpa.defer-datasource-initialization=true 8 | spring.r2dbc.url=r2dbc:postgresql://postgres@localhost:5432/reactive 9 | spring.r2dbc.initialization-mode=always 10 | logging.level.org.springframework.r2dbc=DEBUG 11 | -------------------------------------------------------------------------------- /reactive-programming/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | -- CREATE DATABASE reactive; 2 | 3 | CREATE TABLE IF NOT EXISTS employee 4 | ( 5 | id SERIAL PRIMARY KEY, 6 | first_name TEXT NOT NULL, 7 | last_name TEXT NOT NULL 8 | ); -------------------------------------------------------------------------------- /reactive-programming/src/main/typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sql-generator", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | }, 15 | "node_modules/@types/node": { 16 | "version": "18.0.0", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 18 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "@types/node": { 24 | "version": "18.0.0", 25 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", 26 | "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /reactive-programming/src/main/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-generator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sql-generator.ts", 6 | "scripts": { 7 | "run": "npx ts-node sql-generator.ts" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^18.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /reactive-programming/src/main/typescript/sql-generator.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const TEST_DATA_SIZE = 100_000; 4 | 5 | const base = `-- DELETE FROM EMPLOYEE_PROJECTS; 6 | DELETE FROM EMPLOYEE; 7 | -- DELETE FROM PROJECT;` 8 | 9 | let result = [base]; 10 | 11 | for (let id = 0; id > -TEST_DATA_SIZE; id--) { 12 | let sql = `INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (${id}, 'Em Ploy', 'Yee${id}');`; 13 | result.push(sql); 14 | } 15 | /* 16 | for (let id = 0; id > -1_000_00; id--) { 17 | let sql = `INSERT INTO PROJECT(id, name, description) VALUES (${id}, 'ngx-extended-pdf-viewer${id}', 'A cute little PDF viewer for Angular.${id}');`; 18 | result.push(sql); 19 | } 20 | for (let id = -1; id > -1_000_00; id--) { 21 | let sql = `INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES (${id%100_00}, ${id});`; 22 | result.push(sql); 23 | } 24 | */ 25 | fs.writeFileSync('../resources/data.sql', result.join("\n")); 26 | -------------------------------------------------------------------------------- /reactive-programming/src/test/java/de/beyondjava/exploringmapstruct/ReactiveProgrammingApplicationTests.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.exploringmapstruct; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ReactiveProgrammingApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /snake_case_parameters/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### embedded H2 database 36 | /data/ 37 | 38 | ### OSX 39 | .DS_Store -------------------------------------------------------------------------------- /snake_case_parameters/README.md: -------------------------------------------------------------------------------- 1 | # Goals of this project 2 | 3 | This is a minimal Spring Boot project demonstrating how to use Lombok along with MapStruct, Java Bean Validation, 4 | and domain-oriented layering. 5 | 6 | ## Domain-oriented layering 7 | Traditionally, Java developers use horzontal layering. There's a controller layer, a service layer, and entity layer, 8 | and possibly several others. This project does it the other way round. Technology has evolved to a point we don't 9 | need these horizontal layers any more. Instead, the challenges are in the business domain, so it makes sense to put 10 | the classes which belong together in a common package. 11 | 12 | ## Mapstruct 13 | Mapstruct takes the string out of mapping entities to DTO. The reason for adding DTOs in this project is avoiding 14 | technical informations, such as primary keys. 15 | 16 | ## Lombok 17 | Lombok seems to be a first-class citizen of Spring Boot nowadays, so let's use it to reduce the amount of boiler-plate code. 18 | 19 | I'm also experimenting the Lombok's `val` keyword. It's a pity that `val` didn't make it to the Java language, 20 | but it seems `val` is a useful workaround. There's even IntelliJ support for it. 21 | 22 | ## Using records instead of Lombok 23 | By definition, DTOs are immutable data classes, so it sounds like a good idea to implement the DTO as a record. Mapstruct 24 | generates the ugly parts of the code (i.e. using the constructur which may have dozens of parameters) 25 | and hides it from you. So you're working with code that looks clean enough. 26 | 27 | The only challenge is you need to convince your code formatter to format the record definition nicely. By default, 28 | the entire record definition is single line, which looks a bit weird for a data class. 29 | 30 | ## Creating records directly from a JPA query in a repository 31 | You can also return records from a repository: 32 | ```java 33 | @Query(""" 34 | SELECT new de.beyondjava.business.projects.ProjectDto( 35 | p.name, p.description) 36 | FROM Project p 37 | WHERE LOWER(p.name) = LOWER(:projectName) 38 | """) 39 | List findProjectDtosByProjectName( 40 | @Param("projectName") String projectName); 41 | ``` 42 | 43 | ## Java Bean Validation 44 | Controllers, DTOs, and Entities make use of the bean validation annotations. There's also a global error handler 45 | converting the technical messages Spring generates to more user-friendly messages. 46 | 47 | ## Global and local error handlers 48 | The project has two global error handlers, one of them catching validation errors, the other catching the 49 | "ElementNotFoundException" thrown by Optional.orElseThrow(). 50 | 51 | For the sake of demonstration, there's also an exception handler in a controller. 52 | 53 | ## Getting the application up and running 54 | It's a simple Maven project running on Java 17. You can start it 55 | - by running the commmand mvn spring-boot:run 56 | - or by starting the class de.beyondjava.SnakeCaseApplication. 57 | 58 | There's also a small Postman collection of the API calls. -------------------------------------------------------------------------------- /snake_case_parameters/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.0 9 | 10 | 11 | de.beyondjava 12 | snake_case_parameters 13 | 0.0.1-SNAPSHOT 14 | ExploringMapStruct 15 | ExploringMapStruct 16 | 17 | 17 18 | 1.5.1.Final 19 | 1.18.24 20 | 0.2.0 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-validation 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-devtools 41 | runtime 42 | true 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | 52 | 53 | org.mapstruct 54 | mapstruct 55 | ${org.mapstruct.version} 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | ${org.projectlombok.version} 61 | 62 | 63 | com.h2database 64 | h2 65 | runtime 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-compiler-plugin 74 | 3.8.1 75 | 76 | 17 77 | 17 78 | 79 | 80 | org.projectlombok 81 | lombok 82 | ${org.projectlombok.version} 83 | 84 | 85 | 86 | org.projectlombok 87 | lombok-mapstruct-binding 88 | 0.2.0 89 | 90 | 91 | org.mapstruct 92 | mapstruct-processor 93 | ${org.mapstruct.version} 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-maven-plugin 102 | 103 | 104 | 105 | org.projectlombok 106 | lombok 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /snake_case_parameters/snake_case.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "5b622f93-dc31-4430-90e7-adb6f4e68a89", 4 | "name": "Spring-data-rest", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "get employees", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "localhost:8080/employees/", 15 | "host": [ 16 | "localhost" 17 | ], 18 | "port": "8080", 19 | "path": [ 20 | "employees", 21 | "" 22 | ] 23 | } 24 | }, 25 | "response": [] 26 | }, 27 | { 28 | "name": "get employee 1", 29 | "request": { 30 | "method": "GET", 31 | "header": [], 32 | "url": { 33 | "raw": "localhost:8080/authors", 34 | "host": [ 35 | "localhost" 36 | ], 37 | "port": "8080", 38 | "path": [ 39 | "authors" 40 | ] 41 | } 42 | }, 43 | "response": [] 44 | }, 45 | { 46 | "name": "post employee", 47 | "request": { 48 | "method": "POST", 49 | "header": [], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Riccia\"\n}", 53 | "options": { 54 | "raw": { 55 | "language": "json" 56 | } 57 | } 58 | }, 59 | "url": { 60 | "raw": "localhost:8080/employees/", 61 | "host": [ 62 | "localhost" 63 | ], 64 | "port": "8080", 65 | "path": [ 66 | "employees", 67 | "" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "post employee error message", 75 | "request": { 76 | "method": "POST", 77 | "header": [], 78 | "body": { 79 | "mode": "raw", 80 | "raw": "{\n \"firstName\": \"Pat\",\n \"lastName\": \"Ty\"\n}", 81 | "options": { 82 | "raw": { 83 | "language": "json" 84 | } 85 | } 86 | }, 87 | "url": { 88 | "raw": "localhost:8080/employees/", 89 | "host": [ 90 | "localhost" 91 | ], 92 | "port": "8080", 93 | "path": [ 94 | "employees", 95 | "" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | }, 101 | { 102 | "name": "post employee another error message", 103 | "request": { 104 | "method": "POST", 105 | "header": [], 106 | "body": { 107 | "mode": "raw", 108 | "raw": "{\n \"firstName\": \"Pat\",\n \"surname\": \"Riccia\"\n}", 109 | "options": { 110 | "raw": { 111 | "language": "json" 112 | } 113 | } 114 | }, 115 | "url": { 116 | "raw": "localhost:8080/employees/", 117 | "host": [ 118 | "localhost" 119 | ], 120 | "port": "8080", 121 | "path": [ 122 | "employees", 123 | "" 124 | ] 125 | } 126 | }, 127 | "response": [] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/SnakeCaseApplication.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava; 2 | 3 | import de.beyondjava.tech.startup.Startup; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | public class SnakeCaseApplication { 10 | 11 | @Autowired 12 | private Startup startup; 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(SnakeCaseApplication.class, args); 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/Employee.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import de.beyondjava.business.projects.Project; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | import javax.persistence.*; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.Size; 12 | import java.util.List; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Employee { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "You do have a first name, don't you?") 25 | @Size(min = 3, max = 20) 26 | private String firstName; 27 | 28 | @NonNull 29 | @NotBlank(message = "Don't be shy and tell us your last name.") 30 | @Size(min = 3, max = 20) 31 | private String lastName; 32 | 33 | @NonNull 34 | @OneToMany(cascade = CascadeType.ALL) 35 | private List projects; 36 | } 37 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.validation.Valid; 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping("/employees") 13 | @Data 14 | @AllArgsConstructor 15 | public class EmployeeController { 16 | 17 | private EmployeeService employeeService; 18 | 19 | @PostMapping("/") 20 | @ResponseStatus(HttpStatus.CREATED) 21 | public void save(@Valid @RequestBody EmployeeDto dto) { 22 | employeeService.save(dto); 23 | } 24 | 25 | @GetMapping("/") 26 | @ResponseStatus(HttpStatus.OK) 27 | public List findAll() { 28 | return employeeService.findAll(); 29 | } 30 | 31 | @GetMapping("/query") 32 | @ResponseStatus(HttpStatus.OK) 33 | public EmployeeDto findByName(@RequestParam String firstName) { 34 | return employeeService.findEmployeeByFirstName(firstName); 35 | } 36 | 37 | @GetMapping("/{id}") 38 | @ResponseStatus(HttpStatus.OK) 39 | public EmployeeDto findOne(@PathVariable("id") int id) { 40 | return employeeService.findById(id); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/EmployeeDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.Size; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @RequiredArgsConstructor 14 | public class EmployeeDto { 15 | @NonNull 16 | @NotBlank(message = "You do have a first name, don't you?") 17 | @Size(min = 3, max = 20) 18 | private String firstName; 19 | 20 | @NonNull 21 | @NotBlank(message = "Don't be shy and tell us your last name.") 22 | @Size(min = 3, max = 20) 23 | private String lastName; 24 | } 25 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/EmployeeMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface EmployeeMapper { 9 | EmployeeDto entityToDTO(Employee employee); 10 | 11 | List entityToDTO(Iterable employee); 12 | 13 | Employee dtoToEntity(EmployeeDto employee); 14 | 15 | List dtoToEntity(Iterable employees); 16 | 17 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface EmployeeRepository extends CrudRepository { 9 | List findEmployeeByFirstNameContaining(String start); 10 | 11 | Optional findEmployeeByFirstName(String firstName); 12 | } 13 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/employees/EmployeeService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.employees; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class EmployeeService { 14 | private EmployeeMapper employeeMapper; 15 | 16 | private EmployeeRepository employeeRepository; 17 | 18 | public void save(EmployeeDto employeeDto) { 19 | val employee = employeeMapper.dtoToEntity(employeeDto); 20 | employeeRepository.save(employee); 21 | } 22 | 23 | public List findAll() { 24 | val employees = employeeRepository.findAll(); 25 | return employeeMapper.entityToDTO(employees); 26 | } 27 | 28 | public EmployeeDto findById(long id) { 29 | val employee = employeeRepository.findById(id).orElseThrow(); 30 | return employeeMapper.entityToDTO(employee); 31 | } 32 | 33 | public EmployeeDto findEmployeeByFirstName(String firstName) { 34 | val employee = employeeRepository.findEmployeeByFirstName(firstName).orElseThrow(); 35 | return employeeMapper.entityToDTO(employee); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/Project.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.Id; 11 | import javax.validation.constraints.NotBlank; 12 | import javax.validation.constraints.Size; 13 | 14 | @Entity 15 | @Data 16 | @NoArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Project { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @NonNull 24 | @NotBlank(message = "Your project needs a name.") 25 | @Size(min = 3, max = 30) 26 | private String name; 27 | 28 | @NonNull 29 | @NotBlank(message = "Tell others about your project.") 30 | @Size(min = 1, max = 2000) 31 | private String description; 32 | } 33 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/ProjectController.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | @RestController 15 | @RequestMapping("/projects") 16 | @Data 17 | @AllArgsConstructor 18 | public class ProjectController { 19 | 20 | private ProjectService projectService; 21 | 22 | @PostMapping("/") 23 | @ResponseStatus(HttpStatus.CREATED) 24 | public void save(@Valid @RequestBody ProjectDto dto) { 25 | projectService.save(dto); 26 | } 27 | 28 | @GetMapping("/") 29 | @ResponseStatus(HttpStatus.OK) 30 | public List findAll() { 31 | return projectService.findAll(); 32 | } 33 | 34 | @GetMapping("/find") 35 | @ResponseStatus(HttpStatus.OK) 36 | public List findProjectsByProjectName(@RequestParam String projectName) { 37 | return projectService.findProjectsByProjectName(projectName); 38 | } 39 | 40 | @GetMapping("/{id}") 41 | @ResponseStatus(HttpStatus.OK) 42 | public ProjectDto findOne(@PathVariable("id") int id) { 43 | return projectService.findById(id); 44 | } 45 | 46 | /** 47 | * This is an error handler specific to this controller. It overrides the global error handler. 48 | * In a real-world application, this particular combination of error handlers is almost 49 | * certainly undesirable: the global error handler returns a complex JSON object, while the 50 | * local error handler returns a string. 51 | * 52 | * @param ex 53 | * @return 54 | */ 55 | @ResponseStatus(HttpStatus.BAD_REQUEST) 56 | @ExceptionHandler(MethodArgumentNotValidException.class) 57 | public String handleValidationExceptions( 58 | MethodArgumentNotValidException ex) { 59 | var result = "There's a validation error in the project."; 60 | var errors = ex.getBindingResult().getAllErrors(); 61 | var details = errors.stream().map(error -> { 62 | String fieldName = ((FieldError) error).getField(); 63 | String errorMessage = error.getDefaultMessage(); 64 | return "\n" + fieldName + ":" + errorMessage; 65 | }).collect(Collectors.joining()); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/ProjectDto.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.Size; 6 | 7 | public record ProjectDto( 8 | @NotBlank(message = "Your project needs a name.") 9 | @Size(min = 3, max = 30) 10 | String name, 11 | 12 | @NotBlank(message = "Tell others about your project.") 13 | @Size(min = 1, max = 2000) 14 | String description) { 15 | } 16 | 17 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/ProjectMapper.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface ProjectMapper { 9 | ProjectDto entityToDTO(Project project); 10 | 11 | List entityToDTO(Iterable project); 12 | 13 | Project dtoToEntity(ProjectDto project); 14 | 15 | List dtoToEntity(Iterable projects); 16 | 17 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | import java.util.List; 8 | 9 | public interface ProjectRepository extends CrudRepository { 10 | @Query(""" 11 | SELECT new de.beyondjava.business.projects.ProjectDto(p.name, p.description) 12 | FROM Project p 13 | WHERE LOWER(p.name) = LOWER(:projectName) 14 | """) 15 | List findProjectDtosByProjectName(@Param("projectName") String projectName); 16 | } 17 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/business/projects/ProjectService.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.business.projects; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.val; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @Data 12 | @AllArgsConstructor 13 | public class ProjectService { 14 | private ProjectMapper projectMapper; 15 | 16 | private ProjectRepository projectRepository; 17 | 18 | public void save(ProjectDto projectDto) { 19 | val project = projectMapper.dtoToEntity(projectDto); 20 | projectRepository.save(project); 21 | } 22 | 23 | public List findAll() { 24 | val projects = projectRepository.findAll(); 25 | return projectMapper.entityToDTO(projects); 26 | } 27 | 28 | public List findProjectsByProjectName(String projectName) { 29 | return projectRepository.findProjectDtosByProjectName(projectName); 30 | } 31 | 32 | public ProjectDto findById(long id) { 33 | val project = projectRepository.findById(id); 34 | return projectMapper.entityToDTO(project.orElseThrow()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/SnakeCaseApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.filter.OncePerRequestFilter; 6 | 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletRequestWrapper; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.util.Collections; 14 | import java.util.Enumeration; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | @Configuration 19 | public class SnakeCaseApplicationConfiguration { 20 | // found at https://gist.github.com/azhawkes/3db84b194b3e47423df2 21 | @Bean 22 | public OncePerRequestFilter snakeCaseConverterFilter() { 23 | return new OncePerRequestFilter() { 24 | @Override 25 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 26 | final Map parameters = new ConcurrentHashMap<>(); 27 | 28 | for (String param : request.getParameterMap().keySet()) { 29 | String camelCaseParam = snakeToCamel(param); 30 | 31 | parameters.put(camelCaseParam, request.getParameterValues(param)); 32 | parameters.put(param, request.getParameterValues(param)); 33 | } 34 | 35 | filterChain.doFilter(new HttpServletRequestWrapper(request) { 36 | @Override 37 | public String getParameter(String name) { 38 | return parameters.containsKey(name) ? parameters.get(name)[0] : null; 39 | } 40 | 41 | @Override 42 | public Enumeration getParameterNames() { 43 | return Collections.enumeration(parameters.keySet()); 44 | } 45 | 46 | @Override 47 | public String[] getParameterValues(String name) { 48 | return parameters.get(name); 49 | } 50 | 51 | @Override 52 | public Map getParameterMap() { 53 | return parameters; 54 | } 55 | }, response); 56 | } 57 | }; 58 | } 59 | 60 | // found at https://www.geeksforgeeks.org/convert-snake-case-string-to-camel-case-in-java/#:~:text=replaceFirst()%20method%20to%20convert,next%20letter%20of%20the%20underscore. 61 | // Function to convert the string 62 | // from snake case to camel case 63 | public static String snakeToCamel(String str) 64 | { 65 | // Run a loop till string 66 | // string contains underscore 67 | while (str.contains("_")) { 68 | 69 | // Replace the first occurrence 70 | // of letter that present after 71 | // the underscore, to capitalize 72 | // form of next letter of underscore 73 | str = str 74 | .replaceFirst( 75 | "_[a-z]", 76 | String.valueOf( 77 | Character.toUpperCase( 78 | str.charAt( 79 | str.indexOf("_") + 1)))); 80 | } 81 | 82 | // Return string 83 | return str; 84 | } 85 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.val; 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.web.bind.MethodArgumentNotValidException; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | 11 | import java.util.List; 12 | 13 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 14 | 15 | /** 16 | * Kudos https://stackoverflow.com/questions/33663801/how-do-i-customize-default-error-message-from-spring-valid-validation 17 | * Kudos http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ 18 | */ 19 | @Order(Ordered.HIGHEST_PRECEDENCE) 20 | @ControllerAdvice 21 | public class GlobalMethodArgumentNotValidExceptionHandler { 22 | 23 | // @ResponseStatus(BAD_REQUEST) 24 | @ResponseBody 25 | @ExceptionHandler(MethodArgumentNotValidException.class) 26 | public ValidationErrorList methodArgumentNotValidException(MethodArgumentNotValidException ex) { 27 | var result = ex.getBindingResult(); 28 | var fieldErrors = result.getFieldErrors(); 29 | return processFieldErrors(fieldErrors); 30 | } 31 | 32 | private ValidationErrorList processFieldErrors(List fieldErrors) { 33 | val error = new ValidationErrorList(BAD_REQUEST.value(), fieldErrors.size() + " validation error(s) found."); 34 | for (var fieldError : fieldErrors) { 35 | error.getFieldErrors().add(new ValidationError(fieldError.getObjectName(), fieldError.getField(), 36 | fieldError.getDefaultMessage())); 37 | } 38 | return error; 39 | } 40 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/errorHandlers/GlobalMethodNoSuchElementFoundExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import java.util.NoSuchElementException; 11 | 12 | import static org.springframework.http.HttpStatus.NOT_FOUND; 13 | 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | @ControllerAdvice 16 | public class GlobalMethodNoSuchElementFoundExceptionHandler { 17 | @ExceptionHandler(NoSuchElementException.class) 18 | public ResponseEntity noSuchElementException(NoSuchElementException ex) { 19 | return new ResponseEntity("Couldn't find the resource", NOT_FOUND); 20 | } 21 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/errorHandlers/ValidationError.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @ToString 10 | public class ValidationError { 11 | private String object; 12 | private String field; 13 | private String message; 14 | } 15 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/errorHandlers/ValidationErrorList.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.errorHandlers; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | @ToString 13 | public class ValidationErrorList { 14 | private final int status; 15 | private final String message; 16 | private final List fieldErrors = new ArrayList<>(); 17 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/main/java/de/beyondjava/tech/startup/Startup.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.tech.startup; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import de.beyondjava.business.employees.EmployeeMapper; 5 | import de.beyondjava.business.employees.EmployeeRepository; 6 | import de.beyondjava.business.projects.Project; 7 | import lombok.AllArgsConstructor; 8 | import lombok.val; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | @Configuration 17 | @AllArgsConstructor 18 | public class Startup { 19 | private EmployeeRepository employeeRepository; 20 | 21 | private EmployeeMapper employeeMapper; 22 | 23 | @Bean 24 | public CommandLineRunner run(EmployeeRepository repository) { 25 | return (args) -> { 26 | this.insertFourEmployees(repository); 27 | var employees = repository.findAll(); 28 | var employeeDTOs = employeeMapper.entityToDTO(employees); 29 | System.out.println(employeeDTOs); 30 | }; 31 | } 32 | 33 | public void insertFourEmployees(EmployeeRepository repository) { 34 | val cafelito = new Project("Cafelito", """ 35 | Demo project used at conferences, 36 | see http://trishagee.github.io/presentation/angularjs_html5_groovy_java_mongodb_wcpgw/ 37 | """); 38 | repository.save(new Employee("Dalia", "Abo Sheasha", Collections.emptyList())); 39 | repository.save(new Employee("Trisha", "Gee", List.of(cafelito))); 40 | repository.save(new Employee("Helen", "Scott", Collections.emptyList())); 41 | repository.save(new Employee("Mala", "Gupta", Collections.emptyList())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:file:./data/myDB 2 | spring.datasource.driver-class-name=org.h2.Driver 3 | spring.datasource.username=sa 4 | spring.datasource.password= 5 | spring.jpa.hibernate.ddl-auto=create-drop 6 | spring.sql.init.mode=ALWAYS 7 | spring.jpa.defer-datasource-initialization=true 8 | -------------------------------------------------------------------------------- /snake_case_parameters/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM EMPLOYEE_PROJECTS; 2 | DELETE FROM EMPLOYEE; 3 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-1, 'Anna', 'Konda'); 4 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-2, 'Anno', 'Nymous'); 5 | INSERT INTO EMPLOYEE(id, first_name, last_name) VALUES (-3, 'Beyond', 'Java'); 6 | 7 | DELETE FROM PROJECT; 8 | INSERT INTO PROJECT(id, name, description) VALUES (-1, 'ngx-extended-pdf-viewer', 'A cute little PDF viewer for Angular.'); 9 | 10 | INSERT INTO EMPLOYEE_PROJECTS(employee_id, projects_id) VALUES(-3, -1); -------------------------------------------------------------------------------- /snake_case_parameters/src/test/java/de/beyondjava/employees/EmployeeTest.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.employees; 2 | 3 | import de.beyondjava.business.employees.Employee; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | class EmployeeTest { 7 | @BeforeEach 8 | public void init() { 9 | var employee = new Employee(); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /snake_case_parameters/src/test/java/de/beyondjava/exploringmapstruct/ExploringMapStructApplicationTests.java: -------------------------------------------------------------------------------- 1 | package de.beyondjava.exploringmapstruct; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ExploringMapStructApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | **/*.iml 25 | .idea 26 | target 27 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "java", 5 | "name": "Spring Boot-MetricscaptureApplication", 6 | "request": "launch", 7 | "cwd": "${workspaceFolder}", 8 | "console": "internalConsole", 9 | "mainClass": "io.pratik.metricscapture.MetricscaptureApplication", 10 | "projectName": "metricscapture", 11 | "args": "", 12 | "envFile": "${workspaceFolder}/.env" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/README.md: -------------------------------------------------------------------------------- 1 | # Publishing Metrics from Spring Boot Application to Amazon CloudWatch 2 | 3 | This is a slightly modified version of [the demo code written by Pratik Das](https://github.com/thombergs/code-examples/tree/master/aws/springcloudwatch). 4 | 5 | Example code for using Spring Boot Application with Amazon CloudWatch 6 | 7 | ## Blog posts 8 | 9 | Blog posts about this topic: 10 | 11 | * [Publishing Metrics from Spring Boot Application to Amazon CloudWatch](https://reflectoring.io/spring-aws-cloudwatch/) 12 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.0 10 | 11 | 12 | net.beyondjava 13 | spring-boot-cloudwatch-http-metrics 14 | 0.0.1-SNAPSHOT 15 | spring-boot-cloudwatch-http-metrics 16 | Demo project for capturing cloudwatch metrics in Spring Boot 17 | 18 | 11 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-actuator-autoconfigure 29 | 30 | 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | io.micrometer 45 | micrometer-core 46 | 47 | 48 | 52 | 53 | io.micrometer 54 | micrometer-registry-cloudwatch2 55 | 56 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-test 63 | test 64 | 65 | 66 | 67 | 68 | 69 | io.awspring.cloud 70 | spring-cloud-aws-dependencies 71 | 2.3.0 72 | pom 73 | import 74 | 75 | 76 | software.amazon.awssdk 77 | bom 78 | 2.17.209 79 | pom 80 | import 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-maven-plugin 90 | 91 | 92 | 93 | org.projectlombok 94 | lombok 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/AppConfig.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Duration; 7 | import java.util.Map; 8 | 9 | import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; 10 | import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; 11 | import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; 12 | import io.micrometer.core.instrument.binder.logging.Log4j2Metrics; 13 | import io.micrometer.core.instrument.binder.system.ProcessorMetrics; 14 | import io.micrometer.core.instrument.logging.LoggingMeterRegistry; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.scheduling.annotation.EnableScheduling; 18 | 19 | import io.micrometer.cloudwatch2.CloudWatchConfig; 20 | import io.micrometer.cloudwatch2.CloudWatchMeterRegistry; 21 | import io.micrometer.core.instrument.Clock; 22 | import io.micrometer.core.instrument.MeterRegistry; 23 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 24 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 25 | import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; 26 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 27 | import software.amazon.awssdk.regions.Region; 28 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 29 | 30 | /** 31 | * @author pratikdas 32 | * 33 | */ 34 | @Configuration 35 | public class AppConfig { 36 | @Bean 37 | public CloudWatchAsyncClient cloudWatchAsyncClient() { 38 | 39 | // AwsBasicCredentials awsCreds = AwsBasicCredentials.create("accessid", "secretaccesskey"); 40 | // var provider = StaticCredentialsProvider.create(awsCreds); 41 | 42 | // return CloudWatchAsyncClient.builder().region(Region.EU_CENTRAL_1) 43 | // .credentialsProvider(provider).build(); 44 | 45 | 46 | return CloudWatchAsyncClient.builder().region(Region.EU_CENTRAL_1) 47 | .credentialsProvider(ProfileCredentialsProvider.create("default")).build(); 48 | } 49 | 50 | @Bean 51 | public MeterRegistry getMeterRegistry() { 52 | CloudWatchConfig cloudWatchConfig = setupCloudWatchConfig(); 53 | 54 | MeterRegistry meterRegistry = new CloudWatchMeterRegistry(cloudWatchConfig, Clock.SYSTEM, 55 | cloudWatchAsyncClient()); 56 | meterRegistry.config().commonTags("manual", "learning"); 57 | new JvmMemoryMetrics().bindTo(meterRegistry); 58 | new JvmGcMetrics().bindTo(meterRegistry); 59 | new JvmThreadMetrics().bindTo(meterRegistry); 60 | new ProcessorMetrics().bindTo(meterRegistry); 61 | // new Log4j2Metrics().bindTo(meterRegistry); 62 | 63 | return meterRegistry; 64 | } 65 | 66 | private CloudWatchConfig setupCloudWatchConfig() { 67 | CloudWatchConfig cloudWatchConfig = new CloudWatchConfig() { 68 | 69 | private Map configuration 70 | = Map.of("cloudwatch.namespace", 71 | "BeyondJava/dev/jvm", 72 | "cloudwatch.step", Duration.ofSeconds(15).toString()); 73 | 74 | @Override 75 | public String get(String key) { 76 | return configuration.get(key); 77 | } 78 | }; 79 | return cloudWatchConfig; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/MetricPublisher.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Instant; 7 | import java.time.ZoneOffset; 8 | import java.time.ZonedDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import io.pratik.metricscapture.models.MetricTag; 17 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 18 | import software.amazon.awssdk.services.cloudwatch.model.CloudWatchException; 19 | import software.amazon.awssdk.services.cloudwatch.model.Dimension; 20 | import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; 21 | import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; 22 | import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; 23 | 24 | /** 25 | * @author pratikdas 26 | * 27 | */ 28 | @Service 29 | public class MetricPublisher { 30 | 31 | private CloudWatchAsyncClient cloudWatchAsyncClient; 32 | 33 | 34 | @Autowired 35 | public MetricPublisher(CloudWatchAsyncClient cloudWatchAsyncClient) { 36 | super(); 37 | this.cloudWatchAsyncClient = cloudWatchAsyncClient; 38 | } 39 | 40 | public void putMetricData(final String nameSpace, 41 | final String metricName, 42 | final Double dataPoint, 43 | final List metricTags) { 44 | 45 | try { 46 | 47 | List dimensions = metricTags 48 | .stream() 49 | .map((metricTag)->{ 50 | return Dimension 51 | .builder() 52 | .name(metricTag.getName()) 53 | .value(metricTag.getValue()) 54 | .build(); 55 | }).collect(Collectors.toList()); 56 | 57 | // Set an Instant object 58 | String time = ZonedDateTime 59 | .now(ZoneOffset.UTC) 60 | .format(DateTimeFormatter.ISO_INSTANT); 61 | Instant instant = Instant.parse(time); 62 | 63 | MetricDatum datum = MetricDatum 64 | .builder() 65 | .metricName(metricName) 66 | .unit(StandardUnit.NONE) 67 | .value(dataPoint) 68 | .timestamp(instant) 69 | .dimensions(dimensions) 70 | .build(); 71 | 72 | PutMetricDataRequest request = 73 | PutMetricDataRequest 74 | .builder() 75 | .namespace(nameSpace) 76 | .metricData(datum) 77 | .build(); 78 | 79 | cloudWatchAsyncClient.putMetricData(request); 80 | 81 | } catch (CloudWatchException e) { 82 | System.err.println(e.awsErrorDetails().errorMessage()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/MetricscaptureApplication.java: -------------------------------------------------------------------------------- 1 | package io.pratik.metricscapture; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.client.RestTemplateBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.http.converter.StringHttpMessageConverter; 8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | @SpringBootApplication 13 | @EnableScheduling 14 | public class MetricscaptureApplication { 15 | 16 | @Bean 17 | RestTemplate restTemplate(RestTemplateBuilder builder) { 18 | return builder 19 | .messageConverters(new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter()) 20 | .build(); 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(MetricscaptureApplication.class, args); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/PricingEngine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.util.Random; 7 | 8 | import io.micrometer.core.annotation.Timed; 9 | import io.micrometer.core.instrument.MeterRegistry; 10 | import io.micrometer.core.instrument.Timer; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.stereotype.Service; 14 | 15 | /** 16 | * @author pratikdas 17 | * 18 | */ 19 | @Service 20 | public class PricingEngine { 21 | 22 | private Double price; 23 | 24 | @Autowired 25 | private MeterRegistry registry; 26 | 27 | public Double getProductPrice() { 28 | return price; 29 | 30 | } 31 | 32 | @Timed("myFavoriteTimer") 33 | @Scheduled(fixedRate = 10000, initialDelay = 10) 34 | public void computePrice() { 35 | Timer timer = registry.timer("app.event"); 36 | timer.record(() -> { 37 | Random random = new Random(); 38 | price = random.nextDouble() * 100; 39 | System.out.println("computing price "+price); 40 | try { 41 | Thread.sleep(100); 42 | } catch (InterruptedException e) { 43 | e.printStackTrace(); 44 | } 45 | System.out.println("done computing price "+price); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/ProductController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Duration; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import io.micrometer.core.annotation.Timed; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.ResponseBody; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import io.micrometer.core.instrument.Counter; 18 | import io.micrometer.core.instrument.Gauge; 19 | import io.micrometer.core.instrument.MeterRegistry; 20 | import io.micrometer.core.instrument.Timer; 21 | import io.pratik.metricscapture.models.Order; 22 | import io.pratik.metricscapture.models.Product; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.springframework.web.client.RestTemplate; 25 | 26 | /** 27 | * @author pratikdas 28 | * 29 | */ 30 | @RestController 31 | @Slf4j 32 | public class ProductController { 33 | private Counter pageViewsCounter; 34 | private Timer productTimer; 35 | private Gauge priceGauge; 36 | 37 | private MeterRegistry meterRegistry; 38 | 39 | private PricingEngine pricingEngine; 40 | 41 | @Autowired 42 | ProductController(MeterRegistry meterRegistry, PricingEngine pricingEngine){ 43 | 44 | this.meterRegistry = meterRegistry; 45 | this.pricingEngine = pricingEngine; 46 | 47 | priceGauge = Gauge 48 | .builder("product.price", pricingEngine , 49 | (pe)->{return pe != null? pe.getProductPrice() : null;}) 50 | .description("Product price") 51 | .baseUnit("ms") 52 | .register(meterRegistry); 53 | 54 | pageViewsCounter = meterRegistry 55 | .counter("PAGE_VIEWS.ProductList"); 56 | 57 | productTimer = meterRegistry 58 | .timer("execution.time.fetchProducts"); 59 | 60 | } 61 | 62 | 63 | 64 | 65 | @GetMapping("/products") 66 | @ResponseBody 67 | @Timed("myFavoriteTimer2") 68 | public List fetchProducts() { 69 | long startTime = System.currentTimeMillis(); 70 | 71 | List products = fetchProductsFromStore(); 72 | 73 | // increment page views counter 74 | // pageViewsCounter.increment(); 75 | 76 | // record time to execute the method 77 | System.out.println("productTimer "+productTimer + " " + pageViewsCounter + " " + priceGauge); 78 | // productTimer.record(Duration.ofMillis(System.currentTimeMillis() - startTime)); 79 | 80 | return products; 81 | } 82 | 83 | private List fetchProductsFromStore(){ 84 | List products = new ArrayList(); 85 | products.add(Product.builder().name("Television").build()); 86 | products.add(Product.builder().name("Book").build()); 87 | 88 | products.forEach(product -> { 89 | final String uri = "http://localhost:8080/products/" + product.getName() + "/available"; 90 | 91 | RestTemplate restTemplate = new RestTemplate(); 92 | int result = restTemplate.getForObject(uri, Integer.class); 93 | product.setAvailableItems(result); 94 | }); 95 | 96 | return products; 97 | } 98 | 99 | 100 | 101 | } 102 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/ProductInventoryController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Duration; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import io.micrometer.core.annotation.Timed; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.ResponseBody; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import io.micrometer.core.instrument.Counter; 19 | import io.micrometer.core.instrument.Gauge; 20 | import io.micrometer.core.instrument.MeterRegistry; 21 | import io.micrometer.core.instrument.Timer; 22 | import io.pratik.metricscapture.models.Order; 23 | import io.pratik.metricscapture.models.Product; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | /** 27 | * @author Stephan Rauh 28 | * 29 | */ 30 | @RestController 31 | @Slf4j 32 | public class ProductInventoryController { 33 | 34 | @Autowired 35 | ProductInventoryController(){} 36 | 37 | @GetMapping("/products/{productName}/available") 38 | @ResponseBody 39 | public int fetchProducts(@PathVariable String productName) { 40 | if ("Book".equals(productName)) { 41 | return 50; 42 | } 43 | if ("Television".equals(productName)) { 44 | return 10; 45 | } 46 | 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/models/MetricTag.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | /** 7 | * @author pratikdas 8 | * 9 | */ 10 | public class MetricTag { 11 | private String name; 12 | private String value; 13 | public MetricTag(String name, String value) { 14 | super(); 15 | this.name = name; 16 | this.value = value; 17 | } 18 | public String getName() { 19 | return name; 20 | } 21 | public String getValue() { 22 | return value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/models/Order.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | import java.util.List; 7 | 8 | import lombok.Builder; 9 | 10 | /** 11 | * @author pratikdas 12 | * 13 | */ 14 | @Builder 15 | public class Order { 16 | private List products; 17 | 18 | private String orderDate; 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/java/io/pratik/metricscapture/models/Product.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | /** 10 | * @author pratikdas 11 | * 12 | */ 13 | @Builder 14 | @Data 15 | public class Product { 16 | private String name; 17 | private Double price; 18 | private int availableItems; 19 | } 20 | -------------------------------------------------------------------------------- /spring-cloudwatch-http-observer/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # cloud.aws.credentials.profile-name=pratikpoc 2 | cloud.aws.region.auto=false 3 | cloud.aws.region.static=eu-central-1 4 | # management.metrics.enable.jvm=true 5 | # management.endpoints.web.exposure.include=health,metrics,cloudwatch 6 | management.endpoints.web.exposure.include=health,cloudwatch 7 | 8 | logging.level.io.micrometer=DEBUG 9 | management.metrics.web.server.request.autotime.enabled=true 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /springcloudwatch/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /springcloudwatch/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /springcloudwatch/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "java", 5 | "name": "Spring Boot-MetricscaptureApplication", 6 | "request": "launch", 7 | "cwd": "${workspaceFolder}", 8 | "console": "internalConsole", 9 | "mainClass": "io.pratik.metricscapture.MetricscaptureApplication", 10 | "projectName": "metricscapture", 11 | "args": "", 12 | "envFile": "${workspaceFolder}/.env" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /springcloudwatch/README.md: -------------------------------------------------------------------------------- 1 | # Publishing Metrics from Spring Boot Application to Amazon CloudWatch 2 | 3 | This is a slightly modified version of [the demo code written by Pratik Das](https://github.com/thombergs/code-examples/tree/master/aws/springcloudwatch). 4 | 5 | Example code for using Spring Boot Application with Amazon CloudWatch 6 | 7 | ## Blog posts 8 | 9 | Blog posts about this topic: 10 | 11 | * [Publishing Metrics from Spring Boot Application to Amazon CloudWatch](https://reflectoring.io/spring-aws-cloudwatch/) 12 | -------------------------------------------------------------------------------- /springcloudwatch/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /springcloudwatch/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.0 10 | 11 | 12 | io.pratik 13 | metricscapture 14 | 0.0.1-SNAPSHOT 15 | metricscapture 16 | Demo project for capturing cloudwatch metrics in Spring Boot 17 | 18 | 11 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-actuator-autoconfigure 29 | 30 | 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | io.micrometer 45 | micrometer-core 46 | 47 | 48 | 52 | 53 | io.micrometer 54 | micrometer-registry-cloudwatch2 55 | 56 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-test 63 | test 64 | 65 | 66 | 67 | 68 | 69 | io.awspring.cloud 70 | spring-cloud-aws-dependencies 71 | 2.3.0 72 | pom 73 | import 74 | 75 | 76 | software.amazon.awssdk 77 | bom 78 | 2.17.209 79 | pom 80 | import 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-maven-plugin 90 | 91 | 92 | 93 | org.projectlombok 94 | lombok 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/AppConfig.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Duration; 7 | import java.util.Map; 8 | 9 | import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; 10 | import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; 11 | import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; 12 | import io.micrometer.core.instrument.binder.logging.Log4j2Metrics; 13 | import io.micrometer.core.instrument.binder.system.ProcessorMetrics; 14 | import io.micrometer.core.instrument.logging.LoggingMeterRegistry; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.scheduling.annotation.EnableScheduling; 18 | 19 | import io.micrometer.cloudwatch2.CloudWatchConfig; 20 | import io.micrometer.cloudwatch2.CloudWatchMeterRegistry; 21 | import io.micrometer.core.instrument.Clock; 22 | import io.micrometer.core.instrument.MeterRegistry; 23 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 24 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 25 | import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; 26 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 27 | import software.amazon.awssdk.regions.Region; 28 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 29 | 30 | /** 31 | * @author pratikdas 32 | * 33 | */ 34 | @Configuration 35 | public class AppConfig { 36 | @Bean 37 | public CloudWatchAsyncClient cloudWatchAsyncClient() { 38 | 39 | // AwsBasicCredentials awsCreds = AwsBasicCredentials.create("accessid", "secretaccesskey"); 40 | // var provider = StaticCredentialsProvider.create(awsCreds); 41 | 42 | // return CloudWatchAsyncClient.builder().region(Region.EU_CENTRAL_1) 43 | // .credentialsProvider(provider).build(); 44 | 45 | 46 | return CloudWatchAsyncClient.builder().region(Region.EU_CENTRAL_1) 47 | .credentialsProvider(ProfileCredentialsProvider.create("default")).build(); 48 | } 49 | 50 | @Bean 51 | public MeterRegistry getMeterRegistry() { 52 | CloudWatchConfig cloudWatchConfig = setupCloudWatchConfig(); 53 | 54 | MeterRegistry meterRegistry = new CloudWatchMeterRegistry(cloudWatchConfig, Clock.SYSTEM, 55 | cloudWatchAsyncClient()); 56 | meterRegistry.config().commonTags("manual", "learning"); 57 | new JvmMemoryMetrics().bindTo(meterRegistry); 58 | new JvmGcMetrics().bindTo(meterRegistry); 59 | new JvmThreadMetrics().bindTo(meterRegistry); 60 | new ProcessorMetrics().bindTo(meterRegistry); 61 | // new Log4j2Metrics().bindTo(meterRegistry); 62 | 63 | return meterRegistry; 64 | } 65 | 66 | private CloudWatchConfig setupCloudWatchConfig() { 67 | CloudWatchConfig cloudWatchConfig = new CloudWatchConfig() { 68 | 69 | private Map configuration 70 | = Map.of("cloudwatch.namespace", 71 | "BeyondJava/dev/jvm", 72 | "cloudwatch.step", Duration.ofSeconds(15).toString()); 73 | 74 | @Override 75 | public String get(String key) { 76 | return configuration.get(key); 77 | } 78 | }; 79 | return cloudWatchConfig; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/MetricPublisher.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Instant; 7 | import java.time.ZoneOffset; 8 | import java.time.ZonedDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import io.pratik.metricscapture.models.MetricTag; 17 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 18 | import software.amazon.awssdk.services.cloudwatch.model.CloudWatchException; 19 | import software.amazon.awssdk.services.cloudwatch.model.Dimension; 20 | import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; 21 | import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; 22 | import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; 23 | 24 | /** 25 | * @author pratikdas 26 | * 27 | */ 28 | @Service 29 | public class MetricPublisher { 30 | 31 | private CloudWatchAsyncClient cloudWatchAsyncClient; 32 | 33 | 34 | @Autowired 35 | public MetricPublisher(CloudWatchAsyncClient cloudWatchAsyncClient) { 36 | super(); 37 | this.cloudWatchAsyncClient = cloudWatchAsyncClient; 38 | } 39 | 40 | public void putMetricData(final String nameSpace, 41 | final String metricName, 42 | final Double dataPoint, 43 | final List metricTags) { 44 | 45 | try { 46 | 47 | List dimensions = metricTags 48 | .stream() 49 | .map((metricTag)->{ 50 | return Dimension 51 | .builder() 52 | .name(metricTag.getName()) 53 | .value(metricTag.getValue()) 54 | .build(); 55 | }).collect(Collectors.toList()); 56 | 57 | // Set an Instant object 58 | String time = ZonedDateTime 59 | .now(ZoneOffset.UTC) 60 | .format(DateTimeFormatter.ISO_INSTANT); 61 | Instant instant = Instant.parse(time); 62 | 63 | MetricDatum datum = MetricDatum 64 | .builder() 65 | .metricName(metricName) 66 | .unit(StandardUnit.NONE) 67 | .value(dataPoint) 68 | .timestamp(instant) 69 | .dimensions(dimensions) 70 | .build(); 71 | 72 | PutMetricDataRequest request = 73 | PutMetricDataRequest 74 | .builder() 75 | .namespace(nameSpace) 76 | .metricData(datum) 77 | .build(); 78 | 79 | cloudWatchAsyncClient.putMetricData(request); 80 | 81 | } catch (CloudWatchException e) { 82 | System.err.println(e.awsErrorDetails().errorMessage()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/MetricscaptureApplication.java: -------------------------------------------------------------------------------- 1 | package io.pratik.metricscapture; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.client.RestTemplateBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.http.converter.StringHttpMessageConverter; 8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | @SpringBootApplication 13 | @EnableScheduling 14 | public class MetricscaptureApplication { 15 | 16 | @Bean 17 | RestTemplate restTemplate(RestTemplateBuilder builder) { 18 | return builder 19 | .messageConverters(new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter()) 20 | .build(); 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(MetricscaptureApplication.class, args); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/PricingEngine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.util.Random; 7 | 8 | import io.micrometer.core.annotation.Timed; 9 | import io.micrometer.core.instrument.MeterRegistry; 10 | import io.micrometer.core.instrument.Timer; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.stereotype.Service; 14 | 15 | /** 16 | * @author pratikdas 17 | * 18 | */ 19 | @Service 20 | public class PricingEngine { 21 | 22 | private Double price; 23 | 24 | @Autowired 25 | private MeterRegistry registry; 26 | 27 | public Double getProductPrice() { 28 | return price; 29 | 30 | } 31 | 32 | @Timed("myFavoriteTimer") 33 | @Scheduled(fixedRate = 10000, initialDelay = 10) 34 | public void computePrice() { 35 | Timer timer = registry.timer("app.event"); 36 | timer.record(() -> { 37 | Random random = new Random(); 38 | price = random.nextDouble() * 100; 39 | System.out.println("computing price "+price); 40 | try { 41 | Thread.sleep(100); 42 | } catch (InterruptedException e) { 43 | e.printStackTrace(); 44 | } 45 | System.out.println("done computing price "+price); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/ProductController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture; 5 | 6 | import java.time.Duration; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import io.micrometer.core.annotation.Timed; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Scheduled; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.ResponseBody; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import io.micrometer.core.instrument.Counter; 18 | import io.micrometer.core.instrument.Gauge; 19 | import io.micrometer.core.instrument.MeterRegistry; 20 | import io.micrometer.core.instrument.Timer; 21 | import io.pratik.metricscapture.models.Order; 22 | import io.pratik.metricscapture.models.Product; 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | /** 26 | * @author pratikdas 27 | * 28 | */ 29 | @RestController 30 | @Slf4j 31 | public class ProductController { 32 | private Counter pageViewsCounter; 33 | private Timer productTimer; 34 | private Gauge priceGauge; 35 | 36 | private MeterRegistry meterRegistry; 37 | 38 | private PricingEngine pricingEngine; 39 | 40 | @Autowired 41 | ProductController(MeterRegistry meterRegistry, PricingEngine pricingEngine){ 42 | 43 | this.meterRegistry = meterRegistry; 44 | this.pricingEngine = pricingEngine; 45 | 46 | priceGauge = Gauge 47 | .builder("product.price", pricingEngine , 48 | (pe)->{return pe != null? pe.getProductPrice() : null;}) 49 | .description("Product price") 50 | .baseUnit("ms") 51 | .register(meterRegistry); 52 | 53 | pageViewsCounter = meterRegistry 54 | .counter("PAGE_VIEWS.ProductList"); 55 | 56 | productTimer = meterRegistry 57 | .timer("execution.time.fetchProducts"); 58 | 59 | } 60 | 61 | 62 | 63 | 64 | @GetMapping("/products") 65 | @ResponseBody 66 | @Timed("myFavoriteTimer2") 67 | public List fetchProducts() { 68 | long startTime = System.currentTimeMillis(); 69 | 70 | List products = fetchProductsFromStore(); 71 | 72 | // increment page views counter 73 | // pageViewsCounter.increment(); 74 | 75 | // record time to execute the method 76 | System.out.println("productTimer "+productTimer + " " + pageViewsCounter + " " + priceGauge); 77 | // productTimer.record(Duration.ofMillis(System.currentTimeMillis() - startTime)); 78 | 79 | return products; 80 | } 81 | 82 | private List fetchProductsFromStore(){ 83 | List products = new ArrayList(); 84 | products.add(Product.builder().name("Television").build()); 85 | products.add(Product.builder().name("Book").build()); 86 | return products; 87 | } 88 | 89 | 90 | 91 | } 92 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/models/MetricTag.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | /** 7 | * @author pratikdas 8 | * 9 | */ 10 | public class MetricTag { 11 | private String name; 12 | private String value; 13 | public MetricTag(String name, String value) { 14 | super(); 15 | this.name = name; 16 | this.value = value; 17 | } 18 | public String getName() { 19 | return name; 20 | } 21 | public String getValue() { 22 | return value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/models/Order.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | import java.util.List; 7 | 8 | import lombok.Builder; 9 | 10 | /** 11 | * @author pratikdas 12 | * 13 | */ 14 | @Builder 15 | public class Order { 16 | private List products; 17 | 18 | private String orderDate; 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/java/io/pratik/metricscapture/models/Product.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package io.pratik.metricscapture.models; 5 | 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | /** 10 | * @author pratikdas 11 | * 12 | */ 13 | @Builder 14 | @Data 15 | public class Product { 16 | private String name; 17 | private Double price; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /springcloudwatch/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # cloud.aws.credentials.profile-name=pratikpoc 2 | cloud.aws.region.auto=false 3 | cloud.aws.region.static=eu-central-1 4 | # management.metrics.enable.jvm=true 5 | # management.endpoints.web.exposure.include=health,metrics,cloudwatch 6 | management.endpoints.web.exposure.include=health,cloudwatch 7 | 8 | logging.level.io.micrometer=DEBUG 9 | management.metrics.web.server.request.autotime.enabled=true 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------