使用命令行工具将 Azure Functions 连接到 Azure 存储

本文介绍如何将 Azure 存储队列与在前一篇快速入门中创建的函数和存储帐户相集成。 可以使用一个输出绑定来实现这种集成。该绑定可将 HTTP 请求中的数据写入队列中的消息。 除了在前一篇快速入门中提到的几美分费用以外,完成本文不会产生其他费用。 有关绑定的详细信息,请参阅 Azure Functions 触发器和绑定的概念

配置本地环境

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

在开始之前,必须完成文章快速入门:从命令行创建 Azure Functions 项目。 如果在该文章结束时清理了资源,请再次执行相应的步骤,以在 Azure 中重新创建函数应用和相关资源。

检索 Azure 存储连接字符串

前面你已创建一个供函数应用使用的 Azure 存储帐户。 此帐户的连接字符串安全存储在 Azure 中的应用设置内。 将设置下载到 local.settings.json 文件中后,在本地运行函数时,可以在同一帐户中使用该连接写入存储队列。

  1. 从项目的根目录中运行以下命令,并将 <APP_NAME> 替换为上一步中所用的函数应用名称。 此命令将覆盖该文件中的任何现有值。

    func azure functionapp fetch-app-settings <APP_NAME>
    
  2. 打开 local.settings.json 文件,找到名为 AzureWebJobsStorage 的值,即存储帐户连接字符串。 在本文的其他部分,将使用名称 AzureWebJobsStorage 和该连接字符串。

重要

由于 local.settings.json 文件包含从 Azure 下载的机密,因此请始终从源代码管理中排除此文件。 连同本地函数项目一起创建的 .gitignore 文件默认会排除该文件。

注册绑定扩展

绑定(HTTP 和计时器触发器除外)将实现为扩展包。 在终端窗口中运行以下 dotnet add package 命令,将存储扩展包添加到项目中。

dotnet add package Microsoft.Azure.WebJobs.Extensions.Storage 

现在,你可以将存储输出绑定添加到项目。

将输出绑定定义添加到函数

尽管一个函数只能有一个触发器,但它可以有多个输入和输出绑定,因此,你无需编写自定义集成代码就能连接到其他 Azure 服务和资源。

在函数文件夹中的 function.json 文件内声明这些绑定。 在前一篇快速入门中,HttpExample 文件夹中的 function.json 文件在 集合中包含两个绑定:

    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "get",
                "post"
            ]
        },
        {
            "type": "http",
            "direction": "out",
            "name": "res"
        }
    ]
    "scriptFile": "__init__.py",
    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "get",
                "post"
            ]
        },
        {
            "type": "http",
            "direction": "out",
            "name": "$return"
        }
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "Request",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "Response"
    }
  ]

每个绑定至少有一个类型、一个方向和一个名称。 在以上示例中,第一个绑定的类型为 httpTrigger,方向为 in。 对于 in 方向,name 指定在触发器调用函数时,要发送到该函数的输入参数的名称。

集合中的第二个绑定名为 res。 此 http 绑定是用于写入 HTTP 响应的输出绑定 (out)。

若要从此函数写入 Azure 存储队列,请添加类型为 queue、名称为 msgout 绑定,如以下代码所示:

    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "type": "queue",
      "direction": "out",
      "name": "msg",
      "queueName": "outqueue",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

集合中第二个绑定的类型为 http,方向为 out,在本例中,$return 的特殊 name 指示此绑定使用函数的返回值,而不是提供输入参数。

若要从此函数写入 Azure 存储队列,请添加类型为 queue、名称为 msgout 绑定,如以下代码所示:

  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    },
    {
      "type": "queue",
      "direction": "out",
      "name": "msg",
      "queueName": "outqueue",
      "connection": "AzureWebJobsStorage"
    }
  ]

集合中的第二个绑定名为 res。 此 http 绑定是用于写入 HTTP 响应的输出绑定 (out)。

