Stopping point with initial POC

This commit is contained in:
Oleg Zhurakousky
2023-02-22 12:56:13 +01:00
parent 145335bcb2
commit 2008a0ead3
29 changed files with 1052 additions and 293 deletions

View File

@@ -0,0 +1,3 @@
Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the
context of the execution (i.e., AWS or Azure or whatever).
In fact classes in these package is a slimed-down copy of similar classes in MockMVC.

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-adapter-aws-web</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-function-adapter-aws-web</name>
<description>AWS Lambda Adapter for Spring Cloud Function</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-parent</artifactId>
<version>3.2.9-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
# This file is auto generated by SAM CLI build command
[function_build_definitions]
[function_build_definitions.9341c1d5-9265-48ef-836e-25df000b0c59]
codeuri = "/Users/ozhurakousky/Documents/dev/repo/spring-cloud-function/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store"
runtime = "java11"
architecture = "x86_64"
handler = "org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest"
manifest_hash = ""
packagetype = "Zip"
functions = ["PetStoreFunction"]
[layer_build_definitions]

View File

@@ -0,0 +1,38 @@
Copied from https://github.com/awslabs/aws-serverless-java-container/tree/main/samples/spring/pet-store
# Serverless Spring example
A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/). 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
```

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>oz.spring.petstore</groupId>
<artifactId>pet-store</artifactId>
<version>1.0-SNAPSHOT</version>
<name>pet-store</name>
<description>Simple pet store written with the Spring framework</description>
<url>https://aws.amazon.com/lambda/</url>
<scm>
<url>https://github.com/awslabs/aws-serverless-java-container.git</url>
</scm>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.3.25</spring.version>
<junit.version>4.13.2</junit.version>
<log4j.version>2.19.0</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws-web</artifactId>
<version>3.2.9-SNAPSHOT</version>
</dependency>
<!--
the Spring Context Indexer run an annotation processor at compile time and generates
a META-INF/spring.components file that Spring can use to speed up component scanning at boot time.
For small applications, this doesn't make a big difference. However, for large applications with
complex dependencies this may improve your cold start time significantly.
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>${spring.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j2</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>shaded-jar</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>assembly-zip</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<!-- don't build a jar, we'll use the classes dir -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<!-- select and copy only runtime dependencies to a temporary lib folder -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>zip-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>${project.artifactId}-${project.version}</finalName>
<descriptors>
<descriptor>src${file.separator}assembly${file.separator}bin.xml</descriptor>
</descriptors>
<attach>false</attach>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,24 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>lambda-package</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<!-- copy runtime dependencies with some exclusions -->
<fileSet>
<directory>${project.build.directory}${file.separator}lib</directory>
<outputDirectory>lib</outputDirectory>
</fileSet>
<!-- copy all classes -->
<fileSet>
<directory>${project.build.directory}${file.separator}classes</directory>
<includes>
<include>**</include>
</includes>
<outputDirectory>${file.separator}</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2023-2023 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 oz.spring.petstore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@Import({ PetsController.class })
public class PetStoreSpringAppConfig {
/*
* 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();
}
/*
* optimization - avoids creating default exception resolvers; not required as the serverless container handles
* all exceptions
*
* By default, an ExceptionHandlerExceptionResolver is created which creates many dependent object, including
* an expensive ObjectMapper instance.
*
* To enable custom @ControllerAdvice classes remove this bean.
*/
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
return new HandlerExceptionResolver() {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
};
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2023-2023 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 oz.spring.petstore;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import oz.spring.petstore.model.Pet;
import oz.spring.petstore.model.PetData;
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<Integer> 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;
}
@GetMapping("favicon.ico")
@ResponseBody
void returnNoFavicon() {
}
@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;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2023-2023 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 oz.spring.petstore.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;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2023-2023 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 oz.spring.petstore.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;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2023-2023 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 oz.spring.petstore.model;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class PetData {
private static List<String> 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<String> 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<String> getBreeds() {
return breeds;
}
public static List<String> 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();
}
}

View File

@@ -0,0 +1,37 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example Pet Store API written with spring-cloud-function web-proxy support
Globals:
Api:
# API Gateway regional endpoints
EndpointConfiguration: REGIONAL
Resources:
PetStoreFunction:
Type: AWS::Serverless::Function
Properties:
Handler: org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest
Runtime: java11
CodeUri: .
MemorySize: 512
Policies: AWSLambdaBasicExecutionRole
Timeout: 30
Environment:
Variables:
MAIN_CLASS: oz.spring.petstore.PetStoreSpringAppConfig
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: PetStoreLambda

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2019-2021 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 org.springframework.cloud.function.adapter.aws.web;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//import org.springframework.boot.SpringBootConfiguration;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.KotlinDetector;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* General utility class which aggregates various class-level utility functions
* used by the framework.
*
* @author Oleg Zhurakousky
* @since 3.0.1
*/
public final class FunctionClassUtils {
private static Log logger = LogFactory.getLog(FunctionClassUtils.class);
private FunctionClassUtils() {
}
/**
* Discovers the start class in the currently running application.
* The discover search order is 'MAIN_CLASS' environment property,
* 'MAIN_CLASS' system property, META-INF/MANIFEST.MF:'Start-Class' attribute,
* meta-inf/manifest.mf:'Start-Class' attribute.
*
* @return instance of Class which represent the start class of the application.
*/
public static Class<?> getStartClass() {
ClassLoader classLoader = FunctionClassUtils.class.getClassLoader();
return getStartClass(classLoader);
}
static Class<?> getStartClass(ClassLoader classLoader) {
Class<?> mainClass = null;
if (System.getenv("MAIN_CLASS") != null) {
mainClass = ClassUtils.resolveClassName(System.getenv("MAIN_CLASS"), classLoader);
}
else if (System.getProperty("MAIN_CLASS") != null) {
mainClass = ClassUtils.resolveClassName(System.getProperty("MAIN_CLASS"), classLoader);
}
else {
try {
Class<?> result = getStartClass(
Collections.list(classLoader.getResources(JarFile.MANIFEST_NAME)), classLoader);
if (result == null) {
result = getStartClass(Collections
.list(classLoader.getResources("meta-inf/manifest.mf")), classLoader);
}
Assert.notNull(result, "Failed to locate main class");
mainClass = result;
}
catch (Exception ex) {
throw new IllegalStateException("Failed to discover main class. An attempt was made to discover "
+ "main class as 'MAIN_CLASS' environment variable, system property as well as "
+ "entry in META-INF/MANIFEST.MF (in that order).", ex);
}
}
logger.info("Main class: " + mainClass);
return mainClass;
}
private static Class<?> getStartClass(List<URL> list, ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Searching manifests: " + list);
}
for (URL url : list) {
try {
InputStream inputStream = null;
Manifest manifest = new Manifest(url.openStream());
logger.info("Searching for start class in manifest: " + url);
if (logger.isDebugEnabled()) {
manifest.write(System.out);
}
try {
String startClassName = manifest.getMainAttributes().getValue("Start-Class");
if (!StringUtils.hasText(startClassName)) {
startClassName = manifest.getMainAttributes().getValue("Main-Class");
}
if (StringUtils.hasText(startClassName)) {
Class<?> startClass = ClassUtils.forName(startClassName, classLoader);
if (KotlinDetector.isKotlinType(startClass)) {
PathMatchingResourcePatternResolver r = new PathMatchingResourcePatternResolver(classLoader);
String packageName = startClass.getPackage().getName();
Resource[] resources = r.getResources("classpath:" + packageName.replace(".", "/") + "/*.class");
for (int i = 0; i < resources.length; i++) {
Resource resource = resources[i];
String className = packageName + "." + (resource.getFilename().replace("/", ".")).replace(".class", "");
startClass = ClassUtils.forName(className, classLoader);
// if (isSpringBootApplication(startClass)) {
// logger.info("Loaded Main Kotlin Class: " + startClass);
// return startClass;
// }
}
}
// else if (isSpringBootApplication(startClass)) {
// logger.info("Loaded Start Class: " + startClass);
// return startClass;
// }
}
}
finally {
if (inputStream != null) {
inputStream.close();
}
}
}
catch (Exception ex) {
logger.debug("Failed to determine Start-Class in manifest file of " + url, ex);
}
}
return null;
}
// private static boolean isSpringBootApplication(Class<?> startClass) {
// return startClass.getDeclaredAnnotation(SpringBootApplication.class) != null
// || startClass.getDeclaredAnnotation(SpringBootConfiguration.class) != null;
// }
}

View File

@@ -0,0 +1,5 @@
Classes in this package would remain specific to AWS (in this case). There would be something similar in Azure and others.
And these classes would depend on what is currently in `org.springframework.web.client` package of this module.
However, ideally the contents of the `org.springframework.web.client` package should reside in spring-web somewhere as a light weight
HTTP proxy as we technically already have it in a form of MockMVC.

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2023-2023 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 org.springframework.cloud.function.adapter.aws.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.Filter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ProxyHttpServletRequest;
import org.springframework.web.client.ProxyHttpServletResponse;
import org.springframework.web.client.ProxyMvc;
import org.springframework.web.client.ProxyServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
*
* AWS Lambda specific handler that will proxy API Gateway request to Spring Web-app
* This class represents AWS Lambda fronted by API Gateway and is identified as 'handler' during the deployment.
*
* @author Oleg Zhurakousky
*
*/
public class WebProxyInvoker {
private static Log logger = LogFactory.getLog(WebProxyInvoker.class);
private final ProxyMvc mvc;
private final ServletContext servletContext;
ObjectMapper mapper = new ObjectMapper();
public WebProxyInvoker() throws ServletException {
Class<?> startClass = FunctionClassUtils.getStartClass();
AnnotationConfigWebApplicationContext applpicationContext = new AnnotationConfigWebApplicationContext();
applpicationContext.register(startClass);
this.servletContext = new ProxyServletContext();
ServletConfig servletConfig = new ProxyServletConfig(this.servletContext);
DispatcherServlet servlet = new DispatcherServlet(applpicationContext);
servlet.init(servletConfig);
this.mvc = new ProxyMvc(servlet, applpicationContext.getBeansOfType(Filter.class).values().toArray(new Filter[0]));
}
/*
* TODO
* - Security context propagation from AWS API Gateway (easy)
* - Error handling
*/
@SuppressWarnings("unchecked")
private HttpServletRequest prepareRequest(InputStream input) throws IOException {
Map<String, Object> request = mapper.readValue(input, Map.class);
if (logger.isDebugEnabled()) {
logger.debug("Request: " + request);
}
String httpMethod = (String) request.get("httpMethod");
String path = (String) request.get("path");
if (logger.isDebugEnabled()) {
logger.debug("httpMethod: " + httpMethod);
logger.debug("path: " + path);
}
ProxyHttpServletRequest httpRequest = new ProxyHttpServletRequest(servletContext, httpMethod, path);
if (StringUtils.hasText((String) request.get("body"))) {
httpRequest.setContent(((String) request.get("body")).getBytes());
}
if (request.get("queryStringParameters") != null) {
httpRequest.setParameters((Map<String, ?>) request.get("queryStringParameters"));
}
Map<String, Object> headers = (Map<String, Object>) request.get("headers");
headers.putAll((Map<String, Object>) request.get("multiValueHeaders"));
for (Entry<String, Object> entry : headers.entrySet()) {
httpRequest.addHeader(entry.getKey(), entry.getValue());
}
return httpRequest;
}
public void handleRequest(InputStream input, OutputStream output) throws IOException {
HttpServletRequest httpRequest = this.prepareRequest(input);
ProxyHttpServletResponse httpResponse = new ProxyHttpServletResponse();
try {
this.mvc.perform(httpRequest, httpResponse);
}
catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
String responseString = httpResponse.getContentAsString();
if (StringUtils.hasText(responseString)) {
if (logger.isDebugEnabled()) {
logger.debug("Response: " + responseString);
}
Map<String, Object> apiGatewayResponseStructure = new HashMap<String, Object>();
apiGatewayResponseStructure.put("isBase64Encoded", false);
apiGatewayResponseStructure.put("statusCode", 200);
apiGatewayResponseStructure.put("body", responseString);
Map<String, List<String>> multiValueHeaders = new HashMap<>();
for (String headerName : httpResponse.getHeaderNames()) {
multiValueHeaders.put(headerName, httpResponse.getHeaders(headerName));
}
// TODO investigate why AWS doesn't like List as value
// apiGatewayResponseStructure.put("headers", multiValueHeaders);
byte[] apiGatewayResponseBytes = mapper.writeValueAsBytes(apiGatewayResponseStructure);
StreamUtils.copy(apiGatewayResponseBytes, output);
}
}
private static class ProxyServletConfig implements ServletConfig {
private final ServletContext servletContext;
ProxyServletConfig(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public String getServletName() {
return "serverless-proxy";
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(new ArrayList<String>());
}
@Override
public String getInitParameter(String name) {
return null;
}
}
}

View File

@@ -164,30 +164,9 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
private final Map<String, String[]> parameters = new LinkedHashMap<>(16);
// private String protocol = DEFAULT_PROTOCOL;
//
// private String scheme = DEFAULT_SCHEME;
//
// private String serverName = DEFAULT_SERVER_NAME;
//
// private int serverPort = DEFAULT_SERVER_PORT;
//
// private String remoteAddr = DEFAULT_REMOTE_ADDR;
//
// private String remoteHost = DEFAULT_REMOTE_HOST;
/** List of locales in descending order. */
private final LinkedList<Locale> locales = new LinkedList<>();
private boolean secure = false;
// private int remotePort = DEFAULT_SERVER_PORT;
//
// private String localName = DEFAULT_SERVER_NAME;
//
// private String localAddr = DEFAULT_SERVER_ADDR;
//
// private int localPort = DEFAULT_SERVER_PORT;
private boolean asyncStarted = false;
@@ -249,29 +228,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
// Constructors
// ---------------------------------------------------------------------
// /**
// * Create a new {@code MockHttpServletRequest} with a default
// * {@link MockServletContext}.
// * @param method the request method (may be {@code null})
// * @param requestURI the request URI (may be {@code null})
// * @see #setMethod
// * @see #setRequestURI
// * @see #MockHttpServletRequest(ServletContext, String, String)
// */
// public ServerlessHttpServletRequest(@Nullable String method, @Nullable String requestURI) {
// this(null, method, requestURI);
// }
//
// /**
// * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}.
// * @param servletContext the ServletContext that the request runs in
// * (may be {@code null} to use a default {@link MockServletContext})
// * @see #MockHttpServletRequest(ServletContext, String, String)
// */
// public ServerlessHttpServletRequest(@Nullable ServletContext servletContext) {
// this(servletContext, "", "");
// }
/**
* Create a new {@code MockHttpServletRequest} with the supplied
* {@link ServletContext}, {@code method}, and {@code requestURI}.
@@ -470,18 +426,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Override
public ServletInputStream getInputStream() {
// if (this.inputStream != null) {
// return this.inputStream;
// }
// else if (this.reader != null) {
// throw new IllegalStateException(
// "Cannot call getInputStream() after getReader() has already been called for the current request") ;
// }
//
// this.inputStream = (this.content != null ?
// new DelegatingServletInputStream(new ByteArrayInputStream(this.content)) :
// EMPTY_SERVLET_INPUT_STREAM);
// return this.inputStream;
throw new UnsupportedOperationException();
}
@@ -635,64 +579,24 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Override
public String getScheme() {
// return this.scheme;
throw new UnsupportedOperationException();
}
public void setServerName(String serverName) {
// this.serverName = serverName;
throw new UnsupportedOperationException();
}
@Override
public String getServerName() {
// String rawHostHeader = getHeader(HttpHeaders.HOST);
// String host = rawHostHeader;
// if (host != null) {
// host = host.trim();
// if (host.startsWith("[")) {
// int indexOfClosingBracket = host.indexOf(']');
// Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader);
// host = host.substring(0, indexOfClosingBracket + 1);
// }
// else if (host.contains(":")) {
// host = host.substring(0, host.indexOf(':'));
// }
// return host;
// }
//
// // else
// return this.serverName;
throw new UnsupportedOperationException();
}
public void setServerPort(int serverPort) {
// this.serverPort = serverPort;
throw new UnsupportedOperationException();
}
@Override
public int getServerPort() {
// String rawHostHeader = getHeader(HttpHeaders.HOST);
// String host = rawHostHeader;
// if (host != null) {
// host = host.trim();
// int idx;
// if (host.startsWith("[")) {
// int indexOfClosingBracket = host.indexOf(']');
// Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader);
// idx = host.indexOf(':', indexOfClosingBracket);
// }
// else {
// idx = host.indexOf(':');
// }
// if (idx != -1) {
// return Integer.parseInt(host.substring(idx + 1));
// }
// }
//
// // else
// return this.serverPort;
throw new UnsupportedOperationException();
}
@@ -720,24 +624,20 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
}
public void setRemoteAddr(String remoteAddr) {
// this.remoteAddr = remoteAddr;
throw new UnsupportedOperationException();
}
@Override
public String getRemoteAddr() {
return "proxy";
// throw new UnsupportedOperationException();
}
public void setRemoteHost(String remoteHost) {
// this.remoteHost = remoteHost;
throw new UnsupportedOperationException();
}
@Override
public String getRemoteHost() {
// return this.remoteHost;
throw new UnsupportedOperationException();
}
@@ -840,18 +740,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
return Collections.enumeration(this.locales);
}
/**
* Set the boolean {@code secure} flag indicating whether the mock request was
* made using a secure channel, such as HTTPS.
*
* @see #isSecure()
* @see #getScheme()
* @see #setScheme(String)
*/
public void setSecure(boolean secure) {
this.secure = secure;
}
/**
* Return {@code true} if the {@link #setSecure secure} flag has been set to
* {@code true} or if the {@link #getScheme scheme} is {@code https}.
@@ -860,13 +748,11 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
*/
@Override
public boolean isSecure() {
// return (this.secure || HTTPS.equalsIgnoreCase(this.scheme));
throw new UnsupportedOperationException();
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
// return new MockRequestDispatcher(path);
throw new UnsupportedOperationException();
}
@@ -877,29 +763,24 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
}
public void setRemotePort(int remotePort) {
// this.remotePort = remotePort;
throw new UnsupportedOperationException();
}
@Override
public int getRemotePort() {
// return this.remotePort;
throw new UnsupportedOperationException();
}
public void setLocalName(String localName) {
// this.localName = localName;
throw new UnsupportedOperationException();
}
@Override
public String getLocalName() {
// return this.localName;
throw new UnsupportedOperationException();
}
public void setLocalAddr(String localAddr) {
// this.localAddr = localAddr;
throw new UnsupportedOperationException();
}
@@ -909,7 +790,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
}
public void setLocalPort(int localPort) {
// this.localPort = localPort;
throw new UnsupportedOperationException();
}
@@ -926,10 +806,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Override
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
// Assert.state(this.asyncSupported, "Async not supported");
// this.asyncStarted = true;
// this.asyncContext = new MockAsyncContext(request, response);
// return this.asyncContext;
throw new UnsupportedOperationException();
}
@@ -952,15 +828,12 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
}
public void setAsyncContext(@Nullable AsyncContext asyncContext) {
// this.asyncContext = asyncContext;
throw new UnsupportedOperationException();
}
@Override
@Nullable
public AsyncContext getAsyncContext() {
// return this.asyncContext;
// throw new UnsupportedOperationException();
return null;
}

View File

@@ -0,0 +1,3 @@
Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the
context of the execution (i.e., AWS or Azure or whatever).
In fact classes in these package is a slimed-down copy of similar classes in MockMVC.

View File

@@ -24,8 +24,6 @@ import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

View File

@@ -21,19 +21,17 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.adapter.aws.TestContext;
import org.springframework.cloud.function.json.JacksonMapper;
public class WebProxyInvokerTests {
static String apiGatewayEvent = "{\n" +
" \"resource\": \"/pets\",\n" +
" \"path\": \"/pets/64f56d94-a059-4111-9eeb-ee0c994b1ba8\",\n" +
" \"path\": \"/pets/64f56d94-a059-4111-9eeb-ee0c994b1ba8?foo=bar\",\n" +
" \"httpMethod\": \"GET\",\n" +
" \"headers\": {\n" +
" \"accept\": \"*/*\",\n" +
@@ -116,11 +114,11 @@ public class WebProxyInvokerTests {
InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
ByteArrayOutputStream output = new ByteArrayOutputStream();
invoker.handleRequest(targetStream, output, new TestContext());
invoker.handleRequest(targetStream, output);
JacksonMapper mapper = new JacksonMapper(new ObjectMapper());
ObjectMapper mapper = new ObjectMapper();
System.out.println("RESULT: =======> " + new String(output.toByteArray()));
Map result = mapper.fromJson(output.toByteArray(), Map.class);
Map result = mapper.readValue(output.toByteArray(), Map.class);
System.out.println(result);
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2023-2023 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 org.springframework.cloud.function.adapter.aws.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.function.json.JacksonMapper;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.cloud.function.utils.FunctionClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.ProxyDispatcherServlet;
import org.springframework.web.client.ProxyHttpServletRequest;
import org.springframework.web.client.ProxyHttpServletResponse;
import org.springframework.web.client.ProxyMvc;
import org.springframework.web.client.ProxyServletConfig;
import org.springframework.web.client.ProxyServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class WebProxyInvoker implements RequestStreamHandler {
private final ProxyMvc mvc;
public WebProxyInvoker() throws ServletException {
Class<?> startClass = FunctionClassUtils.getStartClass();
AnnotationConfigWebApplicationContext applpicationContext = new AnnotationConfigWebApplicationContext();
applpicationContext.register(startClass);
ServletContext servletContext = new ProxyServletContext();
ServletConfig servletConfig = new ProxyServletConfig(servletContext);
applpicationContext.setServletConfig(servletConfig);
DispatcherServlet servlet = new DispatcherServlet(applpicationContext);
servlet.init(servletConfig);
this.mvc = new ProxyMvc(servlet, applpicationContext.getBeansOfType(Filter.class).values().toArray(new Filter[0]));
}
@Override
public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
ProxyServletContext servletContext = new ProxyServletContext();
JsonMapper mapper = new JacksonMapper(new ObjectMapper());
Map<String, Object> request = mapper.fromJson(StreamUtils.copyToByteArray(input), Map.class);
System.out.println("!!!==> REQUEST: " + request);
String httpMethod = (String) request.get("httpMethod");
String path = (String) request.get("path");
System.out.println("!!!==> httpMethod: " + httpMethod);
System.out.println("!!!==> path: " + path);
HttpServletRequest resquest = new ProxyHttpServletRequest(null, httpMethod, path);
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
try {
this.mvc.perform(resquest, response);
}
catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
byte[] responseBytes = response.getContentAsByteArray();
if (!ObjectUtils.isEmpty(responseBytes)) {
System.out.println("!!!==> responseBytes: " + response.getContentAsString());
Map<String, Object> apiGatewayResponseStructure = new HashMap<String, Object>();
apiGatewayResponseStructure.put("isBase64Encoded", false);
apiGatewayResponseStructure.put("statusCode", 200);
apiGatewayResponseStructure.put("body", response.getContentAsString());
apiGatewayResponseStructure.put("headers", Collections.singletonMap("foo", "bar"));
byte[] apiGatewayResponseBytes = mapper.toJson(apiGatewayResponseStructure);
System.out.println("!!!==> apiGatewayResponseStructure: " + apiGatewayResponseStructure);
StreamUtils.copy(apiGatewayResponseBytes, output);
System.out.println("!!!==> COPIED RESPONSE");
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright 2023-2023 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 org.springframework.web.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
public class ProxyServletConfig implements ServletConfig {
private final ServletContext servletContext;
public ProxyServletConfig(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public String getServletName() {
return "hello-oleg";
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(new ArrayList<String>());
}
@Override
public String getInitParameter(String name) {
return null;
}
}