Experimental Azure function adapter for HTTP trigger

This commit is contained in:
Soby Chacko
2017-11-26 20:50:09 -05:00
committed by Dave Syer
parent 73098244e3
commit 308e4d5514
12 changed files with 679 additions and 0 deletions

View File

@@ -17,6 +17,7 @@
<modules>
<module>spring-cloud-function-adapter-aws</module>
<module>spring-cloud-function-adapter-openwhisk</module>
<module>spring-cloud-function-adapter-azure</module>
</modules>
</project>

View File

@@ -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://<azure-function-url-from-the-log>/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/<spring boot uber jar>
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"
}
----

View File

@@ -0,0 +1,89 @@
<?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>
<artifactId>spring-cloud-function-adapter-azure</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-function-adapter-aws</name>
<description>Azure Function Adapter for Spring Cloud Function</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-parent</artifactId>
<version>1.0.0.BUILD-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>
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-java-core</artifactId>
<version>[1.0.0-beta-1,1.0.0)</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

View File

@@ -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<I,O> 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<Object> 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);
}
}

View File

@@ -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<?>, 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<String> 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<URL> 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;
}
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,7 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"AzureWebJobsDashboard": ""
}
}

View File

@@ -0,0 +1,155 @@
<?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>io.spring.sample</groupId>
<artifactId>function-sample-azure</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Azure Java Functions</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<functionAppName>function-sample-azure-11262017190909</functionAppName>
<functionAppRegion>westus</functionAppRegion>
<start-class>example.FooConfig</start-class>
</properties>
<dependencies>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-java-core</artifactId>
<version>[1.0.0-beta-1,1.0.0)</version>
</dependency>
<!--<dependency>-->
<!--<groupId>org.springframework.cloud</groupId>-->
<!--<artifactId>spring-cloud-function-web</artifactId>-->
<!--<version>1.0.0.BUILD-SNAPSHOT</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-azure</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>0.1.7-SNAPSHOT</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<configuration>
<resourceGroup>java-functions-group</resourceGroup>
<appName>${functionAppName}</appName>
<region>${functionAppRegion}</region>
<appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>beta</value>
</property>
</appSettings>
</configuration>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<overwrite>true</overwrite>
<outputDirectory>${project.build.directory}/azure-functions/${functionAppName}
</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}</directory>
<includes>
<include>host.json</include>
<include>local.settings.json</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!--<plugin>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-maven-plugin</artifactId>-->
<!--</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>azure</shadedClassifierName>
<outputDirectory>${project.build.directory}/azure-functions/${functionAppName}</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<Foo, Bar> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -19,6 +19,7 @@
<module>function-sample-compiler</module>
<module>function-sample-task</module>
<module>function-sample-aws</module>
<module>function-sample-azure</module>
</modules>
<build>