快速入门:在 Azure 中通过命令行创建 Java 函数

在本文中,我们使用命令行工具创建一个响应 HTTP 请求的 Java 函数。 在本地测试代码后,将代码部署到 Azure Functions 的无服务器环境。

如果 Maven 不是首选开发工具,请查看面向 Java 开发人员的类似教程:

完成本快速入门会从你的 Azure 帐户中扣取最多几美分的费用。

配置本地环境

在开始之前,必须满足以下条件:

安装 Azure Functions Core Tools

建议的 Core Tools 安装方法取决于本地开发计算机的操作系统。

以下步骤使用 Windows 安装程序 (MSI) 安装 Core Tools v4.x。 若要详细了解其他基于包的安装程序,请参阅 Core Tools 自述文件

基于Windows 版本下载并运行 Core Tools 安装程序:

如果之前使用 Windows 安装程序 (MSI) 在 Windows 上安装 Core Tools,则应在安装最新版本之前从“添加/移除程序”中卸载旧版本。

创建本地函数项目

在 Azure Functions 中,有一个函数项目是一个或多个单独函数(每个函数响应特定的触发器)的容器。 项目中的所有函数共享相同的本地和宿主配置。 在本部分,你将创建包含单个函数的函数项目。

  1. 在空的文件夹中,运行以下命令以从 Maven archetype 生成 Functions 项目。

    mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure -DarchetypeArtifactId=azure-functions-archetype -DjavaVersion=8
    

    重要

    • 如果希望函数在 Java 11 上运行,请使用 -DjavaVersion=11。 若要了解详细信息,请参阅 Java 版本
    • 要完成本文中的步骤,JAVA_HOME 环境变量必须设置为正确版本的 JDK 的安装位置。
  2. Maven 会请求你提供所需的值,以在部署上完成项目的生成。
    系统提示时提供以下值:

    Prompt 说明
    groupId com.fabrikam 一个值,用于按照 Java 的包命名规则在所有项目中标识你的项目。
    artifactId fabrikam-functions 一个值,该值是 jar 的名称,没有版本号。
    version 1.0-SNAPSHOT 选择默认值。
    package com.fabrikam 一个值,该值是所生成函数代码的 Java 包。 使用默认值。
  3. 键入 Y 或按 Enter 进行确认。

    Maven 在名为 artifactId 的新文件夹(在此示例中为 fabrikam-functions)中创建项目文件。

  4. 导航到项目文件夹:

    cd fabrikam-functions
    

    此文件夹包含项目的各个文件,其中包括名为 local.settings.jsonhost.json 的配置文件。 由于 local.settings.json 可以包含从 Azure 下载的机密,因此,默认情况下,该文件会从 .gitignore 文件的源代码管理中排除。

(可选)检查文件内容

如果需要,可以跳到在本地运行函数并稍后再检查文件内容。

Function.java

Function.java 包含一个接收 request 变量中的请求数据的 run 方法,该变量是使用 HttpTrigger 注释修饰的 HttpRequestMessage,用于定义触发器行为。

/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See License.txt in the project root for
 * license information.
 */

package com.functions;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FixedDelayRetry;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

import java.io.File;
import java.nio.file.Files;
import java.util.Optional;

/**
 * Azure Functions with HTTP Trigger.
 */
public class Function {
    /**
     * This function listens at endpoint "/api/HttpExample". Two ways to invoke it using "curl" command in bash:
     * 1. curl -d "HTTP Body" {your host}/api/HttpExample
     * 2. curl "{your host}/api/HttpExample?name=HTTP%20Query"
     */
    @FunctionName("HttpExample")
    public HttpResponseMessage run(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }

    public static int count = 1;

