diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/README.md b/spring-cloud-function-samples/function-sample-supplier-exporter/README.md new file mode 100644 index 000000000..560073481 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/README.md @@ -0,0 +1,16 @@ +AWS Lambda custom runtime. + +``` +$ ./build.sh +$ ./mvnw package -P native +``` + +builds a native-zip ZIP file in target. Upload it to AWS and set the handler to "foobar". + +To test locally, run the `TestServer` and then the `DemoApplication` (either in a JVM or natively). Then POST some data into the test server: + +``` +$ curl localhost:8000/add -d world -H "Content-Type: text/plain" +``` + +There is a unit test that does the same thing. Also the `build.sh` script orchestrates the same test for the native image. diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/application.properties b/spring-cloud-function-samples/function-sample-supplier-exporter/application.properties new file mode 100644 index 000000000..175231985 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/application.properties @@ -0,0 +1,5 @@ +# Useful for running locally (hence not in src/main/resources) +spring.cloud.function.web.export.source.url=http://localhost:8000/home +spring.cloud.function.web.export.sink.url=http://localhost:8000/echo +spring.cloud.function.web.export.debug=true +logging.level.org.springframework=DEBUG \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/build.sh b/spring-cloud-function-samples/function-sample-supplier-exporter/build.sh new file mode 100644 index 000000000..6b4a373a2 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +BLUE='\033[0;34m' +NC='\033[0m' + +printf "=== ${BLUE}Building %s sample${NC} ===\n" "${PWD##*/}" + +./compile.sh || exit 1 + +JARDIR=target/native-image +java -cp $JARDIR/BOOT-INF/lib/*:$JARDIR/BOOT-INF/classes:$JARDIR:target/test-classes com.example.test.TestServer & +SPID=$! +sleep 5 + +${PWD%/*samples/*}/scripts/test.sh --spring.cloud.function.web.export.source.url=http://localhost:8000/home --spring.cloud.function.web.export.sink.url=http://localhost:8000/echo + +kill $SPID diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/compile.sh b/spring-cloud-function-samples/function-sample-supplier-exporter/compile.sh new file mode 100644 index 000000000..ec45e4d25 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/compile.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +ARTIFACT=function-aws +MAINCLASS=com.example.demo.DemoApplication +VERSION=0.0.1-SNAPSHOT +FEATURE=../../../../spring-graal-native/target/spring-graal-native-0.7.0.BUILD-SNAPSHOT.jar + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +rm -rf target +mkdir -p target/native-image + +echo "Packaging $ARTIFACT with Maven" +mvn -DskipTests package > target/native-image/output.txt + +JAR="$ARTIFACT-$VERSION.jar" +rm -f $ARTIFACT +echo "Unpacking $JAR" +cd target/native-image +jar -xvf ../$JAR >/dev/null 2>&1 +cp -R META-INF BOOT-INF/classes + +LIBPATH=`find BOOT-INF/lib | tr '\n' ':'` +CP=BOOT-INF/classes:$LIBPATH:$FEATURE + +if [ ! -f "$FEATURE" ]; then + printf "${RED}FAILURE${NC}: $FEATURE does not exist, please build the root project before building this sample.\n" + exit 1 +fi + +GRAALVM_VERSION=`native-image --version` +echo "Compiling $ARTIFACT with $GRAALVM_VERSION" +{ time native-image \ + --verbose \ + --no-server \ + --no-fallback \ + --initialize-at-build-time \ + -H:+PrintMethodHistogram \ + -H:+TraceClassInitialization \ + -H:Name=$ARTIFACT \ + -H:+ReportExceptionStackTraces \ + -Dspring.graal.remove-unused-autoconfig=true \ + -Dspring.graal.remove-yaml-support=true \ + -cp $CP $MAINCLASS >> output.txt ; } 2>> output.txt + +if [[ -f $ARTIFACT ]] +then + printf "${GREEN}SUCCESS${NC}\n" + mv ./$ARTIFACT .. + exit 0 +else + cat output.txt + printf "${RED}FAILURE${NC}: an error occurred when compiling the native-image.\n" + exit 1 +fi + diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml b/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml new file mode 100644 index 000000000..6ae6d240e --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.3.0.M4 + + + com.example + function-sample-aws-supplier-exporter + 1.0.0.RELEASE + function-sample-aws-supplier-exporter + Demo project for Spring Boot + + + 1.8 + 3.1.0.BUILD-SNAPSHOT + Dysprosium-SR6 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + io.netty + netty-transport-native-epoll + + + io.netty + netty-codec-http2 + + + + + org.springframework.cloud + spring-cloud-function-web + + + org.springframework.cloud + spring-cloud-function-adapter-aws + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud-function.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + spring-release + Spring release + https://repo.spring.io/release + + false + + + + spring-snapshot + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestone + Spring Milestone + https://repo.spring.io/milestone + + false + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + spring-release + Spring release + https://repo.spring.io/release + + false + + + + spring-snapshot + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestone + Spring Milestone + https://repo.spring.io/milestone + + false + + + + + diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/java/com/example/demo/DemoApplication.java b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 000000000..d41f097c4 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,55 @@ +package com.example.demo; + +import java.util.function.Function; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.cloud.function.context.FunctionRegistration; +import org.springframework.cloud.function.context.FunctionType; +import org.springframework.cloud.function.context.FunctionalSpringApplication; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; + +@SpringBootConfiguration(proxyBeanMethods = false) +public class DemoApplication + implements ApplicationContextInitializer { + + public static void main(String[] args) { + FunctionalSpringApplication.run(DemoApplication.class, args); + } + + @Override + public void initialize(GenericApplicationContext context) { + context.registerBean("foobar", FunctionRegistration.class, + () -> new FunctionRegistration<>(new Foobar()) + .type(FunctionType.from(Foo.class).to(Foo.class))); + } + +} + +class Foo { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Foo(String name) { + this.name = name; + } + + Foo() { + } +} + +class Foobar implements Function { + + @Override + public Foo apply(Foo input) { + System.err.println("HI: " + input.getName()); + return new Foo("hi " + input.getName() + "!"); + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/META-INF/native-image/reflect-config.json b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..c5f6007b7 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,7 @@ +[ + { + "name": "com.example.demo.Foo", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } +] \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/application.properties b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/application.properties new file mode 100644 index 000000000..9e8695a2c --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.cloud.function.web.export.enabled=true +spring.cloud.function.web.export.debug=true +spring.main.web-application-type=none +logging.level.org.springframework.cloud=DEBUG \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/demo/DemoApplicationTests.java b/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 000000000..5cce20b94 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,55 @@ +package com.example.demo; + +import com.example.test.TestServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.cloud.function.context.test.FunctionalSpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.SocketUtils; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@FunctionalSpringBootTest({"spring.cloud.function.web.export.source.url=http://localhost:${export.port}/home", + "spring.cloud.function.web.export.sink.url=http://localhost:${export.port}/echo", + "logging.level.reactor=OFF", + "logging.level.io.netty=OFF"}) +public class DemoApplicationTests { + + static ConfigurableApplicationContext context; + + @Value("${export.port}") + private int port; + + @Autowired + private WebClient.Builder builder; + + @Test + public void contextLoads() throws Exception { + WebClient client = builder.baseUrl("http://localhost:" + port).build(); + client.post().uri("/add").bodyValue("{\"name\":\"Fred\"}").exchange().block(); + Thread.sleep(1000L); + String response = client.get().uri("/take").exchange().block().bodyToMono(String.class).block(); + assertThat(response).isEqualTo("{\"name\":\"hi Fred!\"}"); + } + + @AfterAll + static void after() { + if (context != null) { + context.close(); + } + } + + @BeforeAll + static void before() { + int port = SocketUtils.findAvailableTcpPort(); + System.setProperty("export.port", "" + port); + context = SpringApplication.run(TestServer.class, "--server.port="+port, "--spring.cloud.function.web.export.enabled=false", "--spring.main.web-application-type=reactive"); + } + +} diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/test/TestServer.java b/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/test/TestServer.java new file mode 100644 index 000000000..7af07eb6d --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/src/test/java/com/example/test/TestServer.java @@ -0,0 +1,85 @@ +package com.example.test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.context.annotation.Bean; + +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +@SpringBootApplication(proxyBeanMethods = false) +public class TestServer { + + private MonoProcessor output = MonoProcessor.create(); + + private String response = ""; + + public static void main(String[] args) { + Set list = new LinkedHashSet<>(Arrays.asList(args)); + list.addAll(Arrays.asList("--server.port=8000", "--spring.cloud.function.web.export.enabled=false", "--spring.main.web-application-type=reactive")); + SpringApplication.run(TestServer.class, list.toArray(new String[0])); + } + + @Bean + public Supplier> home() { + return () -> output; + } + +// @Bean +// public Function echo(JsonMapper mapper) { +// return input -> { +// response = input; +// return "Echo: " + response; +// }; +// } + + @Bean + public Function echo(JsonMapper mapper) { + return input -> { + System.out.println("===> POJO " + input); + response = new String(mapper.toJson(input)); + return "Echo: " + response; + }; + } + + @Bean + public Function add() { + return input -> { + System.err.println("Add: " + input); + output.onNext(input); + output = MonoProcessor.create(); + return "Added: " + input; + }; + } + + @Bean + public Supplier take() { + return () -> response; + } + +} + +class Foo { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Foo(String name) { + this.name = name; + } + + Foo() {} +} diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/verify.sh b/spring-cloud-function-samples/function-sample-supplier-exporter/verify.sh new file mode 100644 index 000000000..3d4560817 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/verify.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +curl -s localhost:8000/add -d '{"name":"world"}' -H "Content-Type: text/plain" +echo +echo Waiting... +sleep 1 +RESPONSE=`curl -s localhost:8000/take` +echo Got response: $RESPONSE +if [[ "$RESPONSE" == '{"name":"hi world!"}' ]]; then + exit 0 +else + exit 1 +fi diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml index 9bad96a2a..f98633316 100644 --- a/spring-cloud-function-samples/pom.xml +++ b/spring-cloud-function-samples/pom.xml @@ -22,6 +22,7 @@ function-sample-task function-sample-aws function-sample-aws-custom + function-sample-supplier-exporter function-sample-azure function-sample-spring-integration function-sample-gcp