diff --git a/spring-cloud-function-adapters/pom.xml b/spring-cloud-function-adapters/pom.xml index 431c83115..19f922f81 100644 --- a/spring-cloud-function-adapters/pom.xml +++ b/spring-cloud-function-adapters/pom.xml @@ -17,6 +17,7 @@ spring-cloud-function-adapter-aws spring-cloud-function-adapter-openwhisk + spring-cloud-function-adapter-azure diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/.jdk8 b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/README.adoc b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/README.adoc new file mode 100644 index 000000000..108e81ba0 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/README.adoc @@ -0,0 +1,75 @@ +This work is experimental. +This project provides an adapter layer for a Spring Cloud Function application onto Azure. +You can write an app with a single `@Bean` of type `Function` and it will be deployable in Azure if you get the JAR file laid out right. + +The adapter has a generic http request handler that you can use. +There is a `AzureSpringBootRequestHandler` which you must extend, and provide the input and output types as type parameters (enabling Azure to inspect the class and do the JSON conversions itself). + +If your app has more than one `@Bean` of type `Function` etc. then you can choose the one to use by configuring `function.name`. +The functions are extracted from the Spring Cloud `FunctionCatalog`. + +=== Notes on JAR Layout + +You don't need the Spring Cloud Function Web at runtime in Azure, so you need to exclude this before you create the JAR you deploy to Azure. +A function application on Azure has to be shaded, but a Spring Boot standalone application does not, so you can run the same app using 2 separate jars (as per the sample here). +The sample app creates the shaded jar file, with an `azure` classifier for deploying in Azure. + +== Build + +---- +./mvnw -U clean package +---- + +== Running the sample + +Before running the sample, we need to install a custom azure maven plugin. +Checkout this fork: https://github.com/sobychacko/azure-maven-plugins/tree/for-spring-boot-apps +---- +cd azure-functions-maven-plugin +mvn clean install -Dcheckstyle.skip=true -DskipTests -Dfindbugs.skip=true +---- + +Build the sample under `spring-cloud-function-samples/function-sample-azure`. + +---- +mvn clean package +---- + +Running Azure function locally. + +---- +mvn azure-functions:run + +On another terminal try this: curl localhost:7071/api/uppercase -d '{"value": "hello foobar"}' +---- + +Deploying the function that ran locally on Azure runtime. + +---- +az login + +mvn azure-functions:deploy + +On another terminal try this: curl https:///api/uppercase -d '{"value": "hello foobar!"}' + +Please ensure that you use the right URL for the function above. +---- + +Running the function as a standalone Spring Boot app + +Go to the samples project and uncomment `spring-cloud-function-web` dependency and `spring-boot-maven-plugin`. + +---- +mvn clean package +java -jar target/ + +On another terminal: curl -H "Content-Type: text/plain" localhost:8080/function -d '{"value": "hello foobar"}' +---- + +The input type for the function in the Azure sample is a Foo with a single property called "value". So you would need this to test it with something as below. + +---- +{ + "value": "foobar" +} +---- \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml new file mode 100644 index 000000000..7f15cd126 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + spring-cloud-function-adapter-azure + jar + + spring-cloud-function-adapter-aws + Azure Function Adapter for Spring Cloud Function + + + org.springframework.cloud + spring-cloud-function-adapter-parent + 1.0.0.BUILD-SNAPSHOT + + + + UTF-8 + UTF-8 + 1.8 + 1.2.1 + + + + + org.springframework.cloud + spring-cloud-function-context + + + org.springframework.boot + spring-boot-starter + + + io.projectreactor + reactor-core + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.microsoft.azure + azure-functions-java-core + [1.0.0-beta-1,1.0.0) + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java new file mode 100644 index 000000000..5bec73e53 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 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 + * + * 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. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.microsoft.azure.serverless.functions.ExecutionContext; +import reactor.core.publisher.Flux; + +/** + * @author Soby Chacko + */ +public class AzureSpringBootRequestHandler extends AzureSpringFunctionInitializer { + + public Object handleRequest(I foo, ExecutionContext context) { + context.getLogger().info("Handler Java HTTP trigger processed a request."); + initialize(context); + + Object convertedEvent = convertEvent(foo); + Flux output = apply(extract(convertedEvent)); + return result(convertedEvent, output); + } + + protected Object convertEvent(I input) { + return input; + } + + private Flux extract(Object input) { + if (input instanceof Collection) { + return Flux.fromIterable((Iterable) input); + } + return Flux.just(input); + } + + private Object result(Object input, Flux output) { + List result = new ArrayList<>(); + for (Object value : output.toIterable()) { + result.add(value); + } + if (isSingleValue(input) && result.size()==1) { + return result.get(0); + } + return result; + } + + private boolean isSingleValue(Object input) { + return !(input instanceof Collection); + } + + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringFunctionInitializer.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringFunctionInitializer.java new file mode 100644 index 000000000..0fb0bdb1e --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringFunctionInitializer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2017 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 + * + * 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. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.jar.Manifest; + +import com.microsoft.azure.serverless.functions.ExecutionContext; +import reactor.core.publisher.Flux; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.core.FunctionCatalog; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * @author Soby Chacko + */ +public class AzureSpringFunctionInitializer implements Closeable { + + private Function, Flux> function; + + private AtomicBoolean initialized = new AtomicBoolean(); + + private Class configurationClass; + + @Autowired(required = false) + private FunctionCatalog catalog; + + private static ConfigurableApplicationContext context; + + public AzureSpringFunctionInitializer(Class configurationClass) { + this.configurationClass = configurationClass; + } + + public AzureSpringFunctionInitializer() { + this(getStartClass()); + } + + + @Override + public void close() throws IOException { + if (AzureSpringFunctionInitializer.context != null) { + AzureSpringFunctionInitializer.context.close(); + } + } + + @SuppressWarnings("unchecked") + protected void initialize(ExecutionContext ctxt) { + + ConfigurableApplicationContext context = AzureSpringFunctionInitializer.context; + + if (!this.initialized.compareAndSet(false, true)) { + return; + } + ctxt.getLogger().info("Initializing function"); + + if (context==null) { + synchronized (AzureSpringFunctionInitializer.class) { + if (context==null) { + SpringApplicationBuilder builder = new SpringApplicationBuilder( + configurationClass); + ClassUtils.overrideThreadContextClassLoader(AzureSpringFunctionInitializer.class.getClassLoader()); + + context = builder.web(false).run(); + AzureSpringFunctionInitializer.context = context; + } + } + + } + + context.getAutowireCapableBeanFactory().autowireBean(this); + String name = context.getEnvironment().getProperty("function.name"); + + if (name == null) { + name = "function"; + } + if (this.catalog == null) { + this.function = context.getBean(name, Function.class); + } + else { + Set functionNames = this.catalog.getFunctionNames(); + this.function = this.catalog.lookupFunction(functionNames.iterator().next()); + } + } + + protected Flux apply(Flux input) { + if (this.function != null) { + return function.apply(input); + } + throw new IllegalStateException("No function defined"); + } + + private static Class getStartClass() { + ClassLoader classLoader = org.springframework.cloud.function.adapter.azure.AzureSpringFunctionInitializer.class.getClassLoader(); + if (System.getenv("MAIN_CLASS") != null) { + return ClassUtils.resolveClassName(System.getenv("MAIN_CLASS"), classLoader); + } + try { + Class result = getStartClass( + Collections.list(classLoader.getResources("META-INF/MANIFEST.MF"))); + if (result == null) { + result = getStartClass(Collections + .list(classLoader.getResources("meta-inf/manifest.mf"))); + } + return result; + } + catch (Exception ex) { + return null; + } + } + + private static Class getStartClass(List list) { + for (URL url : list) { + try { + InputStream inputStream = url.openStream(); + try { + Manifest manifest = new Manifest(inputStream); + String startClass = manifest.getMainAttributes() + .getValue("Main-Class"); + if (startClass != null) { + Class aClass = ClassUtils.forName(startClass, + org.springframework.cloud.function.adapter.azure.AzureSpringFunctionInitializer.class.getClassLoader()); + SpringBootApplication declaredAnnotation = aClass.getDeclaredAnnotation(SpringBootApplication.class); + if (declaredAnnotation != null) { + return aClass; + } + } + } + finally { + inputStream.close(); + } + } + catch (Exception ex) { + } + } + return null; + } +} diff --git a/spring-cloud-function-samples/function-sample-azure/host.json b/spring-cloud-function-samples/function-sample-azure/host.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/host.json @@ -0,0 +1,2 @@ +{ +} diff --git a/spring-cloud-function-samples/function-sample-azure/local.settings.json b/spring-cloud-function-samples/function-sample-azure/local.settings.json new file mode 100644 index 000000000..1d9e4b780 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "AzureWebJobsDashboard": "" + } +} diff --git a/spring-cloud-function-samples/function-sample-azure/pom.xml b/spring-cloud-function-samples/function-sample-azure/pom.xml new file mode 100644 index 000000000..c3a99b80c --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/pom.xml @@ -0,0 +1,155 @@ + + + 4.0.0 + + io.spring.sample + function-sample-azure + 1.0.0.BUILD-SNAPSHOT + jar + + Azure Java Functions + + + org.springframework.boot + spring-boot-starter-parent + 1.5.8.RELEASE + + + + + UTF-8 + 1.8 + 1.8 + function-sample-azure-11262017190909 + westus + example.FooConfig + + + + + com.microsoft.azure + azure-functions-java-core + [1.0.0-beta-1,1.0.0) + + + + + + + + org.springframework.cloud + spring-cloud-function-adapter-azure + 1.0.0.BUILD-SNAPSHOT + + + io.projectreactor + reactor-core + 3.0.7.RELEASE + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.4.0 + test + + + + + + + + maven-resources-plugin + 3.0.2 + + + com.microsoft.azure + azure-functions-maven-plugin + 0.1.7-SNAPSHOT + + + + + + + com.microsoft.azure + azure-functions-maven-plugin + + java-functions-group + ${functionAppName} + ${functionAppRegion} + + + FUNCTIONS_EXTENSION_VERSION + beta + + + + + + package-functions + + package + + + + + + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + true + ${project.build.directory}/azure-functions/${functionAppName} + + + + ${project.basedir} + + host.json + local.settings.json + + + + + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + false + true + azure + ${project.build.directory}/azure-functions/${functionAppName} + + + + + + + + + + + diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java new file mode 100644 index 000000000..beb9f5bb0 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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 + * + * 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. + */ + +package example; + +import java.util.function.Function; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +class FooConfig { + + @Bean + public Function uppercase() { + return value -> new Bar(value.getValue().toUpperCase()); + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(FooConfig.class, args); + } +} + +class Foo { + + private String value; + + Foo() { + } + + public String lowercase() { + return value.toLowerCase(); + } + + public Foo(String value) { + this.value = value; + } + + public String uppercase() { + return value.toUpperCase(); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} + +class Bar { + + private String value; + + Bar() { + } + + public Bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/FooHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/FooHandler.java new file mode 100644 index 000000000..cbdfceabb --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/FooHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 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 + * + * 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. + */ + +package example; + +import com.microsoft.azure.serverless.functions.ExecutionContext; + +import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; + +/** + * @author Soby Chacko + */ +public class FooHandler extends AzureSpringBootRequestHandler { + + @SuppressWarnings("unchecked") + public Bar handleRequest(Foo foo, ExecutionContext context) { + context.getLogger().info("Invoking entry point method"); + return (Bar)super.handleRequest(foo,context); + } + +} diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml index 77df80170..3abdda8b4 100644 --- a/spring-cloud-function-samples/pom.xml +++ b/spring-cloud-function-samples/pom.xml @@ -19,6 +19,7 @@ function-sample-compiler function-sample-task function-sample-aws + function-sample-azure