├── public
├── robots.txt
├── favicon.ico
└── favicon.png
├── rebuild
├── src
├── main
│ ├── resources
│ │ ├── application-aws.properties
│ │ ├── application.properties
│ │ └── logback-aws.xml
│ └── java
│ │ └── my
│ │ └── service
│ │ ├── controller
│ │ └── APIController.java
│ │ ├── Application.java
│ │ └── StreamLambdaHandler.java
├── test
│ ├── resources
│ │ ├── scheduled-event.json
│ │ ├── sqs-event.json
│ │ └── sns-event.json
│ └── java
│ │ └── my
│ │ └── service
│ │ └── StreamLambdaHandlerTest.java
└── assembly
│ └── bin.xml
├── sam-local-env.json
├── serverless-java-spring-boot.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ ├── gradle-wrapper.properties
│ └── gradle-wrapper-wrapper
├── sam-local
├── gradle.properties
├── .gitignore
├── deploy-to-aws
├── mk-git-json
├── .circleci
└── config.yml
├── sam-template.yaml
├── README.md
├── gradlew.bat
├── layers-support.patch
├── gradlew
└── events.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /*
3 |
4 |
5 |
--------------------------------------------------------------------------------
/rebuild:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ./gradlew -x test build --continuous
3 |
--------------------------------------------------------------------------------
/src/main/resources/application-aws.properties:
--------------------------------------------------------------------------------
1 | logging.config = classpath:logback-aws.xml
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/sam-local-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "MyServiceFunction":{
3 | "SPRING_PROFILES_ACTIVE": "aws,local"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/serverless-java-spring-boot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/serverless-java-spring-boot.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sam-local:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json
4 |
5 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # use gradle daemon by default
2 | org.gradle.daemon = true
3 |
4 | systemProp.org.gradle.internal.http.connectionTimeout=120000
5 | systemProp.org.gradle.internal.http.socketTimeout=120000
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.main.banner-mode=off
2 |
3 | # Reduce logging level to make sure the application works with SAM local
4 | # https://github.com/awslabs/aws-serverless-java-container/issues/134
5 | logging.level.root=INFO
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .attach*
2 | .classpath
3 | .project
4 | .gradle
5 | .settings
6 | # VSCOde editor generates this ffs
7 | bin
8 | # Gradle
9 | build
10 | # Maven
11 | target
12 | output-sam.yaml
13 | .vscode
14 | sam-package.yaml
15 | src/main/resources/git.json
16 |
--------------------------------------------------------------------------------
/src/test/resources/scheduled-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0",
3 | "id": "2528c5e5-7feb-9a8d-9329-b445a222897f",
4 | "detail-type": "Scheduled Event",
5 | "source": "aws.events",
6 | "account": "849707200000",
7 | "time": "2019-02-19T08:59:04Z",
8 | "region": "eu-west-1",
9 | "resources": [
10 | "arn:aws:events:eu-west-1:849707200000:rule/ServerlessSpringApi-MyServiceFunctionCheckWebsiteS-17EIBMQX0K6PN"
11 | ],
12 | "detail": {}
13 | }
--------------------------------------------------------------------------------
/src/main/resources/logback-aws.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [%d{yyyy-MM-dd HH:mm:ss.SSS}] %X{AWSRequestId:-} %.-6level %logger{5} - %msg \r%replace(%ex){'\n','\r'}%nopex%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper-wrapper:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Matthias Gr?ter
4 | #set -x
5 |
6 | function find_wcroot()
7 | {
8 | # Return 0 & output working copy root path if the current directory
9 | # is a subversion or git working area. Otherwise return 1.
10 |
11 | SVNROOT="$( svn info --xml --incremental 2> /dev/null | xmllint --xpath '//wcroot-abspath/text()' - 2> /dev/null )"
12 | [[ ! -z $SVNROOT ]] && { echo "$SVNROOT"; return 0; }
13 |
14 | GITROOT="$( git rev-parse --show-toplevel 2> /dev/null )"
15 | [[ ! -z $GITROOT ]] && { echo "$GITROOT"; return 0; }
16 |
17 | return 1
18 | }
19 |
20 | WCROOT=$( find_wcroot )
21 | # if gradle wrapper exists use it, otherwise use gradle from system path
22 | [[ -f "$WCROOT/gradlew" ]] && CMD="$WCROOT/gradlew" || CMD="gradle"
23 |
24 | $CMD "$@"
25 |
--------------------------------------------------------------------------------
/src/assembly/bin.xml:
--------------------------------------------------------------------------------
1 |
4 | lambda-package
5 |
6 | zip
7 |
8 | false
9 |
10 |
11 |
12 | ${project.build.directory}${file.separator}lib
13 | lib
14 |
15 | tomcat-embed*
16 |
17 |
18 |
19 |
20 | ${project.build.directory}${file.separator}classes
21 |
22 | **
23 |
24 | ${file.separator}
25 |
26 |
27 |
--------------------------------------------------------------------------------
/deploy-to-aws:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | ./gradlew compileJava compileTestJava assemble build --scan
5 |
6 | function parse_yaml {
7 | local prefix=$2
8 | local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
9 | sed -ne "s|^\($s\):|\1|" \
10 | -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
11 | -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
12 | awk -F$fs '{
13 | indent = length($1)/2;
14 | vname[indent] = $2;
15 | for (i in vname) {if (i > indent) {delete vname[i]}}
16 | if (length($3) > 0) {
17 | vn=""; for (i=0; i ping() {
21 | log.info("Received ping, sending pong");
22 | Map pong = new HashMap<>();
23 | pong.put("pong", "Hello, World!");
24 | return pong;
25 | }
26 |
27 | /**
28 | * Handles AWS event. Delivered from {@link StreamLambdaHandler#handleRequest} by constructing special proxy request.
29 | */
30 | @RequestMapping(path = "/event", method = RequestMethod.POST)
31 | public Map event(@RequestParam("type") String type, @RequestBody String post) {
32 | log.info("Received event {} body {}", type, post);
33 | Map pong = new HashMap<>();
34 | pong.put("pong", "Hello, World!");
35 | return pong;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/resources/sqs-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [
3 | {
4 | "messageId": "72b03944-1af7-492b-aac9-412698960531",
5 | "receiptHandle": "AQEBicU8TU0NRCELI9WEuVNN/2EU6G1KUJTSVO9BoDPAUvZk4yLsyMQiiKLf0LNmx7ETrdQ1CrwlwdE7JaBVGAcLWPrMCx8wZLxu6x8QtlITVIIAEIwjt+5/rnbmUMY0ajOMok80gO9SceLa+Zr1g3yZnjUMf3xNSGqynvooQkHFRAtSTKf0bkkagkKIHCDt5RghXUeIYruEZTStOm7RTVQwWxaCTQMvbBgNAZFG6j54qg1hCI9Cv9P95FH6Tt8yRDjf9Ad3s7Jykm0yy+IWfvpvHBoSelM03LrUUTX5E6lFpmTByIIE+fmamzeTZOevyNviiiMRbvqvDkDlrVs3M00zjiocX5jz+GVUmE5q0tOzwGYC4yWbcPKDSOc1hiRYUhbo",
6 | "body": "Howdy?",
7 | "attributes": {
8 | "ApproximateReceiveCount": "1",
9 | "SentTimestamp": "1550567133640",
10 | "SenderId": "AIDAIEHDD23VAQL6ZVIAC",
11 | "ApproximateFirstReceiveTimestamp": "1550567133641"
12 | },
13 | "messageAttributes": {
14 | "someattr": {
15 | "stringValue": "someval",
16 | "stringListValues": [],
17 | "binaryListValues": [],
18 | "dataType": "String"
19 | }
20 | },
21 | "md5OfBody": "47b68d3722ece13893e9c5fcb3cac906",
22 | "md5OfMessageAttributes": "1e4b169c724503f3886d943e5bbe88e5",
23 | "eventSource": "aws:sqs",
24 | "eventSourceARN": "arn:aws:sqs:eu-west-1:849707200000:test-queue",
25 | "awsRegion": "eu-west-1"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/mk-git-json:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Creates git.json with repository and deployment information
4 | # NOTE: CodePipeline strips .git directory, so only git information available is commit.id from CODEBUILD_RESOLVED_SOURCE_VERSION env var
5 | #
6 | DST=${1:-src/main/resources}
7 | if [ -d .git ]; then
8 | git log -1 --pretty=format:'{%n"commit.id": "%H",%n"commit.id.abbrev": "%h",%n"commit.user.name": "%an",%n"commit.user.email": "%ae",%n"commit.info":"%D",%n"commit.message": "%s",%n"commit.date": "%ai"%n}%n' | sed -E -e "s/^\* //g;s/^\| //g" > $DST/git.json
9 | else
10 | if [ "$CODEBUILD_RESOLVED_SOURCE_VERSION" != "" ]; then
11 | echo "{ \"commit.id\": \"$CODEBUILD_RESOLVED_SOURCE_VERSION\" }" > $DST/git.json
12 | else
13 | echo "{ \"empty\": true }" > $DST/git.json
14 | fi
15 | fi
16 |
17 | NOW=`date +%Y-%m-%dT%H:%M:%S%z`
18 | HST=`hostname -f`
19 |
20 |
21 | platform='unknown'
22 | unamestr=`uname`
23 | if [[ "$unamestr" == 'Linux' ]]; then
24 | platform='linux'
25 | elif [[ "$unamestr" == 'FreeBSD' ]]; then
26 | platform='freebsd'
27 | fi
28 |
29 | if [[ "$platform" == "unknown" ]]; then
30 | # Probably MacOS X
31 | sed -i bak -E -e "s/^{/{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json
32 | elif [[ "$platform" == "freebsd" ]]; then
33 | # Validate proper -i call
34 | sed -i -E -e "s/^{/{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json
35 | else
36 | sed -i -E -e "s/^\{/\{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json
37 | fi
38 |
--------------------------------------------------------------------------------
/src/test/resources/sns-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [
3 | {
4 | "EventSource": "aws:sns",
5 | "EventVersion": "1.0",
6 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
7 | "Sns": {
8 | "Type": "Notification",
9 | "MessageId": "15d202b9-ecca-5bf1-86aa-565c2460e18e",
10 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic",
11 | "Subject": null,
12 | "Message": "hello world!",
13 | "Timestamp": "2019-02-19T09:03:38.472Z",
14 | "SignatureVersion": "1",
15 | "Signature": "Gtt+vpFy8xd5jEBqChL3GU2DFS2WKFzMzmuUCcndy2y+UH4XJZ9TaU082rXtKZdNnhPuGrRygoOJkemLhh6q2kHu25FkvYBfkddBVCgr/wpgxnukqwlu3xYnmrtNqDJM8louZ38ZGk99miMf6NO1CloleIRc+EA1Vgfq7B/fE8lX1863VlLFViS36G86yUeFagNmPMnnPu7VFOi4RQRnzWa8/fKTGgvLMyGFesv6eWPFgDLEL6wGOpd/0gfxh6vq7sU9yuqQFkLvHTX5j6gf1+9Cfh1yxoJkz36NG3Oo17bIBiSsTqMl5A4Wc/3EKkGaPyjxPRPvxVof4ES0a6pPIA==",
16 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem",
17 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
18 | "MessageAttributes": {
19 | "somekey": {
20 | "Type": "String",
21 | "Value": "dsdasdas"
22 | }
23 | }
24 | }
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/src/main/java/my/service/Application.java:
--------------------------------------------------------------------------------
1 | package my.service;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Import;
8 | import org.springframework.web.servlet.HandlerAdapter;
9 | import org.springframework.web.servlet.HandlerMapping;
10 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
11 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
12 |
13 | import my.service.controller.APIController;
14 |
15 | @SpringBootApplication
16 | // We use direct @Import instead of @ComponentScan to speed up cold starts
17 | // @ComponentScan(basePackages = "my.service.controller")
18 | @Import({ APIController.class })
19 | public class Application extends SpringBootServletInitializer {
20 |
21 | /*
22 | * Create required HandlerMapping, to avoid several default HandlerMapping
23 | * instances being created
24 | */
25 | @Bean
26 | public HandlerMapping handlerMapping() {
27 | return new RequestMappingHandlerMapping();
28 | }
29 |
30 | /*
31 | * Create required HandlerAdapter, to avoid several default HandlerAdapter
32 | * instances being created
33 | */
34 | @Bean
35 | public HandlerAdapter handlerAdapter() {
36 | return new RequestMappingHandlerAdapter();
37 | }
38 |
39 | public static void main(String[] args) {
40 | SpringApplication.run(Application.class, args);
41 | }
42 | }
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | # Build and run tests
5 | build:
6 | working_directory: ~/serverless
7 | docker:
8 | # https://discuss.circleci.com/t/spring-boot-application-build-failed-due-to-test-failure/27687
9 | - image: circleci/openjdk:8u171
10 | steps:
11 | - checkout
12 | - run:
13 | name: Install AWS cli
14 | command: |
15 | python --version
16 | cat /etc/issue
17 | sudo apt-get update
18 | sudo apt-get install -y python-pip
19 | sudo pip install awscli
20 | sudo pip install aws-sam-cli
21 | - run:
22 | name: Configure AWS cli
23 | command: |
24 | aws configure set aws_access_key_id ${AWS_ACCESS_KEY_ID}
25 | aws configure set aws_secret_access_key ${AWS_SECRET_ACCESS_KEY}
26 | aws configure set default.region ${AWS_DEFAULT_REGION}
27 | aws configure list
28 | - run:
29 | name: Build and deploy to AWS
30 | command: ./deploy-to-aws
31 | - save_cache:
32 | key: docs-{{ checksum "build.gradle" }}
33 | paths:
34 | - ~/.m2
35 | - store_artifacts:
36 | path: ~/serverless/build/reports
37 | - store_artifacts:
38 | path: ~/serverless/build/test-results
39 | - run:
40 | when: on_fail
41 | command: find > ~/serverless/all-files.txt
42 | - store_artifacts:
43 | path: ~/serverless/all-files.txt
44 | workflows:
45 | version: 2
46 | build:
47 | jobs:
48 | - build:
49 | filters:
50 | branches:
51 | only: master
52 | ignore: gh-pages
53 |
--------------------------------------------------------------------------------
/sam-template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | # https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#sqs
3 | Transform: AWS::Serverless-2016-10-31
4 | Description: AWS Serverless Spring Boot 2 API - my.service::my-service
5 | Globals:
6 | Api:
7 | EndpointConfiguration: REGIONAL
8 |
9 | Parameters:
10 | Stage:
11 | Type: String
12 | Default: dev
13 | SNSTopic:
14 | Type: String
15 | SQSQueue:
16 | Type: String
17 |
18 | Resources:
19 | ApiGatewayApi:
20 | Type: AWS::Serverless::Api
21 | Properties:
22 | StageName:
23 | Ref: Stage
24 | Variables:
25 | Stage:
26 | Ref: Stage
27 | MyServiceFunction:
28 | Type: AWS::Serverless::Function
29 | FunctionName: "ServerlessSpringApi-MyServiceFunction"
30 | Properties:
31 | Handler: my.service.StreamLambdaHandler::handleRequest
32 | Runtime: java8
33 | CodeUri: "build/distributions/serverless-java-spring-boot.zip"
34 | MemorySize: 2048
35 | Policies: AWSLambdaBasicExecutionRole
36 | Timeout: 30
37 | Environment:
38 | Variables:
39 | # Specify here variables for deployment,
40 | # override local using sam-local-env.json
41 | SPRING_PROFILES_ACTIVE: aws
42 | Events:
43 | GetResource:
44 | Type: Api
45 | Properties:
46 | RestApiId:
47 | Ref: ApiGatewayApi
48 | Path: /ping
49 | Method: GET
50 | CheckWebsiteScheduledEvent:
51 | Type: Schedule
52 | Properties:
53 | Schedule: rate(5 minutes)
54 | ReceiveFromSNS:
55 | Type: SNS
56 | Properties:
57 | Topic:
58 | Ref: SNSTopic
59 | ReceiveFromQueue:
60 | Type: SQS
61 | Properties:
62 | Queue:
63 | Ref: SQSQueue
64 | Outputs:
65 | MyServiceApi:
66 | Description: URL for application
67 | Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/ping'
68 | Export:
69 | Name: MyServiceApi
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java Spring Boot Serverless application
2 |
3 | 
4 |
5 | Using [AWS Proxy wrappers](https://github.com/awslabs/aws-serverless-java-container/wiki/Quick-start---Spring-Boot) and [`aws-serverless-java-container`](https://github.com/awslabs/aws-serverless-java-container) this application shows how you can easily build Java application and deploy it into AWS Lambda.
6 |
7 | Also this application able to handle following AWS Events
8 |
9 | * CloudWatch scheduled event
10 | * SNS Topic message
11 | * SQS queue message
12 |
13 |
14 | This project uses Java8, AWS CLI, AWS SAM CLI and Gradle to build, test and deploy code.
15 | Aslo consult [CircleCI](.circleci/config.yml) for instructions how to setup automated CI/CD build and deployment for this.
16 |
17 | ## Running locally
18 |
19 | To run function locally use `.\sam-local`
20 |
21 | Try API endpoint in terminal or browser
22 |
23 | ```
24 | curl -s http://127.0.0.1:3000/ping | json_pp
25 | ```
26 |
27 | ## Running in AWS
28 |
29 | Deploy to AWS using
30 | ```bash
31 | ./deploy-to-aws
32 | ```
33 |
34 | Open URL provided in output, for example: https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping
35 |
36 | ```bash
37 | curl -s https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping | json_pp
38 | {
39 | "pong" : "Hello, World!"
40 | }
41 | ```
42 |
43 | Bench time to run, notice that the first run with the same or more concurrent number of requests takes 6 seconds (i.e. Spring boot launch time)
44 | ```bash
45 | ab -n 100 -c 20 https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping
46 | ```
47 |
48 | ## Requirements for deploying
49 |
50 | To successfully deploy to AWS you need to define following environment variables:
51 |
52 | * AWS_ACCESS_KEY_ID - AWS access key to use (IAM)
53 | * AWS_SECRET_ACCESS_KEY - AWS access key password to use (IAM)
54 | * AWS_DEFAULT_REGION - AWS Region to use, for example eu-west-1
55 | * S3_BUCKET - Existing S3 bucket to use for deploying artifacts
56 | * SNS_TOPIC - Full ARN for SNS topic to subscribe to
57 | * SQS_QUEUE - Full ARN for SQ queue to subcribe to
58 |
59 | ## Links
60 |
61 | * https://github.com/awslabs/aws-serverless-java-container/
62 | * https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
63 | * https://github.com/aws/aws-lambda-java-libs/tree/master/aws-lambda-java-events
64 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS="-Xmx64m"
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/layers-support.patch:
--------------------------------------------------------------------------------
1 | This patch introduces support for layers,
2 | however sam cli does not support it well, so it is not available here.
3 |
4 | Probably can be turned on later when using serverless framework for running locally.
5 |
6 | diff --git b/build.gradle a/build.gradle
7 | index 0dcc715..a50b21b 100644
8 | --- b/build.gradle
9 | +++ a/build.gradle
10 | @@ -41,5 +41,22 @@ task buildZipFull(type: Zip) {
11 | }
12 | }
13 | }
14 | +
15 | +task buildZipCode(type: Zip) {
16 | + baseName = "serverless-java-spring-boot-code"
17 | + from compileJava
18 | + from processResources
19 | +}
20 | +
21 | +task buildLayerZip(type: Zip) {
22 | + baseName = "serverless-java-spring-boot-layer1"
23 | + into("java/lib") {
24 | + from(configurations.compileClasspath) {
25 | + exclude 'tomcat-embed-*'
26 | + }
27 | + }
28 | +}
29 |
30 | build.dependsOn buildZipFull
31 | +build.dependsOn buildZipCode
32 | +build.dependsOn buildLayerZip
33 | diff --git b/deploy-to-aws a/deploy-to-aws
34 | index 492b7c3..3f36579 100755
35 | --- b/deploy-to-aws
36 | +++ a/deploy-to-aws
37 | @@ -22,9 +22,20 @@ function parse_yaml {
38 |
39 | eval $(parse_yaml sam-template.yaml)
40 |
41 | +#LAYER_PACKAGE=build/distributions/serverless-java-spring-boot-layer1.zip
42 | +#LAYER_NAME=`basename $LAYER_PACKAGE`
43 | +#
44 | +#aws s3 cp $LAYER_PACKAGE s3://${S3_BUCKET}/$LAYER_NAME
45 | +#aws lambda publish-layer-version --layer-name serverless-java-spring-boot-layer1 \
46 | +# --content S3Bucket=${S3_BUCKET},S3Key=${LAYER_NAME} \
47 | +# --compatible-runtimes java8
48 | +#LAYER_ARN=`aws lambda list-layer-versions --layer-name serverless-java-spring-boot-layer1 --max-items 1 | jq ".LayerVersions[0].LayerVersionArn" -r`
49 | +#echo "JAR layer ARN: ${LAYER_ARN}"
50 | +
51 | aws cloudformation package --template-file sam-template.yaml --s3-bucket ${S3_BUCKET} --s3-prefix java --output-template-file sam-package.yaml
52 |
53 | PARAMS="SNSTopic=${SNS_TOPIC} SQSQueue=${SQS_QUEUE}"
54 | +# CodeLayers=${LAYER_ARN}
55 |
56 | aws cloudformation deploy --template-file sam-package.yaml --stack-name ServerlessSpringApi --parameter-overrides ${PARAMS} --capabilities CAPABILITY_IAM
57 |
58 | diff --git b/sam-local a/sam-local
59 | index 047b969..98d8748 100755
60 | --- b/sam-local
61 | +++ a/sam-local
62 | @@ -1,4 +1,9 @@
63 | #!/bin/bash
64 |
65 | -sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json
66 | +LAYER_ARN=`aws lambda list-layer-versions --layer-name serverless-java-spring-boot-layer1 --max-items 1 | jq ".LayerVersions[0].LayerVersionArn" -r`
67 | +echo "JAR layer ARN: ${LAYER_ARN}"
68 | +
69 | +PARAMS=ParameterKey=CodeLayers,ParameterValue=${LAYER_ARN}
70 | +
71 | +sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json --parameter-overrides ${PARAMS}
72 |
73 | diff --git b/sam-template.yaml a/sam-template.yaml
74 | index 17a9b5f..6481466 100644
75 | --- b/sam-template.yaml
76 | +++ a/sam-template.yaml
77 | @@ -14,6 +14,8 @@ Parameters:
78 | Type: String
79 | SQSQueue:
80 | Type: String
81 | +# CodeLayers:
82 | +# Type: CommaDelimitedList
83 |
84 | Resources:
85 | ApiGatewayApi:
86 | @@ -24,16 +26,27 @@ Resources:
87 | Variables:
88 | Stage:
89 | Ref: Stage
90 | + MyServiceLayer:
91 | + Type: AWS::Serverless::LayerVersion
92 | + Properties:
93 | + LayerName: serverless-java-spring-boot-layer1
94 | + ContentUri: "build/distributions/serverless-java-spring-boot-layer1.zip"
95 | + CompatibleRuntimes:
96 | + - java8
97 | + RetentionPolicy: Retain
98 | MyServiceFunction:
99 | Type: AWS::Serverless::Function
100 | FunctionName: "ServerlessSpringApi-MyServiceFunction"
101 | Properties:
102 | Handler: my.service.StreamLambdaHandler::handleRequest
103 | Runtime: java8
104 | - CodeUri: "build/distributions/serverless-java-spring-boot.zip"
105 | + CodeUri: "build/distributions/serverless-java-spring-boot-code.zip"
106 | MemorySize: 2048
107 | Policies: AWSLambdaBasicExecutionRole
108 | Timeout: 30
109 | + Layers:
110 | +# Ref: CodeLayers
111 | + - !Ref MyServiceLayer
112 | Environment:
113 | Variables:
114 | # Specify here variables for deployment,
115 |
--------------------------------------------------------------------------------
/src/test/java/my/service/StreamLambdaHandlerTest.java:
--------------------------------------------------------------------------------
1 | package my.service;
2 |
3 |
4 | import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
5 | import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
6 | import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
7 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
8 | import com.amazonaws.services.lambda.runtime.Context;
9 | import com.fasterxml.jackson.databind.ObjectMapper;
10 |
11 | import org.junit.BeforeClass;
12 | import org.junit.Test;
13 |
14 | import javax.ws.rs.HttpMethod;
15 | import javax.ws.rs.core.HttpHeaders;
16 | import javax.ws.rs.core.MediaType;
17 | import javax.ws.rs.core.Response;
18 |
19 | import java.io.ByteArrayOutputStream;
20 | import java.io.IOException;
21 | import java.io.InputStream;
22 | import java.util.Map;
23 |
24 | import static org.junit.Assert.*;
25 |
26 |
27 | public class StreamLambdaHandlerTest {
28 |
29 | private static StreamLambdaHandler handler;
30 | private static Context lambdaContext;
31 |
32 | @BeforeClass
33 | public static void setUp() {
34 | handler = new StreamLambdaHandler();
35 | lambdaContext = new MockLambdaContext();
36 | }
37 |
38 | @Test
39 | public void ping_streamRequest_respondsWithHello() throws IOException {
40 | InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET)
41 | .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
42 | .buildStream();
43 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
44 |
45 | handle(requestStream, responseStream);
46 |
47 | AwsProxyResponse response = readResponse(responseStream);
48 | assertNotNull(response);
49 | assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
50 |
51 | assertFalse(response.isBase64Encoded());
52 |
53 | // Check it is parseable
54 | String json = response.getBody();
55 | ObjectMapper om = new ObjectMapper();
56 | om.readValue(json, Map.class);
57 |
58 | assertTrue(json.contains("pong"));
59 | assertTrue(json.contains("Hello, World!"));
60 |
61 | assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE));
62 | assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON));
63 | }
64 |
65 | @Test
66 | public void cloudWatch_scheduled_event_processed() throws IOException {
67 | InputStream requestStream = getClass().getResourceAsStream("/scheduled-event.json");
68 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
69 | handle(requestStream, responseStream);
70 | }
71 |
72 | @Test
73 | public void cloudWatch_sns_event_processed() throws IOException {
74 | InputStream requestStream = getClass().getResourceAsStream("/sns-event.json");
75 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
76 | handle(requestStream, responseStream);
77 | }
78 |
79 | @Test
80 | public void cloudWatch_sqs_event_processed() throws IOException {
81 | InputStream requestStream = getClass().getResourceAsStream("/sqs-event.json");
82 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
83 | handle(requestStream, responseStream);
84 | }
85 |
86 | @Test
87 | public void invalidResource_streamRequest_responds404() {
88 | InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET)
89 | .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
90 | .buildStream();
91 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
92 |
93 | handle(requestStream, responseStream);
94 |
95 | AwsProxyResponse response = readResponse(responseStream);
96 | assertNotNull(response);
97 | assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode());
98 | }
99 |
100 | private void handle(InputStream is, ByteArrayOutputStream os) {
101 | try {
102 | handler.handleRequest(is, os, lambdaContext);
103 | } catch (IOException e) {
104 | e.printStackTrace();
105 | fail(e.getMessage());
106 | }
107 | }
108 |
109 | private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) {
110 | try {
111 | return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class);
112 | } catch (IOException e) {
113 | e.printStackTrace();
114 | fail("Error while parsing response: " + e.getMessage());
115 | }
116 | return null;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS='"-Xmx64m"'
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/events.md:
--------------------------------------------------------------------------------
1 | # Different types of events handled
2 |
3 | ## API HTTP(s) event:
4 |
5 | {
6 | "resource": "/ping",
7 | "path": "/ping",
8 | "httpMethod": "GET",
9 | "headers": {
10 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
11 | "accept-encoding": "gzip, deflate, br",
12 | "accept-language": "en-US,en-FI;q=0.9,en;q=0.8,ru-RU;q=0.7,ru;q=0.6,fi;q=0.5",
13 | "dnt": "1",
14 | "Host": "deadbeef.execute-api.eu-west-1.amazonaws.com",
15 | "upgrade-insecure-requests": "1",
16 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36",
17 | "X-Amzn-Trace-Id": "Root=1-5c6bc702-3898654289f358229a7a44a1",
18 | "X-Forwarded-For": "8.8.8.8",
19 | "X-Forwarded-Port": "443",
20 | "X-Forwarded-Proto": "https"
21 | },
22 | "multiValueHeaders": {
23 | "accept": [
24 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
25 | ],
26 | "accept-encoding": [
27 | "gzip, deflate, br"
28 | ],
29 | "accept-language": [
30 | "en-US,en-FI;q=0.9,en;q=0.8,ru-RU;q=0.7,ru;q=0.6,fi;q=0.5"
31 | ],
32 | "dnt": [
33 | "1"
34 | ],
35 | "Host": [
36 | "deadbeef.execute-api.eu-west-1.amazonaws.com"
37 | ],
38 | "upgrade-insecure-requests": [
39 | "1"
40 | ],
41 | "User-Agent": [
42 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36"
43 | ],
44 | "X-Amzn-Trace-Id": [
45 | "Root=1-5c6bc702-3898654289f358229a7a44a1"
46 | ],
47 | "X-Forwarded-For": [
48 | "8.8.8.8"
49 | ],
50 | "X-Forwarded-Port": [
51 | "443"
52 | ],
53 | "X-Forwarded-Proto": [
54 | "https"
55 | ]
56 | },
57 | "queryStringParameters": null,
58 | "multiValueQueryStringParameters": null,
59 | "pathParameters": null,
60 | "stageVariables": {
61 | "Stage": "dev"
62 | },
63 | "requestContext": {
64 | "resourceId": "6asu8s",
65 | "resourcePath": "/ping",
66 | "httpMethod": "GET",
67 | "extendedRequestId": "VVwIYF7nDoEFexw=",
68 | "requestTime": "19/Feb/2019:09:06:10 +0000",
69 | "path": "/dev/ping",
70 | "accountId": "849707200000",
71 | "protocol": "HTTP/1.1",
72 | "stage": "dev",
73 | "domainPrefix": "deadbeef",
74 | "requestTimeEpoch": 1550567170476,
75 | "requestId": "99788f80-3425-11e9-b73f-cd1fe08a2b6e",
76 | "identity": {
77 | "cognitoIdentityPoolId": null,
78 | "accountId": null,
79 | "cognitoIdentityId": null,
80 | "caller": null,
81 | "sourceIp": "8.8.8.8",
82 | "accessKey": null,
83 | "cognitoAuthenticationType": null,
84 | "cognitoAuthenticationProvider": null,
85 | "userArn": null,
86 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36",
87 | "user": null
88 | },
89 | "domainName": "deadbeef.execute-api.eu-west-1.amazonaws.com",
90 | "apiId": "deadbeef"
91 | },
92 | "body": null,
93 | "isBase64Encoded": false
94 | }
95 |
96 | ## CloudWatch scheduled event:
97 |
98 | {
99 | "version": "0",
100 | "id": "2528c5e5-7feb-9a8d-9329-b445a222897f",
101 | "detail-type": "Scheduled Event",
102 | "source": "aws.events",
103 | "account": "849707200000",
104 | "time": "2019-02-19T08:59:04Z",
105 | "region": "eu-west-1",
106 | "resources": [
107 | "arn:aws:events:eu-west-1:849707200000:rule/ServerlessSpringApi-MyServiceFunctionCheckWebsiteS-17EIBMQX0K6PN"
108 | ],
109 | "detail": {}
110 | }
111 |
112 | ## SNS Topic message
113 |
114 | {
115 | "Records": [
116 | {
117 | "EventSource": "aws:sns",
118 | "EventVersion": "1.0",
119 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
120 | "Sns": {
121 | "Type": "Notification",
122 | "MessageId": "bcdfbdd1-eb20-58a7-bf97-e934236795c2",
123 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic",
124 | "Subject": null,
125 | "Message": "dasdsadsa",
126 | "Timestamp": "2019-02-19T09:01:59.174Z",
127 | "SignatureVersion": "1",
128 | "Signature": "Bk2H8nqd9WHlxnBaLjIymS7WK8TJ9baFiSNUn6dcVqRaGqjI6yq66/Qsh2lc09tBTV4eeDonRQWN1nNJfOeBinRunnsoP2DTIfypORo+UsRXGXEUeQJ7tnQUQe52rkVU5G+hX+hafU0hko6NhtCYgxd2ANtCm4klF3oEDybaRFZN1D0W0WZ3u+5jQNQU01rBca8VZ+hCLcQzJtH51p+VKrLYMpVz5h1jFxEB3XTkUtG9Q04bPypuY4o/Qh4mCx1pgghhpeZp6hOvoCWYQrj8EVyJo/SXynXGtK/RPsugRgwCMHRm/TsU1HHJKEVSe8ATMQX1oA6cXfFRzctm9tKnVQ==",
129 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem",
130 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
131 | "MessageAttributes": {}
132 | }
133 | }
134 | ]
135 | }
136 |
137 | {
138 | "Records": [
139 | {
140 | "EventSource": "aws:sns",
141 | "EventVersion": "1.0",
142 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
143 | "Sns": {
144 | "Type": "Notification",
145 | "MessageId": "15d202b9-ecca-5bf1-86aa-565c2460e18e",
146 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic",
147 | "Subject": null,
148 | "Message": "hello world!",
149 | "Timestamp": "2019-02-19T09:03:38.472Z",
150 | "SignatureVersion": "1",
151 | "Signature": "Gtt+vpFy8xd5jEBqChL3GU2DFS2WKFzMzmuUCcndy2y+UH4XJZ9TaU082rXtKZdNnhPuGrRygoOJkemLhh6q2kHu25FkvYBfkddBVCgr/wpgxnukqwlu3xYnmrtNqDJM8louZ38ZGk99miMf6NO1CloleIRc+EA1Vgfq7B/fE8lX1863VlLFViS36G86yUeFagNmPMnnPu7VFOi4RQRnzWa8/fKTGgvLMyGFesv6eWPFgDLEL6wGOpd/0gfxh6vq7sU9yuqQFkLvHTX5j6gf1+9Cfh1yxoJkz36NG3Oo17bIBiSsTqMl5A4Wc/3EKkGaPyjxPRPvxVof4ES0a6pPIA==",
152 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem",
153 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306",
154 | "MessageAttributes": {
155 | "somekey": {
156 | "Type": "String",
157 | "Value": "dsdasdas"
158 | }
159 | }
160 | }
161 | }
162 | ]
163 | }
164 |
165 | ## SQS
166 |
167 | {
168 | "Records": [
169 | {
170 | "messageId": "72b03944-1af7-492b-aac9-412698960531",
171 | "receiptHandle": "AQEBicU8TU0NRCELI9WEuVNN/2EU6G1KUJTSVO9BoDPAUvZk4yLsyMQiiKLf0LNmx7ETrdQ1CrwlwdE7JaBVGAcLWPrMCx8wZLxu6x8QtlITVIIAEIwjt+5/rnbmUMY0ajOMok80gO9SceLa+Zr1g3yZnjUMf3xNSGqynvooQkHFRAtSTKf0bkkagkKIHCDt5RghXUeIYruEZTStOm7RTVQwWxaCTQMvbBgNAZFG6j54qg1hCI9Cv9P95FH6Tt8yRDjf9Ad3s7Jykm0yy+IWfvpvHBoSelM03LrUUTX5E6lFpmTByIIE+fmamzeTZOevyNviiiMRbvqvDkDlrVs3M00zjiocX5jz+GVUmE5q0tOzwGYC4yWbcPKDSOc1hiRYUhbo",
172 | "body": "Howdy?",
173 | "attributes": {
174 | "ApproximateReceiveCount": "1",
175 | "SentTimestamp": "1550567133640",
176 | "SenderId": "AIDAIEHDD23VAQL6ZVIAC",
177 | "ApproximateFirstReceiveTimestamp": "1550567133641"
178 | },
179 | "messageAttributes": {
180 | "someattr": {
181 | "stringValue": "someval",
182 | "stringListValues": [],
183 | "binaryListValues": [],
184 | "dataType": "String"
185 | }
186 | },
187 | "md5OfBody": "47b68d3722ece13893e9c5fcb3cac906",
188 | "md5OfMessageAttributes": "1e4b169c724503f3886d943e5bbe88e5",
189 | "eventSource": "aws:sqs",
190 | "eventSourceARN": "arn:aws:sqs:eu-west-1:849707200000:test-queue",
191 | "awsRegion": "eu-west-1"
192 | }
193 | ]
194 | }
195 |
196 |
--------------------------------------------------------------------------------
/src/main/java/my/service/StreamLambdaHandler.java:
--------------------------------------------------------------------------------
1 | package my.service;
2 |
3 | import com.amazonaws.serverless.exceptions.ContainerInitializationException;
4 | import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
5 | import com.amazonaws.serverless.proxy.internal.jaxrs.AwsProxySecurityContext;
6 | import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity;
7 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
8 | import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext;
9 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
10 | import com.amazonaws.serverless.proxy.model.Headers;
11 | import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap;
12 | import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler;
13 | import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler;
14 | import com.amazonaws.services.lambda.runtime.Context;
15 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
16 | import com.amazonaws.services.lambda.runtime.events.SNSEvent;
17 | import com.amazonaws.services.lambda.runtime.events.SQSEvent;
18 | import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
19 | import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord;
20 | import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
21 | import com.fasterxml.jackson.annotation.JsonIgnore;
22 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
23 | import com.fasterxml.jackson.annotation.JsonProperty;
24 | import com.fasterxml.jackson.core.JsonParser;
25 | import com.fasterxml.jackson.core.JsonProcessingException;
26 | import com.fasterxml.jackson.databind.DeserializationContext;
27 | import com.fasterxml.jackson.databind.ObjectMapper;
28 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
29 | import com.fasterxml.jackson.databind.module.SimpleModule;
30 | import com.fasterxml.jackson.datatype.joda.JodaModule;
31 |
32 | import org.apache.commons.io.IOUtils;
33 | import org.slf4j.Logger;
34 | import org.slf4j.LoggerFactory;
35 | import org.springframework.http.MediaType;
36 |
37 | import java.io.ByteArrayOutputStream;
38 | import java.io.IOException;
39 | import java.io.InputStream;
40 | import java.io.OutputStream;
41 | import java.util.List;
42 | import java.util.Map;
43 |
44 | public class StreamLambdaHandler implements RequestStreamHandler {
45 | private static Logger log = LoggerFactory.getLogger(StreamLambdaHandler.class);
46 |
47 | private static SpringBootLambdaContainerHandler handler;
48 |
49 | static {
50 | try {
51 | log.info("Starting AWS Lambda");
52 | handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
53 | log.info("Created AWS Handler, {}", handler);
54 | } catch (ContainerInitializationException e) {
55 | // if we fail here. We re-throw the exception to force another cold start
56 | e.printStackTrace();
57 | throw new RuntimeException("Could not initialize Spring Boot application", e);
58 | }
59 | }
60 |
61 | @Override
62 | @SuppressWarnings("unchecked")
63 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
64 | log.info("Handle request {}, {}, {}, {}", context, context.getFunctionName(), context.getInvokedFunctionArn(),
65 | context.getLogStreamName());
66 | ByteArrayOutputStream bos = new ByteArrayOutputStream();
67 | IOUtils.copy(inputStream, bos);
68 | String event = new String(bos.toByteArray(),
69 | LambdaContainerHandler.getContainerConfig().getDefaultContentCharset());
70 | log.info("Got event {} context {}", event);
71 |
72 | ObjectMapper mapper = LambdaContainerHandler.getObjectMapper();
73 | mapper.registerModule(new JodaModule());
74 |
75 | SimpleModule module = new SimpleModule();
76 | module.addDeserializer(SNSRecord.class, new SNSRecordDeserializer());
77 | module.addDeserializer(SQSMessage.class, new SQSMessageDeserializer());
78 | mapper.registerModule(module);
79 |
80 | AwsProxyRequest request = mapper.readValue(event, AwsProxyRequest.class);
81 | if (request.getHttpMethod() == null || "".equals(request.getHttpMethod())) {
82 | log.info("Parsing AWS event {}", event);
83 | Map raw = (Map) mapper.readValue(event, Map.class);
84 |
85 | // Peek inside one event
86 | if (raw.get("Records") instanceof List) {
87 | List