若要从此函数写入 Azure 存储队列,请添加类型为 queue、名称为 msgout 绑定,如以下代码所示:

    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "Request",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "Response"
    },
    {
      "type": "queue",
      "direction": "out",
      "name": "msg",
      "queueName": "outqueue",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

在这种情况下,msg 将作为输出参数提供给函数。 对于 queue 类型,还必须在 queueName 中指定队列的名称,并在 connection 中提供 Azure 存储连接的名称(来自 local.settings.json 文件)。

在 C# 项目中,绑定被定义为函数方法上的绑定属性。 具体定义取决于应用是在进程内(C# 类库)还是在隔离进程中运行。

打开 HttpExample.cs 项目文件,并将以下参数添加到 方法定义中:

            [Queue("outqueue"),StorageAccount("AzureWebJobsStorage")] ICollector<string> msg, 

msg 参数为一个 ICollector<T> 类型,表示函数完成时写入到输出绑定的消息集合。 在这种情况下,输出是名为的 outqueue 存储队列。 StorageAccountAttribute 设置存储帐户的连接字符串。 此属性指示包含存储帐户连接字符串的设置,可以在类、方法或参数级别应用。 在本例中,可以省略 StorageAccountAttribute,因为你已使用默认存储帐户。

Run 方法定义现在必须如以下代码所示:

        [FunctionName("HttpExample")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, 
            [Queue("outqueue"),StorageAccount("AzureWebJobsStorage")] ICollector<string> msg, 
            ILogger log)

在 Java 项目中,绑定被定义为函数方法上的绑定注释。 然后根据这些注释自动生成 function.json 文件。

浏览到函数代码在 src/main/java 下的位置,打开 Function.java 项目文件,然后将以下参数添加到 方法定义:

@QueueOutput(name = "msg", queueName = "outqueue", connection = "AzureWebJobsStorage") OutputBinding<String> msg

msg 参数是 OutputBinding<T> 类型,该类型表示字符串的集合。 当函数完成时,这些字符串作为消息写入到输出绑定。 在这种情况下,输出是名为的 outqueue 存储队列。 存储帐户的连接字符串由 connection 方法设置。 请传递包含存储帐户连接字符串的应用程序设置,而不是传递连接字符串本身。

run 方法定义现在必定如以下示例所示:

@FunctionName("HttpTrigger-Java")
public HttpResponseMessage run(
        @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION)  
        HttpRequestMessage<Optional<String>> request, 
        @QueueOutput(name = "msg", queueName = "outqueue", connection = "AzureWebJobsStorage") 
        OutputBinding<String> msg, final ExecutionContext context) {
    ...
}

有关绑定的详细信息,请参阅 Azure Functions 触发器和绑定的概念队列输出配置

添加使用输出绑定的代码

定义队列绑定后,可以更新函数,以接收 msg 输出参数并将消息写入队列。

按以下代码所示更新 HttpExample\__init__.py,将 msg 参数添加到函数定义,并将 msg.set(name) 添加到 if name: 语句下:

import logging

import azure.functions as func


def main(req: func.HttpRequest, msg: func.Out[func.QueueMessage]) -> str:

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        msg.set(name)
        return func.HttpResponse(f"Hello {name}!")
    else:
        return func.HttpResponse(
            "Please pass a name on the query string or in the request body",
            status_code=400
        )

msg 参数是 azure.functions.Out class 的实例。 set 方法将字符串消息写入队列。 在本例中,此消息是在 URL 查询字符串中传递给函数的名称。

添加在 context.bindings 上使用 msg 输出绑定对象来创建队列消息的代码。 请在 context.res 语句之前添加此代码。

        // Add a message to the Storage queue,
        // which is the name passed to the function.
        context.bindings.msg = (req.query.name || req.body.name);

此时,你的函数一定如下所示:

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    if (req.query.name || (req.body && req.body.name)) {
        // Add a message to the Storage queue,
        // which is the name passed to the function.
        context.bindings.msg = (req.query.name || req.body.name);
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
};

添加在 context.bindings 上使用 msg 输出绑定对象来创建队列消息的代码。 请在 context.res 语句之前添加此代码。

        context.bindings.msg = name; 

此时,你的函数一定如下所示:

import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    const name = (req.query.name || (req.body && req.body.name));

    if (name) {
        // Add a message to the storage queue, 
        // which is the name passed to the function.
        context.bindings.msg = name; 
        // Send a "hello" response.
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
};

export default httpTrigger; 

添加使用 Push-OutputBinding cmdlet 通过 msg 输出绑定将文本写入队列的代码。 在 if 语句中设置“正常”状态之前,请添加此代码。

    $outputMsg = $name
    Push-OutputBinding -name msg -Value $outputMsg

此时,你的函数一定如下所示:

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."

# Interact with query parameters or the body of the request.
$name = $Request.Query.Name
if (-not $name) {
    $name = $Request.Body.Name
}

if ($name) {
    # Write the $name value to the queue, 
    # which is the name passed to the function.
    $outputMsg = $name
    Push-OutputBinding -name msg -Value $outputMsg

    $status = [HttpStatusCode]::OK
    $body = "Hello $name"
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "Please pass a name on the query string or in the request body."
}

# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

添加使用 msg 输出绑定对象来创建队列消息的代码。 请在方法返回之前添加此代码。

            if (!string.IsNullOrEmpty(name))
            {
                // Add a message to the output collection.
                msg.Add(name);
            }