    /**
     * This function listens at endpoint "/api/HttpExampleRetry". The function is re-executed in case of errors until the maximum number of retries occur.
     * Retry policies: https://docs.azure.cn/azure-functions/functions-bindings-error-pages?tabs=java
     */
    @FunctionName("HttpExampleRetry")
    @FixedDelayRetry(maxRetryCount = 3, delayInterval = "00:00:05")
    public HttpResponseMessage HttpExampleRetry(
        @HttpTrigger(
            name = "req",
            methods = {HttpMethod.GET, HttpMethod.POST},
            authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context) throws Exception {
        context.getLogger().info("Java HTTP trigger processed a request.");

        if(count<3) {
            count ++;
            throw new Exception("error");
        }

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            return request.createResponseBuilder(HttpStatus.OK).body(name).build();
        }
    }

    /**
     * This function listens at endpoint "/api/HttpTriggerJavaVersion".
     * It can be used to verify the Java home and java version currently in use in your Azure function
     */
    @FunctionName("HttpTriggerJavaVersion")
    public static HttpResponseMessage HttpTriggerJavaVersion(
        @HttpTrigger(
            name = "req",
            methods = {HttpMethod.GET, HttpMethod.POST},
            authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java HTTP trigger processed a request.");
        final String javaVersion = getJavaVersion();
        context.getLogger().info("Function - HttpTriggerJavaVersion" + javaVersion);
        return request.createResponseBuilder(HttpStatus.OK).body("HttpTriggerJavaVersion").build();
    }

    public static String getJavaVersion() {
        return String.join(" - ", System.getProperty("java.home"), System.getProperty("java.version"));
    }

    /**
     * This function listens at endpoint "/api/StaticWebPage".
     * It can be used to read and serve a static web page.
     * Note: Read the file from the right location for local machine and azure portal usage.
     */
    @FunctionName("StaticWebPage")
    public HttpResponseMessage getStaticWebPage(
        @HttpTrigger(
            name = "req",
            methods = {HttpMethod.GET, HttpMethod.POST},
            authLevel = AuthorizationLevel.ANONYMOUS)
        HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        File htmlFile = new File("index.html");
        try{
            byte[] fileContent = Files.readAllBytes(htmlFile.toPath());
            return request.createResponseBuilder(HttpStatus.OK).body(fileContent).build();
        }catch (Exception e){
            context.getLogger().info("Error reading file.");
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

响应消息由 HttpResponseMessage.Builder API 生成。

pom.xml

为托管应用而创建的 Azure 资源的设置在插件的 configuration 元素中使用生成的 pom.xml 文件中 com.microsoft.azure 的 groupId 定义。 例如,以下 configuration 元素指示基于 Maven 的部署在 chinanorth2 区域中的 java-functions-group 资源组内创建一个函数应用。 该函数应用本身在 Windows 上运行,后者托管在 java-functions-app-service-plan 计划(默认情况下是一个无服务器消耗计划)中。

                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <version>${azure.functions.maven.plugin.version}</version>
                <configuration>
                    <!-- function app name -->
                    <appName>${functionAppName}</appName>
                    <!-- function app resource group -->
                    <resourceGroup>${functionResourceGroup}</resourceGroup>
                    <!-- function app service plan name -->
                    <appServicePlanName>${functionServicePlan}</appServicePlanName>
                    <!-- function app region-->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions for all valid values -->
                    <region>${functionAppRegion}</region>
                    <!-- function pricingTier, default to be consumption if not specified -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
                    <pricingTier>${functionPricingTier}</pricingTier>
                    <!-- Whether to disable application insights, default is false -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details for all valid configurations for application insights-->
                    <!-- <disableAppInsights></disableAppInsights> -->

                    <runtime>
                        <!-- runtime os, could be windows, linux or docker-->
                        <os>windows</os>
                        <javaVersion>8</javaVersion>
                        <!-- for docker function, please set the following parameters -->
                        <!-- <image>[hub-user/]repo-name[:tag]</image> -->
                        <!-- <serverId></serverId> -->
                        <!-- <registryUrl></registryUrl>  -->
                    </runtime>
                    <appSettings>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~4</value>
                        </property>
                    </appSettings>
                </configuration>
                <executions>
                    <execution>
                        <id>package-functions</id>
                        <goals>
                            <goal>package</goal>
                        </goals>

若要控制在 Azure 中创建资源的方式,可以更改这些设置,例如,在初始部署之前将 runtime.oswindows 更改为 linux。 有关 Maven 插件支持的设置的完整列表,请参阅配置详细信息

FunctionTest.java

原型还会为函数生成单元测试。 更改函数以便在项目中添加绑定或新函数时,也需要修改 FunctionTest.java 文件中的测试。

在本地运行函数

  1. 通过从 LocalFunctionProj 文件夹启动本地 Azure Functions 运行时主机来运行函数:

    mvn clean package
    mvn azure-functions:run
    

    在输出的末尾,应显示以下行:

     ...
    
     Now listening on: http://0.0.0.0:7071
     Application started. Press Ctrl+C to shut down.
    
     Http Functions:
    
             HttpExample: [GET,POST] http://localhost:7071/api/HttpExample
     ...
    
     

    注意

    如果 HttpExample 未按如上所示出现,则可能是在项目的根文件夹外启动了主机。 在这种情况下,请按 Ctrl+C 停止主机,导航到项目的根文件夹,然后重新运行上一命令。

  2. 将此输出中 HttpExample 函数的 URL 复制到浏览器,并追加查询字符串 ?name=<YOUR_NAME>,使完整 URL 类似于 http://localhost:7071/api/HttpExample?name=Functions。 浏览器应显示回显查询字符串值的消息。 当你发出请求时,启动项目时所在的终端还会显示日志输出。

  3. 完成后,请按 Ctrl+C 并选择 y 以停止函数主机 。

将函数项目部署到 Azure

首次部署函数项目时,会在 Azure 中创建一个函数应用和相关的资源。 为托管应用而创建的 Azure 资源的设置在 pom.xml 文件中定义。 在本文中,我们将接受默认值。

提示

若要创建在 Linux 而不是 Windows 上运行的函数应用,请将 pom.xml 文件中的 runtime.os 元素从 windows 更改为 linux这些区域支持在消耗计划中运行 Linux。 不能在同一个资源组中同时创建在 Linux 上运行的应用和在 Windows 上运行的应用。

  1. 在部署之前,请使用 Azure CLI 或 Azure PowerShell 登录到 Azure 订阅。

    az login
    

    使用 az login 命令登录到 Azure 帐户。

  2. 使用以下命令将项目部署到新的函数应用。

    mvn azure-functions:deploy
    

    这会在 Azure 中创建以下资源:

    • 资源组。 命名为 java-functions-group。
    • 存储帐户。 Functions 所需。 此名称根据存储帐户名称要求随机生成。
    • 托管计划。 在 chinanorth2 区域中为函数应用提供无服务器托管。 名称为 java-functions-app-service-plan。
    • 函数应用。 函数应用是函数的部署和执行单元。 名称根据 artifactId 随机生成,其后面追加了一个随机生成的数字。

    部署会打包项目文件,并使用 zip 部署将其部署到新的函数应用。 此代码从 Azure 中的部署包运行。

重要

存储帐户用于存储重要的应用数据,有时包括应用程序代码本身。 应限制其他应用和用户对存储帐户的访问。

在 Azure 上调用函数

由于函数使用 HTTP 触发器,因此,可以通过在浏览器中或使用 curl 等工具,向此函数的 URL 发出 HTTP 请求来调用它。

将 publish 命令的输出中显示的完整“调用 URL”复制到浏览器的地址栏,并追加查询参数 ?name=Functions。 浏览器显示的输出应与本地运行函数时显示的输出类似。

The output of the function run on Azure in a browser

运行以下命令以查看准实时流式处理日志

func azure functionapp logstream <APP_NAME> 

在单独的终端窗口或浏览器中,再次调用远程函数。 终端中显示了 Azure 中函数执行的详细日志。

清理资源

若要继续执行下一步并添加 Azure 存储队列输出绑定,请保留所有资源,以备将来使用。

否则,请使用以下命令删除资源组及其包含的所有资源,以免产生额外的费用。

az group delete --name java-functions-group

后续步骤