├── .gitignore
├── README.md
├── deploy.sh
├── img
├── logs.png
└── result.png
├── infrastructure
├── cdk
│ ├── .gitignore
│ ├── .mvn
│ │ └── wrapper
│ │ │ ├── maven-wrapper.jar
│ │ │ └── maven-wrapper.properties
│ ├── README.md
│ ├── cdk.json
│ ├── mvnw
│ ├── pom.xml
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── unicorn
│ │ ├── UnicornStoreApp.java
│ │ └── UnicornStoreSpringStack.java
└── loadtest.yaml
├── test-app.sh
└── unicorn-store-spring
├── pom.xml
└── src
└── main
├── java
└── com
│ └── unicorn
│ └── store
│ ├── StoreApplication.java
│ ├── StreamLambdaHandler.java
│ ├── controller
│ └── UnicornController.java
│ ├── data
│ └── UnicornPublisher.java
│ ├── exceptions
│ ├── PublisherException.java
│ └── ResourceNotFoundException.java
│ ├── model
│ ├── Unicorn.java
│ └── UnicornEventType.java
│ └── service
│ └── UnicornService.java
└── resources
└── application.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.iml
3 | runtime.zip
4 |
5 | spring-custom-runtime.zip
6 | .idea/
7 | target/
8 | cdk.out/
9 |
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring Boot 3 on AWS Lambda example
2 |
3 | The following repository contains an example of a Spring Boot 3 application that is running on AWS Lambda (Java 17). It leverages the new version of the [Serverless Java Container library](https://github.com/awslabs/aws-serverless-java-container) for compatibility between API Gateway events and Spring Boot 3. For additional information please see this [hands on workshop](https://catalog.workshops.aws/java-on-aws-lambda/en-US/01-migration/01-setup-and-deploy/java-container).
4 |
5 | ## Prerequisites
6 |
7 | - Maven
8 | - Java 17
9 | - [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/cli.html)
10 |
11 | ## How to build, deploy & test the application
12 |
13 | Ensure you are using Java 17 and build the application:
14 |
15 | ```
16 | mvn clean package -f unicorn-store-spring/pom.xml
17 | cp unicorn-store-spring/target/spring-boot-lambda.jar infrastructure/cdk/app
18 | ```
19 |
20 | If you haven't used AWS CDK on your AWS account before run:
21 |
22 | ```
23 | cdk bootstrap
24 | ```
25 |
26 | Deploy the application via AWS CDK use:
27 |
28 | ```
29 | ./deploy.sh
30 | ```
31 |
32 | After successful deployment you can use the following script to test the application:
33 |
34 |
35 | ```
36 | ./test-app.sh
37 | ```
38 |
39 | ## Result
40 |
41 | 
42 |
43 | 
44 |
45 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd infrastructure/cdk
4 | cdk deploy --all --outputs-file target/output.json
--------------------------------------------------------------------------------
/img/logs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschnetwork/aws-lambda-spring-boot-3/b861e841277f144c71c4ec4ee84d429b48e2c05b/img/logs.png
--------------------------------------------------------------------------------
/img/result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschnetwork/aws-lambda-spring-boot-3/b861e841277f144c71c4ec4ee84d429b48e2c05b/img/result.png
--------------------------------------------------------------------------------
/infrastructure/cdk/.gitignore:
--------------------------------------------------------------------------------
1 | .classpath.txt
2 | target
3 | .classpath
4 | .project
5 | .idea
6 | .settings
7 | .vscode
8 | *.iml
9 |
10 | # CDK asset staging directory
11 | .cdk.staging
12 | cdk.out
13 | cdk.context.json
14 |
15 | app/*
16 |
17 |
--------------------------------------------------------------------------------
/infrastructure/cdk/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschnetwork/aws-lambda-spring-boot-3/b861e841277f144c71c4ec4ee84d429b48e2c05b/infrastructure/cdk/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/infrastructure/cdk/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-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 |
--------------------------------------------------------------------------------
/infrastructure/cdk/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to the Unicorn Store CDK Java project!
2 |
3 | This project is used to deploy the needed infrastructure resources to run the Unicorn Store.
4 | Please follow the workshop instructions on how to properly configure the environment.
5 |
6 | The `cdk.json` file tells the CDK Toolkit how to execute your app.
7 |
8 | It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests.
9 |
10 | ## Useful commands
11 |
12 | * `mvn package` compile and run tests
13 | * `cdk ls` list all stacks in the app
14 | * `cdk synth` emits the synthesized CloudFormation template
15 | * `cdk deploy` deploy this stack to your default AWS account/region
16 | * `cdk diff` compare deployed stack with current state
17 | * `cdk docs` open CDK documentation
18 |
19 | Enjoy!
20 |
--------------------------------------------------------------------------------
/infrastructure/cdk/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "./mvnw -e -q compile exec:java",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "target",
11 | "pom.xml",
12 | "src/test"
13 | ]
14 | },
15 | "context": {
16 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
17 | "aws-cdk:enableDiffNoFail": true,
18 | "@aws-cdk/core:stackRelativeExports": true,
19 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
20 | "@aws-cdk/aws-kms:defaultKeyPolicies": true,
21 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
22 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
23 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
24 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
25 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
26 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/infrastructure/cdk/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------
/infrastructure/cdk/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.unicorn
7 | cdk
8 | 0.1
9 |
10 |
11 | UTF-8
12 | 2.97.0
13 | 5.9.1
14 |
15 |
16 |
17 |
18 |
19 | org.apache.maven.plugins
20 | maven-compiler-plugin
21 | 3.10.1
22 |
23 | 17
24 | 17
25 |
26 |
27 |
28 |
29 | org.codehaus.mojo
30 | exec-maven-plugin
31 | 3.1.0
32 |
33 | com.unicorn.UnicornStoreApp
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | software.amazon.awscdk
43 | aws-cdk-lib
44 | ${cdk.version}
45 |
46 |
47 |
48 | io.github.cdklabs
49 | cdknag
50 | 2.18.25
51 |
52 |
53 |
54 | org.junit.jupiter
55 | junit-jupiter
56 | ${junit.version}
57 | test
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/infrastructure/cdk/src/main/java/com/unicorn/UnicornStoreApp.java:
--------------------------------------------------------------------------------
1 | package com.unicorn;
2 |
3 | import software.amazon.awscdk.App;
4 | import software.amazon.awscdk.StackProps;
5 |
6 | public class UnicornStoreApp {
7 |
8 | public static void main(final String[] args) {
9 | App app = new App();
10 |
11 | new UnicornStoreSpringStack(app, "UnicornStoreSpringStack", StackProps.builder()
12 | .build());
13 |
14 | app.synth();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/infrastructure/cdk/src/main/java/com/unicorn/UnicornStoreSpringStack.java:
--------------------------------------------------------------------------------
1 | package com.unicorn;
2 |
3 | import software.amazon.awscdk.*;
4 | import software.amazon.awscdk.services.apigateway.LambdaRestApi;
5 | import software.amazon.awscdk.services.apigateway.RestApi;
6 | import software.amazon.awscdk.services.events.EventBus;
7 | import software.amazon.awscdk.services.lambda.Runtime;
8 | import software.amazon.awscdk.services.lambda.*;
9 | import software.constructs.Construct;
10 |
11 | import java.util.List;
12 | import java.util.Map;
13 |
14 | import static software.amazon.awscdk.services.lambda.CfnFunction.*;
15 |
16 | public class UnicornStoreSpringStack extends Stack {
17 |
18 | public UnicornStoreSpringStack(final Construct scope, final String id, final StackProps props) {
19 | super(scope, id, props);
20 |
21 | var eventBridge = createEventBus();
22 | var unicornStoreSpringLambda = createUnicornLambdaFunction();
23 | // Create Version and Alias
24 | var liveAlias = unicornStoreSpringLambda.addAlias("live");
25 |
26 | // Permission for Spring Boot Lambda Function
27 | eventBridge.grantPutEventsTo(unicornStoreSpringLambda);
28 |
29 | // Setup a Proxy-Rest API to access the Spring Lambda function
30 | var restApi = setupRestApi(liveAlias);
31 |
32 | // Create output values for later reference
33 | new CfnOutput(this, "unicorn-store-spring-function-arn", CfnOutputProps.builder()
34 | .value(unicornStoreSpringLambda.getFunctionArn())
35 | .build());
36 |
37 | new CfnOutput(this, "ApiEndpointSpring", CfnOutputProps.builder()
38 | .value(restApi.getUrl())
39 | .build());
40 | }
41 |
42 | private RestApi setupRestApi(IFunction handler) {
43 | return LambdaRestApi.Builder.create(this, "UnicornStoreSpringApi")
44 | .restApiName("UnicornStoreSpringApi")
45 | .handler(handler)
46 | .build();
47 | }
48 |
49 | private ILayerVersion getWebAdapterLayer() {
50 | return LayerVersion.fromLayerVersionArn(this, "WebAdapterLayer", String
51 | .format("arn:aws:lambda:%s:753240598075:layer:LambdaAdapterLayerX86:10", Stack.of(this).getRegion()));
52 | }
53 |
54 | private Function createUnicornLambdaFunction() {
55 | return Function.Builder.create(this, "UnicornStoreSpringFunction")
56 | .runtime(Runtime.JAVA_17)
57 | .functionName("unicorn-store-spring-boot-3")
58 | .memorySize(2048)
59 | .handler("com.unicorn.store.StreamLambdaHandler::handleRequest")
60 | .layers(List.of(getWebAdapterLayer()))
61 | .timeout(Duration.seconds(29))
62 | .code(Code.fromAsset("app/spring-boot-lambda.jar"))
63 | .snapStart(SnapStartConf.ON_PUBLISHED_VERSIONS)
64 | .build();
65 | }
66 |
67 | private EventBus createEventBus() {
68 | return EventBus.Builder.create(this, "UnicornEventBus")
69 | .eventBusName("unicorns-spring")
70 | .build();
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/infrastructure/loadtest.yaml:
--------------------------------------------------------------------------------
1 | config:
2 | phases:
3 | - duration: 60
4 | arrivalRate: 25
5 | http:
6 | timeout: 29
7 | scenarios:
8 | - flow:
9 | - post:
10 | url: "{{ url }}"
11 | json:
12 | name: "Big Unicorn"
13 | age: "Quite old"
14 | type: "Beautiful"
15 | size: "Very big"
--------------------------------------------------------------------------------
/test-app.sh:
--------------------------------------------------------------------------------
1 | #bin/sh
2 |
3 | curl --location --request POST $(cat infrastructure/cdk/target/output.json | jq -r '.UnicornStoreSpringStack.ApiEndpointSpring')'/unicorns' \
4 | --header 'Content-Type: application/json' \
5 | --data-raw '{
6 | "name": "Something",
7 | "age": "Older",
8 | "type": "Animal",
9 | "size": "Very big"
10 | }' | jq
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/unicorn-store-spring/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 3.1.4
9 |
10 |
11 |
12 |
13 | spring-milestones
14 | https://repo.spring.io/milestone
15 |
16 |
17 |
18 |
19 | spring-milestones
20 | https://repo.spring.io/milestone
21 |
22 |
23 | com.unicorn
24 | store-spring
25 | 1.0.0
26 | store
27 | Unicorn storage service
28 |
29 | 17
30 | 17
31 | 17
32 |
33 |
34 |
35 |
36 | org.springframework.boot
37 | spring-boot-starter-web
38 |
39 |
40 | software.amazon.awssdk
41 | eventbridge
42 | 2.20.153
43 |
44 |
45 | software.amazon.awssdk
46 | netty-nio-client
47 |
48 |
49 | software.amazon.awssdk
50 | apache-client
51 |
52 |
53 |
54 |
55 | software.amazon.awssdk
56 | aws-crt-client
57 | 2.20.153
58 |
59 |
60 |
61 | org.springframework.boot
62 | spring-boot-starter-test
63 | test
64 |
65 |
66 |
67 | com.amazonaws.serverless
68 | aws-serverless-java-container-springboot3
69 | 2.0.0-M2
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | org.apache.maven.plugins
78 | maven-shade-plugin
79 | 3.5.0
80 |
81 | false
82 |
83 |
84 |
85 | package
86 |
87 | shade
88 |
89 |
90 | spring-boot-lambda
91 |
92 |
93 | org.apache.tomcat.embed:*
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/StoreApplication.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import com.fasterxml.jackson.databind.DeserializationFeature;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import org.springframework.boot.autoconfigure.SpringBootApplication;
7 | import org.springframework.context.annotation.Bean;
8 |
9 | @SpringBootApplication
10 | public class StoreApplication {
11 |
12 | public static void main(String[] args) {
13 | SpringApplication.run(StoreApplication.class, args);
14 | }
15 |
16 | @Bean
17 | public ObjectMapper getObjectMapper() {
18 | ObjectMapper objectMapper = new ObjectMapper();
19 | objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
20 | return objectMapper;
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/StreamLambdaHandler.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store;
2 |
3 | import com.amazonaws.serverless.exceptions.ContainerInitializationException;
4 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
5 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
6 | import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler;
7 | import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder;
8 | import com.amazonaws.services.lambda.runtime.Context;
9 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
10 |
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.OutputStream;
14 |
15 | public class StreamLambdaHandler implements RequestStreamHandler {
16 |
17 | private static final SpringBootLambdaContainerHandler handler;
18 |
19 | static {
20 | try {
21 | handler = new SpringBootProxyHandlerBuilder()
22 | .defaultProxy()
23 | .asyncInit()
24 | .springBootApplication(StoreApplication.class)
25 | .buildAndInitialize();
26 | } catch (ContainerInitializationException e) {
27 | e.printStackTrace();
28 | throw new RuntimeException("Could not initialize Spring Boot application", e);
29 | }
30 | }
31 |
32 | @Override
33 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
34 | throws IOException {
35 | handler.proxyStream(inputStream, outputStream, context);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/controller/UnicornController.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.controller;
2 |
3 | import com.unicorn.store.model.Unicorn;
4 | import com.unicorn.store.service.UnicornService;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.PostMapping;
9 | import org.springframework.web.bind.annotation.RequestBody;
10 | import org.springframework.web.bind.annotation.RestController;
11 | import org.springframework.web.server.ResponseStatusException;
12 |
13 | import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
14 |
15 | @RestController
16 | public class UnicornController {
17 |
18 | private final UnicornService unicornService;
19 | private static final Logger logger = LoggerFactory.getLogger(UnicornController.class);
20 |
21 | public UnicornController(UnicornService unicornService) {
22 | this.unicornService = unicornService;
23 | }
24 |
25 | @PostMapping("/unicorns")
26 | public ResponseEntity createUnicorn(@RequestBody Unicorn unicorn) {
27 | try {
28 | var savedUnicorn = unicornService.createUnicorn(unicorn);
29 | return ResponseEntity.ok(savedUnicorn);
30 | } catch (Exception e) {
31 | String errorMsg = "Error creating unicorn";
32 | logger.error(errorMsg, e);
33 | throw new ResponseStatusException(INTERNAL_SERVER_ERROR, errorMsg, e);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/data/UnicornPublisher.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.data;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.unicorn.store.exceptions.PublisherException;
6 | import com.unicorn.store.model.Unicorn;
7 | import com.unicorn.store.model.UnicornEventType;
8 | import org.springframework.stereotype.Service;
9 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
10 | import software.amazon.awssdk.core.SdkSystemSetting;
11 | import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient;
12 | import software.amazon.awssdk.regions.Region;
13 | import software.amazon.awssdk.services.eventbridge.EventBridgeAsyncClient;
14 | import software.amazon.awssdk.services.eventbridge.model.EventBridgeException;
15 | import software.amazon.awssdk.services.eventbridge.model.PutEventsRequest;
16 | import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry;
17 |
18 | import java.util.concurrent.ExecutionException;
19 |
20 | @Service
21 | public class UnicornPublisher {
22 |
23 | private final ObjectMapper objectMapper;
24 | private static final EventBridgeAsyncClient eventBridgeClient = EventBridgeAsyncClient
25 | .builder()
26 | .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
27 | .httpClient(AwsCrtAsyncHttpClient.create())
28 | .build();
29 |
30 | public UnicornPublisher(ObjectMapper objectMapper) {
31 | this.objectMapper = objectMapper;
32 | }
33 |
34 | public void publish(Unicorn unicorn, UnicornEventType unicornEventType) {
35 | try {
36 | var unicornJson = objectMapper.writeValueAsString(unicorn);
37 | var eventsRequest = createEventRequestEntry(unicornEventType, unicornJson);
38 |
39 | eventBridgeClient.putEvents(eventsRequest).get();
40 | } catch (JsonProcessingException e) {
41 | throw new PublisherException("Error while serializing the Unicorn", e);
42 | } catch (EventBridgeException | ExecutionException | InterruptedException e) {
43 | throw new PublisherException("Error while publishing the event", e);
44 | }
45 | }
46 |
47 | private PutEventsRequest createEventRequestEntry(UnicornEventType unicornEventType, String unicornJson) {
48 | var entry = PutEventsRequestEntry.builder()
49 | .source("com.unicorn.store")
50 | .eventBusName("unicorns-spring")
51 | .detailType(unicornEventType.name())
52 | .detail(unicornJson)
53 | .build();
54 |
55 | return PutEventsRequest.builder()
56 | .entries(entry)
57 | .build();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/exceptions/PublisherException.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.exceptions;
2 |
3 | public class PublisherException extends RuntimeException{
4 |
5 | public PublisherException(String errorMessage, Throwable err) {
6 | super(errorMessage, err);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/exceptions/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.exceptions;
2 |
3 | public class ResourceNotFoundException extends RuntimeException{
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/model/Unicorn.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.model;
2 |
3 | public class Unicorn {
4 |
5 | private String id;
6 | private String name;
7 | private String age;
8 | private String size;
9 | private String type;
10 |
11 | public String getName() {
12 | return name;
13 | }
14 |
15 | public void setName(String name) {
16 | this.name = name;
17 | }
18 |
19 | public String getAge() {
20 | return age;
21 | }
22 |
23 | public void setAge(String age) {
24 | this.age = age;
25 | }
26 |
27 | public String getSize() {
28 | return size;
29 | }
30 |
31 | public void setSize(String size) {
32 | this.size = size;
33 | }
34 |
35 | public String getType() {
36 | return type;
37 | }
38 |
39 | public void setType(String type) {
40 | this.type = type;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/model/UnicornEventType.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.model;
2 |
3 | public enum UnicornEventType {
4 | UNICORN_CREATED, UNICORN_UPDATED, UNICORN_DELETED
5 | }
6 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/java/com/unicorn/store/service/UnicornService.java:
--------------------------------------------------------------------------------
1 | package com.unicorn.store.service;
2 |
3 | import com.unicorn.store.data.UnicornPublisher;
4 | import com.unicorn.store.model.Unicorn;
5 | import com.unicorn.store.model.UnicornEventType;
6 | import org.springframework.stereotype.Service;
7 |
8 | @Service
9 | public class UnicornService {
10 | private final UnicornPublisher unicornPublisher;
11 |
12 | public UnicornService(UnicornPublisher unicornPublisher) {
13 | this.unicornPublisher = unicornPublisher;
14 | }
15 |
16 | public Unicorn createUnicorn(Unicorn unicorn) {
17 | unicornPublisher.publish(unicorn, UnicornEventType.UNICORN_CREATED);
18 | return unicorn;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/unicorn-store-spring/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.error.include-message=always
2 |
--------------------------------------------------------------------------------