此时,你的函数一定如下所示:

        [FunctionName("HttpExample")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, 
            [Queue("outqueue"),StorageAccount("AzureWebJobsStorage")] ICollector<string> msg, 
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            if (!string.IsNullOrEmpty(name))
            {
                // Add a message to the output collection.
                msg.Add(name);
            }
            return name != null
                ? (ActionResult)new OkObjectResult($"Hello, {name}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }

现在可以使用新的 msg 参数,从函数代码写入到输出绑定。 将以下代码行添加到成功响应之前,以便将 name 的值添加到 msg 输出绑定。

            msg.setValue(name);

使用输出绑定时,无需使用 Azure 存储 SDK 代码进行身份验证、获取队列引用或写入数据。 Functions 运行时和队列输出绑定将为你执行这些任务。

run 方法现在一定如以下示例所示:

    public HttpResponseMessage run(
            @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) 
            HttpRequestMessage<Optional<String>> request, 
            @QueueOutput(name = "msg", queueName = "outqueue", 
            connection = "AzureWebJobsStorage") OutputBinding<String> msg, 
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        String query = request.getQueryParameters().get("name");
        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 {
            // Write the name to the message queue. 
            msg.setValue(name);

            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }

更新测试

由于原型还创建一组测试,因此需更新这些测试,以便处理 run 方法签名中的新 msg 参数。

在 src/test/java 下浏览到你的测试代码所在位置,打开 Function.java 项目文件,将 //Invoke 下的代码行替换为以下代码:

        @SuppressWarnings("unchecked")
        final OutputBinding<String> msg = (OutputBinding<String>)mock(OutputBinding.class);
        final HttpResponseMessage ret = new Function().run(req, msg, context);

请注意,不需要编写任何用于身份验证、获取队列引用或写入数据的代码。 在 Azure Functions 运行时和队列输出绑定中可以方便地处理所有这些集成任务。

在本地运行函数

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

    func start
    

    在输出的末尾,必须要显示以下行:

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

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

提示

在启动过程中,主机会下载并安装存储绑定扩展和其他 Microsoft 绑定扩展。 之所以安装这些扩展,是因为默认情况下,已在 host.json 文件中使用以下属性启用了绑定扩展:

{
    "version": "2.0",
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[1.*, 2.0.0)"
    }
}

如果遇到任何与绑定扩展相关的错误,请检查上述属性是否在 host.json 中存在。

查看 Azure 存储队列中的消息

可以在 Azure 门户Azure 存储资源管理器中查看队列。 也可以按以下步骤中所述,在 Azure CLI 中查看队列:

  1. 打开函数项目的 local.setting.json 文件,并复制连接字符串值。 在终端或命令窗口中运行以下命令以创建名为 AZURE_STORAGE_CONNECTION_STRING 的环境变量,并粘贴特定的连接字符串来代替 <MY_CONNECTION_STRING>。 (创建此环境变量后,无需在使用 --connection-string 参数的每个后续命令中提供连接字符串。)

    export AZURE_STORAGE_CONNECTION_STRING="<MY_CONNECTION_STRING>"
    
  2. (可选)使用 az storage queue list 命令查看帐户中的存储队列。 此命令的输出一定包含名为 outqueue 的队列,该队列是函数将其第一条消息写入该队列时创建的。

    az storage queue list --output tsv
    
  3. 使用 az storage message get 命令从该队列中读取消息,这应是之前测试函数时提供的值。 该命令读取再删除该队列中的第一条消息。

    echo `echo $(az storage message get --queue-name outqueue -o tsv --query '[].{Message:content}') | base64 --decode`
    

    由于消息正文是以 base64 编码格式存储的,因此,必须解码消息才能显示消息。 执行 az storage message get 后,该消息将从队列中删除。 如果 outqueue 中只有一条消息,则再次运行此命令时不会检索到消息,而是收到错误。

将项目重新部署到 Azure

在本地验证函数已将消息写入 Azure 存储队列后,接下来可以重新部署项目以更新 Azure 上运行的终结点。

LocalFunctionsProj 文件夹中,使用 func azure functionapp publish 命令重新部署项目(请将 <APP_NAME> 替换为你的应用的名称)。

func azure functionapp publish <APP_NAME>

在本地项目文件夹中,使用以下 Maven 命令重新发布项目:

mvn azure-functions:deploy

在 Azure 中验证

  1. 像在上一篇快速入门中一样,使用浏览器或 CURL 来测试重新部署的函数。

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

  2. 按上一部分所述再次检查存储队列,验证它是否包含已写入到其中的新消息。

清理资源

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

az group delete --name AzureFunctionsQuickstart-rg

后续步骤

现已更新 HTTP 触发的函数,使其将数据写入存储队列。 现在,可以详细了解如何使用 Core Tools 和 Azure CLI 通过命令行进行 Functions 开发: