From b83323fab79f11f05ae19c44e32bd6fbe8b4cce8 Mon Sep 17 00:00:00 2001 From: tobixlea Date: Fri, 16 Jan 2026 15:24:01 -0800 Subject: [PATCH] add springboot4 and spring 7 support --- .../continuous-integration-workflow.yml | 12 + README.md | 2 +- aws-serverless-java-container-core/pom.xml | 11 +- .../proxy/AwsProxyExceptionHandler.java | 4 +- .../internal/LambdaContainerHandler.java | 18 +- .../jaxrs/AwsHttpApiV2SecurityContext.java | 6 +- .../servlet/AwsHttpServletResponse.java | 30 ++ .../servlet/AwsProxyHttpServletRequest.java | 18 +- .../servlet/AwsServletInputStream.java | 20 + .../proxy/model/HttpApiV2AuthorizerMap.java | 39 +- .../proxy/AwsProxyExceptionHandlerTest.java | 14 +- .../testutils/AwsProxyRequestBuilder.java | 12 +- .../proxy/model/AwsProxyRequestTest.java | 2 +- .../model/HttpApiV2ProxyRequestTest.java | 14 +- aws-serverless-java-container-jersey/pom.xml | 26 +- .../proxy/jersey/JerseyAwsProxyTest.java | 10 +- .../proxy/jersey/JerseyParamEncodingTest.java | 7 +- aws-serverless-java-container-spring/pom.xml | 19 +- .../proxy/spring/SpringAwsProxyTest.java | 18 +- .../spring/echoapp/EchoSpringAppConfig.java | 2 +- .../spring/profile/SpringProfileTest.java | 2 +- .../pom.xml | 19 +- .../spotbugs-excludeFilter.xml | 28 ++ .../pom.xml | 408 ++++++++++++++++++ .../spotbugs-excludeFilter.xml | 28 ++ .../spring/AwsSpringAotTypesProcessor.java | 97 +++++ .../spring/AwsSpringHttpProcessingUtils.java | 224 ++++++++++ .../AwsSpringWebCustomRuntimeEventLoop.java | 187 ++++++++ .../AwsSpringWebRuntimeInitializer.java | 66 +++ .../SpringBootAwsProxyExceptionHandler.java | 27 ++ .../SpringBootLambdaContainerHandler.java | 230 ++++++++++ .../spring/SpringBootProxyHandlerBuilder.java | 88 ++++ ...pringDelegatingLambdaContainerHandler.java | 104 +++++ ...sReactiveServletEmbeddedServerFactory.java | 105 +++++ ...erverlessServletEmbeddedServerFactory.java | 65 +++ .../main/resources/META-INF/spring.factories | 2 + .../resources/META-INF/spring/aot.factories | 1 + .../proxy/spring/AWSWebRuntimeTests.java | 39 ++ .../AwsSpringHttpProcessingUtilsTests.java | 278 ++++++++++++ .../serverless/proxy/spring/JpaAppTest.java | 52 +++ .../proxy/spring/SecurityAppTest.java | 39 ++ .../proxy/spring/ServletAppTest.java | 236 ++++++++++ .../serverless/proxy/spring/SlowAppTest.java | 32 ++ ...DelegatingLambdaContainerHandlerTests.java | 335 ++++++++++++++ .../proxy/spring/WebFluxAppTest.java | 68 +++ ...rlessServletEmbeddedServerFactoryTest.java | 49 +++ .../proxy/spring/jpaapp/DatabaseConfig.java | 23 + .../proxy/spring/jpaapp/JpaApplication.java | 12 + .../proxy/spring/jpaapp/LambdaHandler.java | 59 +++ .../spring/jpaapp/MessageController.java | 31 ++ .../spring/securityapp/LambdaHandler.java | 25 ++ .../spring/securityapp/MessageController.java | 17 + .../securityapp/SecurityApplication.java | 14 + .../spring/securityapp/SecurityConfig.java | 45 ++ .../spring/servletapp/LambdaHandler.java | 60 +++ .../servletapp/LambdaStreamHandler.java | 63 +++ .../spring/servletapp/MessageController.java | 75 ++++ .../proxy/spring/servletapp/MessageData.java | 20 + .../spring/servletapp/ServletApplication.java | 25 ++ .../proxy/spring/servletapp/UserData.java | 50 +++ .../proxy/spring/slowapp/LambdaHandler.java | 40 ++ .../spring/slowapp/MessageController.java | 15 + .../spring/slowapp/SlowTestApplication.java | 21 + .../spring/webfluxapp/LambdaHandler.java | 58 +++ .../spring/webfluxapp/MessageController.java | 38 ++ .../proxy/spring/webfluxapp/MessageData.java | 20 + .../webfluxapp/WebFluxTestApplication.java | 14 + aws-serverless-jersey-archetype/pom.xml | 4 +- .../resources/archetype-resources/pom.xml | 12 +- .../src/main/java/StreamLambdaHandler.java | 1 + .../test/java/StreamLambdaHandlerTest.java | 3 +- aws-serverless-spring-archetype/pom.xml | 4 +- .../resources/archetype-resources/pom.xml | 8 +- .../test/java/StreamLambdaHandlerTest.java | 2 +- aws-serverless-springboot3-archetype/pom.xml | 4 +- aws-serverless-springboot4-archetype/pom.xml | 80 ++++ .../META-INF/maven/archetype-metadata.xml | 39 ++ .../resources/archetype-resources/README.md | 99 +++++ .../archetype-resources/build.gradle | 37 ++ .../resources/archetype-resources/pom.xml | 180 ++++++++ .../archetype-resources/src/assembly/bin.xml | 27 ++ .../src/main/java/Application.java | 24 ++ .../src/main/java/StreamLambdaHandler.java | 33 ++ .../main/java/controller/PingController.java | 20 + .../src/main/resources/application.properties | 3 + .../test/java/StreamLambdaHandlerTest.java | 88 ++++ .../archetype-resources/template.yml | 52 +++ .../projects/base/archetype.properties | 3 + .../src/test/resources/projects/base/goal.txt | 1 + pom.xml | 10 +- .../springboot3/pet-store-native/Dockerfile | 2 +- samples/springboot4/alt-pet-store/README.md | 56 +++ .../springboot4/alt-pet-store/build.gradle | 30 ++ samples/springboot4/alt-pet-store/pom.xml | 148 +++++++ .../alt-pet-store/src/assembly/bin.xml | 27 ++ .../sample/springboot4/Application.java | 51 +++ .../controller/PetsController.java | 90 ++++ .../filter/CognitoIdentityFilter.java | 69 +++ .../sample/springboot4/model/Error.java | 29 ++ .../sample/springboot4/model/Pet.java | 55 +++ .../sample/springboot4/model/PetData.java | 117 +++++ .../src/main/resources/logback.xml | 6 + .../springboot4/alt-pet-store/template.yml | 41 ++ .../springboot4/graphql-pet-store/README.md | 38 ++ samples/springboot4/graphql-pet-store/pom.xml | 168 ++++++++ .../graphql-pet-store/src/assembly/bin.xml | 27 ++ .../sample/springboot4/Application.java | 43 ++ .../springboot4/StreamLambdaHandler.java | 44 ++ .../controller/PetsController.java | 21 + .../filter/CognitoIdentityFilter.java | 69 +++ .../sample/springboot4/model/Owner.java | 20 + .../sample/springboot4/model/Pet.java | 20 + .../main/resources/graphql/schema.graphqls | 16 + .../src/main/resources/logback.xml | 5 + .../graphql-pet-store/template.yml | 35 ++ .../springboot4/pet-store-native/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 18 + .../springboot4/pet-store-native/Dockerfile | 37 ++ .../springboot4/pet-store-native/README.md | 39 ++ samples/springboot4/pet-store-native/mvnw | 308 +++++++++++++ samples/springboot4/pet-store-native/mvnw.cmd | 205 +++++++++ samples/springboot4/pet-store-native/pom.xml | 140 ++++++ .../pet-store-native/src/assembly/java.xml | 31 ++ .../pet-store-native/src/assembly/native.xml | 29 ++ .../sample/springboot4/DemoApplication.java | 12 + .../sample/springboot4/HelloController.java | 17 + .../controller/PetsController.java | 82 ++++ .../filter/CognitoIdentityFilter.java | 69 +++ .../sample/springboot4/model/Error.java | 29 ++ .../sample/springboot4/model/Pet.java | 55 +++ .../sample/springboot4/model/PetData.java | 117 +++++ .../src/main/resources/META-INF/.gitignore | 1 + .../src/main/resources/application.properties | 1 + .../pet-store-native/src/shell/java/bootstrap | 7 + .../src/shell/native/bootstrap | 5 + .../pet-store-native/template.yaml | 33 ++ samples/springboot4/pet-store/README.md | 36 ++ samples/springboot4/pet-store/build.gradle | 30 ++ samples/springboot4/pet-store/pom.xml | 160 +++++++ .../pet-store/src/assembly/bin.xml | 27 ++ .../sample/springboot4/Application.java | 49 +++ .../springboot4/StreamLambdaHandler.java | 50 +++ .../controller/PetsController.java | 77 ++++ .../filter/CognitoIdentityFilter.java | 69 +++ .../sample/springboot4/model/Error.java | 29 ++ .../sample/springboot4/model/Pet.java | 55 +++ .../sample/springboot4/model/PetData.java | 117 +++++ .../pet-store/src/main/resources/logback.xml | 5 + samples/springboot4/pet-store/template.yml | 35 ++ 150 files changed, 7640 insertions(+), 146 deletions(-) create mode 100644 aws-serverless-java-container-springboot3/spotbugs-excludeFilter.xml create mode 100644 aws-serverless-java-container-springboot4/pom.xml create mode 100644 aws-serverless-java-container-springboot4/spotbugs-excludeFilter.xml create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java create mode 100644 aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java create mode 100644 aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories create mode 100644 aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java create mode 100644 aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java create mode 100644 aws-serverless-springboot4-archetype/pom.xml create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java create mode 100644 aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml create mode 100644 aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties create mode 100644 aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt create mode 100644 samples/springboot4/alt-pet-store/README.md create mode 100644 samples/springboot4/alt-pet-store/build.gradle create mode 100644 samples/springboot4/alt-pet-store/pom.xml create mode 100644 samples/springboot4/alt-pet-store/src/assembly/bin.xml create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java create mode 100644 samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java create mode 100644 samples/springboot4/alt-pet-store/src/main/resources/logback.xml create mode 100644 samples/springboot4/alt-pet-store/template.yml create mode 100644 samples/springboot4/graphql-pet-store/README.md create mode 100644 samples/springboot4/graphql-pet-store/pom.xml create mode 100644 samples/springboot4/graphql-pet-store/src/assembly/bin.xml create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java create mode 100644 samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls create mode 100644 samples/springboot4/graphql-pet-store/src/main/resources/logback.xml create mode 100644 samples/springboot4/graphql-pet-store/template.yml create mode 100644 samples/springboot4/pet-store-native/.gitignore create mode 100644 samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar create mode 100644 samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties create mode 100644 samples/springboot4/pet-store-native/Dockerfile create mode 100644 samples/springboot4/pet-store-native/README.md create mode 100755 samples/springboot4/pet-store-native/mvnw create mode 100644 samples/springboot4/pet-store-native/mvnw.cmd create mode 100644 samples/springboot4/pet-store-native/pom.xml create mode 100644 samples/springboot4/pet-store-native/src/assembly/java.xml create mode 100644 samples/springboot4/pet-store-native/src/assembly/native.xml create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java create mode 100644 samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java create mode 100644 samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore create mode 100644 samples/springboot4/pet-store-native/src/main/resources/application.properties create mode 100644 samples/springboot4/pet-store-native/src/shell/java/bootstrap create mode 100644 samples/springboot4/pet-store-native/src/shell/native/bootstrap create mode 100644 samples/springboot4/pet-store-native/template.yaml create mode 100644 samples/springboot4/pet-store/README.md create mode 100644 samples/springboot4/pet-store/build.gradle create mode 100644 samples/springboot4/pet-store/pom.xml create mode 100644 samples/springboot4/pet-store/src/assembly/bin.xml create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java create mode 100644 samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java create mode 100644 samples/springboot4/pet-store/src/main/resources/logback.xml create mode 100644 samples/springboot4/pet-store/template.yml diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 2785cd3dc..0095c65a8 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -82,6 +82,18 @@ jobs: - name: Build with Spring Boot 3.3.x run: ./gha_build.sh springboot3 false false -Dspringboot.version=3.3.6 -Dspring.version=6.1.15 -Dspringsecurity.version=6.3.5 -Ddependency-check.skip=true + build_springboot4: + name: Build and test SpringBoot 4 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: 17 + - name: Build latest + run: ./gha_build.sh springboot4 true true # temporarily disabled as Struts is not released at the moment # build_struts2: # name: Build and test Struts diff --git a/README.md b/README.md index 3f26e0c1b..b7da76ea2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Currently the following versions are maintained: |---------|--------|-----------------------------|-----------------|------------------------|----------------|---------------| | 1.x | [1.x](https://github.com/aws/serverless-java-container/tree/1.x) | Java EE (javax.*) | 5.x (Boot 2.x) | 2.x | :white_check_mark: | :white_check_mark: | | 2.x | [main](https://github.com/aws/serverless-java-container/tree/main) | Jakarta EE 9-10 (jakarta.*) | 6.x (Boot 3.x) | 3.x | :x: | :x: | -| 3.x | | Jakarta EE 11 (jakarta.*) | 7.x (Boot 4.x) | 4.x | :x: | :x: | +| 3.x | [main](https://github.com/aws/serverless-java-container/tree/main) | Jakarta EE 11 (jakarta.*) | 7.x (Boot 4.x) | 4.x | :x: | :x: | Follow the quick start guides in [our wiki](https://github.com/aws/serverless-java-container/wiki) to integrate Serverless Java Container with your project: * [Spring quick start](https://github.com/aws/serverless-java-container/wiki/Quick-start---Spring) diff --git a/aws-serverless-java-container-core/pom.xml b/aws-serverless-java-container-core/pom.xml index e9aa6bbec..0090d8518 100644 --- a/aws-serverless-java-container-core/pom.xml +++ b/aws-serverless-java-container-core/pom.xml @@ -6,18 +6,19 @@ AWS Serverless Java container support - Core Allows Java applications written for a servlet container to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. 3.1.0 - 6.0.0 + 6.1.0 + 3.0.2 @@ -40,13 +41,13 @@ - com.fasterxml.jackson.core + tools.jackson.core jackson-databind ${jackson.version} - com.fasterxml.jackson.module + tools.jackson.module jackson-module-afterburner ${jackson.version} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java index 0ae00b4da..8e13bd799 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java @@ -18,7 +18,7 @@ import com.amazonaws.serverless.proxy.model.ErrorModel; import com.amazonaws.serverless.proxy.model.Headers; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import jakarta.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,7 +103,7 @@ protected String getErrorJson(String message) { try { return LambdaContainerHandler.getObjectMapper().writeValueAsString(new ErrorModel(message)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.error("Could not produce error JSON", e); return "{ \"message\": \"" + message + "\" }"; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index ff978456b..86d4216a1 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -19,12 +19,10 @@ import com.amazonaws.serverless.proxy.model.ContainerConfig; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,10 +84,9 @@ public abstract class LambdaContainerHandler { return subject; }); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.error("Error while attempting to parse JWT body for requestId: " + SecurityUtils.crlf(event.getRequestContext().getRequestId()), e); return null; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java index f5395a0bf..2e1d4827e 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java @@ -28,6 +28,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.HttpHeaders; +import java.nio.ByteBuffer; import jakarta.ws.rs.core.MediaType; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -150,6 +151,16 @@ public void sendRedirect(String s) throws IOException { flushBuffer(); } + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + setStatus(sc); + addHeader(HttpHeaders.LOCATION, location); + if (clearBuffer) { + resetBuffer(); + } + flushBuffer(); + } + @Override public void setDateHeader(String s, long l) { @@ -297,6 +308,25 @@ public void write(int b) throws IOException { } } + @Override + public void write(ByteBuffer b) throws IOException { + try { + if (b.hasArray()) { + bodyOutputStream.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); + b.position(b.limit()); + } else { + byte[] buf = new byte[b.remaining()]; + b.get(buf); + bodyOutputStream.write(buf); + } + } catch (Exception e) { + log.error("Cannot write to output stream", e); + if (listener != null) { + listener.onError(e); + } + } + } + @Override public void flush() throws IOException { flushBuffer(); diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java index 9c4b7971b..c2a257d34 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java @@ -607,12 +607,22 @@ private List getHeaderValues(String key) { if (request.getRequestSource() == RequestSource.API_GATEWAY) { if ("referer".equals(key.toLowerCase(Locale.ENGLISH))) { - values.add(request.getRequestContext().getIdentity().getCaller()); - return values; + if (request.getRequestContext() != null && request.getRequestContext().getIdentity() != null) { + String caller = request.getRequestContext().getIdentity().getCaller(); + if (caller != null) { + values.add(caller); + return values; + } + } } if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) { - values.add(request.getRequestContext().getIdentity().getUserAgent()); - return values; + if (request.getRequestContext() != null && request.getRequestContext().getIdentity() != null) { + String userAgent = request.getRequestContext().getIdentity().getUserAgent(); + if (userAgent != null) { + values.add(userAgent); + return values; + } + } } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java index eb155a364..220ca602f 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java @@ -20,6 +20,7 @@ import jakarta.servlet.ServletInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; public class AwsServletInputStream extends ServletInputStream { private static Logger log = LoggerFactory.getLogger(AwsServletInputStream.class); @@ -73,4 +74,23 @@ public int read() } return readByte; } + + @Override + public int read(ByteBuffer b) throws IOException { + if (bodyStream == null || bodyStream instanceof NullInputStream) { + return -1; + } + if (!b.hasRemaining()) { + return 0; + } + byte[] buf = new byte[b.remaining()]; + int bytesRead = bodyStream.read(buf); + if (bytesRead > 0) { + b.put(buf, 0, bytesRead); + } + if (bytesRead == -1) { + finished = true; + } + return bytesRead; + } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java index 2cf6d77a6..8226be983 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java @@ -13,17 +13,16 @@ package com.amazonaws.serverless.proxy.model; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.ser.std.StdSerializer; +import tools.jackson.databind.type.TypeFactory; import java.io.IOException; import java.util.HashMap; @@ -77,10 +76,9 @@ public HttpApiV2AuthorizerDeserializer() { } @Override - public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException, JsonProcessingException { + public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) { HttpApiV2AuthorizerMap map = new HttpApiV2AuthorizerMap(); - JsonNode node = jsonParser.getCodec().readTree(jsonParser); + JsonNode node = deserializationContext.readTree(jsonParser); if (node.has(JWT_KEY)) { HttpApiV2JwtAuthorizer authorizer = LambdaContainerHandler.getObjectMapper() .treeToValue(node.get(JWT_KEY), HttpApiV2JwtAuthorizer.class); @@ -88,7 +86,7 @@ public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, Deserialization } if (node.has(LAMBDA_KEY)) { Map context = LambdaContainerHandler.getObjectMapper().treeToValue(node.get(LAMBDA_KEY), - TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, Object.class)); + LambdaContainerHandler.getObjectMapper().getTypeFactory().constructMapType(HashMap.class, String.class, Object.class)); map.put(LAMBDA_KEY, context); } if (node.has(IAM_KEY)) { @@ -110,16 +108,19 @@ public HttpApiV2AuthorizerSerializer() { @Override public void serialize(HttpApiV2AuthorizerMap httpApiV2AuthorizerMap, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { + SerializationContext serializationContext) { jsonGenerator.writeStartObject(); if (httpApiV2AuthorizerMap.isJwt()) { - jsonGenerator.writeObjectField(JWT_KEY, httpApiV2AuthorizerMap.getJwtAuthorizer()); + jsonGenerator.writeName(JWT_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.getJwtAuthorizer()); } if (httpApiV2AuthorizerMap.isLambda()) { - jsonGenerator.writeObjectField(LAMBDA_KEY, httpApiV2AuthorizerMap.getLambdaAuthorizerContext()); + jsonGenerator.writeName(LAMBDA_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.getLambdaAuthorizerContext()); } if (httpApiV2AuthorizerMap.isIam()) { - jsonGenerator.writeObjectField(IAM_KEY, httpApiV2AuthorizerMap.get(IAM_KEY)); + jsonGenerator.writeName(IAM_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.get(IAM_KEY)); } jsonGenerator.writeEndObject(); } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java index 012827e40..e15b9876f 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java @@ -5,8 +5,8 @@ import com.amazonaws.serverless.exceptions.InvalidResponseObjectException; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.ErrorModel; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -47,7 +47,7 @@ void typedHandle_InvalidRequestEventException_500State() { @Test void typedHandle_InvalidRequestEventException_responseString() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new InvalidRequestEventException(INVALID_REQUEST_MESSAGE, null)); assertNotNull(resp); @@ -74,7 +74,7 @@ void typedHandle_InvalidResponseObjectException_502State() { @Test void typedHandle_InvalidResponseObjectException_responseString() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new InvalidResponseObjectException(INVALID_RESPONSE_MESSAGE, null)); assertNotNull(resp); @@ -106,7 +106,7 @@ void typedHandle_InternalServerErrorException_500State() { @Test void typedHandle_InternalServerErrorException_responseString() - throws JsonProcessingException { + throws JacksonException { InternalServerErrorException mockInternalServerErrorException = Mockito.mock(InternalServerErrorException.class); Mockito.when(mockInternalServerErrorException.getMessage()).thenReturn(INTERNAL_SERVER_ERROR_MESSAGE); @@ -131,7 +131,7 @@ void typedHandle_InternalServerErrorException_jsonContentTypeHeader() { @Test void typedHandle_NullPointerException_responseObject() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new NullPointerException()); assertNotNull(resp); @@ -248,7 +248,7 @@ void getErrorJson_ErrorModel_validJson() void getErrorJson_JsonParsinException_validJson() throws IOException { ObjectMapper mockMapper = mock(ObjectMapper.class); - JsonProcessingException exception = mock(JsonProcessingException.class); + JacksonException exception = mock(JacksonException.class); when(mockMapper.writeValueAsString(any(Object.class))).thenThrow(exception); String output = exceptionHandler.getErrorJson(INVALID_RESPONSE_MESSAGE); diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java index e42130453..a817bd2bf 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java @@ -15,8 +15,8 @@ import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.model.*; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.ContentType; @@ -106,7 +106,7 @@ public AwsProxyRequestBuilder alb() { try { String json = objectMapper.writeValueAsString(this.request); albRequest = objectMapper.readValue(json, AwsProxyRequest.class); - } catch (JsonProcessingException jpe) { + } catch (JacksonException jpe) { throw new RuntimeException(jpe); } @@ -265,7 +265,7 @@ public AwsProxyRequestBuilder body(Object body) { if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)) { try { return body(LambdaContainerHandler.getObjectMapper().writeValueAsString(body)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new UnsupportedOperationException("Could not serialize object: " + e.getMessage()); } } else { @@ -438,7 +438,7 @@ public InputStream buildStream() { try { String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(request); return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { return null; } } @@ -448,7 +448,7 @@ public InputStream toHttpApiV2RequestStream() { try { String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(req); return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { return null; } } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java index 6d0aa9eeb..07bdff979 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java @@ -7,7 +7,7 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; public class AwsProxyRequestTest { private static final String CUSTOM_HEADER_KEY_LOWER_CASE = "custom-header"; diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java index 20ff4dff2..e51a1602b 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java @@ -1,7 +1,7 @@ package com.amazonaws.serverless.proxy.model; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -181,7 +181,7 @@ void deserialize_fromJsonString_authorizerPopulatedCorrectly() { assertTrue(req.getRequestContext().getAuthorizer().getJwtAuthorizer().getClaims().containsKey("claim1")); assertEquals(2, req.getRequestContext().getAuthorizer().getJwtAuthorizer().getScopes().size()); assertEquals(RequestSource.API_GATEWAY, req.getRequestSource()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -196,7 +196,7 @@ void deserialize_fromJsonString_authorizerEmptyMap() { assertFalse(req.getRequestContext().getAuthorizer().isJwt()); assertFalse(req.getRequestContext().getAuthorizer().isLambda()); assertFalse(req.getRequestContext().getAuthorizer().isIam()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -212,7 +212,7 @@ void deserialize_fromJsonString_lambdaAuthorizer() { assertTrue(req.getRequestContext().getAuthorizer().isLambda()); assertEquals(5, req.getRequestContext().getAuthorizer().getLambdaAuthorizerContext().size()); assertEquals(1, req.getRequestContext().getAuthorizer().getLambdaAuthorizerContext().get("numberKey")); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -239,7 +239,7 @@ void deserialize_fromJsonString_iamAuthorizer() { req.getRequestContext().getAuthorizer().getIamAuthorizer().getUserArn()); assertEquals("AIDACOSFODNN7EXAMPLE2", req.getRequestContext().getAuthorizer().getIamAuthorizer().getUserId()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -254,7 +254,7 @@ void deserialize_fromJsonString_isBase64EncodedPopulates() { req = LambdaContainerHandler.getObjectMapper().readValue(NO_AUTH_PROXY, HttpApiV2ProxyRequest.class); assertTrue(req.isBase64Encoded()); assertEquals(RequestSource.API_GATEWAY, req.getRequestSource()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -277,7 +277,7 @@ void serialize_toJsonString_authorizerPopulatesCorrectly() { assertTrue(reqString.contains("\"scopes\":[\"first\",\"second\"]")); assertTrue(reqString.contains("\"authorizer\":{\"jwt\":{")); assertTrue(reqString.contains("\"isBase64Encoded\":false")); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while serializing request" + e.getMessage()); } diff --git a/aws-serverless-java-container-jersey/pom.xml b/aws-serverless-java-container-jersey/pom.xml index 1788a36b8..8fcc07c4b 100644 --- a/aws-serverless-java-container-jersey/pom.xml +++ b/aws-serverless-java-container-jersey/pom.xml @@ -6,12 +6,12 @@ AWS Serverless Java container support - Jersey implementation Allows Java applications written for Jersey to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. @@ -24,18 +24,12 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT - - - com.fasterxml.jackson.core - jackson-databind - - + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT tests test-jar test @@ -68,14 +62,6 @@ test - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - true - test - - org.glassfish.jersey.media jersey-media-json-jackson @@ -88,11 +74,11 @@ jackson-annotations - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java index b845a0b69..23f1078fa 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java @@ -24,8 +24,8 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Base64; import org.glassfish.jersey.logging.LoggingFeature; import org.glassfish.jersey.media.multipart.MultiPartFeature; @@ -303,7 +303,7 @@ void error_statusCode_methodNotAllowed(String reqType) { @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody(String reqType) throws JacksonException { initJerseyAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); @@ -460,7 +460,7 @@ private void validateMapResponseModel(AwsProxyResponse output, String key, Strin MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(key)); assertEquals(value, response.getValues().get(key)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -471,7 +471,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java index 9dc1ab32a..5ca09b11b 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java @@ -11,7 +11,8 @@ import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.junit.jupiter.api.Disabled; @@ -281,7 +282,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -292,7 +293,7 @@ private void validateMapResponseModel(AwsProxyResponse output, String key, Strin MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(key)); assertEquals(value, response.getValues().get(key)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml index f03d9db0a..d85534faf 100644 --- a/aws-serverless-java-container-spring/pom.xml +++ b/aws-serverless-java-container-spring/pom.xml @@ -6,18 +6,18 @@ AWS Serverless Java container support - Spring implementation Allows Java applications written for the Spring framework to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. - 6.2.8 - 6.5.1 + 7.0.0 + 7.0.0 @@ -25,12 +25,12 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT tests test-jar test @@ -57,12 +57,7 @@ test - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - test - + jakarta.activation diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java index 9504b6166..4b7d3eb81 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -15,8 +15,8 @@ import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; import com.amazonaws.serverless.proxy.spring.echoapp.model.SingleValueModel; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -294,7 +294,7 @@ void error_unauthenticatedCall_filterStepsRequest(String reqType) { @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody(String reqType) throws JacksonException { initSpringAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); @@ -311,7 +311,7 @@ void responseBody_responseWriter_validBody(String reqType) throws JsonProcessing @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody_UTF(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody_UTF(String reqType) throws JacksonException { initSpringAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(UNICODE_VALUE); @@ -363,7 +363,7 @@ void injectBody_populatedResponse_noException(String reqType) { try { SingleValueModel output = objectMapper.readValue(response.getBody(), SingleValueModel.class); assertEquals("true", output.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail(); } @@ -373,7 +373,7 @@ void injectBody_populatedResponse_noException(String reqType) { try { SingleValueModel output = objectMapper.readValue(emptyResp.getBody(), SingleValueModel.class); assertNull(output.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail(); } @@ -392,7 +392,7 @@ void servletRequestEncoding_acceptEncoding_okStatusCode(String reqType) { .header(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate") .queryString("status", "200") .body(objectMapper.writeValueAsString(singleValueModel)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { fail("Could not serialize object to JSON"); } @@ -484,7 +484,7 @@ private void validateMapResponseModel(AwsProxyResponse output) { MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(CUSTOM_HEADER_KEY)); assertEquals(CUSTOM_HEADER_VALUE, response.getValues().get(CUSTOM_HEADER_KEY)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -495,7 +495,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java index cf4aa495e..9b3e0cecc 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java @@ -1,7 +1,7 @@ package com.amazonaws.serverless.proxy.spring.echoapp; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java index 7742db7f6..d08a3e45c 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java @@ -8,7 +8,7 @@ import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.echoapp.EchoSpringAppConfig; import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/aws-serverless-java-container-springboot3/pom.xml b/aws-serverless-java-container-springboot3/pom.xml index 5dcac1562..3ababeb9b 100644 --- a/aws-serverless-java-container-springboot3/pom.xml +++ b/aws-serverless-java-container-springboot3/pom.xml @@ -3,7 +3,8 @@ aws-serverless-java-container com.amazonaws.serverless - 2.1.5-SNAPSHOT + 2.1.3 + .. 4.0.0 @@ -12,11 +13,10 @@ AWS Serverless Java container support - SpringBoot 3 implementation Allows Java applications written for SpringBoot 3 to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT 6.2.8 - 3.4.5 + 3.5.8 6.4.5 @@ -30,16 +30,22 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 2.1.3 com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 2.1.3 tests test-jar test + + com.github.spotbugs + spotbugs-annotations + 4.9.3 + provided + org.springframework spring-webflux @@ -296,6 +302,9 @@ com.github.spotbugs spotbugs-maven-plugin + + ${project.basedir}/spotbugs-excludeFilter.xml + + + + + + + + + + \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/pom.xml b/aws-serverless-java-container-springboot4/pom.xml new file mode 100644 index 000000000..e12ac19b5 --- /dev/null +++ b/aws-serverless-java-container-springboot4/pom.xml @@ -0,0 +1,408 @@ + + + + aws-serverless-java-container + com.amazonaws.serverless + 3.0.0-SNAPSHOT + .. + + 4.0.0 + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + AWS Serverless Java container support - SpringBoot 4 implementation + Allows Java applications written for SpringBoot 4 to run in AWS Lambda + https://aws.amazon.com/lambda + + + 7.0.1 + 4.0.0 + 7.0.0 + + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + + + + + + + org.springframework.cloud + spring-cloud-function-serverless-web + 5.0.0 + + + com.amazonaws.serverless + aws-serverless-java-container-core + 3.0.0-SNAPSHOT + + + com.amazonaws.serverless + aws-serverless-java-container-core + 3.0.0-SNAPSHOT + tests + test-jar + test + + + com.github.spotbugs + spotbugs-annotations + 4.9.3 + provided + + + org.springframework + spring-webflux + ${spring.version} + true + + + org.springframework.boot + spring-boot + ${springboot.version} + true + + + org.springframework + spring-context + + + org.springframework + spring-core + + + + + org.springframework.boot + spring-boot-autoconfigure + ${springboot.version} + true + + + org.springframework.boot + spring-boot-web-server + ${springboot.version} + + + org.springframework.boot + spring-boot-starter-web + ${springboot.version} + true + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-webmvc + ${springboot.version} + + + org.springframework + spring-core + ${spring.version} + true + + + org.springframework + spring-context + ${spring.version} + true + + + org.springframework + spring-webmvc + ${spring.version} + true + + + org.springframework + spring-aop + + + org.springframework + spring-expression + + + + + + org.springframework.security + spring-security-config + ${springsecurity.version} + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + org.springframework + spring-expression + + + org.springframework + spring-aop + + + test + + + org.springframework.security + spring-security-web + ${springsecurity.version} + + + org.springframework + spring-core + + + org.springframework + spring-web + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-expression + + + org.springframework + spring-aop + + + test + + + org.hibernate.validator + hibernate-validator + 9.1.0.Final + test + + + + org.junit.jupiter + junit-jupiter + test + + + + jakarta.validation + jakarta.validation-api + 3.1.1 + test + + + + jakarta.websocket + jakarta.websocket-api + 2.2.0 + test + + + + jakarta.websocket + jakarta.websocket-client-api + 2.2.0 + test + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${springboot.version} + test + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.apache.tomcat.embed + tomcat-embed-websocket + + + + + com.h2database + h2 + 2.3.232 + test + + + org.springframework.boot + spring-boot-starter-webflux + ${springboot.version} + true + + + org.glassfish.expressly + expressly + 6.0.0 + test + + + + + + + + + org.jacoco + jacoco-maven-plugin + + ${basedir}/target/coverage-reports/jacoco-unit.exec + ${basedir}/target/coverage-reports/jacoco-unit.exec + + + com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop* + com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor* + + + + + default-prepare-agent + + prepare-agent + + + + jacoco-site + package + + report + + + + jacoco-check + test + + check + + + true + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${jacoco.minCoverage} + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + com.github.spotbugs + spotbugs-maven-plugin + + ${project.basedir}/spotbugs-excludeFilter.xml + + + + + analyze-compile + compile + + check + + + + + + org.owasp + dependency-check-maven + ${dependencyCheck.version} + + true + + ${project.basedir}/../owasp-suppression.xml + + 7 + false + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + diff --git a/aws-serverless-java-container-springboot4/spotbugs-excludeFilter.xml b/aws-serverless-java-container-springboot4/spotbugs-excludeFilter.xml new file mode 100644 index 000000000..b26b1ad24 --- /dev/null +++ b/aws-serverless-java-container-springboot4/spotbugs-excludeFilter.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java new file mode 100644 index 000000000..0a461aa97 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.model.*; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import tools.jackson.core.JsonToken; + +/** + * AOT Initialization processor required to register reflective hints for GraalVM. + * This is necessary to ensure proper JSON serialization/deserialization. + * It is registered with META-INF/spring/aot.factories + * + * @author Oleg Zhurakousky + */ +public class AwsSpringAotTypesProcessor implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return new ReflectiveProcessorBeanFactoryInitializationAotContribution(); + } + + private static final class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + // known static types + + runtimeHints.reflection().registerType(AwsProxyRequest.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsProxyResponse.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(SingleValueHeaders.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(JsonToken.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(MultiValuedTreeMap.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(Headers.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsProxyRequestContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(ApiGatewayRequestIdentity.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsHttpServletResponse.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2ProxyRequest.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2HttpContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2ProxyRequestContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.HttpApiV2AuthorizerDeserializer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.HttpApiV2AuthorizerSerializer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2IamAuthorizer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2JwtAuthorizer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + } + + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java new file mode 100644 index 000000000..15984bc4e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java @@ -0,0 +1,224 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import com.amazonaws.serverless.proxy.internal.HttpUtils; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.model.RequestSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.function.serverless.web.ServerlessHttpServletRequest; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.MultiValueMapAdapter; +import org.springframework.util.StringUtils; + +import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter; +import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; + +import static com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest.decodeValueIfEncoded; +import static com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest.getQueryParamValuesAsList; + +class AwsSpringHttpProcessingUtils { + + private static Log logger = LogFactory.getLog(AwsSpringHttpProcessingUtils.class); + private static final int LAMBDA_MAX_REQUEST_DURATION_MINUTES = 15; + + private AwsSpringHttpProcessingUtils() { + + } + + public static AwsProxyResponse processRequest(HttpServletRequest request, ServerlessMVC mvc, + AwsProxyHttpServletResponseWriter responseWriter) { + CountDownLatch latch = new CountDownLatch(1); + AwsHttpServletResponse response = new AwsHttpServletResponse(request, latch); + try { + mvc.service(request, response); + boolean requestTimedOut = !latch.await(LAMBDA_MAX_REQUEST_DURATION_MINUTES, TimeUnit.MINUTES); // timeout is potentially lower as user configures it + if (requestTimedOut) { + logger.warn("request timed out after " + LAMBDA_MAX_REQUEST_DURATION_MINUTES + " minutes"); + } + AwsProxyResponse awsResponse = responseWriter.writeResponse(response, null); + return awsResponse; + } + catch (Exception e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + } + + public static String extractVersion() { + try { + String path = AwsSpringHttpProcessingUtils.class.getProtectionDomain().getCodeSource().getLocation().toString(); + int endIndex = path.lastIndexOf('.'); + if (endIndex < 0) { + return "UNKNOWN-VERSION"; + } + int startIndex = path.lastIndexOf("/") + 1; + return path.substring(startIndex, endIndex).replace("spring-cloud-function-serverless-web-", ""); + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to detect version", e); + } + return "UNKNOWN-VERSION"; + } + + } + + public static HttpServletRequest generateHttpServletRequest(InputStream jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + try { + String text = new String(FileCopyUtils.copyToByteArray(jsonRequest), StandardCharsets.UTF_8); + if (logger.isDebugEnabled()) { + logger.debug("Creating HttpServletRequest from: " + text); + } + return generateHttpServletRequest(text, lambdaContext, servletContext, mapper); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static HttpServletRequest generateHttpServletRequest(String jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + Map _request = readValue(jsonRequest, Map.class, mapper); + SecurityContextWriter securityWriter = "2.0".equals(_request.get("version")) + ? new AwsHttpApiV2SecurityContextWriter() + : new AwsProxySecurityContextWriter(); + HttpServletRequest httpServletRequest = "2.0".equals(_request.get("version")) + ? AwsSpringHttpProcessingUtils.generateRequest2(jsonRequest, lambdaContext, securityWriter, mapper, servletContext) + : AwsSpringHttpProcessingUtils.generateRequest1(jsonRequest, lambdaContext, securityWriter, mapper, servletContext); + return httpServletRequest; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static HttpServletRequest generateRequest1(String request, Context lambdaContext, + SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { + AwsProxyRequest v1Request = readValue(request, AwsProxyRequest.class, mapper); + + // Use AWS container's servlet request instead of Spring Cloud Function's + AwsProxyHttpServletRequest httpServletRequest = new AwsProxyHttpServletRequest(v1Request, lambdaContext, securityWriter.writeSecurityContext(v1Request, lambdaContext)); + httpServletRequest.setServletContext(servletContext); + return httpServletRequest; + } + + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static HttpServletRequest generateRequest2(String request, Context lambdaContext, + SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { + HttpApiV2ProxyRequest v2Request = readValue(request, HttpApiV2ProxyRequest.class, mapper); + + + ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, + v2Request.getRequestContext().getHttp().getMethod(), v2Request.getRequestContext().getHttp().getPath()); + populateQueryStringParametersV2(v2Request.getQueryStringParameters(), httpRequest); + + v2Request.getHeaders().forEach(httpRequest::setHeader); + + populateContentAndContentType( + v2Request.getBody(), + v2Request.getHeaders().get(HttpHeaders.CONTENT_TYPE), + v2Request.isBase64Encoded(), + httpRequest + ); + + httpRequest.setAttribute(RequestReader.HTTP_API_CONTEXT_PROPERTY, v2Request.getRequestContext()); + httpRequest.setAttribute(RequestReader.HTTP_API_STAGE_VARS_PROPERTY, v2Request.getStageVariables()); + httpRequest.setAttribute(RequestReader.HTTP_API_EVENT_PROPERTY, v2Request); + httpRequest.setAttribute(RequestReader.LAMBDA_CONTEXT_PROPERTY, lambdaContext); + httpRequest.setAttribute(RequestReader.JAX_SECURITY_CONTEXT_PROPERTY, + securityWriter.writeSecurityContext(v2Request, lambdaContext)); + return httpRequest; + } + + private static void populateQueryStringParametersV2(Map requestParameters, ServerlessHttpServletRequest httpRequest) { + if (!CollectionUtils.isEmpty(requestParameters)) { + for (Entry entry : requestParameters.entrySet()) { + // fix according to parseRawQueryString + httpRequest.setParameter(entry.getKey(), entry.getValue()); + } + } + } + + private static void populateQueryStringParametersV1(AwsProxyRequest v1Request, ServerlessHttpServletRequest httpRequest) { + Map requestParameters = v1Request.getQueryStringParameters(); + if (!CollectionUtils.isEmpty(requestParameters)) { + // decode all keys and values in map + for (Entry entry : requestParameters.entrySet()) { + String k = v1Request.getRequestSource() == RequestSource.ALB ? decodeValueIfEncoded(entry.getKey()) : entry.getKey(); + String v = v1Request.getRequestSource() == RequestSource.ALB ? decodeValueIfEncoded(entry.getValue()) : entry.getValue(); + httpRequest.setParameter(k, v); + } + } + } + + private static void populateMultiValueQueryStringParametersV1(AwsProxyRequest v1Request, ServerlessHttpServletRequest httpRequest) { + if (v1Request.getMultiValueQueryStringParameters() != null) { + MultiValueMapAdapter queryStringParameters = new MultiValueMapAdapter<>(v1Request.getMultiValueQueryStringParameters()); + queryStringParameters.forEach((k, v) -> { + String key = v1Request.getRequestSource() == RequestSource.ALB + ? decodeValueIfEncoded(k) + : k; + List value = v1Request.getRequestSource() == RequestSource.ALB + ? getQueryParamValuesAsList(v1Request.getMultiValueQueryStringParameters(), k, false).stream() + .map(AwsHttpServletRequest::decodeValueIfEncoded) + .toList() + : v; + httpRequest.setParameter(key, value.toArray(new String[0])); + }); + } + } + + private static T readValue(String json, Class clazz, ObjectMapper mapper) { + try { + return mapper.readValue(json, clazz); + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static void populateContentAndContentType( + String body, + String contentType, + boolean base64Encoded, + ServerlessHttpServletRequest httpRequest) { + if (StringUtils.hasText(body)) { + httpRequest.setContentType(contentType == null ? MediaType.APPLICATION_JSON_VALUE : contentType); + if (base64Encoded) { + httpRequest.setContent(Base64.getMimeDecoder().decode(body)); + } else { + Charset charseEncoding = HttpUtils.parseCharacterEncoding(contentType,StandardCharsets.UTF_8); + httpRequest.setContent(body.getBytes(charseEncoding)); + } + } + } + + + +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java new file mode 100644 index 000000000..970418246 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java @@ -0,0 +1,187 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.serverless.proxy.spring; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.env.Environment; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import jakarta.servlet.http.HttpServletRequest; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +/** + * Event loop and necessary configurations to support AWS Lambda Custom Runtime + * - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html. + * + * @author Oleg Zhurakousky + * @author Mark Sailes + * + */ +public final class AwsSpringWebCustomRuntimeEventLoop implements SmartLifecycle { + + private static Log logger = LogFactory.getLog(AwsSpringWebCustomRuntimeEventLoop.class); + + static final String LAMBDA_VERSION_DATE = "2018-06-01"; + private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error"; + private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next"; + private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response"; + private static final String USER_AGENT_VALUE = String.format("spring-cloud-function/%s-%s", + System.getProperty("java.runtime.version"), AwsSpringHttpProcessingUtils.extractVersion()); + + private final ServletWebServerApplicationContext applicationContext; + + private volatile boolean running; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + public AwsSpringWebCustomRuntimeEventLoop(ServletWebServerApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public void run() { + this.running = true; + this.executor.execute(() -> { + eventLoop(this.applicationContext); + }); + } + + @Override + public void start() { + this.run(); + } + + @Override + public void stop() { + this.executor.shutdownNow(); + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + private void eventLoop(ServletWebServerApplicationContext context) { + ServerlessMVC mvc = ServerlessMVC.INSTANCE(context); + + Environment environment = context.getEnvironment(); + logger.info("Starting AWSWebRuntimeEventLoop"); + + String runtimeApi = environment.getProperty("AWS_LAMBDA_RUNTIME_API"); + String eventUri = MessageFormat.format(LAMBDA_RUNTIME_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE); + if (logger.isDebugEnabled()) { + logger.debug("Event URI: " + eventUri); + } + + RequestEntity requestEntity = RequestEntity.get(URI.create(eventUri)) + .header("User-Agent", USER_AGENT_VALUE).build(); + RestTemplate rest = new RestTemplate(); + ObjectMapper mapper = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .build(); + AwsProxyHttpServletResponseWriter responseWriter = new AwsProxyHttpServletResponseWriter(); + + logger.info("Entering event loop"); + while (this.isRunning()) { + logger.debug("Attempting to get new event"); + ResponseEntity incomingEvent = rest.exchange(requestEntity, String.class); + + if (incomingEvent != null && incomingEvent.hasBody()) { + if (logger.isDebugEnabled()) { + logger.debug("New Event received from AWS Gateway: " + incomingEvent.getBody()); + } + String requestId = incomingEvent.getHeaders().getFirst("Lambda-Runtime-Aws-Request-Id"); + + try { + logger.debug("Submitting request to the user's web application"); + + HttpServletRequest httpServletRequest = AwsSpringHttpProcessingUtils.generateHttpServletRequest( + incomingEvent.getBody(), null, mvc.getServletContext(), mapper); + httpServletRequest.startAsync(); + AwsProxyResponse awsResponse = AwsSpringHttpProcessingUtils.processRequest( + httpServletRequest, mvc, responseWriter); + if (logger.isDebugEnabled()) { + logger.debug("Received response - body: " + awsResponse.getBody() + + "; status: " + awsResponse.getStatusCode() + "; headers: " + awsResponse.getHeaders()); + } + + String invocationUrl = MessageFormat.format(LAMBDA_INVOCATION_URL_TEMPLATE, runtimeApi, + LAMBDA_VERSION_DATE, requestId); + + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(invocationUrl)) + .header("User-Agent", USER_AGENT_VALUE).body(awsResponse), byte[].class); + if (logger.isDebugEnabled()) { + logger.debug("Response sent: body: " + result.getBody() + + "; status: " + result.getStatusCode() + "; headers: " + result.getHeaders()); + } + if (logger.isInfoEnabled()) { + logger.info("Result POST status: " + result); + } + } + catch (Exception e) { + logger.error(e); + this.propagateAwsError(requestId, e, mapper, runtimeApi, rest); + } + } + } + } + + private void propagateAwsError(String requestId, Exception e, ObjectMapper mapper, String runtimeApi, RestTemplate rest) { + String errorMessage = e.getMessage(); + String errorType = e.getClass().getSimpleName(); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + Map em = new HashMap<>(); + em.put("errorMessage", errorMessage); + em.put("errorType", errorType); + em.put("stackTrace", stackTrace); + try { + byte[] outputBody = mapper.writeValueAsBytes(em); + String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(errorUrl)) + .header("User-Agent", USER_AGENT_VALUE) + .body(outputBody), Object.class); + if (logger.isInfoEnabled()) { + logger.info("Result ERROR status: " + result.getStatusCode()); + } + } + catch (Exception e2) { + throw new IllegalArgumentException("Failed to report error", e2); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java new file mode 100644 index 000000000..9324ebdf0 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.serverless.proxy.spring; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * Initializer to optionally start Custom Runtime to process web workloads. + * Registered with META-INF/spring.factories + * + * @author Dave Syer + * @author Oleg Zhurakousky + */ +public class AwsSpringWebRuntimeInitializer implements ApplicationContextInitializer { + + private static Log logger = LogFactory.getLog(AwsSpringWebRuntimeInitializer.class); + + @Override + public void initialize(GenericApplicationContext context) { + Environment environment = context.getEnvironment(); + + if (context instanceof ServletWebServerApplicationContext && isCustomRuntime(environment)) { + if (context.getBeanFactory().getBeanNamesForType(AwsSpringWebCustomRuntimeEventLoop.class, false, false).length == 0) { + context.registerBean(StringUtils.uncapitalize(AwsSpringWebCustomRuntimeEventLoop.class.getSimpleName()), + SmartLifecycle.class, () -> new AwsSpringWebCustomRuntimeEventLoop((ServletWebServerApplicationContext) context)); + } + } + } + + private boolean isCustomRuntime(Environment environment) { + String handler = environment.getProperty("_HANDLER"); + if (StringUtils.hasText(handler)) { + handler = handler.split(":")[0]; + logger.info("AWS Handler: " + handler); + try { + Thread.currentThread().getContextClassLoader().loadClass(handler); + } + catch (Exception e) { + logger.debug("Will execute Lambda in Custom Runtime"); + return true; + } + } + return false; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java new file mode 100644 index 000000000..127ef6684 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java @@ -0,0 +1,27 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.web.ErrorResponse; + +/** + * This ExceptionHandler implementation enhances the standard AwsProxyExceptionHandler + * by mapping additional details from org.springframework.web.ErrorResponse + * + * As of now this class is identical with SpringAwsProxyExceptionHandler. We may consider + * moving it to a common module to share it in the future. + */ +public class SpringBootAwsProxyExceptionHandler extends AwsProxyExceptionHandler + implements ExceptionHandler { + @Override + public AwsProxyResponse handle(Throwable ex) { + if (ex instanceof ErrorResponse) { + return new AwsProxyResponse(((ErrorResponse) ex).getStatusCode().value(), + HEADERS, getErrorJson(ex.getMessage())); + } else { + return super.handle(ex); + } + } + +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java new file mode 100644 index 000000000..34dae1529 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -0,0 +1,230 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.proxy.spring; + +import java.util.concurrent.CountDownLatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.ResponseWriter; +import com.amazonaws.serverless.proxy.SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; +import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; +import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.embedded.ServerlessReactiveServletEmbeddedServerFactory; +import com.amazonaws.serverless.proxy.spring.embedded.ServerlessServletEmbeddedServerFactory; +import com.amazonaws.services.lambda.runtime.Context; + +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServletRequest; + +/** + * SpringBoot implementation of the `LambdaContainerHandler` abstract class. This class uses the `LambdaSpringApplicationInitializer` + * object behind the scenes to proxy requests. The default implementation leverages the `AwsProxyHttpServletRequest` and + * `AwsHttpServletResponse` implemented in the `aws-serverless-java-container-core` package. + * + * Important: Make sure to add LambdaFlushResponseListener in your SpringBootServletInitializer subclass configure(). + * + * @param The incoming event type + * @param The expected return type + */ +public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { + private static final String DISPATCHER_SERVLET_REGISTRATION_NAME = "dispatcherServlet"; + + private final Class springBootInitializer; + private static final Logger log = LoggerFactory.getLogger(SpringBootLambdaContainerHandler.class); + private String[] springProfiles = null; + private WebApplicationType springWebApplicationType; + private ConfigurableApplicationContext applicationContext; + + private static SpringBootLambdaContainerHandler instance; + + // State vars + private boolean initialized; + + /** + * We need to rely on the static instance of this for SpringBoot because we need it to access the ServletContext. + * Normally, SpringBoot would initialize its own embedded container through the SpringApplication.run() + * method. However, in our case we need to rely on the pre-initialized handler and need to fetch information from it + * for our mock {@link ServerlessReactiveServletEmbeddedServerFactory}. + * + * @return The initialized instance + */ + public static SpringBootLambdaContainerHandler getInstance() { + return instance; + } + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects and the given Spring profiles + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param profiles A list of Spring profiles to activate + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException If an error occurs while initializing the Spring framework + */ + public static SpringBootLambdaContainerHandler getAwsProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `HttpApiV2ProxyRequest` objects and the given Spring profiles + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param profiles A list of Spring profiles to activate + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException If an error occurs while initializing the Spring framework + */ + public static SpringBootLambdaContainerHandler getHttpApiV2ProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + + /** + * Creates a new container handler with the given reader and writer objects + * + * @param requestTypeClass The class for the incoming Lambda event + * @param responseTypeClass The class for the Lambda function output + * @param requestReader An implementation of `RequestReader` + * @param responseWriter An implementation of `ResponseWriter` + * @param securityContextWriter An implementation of `SecurityContextWriter` + * @param exceptionHandler An implementation of `ExceptionHandler` + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param init The initialization Wrapper that will be used to start Spring Boot + * @param applicationType The Spring Web Application Type + */ + public SpringBootLambdaContainerHandler(Class requestTypeClass, + Class responseTypeClass, + RequestReader requestReader, + ResponseWriter responseWriter, + SecurityContextWriter securityContextWriter, + ExceptionHandler exceptionHandler, + Class springBootInitializer, + InitializationWrapper init, + WebApplicationType applicationType) { + super(requestTypeClass, responseTypeClass, requestReader, responseWriter, securityContextWriter, exceptionHandler); + Timer.start("SPRINGBOOT2_CONTAINER_HANDLER_CONSTRUCTOR"); + initialized = false; + this.springBootInitializer = springBootInitializer; + springWebApplicationType = applicationType; + setInitializationWrapper(init); + SpringBootLambdaContainerHandler.setInstance(this); + + Timer.stop("SPRINGBOOT2_CONTAINER_HANDLER_CONSTRUCTOR"); + } + + // this is not pretty. However, because SpringBoot wants to control all of the initialization + // we need to access this handler as a singleton from the EmbeddedContainer to set the servlet + // context and from the ServletConfigurationSupport implementation + private static void setInstance(SpringBootLambdaContainerHandler h) { + SpringBootLambdaContainerHandler.instance = h; + } + + public void activateSpringProfiles(String... profiles) { + springProfiles = profiles; + // force a re-initialization + initialized = false; + } + + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { + return new AwsHttpServletResponse(request, latch); + } + + @Override + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + // this method of the AwsLambdaServletContainerHandler sets the servlet context + Timer.start("SPRINGBOOT2_HANDLE_REQUEST"); + + // wire up the application context on the first invocation + if (!initialized) { + initialize(); + } + + // process filters & invoke servlet + Servlet reqServlet = ((AwsServletContext)getServletContext()).getServletForPath(containerRequest.getPathInfo()); + if (AwsHttpServletRequest.class.isAssignableFrom(containerRequest.getClass())) { + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setResponse(containerResponse); + } + doFilter(containerRequest, containerResponse, reqServlet); + Timer.stop("SPRINGBOOT2_HANDLE_REQUEST"); + } + + + @Override + public void initialize() + throws ContainerInitializationException { + Timer.start("SPRINGBOOT2_COLD_START"); + + SpringApplicationBuilder builder = new SpringApplicationBuilder(getEmbeddedContainerClasses()) + .web(springWebApplicationType); // .REACTIVE, .SERVLET + if (springProfiles != null) { + builder.profiles(springProfiles); + } + applicationContext = builder.run(); + if (springWebApplicationType == WebApplicationType.SERVLET) { + ((AnnotationConfigServletWebServerApplicationContext)applicationContext).setServletContext(getServletContext()); + AwsServletRegistration reg = (AwsServletRegistration)getServletContext().getServletRegistration(DISPATCHER_SERVLET_REGISTRATION_NAME); + if (reg != null) { + reg.setLoadOnStartup(1); + } + } + super.initialize(); + initialized = true; + Timer.stop("SPRINGBOOT2_COLD_START"); + } + + private Class[] getEmbeddedContainerClasses() { + Class[] classes = new Class[2]; + if (springWebApplicationType == WebApplicationType.REACTIVE) { + try { + // if HandlerAdapter is available we assume they are using WebFlux. Otherwise plain servlet. + this.getClass().getClassLoader().loadClass("org.springframework.web.reactive.HandlerAdapter"); + log.debug("Found WebFlux HandlerAdapter on classpath, using reactive server factory"); + classes[0] = ServerlessReactiveServletEmbeddedServerFactory.class; + } catch (ClassNotFoundException e) { + springWebApplicationType = WebApplicationType.SERVLET; + classes[0] = ServerlessServletEmbeddedServerFactory.class; + } + } else { + classes[0] = ServerlessServletEmbeddedServerFactory.class; + } + + classes[1] = springBootInitializer; + return classes; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java new file mode 100644 index 000000000..e7ad017f1 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.internal.servlet.ServletLambdaContainerHandlerBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.boot.WebApplicationType; + +import jakarta.servlet.http.HttpServletRequest; + +public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< + RequestType, + AwsProxyResponse, + HttpServletRequest, + SpringBootLambdaContainerHandler, + SpringBootProxyHandlerBuilder> { + private Class springBootInitializer; + private String[] profiles; + private WebApplicationType applicationType = WebApplicationType.REACTIVE; + + @Override + protected SpringBootProxyHandlerBuilder self() { + return this; + } + + + public SpringBootProxyHandlerBuilder springBootApplication(Class app) { + springBootInitializer = app; + return self(); + } + + public SpringBootProxyHandlerBuilder profiles(String... profiles) { + this.profiles = profiles; + return self(); + } + + public SpringBootProxyHandlerBuilder servletApplication() { + this.applicationType = WebApplicationType.SERVLET; + return self(); + } + + @Override + public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { + validate(); + if (springBootInitializer == null) { + throw new ContainerInitializationException("Missing spring boot application class in builder", null); + } + SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler( + requestTypeClass, + responseTypeClass, + requestReader, + responseWriter, + securityContextWriter, + exceptionHandler, + springBootInitializer, + initializationWrapper, + applicationType + ); + if (profiles != null) { + handler.activateSpringProfiles(profiles); + } + return handler; + } + + @Override + public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { + SpringBootLambdaContainerHandler handler = build(); + initializationWrapper.start(handler); + return handler; + } + + @Override + protected ExceptionHandler defaultExceptionHandler() { + return new SpringBootAwsProxyExceptionHandler(); + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java new file mode 100644 index 000000000..5af476d77 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java @@ -0,0 +1,104 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.AsyncInitializationWrapper; +import com.amazonaws.serverless.proxy.InitializationTypeHelper; +import com.amazonaws.serverless.proxy.internal.InitializableLambdaContainerHandler; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.cloud.function.serverless.web.FunctionClassUtils; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; + +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * An implementation of {@link RequestStreamHandler} which delegates to + * Spring Cloud Function serverless web module managed by Spring team. + * + * It requires no sub-classing from the user other then being identified as "Handler". + * The configuration class(es) should be provided via MAIN_CLASS environment variable. + * + */ +public class SpringDelegatingLambdaContainerHandler implements RequestStreamHandler { + private final SpringDelegatingInitHandler initHandler; + private final ObjectMapper mapper; + private final AwsProxyHttpServletResponseWriter responseWriter; + private final AsyncInitializationWrapper asyncInitWrapper; + + public SpringDelegatingLambdaContainerHandler() throws ContainerInitializationException { + this(new Class[] {FunctionClassUtils.getStartClass()}); + } + + public SpringDelegatingLambdaContainerHandler(final Class... startupClasses) throws ContainerInitializationException { + this.initHandler = new SpringDelegatingInitHandler(startupClasses); + if (InitializationTypeHelper.isAsyncInitializationDisabled()) { + initHandler.initialize(); + this.asyncInitWrapper = null; + } else { + this.asyncInitWrapper = new AsyncInitializationWrapper(); + asyncInitWrapper.start(initHandler); + } + this.mapper = new ObjectMapper(); + this.responseWriter = new AwsProxyHttpServletResponseWriter(); + } + + @Override + public void handleRequest(InputStream input, OutputStream output, Context lambdaContext) throws IOException { + // Wait for async initialization to complete if needed to avoid race condition + if (asyncInitWrapper != null) { + try { + asyncInitWrapper.getInitializationLatch().await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Initialization interrupted", e); + } + } + + ServerlessMVC mvc = initHandler.getMvc(); + HttpServletRequest httpServletRequest = AwsSpringHttpProcessingUtils + .generateHttpServletRequest(input, lambdaContext, mvc.getServletContext(), this.mapper); + httpServletRequest.startAsync(); + AwsProxyResponse awsProxyResponse = AwsSpringHttpProcessingUtils.processRequest(httpServletRequest, mvc, responseWriter); + this.mapper.writeValue(output, awsProxyResponse); + } + + private static final class SpringDelegatingInitHandler implements InitializableLambdaContainerHandler { + private ServerlessMVC mvc; + private final Class[] startupClasses; + + public SpringDelegatingInitHandler(final Class... startupClasses) { + this.startupClasses = startupClasses; + } + + @Override + public void initialize() throws ContainerInitializationException { + this.mvc = ServerlessMVC.INSTANCE(this.startupClasses); + this.mvc.waitForContext(); + } + + public ServerlessMVC getMvc() { + return this.mvc; + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java new file mode 100644 index 000000000..917523a3b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactory; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; + +import jakarta.servlet.*; +import java.io.IOException; +import java.util.Enumeration; + +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +public class ServerlessReactiveServletEmbeddedServerFactory extends AbstractReactiveWebServerFactory implements WebServer, Servlet { + private ServletHttpHandlerAdapter handler; + private ServletConfig config; + static final String SERVLET_NAME = "com.amazonaws.serverless.proxy.spring.embedded.ServerlessReactiveEmbeddedServerFactory"; + static final String SERVLET_INFO = "ServerlessReactiveEmbeddedServerFactory"; + + @Override + @SuppressFBWarnings("MTIA_SUSPECT_SERVLET_INSTANCE_FIELD") + public WebServer getWebServer(HttpHandler httpHandler) { + handler = new ServletHttpHandlerAdapter(httpHandler); + return this; + } + + @Override + public void start() throws WebServerException { + // register this object as the main handler servlet with a mapping of / + SpringBootLambdaContainerHandler + .getInstance() + .getServletContext() + .addServlet(SERVLET_NAME, this) + .addMapping("/"); + handler.init(new ServletAdapterConfig()); + } + + @Override + public void stop() throws WebServerException { + // nothing to do here. + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + config = servletConfig; + } + + @Override + public ServletConfig getServletConfig() { + return config; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { + handler.service(servletRequest, servletResponse); + } + + @Override + public String getServletInfo() { + return SERVLET_INFO; + } + + @Override + public void destroy() { + + } + + private static class ServletAdapterConfig implements ServletConfig { + @Override + public String getServletName() { + return SERVLET_NAME; + } + + @Override + public ServletContext getServletContext() { + return SpringBootLambdaContainerHandler.getInstance().getServletContext(); + } + + @Override + public String getInitParameter(String s) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java new file mode 100644 index 000000000..7278ba444 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.server.servlet.ServletWebServerFactory; +import org.springframework.core.Ordered; + +import jakarta.servlet.ServletException; + +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +public class ServerlessServletEmbeddedServerFactory implements ServletWebServerFactory, WebServer { + @SuppressWarnings("rawtypes") + private AwsLambdaServletContainerHandler handler; + + public ServerlessServletEmbeddedServerFactory() { + super(); + handler = SpringBootLambdaContainerHandler.getInstance(); + } + + @Override + public WebServer getWebServer(ServletContextInitializer... initializers) { + for (ServletContextInitializer i : initializers) { + try { + if (handler.getServletContext() == null) { + throw new WebServerException("Attempting to initialize ServletEmbeddedWebServer without ServletContext in Handler", null); + } + i.onStartup(handler.getServletContext()); + } catch (ServletException e) { + throw new WebServerException("Could not initialize Servlets", e); + } + } + return this; + } + + @Override + public void start() throws WebServerException { + + } + + @Override + public void stop() throws WebServerException { + + } + + @Override + public int getPort() { + return 0; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..cd5c2e70b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +com.amazonaws.serverless.proxy.spring.AwsSpringWebRuntimeInitializer diff --git a/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000..44acc0d83 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=com.amazonaws.serverless.proxy.spring.AwsSpringAotTypesProcessor \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java new file mode 100644 index 000000000..9903e8f8b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java @@ -0,0 +1,39 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; + +public class AWSWebRuntimeTests { + + @AfterEach + public void after() { + System.clearProperty("_HANDLER"); + } + + @Test + public void testWebRuntimeInitialization() throws Exception { + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + assertFalse(context.getBeansOfType(AwsSpringWebCustomRuntimeEventLoop.class).size() > 0); + } + System.setProperty("_HANDLER", "foo"); + AwsSpringWebCustomRuntimeEventLoop loop = null; + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + Thread.sleep(100); + assertTrue(context.getBeansOfType(AwsSpringWebCustomRuntimeEventLoop.class).size() > 0); + loop = context.getBean(AwsSpringWebCustomRuntimeEventLoop.class); + assertTrue(loop.isRunning()); + } + assertFalse(loop.isRunning()); + } + + @EnableAutoConfiguration + private static class EmptyApplication { + + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java new file mode 100644 index 000000000..b0271b7bd --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java @@ -0,0 +1,278 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AlbContext; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.cloud.function.serverless.web.ServerlessServletContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; + +import static org.junit.jupiter.api.Assertions.*; + +public class AwsSpringHttpProcessingUtilsTests { + + private static String API_GATEWAY_EVENT = "{\n" + + " \"version\": \"1.0\",\n" + + " \"resource\": \"$default\",\n" + + " \"path\": \"/async\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Content-Length\": \"45\",\n" + + " \"Content-Type\": \"application/json\",\n" + + " \"Host\": \"i76bfh111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n" + + " \"X-Forwarded-For\": \"109.210.252.44\",\n" + + " \"X-Forwarded-Port\": \"443\",\n" + + " \"X-Forwarded-Proto\": \"https\",\n" + + " \"accept\": \"*/*\"\n" + + " },\n" + + " \"multiValueHeaders\": {\n" + + " \"Content-Length\": [\n" + + " \"45\"\n" + + " ],\n" + + " \"Content-Type\": [\n" + + " \"application/json\"\n" + + " ],\n" + + " \"Host\": [\n" + + " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n" + + " ],\n" + + " \"User-Agent\": [\n" + + " \"curl/7.79.1\"\n" + + " ],\n" + + " \"X-Amzn-Trace-Id\": [\n" + + " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n" + + " ],\n" + + " \"X-Forwarded-For\": [\n" + + " \"109.210.252.44\"\n" + + " ],\n" + + " \"X-Forwarded-Port\": [\n" + + " \"443\"\n" + + " ],\n" + + " \"X-Forwarded-Proto\": [\n" + + " \"https\"\n" + + " ],\n" + + " \"accept\": [\n" + + " \"*/*\"\n" + + " ]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"abc\": \"xyz\",\n" + + " \"parameter1\": \"value2\"\n" + + " },\n" + + " \"multiValueQueryStringParameters\": {\n" + + " \"abc\": [\n" + + " \"xyz\"\n" + + " ],\n" + + " \"parameter1\": [\n" + + " \"value1\",\n" + + " \"value2\"\n" + + " ]\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789098\",\n" + + " \"apiId\": \"i76bfhczs0\",\n" + + " \"domainName\": \"i76bfhc111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"domainPrefix\": \"i76bfhczs0\",\n" + + " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"path\": \"/pets\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n" + + " \"requestTimeEpoch\": 1678276240455,\n" + + " \"resourceId\": \"$default\",\n" + + " \"resourcePath\": \"$default\",\n" + + " \"stage\": \"$default\"\n" + + " },\n" + + " \"pathParameters\": null,\n" + + " \"stageVariables\": null,\n" + + " \"body\": \"{\\\"name\\\":\\\"bob\\\"}\",\n" + + " \"isBase64Encoded\": false\n" + + "}"; + + private static String API_GATEWAY_EVENT_V2 = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/async\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2¶meter2=value\",\n" + + " \"cookies\": [\n" + + " \"cookie1\",\n" + + " \"cookie2\"\n" + + " ],\n" + + " \"headers\": {\n" + + " \"header1\": \"value1\",\n" + + " \"header2\": \"value1,value2\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Forwarded-Port\": \"443\"\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"parameter1\": \"value1,value2\",\n" + + " \"parameter2\": \"value\"\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authentication\": {\n" + + " \"clientCert\": {\n" + + " \"clientCertPem\": \"CERT_CONTENT\",\n" + + " \"subjectDN\": \"www.example.com\",\n" + + " \"issuerDN\": \"Example issuer\",\n" + + " \"serialNumber\": \"a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1\",\n" + + " \"validity\": {\n" + + " \"notBefore\": \"May 28 12:30:02 2019 GMT\",\n" + + " \"notAfter\": \"Aug 5 09:36:04 2021 GMT\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"authorizer\": {\n" + + " \"jwt\": {\n" + + " \"claims\": {\n" + + " \"claim1\": \"value1\",\n" + + " \"claim2\": \"value2\"\n" + + " },\n" + + " \"scopes\": [\n" + + " \"scope1\",\n" + + " \"scope2\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/async\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"pathParameters\": {\n" + + " \"parameter1\": \"value1\"\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"stageVariables\": {\n" + + " \"stageVariable1\": \"value1\",\n" + + " \"stageVariable2\": \"value2\"\n" + + " }\n" + + "}"; + + private static final String ALB_EVENT = "{\n" + + " \"requestContext\": {\n" + + " \"elb\": {\n" + + " \"targetGroupArn\": \"arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09\"\n" + + " }\n" + + " },\n" + + " \"httpMethod\": \"POST\",\n" + + " \"path\": \"/async\",\n" + + " \"multiValueQueryStringParameters\": { \"parameter1\": [\"value1\", \"value2\"], \"parameter2\": [\"1970-01-01T00%3A00%3A00.004Z\"]},\n" + + " \"multiValueHeaders\": {\n" + + " \"accept\": [\"text/html,application/xhtml+xml\"],\n" + + " \"accept-language\": [\"en-US,en;q=0.8\"],\n" + + " \"content-type\": [\"text/plain\"],\n" + + " \"cookie\": [\"cookies\"],\n" + + " \"host\": [\"lambda-846800462-us-east-2.elb.amazonaws.com\"],\n" + + " \"User-Agent\": [\"curl/7.79.1\"],\n" + + " \"x-amzn-trace-id\": [\"Root=1-5bdb40ca-556d8b0c50dc66f0511bf520\"],\n" + + " \"x-forwarded-for\": [\"72.21.198.66\"],\n" + + " \"x-forwarded-port\": [\"443\"],\n" + + " \"x-forwarded-proto\": [\"https\"]\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"body\": \"request_body\"\n" + + "}"; + + private final ObjectMapper mapper = new ObjectMapper(); + + public static Collection data() { + return Arrays.asList(new String[]{API_GATEWAY_EVENT, API_GATEWAY_EVENT_V2, ALB_EVENT}); + } + + @MethodSource("data") + @ParameterizedTest + public void validateHttpServletRequestGenerationWithInputStream(String jsonEvent) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonEvent.getBytes(StandardCharsets.UTF_8)); + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateHttpServletRequest(inputStream, null, servletContext, mapper); + assertRequest(request); + } + + private static void assertRequest(HttpServletRequest request) { + assertEquals("curl/7.79.1", request.getHeader("User-Agent")); + assertEquals("443", request.getHeader("X-Forwarded-Port")); + assertEquals("POST", request.getMethod()); + assertEquals("/async", request.getRequestURI()); + assertNotNull(request.getServletContext()); + // parameter handling for 2.0 requests is currently not spec compliant and to be fixed in future version + // see also GitHub issue https://github.com/aws/serverless-java-container/issues/1278 + if (!(request.getAttribute(RequestReader.HTTP_API_EVENT_PROPERTY) instanceof HttpApiV2ProxyRequest)) { + assertEquals("value1", request.getParameter("parameter1")); + assertArrayEquals(new String[]{"value1", "value2"}, request.getParameterValues("parameter1")); + } + if (request.getAttribute(RequestReader.ALB_CONTEXT_PROPERTY) instanceof AlbContext) { + // query params should be decoded + assertEquals("1970-01-01T00:00:00.004Z", request.getParameter("parameter2")); + } + } + + @MethodSource("data") + @ParameterizedTest + public void validateHttpServletRequestGenerationWithJson(String jsonEvent) { + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateHttpServletRequest(jsonEvent, null, servletContext, mapper); + // spot check some headers + assertRequest(request); + } + + + @EnableAutoConfiguration + @Configuration + @EnableWebSecurity + public static class EmptyApplication { + @RestController + @EnableWebMvc + public static class MyController { + @PostMapping(path = "/async") + public String async(@RequestBody String body) { + return "hello"; + } + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf((csrf) -> csrf.disable()); + return http.build(); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java new file mode 100644 index 000000000..a111e510a --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java @@ -0,0 +1,52 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.jpaapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.jpaapp.MessageController; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JpaAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initJpaAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void asyncRequest(String reqType) { + initJpaAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/async", "POST") + .json() + .body("{\"name\":\"kong\"}"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals("{\"name\":\"KONG\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initJpaAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } + +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java new file mode 100644 index 000000000..d0b579509 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java @@ -0,0 +1,39 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.securityapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.securityapp.SecurityConfig; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SecurityAppTest { + + LambdaHandler handler = new LambdaHandler(); + MockLambdaContext lambdaContext = new MockLambdaContext(); + + public SecurityAppTest() { + System.setProperty("logging.level.root", "DEBUG"); + } + + @Test + void helloRequest_withAuth_respondsWithSingleMessage() { + AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(401, resp.getStatusCode()); + assertTrue(resp.getMultiValueHeaders().containsKey(HttpHeaders.WWW_AUTHENTICATE)); + req = new AwsProxyRequestBuilder("/hello", "GET") + .basicAuth(SecurityConfig.USERNAME, SecurityConfig.PASSWORD) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .build(); + resp = handler.handleRequest(req, lambdaContext); + assertEquals(200, resp.getStatusCode()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java new file mode 100644 index 000000000..3e7cd0cf6 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java @@ -0,0 +1,236 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.ContainerConfig; +import com.amazonaws.serverless.proxy.spring.servletapp.*; +import tools.jackson.core.JacksonException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class ServletAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initServletAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void asyncRequest(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/async", "POST") + .json() + .body("{\"name\":\"bob\"}"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals("{\"name\":\"BOB\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void validateRequest_invalidData_respondsWith400(String reqType) { + initServletAppTest(reqType); + UserData ud = new UserData(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + try { + System.out.println(LambdaContainerHandler.getObjectMapper().writeValueAsString(resp)); + } catch (JacksonException e) { + e.printStackTrace(); + } + assertEquals("3", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + + UserData ud2 = new UserData(); + ud2.setFirstName("Test"); + ud2.setLastName("Test"); + ud2.setEmail("Test"); + req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud2); + resp = handler.handleRequest(req, lambdaContext); + assertEquals("1", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_parsesObject_returnsCorrectMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_propertiesInContentType_returnsCorrectMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .header(HttpHeaders.CONTENT_TYPE, "application/json;v=1") + .header(HttpHeaders.ACCEPT, "application/json;v=1") + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void echoMessage_fileNameLikeParameter_returnsMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/echo/test.test.test", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test.test.test", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void getUtf8String_returnsValidUtf8String(String reqType) { + initServletAppTest(reqType); + // We expect strings to come back as UTF-8 correctly because Spring itself will call the setCharacterEncoding + // method on the response to set it to UTF- + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/content-type/utf8", "GET") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("text/plain; charset=UTF-8", resp.getMultiValueHeaders().get(HttpHeaders.CONTENT_TYPE).stream().collect(Collectors.joining(","))); + assertEquals(MessageController.UTF8_RESPONSE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void getUtf8Json_returnsValidUtf8String(String reqType) { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/content-type/jsonutf8", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("{\"s\":\"" + MessageController.UTF8_RESPONSE + "\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void stream_getUtf8String_returnsValidUtf8String(String reqType) throws IOException { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + LambdaStreamHandler streamHandler = new LambdaStreamHandler(type); + AwsProxyRequestBuilder reqBuilder = new AwsProxyRequestBuilder("/content-type/utf8", "GET") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); + InputStream req = null; + switch (type) { + case "ALB": + req = reqBuilder.alb().buildStream(); + break; + case "API_GW": + req = reqBuilder.buildStream(); + break; + case "HTTP_API": + req = reqBuilder.toHttpApiV2RequestStream(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamHandler.handleRequest(req, out, lambdaContext); + AwsProxyResponse resp = LambdaContainerHandler.getObjectMapper().readValue(out.toByteArray(), AwsProxyResponse.class); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals(MessageController.UTF8_RESPONSE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void stream_getUtf8Json_returnsValidUtf8String(String reqType) throws IOException { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + LambdaStreamHandler streamHandler = new LambdaStreamHandler(type); + AwsProxyRequestBuilder reqBuilder = new AwsProxyRequestBuilder("/content-type/jsonutf8", "GET"); + InputStream req = null; + switch (type) { + case "ALB": + req = reqBuilder.alb().buildStream(); + break; + case "API_GW": + req = reqBuilder.buildStream(); + break; + case "HTTP_API": + req = reqBuilder.toHttpApiV2RequestStream(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamHandler.handleRequest(req, out, lambdaContext); + AwsProxyResponse resp = LambdaContainerHandler.getObjectMapper().readValue(out.toByteArray(), AwsProxyResponse.class); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("{\"s\":\"" + MessageController.UTF8_RESPONSE + "\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void springExceptionMapping_throw404Ex_expectMappedTo404(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/ex/customstatus", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(404, resp.getStatusCode()); + } + + @MethodSource("data") + @ParameterizedTest + void echoMessage_populatesSingleValueHeadersForHttpApiV2(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .header(HttpHeaders.CONTENT_TYPE, "application/json;v=1") + .header(HttpHeaders.ACCEPT, "application/json;v=1") + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + if ("HTTP_API".equals(type)) { + assertNotNull(resp.getHeaders()); + } else { + assertNull(resp.getHeaders()); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java new file mode 100644 index 000000000..f5e83e85e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java @@ -0,0 +1,32 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.slowapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.slowapp.MessageController; +import com.amazonaws.serverless.proxy.spring.slowapp.SlowTestApplication; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SlowAppTest { + + @Test + void slowAppInit_continuesInBackgroundThread_returnsCorrect() { + LambdaHandler slowApp = new LambdaHandler(); + System.out.println("Start time: " + slowApp.getConstructorTime()); + assertTrue(slowApp.getConstructorTime() < 10_000); + AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + long startRequestTime = Instant.now().toEpochMilli(); + AwsProxyResponse resp = slowApp.handleRequest(req, new MockLambdaContext()); + long endRequestTime = Instant.now().toEpochMilli(); + assertTrue(endRequestTime - startRequestTime > SlowTestApplication.SlowDownInit.INIT_SLEEP_TIME_MS - 10_000); + assertEquals(200, resp.getStatusCode()); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java new file mode 100644 index 000000000..e98b19871 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java @@ -0,0 +1,335 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.util.CollectionUtils; + +import com.amazonaws.serverless.proxy.spring.servletapp.MessageData; +import com.amazonaws.serverless.proxy.spring.servletapp.ServletApplication; +import com.amazonaws.serverless.proxy.spring.servletapp.UserData; +import tools.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.core.HttpHeaders; + +@SuppressWarnings("rawtypes") +public class SpringDelegatingLambdaContainerHandlerTests { + + private static String API_GATEWAY_EVENT = "{\n" + + " \"version\": \"1.0\",\n" + + " \"resource\": \"$default\",\n" + + " \"path\": \"/async\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Content-Length\": \"45\",\n" + + " \"Content-Type\": \"application/json\",\n" + + " \"Host\": \"i76bfh111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n" + + " \"X-Forwarded-For\": \"109.210.252.44\",\n" + + " \"X-Forwarded-Port\": \"443\",\n" + + " \"X-Forwarded-Proto\": \"https\",\n" + + " \"accept\": \"*/*\"\n" + + " },\n" + + " \"multiValueHeaders\": {\n" + + " \"Content-Length\": [\n" + + " \"45\"\n" + + " ],\n" + + " \"Content-Type\": [\n" + + " \"application/json\"\n" + + " ],\n" + + " \"Host\": [\n" + + " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n" + + " ],\n" + + " \"User-Agent\": [\n" + + " \"curl/7.79.1\"\n" + + " ],\n" + + " \"X-Amzn-Trace-Id\": [\n" + + " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n" + + " ],\n" + + " \"X-Forwarded-For\": [\n" + + " \"109.210.252.44\"\n" + + " ],\n" + + " \"X-Forwarded-Port\": [\n" + + " \"443\"\n" + + " ],\n" + + " \"X-Forwarded-Proto\": [\n" + + " \"https\"\n" + + " ],\n" + + " \"accept\": [\n" + + " \"*/*\"\n" + + " ]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"abc\": \"xyz\",\n" + + " \"name\": \"Ricky\",\n" + + " \"foo\": \"baz\"\n" + + " },\n" + + " \"multiValueQueryStringParameters\": {\n" + + " \"abc\": [\n" + + " \"xyz\"\n" + + " ],\n" + + " \"name\": [\n" + + " \"Ricky\"\n" + + " ],\n" + + " \"foo\": [\n" + + " \"bar\",\n" + + " \"baz\"\n" + + " ]\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789098\",\n" + + " \"apiId\": \"i76bfhczs0\",\n" + + " \"domainName\": \"i76bfhc111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"domainPrefix\": \"i76bfhczs0\",\n" + + " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"identity\": {\n" + + " \"accessKey\": null,\n" + + " \"accountId\": null,\n" + + " \"caller\": null,\n" + + " \"cognitoAmr\": null,\n" + + " \"cognitoAuthenticationProvider\": null,\n" + + " \"cognitoAuthenticationType\": null,\n" + + " \"cognitoIdentityId\": null,\n" + + " \"cognitoIdentityPoolId\": null,\n" + + " \"principalOrgId\": null,\n" + + " \"sourceIp\": \"109.210.252.44\",\n" + + " \"user\": null,\n" + + " \"userAgent\": \"curl/7.79.1\",\n" + + " \"userArn\": null\n" + + " },\n" + + " \"path\": \"/pets\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n" + + " \"requestTimeEpoch\": 1678276240455,\n" + + " \"resourceId\": \"$default\",\n" + + " \"resourcePath\": \"$default\",\n" + + " \"stage\": \"$default\"\n" + + " },\n" + + " \"pathParameters\": null,\n" + + " \"stageVariables\": null,\n" + + " \"body\": \"{\\\"name\\\":\\\"bob\\\"}\",\n" + + " \"isBase64Encoded\": false\n" + + "}"; + + private static String API_GATEWAY_EVENT_V2 = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/my/path\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2&name=Ricky¶meter2=value\",\n" + + " \"cookies\": [\n" + + " \"cookie1\",\n" + + " \"cookie2\"\n" + + " ],\n" + + " \"headers\": {\n" + + " \"header1\": \"value1\",\n" + + " \"header2\": \"value1,value2\"\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"parameter1\": \"value1,value2\",\n" + + " \"name\": \"Ricky\",\n" + + " \"parameter2\": \"value\"\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authentication\": {\n" + + " \"clientCert\": {\n" + + " \"clientCertPem\": \"CERT_CONTENT\",\n" + + " \"subjectDN\": \"www.example.com\",\n" + + " \"issuerDN\": \"Example issuer\",\n" + + " \"serialNumber\": \"a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1\",\n" + + " \"validity\": {\n" + + " \"notBefore\": \"May 28 12:30:02 2019 GMT\",\n" + + " \"notAfter\": \"Aug 5 09:36:04 2021 GMT\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"authorizer\": {\n" + + " \"jwt\": {\n" + + " \"claims\": {\n" + + " \"claim1\": \"value1\",\n" + + " \"claim2\": \"value2\"\n" + + " },\n" + + " \"scopes\": [\n" + + " \"scope1\",\n" + + " \"scope2\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/my/path\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"pathParameters\": {\n" + + " \"parameter1\": \"value1\"\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"stageVariables\": {\n" + + " \"stageVariable1\": \"value1\",\n" + + " \"stageVariable2\": \"value2\"\n" + + " }\n" + + "}"; + + private SpringDelegatingLambdaContainerHandler handler; + + private ObjectMapper mapper = new ObjectMapper(); + + public void initServletAppTest() throws ContainerInitializationException { + this.handler = new SpringDelegatingLambdaContainerHandler(ServletApplication.class); + } + + public static Collection data() { + return Arrays.asList(new String[]{API_GATEWAY_EVENT, API_GATEWAY_EVENT_V2}); + } + + @MethodSource("data") + @ParameterizedTest + public void validateComplesrequest(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", + "/foo/male/list/24", "{\"name\":\"bob\"}", false,null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + String[] responseBody = ((String) result.get("body")).split("/"); + assertEquals("male", responseBody[0]); + assertEquals("24", responseBody[1]); + assertEquals("Ricky", responseBody[2]); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate400(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", mapper.writeValueAsString(ud),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(400, result.get("statusCode")); + assertEquals("3", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate200(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + ud.setFirstName("bob"); + ud.setLastName("smith"); + ud.setEmail("foo@bar.com"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", mapper.writeValueAsString(ud),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("VALID", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate200Base64(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + ud.setFirstName("bob"); + ud.setLastName("smith"); + ud.setEmail("foo@bar.com"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", + Base64.getMimeEncoder().encodeToString(mapper.writeValueAsString(ud).getBytes()),true, null)); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("VALID", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void messageObject_parsesObject_returnsCorrectMessage(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/message", + mapper.writeValueAsString(new MessageData("test message")),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("test message", result.get("body")); + } + + @SuppressWarnings({"unchecked" }) + @MethodSource("data") + @ParameterizedTest + void messageObject_propertiesInContentType_returnsCorrectMessage(String jsonEvent) throws Exception { + initServletAppTest(); + + Map headers = new HashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, "application/json;v=1"); + headers.put(HttpHeaders.ACCEPT, "application/json;v=1"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/message", + mapper.writeValueAsString(new MessageData("test message")),false, headers)); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals("test message", result.get("body")); + } + + private byte[] generateHttpRequest(String jsonEvent, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + Map requestMap = mapper.readValue(jsonEvent, Map.class); + if (requestMap.get("version").equals("2.0")) { + return generateHttpRequest2(requestMap, method, path, body, isBase64Encoded,headers); + } + return generateHttpRequest(requestMap, method, path, body,isBase64Encoded, headers); + } + + @SuppressWarnings({ "unchecked"}) + private byte[] generateHttpRequest(Map requestMap, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + requestMap.put("path", path); + requestMap.put("httpMethod", method); + requestMap.put("body", body); + requestMap.put("isBase64Encoded", isBase64Encoded); + if (!CollectionUtils.isEmpty(headers)) { + requestMap.put("headers", headers); + } + return mapper.writeValueAsBytes(requestMap); + } + + @SuppressWarnings({ "unchecked"}) + private byte[] generateHttpRequest2(Map requestMap, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + Map map = mapper.readValue(API_GATEWAY_EVENT_V2, Map.class); + Map http = (Map) ((Map) map.get("requestContext")).get("http"); + http.put("path", path); + http.put("method", method); + map.put("body", body); + map.put("isBase64Encoded", isBase64Encoded); + if (!CollectionUtils.isEmpty(headers)) { + map.put("headers", headers); + } + return mapper.writeValueAsBytes(map); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java new file mode 100644 index 000000000..cc7d3365d --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -0,0 +1,68 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.webfluxapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageController; +import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageData; +import tools.jackson.core.JacksonException; + +public class WebFluxAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initWebFluxAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/single", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + System.out.println(resp.getBody()); + assertEquals(MessageController.MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloDoubleRequest_respondsWithDoubleMessage(String reqType) { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/double", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + + assertEquals(MessageController.MESSAGE + MessageController.MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_parsesObject_returnsCorrectMessage(String reqType) throws JacksonException { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java new file mode 100644 index 000000000..5ffd4a311 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java @@ -0,0 +1,49 @@ +package com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; +import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.web.servlet.ServletContextInitializer; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import static org.junit.jupiter.api.Assertions.fail; + +public class ServerlessServletEmbeddedServerFactoryTest { + private SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler<>( + AwsProxyRequest.class, + AwsProxyResponse.class, + new AwsProxyHttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), + new AwsProxyExceptionHandler(), + null, + new InitializationWrapper(), + WebApplicationType.REACTIVE + ); + + public ServerlessServletEmbeddedServerFactoryTest() throws ContainerInitializationException { + } + + @Test + void getWebServer_callsInitializers() { + ServerlessServletEmbeddedServerFactory factory = new ServerlessServletEmbeddedServerFactory(); + factory.getWebServer(new ServletContextInitializer() { + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (servletContext == null) { + fail("Null servlet context"); + } + } + }); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java new file mode 100644 index 000000000..aeef7c65e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java @@ -0,0 +1,23 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +public class DatabaseConfig { + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:testdb"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + + return dataSource; + } +} + diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java new file mode 100644 index 000000000..bc80c95a5 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java @@ -0,0 +1,12 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@Import(MessageController.class) +public class JpaApplication {} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java new file mode 100644 index 000000000..0cf67c10f --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java @@ -0,0 +1,59 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(JpaApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(JpaApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java new file mode 100644 index 000000000..a85292262 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java @@ -0,0 +1,31 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.async.DeferredResult; +import java.util.Collections; +import java.util.Map; + +@RestController +public class MessageController { + + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public String hello() { + return HELLO_MESSAGE; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RequestMapping(path = "/async", method = RequestMethod.POST) + @ResponseBody + public DeferredResult> asyncResult(@RequestBody Map value) { + DeferredResult result = new DeferredResult<>(); + result.setResult(Collections.singletonMap("name", value.get("name").toUpperCase())); + return result; + } + +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java new file mode 100644 index 000000000..ae8ba21ac --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java @@ -0,0 +1,25 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(SecurityApplication.class); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java new file mode 100644 index 000000000..ad67d4766 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public Mono hello() { + return Mono.just(HELLO_MESSAGE); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java new file mode 100644 index 000000000..cafcd4000 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java @@ -0,0 +1,14 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.web.reactive.config.EnableWebFlux; + +@SpringBootApplication +@EnableWebFluxSecurity +@EnableWebFlux +@Import(SecurityConfig.class) +public class SecurityApplication { +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java new file mode 100644 index 000000000..497d20845 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java @@ -0,0 +1,45 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig +{ + public static final String USERNAME = "admin"; + public static final String PASSWORD = "{noop}password"; + private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Bean + public SecurityWebFilterChain securitygWebFilterChain( + ServerHttpSecurity http) { + return http.authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated()) + .csrf(csrf -> csrf.disable()) + .httpBasic(httpBasic -> {}) + .build(); + } + + @Bean + public static BCryptPasswordEncoder passwordEncoder() { + return passwordEncoder; + } + + @Bean + public MapReactiveUserDetailsService userDetailsService() { + UserDetails user = User + .withUsername(USERNAME) + .password(passwordEncoder.encode(PASSWORD)) + .roles("USER") + .build(); + return new MapReactiveUserDetailsService(user); + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java new file mode 100644 index 000000000..88441988e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java @@ -0,0 +1,60 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java new file mode 100644 index 000000000..fd7d71d79 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java @@ -0,0 +1,63 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class LambdaStreamHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaStreamHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + switch (type) { + case "API_GW": + case "ALB": + handler.proxyStream(inputStream, outputStream, context); + break; + case "HTTP_API": + httpApiHandler.proxyStream(inputStream, outputStream, context); + } + + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java new file mode 100644 index 000000000..1923396c6 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java @@ -0,0 +1,75 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.server.ResponseStatusException; + +import jakarta.validation.Valid; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + public static final String VALID_MESSAGE = "VALID"; + public static final String UTF8_RESPONSE = "öüäß фрыцшщ"; + public static final String EX_MESSAGE = "404 exception message"; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RequestMapping(path = "/async", method = RequestMethod.POST) + @ResponseBody + public DeferredResult> asyncResult(@RequestBody Map value) { + DeferredResult result = new DeferredResult<>(); + result.setResult(Collections.singletonMap("name", value.get("name").toUpperCase())); + return result; + } + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public String hello() { + return HELLO_MESSAGE; + } + + @RequestMapping(path="/validate", method=RequestMethod.POST, produces = {"text/plain"}) + public ResponseEntity validateBody(@RequestBody @Valid UserData userData, Errors errors) { + if (errors != null && errors.hasErrors()) { + return ResponseEntity.badRequest().body(errors.getErrorCount() + ""); + } + return ResponseEntity.ok(VALID_MESSAGE); + } + + @RequestMapping(path="/message", method = RequestMethod.POST) + public String returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return data.getMessage(); + } + + @RequestMapping(path="/echo/{message}", method=RequestMethod.GET) + public String returnPathMessage(@PathVariable(value="message") String message) { + return message; + } + + @GetMapping(value = "/content-type/utf8", produces = "text/plain") + public ResponseEntity getUtf8String() { + return ResponseEntity.ok(UTF8_RESPONSE); + } + + @GetMapping(value = "/content-type/jsonutf8", produces=MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUtf8Json() { + Map resp = new HashMap(); + resp.put("s", UTF8_RESPONSE); + return ResponseEntity.ok(resp); + } + + @GetMapping(value = "/ex/customstatus") + public String throw404Exception() { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, EX_MESSAGE); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java new file mode 100644 index 000000000..129101cbe --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java new file mode 100644 index 000000000..b2dc1a315 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java @@ -0,0 +1,25 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@Import(MessageController.class) +@RestController +public class ServletApplication { + + @RequestMapping(path = "/foo/{gender}/list/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest( + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + return gender + "/" + age + "/" + name; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java new file mode 100644 index 000000000..379291a39 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java @@ -0,0 +1,50 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class UserData { + @NotBlank + private String firstName; + @NotBlank + private String lastName; + @NotNull @Email + private String email; + private String error; + + public UserData() { + + } + + public UserData(String err) { + error = err; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getError() { return error; } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java new file mode 100644 index 000000000..22f75e7a9 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java @@ -0,0 +1,40 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import java.time.Instant; + +public class LambdaHandler implements RequestHandler { + private SpringBootLambdaContainerHandler handler; + private long constructorTime; + + public LambdaHandler() { + try { + long startTime = Instant.now().toEpochMilli(); + System.out.println("startCall: " + startTime); + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .springBootApplication(SlowTestApplication.class) + .buildAndInitialize(); + constructorTime = Instant.now().toEpochMilli() - startTime; + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + public long getConstructorTime() { + return constructorTime; + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java new file mode 100644 index 000000000..098e8e7df --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java @@ -0,0 +1,15 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method= RequestMethod.GET) + public String hello() { + return HELLO_MESSAGE; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java new file mode 100644 index 000000000..adb4bcb3a --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java @@ -0,0 +1,21 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.stereotype.Component; + +import java.time.Instant; + +@SpringBootApplication +public class SlowTestApplication { + + @Component + public static class SlowDownInit implements InitializingBean { + public static final int INIT_SLEEP_TIME_MS = 13_000; + + @Override + public void afterPropertiesSet() throws Exception { + Thread.sleep(INIT_SLEEP_TIME_MS); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java new file mode 100644 index 000000000..0eb52a7bc --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java @@ -0,0 +1,58 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java new file mode 100644 index 000000000..61e55a137 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java @@ -0,0 +1,38 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import reactor.core.publisher.Flux; + +@EnableAutoConfiguration +@RestController +public class MessageController { + public static final String MESSAGE = "Hello"; + + @RequestMapping(path="/single", method= RequestMethod.GET, produces = {"text/plain"}) + Flux singleMessage(){ + return Flux.just( + MESSAGE + ); + } + + @RequestMapping(path="/double", method= RequestMethod.GET, produces={"text/plain"}) + Flux doubleMessage(){ + return Flux.just( + MESSAGE, + MESSAGE + ); + } + + @RequestMapping(path="/message", method = RequestMethod.POST, produces={"text/plain"}, consumes = {"application/json"}) + public Flux returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return Flux.just(data.getMessage()); + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java new file mode 100644 index 000000000..2be6b4f2d --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java new file mode 100644 index 000000000..891abd18c --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java @@ -0,0 +1,14 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@SpringBootApplication +public class WebFluxTestApplication { + +} diff --git a/aws-serverless-jersey-archetype/pom.xml b/aws-serverless-jersey-archetype/pom.xml index 4afdc9996..8c1d68edc 100644 --- a/aws-serverless-jersey-archetype/pom.xml +++ b/aws-serverless-jersey-archetype/pom.xml @@ -4,12 +4,12 @@ com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless.archetypes aws-serverless-jersey-archetype - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT maven-archetype diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml index 1f05e9018..da0c05405 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ 1.8 3.1.10 - 2.18.3 + 3.0.2 5.12.1 @@ -24,12 +24,12 @@ com.amazonaws.serverless aws-serverless-java-container-jersey - ${project.version} + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - ${project.version} + 3.0.0-SNAPSHOT tests test-jar test @@ -51,18 +51,18 @@ jackson-annotations - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core - com.fasterxml.jackson.core + tools.jackson.core jackson-databind \${jackson.version} diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java index 768a5ad98..a4e2251e4 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java @@ -10,6 +10,7 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; +import tools.jackson.core.JacksonException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java index 53db9b161..a247daed7 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -16,6 +16,7 @@ import jakarta.ws.rs.core.Response; import java.io.ByteArrayOutputStream; +import tools.jackson.core.JacksonException; import java.io.IOException; import java.io.InputStream; @@ -81,7 +82,7 @@ private void handle(InputStream is, ByteArrayOutputStream os) { private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { try { return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Error while parsing response: " + e.getMessage()); } diff --git a/aws-serverless-spring-archetype/pom.xml b/aws-serverless-spring-archetype/pom.xml index 2a2f59b8b..fc00bb2ce 100644 --- a/aws-serverless-spring-archetype/pom.xml +++ b/aws-serverless-spring-archetype/pom.xml @@ -4,12 +4,12 @@ com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless.archetypes aws-serverless-spring-archetype - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT maven-archetype diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml index 48b70f27f..4dd847a38 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,8 +16,8 @@ 1.8 1.8 - 6.2.6 - 5.12.1 + 7.0.0 + 6.0.0 2.24.2 @@ -25,12 +25,12 @@ com.amazonaws.serverless aws-serverless-java-container-spring - ${project.version} + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - ${project.version} + 3.0.0-SNAPSHOT tests test-jar test diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java index 53db9b161..cfb809f88 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -81,7 +81,7 @@ private void handle(InputStream is, ByteArrayOutputStream os) { private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { try { return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); - } catch (IOException e) { + } catch (tools.jackson.core.JacksonException e) { e.printStackTrace(); fail("Error while parsing response: " + e.getMessage()); } diff --git a/aws-serverless-springboot3-archetype/pom.xml b/aws-serverless-springboot3-archetype/pom.xml index 74fac6156..f4c56c05b 100644 --- a/aws-serverless-springboot3-archetype/pom.xml +++ b/aws-serverless-springboot3-archetype/pom.xml @@ -4,12 +4,12 @@ com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 2.1.3 com.amazonaws.serverless.archetypes aws-serverless-springboot3-archetype - 2.1.5-SNAPSHOT + 2.1.3 maven-archetype diff --git a/aws-serverless-springboot4-archetype/pom.xml b/aws-serverless-springboot4-archetype/pom.xml new file mode 100644 index 000000000..82e1ad033 --- /dev/null +++ b/aws-serverless-springboot4-archetype/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + + + com.amazonaws.serverless + aws-serverless-java-container + 3.0.0-SNAPSHOT + + + com.amazonaws.serverless.archetypes + aws-serverless-springboot4-archetype + 3.0.0-SNAPSHOT + maven-archetype + + + https://github.com/aws/serverless-java-container.git + HEAD + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + src/main/resources + true + + archetype-resources/pom.xml + archetype-resources/README.md + + + + src/main/resources + false + + archetype-resources/pom.xml + + + + + + + org.apache.maven.archetype + archetype-packaging + 3.4.0 + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + \ + + + + org.apache.maven.plugins + maven-archetype-plugin + 3.4.0 + + + + integration-test + + + + + + + + diff --git a/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 000000000..5379692ba --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -0,0 +1,39 @@ + + + + src/main/java + + **/*.java + + + + src/main/resources + + **/*.properties + + + + src/test/java + + **/*.java + + + + src/assembly + + * + + + + + + template.yml + README.md + build.gradle + + + + \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md new file mode 100644 index 000000000..311c40aee --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md @@ -0,0 +1,99 @@ +#set($resourceName = $artifactId) +#macro(replaceChar $originalName, $char) + #if($originalName.contains($char)) + #set($tokens = $originalName.split($char)) + #set($newResourceName = "") + #foreach($token in $tokens) + #set($newResourceName = $newResourceName + $token.substring(0,1).toUpperCase() + $token.substring(1).toLowerCase()) + #end + ${newResourceName} + #else + #set($newResourceName = $originalName.substring(0,1).toUpperCase() + $originalName.substring(1)) + ${newResourceName} + #end +#end +#set($resourceName = "#replaceChar($resourceName, '-')") +#set($resourceName = "#replaceChar($resourceName, '.')") +#set($resourceName = $resourceName.replaceAll("\n", "").trim()) +# \${artifactId} serverless API +The \${artifactId} project, created with [`aws-serverless-java-container`](https://github.com/aws/serverless-java-container). + +The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. + +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). + +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project +```bash +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ cd \${artifactId} +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts + +Build Succeeded + +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml + +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided +``` + +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. + +```bash +$ sam local start-api + +... +Mounting ${groupId}.StreamLambdaHandler::handleRequest (java11) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] +... +``` + +Using a new shell, you can send a test ping request to your API: + +```bash +$ curl -s http://127.0.0.1:3000/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` + +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- +``` + +Copy the `OutputValue` into a browser or use curl to test your first request: + +```bash +$ curl -s https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle new file mode 100644 index 000000000..3aa54825c --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + 'org.springframework.boot:spring-boot-starter-web:3.4.5', + 'com.amazonaws.serverless:aws-serverless-java-container-springboot3:[2.0-SNAPSHOT,)', + ) + + testImplementation("com.amazonaws.serverless:aws-serverless-java-container-core:[2.0-SNAPSHOT,):tests") + testImplementation("org.apache.httpcomponents.client5:httpclient5:5.5") + testImplementation(platform("org.junit:junit-bom:5.13.1")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +test { + useJUnitPlatform() +} + +build.dependsOn buildZip diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 000000000..c122a405a --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,180 @@ +#set($dollar = '$') + + + 4.0.0 + + \${groupId} + \${artifactId} + \${version} + jar + + Serverless Spring Boot 4 API + https://github.com/aws/serverless-java-container + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + 17 + 5.12.1 + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + ${project.version} + + + com.amazonaws.serverless + aws-serverless-java-container-core + ${project.version} + tests + test-jar + test + + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.3 + test + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.junit + junit-bom + ${junit.version} + import + pom + + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.2 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${dollar}{project.build.directory}${dollar}{file.separator}lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${dollar}{project.artifactId}-${dollar}{project.version} + + src${dollar}{file.separator}assembly${dollar}{file.separator}bin.xml + + false + + + + + + + + + diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java new file mode 100644 index 000000000..1b74086f7 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java @@ -0,0 +1,24 @@ +#macro(loggingOff) + logging.level.root:OFF +#end +#set($logging = "#loggingOff()") +#set($logging = $logging.replaceAll("\n", "").trim()) +package ${groupId}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +import ${groupId}.controller.PingController; + + +@SpringBootApplication +// We use direct @Import instead of @ComponentScan to speed up cold starts +// @ComponentScan(basePackages = "${groupId}.controller") +@Import({ PingController.class }) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java new file mode 100644 index 000000000..e022540c1 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java @@ -0,0 +1,33 @@ +package ${groupId}; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java new file mode 100644 index 000000000..94f517f07 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java @@ -0,0 +1,20 @@ +package ${groupId}.controller; + + +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.HashMap; +import java.util.Map; + + +@RestController +@EnableWebMvc +public class PingController { + @RequestMapping(path = "/ping", method = RequestMethod.GET) + public Map ping() { + Map pong = new HashMap<>(); + pong.put("pong", "Hello, World!"); + return pong; + } +} diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties new file mode 100644 index 000000000..070e632fe --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Reduce logging level to make sure the application works with SAM local +# https://github.com/aws/serverless-java-container/issues/134 +logging.level.root=WARN \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java new file mode 100644 index 000000000..4bceb78f1 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -0,0 +1,88 @@ +package ${groupId}; + + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class StreamLambdaHandlerTest { + + private static StreamLambdaHandler handler; + private static Context lambdaContext; + + @BeforeAll + public static void setUp() { + handler = new StreamLambdaHandler(); + lambdaContext = new MockLambdaContext(); + } + + @Test + public void ping_streamRequest_respondsWithHello() { + InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); + + assertFalse(response.isBase64Encoded()); + + assertTrue(response.getBody().contains("pong")); + assertTrue(response.getBody().contains("Hello, World!")); + + assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); + assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)); + } + + @Test + public void invalidResource_streamRequest_responds404() { + InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode()); + } + + private void handle(InputStream is, ByteArrayOutputStream os) { + try { + handler.handleRequest(is, os, lambdaContext); + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { + try { + return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); + } catch (Exception e) { + e.printStackTrace(); + fail("Error while parsing response: " + e.getMessage()); + } + return null; + } +} diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml new file mode 100644 index 000000000..18c231878 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml @@ -0,0 +1,52 @@ +#set($resourceName = $artifactId) +#macro(replaceChar $originalName, $char) + #if($originalName.contains($char)) + #set($tokens = $originalName.split($char)) + #set($newResourceName = "") + #foreach($token in $tokens) + #set($newResourceName = $newResourceName + $token.substring(0,1).toUpperCase() + $token.substring(1).toLowerCase()) + #end + ${newResourceName} + #else + #set($newResourceName = $originalName.substring(0,1).toUpperCase() + $originalName.substring(1)) + ${newResourceName} + #end +#end +#set($resourceName = "#replaceChar($resourceName, '-')") +#set($resourceName = "#replaceChar($resourceName, '.')") +#set($resourceName = $resourceName.replaceAll("\n", "").trim()) +#macro(regionVar) + AWS::Region +#end +#set($awsRegion = "#regionVar()") +#set($awsRegion = $awsRegion.replaceAll("\n", "").trim()) +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS Serverless Spring Boot 2 API - ${groupId}::${artifactId} +Globals: + Api: + EndpointConfiguration: REGIONAL + +Resources: + ${resourceName}Function: + Type: AWS::Serverless::Function + Properties: + Handler: ${groupId}.StreamLambdaHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Events: + ProxyResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + ${resourceName}Api: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${${awsRegion}}.amazonaws.com/Prod/ping' + Export: + Name: ${resourceName}Api diff --git a/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties new file mode 100644 index 000000000..7df3bf6e1 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties @@ -0,0 +1,3 @@ +groupId=test.service +artifactId=springboot-archetype-test +version=1.0-SNAPSHOT diff --git a/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt new file mode 100644 index 000000000..597acc768 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt @@ -0,0 +1 @@ +package \ No newline at end of file diff --git a/pom.xml b/pom.xml index ebc634fbb..30bf82e93 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.amazonaws.serverless aws-serverless-java-container pom - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT AWS Serverless Java container A Java framework to run Spring, Spring Boot, Jersey, Spark, and Struts applications inside AWS Lambda https://github.com/aws/serverless-java-container @@ -29,9 +29,11 @@ aws-serverless-java-container-jersey aws-serverless-java-container-spring aws-serverless-java-container-springboot3 + aws-serverless-java-container-springboot4 aws-serverless-jersey-archetype aws-serverless-spring-archetype aws-serverless-springboot3-archetype + aws-serverless-springboot4-archetype @@ -78,9 +80,9 @@ 0.7 12.1.1 - 2.19.1 + 2.0.17 - 5.12.2 + 6.0.0 5.19.0 1.3 UTF-8 @@ -265,7 +267,7 @@ true ${project.build.directory}/spotbugs - ${project.parent.basedir}/spotbugs-excludeFilter.xml + ${project.basedir}/../spotbugs-excludeFilter.xml com.h3xstream.findsecbugs diff --git a/samples/springboot3/pet-store-native/Dockerfile b/samples/springboot3/pet-store-native/Dockerfile index 4fb614e3f..7d5833bfe 100644 --- a/samples/springboot3/pet-store-native/Dockerfile +++ b/samples/springboot3/pet-store-native/Dockerfile @@ -8,7 +8,7 @@ RUN yum -y update \ # Graal VM ENV GRAAL_VERSION 21.0.2 -ENV ARCHITECTURE x64 +ENV ARCHITECTURE aarch64 ENV GRAAL_FILENAME graalvm-community-jdk-${GRAAL_VERSION}_linux-${ARCHITECTURE}_bin.tar.gz RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAAL_VERSION}/${GRAAL_FILENAME} | tar -xvz RUN mv graalvm-community-openjdk-${GRAAL_VERSION}* /usr/lib/graalvm diff --git a/samples/springboot4/alt-pet-store/README.md b/samples/springboot4/alt-pet-store/README.md new file mode 100644 index 000000000..56cfca327 --- /dev/null +++ b/samples/springboot4/alt-pet-store/README.md @@ -0,0 +1,56 @@ +# Serverless Spring Boot 4 example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. Unlike older examples, this example is relying on the new +`SpringDelegatingLambdaContainerHandler`, which you simply need to identify as a _handler_ of the Lambda function. The main configuration class identified as `MAIN_CLASS` +environment variable or `Start-Class` or `Main-Class` entry in Manifest file. See provided `template.yml` file for reference. + + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` + +You can also try a complex request passing both path and request parameters to complex endpoint such as this: + + +``` +@RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) +public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name +) +``` +For example. + +``` +curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -X POST https://zuhd709386.execute-api.us-east-2.amazonaws.com/foo/male/bar/25?name=Ricky +``` diff --git a/samples/springboot4/alt-pet-store/build.gradle b/samples/springboot4/alt-pet-store/build.gradle new file mode 100644 index 000000000..298feedf6 --- /dev/null +++ b/samples/springboot4/alt-pet-store/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + implementation('org.springframework.boot:spring-boot-starter-web:3.4.5') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + }, + 'com.amazonaws.serverless:aws-serverless-java-container-springboot4:[2.0-SNAPSHOT,)', + 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + ) +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/samples/springboot4/alt-pet-store/pom.xml b/samples/springboot4/alt-pet-store/pom.xml new file mode 100644 index 000000000..c898b94f5 --- /dev/null +++ b/samples/springboot4/alt-pet-store/pom.xml @@ -0,0 +1,148 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + petstore-springboot4-example + 2.0-SNAPSHOT + Spring Boot 4 example for the aws-serverless-java-container library + Simple pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.2.0-SNAPSHOT,),[2.1.1,) + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/alt-pet-store/src/assembly/bin.xml b/samples/springboot4/alt-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..f5d30c519 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,51 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; + +import jakarta.servlet.Filter; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + @Bean("CognitoIdentityFilter") + public Filter cognitoFilter() { + return new CognitoIdentityFilter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..f31542e55 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.controller; + + + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + + @RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + System.out.println("Body: " + body + " - " + gender + "/" + age + "/" + name); + return gender + "/" + age + "/" + name; + } + +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/resources/logback.xml b/samples/springboot4/alt-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..81d891777 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/resources/logback.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/template.yml b/samples/springboot4/alt-pet-store/template.yml new file mode 100644 index 000000000..4a7e8bb43 --- /dev/null +++ b/samples/springboot4/alt-pet-store/template.yml @@ -0,0 +1,41 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4 spring-cloud-function web-proxy support + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: +# AutoPublishAlias: bcn + FunctionName: pet-store-boot-4 + Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler::handleRequest + Runtime: java25 + SnapStart: + ApplyOn: PublishedVersions + CodeUri: . + MemorySize: 1024 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringPetStoreApi + + diff --git a/samples/springboot4/graphql-pet-store/README.md b/samples/springboot4/graphql-pet-store/README.md new file mode 100644 index 000000000..9f1f8db3a --- /dev/null +++ b/samples/springboot4/graphql-pet-store/README.md @@ -0,0 +1,38 @@ +# Serverless Spring Boot 4 with GraphQL example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. Unlike older examples, this example uses the [Spring for GraphQl](https://docs.spring.io/spring-graphql/reference/) library. + + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/graphQl +--------------------------------------------------------------------------------------------------------- + +$ curl -X POST https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/graphQl -d '{"query":"query petDetails {\n petById(id: \"pet-1\") {\n id\n name\n breed\n owner {\n id\n firstName\n lastName\n }\n }\n}","operationName":"petDetails"}' -H "Content-Type: application/json" + +``` \ No newline at end of file diff --git a/samples/springboot4/graphql-pet-store/pom.xml b/samples/springboot4/graphql-pet-store/pom.xml new file mode 100644 index 000000000..235586941 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/pom.xml @@ -0,0 +1,168 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-springboot4-graphql-example + 2.0-SNAPSHOT + Spring Boot 4 GraphQL example for the aws-serverless-java-container library + GraphQL pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter-graphql + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.graphql + spring-graphql-test + test + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/graphql-pet-store/src/assembly/bin.xml b/samples/springboot4/graphql-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..efc312c25 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..b60367223 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,43 @@ +package com.amazonaws.serverless.sample.springboot4; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java new file mode 100644 index 000000000..863af6351 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java @@ -0,0 +1,44 @@ +package com.amazonaws.serverless.sample.springboot4; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringDelegatingLambdaContainerHandler handler; + static { + try { + handler = new SpringDelegatingLambdaContainerHandler(Application.class); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + public StreamLambdaHandler() { + // we enable the timer for debugging. This SHOULD NOT be enabled in production. + Timer.enable(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.handleRequest(inputStream, outputStream, context); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..c76a624e1 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,21 @@ +package com.amazonaws.serverless.sample.springboot4.controller; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; +import com.amazonaws.serverless.sample.springboot4.model.Owner; +import com.amazonaws.serverless.sample.springboot4.model.Pet; + +@Controller +public class PetsController { + @QueryMapping + public Pet petById(@Argument String id) { + return Pet.getById(id); + } + + @SchemaMapping + public Owner owner(Pet pet) { + return Owner.getById(pet.ownerId()); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java new file mode 100644 index 000000000..5349a85b3 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Arrays; +import java.util.List; + +public record Owner (String id, String firstName, String lastName) { + + private static List owners = Arrays.asList( + new Owner("owner-1", "Joshua", "Bloch"), + new Owner("owner-2", "Douglas", "Adams"), + new Owner("owner-3", "Bill", "Bryson") + ); + + public static Owner getById(String id) { + return owners.stream() + .filter(owner -> owner.id().equals(id)) + .findFirst() + .orElse(null); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b14199453 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Arrays; +import java.util.List; + +public record Pet (String id, String name, String breed, String ownerId) { + + private static List pets = Arrays.asList( + new Pet("pet-1", "Alpha", "Bulldog", "owner-1"), + new Pet("pet-2", "Max", "German Shepherd", "owner-2"), + new Pet("pet-3", "Rockie", "Golden Retriever", "owner-3") + ); + + public static Pet getById(String id) { + return pets.stream() + .filter(pet -> pet.id().equals(id)) + .findFirst() + .orElse(null); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls b/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls new file mode 100644 index 000000000..293cdcc40 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,16 @@ +type Query { + petById(id: ID): Pet +} + +type Pet { + id: ID + name: String + breed: String + owner: Owner +} + +type Owner { + id: ID + firstName: String + lastName: String +} diff --git a/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml b/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..8ff988992 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/springboot4/graphql-pet-store/template.yml b/samples/springboot4/graphql-pet-store/template.yml new file mode 100644 index 000000000..5db3eefd3 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/template.yml @@ -0,0 +1,35 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4, Spring for GraphQl and the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 1024 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/graphql' + Export: + Name: SpringBootPetStoreApi diff --git a/samples/springboot4/pet-store-native/.gitignore b/samples/springboot4/pet-store-native/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/samples/springboot4/pet-store-native/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..7d02699af --- /dev/null +++ b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/samples/springboot4/pet-store-native/Dockerfile b/samples/springboot4/pet-store-native/Dockerfile new file mode 100644 index 000000000..ec8eb4a0d --- /dev/null +++ b/samples/springboot4/pet-store-native/Dockerfile @@ -0,0 +1,37 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2023 + +RUN yum -y update \ + && yum install -y unzip tar gzip bzip2-devel ed gcc gcc-c++ gcc-gfortran \ + less libcurl-devel openssl openssl-devel readline-devel xz-devel \ + zlib-devel glibc-static zlib-static \ + && rm -rf /var/cache/yum + +# Graal VM +ENV GRAAL_VERSION 25.0.1 +ENV ARCHITECTURE aarch64 +ENV GRAAL_FILENAME graalvm-community-jdk-${GRAAL_VERSION}_linux-${ARCHITECTURE}_bin.tar.gz +RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAAL_VERSION}/${GRAAL_FILENAME} | tar -xvz +RUN mv graalvm-community-openjdk-${GRAAL_VERSION}* /usr/lib/graalvm +ENV JAVA_HOME /usr/lib/graalvm + +# Maven +ENV MVN_VERSION 3.9.9 +ENV MVN_FOLDERNAME apache-maven-${MVN_VERSION} +ENV MVN_FILENAME apache-maven-${MVN_VERSION}-bin.tar.gz +RUN curl -4 -L https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/${MVN_FILENAME} | tar -xvz +RUN mv $MVN_FOLDERNAME /usr/lib/maven +RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn + +# Gradle +ENV GRADLE_VERSION 7.4.1 +ENV GRADLE_FOLDERNAME gradle-${GRADLE_VERSION} +ENV GRADLE_FILENAME gradle-${GRADLE_VERSION}-bin.zip +RUN curl -LO https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip +RUN unzip gradle-${GRADLE_VERSION}-bin.zip +RUN mv $GRADLE_FOLDERNAME /usr/lib/gradle +RUN ln -s /usr/lib/gradle/bin/gradle /usr/bin/gradle + +VOLUME /project +WORKDIR /project + +WORKDIR /pet-store-native diff --git a/samples/springboot4/pet-store-native/README.md b/samples/springboot4/pet-store-native/README.md new file mode 100644 index 000000000..57994a4e6 --- /dev/null +++ b/samples/springboot4/pet-store-native/README.md @@ -0,0 +1,39 @@ +In this sample, you'll build a native GraalVM image for running Spring Boot 4.0 and Spring Framework 7.0 web workloads in AWS Lambda. + +**Important**: Spring Boot 4.0 requires GraalVM 25 for native image compilation. GraalVM 21 is not compatible. + +## To build the sample + +You first need to build the function, then you will deploy it to AWS Lambda. + +Please note that the sample is for `x86` architectures. In case you want to build and run it on ARM, e.g. Apple Mac M1, M2, ... +you must change the according line in the `Dockerfile` to `ENV ARCHITECTURE aarch64`. +In addition, uncomment the `arm64` Architectures section in `template.yml`. + +### Step 1 - Build the native image + +Before starting the build, you must clone or download the code in **pet-store-native**. + +1. Change into the project directory: `samples/springboot4/pet-store-native` +2. Run the following to build a Docker container image with GraalVM 25 which will include all the necessary dependencies to build the application + ``` + docker build -t al2023-graalvm25:native-web . + ``` +3. Build the application within the previously created build image + ``` + docker run -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 al2023-graalvm25:native-web mvn clean native:compile -Pnative + ``` +4. After the build finishes, you need to deploy the function: + ``` + sam deploy --guided + ``` + +This will deploy your application and will attach an AWS API Gateway +Once the deployment is finished you should see the following: +``` +Key ServerlessWebNativeApi +Description URL for application +Value https://xxxxxxxx.execute-api.us-east-2.amazonaws.com/pets +``` + +You can now simply execute GET on this URL and see the listing fo all pets. diff --git a/samples/springboot4/pet-store-native/mvnw b/samples/springboot4/pet-store-native/mvnw new file mode 100755 index 000000000..8d937f4c1 --- /dev/null +++ b/samples/springboot4/pet-store-native/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/samples/springboot4/pet-store-native/mvnw.cmd b/samples/springboot4/pet-store-native/mvnw.cmd new file mode 100644 index 000000000..f80fbad3e --- /dev/null +++ b/samples/springboot4/pet-store-native/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/samples/springboot4/pet-store-native/pom.xml b/samples/springboot4/pet-store-native/pom.xml new file mode 100644 index 000000000..1946401f3 --- /dev/null +++ b/samples/springboot4/pet-store-native/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + com.amazonaws.serverless.sample + pet-store-native-springboot4 + 0.0.1-SNAPSHOT + pet-store-native-springboot4 + Sample of AWS with Spring Boot 4.0 Native + + 17 + + + + org.springframework.boot + spring-boot-starter + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + org.crac + crac + runtime + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + native + + + + org.springframework.boot + spring-boot-maven-plugin + + -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/ --enable-preview + + + + + org.graalvm.buildtools + native-maven-plugin + + pet-store-native + + --enable-url-protocols=http + -march=compatibility + + + + + + build + + package + + + test + + test + + test + + + + + maven-assembly-plugin + + + native-zip + package + + single + + false + + + + + src/assembly/native.xml + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/assembly/java.xml b/samples/springboot4/pet-store-native/src/assembly/java.xml new file mode 100644 index 000000000..bd4961b58 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/assembly/java.xml @@ -0,0 +1,31 @@ + + java-zip + + zip + + + + + target/classes + / + + + src/shell/java + / + true + 0775 + + bootstrap + + + + + + /lib + false + runtime + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/assembly/native.xml b/samples/springboot4/pet-store-native/src/assembly/native.xml new file mode 100644 index 000000000..9bd97a5b7 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/assembly/native.xml @@ -0,0 +1,29 @@ + + native-zip + + zip + + + + + src/shell/native + / + true + 0775 + + bootstrap + + + + target + / + true + 0775 + + pet-store-native + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java new file mode 100644 index 000000000..3f1d42559 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java @@ -0,0 +1,12 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java new file mode 100644 index 000000000..4f0abad79 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + public HelloController() { + System.out.println("Creating controller"); + } + + @GetMapping("/hello") + public String something(){ + return "Hello World"; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..7576b7898 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.controller; + + + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @PostMapping(path = "/pets") + public Pet createPet(@RequestBody Pet newPet) { + System.out.println("==> Creating Pet: " + newPet); + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @GetMapping(path = "/pets") + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + System.out.println("==> Listing Pets"); + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @GetMapping(path = "/pets/{petId}") + public Pet listPets() { + System.out.println("==> Listing Pets"); + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore b/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore new file mode 100644 index 000000000..0726bbaa2 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore @@ -0,0 +1 @@ +/native-image/ diff --git a/samples/springboot4/pet-store-native/src/main/resources/application.properties b/samples/springboot4/pet-store-native/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/samples/springboot4/pet-store-native/src/shell/java/bootstrap b/samples/springboot4/pet-store-native/src/shell/java/bootstrap new file mode 100644 index 000000000..e30ee22e9 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/shell/java/bootstrap @@ -0,0 +1,7 @@ +#!/bin/sh + +cd ${LAMBDA_TASK_ROOT:-.} + +java -Dspring.main.web-application-type=none -Dlogging.level.org.springframework=DEBUG \ + -noverify -XX:TieredStopAtLevel=1 -Xss256K -XX:MaxMetaspaceSize=128M \ + -cp .:`echo lib/*.jar | tr ' ' :` com.amazonaws.serverless.sample.springboot4.DemoApplication \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/shell/native/bootstrap b/samples/springboot4/pet-store-native/src/shell/native/bootstrap new file mode 100644 index 000000000..0156b090b --- /dev/null +++ b/samples/springboot4/pet-store-native/src/shell/native/bootstrap @@ -0,0 +1,5 @@ +#!/bin/sh + +cd ${LAMBDA_TASK_ROOT:-.} + +./pet-store-native -Dlogging.level.org.springframework=DEBUG -Dlogging.level.com.amazonaws.serverless.proxy.spring=DEBUG diff --git a/samples/springboot4/pet-store-native/template.yaml b/samples/springboot4/pet-store-native/template.yaml new file mode 100644 index 000000000..d0b63d9a6 --- /dev/null +++ b/samples/springboot4/pet-store-native/template.yaml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Serverless Java Container GraalVM with Spring Boot 4 +Resources: + ServerlessWebNativeFunction: + Type: AWS::Serverless::Function + Properties: + MemorySize: 512 + FunctionName: pet-store-native-springboot4 + Timeout: 15 + CodeUri: ./target/pet-store-native-springboot4-0.0.1-SNAPSHOT-native-zip.zip + Handler: NOP + Runtime: provided.al2023 + Architectures: + - arm64 + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL +Outputs: + ServerlessWebNativeApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: ServerlessWebNativeApi + \ No newline at end of file diff --git a/samples/springboot4/pet-store/README.md b/samples/springboot4/pet-store/README.md new file mode 100644 index 000000000..40955e968 --- /dev/null +++ b/samples/springboot4/pet-store/README.md @@ -0,0 +1,36 @@ +# Serverless Spring Boot 4 example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. The `StreamLambdaHandler` object is the main entry point for Lambda. + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot4/pet-store/build.gradle b/samples/springboot4/pet-store/build.gradle new file mode 100644 index 000000000..298feedf6 --- /dev/null +++ b/samples/springboot4/pet-store/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + implementation('org.springframework.boot:spring-boot-starter-web:3.4.5') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + }, + 'com.amazonaws.serverless:aws-serverless-java-container-springboot4:[2.0-SNAPSHOT,)', + 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + ) +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/samples/springboot4/pet-store/pom.xml b/samples/springboot4/pet-store/pom.xml new file mode 100644 index 000000000..eb8424808 --- /dev/null +++ b/samples/springboot4/pet-store/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-springboot4-example + 2.0-SNAPSHOT + Spring Boot 4 example for the aws-serverless-java-container library + Simple pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/pet-store/src/assembly/bin.xml b/samples/springboot4/pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot4/pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..13a80b32a --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,49 @@ +package com.amazonaws.serverless.sample.springboot4; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java new file mode 100644 index 000000000..61cde4fac --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java @@ -0,0 +1,50 @@ +package com.amazonaws.serverless.sample.springboot4; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + + // we use the onStartup method of the handler to register our custom filter + handler.onStartup(servletContext -> { + FilterRegistration.Dynamic registration = servletContext.addFilter("CognitoIdentityFilter", CognitoIdentityFilter.class); + registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + }); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + public StreamLambdaHandler() { + // we enable the timer for debugging. This SHOULD NOT be enabled in production. + Timer.enable(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..cb80068bc --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.controller; + + + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/pet-store/src/main/resources/logback.xml b/samples/springboot4/pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..14a3a84fa --- /dev/null +++ b/samples/springboot4/pet-store/src/main/resources/logback.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store/template.yml b/samples/springboot4/pet-store/template.yml new file mode 100644 index 000000000..789057a50 --- /dev/null +++ b/samples/springboot4/pet-store/template.yml @@ -0,0 +1,35 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4 with the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 1512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringBootPetStoreApi