forked from Azure/azure-functions-java-worker
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6d76ce1
commit 1f89c6e
Showing
88 changed files
with
383 additions
and
2,241 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,188 +1,119 @@ | ||
# Building Microsoft Azure Functions in Java | ||
# Contributing | ||
|
||
## Prerequisites | ||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [[email protected]](mailto:[email protected]) with any additional questions or comments. | ||
|
||
* JDK 8 | ||
* Maven | ||
* JAVA_HOME environment variable | ||
* Node | ||
* NPM Azure Functions CLI | ||
# Environment Setup | ||
|
||
## Programming Model | ||
## Maven | ||
|
||
Your Azure function should be a stateless method to process input and produce output. Although you are allowed to write instance methods, your function must not depend on any instance fields of the class. You need to make sure all the function methods are `public` accessible. | ||
* Run all maven commands under the root folder of this repository | ||
|
||
You can put multiple functions in one single project (or specifically speaking, one single jar). We strongly recommend you **not to** put your functions in separate jars (or `pom.xml`). | ||
## IntelliJ | ||
|
||
Typically an Azure function is invoked because of one trigger. Your function needs to process that trigger (sometimes with additional inputs) and gives one or more output values. | ||
* Import the root folder of this repository as an existing project in IntelliJ | ||
* Configure the Language level (under Project Structure -> Modules -> Sources) to 8 | ||
|
||
All the input and output bindings can be defined in `function.json` (not recommended), or in the Java method by using annotations (recommended). All the types and annotations used in this document are included in the `azure-functions-java-core` package. | ||
## Eclipse | ||
|
||
Here is an example for a simple Azure function written in Java: | ||
* Set workspace to the parent folder of this repository | ||
* Import the root folder of this repository as an existing Maven project in Eclipse | ||
* Configure the project Java compiler compliance level to 1.8 | ||
* Set the JRE libraries to JRE 1.8 | ||
* "Ignore optional compiler problems" in "Java Build Path" for "target/generated-sources/\*\*/\*.java" | ||
|
||
```Java | ||
package com.example; | ||
# Development Cycle | ||
|
||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
## Build | ||
|
||
public class MyClass { | ||
@FunctionName("echo") | ||
public static String echo(@HttpTrigger(name = "req", methods = { "post" }, authLevel = AuthorizationLevel.ANONYMOUS) String in) { | ||
return "Hello, " + in + "."; | ||
} | ||
} | ||
``` | ||
|
||
### Including 3rd Party Libraries | ||
|
||
Azure Functions supports the use of 3rd party libraries. By default, all dependencies specified in your project pom.xml file will be automatically bundled during the `mvn package` step. For libraries that are not specified as dependencies in the pom.xml file, you may place them in a `lib` directory in the function's root directory. These will then be deployed as part of your functions application. All dependencies that are placed in the `lib` directory will be added to the system class loader at runtime. | ||
This is a maven based project, thus you can use any command line tools or IDEs which support maven to build it. Here we will use command line as the example (you could configure your own development environment accordingly). | ||
|
||
## General Data Types | ||
To build the project, you just need to run one command from the root folder of this project: | ||
|
||
You are free to use all the data types in Java for the input and output data, including native types; customized POJO types and specialized Azure types defined in `azure-functions-java-core` package. And Azure Functions runtime will try its best to convert the actual input value to the type you need (for example, a `String` input will be treated as a JSON string and be parsed to a POJO type defined in your code). | ||
|
||
The POJO types you defined have the same accessible requirements as the function methods, it needs to be `public` accessible. While the POJO class fields are not; for example a JSON string `{ "x": 3 }` is able to be converted to the following POJO type: | ||
|
||
```Java | ||
public class MyData { | ||
private int x; | ||
} | ||
```sh | ||
mvn clean package | ||
``` | ||
|
||
Binary data is represented as `byte[]` or `Byte[]` in your Azure functions code. And make sure you specify `dataType = "binary"` in the corresponding triggers/bindings. | ||
|
||
Empty input values could be `null` as your functions argument, but a recommended way to deal with potential empty values is to use `Optional<T>` type. | ||
And the binary will be built to `"./azure-functions-java-worker/target/azure-functions-java-worker-<version>.jar"`. | ||
|
||
If you have updated the core interface (azure-functions-java-core), a `mvn clean install` is required for your test functions app to reference the latest core package. | ||
|
||
## Inputs | ||
## Debug | ||
|
||
Inputs are divided into two categories in Azure Functions: one is the trigger input and the other is the additional input. Trigger input is the input who triggers your function. And besides that, you may also want to get inputs from other sources (like a blob), that is the additional input. | ||
The Java worker alone is not enough to establish the functions app, we also need the support from [Azure Functions Host](https://github.com/Azure/azure-functions-host). You may either use a published host CLI or use the in-development host. But both of the methods require you to attach to the java process if you want a step-by-step debugging experience. | ||
|
||
Let's take the following code snippet as an example: | ||
### Published Host | ||
|
||
```Java | ||
package com.example; | ||
You can install the latest Azure functions CLI tool by: | ||
|
||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
|
||
public class MyClass { | ||
@FunctionName("echo") | ||
public static String echo( | ||
@HttpTrigger(name = "req", methods = { "put" }, authLevel = AuthorizationLevel.ANONYMOUS, route = "items/{id}") String in, | ||
@TableInput(name = "item", tableName = "items", partitionKey = "Example", rowKey = "{id}", connection = "AzureWebJobsStorage") MyObject obj | ||
) { | ||
return "Hello, " + in + " and " + obj.getKey() + "."; | ||
} | ||
|
||
public static class MyObject { | ||
public String getKey() { return this.RowKey; } | ||
private String RowKey; | ||
} | ||
} | ||
```sh | ||
npm install -g azure-functions-core-tools@core | ||
``` | ||
|
||
When this function is invoked, the HTTP request payload will be passed as the `String` for argument `in`; and one entry will be retrieved from the Azure Table Storage and be passed to argument `obj` as `MyObject` type. | ||
|
||
## Outputs | ||
|
||
Outputs can be expressed in return value or output parameters. If there is only one output, you are recommended to use the return value. For multiple outputs, you have to use **output parameters**. | ||
|
||
Return value is the simplest form of output, you just return the value of any type, and Azure Functions runtime will try to marshal it back to the actual type (such as an HTTP response). You could apply any *output annotations* to the function method (the `name` property of the annotation has to be `$return`) to define the return value output. | ||
By default, the binaries are located in `"<Home Folder>/.azurefunctions/bin"`. Copy the `"<Azure Functions Java Worker Root>/azure-functions-java-worker/target/azure-functions-java-worker-<version>.jar"` to `"<Home Folder>/.azurefunctions/bin/workers/java/azure-functions-java-worker.jar"`. And start it normally using: | ||
|
||
For example, a blob content copying function could be defined as the following code. `@StorageAccount` annotation is used here to prevent the duplicating of the `connection` property for both `@BlobTrigger` and `@BlobOutput`. | ||
|
||
```Java | ||
package com.example; | ||
|
||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
|
||
public class MyClass { | ||
@FunctionName("copy") | ||
@StorageAccount("AzureWebJobsStorage") | ||
@BlobOutput(name = "$return", path = "samples-output-java/{name}") | ||
public static String copy(@BlobTrigger(name = "blob", path = "samples-input-java/{name}") String content) { | ||
return content; | ||
} | ||
} | ||
``` | ||
|
||
To produce multiple output values, use `OutputBinding<T>` type defined in the `azure-functions-java-core` package. If you need to make an HTTP response and push a message to a queue, you can write something like: | ||
```sh | ||
func start | ||
``` | ||
|
||
```Java | ||
package com.example; | ||
### Latest Host | ||
|
||
import com.microsoft.azure.serverless.functions.*; | ||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
A developer may also use the latest host code by cloning the git repository [Azure Functions Host](https://github.com/Azure/azure-functions-host). Now you need to navigate to the root folder of the host project and build it through: | ||
|
||
public class MyClass { | ||
@FunctionName("push") | ||
public static String push( | ||
@HttpTrigger(name = "req", methods = { "post" }, authLevel = AuthorizationLevel.ANONYMOUS) String body, | ||
@QueueOutput(name = "message", queueName = "myqueue", connection = "AzureWebJobsStorage") OutputBinding<String> queue | ||
) { | ||
queue.setValue("This is the queue message to be pushed"); | ||
return "This is the HTTP response content"; | ||
} | ||
} | ||
```sh | ||
dotnet restore WebJobs.Script.sln | ||
dotnet build WebJobs.Script.sln | ||
``` | ||
|
||
Of course you could use `OutputBinding<byte[]>` type to make a binary output value (for parameters); for return values, just use `byte[]`. | ||
After the build succeeded, set the environment variable `"AzureWebJobsScriptRoot"` to the root folder path (the folder which contains the `host.json`) of your test functions app; and copy the `"<Azure Functions Java Worker Root>/azure-functions-java-worker/target/azure-functions-java-worker-<version>.jar"` to `"<Azure Functions Host Root>/src/WebJobs.Script.WebHost/bin/Debug/netcoreapp2.0/workers/java/azure-functions-java-worker.jar"`. Now it's time to start the host: | ||
|
||
```sh | ||
dotnet ./src/WebJobs.Script.WebHost/bin/Debug/netcoreapp2.0/Microsoft.Azure.WebJobs.Script.WebHost.dll | ||
``` | ||
|
||
## Execution Context | ||
|
||
You interact with Azure Functions execution environment via the `ExecutionContext` object defined in the `azure-functions-java-core` package. You are able to get the invocation ID, the function name and a built-in logger (which is integrated prefectly with Azure Function Portal experience as well as AppInsights) from the context object. | ||
|
||
What you need to do is just add one more `ExecutionContext` typed parameter to your function method. Let's take a timer triggered function as an example: | ||
> Note: Remember to remove `"AzureWebJobsScriptRoot"` environment variable after you have finished debugging, because it will also influence the `func` CLI tool. | ||
```Java | ||
package com.example; | ||
## Generate JavaDoc | ||
|
||
import com.microsoft.azure.serverless.functions.*; | ||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
Simply using the following command to do so (if there are dependency errors, run `mvn clean install` beforehand): | ||
|
||
public class MyClass { | ||
@FunctionName("heartbeat") | ||
public static void heartbeat( | ||
@TimerTrigger(name = "schedule", schedule = "*/30 * * * * *") String timerInfo, | ||
ExecutionContext context | ||
) { | ||
context.getLogger().info("Heartbeat triggered by " + context.getFunctionName()); | ||
} | ||
} | ||
```sh | ||
mvn javadoc:javadoc | ||
``` | ||
|
||
# Coding Convention | ||
|
||
## Specialized Data Types | ||
## Version Management | ||
|
||
### HTTP Request and Response | ||
Our version strategy just follows the maven package version convention: `<major>.<minor>.<hotfix>-<prerelease>`, where: | ||
|
||
Sometimes a function need to take a more detailed control of the input and output, and that's why we also provide some specialized types in the `azure-functions-java-core` package for you to manipulate: | ||
* `<major>`: Increasing when incompatible breaking changes happened | ||
* `<minor>`: Increasing when new features added | ||
* `<hotfix>`: Increasing when a hotfix is pushed | ||
* `<prerelease>`: A string representing a pre-release version | ||
|
||
| Specialized Type | Target | Typical Usage | | ||
| ------------------------ | :-----------------: | ------------------------------ | | ||
| `HttpRequestMessage<T>` | HTTP Trigger | Get method, headers or queries | | ||
| `HttpResponseMessage<T>` | HTTP Output Binding | Return status other than 200 | | ||
**Use `SNAPSHOT` pre-release tag for packages under development**. Here is the sample workflow: | ||
|
||
### Metadata | ||
1. Initially the package version is `1.0-SNAPSHOT`. *There is no hotfix for SNAPSHOT* | ||
2. Modify the version to `1.0.0-ALPHA` for internal testing purpose. *Notice the hotfix exists here* | ||
3. After several BUG fixes, update the version to `1.0.0`. | ||
4. Create a new development version `1.1-SNAPSHOT`. | ||
5. Make a new hotfix into `1.0-SNAPSHOT`, and release to version `1.0.1`. | ||
6. New features are added to `1.1-SNAPSHOT`. | ||
|
||
Metadata comes from different sources, like HTTP headers, HTTP queries, and [trigger metadata](https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings#trigger-metadata-properties). You can use `@BindingName` annotation together with the metadata name to get the value. | ||
Every time you release a non-development version (like `1.0.0-ALPHA` or `1.0.1`), you also need to update the tag in your git repository. | ||
|
||
For example, the `queryValue` in the following code snippet will be `"test"` if the requested URL is `http://{example.host}/api/metadata?name=test`. | ||
# Advanced Java Concepts | ||
|
||
```Java | ||
package com.example; | ||
## Reflection for Type | ||
|
||
import java.util.Optional; | ||
import com.microsoft.azure.serverless.functions.annotation.*; | ||
Primitives have two different type definitions, for example: `int.class` (which is identical to `Integer.TYPE`) is not `Integer.class`. | ||
|
||
public class MyClass { | ||
@FunctionName("metadata") | ||
public static String metadata( | ||
@HttpTrigger(name = "req", methods = { "get", "post" }, authLevel = AuthorizationLevel.ANONYMOUS) Optional<String> body, | ||
@BindingName("name") String queryValue | ||
) { | ||
return body.orElse(queryValue); | ||
} | ||
} | ||
``` | ||
All Java types are represented by `Type` interface, which may be one of the following implementations: | ||
* `Class<?>`: normal class type like `String` | ||
* `ParameterizedType`: generic class type like `List<Integer>` | ||
* `WildcardType`: generic argument contains question mark like `? extends Number` | ||
* `TypeVariable<?>`: generic argument like `T` | ||
* `GenericArrayType`: generic array like `T[]` | ||
|
||
For the generic type behaviors (including compile-time validation and runtime type erasure) in Java, please refer to *[Generics in the Java Programming Language | ||
](https://www.cs.rice.edu/~cork/312/Readings/GenericsTutorial.pdf)*. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.