Experimental Azure function adapter for HTTP trigger
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
----
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user