Dapr 软件开发工具包 (SDKs) 使用您喜欢的语言与 Dapr 一起工作
Dapr SDKs 是将 Dapr 集成到应用程序中的最简单方法。选择您喜欢的语言,几分钟内即可开始使用 Dapr。
SDK 包 选择您偏好的语言 以了解有关客户端、服务扩展、actor 和工作流包的更多信息。
客户端 : Dapr 客户端允许您调用 Dapr 构建块 API 并执行每个构建块的操作。服务扩展 : Dapr 服务扩展使您能够创建可被其他服务调用的服务并订阅主题。actor : Dapr actor SDK 允许您构建具有方法、状态、计时器和持久性提醒的虚拟 actor。工作流 : Dapr 工作流使您能够可靠地编写长时间运行的业务逻辑和集成。SDK 语言 进一步阅读 1 - Dapr .NET SDK 用于开发 Dapr 应用程序的 .NET SDK 包
Dapr 提供多种包以协助 .NET 应用程序的开发。通过这些包,您可以使用 Dapr 创建 .NET 客户端、服务器和虚拟 actor。
先决条件
注意 请注意,虽然 .NET 6 通常是 Dapr .NET SDK 包的最低 .NET 要求,并且 .NET 7 是 Dapr v1.15 中 Dapr.Workflows 的最低支持版本,但从 v1.16 开始,Dapr 仅支持 .NET 8 和 .NET 9。安装 要开始使用 Client .NET SDK,请安装 Dapr .NET SDK 包:
dotnet add package Dapr.Client
体验 尝试 Dapr .NET SDK。通过 .NET 快速入门和教程来探索 Dapr 的实际应用:
SDK 示例 描述 快速入门 使用 .NET SDK 在几分钟内体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始使用。 发布/订阅教程 查看 Dapr .NET SDK 如何与其他 Dapr SDK 一起工作以启用发布/订阅应用程序。
可用包 客户端 创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 .NET 客户端。
服务器 使用 Dapr SDK 编写 .NET 服务器和服务。包括对 ASP.NET 的支持。
Actors 在 .NET 中创建具有状态、提醒/计时器和方法的虚拟 actor。
工作流 创建和管理与其他 Dapr API 一起工作的工作流。
更多信息 了解更多关于本地开发选项的信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。
开发 了解 .NET Dapr 应用程序的本地开发选项
NuGet 包 用于将 .NET SDK 添加到您的 .NET 应用程序的 Dapr 包。
1.1 - 开始使用 Dapr 客户端 .NET SDK 如何使用 Dapr .NET SDK 快速上手
Dapr 客户端包使您能够从 .NET 应用程序与其他 Dapr 应用程序进行交互。
注意 如果您还没有这样做,
请尝试其中一个快速入门 ,以快速了解如何使用 Dapr .NET SDK 和 API 构建块。
构建块 .NET SDK 允许您与所有 Dapr 构建块 进行接口交互。
调用服务 HTTP 您可以使用 DaprClient
或 System.Net.Http.HttpClient
来调用服务。
using var client = new DaprClientBuilder ().
UseTimeout ( TimeSpan . FromSeconds ( 2 )). // 可选:设置超时
Build ();
// 调用名为 "deposit" 的 POST 方法,输入类型为 "Transaction"
var data = new { id = "17" , amount = 99 m };
var account = await client . InvokeMethodAsync < object , Account >( "routing" , "deposit" , data , cancellationToken );
Console . WriteLine ( "返回: id:{0} | 余额:{1}" , account . Id , account . Balance );
var client = DaprClient . CreateInvokeHttpClient ( appId : "routing" );
// 设置 HTTP 客户端的超时:
client . Timeout = TimeSpan . FromSeconds ( 2 );
var deposit = new Transaction { Id = "17" , Amount = 99 m };
var response = await client . PostAsJsonAsync ( "/deposit" , deposit , cancellationToken );
var account = await response . Content . ReadFromJsonAsync < Account >( cancellationToken : cancellationToken );
Console . WriteLine ( "返回: id:{0} | 余额:{1}" , account . Id , account . Balance );
gRPC 您可以使用 DaprClient
通过 gRPC 调用服务。
using var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 20 ));
var invoker = DaprClient . CreateInvocationInvoker ( appId : myAppId , daprEndpoint : serviceEndpoint );
var client = new MyService . MyServiceClient ( invoker );
var options = new CallOptions ( cancellationToken : cts . Token , deadline : DateTime . UtcNow . AddSeconds ( 1 ));
await client . MyMethodAsync ( new Empty (), options );
Assert . Equal ( StatusCode . DeadlineExceeded , ex . StatusCode );
保存和获取应用程序状态 var client = new DaprClientBuilder (). Build ();
var state = new Widget () { Size = "small" , Color = "yellow" , };
await client . SaveStateAsync ( storeName , stateKeyName , state , cancellationToken : cancellationToken );
Console . WriteLine ( "状态已保存!" );
state = await client . GetStateAsync < Widget >( storeName , stateKeyName , cancellationToken : cancellationToken );
Console . WriteLine ( $"获取状态: {state.Size} {state.Color}" );
await client . DeleteStateAsync ( storeName , stateKeyName , cancellationToken : cancellationToken );
Console . WriteLine ( "状态已删除!" );
查询状态 (Alpha) var query = "{" +
"\"filter\": {" +
"\"EQ\": { \"value.Id\": \"1\" }" +
"}," +
"\"sort\": [" +
"{" +
"\"key\": \"value.Balance\"," +
"\"order\": \"DESC\"" +
"}" +
"]" +
"}" ;
var client = new DaprClientBuilder (). Build ();
var queryResponse = await client . QueryStateAsync < Account >( "querystore" , query , cancellationToken : cancellationToken );
Console . WriteLine ( $"获取 {queryResponse.Results.Count}" );
foreach ( var account in queryResponse . Results )
{
Console . WriteLine ( $"账户: {account.Data.Id} 余额 {account.Data.Balance}" );
}
发布消息 var client = new DaprClientBuilder (). Build ();
var eventData = new { Id = "17" , Amount = 10 m , };
await client . PublishEventAsync ( pubsubName , "deposit" , eventData , cancellationToken );
Console . WriteLine ( "已发布存款事件!" );
与输出绑定交互 using var client = new DaprClientBuilder (). Build ();
// Twilio SendGrid 绑定的示例负载
var email = new
{
metadata = new
{
emailTo = "customer@example.com" ,
subject = "来自 Dapr SendGrid 绑定的邮件" ,
},
data = "<h1>测试 Dapr 绑定</h1>这是一个测试。<br>再见!" ,
};
await client . InvokeBindingAsync ( "send-email" , "create" , email );
检索秘密
Multi-value-secret
Single-value-secret var client = new DaprClientBuilder (). Build ();
// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client . GetSecretAsync ( "mysecretstore" , "key-value-pair-secret" );
Console . WriteLine ( $"获取秘密键: {string.Join(" , ", secrets.Keys)}" );
var client = new DaprClientBuilder (). Build ();
// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client . GetSecretAsync ( "mysecretstore" , "key-value-pair-secret" );
Console . WriteLine ( $"获取秘密键: {string.Join(" , ", secrets.Keys)}" );
// 检索单值秘密 - 返回一个 Dictionary<string, string>
// 包含一个以秘密名称为键的单个值
var data = await client . GetSecretAsync ( "mysecretstore" , "single-value-secret" );
var value = data [ "single-value-secret" ]
Console . WriteLine ( "获取了一个秘密值,我不会打印它,因为它是秘密!" );
获取配置键 var client = new DaprClientBuilder (). Build ();
// 检索特定的一组键。
var specificItems = await client . GetConfiguration ( "configstore" , new List < string >() { "key1" , "key2" });
Console . WriteLine ( $"这是我的值:\n{specificItems[0].Key} -> {specificItems[0].Value}\n{specificItems[1].Key} -> {specificItems[1].Value}" );
// 通过提供一个空列表来检索所有配置项。
var specificItems = await client . GetConfiguration ( "configstore" , new List < string >());
Console . WriteLine ( $"我得到了 {configItems.Count} 个条目!" );
foreach ( var item in configItems )
{
Console . WriteLine ( $"{item.Key} -> {item.Value}" )
}
订阅配置键 var client = new DaprClientBuilder (). Build ();
// 订阅配置 API 返回一个 IAsyncEnumerable<IEnumerable<ConfigurationItem>> 的包装器。
// 通过在 foreach 循环中访问其 Source 进行迭代。当流被切断或取消令牌被取消时,循环将结束。
var subscribeConfigurationResponse = await daprClient . SubscribeConfiguration ( store , keys , metadata , cts . Token );
await foreach ( var items in subscribeConfigurationResponse . Source . WithCancellation ( cts . Token ))
{
foreach ( var item in items )
{
Console . WriteLine ( $"{item.Key} -> {item.Value}" )
}
}
分布式锁 (Alpha) 获取锁 using System ;
using Dapr.Client ;
namespace LockService
{
class Program
{
[Obsolete("分布式锁 API 处于 Alpha 阶段,一旦稳定可以移除。")]
static async Task Main ( string [] args )
{
var daprLockName = "lockstore" ;
var fileName = "my_file_name" ;
var client = new DaprClientBuilder (). Build ();
// 使用这种方法锁定也会自动解锁,因为这是一个可释放对象
await using ( var fileLock = await client . Lock ( DAPR_LOCK_NAME , fileName , "random_id_abc123" , 60 ))
{
if ( fileLock . Success )
{
Console . WriteLine ( "成功" );
}
else
{
Console . WriteLine ( $"锁定 {fileName} 失败。" );
}
}
}
}
}
解锁现有锁 using System ;
using Dapr.Client ;
namespace LockService
{
class Program
{
static async Task Main ( string [] args )
{
var daprLockName = "lockstore" ;
var client = new DaprClientBuilder (). Build ();
var response = await client . Unlock ( DAPR_LOCK_NAME , "my_file_name" , "random_id_abc123" ));
Console . WriteLine ( response . status );
}
}
}
管理工作流实例 (Alpha) var daprClient = new DaprClientBuilder (). Build ();
string instanceId = "MyWorkflowInstance1" ;
string workflowComponentName = "dapr" ; // 或者,这可以是 yaml 中定义的工作流组件的名称
string workflowName = "MyWorkflowDefinition" ;
var input = new { name = "Billy" , age = 30 }; // 任何 JSON 可序列化的值都可以
// 启动工作流
var startResponse = await daprClient . StartWorkflowAsync ( instanceId , workflowComponentName , workflowName , input );
// 终止工作流
await daprClient . TerminateWorkflowAsync ( instanceId , workflowComponentName );
// 获取工作流元数据
var getResponse = await daprClient . GetWorkflowAsync ( instanceId , workflowComponentName , workflowName );
Sidecar APIs Sidecar 健康 .NET SDK 提供了一种轮询 sidecar 健康状态的方法,以及一个等待 sidecar 准备就绪的便捷方法。
轮询健康状态 当 sidecar 和您的应用程序都启动(完全初始化)时,此健康端点返回 true。
var client = new DaprClientBuilder (). Build ();
var isDaprReady = await client . CheckHealthAsync ();
if ( isDaprReady )
{
// 执行依赖 Dapr 的代码。
}
轮询健康状态(出站) 当 Dapr 初始化了其所有组件时,此健康端点返回 true,但可能尚未完成与您的应用程序的通信通道设置。
当您希望在启动路径中利用 Dapr 组件时,这种方法最好,例如,从 secretstore 加载秘密。
var client = new DaprClientBuilder (). Build ();
var isDaprComponentsReady = await client . CheckOutboundHealthAsync ();
if ( isDaprComponentsReady )
{
// 执行依赖 Dapr 组件的代码。
}
等待 sidecar DaprClient
还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,建议包含一个 CancellationToken
以允许请求超时。以下是 DaprSecretStoreConfigurationProvider
中使用此方法的示例。
// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
using ( var tokenSource = new CancellationTokenSource ( sidecarWaitTimeout ))
{
await client . WaitForSidecarAsync ( tokenSource . Token );
}
// 在此处执行 Dapr 组件操作,例如获取秘密。
关闭 sidecar var client = new DaprClientBuilder (). Build ();
await client . ShutdownSidecarAsync ();
相关链接 1.1.1 - DaprClient 使用 使用 DaprClient 的基本提示和建议
生命周期管理 DaprClient
使用 TCP 套接字来访问网络资源,与 Dapr sidecar 进行通信。它实现了 IDisposable
接口,以便快速清理资源。
依赖注入 通过 AddDaprClient()
方法可以在 ASP.NET Core 中注册 Dapr 客户端。此方法接受一个可选的配置委托,用于配置 DaprClient
,以及一个 ServiceLifetime
参数,允许您为注册的资源指定不同的生命周期,默认是 Singleton
。
以下示例展示了如何使用默认值注册 DaprClient
:
services . AddDaprClient ();
您可以通过配置委托在 DaprClientBuilder
上指定选项来配置 DaprClient
,例如:
services . AddDaprClient ( daprBuilder => {
daprBuilder . UseJsonSerializerOptions ( new JsonSerializerOptions {
WriteIndented = true ,
MaxDepth = 8
});
daprBuilder . UseTimeout ( TimeSpan . FromSeconds ( 30 ));
});
另一个重载允许访问 DaprClientBuilder
和 IServiceProvider
,以便进行更高级的配置,例如从依赖注入容器中获取服务:
services . AddSingleton < SampleService >();
services . AddDaprClient (( serviceProvider , daprBuilder ) => {
var sampleService = serviceProvider . GetRequiredService < SampleService >();
var timeoutValue = sampleService . TimeoutOptions ;
daprBuilder . UseTimeout ( timeoutValue );
});
手动实例化 除了依赖注入,您还可以使用静态客户端构建器手动创建 DaprClient
。
为了优化性能,建议创建一个长生命周期的 DaprClient
实例,并在整个应用程序中共享。DaprClient
是线程安全的,适合共享使用。
避免为每个操作创建一个新的 DaprClient
实例并在操作完成后释放它。
配置 DaprClient 在调用 .Build()
创建客户端之前,可以通过 DaprClientBuilder
类上的方法来配置 DaprClient
。每个 DaprClient
对象的设置是独立的,创建后无法更改。
var daprClient = new DaprClientBuilder ()
. UseJsonSerializerSettings ( ... ) // 配置 JSON 序列化器
. Build ();
默认情况下,DaprClientBuilder
会按以下顺序优先获取配置值:
直接提供给 DaprClientBuilder
方法的值(例如 UseTimeout(TimeSpan.FromSeconds(30))
) 从可选的 IConfiguration
中提取的值,与环境变量名称匹配 从环境变量中提取的值 默认值 在 DaprClientBuilder
上配置 DaprClientBuilder
提供以下方法来设置配置选项:
UseHttpEndpoint(string)
: 设置 Dapr sidecar 的 HTTP 端点UseGrpcEndpoint(string)
: 设置 Dapr sidecar 的 gRPC 端点UseGrpcChannelOptions(GrpcChannelOptions)
: 设置 gRPC 通道选项UseHttpClientFactory(IHttpClientFactory)
: 配置 DaprClient
使用的 HttpClient
工厂UseJsonSerializationOptions(JsonSerializerOptions)
: 配置 JSON 序列化UseDaprApiToken(string)
: 为 Dapr sidecar 的身份验证提供令牌UseTimeout(TimeSpan)
: 指定与 Dapr sidecar 通信时的超时值从 IConfiguration
配置 除了直接从环境变量获取配置值,您还可以通过 IConfiguration
提供这些值。
例如,在多租户环境中,您可能需要为环境变量添加前缀。以下示例展示了如何从环境变量中获取这些值到 IConfiguration
,并移除前缀:
var builder = WebApplication . CreateBuilder ( args );
builder . Configuration . AddEnvironmentVariables ( "test_" ); // 获取所有以 "test_" 开头的环境变量,并移除前缀
builder . Services . AddDaprClient ();
从环境变量配置 SDK 会读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
: Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
: Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
: 如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找本地 HTTP 端点DAPR_GRPC_PORT
: 如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找本地 gRPC 端点DAPR_API_TOKEN
: 设置 API 令牌
注意 如果同时指定了 DAPR_HTTP_ENDPOINT
和 DAPR_HTTP_PORT
,则会忽略 DAPR_HTTP_PORT
的端口值,而使用 DAPR_HTTP_ENDPOINT
上定义的端口。DAPR_GRPC_ENDPOINT
和 DAPR_GRPC_PORT
也是如此。配置 gRPC 通道选项 Dapr 使用 CancellationToken
进行取消,依赖于 gRPC 通道选项的配置,默认已启用。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置 。
var daprClient = new DaprClientBuilder ()
. UseGrpcChannelOptions ( new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
. Build ();
使用 DaprClient 进行取消 在 DaprClient 上执行异步操作的 API 接受一个可选的 CancellationToken
参数。这遵循 .NET 的标准惯例,用于可取消的操作。请注意,当取消发生时,不能保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,将抛出一个 OperationCancelledException
。
理解 DaprClient 的 JSON 序列化 DaprClient
上的许多方法使用 System.Text.Json
序列化器执行 JSON 序列化。接受应用程序数据类型作为参数的方法将对其进行 JSON 序列化,除非文档明确说明了其他情况。
如果您有高级需求,建议阅读 System.Text.Json 文档 。Dapr .NET SDK 不提供独特的序列化行为或自定义 - 它依赖于底层序列化器将数据转换为和从应用程序的 .NET 类型。
DaprClient
被配置为使用从 JsonSerializerDefaults.Web 配置的序列化器选项对象。这意味着 DaprClient
将使用 camelCase
作为属性名称,允许读取带引号的数字("10.99"
),并将不区分大小写地绑定属性。这些是与 ASP.NET Core 和 System.Text.Json.Http
API 一起使用的相同设置,旨在遵循可互操作的 Web 约定。
截至 .NET 5.0,System.Text.Json
对所有 F# 语言特性内置支持不佳。如果您使用 F#,您可能需要使用一个添加对 F# 特性支持的转换器包,例如 FSharp.SystemTextJson 。
JSON 序列化的简单指导 如果您使用的功能集映射到 JSON 的类型系统,您在使用 JSON 序列化和 DaprClient
时的体验将会很顺利。这些是可以简化代码的通用指南。
避免继承和多态 不要尝试序列化具有循环引用的数据 不要在构造函数或属性访问器中放置复杂或昂贵的逻辑 使用与 JSON 类型(数值类型、字符串、DateTime
)清晰映射的 .NET 类型 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性 设计具有 get
/set
属性的类型,或者使用 支持的模式 用于 JSON 的不可变类型 多态性和序列化 DaprClient
使用的 System.Text.Json
序列化器在执行序列化时使用值的声明类型。
本节将使用 DaprClient.SaveStateAsync<TValue>(...)
作为示例,但建议适用于 SDK 暴露的任何 Dapr 构建块。
public class Widget
{
public string Color { get ; set ; }
}
...
// 将 Widget 值作为 JSON 存储在状态存储中
Widget widget = new Widget () { Color = "Green" , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
在上面的示例中,类型参数 TValue
的类型参数是从 widget
变量的类型推断出来的。这很重要,因为 System.Text.Json
序列化器将根据值的声明类型 执行序列化。结果是 JSON 值 { "color": "Green" }
将被存储。
考虑当您尝试使用 Widget
的派生类型时会发生什么:
public class Widget
{
public string Color { get ; set ; }
}
public class SuperWidget : Widget
{
public bool HasSelfCleaningFeature { get ; set ; }
}
...
// 将 SuperWidget 值作为 JSON 存储在状态存储中
Widget widget = new SuperWidget () { Color = "Green" , HasSelfCleaningFeature = true , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
在此示例中,我们使用了一个 SuperWidget
,但变量的声明类型是 Widget
。由于 JSON 序列化器的行为由声明类型决定,它只看到一个简单的 Widget
,并将保存值 { "color": "Green" }
,而不是 { "color": "Green", "hasSelfCleaningFeature": true }
。
如果您希望 SuperWidget
的属性被序列化,那么最好的选择是用 object
覆盖类型参数。这将导致序列化器包含所有数据,因为它对类型一无所知。
Widget widget = new SuperWidget () { Color = "Green" , HasSelfCleaningFeature = true , };
await client . SaveStateAsync < object >( "mystatestore" , "mykey" , widget );
错误处理 当遇到故障时,DaprClient
上的方法将抛出 DaprException
或其子类。
try
{
var widget = new Widget () { Color = "Green" , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
}
catch ( DaprException ex )
{
// 处理异常,记录日志,重试等
}
最常见的故障情况将与以下内容相关:
Dapr 组件配置不正确 瞬时故障,例如网络问题 无效数据,例如 JSON 反序列化失败 在任何这些情况下,您都可以通过 .InnerException
属性检查更多异常详细信息。
1.2 - Dapr actors .NET SDK 快速掌握使用 Dapr actors .NET SDK 的方法
借助 Dapr actor 包,您可以在 .NET 应用程序中轻松与 Dapr 的虚拟 actor 进行交互。
要开始,请参阅 Dapr actors 指南。
1.2.1 - IActorProxyFactory 接口 了解如何使用 IActorProxyFactory 接口创建 actor 客户端
在使用 actor
类或 ASP.NET Core 项目时,推荐使用 IActorProxyFactory
接口来创建 actor 客户端。
通过 AddActors(...)
方法,actor 服务将通过 ASP.NET Core 的依赖注入机制进行注册。
在 actor 实例之外: IActorProxyFactory
实例作为单例服务通过依赖注入提供。在 actor 实例内部: IActorProxyFactory
实例作为属性 (this.ProxyFactory
) 提供。以下是在 actor 内部创建代理的示例:
public Task < MyData > GetDataAsync ()
{
var proxy = this . ProxyFactory . CreateActorProxy < IOtherActor >( ActorId . CreateRandom (), "OtherActor" );
await proxy . DoSomethingGreat ();
return this . StateManager . GetStateAsync < MyData >( "my_data" );
}
在本指南中,您将学习如何使用 IActorProxyFactory
。
提示 对于不使用依赖注入的应用程序,您可以使用 ActorProxy
的静态方法。由于 ActorProxy
方法容易出错,建议在配置自定义设置时尽量避免使用。确定 actor IActorProxyFactory
的所有 API 都需要提供 actor 的 类型 和 id 以便与其通信。对于强类型客户端,您还需要提供其接口之一。
actor 类型 在整个应用程序中唯一标识 actor 实现。actor id 唯一标识该类型的一个实例。如果您没有 actor id
并希望与新实例通信,可以使用 ActorId.CreateRandom()
创建一个随机 id。随机 id 是一个加密强标识符,运行时将在您与其交互时创建一个新的 actor 实例。
您可以使用 ActorReference
类型在消息中传递 actor 类型和 actor id,以便与其他 actor 进行交换。
两种风格的 actor 客户端 actor 客户端支持两种不同的调用方式:
actor 客户端风格 描述 强类型 强类型客户端基于 .NET 接口,提供强类型的优势。它们不适用于非 .NET actor。 弱类型 弱类型客户端使用 ActorProxy
类。建议仅在需要互操作或其他高级原因时使用这些。
使用强类型客户端 以下示例使用 CreateActorProxy<>
方法创建强类型客户端。CreateActorProxy<>
需要一个 actor 接口类型,并返回该接口的一个实例。
// 为 IOtherActor 创建一个代理,将类型设为 OtherActor,使用随机 id
var proxy = this . ProxyFactory . CreateActorProxy < IOtherActor >( ActorId . CreateRandom (), "OtherActor" );
// 调用接口定义的方法以调用 actor
//
// proxy 是 IOtherActor 的实现,因此我们可以直接调用其方法
await proxy . DoSomethingGreat ();
使用弱类型客户端 以下示例使用 Create
方法创建弱类型客户端。Create
返回一个 ActorProxy
实例。
// 为类型 OtherActor 创建一个代理,使用随机 id
var proxy = this . ProxyFactory . Create ( ActorId . CreateRandom (), "OtherActor" );
// 通过名称调用方法以调用 actor
//
// proxy 是 ActorProxy 的一个实例。
await proxy . InvokeMethodAsync ( "DoSomethingGreat" );
由于 ActorProxy
是一个弱类型代理,您需要以字符串形式传递 actor 方法名称。
您还可以使用 ActorProxy
调用带有请求和响应消息的方法。请求和响应消息将使用 System.Text.Json
序列化器进行序列化。
// 为类型 OtherActor 创建一个代理,使用随机 id
var proxy = this . ProxyFactory . Create ( ActorId . CreateRandom (), "OtherActor" );
// 在代理上调用方法以调用 actor
//
// proxy 是 ActorProxy 的一个实例。
var request = new MyRequest () { Message = "Hi, it's me." , };
var response = await proxy . InvokeMethodAsync < MyRequest , MyResponse >( "DoSomethingGreat" , request );
使用弱类型代理时,您 必须 主动定义正确的 actor 方法名称和消息类型。使用强类型代理时,这些名称和类型作为接口定义的一部分为您定义。
actor 方法调用异常详细信息 actor 方法调用异常的详细信息会显示给调用者和被调用者,提供一个追踪问题的入口点。异常详细信息包括:
您可以使用 UUID 匹配调用者和被调用者一侧的异常。以下是异常详细信息的示例:
Dapr.Actors.ActorMethodInvocationException: 远程 actor 方法异常,详细信息:异常:NotImplementedException,方法名称:ExceptionExample,行号:14,异常 uuid:d291a006-84d5-42c4-b39e-d6300e9ac38b
下一步 了解如何使用 ActorHost
编写和运行 actor 。
1.2.2 - 编写和运行actor 了解如何使用.NET SDK编写和运行actor
编写actor ActorHost ActorHost
:
是所有actor构造函数所需的参数 由运行时提供的 必须传递给基类的构造函数 包含允许该actor实例与运行时通信的所有状态信息 internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host ) // 在构造函数中接收ActorHost
: base ( host ) // 将ActorHost传递给基类的构造函数
{
}
}
由于ActorHost
包含actor特有的状态信息,您不需要将其实例传递给代码的其他部分。建议仅在测试中创建您自己的ActorHost
实例。
依赖注入 actor支持通过依赖注入 将额外的参数传递到构造函数中。您定义的任何其他参数都将从依赖注入容器中获取其值。
internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host , BankService bank ) // 在构造函数中接收BankService
: base ( host )
{
...
}
}
一个actor类型应该只有一个public
构造函数。actor系统使用ActivatorUtilities
模式来创建actor实例。
您可以在Startup.cs
中注册类型以进行依赖注入以使其可用。阅读更多关于注册类型的不同方法 。
// 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
...
// 使用依赖注入注册额外的类型。
services . AddSingleton < BankService >();
}
每个actor实例都有其自己的依赖注入范围,并在执行操作后在内存中保留一段时间。在此期间,与actor关联的依赖注入范围也被视为活动状态。该范围将在actor被停用时释放。
如果actor在构造函数中注入IServiceProvider
,actor将接收到与其范围关联的IServiceProvider
的引用。IServiceProvider
可以用于将来动态解析服务。
internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host , IServiceProvider services ) // 在构造函数中接收IServiceProvider
: base ( host )
{
...
}
}
使用此模式时,避免创建许多实现IDisposable
的瞬态 服务。由于与actor关联的范围可能被视为有效时间较长,您可能会在内存中积累许多服务。有关更多信息,请参阅依赖注入指南 。
IDisposable和actor actor可以实现IDisposable
或IAsyncDisposable
。建议您依赖依赖注入进行资源管理,而不是在应用程序代码中实现释放功能。仅在确实必要的情况下提供释放支持。
日志记录 在actor类内部,您可以通过基类Actor
上的属性访问ILogger
实例。此实例连接到ASP.NET Core日志系统,应该用于actor内部的所有日志记录。阅读更多关于日志记录 。您可以配置各种不同的日志格式和输出接收器。
使用_结构化日志记录_和_命名占位符_,如下例所示:
public Task < MyData > GetDataAsync ()
{
this . Logger . LogInformation ( "获取状态时间为 {CurrentTime}" , DateTime . UtcNow );
return this . StateManager . GetStateAsync < MyData >( "my_data" );
}
记录日志时,避免使用格式字符串,如:$"获取状态时间为 {DateTime.UtcNow}"
日志记录应使用命名占位符语法 ,这提供了更好的性能和与日志系统的集成。
使用显式actor类型名称 默认情况下,客户端看到的actor的_类型_是从actor实现类的_名称_派生的。默认名称将是类名(不包括命名空间)。
如果需要,您可以通过将ActorAttribute
属性附加到actor实现类来指定显式类型名称。
[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor , IMyActor
{
// ...
}
在上面的例子中,名称将是MyCustomActorTypeName
。
无需更改注册actor类型与运行时的代码,通过属性提供值是唯一需要的。
在服务器上托管actor 注册actor actor注册是Startup.cs
中ConfigureServices
的一部分。您可以通过ConfigureServices
方法使用依赖注入注册服务。注册actor类型集是actor服务注册的一部分。
在ConfigureServices
中,您可以:
注册actor运行时(AddActors
) 注册actor类型(options.Actors.RegisterActor<>
) 配置actor运行时设置options
注册额外的服务类型以进行actor的依赖注入(services
) // 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
// 使用DI注册actor运行时
services . AddActors ( options =>
{
// 注册actor类型并配置actor设置
options . Actors . RegisterActor < MyActor >();
// 配置默认设置
options . ActorIdleTimeout = TimeSpan . FromMinutes ( 10 );
options . ActorScanInterval = TimeSpan . FromSeconds ( 35 );
options . DrainOngoingCallTimeout = TimeSpan . FromSeconds ( 35 );
options . DrainRebalancedActors = true ;
});
// 注册额外的服务以供actor使用
services . AddSingleton < BankService >();
}
配置JSON选项 actor运行时使用System.Text.Json 进行:
默认情况下,actor运行时使用基于JsonSerializerDefaults.Web 的设置。
您可以在ConfigureServices
中配置JsonSerializerOptions
:
// 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
services . AddActors ( options =>
{
...
// 自定义JSON选项
options . JsonSerializerOptions = ...
});
}
actor和路由 ASP.NET Core对actor的托管支持使用端点路由 系统。.NET SDK不支持使用早期ASP.NET Core版本的传统路由系统托管actor。
由于actor使用端点路由,actor的HTTP处理程序是中间件管道的一部分。以下是设置包含actor的中间件管道的Configure
方法的最小示例。
// 在Startup.cs中
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
app . UseRouting ();
app . UseEndpoints ( endpoints =>
{
// 注册与Dapr运行时接口的actor处理程序。
endpoints . MapActorsHandlers ();
});
}
UseRouting
和UseEndpoints
调用是配置路由所必需的。通过在端点中间件中添加MapActorsHandlers
将actor配置为管道的一部分。
这是一个最小示例,actor功能可以与以下内容共存:
控制器 Razor页面 Blazor gRPC服务 Dapr pub/sub处理程序 其他端点,如健康检查 问题中间件 某些中间件可能会干扰Dapr请求到actor处理程序的路由。特别是,UseHttpsRedirection
对于Dapr的默认配置是有问题的。Dapr默认通过未加密的HTTP发送请求,这将被UseHttpsRedirection
中间件阻止。此中间件目前不能与Dapr一起使用。
// 在Startup.cs中
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
// 无效 - 这将阻止非HTTPS请求
app . UseHttpsRedirection ();
// 无效 - 这将阻止非HTTPS请求
app . UseRouting ();
app . UseEndpoints ( endpoints =>
{
// 注册与Dapr运行时接口的actor处理程序。
endpoints . MapActorsHandlers ();
});
}
下一步 尝试运行和使用虚拟actor示例 。
1.2.3 - .NET SDK 中的 Actor 序列化 使用 .NET 中的远程 Actor 序列化类型的必要步骤
Actor 序列化 Dapr actor 包使您能够在 .NET 应用程序中使用 Dapr 虚拟 actor,您可以选择使用弱类型或强类型客户端。每种方式都有不同的序列化方法。本文档将回顾这些差异,并传达一些在任一场景中需要理解的关键基本规则。
请注意,由于序列化方法的不同,弱类型和强类型 actor 客户端不能交替使用。使用一个 actor 客户端持久化的数据将无法通过另一个 actor 客户端访问,因此在整个应用程序中选择一种并一致使用非常重要。
弱类型 Dapr Actor 客户端 本节将介绍如何配置 C# 类型,以便在使用弱类型 actor 客户端时正确进行序列化和反序列化。这些客户端使用基于字符串的方法名称,并通过 System.Text.Json 序列化器来处理请求和响应负载。请注意,这个序列化框架并不是 Dapr 特有的,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护的。
当使用弱类型 Dapr Actor 客户端从各种 actor 调用方法时,不需要独立序列化或反序列化方法负载,因为 SDK 会透明地为您处理这些操作。
客户端将使用您构建的 .NET 版本中可用的最新 System.Text.Json 版本,序列化受 相关 .NET 文档 中提供的所有固有功能的影响。
序列化器将配置为使用 JsonSerializerOptions.Web
默认选项 ,除非通过自定义选项配置覆盖,这意味着将应用以下内容:
属性名称的反序列化以不区分大小写的方式进行 属性名称的序列化使用 驼峰命名法 ,除非属性被 [JsonPropertyName]
属性覆盖 反序列化将从数字和/或字符串值读取数值 基本序列化 在以下示例中,我们展示了一个名为 Doodad 的简单类,尽管它也可以是一个记录。
public class Doodad
{
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public int Count { get ; set ; }
}
默认情况下,这将使用类型中成员的名称以及实例化时的值进行序列化:
{ "id" : "a06ced64-4f42-48ad-84dd-46ae6a7e333d" , "name" : "DoodadName" , "count" : 5 }
覆盖序列化属性名称 可以通过将 [JsonPropertyName]
属性应用于所需属性来覆盖默认属性名称。
通常,对于您要持久化到 actor state 的类型,这不是必需的,因为您不打算独立于 Dapr 相关功能读取或写入它们,但以下内容仅用于清楚地说明这是可能的。
覆盖类上的属性名称 以下是使用 JsonPropertyName
更改序列化后第一个属性名称的示例。请注意,Count
属性上最后一次使用 JsonPropertyName
与预期的序列化结果相匹配。这主要是为了演示应用此属性不会对任何内容产生负面影响——事实上,如果您稍后决定更改默认序列化选项但仍需要一致地访问之前序列化的属性,这可能是更可取的,因为 JsonPropertyName
将覆盖这些选项。
public class Doodad
{
[JsonPropertyName("identifier")]
public Guid Id { get ; set ; }
public string Name { get ; set ; }
[JsonPropertyName("count")]
public int Count { get ; set ; }
}
这将序列化为以下内容:
{ "identifier" : "a06ced64-4f42-48ad-84dd-46ae6a7e333d" , "name" : "DoodadName" , "count" : 5 }
覆盖记录上的属性名称 让我们尝试对 C# 12 或更高版本中的记录做同样的事情:
public record Thingy ( string Name , [ JsonPropertyName ( "count" )] int Count );
由于在主构造函数中传递的参数(在 C# 12 中引入)可以应用于记录中的属性或字段,因此在某些模糊情况下,使用 [JsonPropertyName]
属性可能需要指定您打算将属性应用于属性而不是字段。如果需要这样做,您可以在主构造函数中指明:
public record Thingy ( string Name , [ property : JsonPropertyName ( "count" )] int Count );
如果 [property: ]
应用于不需要的 [JsonPropertyName]
属性,它不会对序列化或反序列化产生负面影响,因为操作将正常进行,就像它是一个属性一样(如果没有标记为这样,通常会这样)。
枚举类型 枚举,包括平面枚举,可以序列化为 JSON,但持久化的值可能会让您感到惊讶。同样,开发人员不应独立于 Dapr 处理序列化数据,但以下信息至少可以帮助诊断为什么看似轻微的版本迁移没有按预期工作。
以下是提供一年中不同季节的 enum
类型:
public enum Season
{
Spring ,
Summer ,
Fall ,
Winter
}
我们将使用一个单独的演示类型来引用我们的 Season
,同时展示这如何与记录一起工作:
public record Engagement ( string Name , Season TimeOfYear );
给定以下初始化实例:
var myEngagement = new Engagement ( "Ski Trip" , Season . Winter );
这将序列化为以下 JSON:
{ "name" : "Ski Trip" , "season" : 3 }
这可能会让人意外,我们的 Season.Winter
值被表示为 3
,但这是因为序列化器将自动使用从零开始的枚举值的数字表示,并为每个可用的附加值递增数字值。同样,如果进行迁移并且开发人员更改了枚举的顺序,这将在您的解决方案中引发破坏性更改,因为序列化的数字值在反序列化时将指向不同的值。
相反,System.Text.Json
提供了一个 JsonConverter
,它将选择使用基于字符串的值而不是数字值。需要将 [JsonConverter]
属性应用于枚举类型本身以启用此功能,但随后将在引用枚举的任何下游序列化或反序列化操作中实现。
[JsonConverter(typeof(JsonStringEnumConverter<Season>))]
public enum Season
{
Spring ,
Summer ,
Fall ,
Winter
}
使用我们上面 myEngagement
实例中的相同值,这将生成以下 JSON:
{ "name" : "Ski Trip" , "season" : "Winter" }
因此,枚举成员可以在不担心在反序列化期间引入错误的情况下进行调整。
自定义枚举值 System.Text.Json 序列化平台不支持使用 [EnumMember]
来更改序列化或反序列化期间使用的枚举值,但在某些情况下这可能很有用。同样,假设您正在重构解决方案以为各种枚举应用更好的名称。您正在使用上面详细介绍的 JsonStringEnumConverter<TType>
,因此您将枚举的名称保存为值而不是数字值,但如果您更改枚举名称,这将引入破坏性更改,因为名称将不再与 state 中的内容匹配。
请注意,如果您选择使用此方法,您应该为所有枚举成员装饰 [EnumMeber]
属性,以便为每个枚举值一致地应用值,而不是随意地。没有任何东西会在构建或运行时验证这一点,但这被认为是最佳实践操作。
在这种情况下,如何在仍然更改枚举成员名称的同时指定持久化的精确值?使用自定义 JsonConverter
和扩展方法,可以从附加的 [EnumMember]
属性中提取值。将以下内容添加到您的解决方案中:
public sealed class EnumMemberJsonConverter < T > : JsonConverter < T > where T : struct , Enum
{
/// <summary>读取并将 JSON 转换为类型 <typeparamref name="T" />。</summary>
/// <param name="reader">读取器。</param>
/// <param name="typeToConvert">要转换的类型。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
/// <returns>转换后的值。</returns>
public override T Read ( ref Utf8JsonReader reader , Type typeToConvert , JsonSerializerOptions options )
{
// 从 JSON 读取器获取字符串值
var value = reader . GetString ();
// 遍历所有枚举值
foreach ( var enumValue in Enum . GetValues < T >())
{
// 从 EnumMember 属性中获取值(如果有)
var enumMemberValue = GetValueFromEnumMember ( enumValue );
// 如果值匹配,返回枚举值
if ( value == enumMemberValue )
{
return enumValue ;
}
}
// 如果没有找到匹配项,抛出异常
throw new JsonException ( $"Invalid value for {typeToConvert.Name}: {value}" );
}
/// <summary>将指定的值写为 JSON。</summary>
/// <param name="writer">要写入的写入器。</param>
/// <param name="value">要转换为 JSON 的值。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
public override void Write ( Utf8JsonWriter writer , T value , JsonSerializerOptions options )
{
// 从 EnumMember 属性中获取值(如果有)
var enumMemberValue = GetValueFromEnumMember ( value );
// 将值写入 JSON 写入器
writer . WriteStringValue ( enumMemberValue );
}
private static string GetValueFromEnumMember ( T value )
{
MemberInfo [] member = typeof ( T ). GetMember ( value . ToString (), BindingFlags . DeclaredOnly | BindingFlags . Static | BindingFlags . Public );
if ( member . Length == 0 )
return value . ToString ();
object [] customAttributes = member . GetCustomAttributes ( typeof ( EnumMemberAttribute ), false );
if ( customAttributes . Length != 0 )
{
EnumMemberAttribute enumMemberAttribute = ( EnumMemberAttribute ) customAttributes ;
if ( enumMemberAttribute != null && enumMemberAttribute . Value != null )
return enumMemberAttribute . Value ;
}
return value . ToString ();
}
}
现在让我们添加一个示例枚举器。我们将设置一个值,使用每个枚举成员的小写版本来演示这一点。不要忘记用 JsonConverter
属性装饰枚举,并在上节中使用我们的自定义转换器代替数字到字符串的转换器。
[JsonConverter(typeof(EnumMemberJsonConverter<Season>))]
public enum Season
{
[EnumMember(Value="spring")]
Spring ,
[EnumMember(Value="summer")]
Summer ,
[EnumMember(Value="fall")]
Fall ,
[EnumMember(Value="winter")]
Winter
}
让我们使用之前的示例记录。我们还将添加一个 [JsonPropertyName]
属性以增强演示:
public record Engagement ([ property : JsonPropertyName ( "event" )] string Name , Season TimeOfYear );
最后,让我们初始化这个新实例:
var myEngagement = new Engagement ( "Conference" , Season . Fall );
这次,序列化将考虑附加的 [EnumMember]
属性中的值,为我们提供了一种机制来重构我们的应用程序,而无需为 state 中现有的枚举值制定复杂的版本控制方案。
{ "event" : "Conference" , "season" : "fall" }
强类型 Dapr Actor 客户端 在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时,它们在运行时能够正确序列化和反序列化。这些客户端是使用 .NET 接口实现的,并且不 与使用其他语言编写的 Dapr actor 兼容。
此 actor 客户端使用称为 数据契约序列化器 的引擎序列化数据,该引擎将您的 C# 类型转换为 XML 文档。此序列化框架并不是 Dapr 特有的,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护的。
在发送或接收原始类型(如字符串或整数)时,此序列化会透明地进行,您无需进行任何准备。然而,当处理您创建的复杂类型时,有一些重要规则需要考虑,以便此过程顺利进行。
可序列化类型 使用数据契约序列化器时需要牢记几个重要注意事项:
默认情况下,所有类型、读/写属性(构造后)和标记为公开可见的字段都会被序列化 所有类型必须公开一个无参数构造函数或用 DataContractAttribute 属性装饰 仅在使用 DataContractAttribute 属性时支持仅初始化的设置器 只读字段、没有 Get 和 Set 方法的属性以及具有私有 Get 和 Set 方法的内部或属性在序列化期间会被忽略 通过使用 KnownTypesAttribute 属性,支持使用其他复杂类型的类型的序列化,这些复杂类型本身未标记为 DataContractAttribute 属性 如果类型标记为 DataContractAttribute 属性,则您希望序列化和反序列化的所有成员也必须用 DataMemberAttribute 属性装饰,否则它们将被设置为默认值 反序列化如何工作? 反序列化使用的方法取决于类型是否用 DataContractAttribute 属性装饰。如果没有此属性,则使用无参数构造函数创建类型的实例。然后使用各自的设置器将每个属性和字段映射到类型中,并将实例返回给调用者。
如果类型标记为 [DataContract]
,则序列化器会使用反射读取类型的元数据,并根据它们是否标记为 DataMemberAttribute 属性来确定应包含哪些属性或字段,因为这是基于选择加入的。然后在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用仅初始化的设置器。在整个过程中会根据需要调用序列化回调,然后将对象返回给调用者。
强烈建议使用序列化属性,因为它们提供了更多灵活性来覆盖名称和命名空间,并且通常使用更多现代 C# 功能。虽然默认序列化器可以依赖于原始类型,但不建议用于您自己的任何类型,无论它们是类、结构还是记录。建议如果您用 DataContractAttribute 属性装饰类型,还要显式装饰您希望序列化或反序列化的每个成员的 DataMemberAttribute 属性。
.NET 类 只要遵循本页和 数据契约序列化器 文档中详细说明的其他规则,类在数据契约序列化器中是完全支持的。
这里最重要的是要记住,您必须要么有一个公共无参数构造函数,要么用适当的属性装饰它。让我们通过一些示例来真正澄清什么会起作用,什么不会。
在以下示例中,我们展示了一个名为 Doodad 的简单类。我们没有提供显式构造函数,因此编译器将提供一个默认的无参数构造函数。因为我们使用的是 支持的原始类型 (Guid、string 和 int32),并且我们所有的成员都有公共的 getter 和 setter,所以不需要任何属性,我们将能够在从 Dapr actor 方法发送和接收时使用此类而不会出现问题。
public class Doodad
{
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public int Count { get ; set ; }
}
默认情况下,这将使用类型中成员的名称以及实例化时的值进行序列化:
<Doodad>
<Id> a06ced64-4f42-48ad-84dd-46ae6a7e333d</Id>
<Name> DoodadName</Name>
<Count> 5</Count>
</Doodad>
所以让我们调整一下——让我们添加我们自己的构造函数,并仅在成员上使用仅初始化的设置器。这将无法正确序列化和反序列化,不是因为使用了仅初始化的设置器,而是因为缺少无参数构造函数。
// 无法正确序列化!
public class Doodad
{
public Doodad ( string name , int count )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
public Guid Id { get ; set ; }
public string Name { get ; init ; }
public int Count { get ; init ; }
}
如果我们为类型添加一个公共无参数构造函数,我们就可以继续使用它,而无需进一步的注释。
public class Doodad
{
public Doodad ()
{
}
public Doodad ( string name , int count )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public int Count { get ; set ; }
}
但如果我们不想添加这个构造函数怎么办?也许您不希望您的开发人员意外地使用意外的构造函数创建此 Doodad 的实例。这就是更灵活的属性有用的地方。如果您用 DataContractAttribute 属性装饰您的类型,您可以删除无参数构造函数,它将再次起作用。
[DataContract]
public class Doodad
{
public Doodad ( string name , int count )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public int Count { get ; set ; }
}
在上面的示例中,我们不需要使用 DataMemberAttribute 属性,因为我们使用的是序列化器支持的 内置原始类型 。但是,如果我们使用这些属性,我们确实可以获得更多的灵活性。通过 DataContractAttribute 属性,我们可以使用 Namespace 参数指定我们自己的 XML 命名空间,并通过 Name 参数更改类型在序列化为 XML 文档时使用的名称。
建议的做法是将 DataContractAttribute 属性附加到类型,并将 DataMemberAttribute 属性附加到您希望序列化的所有成员上——如果它们不是必需的,并且您没有更改默认值,它们将被忽略,但它们为您提供了一种机制,可以选择加入序列化原本不会包含的成员,例如标记为私有的成员,或者它们本身是复杂类型或集合。
请注意,如果您选择序列化私有成员,它们的值将被序列化为纯文本——它们很可能会被查看、拦截,并可能根据您序列化后如何处理数据而被操控,因此在您的用例中是否要标记这些成员是一个重要的考虑因素。
在以下示例中,我们将查看使用属性更改某些成员的序列化名称,并引入 IgnoreDataMemberAttribute 属性。顾名思义,这告诉序列化器跳过此属性,即使它本来有资格进行序列化。此外,由于我用 DataContractAttribute 属性装饰了类型,这意味着我可以在属性上使用仅初始化的设置器。
[DataContract(Name="Doodad")]
public class Doodad
{
public Doodad ( string name = "MyDoodad" , int count = 5 )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
[DataMember(Name = "id")]
public Guid Id { get ; init ; }
[IgnoreDataMember]
public string Name { get ; init ; }
[DataMember]
public int Count { get ; init ; }
}
当这个被序列化时,因为我们更改了序列化成员的名称,我们可以期望使用默认值的新 Doodad 实例被序列化为:
<Doodad>
<id> a06ced64-4f42-48ad-84dd-46ae6a7e333d</id>
<Count> 5</Count>
</Doodad>
C# 12 中的类 - 主构造函数 C# 12 为类引入了主构造函数。使用主构造函数意味着编译器将被阻止创建默认的隐式无参数构造函数。虽然类上的主构造函数不会生成任何公共属性,但这意味着如果您将任何参数传递给主构造函数或在类中有非原始类型,您将需要指定您自己的无参数构造函数或使用序列化属性。
这是一个示例,我们使用主构造函数将 ILogger 注入到一个字段中,并添加我们自己的无参数构造函数,而无需任何属性。
public class Doodad ( ILogger < Doodad > _logger )
{
public Doodad () {} //我们的无参数构造函数
public Doodad ( string name , int count )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public int Count { get ; set ; }
}
以及使用我们的序列化属性(再次选择仅初始化的设置器,因为我们使用的是序列化属性):
[DataContract]
public class Doodad ( ILogger < Doodad > _logger )
{
public Doodad ( string name , int count )
{
Id = Guid . NewGuid ();
Name = name ;
Count = count ;
}
[DataMember]
public Guid Id { get ; init ; }
[DataMember]
public string Name { get ; init ; }
[DataMember]
public int Count { get ; init ; }
}
.NET 结构体 只要它们标记为 DataContractAttribute 属性,并且您希望序列化的成员标记为 DataMemberAttribute 属性,结构体就可以被数据契约序列化器支持。此外,为了支持反序列化,结构体还需要有一个无参数构造函数。即使您定义了自己的无参数构造函数(在 C# 10 中启用),这也能正常工作。
[DataContract]
public struct Doodad
{
[DataMember]
public int Count { get ; set ; }
}
.NET 记录 记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该用 DataContractAttribute 属性装饰所有记录,并用 DataMemberAttribute 属性装饰您希望序列化的成员,以便在使用此或其他较新的 C# 功能时不会遇到反序列化问题。因为记录类默认使用仅初始化的设置器来设置属性,并鼓励使用主构造函数,所以将这些属性应用于您的类型可以确保序列化器能够正确处理您的类型。
通常,记录以使用新主构造函数概念的简单单行语句呈现:
public record Doodad ( Guid Id , string Name , int Count );
这将抛出一个错误,鼓励使用序列化属性,因为在 Dapr actor 方法调用中使用它时没有可用的无参数构造函数,也没有用上述属性装饰。
在这里,我们添加了一个显式的无参数构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用仅初始化的设置器创建的。因为这没有使用 DataContractAttribute 属性或任何成员上的 DataMemberAttribute 属性,序列化器将无法在反序列化期间正确映射目标成员。
public record Doodad ( Guid Id , string Name , int Count )
{
public Doodad () {}
}
这种方法不需要额外的构造函数,而是依赖于序列化属性。因为我们用 DataContractAttribute 属性标记类型,并为每个成员装饰自己的 DataMemberAttribute 属性,序列化引擎将能够从 XML 文档映射到我们的类型而不会出现问题。
[DataContract]
public record Doodad (
[property: DataMember] Guid Id ,
[property: DataMember] string Name ,
[property: DataMember] int Count )
支持的原始类型 .NET 中有几种内置类型被认为是原始类型,并且可以在不需要开发人员额外努力的情况下进行序列化:
还有其他类型实际上不是原始类型,但具有类似的内置支持:
同样,如果您想通过 actor 方法传递这些类型,则不需要额外的考虑,因为它们将被序列化和反序列化而不会出现问题。此外,标记为 (SerializeableAttribute)[https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute] 属性的类型将被序列化。
枚举类型 枚举,包括标志枚举,如果适当标记,可以序列化。您希望序列化的枚举成员必须标记为 EnumMemberAttribute 属性才能被序列化。在此属性的可选 Value 参数中传递自定义值将允许您指定用于成员的值,而不是让序列化器从成员的名称中派生它。
枚举类型不需要用 DataContractAttribute
属性装饰——只需要您希望序列化的成员用 EnumMemberAttribute
属性装饰。
public enum Colors
{
[EnumMember]
Red ,
[EnumMember(Value="g")]
Green ,
Blue , //即使被类型使用,此值也不会被序列化,因为它没有用 EnumMember 属性装饰
}
集合类型 对于数据契约序列化器,所有实现 IEnumerable 接口的集合类型,包括数组和泛型集合,都被视为集合。那些实现 IDictionary 或泛型 IDictionary<TKey, TValue> 的类型被视为字典集合;所有其他类型是列表集合。
与其他复杂类型类似,集合类型必须有一个可用的无参数构造函数。此外,它们还必须有一个名为 Add 的方法,以便能够正确序列化和反序列化。这些集合类型使用的类型本身必须标记为 DataContractAttribute
属性或如本文档中所述的其他可序列化类型。
数据契约版本控制 由于数据契约序列化器仅在 Dapr 中用于通过代理方法将 .NET SDK 中的值序列化到 Dapr actor 实例中,因此几乎不需要考虑数据契约的版本控制,因为数据不会在使用相同序列化器的应用程序版本之间持久化。对于那些有兴趣了解更多关于数据契约版本控制的人,请访问这里 。
已知类型 通过将每个类型标记为 DataContractAttribute 属性,可以轻松地嵌套您自己的复杂类型。这会通知序列化器如何执行反序列化。
但如果您正在处理多态类型,并且您的成员之一是具有派生类或其他实现的基类或接口,该怎么办?在这里,您将使用 KnownTypeAttribute 属性来提示序列化器如何继续。
当您将 KnownTypeAttribute 属性应用于类型时,您是在通知数据契约序列化器它可能遇到的子类型,从而允许它正确处理这些类型的序列化和反序列化,即使运行时的实际类型与声明的类型不同。
[DataContract]
[KnownType(typeof(DerivedClass))]
public class BaseClass
{
//基类的成员
}
[DataContract]
public class DerivedClass : BaseClass
{
//派生类的附加成员
}
在此示例中,BaseClass
被标记为 [KnownType(typeof(DerivedClass))]
,这告诉数据契约序列化器 DerivedClass
是 BaseClass
的可能实现,它可能需要序列化或反序列化。如果没有此属性,当序列化器遇到一个实际上是 DerivedClass
类型的 BaseClass
实例时,它将不知道如何处理派生类型,这可能导致序列化异常。通过将所有可能的派生类型指定为已知类型,您可以确保序列化器能够正确处理类型及其成员。
有关使用 [KnownType]
的更多信息和示例,请参阅官方文档 。
1.2.4 - 如何:在 .NET SDK 中运行和使用虚拟 actor 通过此示例尝试 .NET Dapr 虚拟 actor
Dapr actor 包使您能够从 .NET 应用程序中与 Dapr 虚拟 actor 交互。在本指南中,您将学习如何:
创建一个 actor (MyActor
)。 在客户端应用程序上调用其方法。 MyActor --- MyActor.Interfaces
|
+- MyActorService
|
+- MyActorClient
接口项目 (\MyActor\MyActor.Interfaces)
此项目包含 actor 的接口定义。actor 接口可以在任何项目中定义,名称不限。接口定义了 actor 实现和调用 actor 的客户端共享的 actor 合约:
由于客户端项目可能依赖于它,最好将其定义在与 actor 实现分开的程序集内。
actor 服务项目 (\MyActor\MyActorService)
此项目实现了托管 actor 的 ASP.Net Core Web 服务。它包含 actor 的实现,MyActor.cs
。actor 实现是一个类,它:
派生自基础类型 actor 实现 MyActor.Interfaces
项目中定义的接口。 actor 类还必须实现一个构造函数,该构造函数接受一个 ActorService
实例和一个 ActorId
,并将它们传递给基础 actor 类。
actor 客户端项目 (\MyActor\MyActorClient)
此项目包含 actor 客户端的实现,该客户端调用在 actor 接口中定义的 MyActor 的方法。
准备工作
注意 请注意,虽然 .NET 6 通常作为 Dapr .NET SDK 包的最低 .NET 要求得到支持,而 .NET 7 是 Dapr v1.15 中 Dapr.Workflows 的最低支持版本,但只有 .NET 8 和 .NET 9 将继续在 v1.16 及更高版本中得到 Dapr 的支持。步骤 0:准备 我们将创建 3 个项目,请选择一个空目录开始,并在您选择的终端中打开它。
步骤 1:创建 actor 接口 actor 接口定义了 actor 实现和调用 actor 的客户端共享的 actor 合约。
actor 接口定义如下要求:
actor 接口必须继承 Dapr.Actors.IActor
接口 actor 方法的返回类型必须是 Task
或 Task<object>
actor 方法最多可以有一个参数 创建接口项目并添加依赖项 # 创建 actor 接口
dotnet new classlib -o MyActor.Interfaces
cd MyActor.Interfaces
# 添加 Dapr.Actors nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors
cd ..
实现 IMyActor 接口 定义 IMyActor
接口和 MyData
数据对象。将以下代码粘贴到 MyActor.Interfaces
项目的 MyActor.cs
中。
using Dapr.Actors ;
using Dapr.Actors.Runtime ;
using System.Threading.Tasks ;
namespace MyActor.Interfaces
{
public interface IMyActor : IActor
{
Task < string > SetDataAsync ( MyData data );
Task < MyData > GetDataAsync ();
Task RegisterReminder ();
Task UnregisterReminder ();
Task < IActorReminder > GetReminder ();
Task RegisterTimer ();
Task UnregisterTimer ();
}
public class MyData
{
public string PropertyA { get ; set ; }
public string PropertyB { get ; set ; }
public override string ToString ()
{
var propAValue = this . PropertyA == null ? "null" : this . PropertyA ;
var propBValue = this . PropertyB == null ? "null" : this . PropertyB ;
return $"PropertyA: {propAValue}, PropertyB: {propBValue}" ;
}
}
}
步骤 2:创建 actor 服务 Dapr 使用 ASP.NET Web 服务来托管 actor 服务。本节将实现 IMyActor
actor 接口并将 actor 注册到 Dapr 运行时。
创建 actor 服务项目并添加依赖项 # 创建 ASP.Net Web 服务以托管 Dapr actor
dotnet new web -o MyActorService
cd MyActorService
# 添加 Dapr.Actors.AspNetCore nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors.AspNetCore
# 添加 actor 接口引用
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
添加 actor 实现 实现 IMyActor 接口并从 Dapr.Actors.Actor
类派生。以下示例还展示了如何使用 actor reminder。对于使用 reminder 的 actor,它必须从 IRemindable 派生。如果您不打算使用 reminder 功能,可以跳过实现 IRemindable 和 reminder 特定的方法,这些方法在下面的代码中显示。
将以下代码粘贴到 MyActorService
项目的 MyActor.cs
中:
using Dapr.Actors ;
using Dapr.Actors.Runtime ;
using MyActor.Interfaces ;
using System ;
using System.Threading.Tasks ;
namespace MyActorService
{
internal class MyActor : Actor , IMyActor , IRemindable
{
// 构造函数必须接受 ActorHost 作为参数,并且还可以接受将从依赖注入容器中检索的其他参数
//
/// <summary>
/// 初始化 MyActor 的新实例
/// </summary>
/// <param name="host">将托管此 actor 实例的 Dapr.Actors.Runtime.ActorHost。</param>
public MyActor ( ActorHost host )
: base ( host )
{
}
/// <summary>
/// 每当 actor 被激活时调用此方法。
/// actor 在其任何方法首次被调用时被激活。
/// </summary>
protected override Task OnActivateAsync ()
{
// 提供执行一些可选设置的机会。
Console . WriteLine ( $"Activating actor id: {this.Id}" );
return Task . CompletedTask ;
}
/// <summary>
/// 每当 actor 在一段时间不活动后被停用时调用此方法。
/// </summary>
protected override Task OnDeactivateAsync ()
{
// 提供执行可选清理的机会。
Console . WriteLine ( $"Deactivating actor id: {this.Id}" );
return Task . CompletedTask ;
}
/// <summary>
/// 将 MyData 设置到 actor 的私有状态存储中
/// </summary>
/// <param name="data">用户定义的 MyData,将作为 "my_data" 状态存储到状态存储中</param>
public async Task < string > SetDataAsync ( MyData data )
{
// 数据在每次方法执行后由 actor 的运行时隐式保存到配置的状态存储中。
// 数据也可以通过调用 this.StateManager.SaveStateAsync() 显式保存。
// 要保存的状态必须是 DataContract 可序列化的。
await this . StateManager . SetStateAsync < MyData >(
"my_data" , // 状态名称
data ); // 为命名状态 "my_data" 保存的数据
return "Success" ;
}
/// <summary>
/// 从 actor 的私有状态存储中获取 MyData
/// </summary>
/// <return>存储到状态存储中的用户定义的 MyData,作为 "my_data" 状态</return>
public Task < MyData > GetDataAsync ()
{
// 从状态存储中获取状态。
return this . StateManager . GetStateAsync < MyData >( "my_data" );
}
/// <summary>
/// 向 actor 注册 MyReminder reminder
/// </summary>
public async Task RegisterReminder ()
{
await this . RegisterReminderAsync (
"MyReminder" , // reminder 的名称
null , // 传递给 IRemindable.ReceiveReminderAsync() 的用户状态
TimeSpan . FromSeconds ( 5 ), // 在首次调用 reminder 之前的延迟时间
TimeSpan . FromSeconds ( 5 )); // 在首次调用后 reminder 调用之间的时间间隔
}
/// <summary>
/// 获取 actor 的 MyReminder reminder 详细信息
/// </summary>
public async Task < IActorReminder > GetReminder ()
{
await this . GetReminderAsync ( "MyReminder" );
}
/// <summary>
/// 取消注册 actor 的 MyReminder reminder
/// </summary>
public Task UnregisterReminder ()
{
Console . WriteLine ( "Unregistering MyReminder..." );
return this . UnregisterReminderAsync ( "MyReminder" );
}
// <summary>
// 实现 IRemindeable.ReceiveReminderAsync(),这是在 actor reminder 触发时调用的回调。
// </summary>
public Task ReceiveReminderAsync ( string reminderName , byte [] state , TimeSpan dueTime , TimeSpan period )
{
Console . WriteLine ( "ReceiveReminderAsync is called!" );
return Task . CompletedTask ;
}
/// <summary>
/// 向 actor 注册 MyTimer timer
/// </summary>
public Task RegisterTimer ()
{
return this . RegisterTimerAsync (
"MyTimer" , // timer 的名称
nameof ( this . OnTimerCallBack ), // timer 回调
null , // 传递给 OnTimerCallback() 的用户状态
TimeSpan . FromSeconds ( 5 ), // 在首次调用异步回调之前的延迟时间
TimeSpan . FromSeconds ( 5 )); // 异步回调调用之间的时间间隔
}
/// <summary>
/// 取消注册 actor 的 MyTimer timer
/// </summary>
public Task UnregisterTimer ()
{
Console . WriteLine ( "Unregistering MyTimer..." );
return this . UnregisterTimerAsync ( "MyTimer" );
}
/// <summary>
/// timer 到期后调用的回调
/// </summary>
private Task OnTimerCallBack ( byte [] data )
{
Console . WriteLine ( "OnTimerCallBack is called!" );
return Task . CompletedTask ;
}
}
}
使用 ASP.NET Core 注册 actor 运行时 actor 运行时通过 ASP.NET Core 的 Startup.cs
进行配置。
运行时使用 ASP.NET Core 依赖注入系统来注册 actor 类型和必要的服务。此集成通过 ConfigureServices(...)
中的 AddActors(...)
方法调用提供。使用传递给 AddActors(...)
的委托来注册 actor 类型并配置 actor 运行时设置。您可以在 ConfigureServices(...)
中注册其他类型以进行依赖注入。这些将可用于注入到您的 actor 类型的构造函数中。
actor 是通过与 Dapr 运行时的 HTTP 调用实现的。此功能是应用程序 HTTP 处理管道的一部分,并在 Configure(...)
中的 UseEndpoints(...)
内注册。
将以下代码粘贴到 MyActorService
项目的 Startup.cs
中:
using Microsoft.AspNetCore.Builder ;
using Microsoft.AspNetCore.Hosting ;
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Hosting ;
namespace MyActorService
{
public class Startup
{
public void ConfigureServices ( IServiceCollection services )
{
services . AddActors ( options =>
{
// 注册 actor 类型并配置 actor 设置
options . Actors . RegisterActor < MyActor >();
});
}
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
app . UseRouting ();
// 注册与 Dapr 运行时接口的 actor 处理程序。
app . MapActorsHandlers ();
}
}
}
步骤 3:添加客户端 创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 actor 代理客户端来调用 actor 接口中定义的 actor 方法。
创建 actor 客户端项目并添加依赖项 # 创建 actor 的客户端
dotnet new console -o MyActorClient
cd MyActorClient
# 添加 Dapr.Actors nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors
# 添加 actor 接口引用
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
使用强类型客户端调用 actor 方法 您可以使用 ActorProxy.Create<IMyActor>(..)
创建一个强类型客户端并调用 actor 的方法。
将以下代码粘贴到 MyActorClient
项目的 Program.cs
中:
using System ;
using System.Threading.Tasks ;
using Dapr.Actors ;
using Dapr.Actors.Client ;
using MyActor.Interfaces ;
namespace MyActorClient
{
class Program
{
static async Task MainAsync ( string [] args )
{
Console . WriteLine ( "Startup up..." );
// 在 actor 服务中注册的 actor 类型
var actorType = "MyActor" ;
// ActorId 唯一标识一个 actor 实例
// 如果与此 id 匹配的 actor 不存在,将会创建它
var actorId = new ActorId ( "1" );
// 使用服务实现的相同接口创建本地代理。
//
// 您需要提供类型和 id,以便可以定位 actor。
var proxy = ActorProxy . Create < IMyActor >( actorId , actorType );
// 现在您可以使用 actor 接口调用 actor 的方法。
Console . WriteLine ( $"Calling SetDataAsync on {actorType}:{actorId}..." );
var response = await proxy . SetDataAsync ( new MyData ()
{
PropertyA = "ValueA" ,
PropertyB = "ValueB" ,
});
Console . WriteLine ( $"Got response: {response}" );
Console . WriteLine ( $"Calling GetDataAsync on {actorType}:{actorId}..." );
var savedData = await proxy . GetDataAsync ();
Console . WriteLine ( $"Got response: {savedData}" );
}
}
}
运行代码 您创建的项目现在可以测试示例。
运行 MyActorService
由于 MyActorService
托管 actor,因此需要使用 Dapr CLI 运行。
cd MyActorService
dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
您将在此终端中看到来自 daprd
和 MyActorService
的命令行输出。您应该看到类似以下内容的内容,这表明应用程序已成功启动。
...
ℹ️ Updating metadata for app command: dotnet run
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Now listening on: https://localhost:5001
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Now listening on: http://localhost:5000
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Application started. Press Ctrl+C to shut down.
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Hosting environment: Development
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Content root path: /Users/ryan/actortest/MyActorService
运行 MyActorClient
MyActorClient
作为客户端,可以通过 dotnet run
正常运行。
打开一个新终端并导航到 MyActorClient
目录。然后运行项目:
您应该看到类似以下的命令行输出:
Startup up...
Calling SetDataAsync on MyActor:1...
Got response: Success
Calling GetDataAsync on MyActor:1...
Got response: PropertyA: ValueA, PropertyB: ValueB
💡 此示例依赖于一些假设。ASP.NET Core Web 项目的默认监听端口是 5000,这被传递给 dapr run
作为 --app-port 5000
。Dapr sidecar 的默认 HTTP 端口是 3500。我们告诉 MyActorService
的 sidecar 使用 3500,以便 MyActorClient
可以依赖默认值。
现在您已成功创建了一个 actor 服务和客户端。请参阅相关链接部分以了解更多信息。
相关链接 1.3 - Dapr Workflow .NET SDK 快速上手并掌握 Dapr Workflow 和 Dapr .NET SDK 的使用
1.3.1 - DaprWorkflowClient 使用 使用 DaprWorkflowClient 的基本提示和建议
生命周期管理 DaprWorkflowClient
可以访问网络资源,这些资源通过 TCP 套接字与 Dapr sidecar 以及其他用于管理和操作工作流的类型进行通信。DaprWorkflowClient
实现了 IAsyncDisposable
接口,以便快速清理资源。
依赖注入 AddDaprWorkflow()
方法用于通过 ASP.NET Core 的依赖注入机制注册 Dapr 工作流服务。此方法需要一个选项委托,用于定义您希望在应用程序中注册和使用的每个工作流和活动。
注意 此方法会尝试注册一个 DaprClient
实例,但仅在尚未以其他生命周期注册的情况下才有效。例如,如果之前以单例生命周期调用了 AddDaprClient()
,那么无论为工作流客户端选择何种生命周期,都会始终使用单例。DaprClient
实例用于与 Dapr sidecar 通信,如果尚未注册,则在 AddDaprWorkflow()
注册期间提供的生命周期将用于注册 DaprWorkflowClient
及其依赖项。单例注册 默认情况下,AddDaprWorkflow
方法会以单例生命周期注册 DaprWorkflowClient
和相关服务。这意味着服务只会被实例化一次。
以下是在典型的 Program.cs
文件中注册 DaprWorkflowClient
的示例:
builder . Services . AddDaprWorkflow ( options => {
options . RegisterWorkflow < YourWorkflow >();
options . RegisterActivity < YourActivity >();
});
var app = builder . Build ();
await app . RunAsync ();
作用域注册 虽然默认的单例注册通常适用,但您可能希望指定不同的生命周期。这可以通过在 AddDaprWorkflow
中传递一个 ServiceLifetime
参数来实现。例如,您可能需要将另一个作用域服务注入到 ASP.NET Core 处理管道中,该管道需要 DaprClient
使用的上下文,如果前者服务注册为单例,则无法使用。
以下示例演示了这一点:
builder . Services . AddDaprWorkflow ( options => {
options . RegisterWorkflow < YourWorkflow >();
options . RegisterActivity < YourActivity >();
}, ServiceLifecycle . Scoped );
var app = builder . Build ();
await app . RunAsync ();
瞬态注册 最后,Dapr 服务也可以使用瞬态生命周期注册,这意味着每次注入时都会重新初始化。这在以下示例中演示:
builder . Services . AddDaprWorkflow ( options => {
options . RegisterWorkflow < YourWorkflow >();
options . RegisterActivity < YourActivity >();
}, ServiceLifecycle . Transient );
var app = builder . Build ();
await app . RunAsync ();
将服务注入到工作流活动中 工作流活动支持现代 C# 应用程序中常用的依赖注入。假设在启动时进行了适当的注册,任何此类类型都可以注入到工作流活动的构造函数中,并在工作流执行期间使用。这使得通过注入的 ILogger
添加日志记录或通过注入 DaprClient
或 DaprJobsClient
访问其他 Dapr 组件变得简单。
internal sealed class SquareNumberActivity : WorkflowActivity < int , int >
{
private readonly ILogger _logger ;
public MyActivity ( ILogger logger )
{
this . _logger = logger ;
}
public override Task < int > RunAsync ( WorkflowActivityContext context , int input )
{
this . _logger . LogInformation ( "Squaring the value {number}" , input );
var result = input * input ;
this . _logger . LogInformation ( "Got a result of {squareResult}" , result );
return Task . FromResult ( result );
}
}
在工作流中使用 ILogger 由于工作流必须是确定性的,因此不能将任意服务注入其中。例如,如果您能够将标准 ILogger
注入到工作流中,并且由于错误需要重放它,日志记录的重复操作可能会导致混淆,因为这些操作实际上并没有再次发生。为了解决这个问题,工作流中提供了一种重放安全的日志记录器。它只会在工作流第一次运行时记录事件,而在重放时不会记录任何内容。
这种日志记录器可以通过工作流实例上的 WorkflowContext
中的方法获取,并可以像使用 ILogger
实例一样使用。
一个展示此功能的完整示例可以在 .NET SDK 仓库 中找到,以下是该示例的简要摘录。
public class OrderProcessingWorkflow : Workflow < OrderPayload , OrderResult >
{
public override async Task < OrderResult > RunAsync ( WorkflowContext context , OrderPayload order )
{
string orderId = context . InstanceId ;
var logger = context . CreateReplaySafeLogger < OrderProcessingWorkflow >(); //使用此方法访问日志记录器实例
logger . LogInformation ( "Received order {orderId} for {quantity} {name} at ${totalCost}" , orderId , order . Quantity , order . Name , order . TotalCost );
//...
}
}
1.3.2 - 如何:在 .NET SDK 中编写和管理 Dapr 工作流 学习如何使用 .NET SDK 编写和管理 Dapr 工作流
我们来创建一个 Dapr 工作流并通过控制台调用它。在提供的订单处理工作流示例 中,控制台会提示如何进行购买和补货。在本指南中,您将:
在 .NET 示例项目里:
先决条件
注意 Dapr.Workflows 在 v1.15 中支持 .NET 7 或更高版本。然而,从 Dapr v1.16 开始,仅支持 .NET 8 和 .NET 9。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 工作流示例。
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 WorkflowConsoleApp
目录。
启动程序。
在一个新的终端中,再次导航到 WorkflowConsoleApp
目录,并在程序旁边运行 Dapr sidecar。
dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500
Dapr 会监听 HTTP 请求在 http://localhost:3500
和内部工作流 gRPC 请求在 http://localhost:4001
。
启动工作流 要启动工作流,您有两种选择:
按照控制台提示的指示。 使用工作流 API 并直接向 Dapr 发送请求。 本指南重点介绍工作流 API 选项。
注意 您可以在 WorkflowConsoleApp
/demo.http
文件中找到以下命令。 curl 请求的主体是作为工作流输入的采购订单信息。 命令中的 “12345678” 表示工作流的唯一标识符,可以替换为您选择的任何标识符。 运行以下命令以启动工作流。
curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID= 12345678 \
-H "Content-Type: application/json" \
-d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'
curl -i -X POST http : // localhost : 3500 / v1 . 0 / workflows / dapr / OrderProcessingWorkflow / start ? instanceID = 12345678 `
-H "Content-Type: application/json" `
-d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'
如果成功,您应该会看到如下响应:
{ "instanceID" : "12345678" }
发送 HTTP 请求以获取已启动工作流的状态:
curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/12345678
工作流设计为需要几秒钟才能完成。如果在您发出 HTTP 请求时工作流尚未完成,您将看到以下 JSON 响应(为便于阅读而格式化),工作流状态为 RUNNING
:
{
"instanceID" : "12345678" ,
"workflowName" : "OrderProcessingWorkflow" ,
"createdAt" : "2023-05-10T00:42:03.911444105Z" ,
"lastUpdatedAt" : "2023-05-10T00:42:06.142214153Z" ,
"runtimeStatus" : "RUNNING" ,
"properties" : {
"dapr.workflow.custom_status" : "" ,
"dapr.workflow.input" : "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}"
}
}
一旦工作流完成运行,您应该会看到以下输出,表明它已达到 COMPLETED
状态:
{
"instanceID" : "12345678" ,
"workflowName" : "OrderProcessingWorkflow" ,
"createdAt" : "2023-05-10T00:42:03.911444105Z" ,
"lastUpdatedAt" : "2023-05-10T00:42:18.527704176Z" ,
"runtimeStatus" : "COMPLETED" ,
"properties" : {
"dapr.workflow.custom_status" : "" ,
"dapr.workflow.input" : "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" ,
"dapr.workflow.output" : "{\"Processed\":true}"
}
}
当工作流完成时,工作流应用程序的标准输出应如下所示:
info: WorkflowConsoleApp.Activities.NotifyActivity[0]
Received order 12345678 for Paperclips at $99.95
info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0]
Reserving inventory: 12345678, Paperclips, 1
info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0]
Processing payment: 12345678, 99.95, USD
info: WorkflowConsoleApp.Activities.NotifyActivity[0]
Order 12345678 processed successfully!
如果您在本地机器上为 Dapr 配置了 Zipkin,那么您可以在 Zipkin Web UI(通常在 http://localhost:9411/zipkin/)中查看工作流跟踪跨度。
演示 观看此视频演示 .NET 工作流 :
下一步 1.4 - Dapr AI .NET SDK 快速上手使用 Dapr AI .NET SDK
使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。
目前,Dapr 提供了一个会话 API,用于与大型语言模型进行交互。要开始使用此功能,请参阅 Dapr 会话 AI 指南。
1.4.1 - Dapr AI 客户端 学习如何创建 Dapr AI 客户端
Dapr AI 客户端包使您能够与 Dapr sidecar 提供的 AI 功能进行交互。
生命周期的管理 DaprConversationClient
是专门用于与 Dapr conversation API 交互的客户端版本。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,以便访问网络资源。
为了获得最佳性能,建议创建一个长期存在的 DaprConversationClient
实例,并在整个应用程序中共享使用。DaprConversationClient
实例是线程安全的,适合共享。
这可以通过依赖注入来实现。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以利用 IConfiguration
或其他注入服务中的值进行注册,这在每个类中从头创建客户端时是不切实际的。
避免为每个操作都创建一个新的 DaprConversationClient
。
通过 DaprConversationClientBuilder 配置 DaprConversationClient 可以通过在 DaprConversationClientBuilder
类上调用方法来配置 DaprConversationClient
,然后调用 .Build()
来创建客户端。每个 DaprConversationClient
的设置是独立的,并且在调用 .Build()
后无法更改。
var daprConversationClient = new DaprConversationClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于验证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprConversationClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于验证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 gRPC 端点DAPR_API_TOKEN
:用于设置 API 令牌配置 gRPC 通道选项 Dapr 使用 CancellationToken
进行取消依赖于 gRPC 通道选项的配置。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置 。
var daprConversationClient = new DaprConversationClientBuilder ()
. UseGrpcChannelOptions ( new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
. Build ();
使用 DaprConversationClient
进行取消 DaprConversationClient
上的 API 执行异步操作并接受一个可选的 CancellationToken
参数。这是 .NET 中用于可取消操作的标准做法。请注意,当取消发生时,不能保证远程端点会停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出一个 OperationCancelledException
。
通过依赖注入配置 DaprConversationClient
使用内置的扩展方法在依赖注入容器中注册 DaprConversationClient
可以提供一次注册长期服务的好处,集中复杂的配置,并通过确保在可能的情况下重新利用类似的长期资源(例如 HttpClient
实例)来提高性能。
有三种重载可用,以便开发人员在为其场景配置客户端时具有最大的灵活性。每个重载都会代表您注册 IHttpClientFactory
(如果尚未注册),并配置 DaprConversationClientBuilder
以在创建 HttpClient
实例时使用它,以便尽可能多地重用相同的实例,避免套接字耗尽和其他问题。
在第一种方法中,开发人员没有进行任何配置,DaprConversationClient
使用默认设置进行配置。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprConversationClient (); // 注册 `DaprConversationClient` 以便根据需要注入
var app = builder . Build ();
有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 DaprConversationClientBuiler
的重载来完成的,并公开用于配置必要选项的方法。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprConversationClient (( _ , daprConversationClientBuilder ) => {
// 设置 API 令牌
daprConversationClientBuilder . UseDaprApiToken ( "abc123" );
// 指定一个非标准的 HTTP 端点
daprConversationClientBuilder . UseHttpEndpoint ( "http://dapr.my-company.com" );
});
var app = builder . Build ();
最后,开发人员可能需要从其他服务中检索信息以填充这些配置值。该值可以从 DaprClient
实例、供应商特定的 SDK 或某些本地服务中提供,但只要它也在 DI 中注册,就可以通过最后一个重载将其注入到此配置操作中:
var builder = WebApplication . CreateBuilder ( args );
// 注册一个虚构的服务,从某处检索 secret
builder . Services . AddSingleton < SecretService >();
builder . Services . AddDaprConversationClient (( serviceProvider , daprConversationClientBuilder ) => {
// 从服务提供者中检索 `SecretService` 的实例
var secretService = serviceProvider . GetRequiredService < SecretService >();
var daprApiToken = secretService . GetSecret ( "DaprApiToken" ). Value ;
// 配置 `DaprConversationClientBuilder`
daprConversationClientBuilder . UseDaprApiToken ( daprApiToken );
});
var app = builder . Build ();
1.4.2 - 如何在 .NET SDK 中创建和使用 Dapr AI 会话 学习如何使用 .NET SDK 创建和使用 Dapr 会话 AI 客户端
前提条件
注意事项 .NET 6 是此版本中 Dapr .NET SDK 包的最低支持版本。仅 .NET 8 和 .NET 9 将在 Dapr v1.16 及更高版本中得到支持。安装 要开始使用 Dapr AI .NET SDK 客户端,请从 NuGet 安装 Dapr.AI 包 :
dotnet add package Dapr.AI
DaprConversationClient
通过 TCP 套接字形式维护对网络资源的访问,用于与 Dapr sidecar 通信。
依赖注入 AddDaprAiConversation()
方法将注册 Dapr 客户端到 ASP.NET Core 的依赖注入中,这是使用此包的推荐方法。此方法接受一个可选的选项委托,用于配置 DaprConversationClient
,以及一个 ServiceLifetime
参数,允许您为注册的服务指定不同的生命周期,而不是默认的 Singleton
值。
以下示例假设所有默认值均可接受,并足以注册 DaprConversationClient
:
services . AddDaprAiConversation ();
可选的配置委托用于通过在 DaprConversationClientBuilder
上指定选项来配置 DaprConversationClient
,如下例所示:
services . AddSingleton < DefaultOptionsProvider >();
services . AddDaprAiConversation (( serviceProvider , clientBuilder ) => {
//注入服务以获取值
var optionsProvider = serviceProvider . GetRequiredService < DefaultOptionsProvider >();
var standardTimeout = optionsProvider . GetStandardTimeout ();
//在客户端构建器上配置值
clientBuilder . UseTimeout ( standardTimeout );
});
手动实例化 除了使用依赖注入,还可以使用静态客户端构建器构建 DaprConversationClient
。
为了获得最佳性能,请创建一个长期使用的 DaprConversationClient
实例,并在整个应用程序中共享该实例。DaprConversationClient
实例是线程安全的,旨在共享。
避免为每个操作创建一个新的 DaprConversationClient
。
可以通过在调用 .Build()
创建客户端之前调用 DaprConversationClientBuilder
类上的方法来配置 DaprConversationClient
。每个 DaprConversationClient
的设置是独立的,调用 .Build()
后无法更改。
var daprConversationClient = new DaprConversationClientBuilder ()
. UseJsonSerializerSettings ( ... ) //配置 JSON 序列化器
. Build ();
有关通过构建器配置 Dapr 客户端时可用选项的更多信息,请参阅 .NET 文档 。
动手试试 测试 Dapr AI .NET SDK。通过示例查看 Dapr 的实际应用:
SDK 示例 描述 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始使用。
基础模块 .NET SDK 的这一部分允许您与会话 API 接口,以便从大型语言模型发送和接收消息。
发送消息 1.5 - Dapr Jobs .NET SDK 快速上手使用 Dapr Jobs 和 Dapr .NET SDK
使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 进行交互。通过预设的计划,您可以安排未来的操作,并可选择附带数据。
要开始使用,请查看 Dapr Jobs 指南,并参考 最佳实践文档 以获取更多指导。
1.5.1 - 如何:在 .NET SDK 中编写和管理 Dapr 任务 学习如何使用 .NET SDK 编写和管理 Dapr 任务
我们来创建一个端点,该端点将在 Dapr 任务触发时被调用,然后在同一个应用中调度该任务。我们将使用此处提供的简单示例 ,进行以下演示,并通过它来解释如何使用间隔或 Cron 表达式自行调度一次性或重复性任务。在本指南中,您将:
部署一个 .NET Web API 应用程序 (JobsSample ) 利用 Dapr .NET 任务 SDK 调度任务调用并设置被触发的端点 在 .NET 示例项目中:
前提条件
注意 请注意,虽然 .NET 6 是 Dapr v1.15 中支持的最低版本,但从 v1.16 开始,Dapr 仅支持 .NET 8 和 .NET 9。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 任务示例。
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 JobsSample
目录。
我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。
dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 监听 HTTP 请求在 http://localhost:3500
和内部任务 gRPC 请求在 http://localhost:4001
。
使用依赖注入注册 Dapr 任务客户端 Dapr 任务 SDK 提供了一个扩展方法来简化 Dapr 任务客户端的注册。在 Program.cs
中完成依赖注入注册之前,添加以下行:
var builder = WebApplication . CreateBuilder ( args );
//在这两行之间的任意位置添加
builder . Services . AddDaprJobsClient (); //这样就完成了
var app = builder . Build ();
请注意,在当前的任务 API 实现中,调度任务的应用也将是接收触发通知的应用。换句话说,您不能调度一个触发器在另一个应用中运行。因此,虽然您不需要在应用中显式注册 Dapr 任务客户端来调度触发调用端点,但如果没有同一个应用以某种方式调度任务(无论是通过此 Dapr 任务 .NET SDK 还是对 sidecar 的 HTTP 调用),您的端点将永远不会被调用。
您可能希望为 Dapr 任务客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprJobsClientBuilder
实例的注册方法重载来实现:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (( _ , daprJobsClientBuilder ) =>
{
daprJobsClientBuilder . UseDaprApiToken ( "abc123" );
daprJobsClientBuilder . UseHttpEndpoint ( "http://localhost:8512" ); //非标准 sidecar HTTP 端点
});
var app = builder . Build ();
如果您需要从其他来源检索注入的值,这些来源本身注册为依赖项,您可以使用另一个重载来将 IServiceProvider
注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret,并将其传递到 AddDaprJobClient
的配置方法中,以便我们可以从其他地方检索我们的 Dapr API 令牌以在此处注册:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddSingleton < SecretRetriever >();
builder . Services . AddDaprJobsClient (( serviceProvider , daprJobsClientBuilder ) =>
{
var secretRetriever = serviceProvider . GetRequiredService < SecretRetriever >();
var daprApiToken = secretRetriever . GetSecret ( "DaprApiToken" ). Value ;
daprJobsClientBuilder . UseDaprApiToken ( daprApiToken );
daprJobsClientBuilder . UseHttpEndpoint ( "http://localhost:8512" );
});
var app = builder . Build ();
使用 IConfiguration 配置 Dapr 任务客户端 可以使用注册的 IConfiguration
中的值来配置 Dapr 任务客户端,而无需显式指定每个值的重写,如前一节中使用 DaprJobsClientBuilder
所示。相反,通过填充通过依赖注入提供的 IConfiguration
,AddDaprJobsClient()
注册将自动使用这些值覆盖其各自的默认值。
首先在您的配置中填充值。这可以通过以下示例中的几种不同方式完成。
通过 ConfigurationBuilder
配置 应用程序设置可以在不使用配置源的情况下配置,而是通过填充 ConfigurationBuilder
实例中的值来实现:
var builder = WebApplication . CreateBuilder ();
//创建配置
var configuration = new ConfigurationBuilder ()
. AddInMemoryCollection ( new Dictionary < string , string > {
{ "DAPR_HTTP_ENDPOINT" , "http://localhost:54321" },
{ "DAPR_API_TOKEN" , "abc123" }
})
. Build ();
builder . Configuration . AddConfiguration ( configuration );
builder . Services . AddDaprJobsClient (); //这将自动从 IConfiguration 中填充 HTTP 端点和 API 令牌值
通过环境变量配置 应用程序设置可以从应用程序可用的环境变量中访问。
以下环境变量将用于填充用于注册 Dapr 任务客户端的 HTTP 端点和 API 令牌。
Key Value DAPR_HTTP_ENDPOINT http://localhost:54321 DAPR_API_TOKEN abc123
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ();
builder . Services . AddDaprJobsClient ();
Dapr 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
通过前缀环境变量配置 然而,在共享主机场景中,多个应用程序都在同一台机器上运行而不使用容器或在开发环境中,前缀环境变量并不罕见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:
Key Value myapp_DAPR_HTTP_ENDPOINT http://localhost:54321 myapp_DAPR_API_TOKEN abc123
这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ( prefix : "myapp_" );
builder . Services . AddDaprJobsClient ();
Dapr 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
不依赖于依赖注入使用 Dapr 任务客户端 虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更容易,但您不需要以这种方式注册 DaprJobsClient
。相反,您也可以选择从 DaprJobsClientBuilder
实例创建它的实例,如下所示:
public class MySampleClass
{
public void DoSomething ()
{
var daprJobsClientBuilder = new DaprJobsClientBuilder ();
var daprJobsClient = daprJobsClientBuilder . Build ();
//使用 `daprJobsClient` 做一些事情
}
}
设置一个在任务触发时被调用的端点 如果您熟悉 ASP.NET Core 中的最小 API ,那么设置一个任务端点很简单,因为两者的语法相同。
一旦完成依赖注入注册,按照您在 ASP.NET Core 中使用最小 API 功能处理 HTTP 请求映射的方式配置应用程序。实现为扩展方法,传递它应该响应的任务名称和一个委托。服务可以根据需要注入到委托的参数中,您可以选择传递 JobDetails
以获取有关已触发任务的信息(例如,访问其调度设置或负载)。
这里有两个委托可以使用。一个提供 IServiceProvider
以防您需要将其他服务注入处理程序:
//我们从上面的示例中得到了这个
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient ();
var app = builder . Build ();
//添加我们的端点注册
app . MapDaprScheduledJob ( "myJob" , ( IServiceProvider serviceProvider , string? jobName , JobDetails ? jobDetails ) => {
var logger = serviceProvider . GetService < ILogger >();
logger ?. LogInformation ( "Received trigger invocation for '{jobName}'" , "myJob" );
//做一些事情...
});
app . Run ();
如果不需要,委托的另一个重载不需要 IServiceProvider
:
//我们从上面的示例中得到了这个
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient ();
var app = builder . Build ();
//添加我们的端点注册
app . MapDaprScheduledJob ( "myJob" , ( string? jobName , JobDetails ? jobDetails ) => {
//做一些事情...
});
app . Run ();
注册任务 最后,我们必须注册我们想要调度的任务。请注意,从这里开始,所有 SDK 方法都支持取消令牌,并在未设置时使用默认令牌。
有三种不同的方式来设置任务,具体取决于您想要如何配置调度:
一次性任务 一次性任务就是这样;它将在某个时间点运行,并且不会重复。这种方法要求您选择一个任务名称并指定一个触发时间。
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 scheduledTime DateTime 任务应运行的时间点。 是 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度一次性任务,如以下示例所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleOneTimeJobAsync ( CancellationToken cancellationToken )
{
var today = DateTime . UtcNow ;
var threeDaysFromNow = today . AddDays ( 3 );
await daprJobsClient . ScheduleOneTimeJobAsync ( "myJobName" , threeDaysFromNow , cancellationToken : cancellationToken );
}
}
基于间隔的任务 基于间隔的任务是一个在配置为固定时间量的循环中运行的任务,不像今天在 actor 构建块中工作的提醒 。这些任务也可以通过许多可选参数进行调度:
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 interval TimeSpan 任务应触发的间隔。 是 startingFrom DateTime 任务调度应开始的时间点。 否 repeats int 任务应触发的最大次数。 否 ttl 任务何时过期且不再触发。 否 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度基于间隔的任务,如以下示例所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleIntervalJobAsync ( CancellationToken cancellationToken )
{
var hourlyInterval = TimeSpan . FromHours ( 1 );
//每小时触发任务,但最多触发 5 次
await daprJobsClient . ScheduleIntervalJobAsync ( "myJobName" , hourlyInterval , repeats : 5 ), cancellationToken : cancellationToken ;
}
}
基于 Cron 的任务 基于 Cron 的任务是使用 Cron 表达式调度的。这提供了更多基于日历的控制,以便在任务触发时使用日历值在表达式中。与其他选项一样,这些任务也可以通过许多可选参数进行调度:
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 cronExpression string 指示任务应触发的 systemd 类似 Cron 表达式。 是 startingFrom DateTime 任务调度应开始的时间点。 否 repeats int 任务应触发的最大次数。 否 ttl 任务何时过期且不再触发。 否 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度基于 Cron 的任务,如下所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleCronJobAsync ( CancellationToken cancellationToken )
{
//在每个月的第五天的每隔一小时的顶部
const string cronSchedule = "0 */2 5 * *" ;
//直到下个月才开始
var now = DateTime . UtcNow ;
var oneMonthFromNow = now . AddMonths ( 1 );
var firstOfNextMonth = new DateTime ( oneMonthFromNow . Year , oneMonthFromNow . Month , 1 , 0 , 0 , 0 );
//每小时触发任务,但最多触发 5 次
await daprJobsClient . ScheduleCronJobAsync ( "myJobName" , cronSchedule , dueTime : firstOfNextMonth , cancellationToken : cancellationToken );
}
}
获取已调度任务的详细信息 如果您知道已调度任务的名称,您可以在不等待其触发的情况下检索其元数据。返回的 JobDetails
提供了一些有用的属性,用于从 Dapr 任务 API 消费信息:
如果 Schedule
属性包含 Cron 表达式,则 IsCronExpression
属性将为 true,并且表达式也将在 CronExpression
属性中可用。 如果 Schedule
属性包含持续时间值,则 IsIntervalExpression
属性将为 true,并且该值将转换为 TimeSpan
值,可从 Interval
属性访问。 这可以通过使用以下方法完成:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task < JobDetails > GetJobDetailsAsync ( string jobName , CancellationToken cancellationToken )
{
var jobDetails = await daprJobsClient . GetJobAsync ( jobName , canecllationToken );
return jobDetails ;
}
}
删除已调度的任务 要删除已调度的任务,您需要知道其名称。从那里开始,只需在 Dapr 任务客户端上调用 DeleteJobAsync
方法即可:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task DeleteJobAsync ( string jobName , CancellationToken cancellationToken )
{
await daprJobsClient . DeleteJobAsync ( jobName , cancellationToken );
}
}
1.5.2 - DaprJobsClient 使用指南 使用 DaprJobsClient 的基本技巧和建议
生命周期管理 DaprJobsClient
是专门用于与 Dapr Jobs API 交互的 Dapr 客户端。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,并实现了 IDisposable
接口以便快速清理资源。
为了获得最佳性能,建议创建一个长生命周期的 DaprJobsClient
实例,并在整个应用程序中共享使用。DaprJobsClient
实例是线程安全的,适合在多个线程中共享。
可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态方式注册,但也可以利用 IConfiguration
或其他注入服务中的值进行注册,这样就不需要在每个类中重新创建客户端。
避免为每个操作创建一个新的 DaprJobsClient
并在操作完成后销毁它。
通过 DaprJobsClientBuilder 配置 DaprJobsClient 可以通过 DaprJobsClientBuilder
类上的方法来配置 DaprJobsClient
,然后调用 .Build()
来创建客户端。每个 DaprJobsClient
的设置是独立的,调用 .Build()
后无法更改。
var daprJobsClient = new DaprJobsClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于验证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprJobsClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于验证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 gRPC 端点DAPR_API_TOKEN
:用于设置 API 令牌配置 gRPC 通道选项 Dapr 使用 CancellationToken
进行取消依赖于 gRPC 通道选项的配置。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置 。
var daprJobsClient = new DaprJobsClientBuilder ()
. UseGrpcChannelOptions ( new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
. Build ();
使用 DaprJobsClient
进行取消操作 DaprJobsClient
上的 API 执行异步操作并接受一个可选的 CancellationToken
参数。这遵循 .NET 的标准做法,用于可取消的操作。请注意,当取消发生时,不能保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,将抛出 OperationCancelledException
。
通过依赖注入配置 DaprJobsClient
使用内置的扩展方法在依赖注入容器中注册 DaprJobsClient
可以提供一次性注册长生命周期服务的好处,集中复杂配置并通过确保在可能的情况下重新利用类似的长生命周期资源(例如 HttpClient
实例)来提高性能。
有三种重载可用,以便开发人员在为其场景配置客户端时具有最大的灵活性。每个重载都会代表您注册 IHttpClientFactory
(如果尚未注册),并在创建 HttpClient
实例时配置 DaprJobsClientBuilder
以尽可能多地重用相同的实例,避免套接字耗尽和其他问题。
在第一种方法中,开发人员没有进行任何配置,DaprJobsClient
使用默认设置进行配置。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (); //根据需要注册 `DaprJobsClient` 以进行注入
var app = builder . Build ();
有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler
并公开用于配置必要选项的方法的重载来完成的。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (( _ , daprJobsClientBuilder ) => {
//设置 API 令牌
daprJobsClientBuilder . UseDaprApiToken ( "abc123" );
//指定非标准 HTTP 端点
daprJobsClientBuilder . UseHttpEndpoint ( "http://dapr.my-company.com" );
});
var app = builder . Build ();
最后,开发人员可能需要从其他服务中检索信息以填充这些配置值。该值可以从 DaprClient
实例、供应商特定的 SDK 或某些本地服务中提供,但只要它也在 DI 中注册,就可以通过最后一个重载将其注入到此配置操作中:
var builder = WebApplication . CreateBuilder ( args );
//注册一个虚构的服务,从某处检索 secret
builder . Services . AddSingleton < SecretService >();
builder . Services . AddDaprJobsClient (( serviceProvider , daprJobsClientBuilder ) => {
//从服务提供者中检索 `SecretService` 的实例
var secretService = serviceProvider . GetRequiredService < SecretService >();
var daprApiToken = secretService . GetSecret ( "DaprApiToken" ). Value ;
//配置 `DaprJobsClientBuilder`
daprJobsClientBuilder . UseDaprApiToken ( daprApiToken );
});
var app = builder . Build ();
理解 DaprJobsClient 上的负载序列化 虽然 DaprClient
上有许多方法可以使用 System.Text.Json
序列化器自动序列化和反序列化数据,但此 SDK 采用不同的理念。相反,相关方法接受一个可选的 ReadOnlyMemory<byte>
负载,这意味着序列化是留给开发人员的练习,通常不由 SDK 处理。
话虽如此,每种调度方法都有一些可用的辅助扩展方法。如果您知道要使用 JSON 可序列化的类型,可以使用每种调度类型的 Schedule*WithPayloadAsync
方法,该方法接受一个 object
作为负载和一个可选的 JsonSerializerOptions
用于序列化值。这将为您将值转换为 UTF-8 编码的字节,作为一种便利。以下是调度 Cron 表达式时可能的示例:
public sealed record Doodad ( string Name , int Value );
//...
var doodad = new Doodad ( "Thing" , 100 );
await daprJobsClient . ScheduleCronJobWithPayloadAsync ( "myJob" , "5 * * * *" , doodad );
同样,如果您有一个普通的字符串值,可以使用相同方法的重载来序列化字符串类型的负载,JSON 序列化步骤将被跳过,只会被编码为 UTF-8 编码字节的数组。以下是调度一次性 job 时可能的示例:
var now = DateTime . UtcNow ;
var oneWeekFromNow = now . AddDays ( 7 );
await daprJobsClient . ScheduleOneTimeJobWithPayloadAsync ( "myOtherJob" , oneWeekFromNow , "This is a test!" );
JobDetails
类型将数据返回为 ReadOnlyMemory<byte>?
,因此开发人员可以根据需要进行反序列化,但同样有两个包含的辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。这两种方法都假设开发人员对最初调度的 job 进行了编码(可能使用辅助序列化方法),因为这些方法不会强制字节表示它们不是的东西。
要将字节反序列化为字符串,可以使用以下辅助方法:
if ( jobDetails . Payload is not null )
{
string payloadAsString = jobDetails . Payload . DeserializeToString (); //如果成功,返回一个具有值的字符串
}
要将 JSON 编码的 UTF-8 字节反序列化为相应的类型,可以使用以下辅助方法。提供了一个重载参数,允许开发人员传入自己的 JsonSerializerOptions
以在反序列化期间应用。
public sealed record Doodad ( string Name , int Value );
//...
if ( jobDetails . Payload is not null )
{
var deserializedDoodad = jobDetails . Payload . DeserializeFromJsonBytes < Doodad >();
}
错误处理 DaprJobsClient
上的方法如果在 SDK 和运行在 Dapr sidecar 上的 Jobs API 服务之间遇到问题,将抛出 DaprJobsServiceException
。如果由于通过此 SDK 向 Jobs API 服务发出的请求格式不正确而遇到失败,将抛出 DaprMalformedJobException
。在非法参数值的情况下,将抛出适当的标准异常(例如 ArgumentOutOfRangeException
或 ArgumentNullException
),并附上有问题的参数名称。对于其他任何情况,将抛出 DaprException
。
最常见的失败情况将与以下内容相关:
在与 Jobs API 交互时参数格式不正确 瞬态故障,例如网络问题 无效数据,例如无法将值反序列化为其最初未从中序列化的类型 在任何这些情况下,您都可以通过 .InnerException
属性检查更多异常详细信息。
1.6 - Dapr Messaging .NET SDK 快速上手使用 Dapr Messaging .NET SDK
使用 Dapr Messaging 包,您可以在 .NET 应用程序中与 Dapr 消息 API 进行交互。在 v1.15 版本中,该包仅支持流式 pubsub 功能 。
未来的 Dapr .NET SDK 版本将会把现有的消息功能从 Dapr.Client 迁移到 Dapr.Messaging 包中。这一变更将在发布说明、文档和相关的技术说明中提前告知。
要开始使用,请查看 Dapr Messaging 指南,并参考最佳实践文档 以获取更多指导。
1.6.1 - 如何:在 .NET SDK 中编写和管理 Dapr 流式订阅 学习如何使用 .NET SDK 编写和管理 Dapr 流式订阅
我们来创建一个使用流式功能的发布/订阅主题或队列的订阅。我们将使用此处提供的简单示例 ,进行演示,并逐步讲解如何在运行时配置消息处理程序,而无需预先配置端点。在本指南中,您将会学习如何:
前提条件
注意 请注意,虽然 .NET 6 是 Dapr v1.15 中支持的最低版本,但只有 .NET 8 和 .NET 9 将在 v1.16 及更高版本中继续受到支持。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 流式发布/订阅示例。
cd examples/Client/PublishSubscribe
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 StreamingSubscriptionExample
目录。
cd StreamingSubscriptionExample
我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。
dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 监听 HTTP 请求在 http://localhost:3500
,而 gRPC 请求在 http://localhost:4001
。
使用依赖注入注册 Dapr PubSub 客户端 Dapr Messaging SDK 提供了一个扩展方法来简化 Dapr PubSub 客户端的注册。在 Program.cs
中完成依赖注入注册之前,添加以下行:
var builder = WebApplication . CreateBuilder ( args );
//可以在这两行之间的任何位置添加
builder . Services . AddDaprPubSubClient (); //就是这样
var app = builder . Build ();
您可能希望为 Dapr PubSub 客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprPublishSubscribeClientBuilder
实例的注册方法重载来实现:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprPubSubClient (( _ , daprPubSubClientBuilder ) => {
daprPubSubClientBuilder . UseDaprApiToken ( "abc123" );
daprPubSubClientBuilder . UseHttpEndpoint ( "http://localhost:8512" ); //非标准 sidecar HTTP 端点
});
var app = builder . Build ();
尽管如此,您可能希望注入的任何值需要从其他来源检索,该来源本身注册为依赖项。您可以使用另一个重载将 IServiceProvider
注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret 并将其传递到 AddDaprJobClient
的配置方法中,以便我们可以从其他地方检索我们的 Dapr API 令牌以在此处注册:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddSingleton < SecretRetriever >();
builder . Services . AddDaprPubSubClient (( serviceProvider , daprPubSubClientBuilder ) => {
var secretRetriever = serviceProvider . GetRequiredService < SecretRetriever >();
var daprApiToken = secretRetriever . GetSecret ( "DaprApiToken" ). Value ;
daprPubSubClientBuilder . UseDaprApiToken ( daprApiToken );
daprPubSubClientBuilder . UseHttpEndpoint ( "http://localhost:8512" );
});
var app = builder . Build ();
使用 IConfiguration 使用 Dapr PubSub 客户端 可以使用注册的 IConfiguration
中的值配置 Dapr PubSub 客户端,而无需显式指定每个值覆盖,如前一节中使用 DaprPublishSubscribeClientBuilder
所示。相反,通过填充通过依赖注入提供的 IConfiguration
,AddDaprPubSubClient()
注册将自动使用这些值覆盖其各自的默认值。
首先在您的配置中填充值。这可以通过多种不同的方式完成,如下所示。
通过 ConfigurationBuilder
配置 应用程序设置可以在不使用配置源的情况下配置,而是通过使用 ConfigurationBuilder
实例在内存中填充值:
var builder = WebApplication . CreateBuilder ();
//创建配置
var configuration = new ConfigurationBuilder ()
. AddInMemoryCollection ( new Dictionary < string , string > {
{ "DAPR_HTTP_ENDPOINT" , "http://localhost:54321" },
{ "DAPR_API_TOKEN" , "abc123" }
})
. Build ();
builder . Configuration . AddConfiguration ( configuration );
builder . Services . AddDaprPubSubClient (); //这将自动从 IConfiguration 填充 HTTP 端点和 API 令牌值
通过环境变量配置 应用程序设置可以从可用于您的应用程序的环境变量中访问。
以下环境变量将用于填充用于注册 Dapr PubSub 客户端的 HTTP 端点和 API 令牌。
键 值 DAPR_HTTP_ENDPOINT http://localhost:54321 DAPR_API_TOKEN abc123
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ();
builder . Services . AddDaprPubSubClient ();
Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
通过前缀环境变量配置 然而,在共享主机场景中,多个应用程序都在同一台机器上运行而不使用容器或在开发环境中,前缀环境变量并不罕见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:
键 值 myapp_DAPR_HTTP_ENDPOINT http://localhost:54321 myapp_DAPR_API_TOKEN abc123
这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ( prefix : "myapp_" );
builder . Services . AddDaprPubSubClient ();
Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
不依赖于依赖注入使用 Dapr PubSub 客户端 虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更容易,但您不需要以这种方式注册 DaprPublishSubscribeClient
。相反,您还可以选择从 DaprPublishSubscribeClientBuilder
实例创建它的实例,如下所示:
public class MySampleClass
{
public void DoSomething ()
{
var daprPubSubClientBuilder = new DaprPublishSubscribeClientBuilder ();
var daprPubSubClient = daprPubSubClientBuilder . Build ();
//使用 `daprPubSubClient` 做一些事情
}
}
设置消息处理程序 Dapr 中的流式订阅实现使您可以更好地控制事件的背压处理,通过在您的应用程序准备好接受它们之前将消息保留在 Dapr 运行时中。 .NET SDK 支持一个高性能队列,用于在处理挂起时在您的应用程序中维护这些消息的本地缓存。这些消息将保留在队列中,直到每个消息的处理超时或采取响应操作(通常在处理成功或失败后)。在 Dapr 运行时收到此响应操作之前,消息将由 Dapr 保留,并在服务故障时可用。
可用的各种响应操作如下:
响应操作 描述 重试 事件应在将来再次传递。 丢弃 事件应被删除(或转发到死信队列,如果已配置)并且不再尝试。 成功 事件应被删除,因为它已成功处理。
处理程序将一次只接收一条消息,如果为订阅提供了取消令牌,则将在处理程序调用期间提供此令牌。
处理程序必须配置为返回一个 Task<TopicResponseAction>
,指示这些操作之一,即使是从 try/catch 块中返回。如果您的处理程序未捕获异常,订阅将在订阅注册期间配置的选项中使用响应操作。
以下演示了示例中提供的示例消息处理程序:
Task < TopicResponseAction > HandleMessageAsync ( TopicMessage message , CancellationToken cancellationToken = default )
{
try
{
//对消息做一些事情
Console . WriteLine ( Encoding . UTF8 . GetString ( message . Data . Span ));
return Task . FromResult ( TopicResponseAction . Success );
}
catch
{
return Task . FromResult ( TopicResponseAction . Retry );
}
}
配置并订阅 PubSub 主题 流式订阅的配置需要在 Dapr 中注册的 PubSub 组件的名称、要订阅的主题或队列的名称、提供订阅配置的 DaprSubscriptionOptions
、消息处理程序和可选的取消令牌。 DaprSubscriptionOptions
的唯一必需参数是默认的 MessageHandlingPolicy
,它由每个事件的超时和超时时要采取的 TopicResponseAction
组成。
其他选项如下:
属性名称 描述 Metadata 额外的订阅元数据 DeadLetterTopic 发送丢弃消息的死信主题的可选名称。 MaximumQueuedMessages 默认情况下,内部队列没有强制的最大边界,但设置此属性将施加上限。 MaximumCleanupTimeout 当订阅被处理或令牌标记取消请求时,这指定了处理内部队列中剩余消息的最大时间。
然后按以下示例配置订阅:
var messagingClient = app . Services . GetRequiredService < DaprPublishSubscribeClient >();
var cancellationTokenSource = new CancellationTokenSource ( TimeSpan . FromSeconds ( 60 )); //覆盖默认的30秒
var options = new DaprSubscriptionOptions ( new MessageHandlingPolicy ( TimeSpan . FromSeconds ( 10 ), TopicResponseAction . Retry ));
var subscription = await messagingClient . SubscribeAsync ( "pubsub" , "mytopic" , options , HandleMessageAsync , cancellationTokenSource . Token );
终止并清理订阅 当您完成订阅并希望停止接收新事件时,只需等待对订阅实例的 DisposeAsync()
调用。这将导致客户端取消注册其他事件,并在处理所有仍在背压队列中的事件(如果有)后,处理任何内部资源。此清理将限于在注册订阅时提供的 DaprSubscriptionOptions
中的超时间隔,默认情况下设置为 30 秒。
1.6.2 - DaprPublishSubscribeClient 使用指南 使用 DaprPublishSubscribeClient 的基本提示和建议
生命周期管理 DaprPublishSubscribeClient
是 Dapr 客户端的一个版本,专门用于与 Dapr 消息 API 交互。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,维护对网络资源的访问,并实现了 IAsyncDisposable
接口以支持资源的快速清理。
为了获得最佳性能,建议创建一个长生命周期的 DaprPublishSubscribeClient
实例,并在整个应用程序中共享使用。DaprPublishSubscribeClient
实例是线程安全的,适合共享使用。
可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以通过 IConfiguration
或其他注入服务的值来注册,这在每个类中从头创建客户端时是不切实际的。
避免为每个操作创建一个 DaprPublishSubscribeClient
并在操作完成后销毁它。DaprPublishSubscribeClient
应仅在您不再希望接收订阅事件时才被销毁,因为销毁它将取消正在进行的新事件接收。
通过 DaprPublishSubscribeClientBuilder 配置 DaprPublishSubscribeClient 可以通过在 DaprPublishSubscribeClientBuilder
类上调用方法来配置 DaprPublishSubscribeClient
,然后调用 .Build()
来创建客户端本身。每个 DaprPublishSubscribeClient
的设置是独立的,并且在调用 .Build()
之后无法更改。
var daprPubsubClient = new DaprPublishSubscribeClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于认证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprPublishSubscribeClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于认证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 gRPC 端点DAPR_API_TOKEN
:用于设置 API 令牌配置 gRPC 通道选项 Dapr 使用 CancellationToken
进行取消依赖于 gRPC 通道选项的配置。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置 。
var daprPubsubClient = new DaprPublishSubscribeClientBuilder ()
. UseGrpcChannelOptions ( new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
. Build ();
使用 DaprPublishSubscribeClient
进行取消操作 DaprPublishSubscribeClient
上的 API 执行异步操作并接受一个可选的 CancellationToken
参数。这遵循 .NET 的标准做法,用于可取消的操作。请注意,当取消发生时,不能保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出一个 OperationCancelledException
。
通过依赖注入配置 DaprPublishSubscribeClient
使用内置的扩展方法在依赖注入容器中注册 DaprPublishSubscribeClient
可以提供注册长生命周期服务一次的好处,集中复杂的配置并通过确保在可能的情况下重新利用类似的长生命周期资源(例如 HttpClient
实例)来提高性能。
有三种重载可用,以便为开发人员提供最大的灵活性来配置客户端以适应他们的场景。每个重载都会代表您注册 IHttpClientFactory
(如果尚未注册),并配置 DaprPublishSubscribeClientBuilder
以在创建 HttpClient
实例时使用它,以便尽可能多地重用相同的实例并避免套接字耗尽和其他问题。
在第一种方法中,开发人员没有进行任何配置,DaprPublishSubscribeClient
使用默认设置进行配置。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . DaprPublishSubscribeClient (); //根据需要注册 `DaprPublishSubscribeClient` 以进行注入
var app = builder . Build ();
有时,开发人员需要使用上述各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler
的重载来完成的,并公开用于配置必要选项的方法。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (( _ , daprPubSubClientBuilder ) => {
//设置 API 令牌
daprPubSubClientBuilder . UseDaprApiToken ( "abc123" );
//指定非标准 HTTP 端点
daprPubSubClientBuilder . UseHttpEndpoint ( "http://dapr.my-company.com" );
});
var app = builder . Build ();
最后,开发人员可能需要从其他服务中检索信息以填充这些配置值。该值可以从 DaprClient
实例、供应商特定的 SDK 或某些本地服务中提供,但只要它也在 DI 中注册,就可以通过最后一个重载将其注入到此配置操作中:
var builder = WebApplication . CreateBuilder ( args );
//注册一个虚构的服务,从某处检索秘密
builder . Services . AddSingleton < SecretService >();
builder . Services . AddDaprPublishSubscribeClient (( serviceProvider , daprPubSubClientBuilder ) => {
//从服务提供者中检索 `SecretService` 的实例
var secretService = serviceProvider . GetRequiredService < SecretService >();
var daprApiToken = secretService . GetSecret ( "DaprApiToken" ). Value ;
//配置 `DaprPublishSubscribeClientBuilder`
daprPubSubClientBuilder . UseDaprApiToken ( daprApiToken );
});
var app = builder . Build ();
1.7 - Dapr .NET SDK 的错误处理 探索如何在 Dapr .NET SDK 中进行错误处理。
1.7.1 - Dapr .NET SDK 中更全面的错误模型 了解如何在 .NET SDK 中使用更全面的错误模型。
Dapr .NET SDK 支持由 Dapr 运行时实现的更全面的错误模型。这个模型为应用程序提供了一种丰富错误信息的方式,提供更多上下文信息,使应用程序的用户能够更好地理解问题并更快地解决。您可以在这里 阅读更多关于更全面错误模型的信息,并可以在这里 找到实现这些错误的 Dapr proto 文件。
Dapr .NET SDK 实现了 Dapr 运行时支持的所有细节,这些细节在 Dapr.Common.Exceptions
命名空间中实现,并可以通过 DaprException
的扩展方法 TryGetExtendedErrorInfo
进行访问。目前,此细节提取仅支持存在细节的 RpcException
。
// 扩展错误信息的示例用法
try
{
// 使用 Dapr 客户端执行某些操作,该操作抛出 DaprException。
}
catch ( DaprException daprEx )
{
if ( daprEx . TryGetExtendedErrorInfo ( out DaprExtendedErrorInfo errorInfo ))
{
Console . WriteLine ( errorInfo . Code );
Console . WriteLine ( errorInfo . Message );
foreach ( DaprExtendedErrorDetail detail in errorInfo . Details )
{
Console . WriteLine ( detail . ErrorType );
switch ( detail . ErrorType )
{
case ExtendedErrorType . ErrorInfo :
Console . WriteLine ( detail . Reason );
Console . WriteLine ( detail . Domain );
break ;
default :
Console . WriteLine ( detail . TypeUrl );
break ;
}
}
}
}
DaprExtendedErrorInfo 包含与错误相关的 Code
(状态码)和 Message
(错误信息),这些信息从内部的 RpcException
解析而来。还包含从异常细节中解析的 DaprExtendedErrorDetails
集合。
DaprExtendedErrorDetail 所有细节都实现了抽象的 DaprExtendedErrorDetail
,并具有相关的 DaprExtendedErrorType
。
RetryInfo
DebugInfo
QuotaFailure
PreconditionFailure
RequestInfo
LocalizedMessage
BadRequest
ErrorInfo
Help
ResourceInfo
Unknown
RetryInfo 告知客户端在重试之前应等待多长时间的信息。提供一个 DaprRetryDelay
,其属性包括 Second
(秒偏移)和 Nano
(纳秒偏移)。
DebugInfo 服务器提供的调试信息。包含 StackEntries
(包含堆栈跟踪的字符串集合)和 Detail
(进一步的调试信息)。
QuotaFailure 与可能已达到的某些配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations
,是 DaprQuotaFailureViolation
的集合,每个都包含 Subject
(请求的主题)和 Description
(有关失败的更多信息)。
PreconditionFailure 告知客户端某些必需的前置条件未满足的信息。具有一个属性 Violations
,是 DaprPreconditionFailureViolation
的集合,每个都有 Subject
(前置条件失败发生的主题,例如 “Azure”)、Type
(前置条件类型的表示,例如 “TermsOfService”)和 Description
(进一步描述,例如 “ToS 必须被接受。")。
RequestInfo 服务器返回的信息,可用于服务器识别客户端请求。包含 RequestId
和 ServingData
属性,RequestId
是服务器可以解释的某个字符串(例如 UID),ServingData
是构成请求一部分的任意数据。
LocalizedMessage 包含本地化消息及其语言环境。包含 Locale
(语言环境,例如 “en-US”)和 Message
(本地化消息)。
BadRequest 描述错误请求字段。包含 DaprBadRequestDetailFieldViolation
的集合,每个都有 Field
(请求中有问题的字段,例如 ‘first_name’)和 Description
(详细说明原因,例如 “first_name 不能包含特殊字符”)。
ErrorInfo 详细说明错误的原因。包含三个属性,Reason
(错误原因,应采用 UPPER_SNAKE_CASE 形式,例如 DAPR_INVALID_KEY)、Domain
(错误所属的域,例如 ‘dapr.io’)和 Metadata
,一个基于键值的进一步信息集合。
Help 为客户端提供资源以进行进一步研究。包含 DaprHelpDetailLink
的集合,提供 Url
(帮助或文档的 URL)和 Description
(链接提供的内容描述)。
ResourceInfo 提供与访问资源相关的信息。提供三个属性 ResourceType
(访问的资源类型,例如 “Azure service bus”)、ResourceName
(资源名称,例如 “my-configured-service-bus”)、Owner
(资源的所有者,例如 “subscriptionowner@dapr.io ”)和 Description
(与错误相关的资源的进一步信息,例如 “缺少使用此资源的权限”)。
Unknown 当详细类型 URL 无法映射到正确的 DaprExtendedErrorDetail
实现时返回。提供一个属性 TypeUrl
(无法解析的类型 URL,例如 “type.googleapis.com/Google.rpc.UnrecognizedType”)。
1.8 - 使用 Dapr .NET SDK 开发应用程序 了解 .NET Dapr 应用程序的本地开发集成选项
同时管理多个任务 通常情况下,使用您喜欢的 IDE 或编辑器启动应用程序时,您只需运行一个任务:您正在调试的应用程序。然而,开发微服务要求您在本地开发过程中同时管理多个任务 。一个微服务应用程序包含多个服务,您可能需要同时运行这些服务,并管理依赖项(如状态存储)。
将 Dapr 集成到您的开发过程中意味着您需要管理以下事项:
您想要运行的每个服务 每个服务的 Dapr sidecar Dapr 组件和配置清单 额外的依赖项,如状态存储 可选:用于 actor 的 Dapr placement 服务 本文档假设您正在构建一个生产应用程序,并希望创建一套可重复且稳健的开发实践。这里的指导是通用的,适用于任何使用 Dapr 的 .NET 服务器应用程序(包括 actor)。
组件管理 您有两种主要方法来存储 Dapr 本地开发的组件定义:
使用默认位置 (~/.dapr/components
) 使用您自定义的位置 在您的源代码库中创建一个文件夹来存储组件和配置,这样可以方便地对这些定义进行版本控制和共享。本文假设您在应用程序源代码旁边创建了一个文件夹来存储这些文件。
开发选项 选择以下链接之一以了解您可以在本地开发场景中使用的工具。这些文章按投入程度从低到高排序。您可能希望阅读所有文章以全面了解可用选项。
1.8.1 - 使用 Dapr CLI 进行 Dapr .NET SDK 开发 了解如何使用 Dapr CLI 进行本地开发
Dapr CLI 可以将其视为 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南 的补充 。
Dapr CLI 通过初始化本地的 Redis 容器、Zipkin 容器、placement 服务和 Redis 的组件清单,为您提供了一个良好的基础环境。这使您能够在全新安装且无需额外设置的情况下使用以下功能模块:
您可以使用 dapr run
命令来运行 .NET 服务,作为本地开发的一种策略。为每个服务运行此命令以启动您的应用程序。
优势: 由于这是 Dapr 默认安装的一部分,因此设置简单劣势: 这会在您的机器上运行长时间的 Docker 容器,可能不太理想劣势: 这种方法的可扩展性较差,因为需要为每个服务运行一个单独的命令使用 Dapr CLI 对于每个服务,您需要选择:
用于寻址的唯一应用 ID (app-id
) 用于 HTTP 的唯一监听端口 (port
) 您还应该决定存储组件的位置 (components-path
)。
可以从多个终端运行以下命令以启动每个服务,并替换相应的值。
dapr run --app-id <app-id> --app-port <port> --components-path <components-path> -- dotnet run -p <project> --urls http://localhost:<port>
解释: 此命令使用 dapr run
启动每个服务及其附属进程。命令的前半部分(在 --
之前)将所需的配置传递给 Dapr CLI。命令的后半部分(在 --
之后)将所需的配置传递给 dotnet run
命令。
💡 端口 由于您需要为每个服务配置一个唯一的端口,您可以使用此命令将该端口值传递给 Dapr 和服务 。--urls http://localhost:<port>
将配置 ASP.NET Core 以监听提供的端口上的流量。在命令行中配置比在代码中硬编码监听端口更灵活。如果您的任何服务不接受 HTTP 流量,请通过删除 --app-port
和 --urls
参数来修改上述命令。
下一步 如果您需要调试,请使用调试器的附加功能附加到其中一个正在运行的进程。
如果您想扩展这种方法,请考虑编写一个脚本来为您的整个应用程序自动化此过程。
1.8.2 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发 了解如何使用 .NET Aspire 进行本地开发
.NET Aspire .NET Aspire 是一款开发工具,旨在通过提供一个框架,简化外部软件与 .NET 应用程序的集成过程。该框架允许第三方服务轻松地与您的软件集成、监控和配置。
Aspire 通过与流行的 IDE(包括 Microsoft Visual Studio 、Visual Studio Code 、JetBrains Rider 等)深度集成,简化了本地开发。在启动调试器的同时,自动启动并配置对其他集成(包括 Dapr)的访问。
虽然 Aspire 也支持将应用程序部署到各种云平台(如 Microsoft Azure 和 Amazon AWS),但本指南不涉及部署相关内容。更多信息请参阅 Aspire 的文档 这里 。
先决条件 通过 CLI 使用 .NET Aspire 我们将从创建一个全新的 .NET 应用程序开始。打开您喜欢的 CLI 并导航到您希望创建新 .NET 解决方案的目录。首先使用以下命令安装一个模板,该模板将创建一个空的 Aspire 应用程序:
dotnet new install Aspire.ProjectTemplates
安装完成后,继续在当前目录中创建一个空的 .NET Aspire 应用程序。-n
参数允许您指定输出解决方案的名称。如果省略,.NET CLI 将使用输出目录的名称,例如 C:\source\aspiredemo
将导致解决方案被命名为 aspiredemo
。本教程的其余部分将假设解决方案名为 aspiredemo
。
dotnet new aspire -n aspiredemo
这将在您的目录中创建两个 Aspire 特定的目录和一个文件:
aspiredemo.AppHost/
包含用于配置应用程序中使用的每个集成的 Aspire 编排项目。aspiredemo.ServiceDefaults/
包含一组扩展,旨在跨您的解决方案共享,以帮助提高 Aspire 提供的弹性、服务发现和遥测能力(这些与 Dapr 本身提供的功能不同)。aspiredemo.sln
是维护当前解决方案布局的文件接下来,我们将创建一个项目,作为我们的 Dapr 应用程序。从同一目录中,使用以下命令创建一个名为 MyApp
的空 ASP.NET Core 项目。它将在 MyApp\MyApp.csproj
中相对于您的当前目录创建。
接下来,我们将配置 AppHost 项目以添加支持本地 Dapr 开发所需的包。使用以下命令导航到 AppHost 目录,并从 NuGet 安装 Aspire.Hosting.Dapr
包到项目中。我们还将添加对 MyApp
项目的引用,以便在注册过程中引用它。
cd aspiredemo.AppHost
dotnet add package Aspire.Hosting.Dapr
dotnet add reference ../MyApp/
接下来,我们需要将 Dapr 配置为与您的项目一起加载的资源。在您喜欢的 IDE 中打开该项目中的 Program.cs
文件。它应类似于以下内容:
var builder = DistributedApplication . CreateBuilder ( args );
builder . Build (). Run ();
如果您熟悉 ASP.NET Core 项目中使用的依赖注入方法或其他使用 Microsoft.Extensions.DependencyInjection
功能的项目,您会发现这将是一个熟悉的体验。
因为我们已经添加了对 MyApp
的项目引用,我们需要在此配置中添加一个引用。在 builder.Build().Run()
行之前添加以下内容:
var myApp = builder
. AddProject < Projects . MyApp >( "myapp" )
. WithDaprSidecar ();
因为项目引用已添加到此解决方案中,您的项目在此处显示为 Projects.
命名空间中的一个类型。您为项目分配的变量名称在本教程中并不重要,但如果您想在此项目和另一个项目之间创建引用以使用 Aspire 的服务发现功能,则会使用它。
添加 .WithDaprSidecar()
将 Dapr 配置为 .NET Aspire 资源,以便在项目运行时,sidecar 将与您的应用程序一起部署。这接受许多不同的选项,并可以选择性地配置,如以下示例所示:
DaprSidecarOptions sidecarOptions = new ()
{
AppId = "my-other-app" ,
AppPort = 8080 , //注意,如果您打算配置 pubsub、actor 或 workflow,从 Aspire v9.0 开始,此参数是必需的
DaprGrpcPort = 50001 ,
DaprHttpPort = 3500 ,
MetricsPort = 9090
};
builder
. AddProject < Projects . MyOtherApp >( "myotherapp" )
. WithReference ( myApp )
. WithDaprSidecar ( sidecarOptions );
如上例所示,从 .NET Aspire 9.0 开始,如果您打算使用 Dapr 需要调用到您的应用程序的任何功能,例如 pubsub、actor 或 workflow,您将需要指定您的 AppPort 作为配置选项,因为 Aspire 不会在运行时自动将其传递给 Dapr。预计这种行为将在未来的版本中更改,因为修复已合并并可以在
这里 跟踪。
当您在 IDE 中打开解决方案时,确保 aspiredemo.AppHost
被配置为您的启动项目,但当您在调试配置中启动它时,您会注意到您的集成控制台应反映您预期的 Dapr 日志,并且它将可用于您的应用程序。
1.8.3 - 使用 Project Tye 进行 Dapr .NET SDK 开发 了解如何使用 Project Tye 进行本地开发
Project Tye .NET Project Tye 是一个专为简化运行多个 .NET 服务而设计的微服务开发工具。Tye 允许您将多个 .NET 服务、进程和容器镜像的配置整合为一个可运行的应用程序。
对于 .NET Dapr 开发者来说,Tye 的优势在于:
Tye 可以自动化使用 dapr CLI Tye 遵循 .NET 的约定,对 .NET 服务几乎无需额外配置 Tye 能够管理容器中依赖项的生命周期 优缺点:
优点: Tye 可以自动化上述所有步骤。您无需再担心端口或应用程序 ID 等细节。优点: 由于 Tye 也可以管理容器,您可以将这些容器作为应用程序的一部分定义,并避免机器上长时间运行的容器。使用 Tye 按照 Tye 入门指南 安装 tye
CLI,并为您的应用程序创建 tye.yaml
文件。
接下来,按照 Tye Dapr 配方 中的步骤添加 Dapr。确保在 tye.yaml
中使用 components-path
指定组件文件夹的相对路径。
然后,添加任何额外的容器依赖项,并将组件定义添加到您之前创建的文件夹中。
您应该得到如下内容:
name : store-application
extensions :
# Dapr 的配置在这里。
- name : dapr
components-path : <components-path>
# 要运行的服务在这里。
services :
# 名称将用作应用程序 ID。对于 .NET 项目,Tye 只需要项目文件的路径。
- name : orders
project : orders/orders.csproj
- name : products
project : products/products.csproj
- name : store
project : store/store.csproj
# 您想要运行的容器需要一个镜像名称和一组要暴露的端口。
- name : redis
image : redis
bindings :
- port : 6973
将 tye.yaml
和应用程序代码一起提交到源代码管理中。
您现在可以使用 tye run
从一个终端启动整个应用程序。运行时,Tye 在 http://localhost:8000
提供一个仪表板以查看应用程序状态和日志。
下一步 Tye 会将您的服务作为标准 .NET 进程在本地运行。如果您需要调试,可以使用调试器附加到正在运行的进程之一。由于 Tye 了解 .NET,它可以在启动时暂停进程以便进行调试。
如果您希望在容器中进行本地测试,Tye 还提供了一个选项,可以在容器中运行您的服务。
1.8.4 - 使用 Docker-Compose 进行 Dapr .NET SDK 开发 了解如何使用 Docker-Compose 进行本地开发
Docker-Compose 这可以看作是 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南 的补充。
docker-compose
是 Docker Desktop 附带的一个命令行工具,您可以用它同时运行多个容器。它提供了一种自动化管理多个容器生命周期的方法,为面向 Kubernetes 的应用程序提供类似于生产环境的开发体验。
优势在于: docker-compose
帮助您管理容器,您可以将依赖项作为应用程序的一部分进行定义,并停止机器上长时间运行的容器。劣势在于: 需要较多的前期投入,服务需要先容器化。劣势在于: 如果您不熟悉 Docker,可能会遇到调试和故障排除的困难。使用 docker-compose 从 .NET 的角度来看,使用 Dapr 的 docker-compose
并不需要特别的指导。docker-compose
负责运行容器,一旦您的服务在容器中,配置它就和其他编程技术类似。
💡 应用端口 在容器中,ASP.NET Core 应用程序默认监听 80 端口。请记住这一点,以便在配置 --app-port
时使用。总结这种方法:
为每个服务创建一个 Dockerfile
创建一个 docker-compose.yaml
并将其提交到源代码库 要了解如何编写 docker-compose.yaml
,您可以从 Hello, docker-compose 示例 开始。
类似于使用 dapr run
本地运行,对于每个服务,您需要选择一个唯一的 app-id。选择容器名称作为 app-id 可以帮助您更容易记住。
compose 文件至少应包含以下内容:
容器之间通信所需的网络 每个服务的容器 一个 <service>-daprd
sidecar 容器,指定服务的端口和 app-id 在容器中运行的其他依赖项(例如 redis) 可选:Dapr placement 容器(用于 actor) 您还可以查看 eShopOnContainers 示例应用程序中的更大示例。
1.9 - Dapr .NET SDK 故障排除与调试 掌握使用 Dapr .NET SDK 进行故障排除与调试的实用方法和指南
1.9.1 - 使用 .NET SDK 进行 Pub/Sub 故障排查 使用 .NET SDK 进行 Pub/Sub 故障排查
Pub/Sub 故障排查 Pub/Sub 的常见问题是应用程序中的 Pub/Sub 端点未被调用。
这个问题可以分为几个层次,每个层次有不同的解决方案:
应用程序没有接收到来自 Dapr 的任何流量 应用程序没有向 Dapr 注册 Pub/Sub 端点 Pub/Sub 端点已在 Dapr 中注册,但请求没有到达预期的端点 步骤 1:提高日志级别 这一点很重要。后续步骤将依赖于您查看日志输出的能力。ASP.NET Core 默认日志设置几乎不记录任何内容,因此您需要更改它。
调整日志详细程度以包括 ASP.NET Core 的 Information
日志,如此处 所述。将 Microsoft
键设置为 Information
。
步骤 2:验证您可以接收到来自 Dapr 的流量 像往常一样启动应用程序(dapr run ...
)。确保在命令行中包含 --app-port
参数。Dapr 需要知道您的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序将在本地开发中监听 5000 端口的 HTTP。
等待 Dapr 启动完成
检查日志
您应该看到类似这样的日志条目:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.....
在初始化过程中,Dapr 会向您的应用程序发送一些请求以进行配置。如果找不到这些请求,则意味着出现了问题。请通过问题或 Discord 请求帮助(包括日志)。如果您看到对应用程序的请求,请继续执行下一步。
步骤 3:验证端点注册 像往常一样启动应用程序(dapr run ...
)。
使用命令行中的 curl
(或其他 HTTP 测试工具)访问 /dapr/subscribe
端点。
假设您的应用程序监听端口为 5000,这里是一个示例命令:
curl http://localhost:5000/dapr/subscribe -v
对于配置正确的应用程序,输出应如下所示:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /dapr/subscribe HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 15 Jan 2021 22:31:40 GMT
< Content-Type: application/json
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
[{"topic":"deposit","route":"deposit","pubsubName":"pubsub"},{"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}]* Closing connection 0
特别注意 HTTP 状态码和 JSON 输出。
200 状态码表示成功。
JSON 数据块是 /dapr/subscribe
的输出,由 Dapr 运行时处理。在这种情况下,它使用的是此仓库中的 ControllerSample
- 这是正确输出的示例。
[
{ "topic" : "deposit" , "route" : "deposit" , "pubsubName" : "pubsub" },
{ "topic" : "withdraw" , "route" : "withdraw" , "pubsubName" : "pubsub" }
]
通过此命令的输出,您可以诊断问题或继续下一步。
选项 0:响应为 200 并包含一些 Pub/Sub 条目 如果您在此测试的 JSON 输出中有条目,则问题出在其他地方,请继续执行下一步。
选项 1:响应不是 200,或不包含 JSON 如果响应不是 200 或不包含 JSON,则 MapSubscribeHandler()
端点未被访问。
确保在 Startup.cs
中有如下代码并重复测试。
app . UseRouting ();
app . UseCloudEvents ();
app . UseEndpoints ( endpoints =>
{
endpoints . MapSubscribeHandler (); // 这是 Dapr 订阅处理程序
endpoints . MapControllers ();
});
如果添加订阅处理程序没有解决问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
文件的内容。
选项 2:响应包含 JSON 但为空(如 []
) 如果 JSON 输出是一个空数组(如 []
),则订阅处理程序已注册,但没有注册主题端点。
如果您使用控制器进行 Pub/Sub,您应该有一个类似的方法:
[Topic("pubsub", "deposit")]
[HttpPost("deposit")]
public async Task < ActionResult > Deposit (...)
// 使用 Pub/Sub 路由
[Topic("pubsub", "transactions", "event.type == \"withdraw.v2\"", 1)]
[HttpPost("withdraw")]
public async Task < ActionResult > Withdraw (...)
在此示例中,Topic
和 HttpPost
属性是必需的,但其他细节可能不同。
如果您使用路由进行 Pub/Sub,您应该有一个类似的端点:
endpoints . MapPost ( "deposit" , ...). WithTopic ( "pubsub" , "deposit" );
在此示例中,调用 WithTopic(...)
是必需的,但其他细节可能不同。
在更正此代码并重新测试后,如果 JSON 输出仍然是空数组(如 []
),请在此仓库中打开一个问题,并包括 Startup.cs
和您的 Pub/Sub 端点的内容。
步骤 4:验证端点可达性 在此步骤中,我们将验证注册的 Pub/Sub 条目是否可达。上一步应该让您得到如下的 JSON 输出:
[
{
"pubsubName" : "pubsub" ,
"topic" : "deposit" ,
"route" : "deposit"
},
{
"pubsubName" : "pubsub" ,
"topic" : "deposit" ,
"routes" : {
"rules" : [
{
"match" : "event.type == \"withdraw.v2\"" ,
"path" : "withdraw"
}
]
}
}
]
保留此输出,因为我们将使用 route
信息来测试应用程序。
像往常一样启动应用程序(dapr run ...
)。
使用命令行中的 curl
(或其他 HTTP 测试工具)访问注册的 Pub/Sub 端点之一。
假设您的应用程序监听端口为 5000,并且您的一个 Pub/Sub 路由是 withdraw
,这里是一个示例命令:
curl http://localhost:5000/withdraw -H 'Content-Type: application/json' -d '{}' -v
以下是对示例运行上述命令的输出:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> POST /withdraw HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 2
>
* upload completely sent off: 2 out of 2 bytes
< HTTP/1.1 400 Bad Request
< Date: Fri, 15 Jan 2021 22:53:27 GMT
< Content-Type: application/problem+json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|5e9d7eee-4ea66b1e144ce9bb.","errors":{"Id":["The Id field is required."]}}* Closing connection 0
根据 HTTP 400 和 JSON 负载,此响应表明端点已被访问,但请求由于验证错误而被拒绝。
您还应该查看正在运行的应用程序的控制台输出。这是去掉 Dapr 日志头后的示例输出,以便更清晰。
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 POST http://localhost:5000/withdraw application/json 2
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
Route matched with {action = "Withdraw", controller = "Sample"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[ControllerSample.Account]] Withdraw(ControllerSample.Transaction, Dapr.Client.DaprClient) on controller ControllerSample.Controllers.SampleController (ControllerSample).
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ValidationProblemDetails'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
Executed action ControllerSample.Controllers.SampleController.Withdraw (ControllerSample) in 52.1211ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 157.056ms 400 application/problem+json; charset=utf-8
主要关注的日志条目是来自路由的:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
此条目显示:
路由已执行 路由选择了 ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)
端点 现在您有了排查此步骤问题所需的信息。
选项 0:路由选择了正确的端点 如果路由日志条目中的信息是正确的,则意味着在隔离情况下,您的应用程序行为正确。
示例:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
您可能想尝试使用 Dapr CLI 直接发送 Pub/Sub 消息并比较日志输出。
示例命令:
dapr publish --pubsub pubsub --topic withdraw --data '{}'
如果在这样做之后您仍然不理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。
选项 1:路由未执行 如果您在日志中没有看到 Microsoft.AspNetCore.Routing.EndpointMiddleware
的条目,则意味着请求被其他东西处理了。通常情况下,问题是一个行为不当的中间件。请求的其他日志可能会给您一个线索。
如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。
选项 2:路由选择了错误的端点 如果您在日志中看到 Microsoft.AspNetCore.Routing.EndpointMiddleware
的条目,但它包含错误的端点,则意味着您有一个路由冲突。被选择的端点将出现在日志中,这应该能给您一个关于冲突原因的想法。
如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。
2 - Dapr Go SDK 用于开发 Dapr 应用的 Go SDK 包
这是一个用于在 Go 中构建 Dapr 应用的客户端库。该客户端支持所有公共 Dapr API,专注于提供符合 Go 语言习惯的开发体验,提高开发者的工作效率。
客户端 使用 Go 客户端 SDK 来调用公共 Dapr API。
[**了解更多关于 Go 客户端 SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-client/)
服务 使用 Dapr 服务(回调)SDK 创建可被 Dapr 调用的服务。
[**了解更多关于 Go 服务(回调)SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-service/)
2.1 - Dapr 服务(回调)SDK for Go 入门指南 快速掌握如何使用 Dapr 服务(回调)SDK for Go
除了 Dapr API 客户端,Dapr Go SDK 还提供了用于启动 Dapr 回调服务的服务模块。您可以选择通过 gRPC 或 HTTP 来开发这些服务:
2.1.1 - 使用 Dapr HTTP 服务 SDK for Go 入门 如何使用 Dapr HTTP 服务 SDK for Go 快速上手
前置条件 首先导入 Dapr Go 的 service/http 包:
daprd "github.com/dapr/go-sdk/service/http"
创建和启动服务 要创建一个 HTTP Dapr 服务,首先需要在特定地址上创建一个 Dapr 回调实例:
s := daprd . NewService ( ":8080" )
或者结合现有的 http.ServeMux 使用特定地址创建服务,以便与现有服务器集成:
mux := http . NewServeMux ()
mux . HandleFunc ( "/" , myOtherHandler )
s := daprd . NewServiceWithMux ( ":8080" , mux )
创建服务实例后,你可以添加任意数量的事件、绑定和服务调用处理程序。定义好这些逻辑后,就可以启动服务:
if err := s . Start (); err != nil && err != http . ErrServerClosed {
log . Fatalf ( "error: %v" , err )
}
事件处理 要处理来自特定主题的事件,你需要在启动服务之前添加至少一个主题事件处理程序:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/events" ,
}
err := s . AddTopicEventHandler ( sub , eventHandler )
if err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func eventHandler ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// 处理事件
return true , nil
}
你可以选择使用路由规则 根据 CloudEvent 的内容将消息路由到不同的处理程序。
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/important" ,
Match : `event.type == "important"` ,
Priority : 1 ,
}
err := s . AddTopicEventHandler ( sub , importantHandler )
if err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
你还可以创建一个自定义类型来实现 TopicEventSubscriber
接口以处理事件:
type EventHandler struct {
// 事件处理程序所需的任何数据或引用。
}
func ( h * EventHandler ) Handle ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// 处理事件
return true , nil
}
然后可以使用 AddTopicEventSubscriber
方法添加 EventHandler
:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
eventHandler := & EventHandler {
// 初始化字段
}
if err := s . AddTopicEventSubscriber ( sub , eventHandler ); err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
服务调用处理程序 要处理服务调用,你需要在启动服务之前添加至少一个服务调用处理程序:
if err := s . AddServiceInvocationHandler ( "/echo" , echoHandler ); err != nil {
log . Fatalf ( "error adding invocation handler: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func echoHandler ( ctx context . Context , in * common . InvocationEvent ) ( out * common . Content , err error ) {
log . Printf ( "echo - ContentType:%s, Verb:%s, QueryString:%s, %+v" , in . ContentType , in . Verb , in . QueryString , string ( in . Data ))
// 处理调用
out = & common . Content {
Data : in . Data ,
ContentType : in . ContentType ,
DataTypeURL : in . DataTypeURL ,
}
return
}
绑定调用处理程序 if err := s . AddBindingInvocationHandler ( "/run" , runHandler ); err != nil {
log . Fatalf ( "error adding binding handler: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func runHandler ( ctx context . Context , in * common . BindingEvent ) ( out [] byte , err error ) {
log . Printf ( "binding - Data:%v, Meta:%v" , in . Data , in . Metadata )
// 处理调用
return nil , nil
}
相关链接 2.1.2 - 使用 Dapr 服务(回调)SDK for Go 入门 如何使用 Dapr 服务(回调)SDK for Go 快速上手
Dapr gRPC 服务 SDK for Go 前置条件 首先,导入 Dapr Go 服务/gRPC 包:
daprd "github.com/dapr/go-sdk/service/grpc"
创建和启动服务 要创建一个 gRPC Dapr 服务,首先需要在特定地址上创建一个 Dapr 回调实例:
s , err := daprd . NewService ( ":50001" )
if err != nil {
log . Fatalf ( "无法启动服务器: %v" , err )
}
或者,使用地址和现有的 net.Listener,以便与现有的服务器监听器结合:
list , err := net . Listen ( "tcp" , "localhost:0" )
if err != nil {
log . Fatalf ( "gRPC 监听器创建失败: %s" , err )
}
s := daprd . NewServiceWithListener ( list )
创建服务实例后,你可以为该服务添加任意数量的事件、绑定和服务调用处理程序,如下所示。定义好逻辑后,你就可以启动服务:
if err := s . Start (); err != nil {
log . Fatalf ( "服务器错误: %v" , err )
}
事件处理 要处理来自特定主题的事件,你需要在启动服务之前添加至少一个主题事件处理程序:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
if err := s . AddTopicEventHandler ( sub , eventHandler ); err != nil {
log . Fatalf ( "添加主题订阅时出错: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func eventHandler ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// 在这里处理事件
return true , nil
}
你可以选择使用路由规则 根据 CloudEvent 的内容将消息路由到不同的处理程序。
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/important" ,
Match : `event.type == "important"` ,
Priority : 1 ,
}
err := s . AddTopicEventHandler ( sub , importantHandler )
if err != nil {
log . Fatalf ( "添加主题订阅时出错: %v" , err )
}
你还可以创建一个自定义类型来实现 TopicEventSubscriber
接口,以处理你的事件:
type EventHandler struct {
// 你的事件处理程序需要的任何数据或引用。
}
func ( h * EventHandler ) Handle ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// 在这里处理事件
return true , nil
}
然后可以使用 AddTopicEventSubscriber
方法添加 EventHandler
:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
eventHandler := & EventHandler {
// 初始化任何字段
}
if err := s . AddTopicEventSubscriber ( sub , eventHandler ); err != nil {
log . Fatalf ( "添加主题订阅时出错: %v" , err )
}
服务调用处理程序 要处理服务调用,你需要在启动服务之前添加至少一个服务调用处理程序:
if err := s . AddServiceInvocationHandler ( "echo" , echoHandler ); err != nil {
log . Fatalf ( "添加调用处理程序时出错: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func echoHandler ( ctx context . Context , in * common . InvocationEvent ) ( out * common . Content , err error ) {
log . Printf ( "回声 - ContentType:%s, Verb:%s, QueryString:%s, %+v" , in . ContentType , in . Verb , in . QueryString , string ( in . Data ))
// 在这里处理调用
out = & common . Content {
Data : in . Data ,
ContentType : in . ContentType ,
DataTypeURL : in . DataTypeURL ,
}
return
}
绑定调用处理程序 要处理绑定调用,你需要在启动服务之前添加至少一个绑定调用处理程序:
if err := s . AddBindingInvocationHandler ( "run" , runHandler ); err != nil {
log . Fatalf ( "添加绑定处理程序时出错: %v" , err )
}
处理程序方法可以是任何符合预期签名的方法:
func runHandler ( ctx context . Context , in * common . BindingEvent ) ( out [] byte , err error ) {
log . Printf ( "绑定 - Data:%v, Meta:%v" , in . Data , in . Metadata )
// 在这里处理调用
return nil , nil
}
相关链接 2.2 - 使用 Dapr 客户端 Go SDK 入门 如何使用 Dapr Go SDK 快速上手
Dapr 客户端包使您能够从 Go 应用程序与其他 Dapr 应用程序进行交互。
前提条件 在开始之前,您需要确保以下条件已满足:
导入客户端包 import "github.com/dapr/go-sdk/client"
错误处理 Dapr 的错误处理基于 gRPC 的丰富错误模型 。以下代码示例展示了如何解析和处理错误详情:
if err != nil {
st := status . Convert ( err )
fmt . Printf ( "Code: %s\n" , st . Code (). String ())
fmt . Printf ( "Message: %s\n" , st . Message ())
for _ , detail := range st . Details () {
switch t := detail .( type ) {
case * errdetails . ErrorInfo :
// 处理 ErrorInfo 详情
fmt . Printf ( "ErrorInfo:\n- Domain: %s\n- Reason: %s\n- Metadata: %v\n" , t . GetDomain (), t . GetReason (), t . GetMetadata ())
case * errdetails . BadRequest :
// 处理 BadRequest 详情
fmt . Println ( "BadRequest:" )
for _ , violation := range t . GetFieldViolations () {
fmt . Printf ( "- Key: %s\n" , violation . GetField ())
fmt . Printf ( "- The %q field was wrong: %s\n" , violation . GetField (), violation . GetDescription ())
}
case * errdetails . ResourceInfo :
// 处理 ResourceInfo 详情
fmt . Printf ( "ResourceInfo:\n- Resource type: %s\n- Resource name: %s\n- Owner: %s\n- Description: %s\n" ,
t . GetResourceType (), t . GetResourceName (), t . GetOwner (), t . GetDescription ())
case * errdetails . Help :
// 处理 Help 详情
fmt . Println ( "HelpInfo:" )
for _ , link := range t . GetLinks () {
fmt . Printf ( "- Url: %s\n" , link . Url )
fmt . Printf ( "- Description: %s\n" , link . Description )
}
default :
// 添加其他类型详情的处理
fmt . Printf ( "Unhandled error detail type: %v\n" , t )
}
}
}
构建块 Go SDK 允许您与所有 Dapr 构建块 进行交互。
服务调用 要调用运行在 Dapr sidecar 中的另一个服务上的特定方法,Dapr 客户端 Go SDK 提供了两种选项:
调用不带数据的服务:
resp , err := client . InvokeMethod ( ctx , "app-id" , "method-name" , "post" )
调用带数据的服务:
content := & dapr . DataContent {
ContentType : "application/json" ,
Data : [] byte ( `{ "id": "a123", "value": "demo", "valid": true }` ),
}
resp , err = client . InvokeMethodWithContent ( ctx , "app-id" , "method-name" , "post" , content )
有关服务调用的完整指南,请访问 如何调用服务 。
状态管理 对于简单的用例,Dapr 客户端提供了易于使用的 Save
、Get
、Delete
方法:
ctx := context . Background ()
data := [] byte ( "hello" )
store := "my-store" // 在组件 YAML 中定义
// 使用键 key1 保存状态,默认选项:强一致性,最后写入
if err := client . SaveState ( ctx , store , "key1" , data , nil ); err != nil {
panic ( err )
}
// 获取键 key1 的状态
item , err := client . GetState ( ctx , store , "key1" , nil )
if err != nil {
panic ( err )
}
fmt . Printf ( "data [key:%s etag:%s]: %s" , item . Key , item . Etag , string ( item . Value ))
// 删除键 key1 的状态
if err := client . DeleteState ( ctx , store , "key1" , nil ); err != nil {
panic ( err )
}
为了更细粒度的控制,Dapr Go 客户端公开了 SetStateItem
类型,可以用于更好地控制状态操作,并允许一次保存多个项目:
item1 := & dapr . SetStateItem {
Key : "key1" ,
Etag : & ETag {
Value : "1" ,
},
Metadata : map [ string ] string {
"created-on" : time . Now (). UTC (). String (),
},
Value : [] byte ( "hello" ),
Options : & dapr . StateOptions {
Concurrency : dapr . StateConcurrencyLastWrite ,
Consistency : dapr . StateConsistencyStrong ,
},
}
item2 := & dapr . SetStateItem {
Key : "key2" ,
Metadata : map [ string ] string {
"created-on" : time . Now (). UTC (). String (),
},
Value : [] byte ( "hello again" ),
}
item3 := & dapr . SetStateItem {
Key : "key3" ,
Etag : & dapr . ETag {
Value : "1" ,
},
Value : [] byte ( "hello again" ),
}
if err := client . SaveBulkState ( ctx , store , item1 , item2 , item3 ); err != nil {
panic ( err )
}
同样,GetBulkState
方法提供了一种在单个操作中检索多个状态项的方法:
keys := [] string { "key1" , "key2" , "key3" }
items , err := client . GetBulkState ( ctx , store , keys , nil , 100 )
以及 ExecuteStateTransaction
方法,用于以事务方式执行多个插入或删除操作。
ops := make ([] * dapr . StateOperation , 0 )
op1 := & dapr . StateOperation {
Type : dapr . StateOperationTypeUpsert ,
Item : & dapr . SetStateItem {
Key : "key1" ,
Value : [] byte ( data ),
},
}
op2 := & dapr . StateOperation {
Type : dapr . StateOperationTypeDelete ,
Item : & dapr . SetStateItem {
Key : "key2" ,
},
}
ops = append ( ops , op1 , op2 )
meta := map [ string ] string {}
err := testClient . ExecuteStateTransaction ( ctx , store , meta , ops )
使用 QueryState
检索、过滤和排序存储在状态存储中的键/值数据。
// 定义查询字符串
query := `{
"filter": {
"EQ": { "value.Id": "1" }
},
"sort": [
{
"key": "value.Balance",
"order": "DESC"
}
]
}`
// 使用客户端查询状态
queryResponse , err := c . QueryState ( ctx , "querystore" , query )
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Got %d\n" , len ( queryResponse ))
for _ , account := range queryResponse {
var data Account
err := account . Unmarshal ( & data )
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Account: %s has %f\n" , data . ID , data . Balance )
}
注意: 查询状态 API 目前处于 alpha 阶段
有关状态管理的完整指南,请访问 如何保存和获取状态 。
发布消息 要将数据发布到主题上,Dapr Go 客户端提供了一个简单的方法:
data := [] byte ( `{ "id": "a123", "value": "abcdefg", "valid": true }` )
if err := client . PublishEvent ( ctx , "component-name" , "topic-name" , data ); err != nil {
panic ( err )
}
要一次发布多个消息,可以使用 PublishEvents
方法:
events := [] string { "event1" , "event2" , "event3" }
res := client . PublishEvents ( ctx , "component-name" , "topic-name" , events )
if res . Error != nil {
panic ( res . Error )
}
有关发布/订阅的完整指南,请访问 如何发布和订阅 。
工作流 您可以使用 Go SDK 创建 工作流 。例如,从一个简单的工作流活动开始:
func TestActivity ( ctx workflow . ActivityContext ) ( any , error ) {
var input int
if err := ctx . GetInput ( & input ); err != nil {
return "" , err
}
// 在这里做一些事情
return "result" , nil
}
编写一个简单的工作流函数:
func TestWorkflow ( ctx * workflow . WorkflowContext ) ( any , error ) {
var input int
if err := ctx . GetInput ( & input ); err != nil {
return nil , err
}
var output string
if err := ctx . CallActivity ( TestActivity , workflow . ActivityInput ( input )). Await ( & output ); err != nil {
return nil , err
}
if err := ctx . WaitForExternalEvent ( "testEvent" , time . Second * 60 ). Await ( & output ); err != nil {
return nil , err
}
if err := ctx . CreateTimer ( time . Second ). Await ( nil ); err != nil {
return nil , nil
}
return output , nil
}
然后编写将使用您创建的工作流的应用程序。有关完整的演练,请参阅 如何编写工作流指南 。
尝试 Go SDK 工作流示例 。
输出绑定 Dapr Go 客户端 SDK 提供了两种方法来调用 Dapr 定义的绑定上的操作。Dapr 支持输入、输出和双向绑定。
对于简单的输出绑定:
in := & dapr . InvokeBindingRequest { Name : "binding-name" , Operation : "operation-name" }
err = client . InvokeOutputBinding ( ctx , in )
调用带内容和元数据的方法:
in := & dapr . InvokeBindingRequest {
Name : "binding-name" ,
Operation : "operation-name" ,
Data : [] byte ( "hello" ),
Metadata : map [ string ] string { "k1" : "v1" , "k2" : "v2" },
}
out , err := client . InvokeBinding ( ctx , in )
有关输出绑定的完整指南,请访问 如何使用绑定 。
Actor 使用 Dapr Go 客户端 SDK 编写 actor。
// MyActor 表示一个示例 actor 类型。
type MyActor struct {
actors . Actor
}
// MyActorMethod 是可以在 MyActor 上调用的方法。
func ( a * MyActor ) MyActorMethod ( ctx context . Context , req * actors . Message ) ( string , error ) {
log . Printf ( "Received message: %s" , req . Data )
return "Hello from MyActor!" , nil
}
func main () {
// 创建一个 Dapr 客户端
daprClient , err := client . NewClient ()
if err != nil {
log . Fatal ( "Error creating Dapr client: " , err )
}
// 向 Dapr 注册 actor 类型
actors . RegisterActor ( & MyActor {})
// 创建一个 actor 客户端
actorClient := actors . NewClient ( daprClient )
// 创建一个 actor ID
actorID := actors . NewActorID ( "myactor" )
// 获取或创建 actor
err = actorClient . SaveActorState ( context . Background (), "myactorstore" , actorID , map [ string ] interface {}{ "data" : "initial state" })
if err != nil {
log . Fatal ( "Error saving actor state: " , err )
}
// 调用 actor 上的方法
resp , err := actorClient . InvokeActorMethod ( context . Background (), "myactorstore" , actorID , "MyActorMethod" , & actors . Message { Data : [] byte ( "Hello from client!" )})
if err != nil {
log . Fatal ( "Error invoking actor method: " , err )
}
log . Printf ( "Response from actor: %s" , resp . Data )
// 在终止前等待几秒钟
time . Sleep ( 5 * time . Second )
// 删除 actor
err = actorClient . DeleteActor ( context . Background (), "myactorstore" , actorID )
if err != nil {
log . Fatal ( "Error deleting actor: " , err )
}
// 关闭 Dapr 客户端
daprClient . Close ()
}
有关 actor 的完整指南,请访问 actor 构建块文档 。
Secret 管理 Dapr 客户端还提供对运行时 secret 的访问,这些 secret 可以由任意数量的 secret 存储(例如 Kubernetes Secrets、HashiCorp Vault 或 Azure KeyVault)支持:
opt := map [ string ] string {
"version" : "2" ,
}
secret , err := client . GetSecret ( ctx , "store-name" , "secret-name" , opt )
认证 默认情况下,Dapr 依赖于网络边界来限制对其 API 的访问。然而,如果目标 Dapr API 配置了基于令牌的认证,用户可以通过两种方式配置 Go Dapr 客户端以使用该令牌:
环境变量
如果定义了 DAPR_API_TOKEN 环境变量,Dapr 将自动使用它来增强其 Dapr API 调用以确保认证。
显式方法
此外,用户还可以在任何 Dapr 客户端实例上显式设置 API 令牌。这种方法在用户代码需要为不同的 Dapr API 端点创建多个客户端时非常有用。
func main () {
client , err := dapr . NewClient ()
if err != nil {
panic ( err )
}
defer client . Close ()
client . WithAuthToken ( "your-Dapr-API-token-here" )
}
有关 secret 的完整指南,请访问 如何检索 secret 。
分布式锁 Dapr 客户端提供了使用锁对资源的互斥访问。通过锁,您可以:
提供对数据库行、表或整个数据库的访问 以顺序方式锁定从队列中读取消息 package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main () {
client , err := dapr . NewClient ()
if err != nil {
panic ( err )
}
defer client . Close ()
resp , err := client . TryLockAlpha1 ( ctx , "lockstore" , & dapr . LockRequest {
LockOwner : "random_id_abc123" ,
ResourceID : "my_file_name" ,
ExpiryInSeconds : 60 ,
})
fmt . Println ( resp . Success )
}
有关分布式锁的完整指南,请访问 如何使用锁 。
配置 使用 Dapr 客户端 Go SDK,您可以消费作为只读键/值对返回的配置项,并订阅配置项的更改。
配置获取 items , err := client . GetConfigurationItem ( ctx , "example-config" , "mykey" )
if err != nil {
panic ( err )
}
fmt . Printf ( "get config = %s\n" , ( * items ). Value )
配置订阅 go func () {
if err := client . SubscribeConfigurationItems ( ctx , "example-config" , [] string { "mySubscribeKey1" , "mySubscribeKey2" , "mySubscribeKey3" }, func ( id string , items map [ string ] * dapr . ConfigurationItem ) {
for k , v := range items {
fmt . Printf ( "get updated config key = %s, value = %s \n" , k , v . Value )
}
subscribeID = id
}); err != nil {
panic ( err )
}
}()
有关配置的完整指南,请访问 如何从存储管理配置 。
加密 使用 Dapr 客户端 Go SDK,您可以使用高级 Encrypt
和 Decrypt
加密 API 在处理数据流时加密和解密文件。
加密:
// 使用 Dapr 加密数据
out , err := client . Encrypt ( context . Background (), rf , dapr . EncryptOptions {
// 这是 3 个必需的参数
ComponentName : "mycryptocomponent" ,
KeyName : "mykey" ,
Algorithm : "RSA" ,
})
if err != nil {
panic ( err )
}
解密:
// 使用 Dapr 解密数据
out , err := client . Decrypt ( context . Background (), rf , dapr . EncryptOptions {
// 唯一必需的选项是组件名称
ComponentName : "mycryptocomponent" ,
})
有关加密的完整指南,请访问 如何使用加密 API 。
相关链接 Go SDK 示例
3 - Dapr Java SDK Java SDK包,用于开发Dapr应用
Dapr 提供多种包以帮助开发 Java 应用程序。通过这些包,您可以使用 Dapr 创建 Java 客户端、服务器和虚拟 actor。
前提条件 已安装 Dapr CLI 已初始化 Dapr 环境 JDK 11 或更高版本 - 发布的 jar 与 Java 8 兼容: 安装以下 Java 构建工具之一: 导入 Dapr Java SDK 接下来,导入 Java SDK 包以开始使用。选择您喜欢的构建工具以了解如何导入。
对于 Maven 项目,将以下内容添加到您的 pom.xml
文件中:
<project>
...
<dependencies>
...
<!-- Dapr 的核心 SDK,包含所有功能,actor 除外。 -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk</artifactId>
<version> 1.13.1</version>
</dependency>
<!-- Dapr 的 actor SDK(可选)。 -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk-actors</artifactId>
<version> 1.13.1</version>
</dependency>
<!-- Dapr 与 SpringBoot 的 SDK 集成(可选)。 -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk-springboot</artifactId>
<version> 1.13.1</version>
</dependency>
...
</dependencies>
...
</project>
对于 Gradle 项目,将以下内容添加到您的 build.gradle
文件中:
dependencies {
...
// Dapr 的核心 SDK,包含所有功能,actor 除外。
compile ( ' io . dapr : dapr - sdk : 1 . 13 . 1 ' )
// Dapr 的 actor SDK(可选)。
compile ( ' io . dapr : dapr - sdk - actors : 1 . 13 . 1 ' )
// Dapr 与 SpringBoot 的 SDK 集成(可选)。
compile ( ' io . dapr : dapr - sdk - springboot : 1 . 13 . 1 ' )
}
如果您也在使用 Spring Boot,可能会遇到一个常见问题,即 Dapr SDK 使用的 OkHttp
版本与 Spring Boot Bill of Materials 中指定的版本冲突。
您可以通过在项目中指定与 Dapr SDK 使用的版本兼容的 OkHttp
版本来解决此问题:
<dependency>
<groupId> com.squareup.okhttp3</groupId>
<artifactId> okhttp</artifactId>
<version> 1.13.1</version>
</dependency>
试用 测试 Dapr Java SDK。通过 Java 快速入门和教程来查看 Dapr 的实际应用:
SDK 示例 描述 快速入门 使用 Java SDK 在几分钟内体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始使用。
import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 发送带有消息的类;BINDING_OPERATION="create"
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , myClass ). block ();
// 发送纯字符串
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , message ). block ();
}
可用包 客户端 创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 Java 客户端。
工作流 创建和管理与其他 Dapr API 一起使用的工作流。
3.1 - 工作流 如何使用 Dapr 工作流扩展快速启动和运行
在这篇文档中,我们将介绍如何使用 Dapr 工作流扩展来快速启动和运行工作流。Dapr 工作流扩展为定义和管理工作流提供了一种简便的方法,使开发者能够轻松地在分布式系统中编排复杂的业务流程。
Dapr 工作流扩展的主要功能包括:
工作流定义 :可以使用 YAML 或 JSON 格式来定义工作流。状态管理 :通过 Dapr 的状态管理功能,确保工作流在执行过程中状态的正确维护。服务调用 :利用 Dapr 的服务调用功能,在工作流中与其他服务进行交互。事件驱动 :通过发布/订阅机制,工作流可以响应事件并触发相应的操作。定时器和提醒 :使用定时器和提醒功能,在工作流中设置定时任务和提醒。通过这些功能,Dapr 工作流扩展能够帮助开发者更高效地构建和管理分布式应用程序中的工作流。
3.1.1 - 如何:在 Java SDK 中编写和管理 Dapr 工作流 如何使用 Dapr Java SDK 快速启动和运行工作流
我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的工作流示例 ,您将:
在自托管模式 下,此示例使用 dapr init
的默认配置运行。
准备工作 设置环境 克隆 Java SDK 仓库并进入其中。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装运行此工作流示例所需的 Dapr Java SDK 依赖项。
从 Java SDK 根目录,导航到 Dapr 工作流示例。
运行 DemoWorkflowWorker
DemoWorkflowWorker
类在 Dapr 的工作流运行时引擎中注册了 DemoWorkflow
的实现。在 DemoWorkflowWorker.java
文件中,您可以找到 DemoWorkflowWorker
类和 main
方法:
public class DemoWorkflowWorker {
public static void main ( String [] args ) throws Exception {
// Register the Workflow with the runtime.
WorkflowRuntime . getInstance (). registerWorkflow ( DemoWorkflow . class );
System . out . println ( "Start workflow runtime" );
WorkflowRuntime . getInstance (). startAndBlock ();
System . exit ( 0 );
}
}
在上面的代码中:
WorkflowRuntime.getInstance().registerWorkflow()
将 DemoWorkflow
注册为 Dapr 工作流运行时中的一个工作流。WorkflowRuntime.getInstance().start()
在 Dapr 工作流运行时中构建并启动引擎。在终端中,执行以下命令以启动 DemoWorkflowWorker
:
dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowWorker
预期输出
You're up and running! Both Dapr and your app logs will appear here.
...
== APP == Start workflow runtime
== APP == Sep 13, 2023 9:02:03 AM com.microsoft.durabletask.DurableTaskGrpcWorker startAndBlock
== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001.
运行 DemoWorkflowClient
DemoWorkflowClient
启动已在 Dapr 中注册的工作流实例。
public class DemoWorkflowClient {
// ...
public static void main ( String [] args ) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient ();
try ( client ) {
String separatorStr = "*******" ;
System . out . println ( separatorStr );
String instanceId = client . scheduleNewWorkflow ( DemoWorkflow . class , "input data" );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**GetInstanceMetadata:Running Workflow**" );
WorkflowInstanceStatus workflowMetadata = client . getInstanceState ( instanceId , true );
System . out . printf ( "Result: %s%n" , workflowMetadata );
System . out . println ( separatorStr );
System . out . println ( "**WaitForInstanceStart**" );
try {
WorkflowInstanceStatus waitForInstanceStartResult =
client . waitForInstanceStart ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForInstanceStartResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForInstanceStart has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**SendExternalMessage**" );
client . raiseEvent ( instanceId , "TestEvent" , "TestEventPayload" );
System . out . println ( separatorStr );
System . out . println ( "** Registering parallel Events to be captured by allOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "event1" , "TestEvent 1 Payload" );
client . raiseEvent ( instanceId , "event2" , "TestEvent 2 Payload" );
client . raiseEvent ( instanceId , "event3" , "TestEvent 3 Payload" );
System . out . printf ( "Events raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "** Registering Event to be captured by anyOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "e2" , "event 2 Payload" );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**WaitForInstanceCompletion**" );
try {
WorkflowInstanceStatus waitForInstanceCompletionResult =
client . waitForInstanceCompletion ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForInstanceCompletionResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForInstanceCompletion has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**purgeInstance**" );
boolean purgeResult = client . purgeInstance ( instanceId );
System . out . printf ( "purgeResult: %s%n" , purgeResult );
System . out . println ( separatorStr );
System . out . println ( "**raiseEvent**" );
String eventInstanceId = client . scheduleNewWorkflow ( DemoWorkflow . class );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , eventInstanceId );
client . raiseEvent ( eventInstanceId , "TestException" , null );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , eventInstanceId );
System . out . println ( separatorStr );
String instanceToTerminateId = "terminateMe" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , instanceToTerminateId );
System . out . printf ( "Started new workflow instance with specified ID: %s%n" , instanceToTerminateId );
TimeUnit . SECONDS . sleep ( 5 );
System . out . println ( "Terminate this workflow instance manually before the timeout is reached" );
client . terminateWorkflow ( instanceToTerminateId , null );
System . out . println ( separatorStr );
String restartingInstanceId = "restarting" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , restartingInstanceId );
System . out . printf ( "Started new workflow instance with ID: %s%n" , restartingInstanceId );
System . out . println ( "Sleeping 30 seconds to restart the workflow" );
TimeUnit . SECONDS . sleep ( 30 );
System . out . println ( "**SendExternalMessage: RestartEvent**" );
client . raiseEvent ( restartingInstanceId , "RestartEvent" , "RestartEventPayload" );
System . out . println ( "Sleeping 30 seconds to terminate the eternal workflow" );
TimeUnit . SECONDS . sleep ( 30 );
client . terminateWorkflow ( restartingInstanceId , null );
}
System . out . println ( "Exiting DemoWorkflowClient." );
System . exit ( 0 );
}
}
在第二个终端窗口中,通过运行以下命令启动工作流:
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowClient
预期输出
*******
Started new workflow instance with random ID: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**GetInstanceMetadata:Running Workflow**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**WaitForInstanceStart**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**SendExternalMessage**
*******
** Registering parallel Events to be captured by allOf(t1,t2,t3) **
Events raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
** Registering Event to be captured by anyOf(t1,t2,t3) **
Event raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**WaitForInstanceCompletion**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: FAILED, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:55.054Z, Input: '"input data"', Output: '']
*******
**purgeInstance**
purgeResult: true
*******
**raiseEvent**
Started new workflow instance with random ID: 7707d141-ebd0-4e54-816e-703cb7a52747
Event raised for workflow with instanceId: 7707d141-ebd0-4e54-816e-703cb7a52747
*******
Started new workflow instance with specified ID: terminateMe
Terminate this workflow instance manually before the timeout is reached
*******
Started new workflow instance with ID: restarting
Sleeping 30 seconds to restart the workflow
**SendExternalMessage: RestartEvent**
Sleeping 30 seconds to terminate the eternal workflow
Exiting DemoWorkflowClient.
发生了什么? 当您运行 dapr run
时,工作流工作者将工作流(DemoWorkflow
)及其活动注册到 Dapr 工作流引擎。 当您运行 java
时,工作流客户端启动了具有以下活动的工作流实例。您可以在运行 dapr run
的终端中查看输出。工作流启动,触发三个并行任务,并等待它们完成。 工作流客户端调用活动并将 “Hello Activity” 消息发送到控制台。 工作流超时并被清除。 工作流客户端启动一个具有随机 ID 的新工作流实例,使用另一个名为 terminateMe
的工作流实例终止它,并使用名为 restarting
的工作流重新启动它。 然后工作流客户端退出。 下一步 3.2 - 使用 Dapr 客户端 Java SDK 入门 如何使用 Dapr Java SDK 快速上手
Dapr 客户端包使您能够从 Java 应用程序与其他 Dapr 应用程序进行交互。
注意 如果您还没有尝试过,
请尝试其中一个快速入门 ,以快速了解如何使用 Dapr Java SDK 和 API 构建块。
前提条件 完成初始设置并将 Java SDK 导入您的项目
初始化客户端 您可以这样初始化 Dapr 客户端:
DaprClient client = new DaprClientBuilder (). build ();
这会连接到默认的 Dapr gRPC 端点 localhost:50001
。
环境变量 Dapr Sidecar 端点 您可以使用标准化的 DAPR_GRPC_ENDPOINT
和 DAPR_HTTP_ENDPOINT
环境变量来指定不同的 gRPC 或 HTTP 端点。当设置了这些变量时,客户端将自动使用它们连接到 Dapr sidecar。
旧的环境变量 DAPR_HTTP_PORT
和 DAPR_GRPC_PORT
仍然受支持,但 DAPR_GRPC_ENDPOINT
和 DAPR_HTTP_ENDPOINT
优先。
Dapr API 令牌 如果您的 Dapr 实例需要 DAPR_API_TOKEN
环境变量,您可以在环境中设置,客户端会自动使用。 您可以在这里 阅读更多关于 Dapr API 令牌认证的信息。
错误处理 最初,Dapr 中的错误遵循标准 gRPC 错误模型。为了提供更详细的信息,在 1.13 版本中引入了一个增强的错误模型,与 gRPC 的丰富错误模型对齐。Java SDK 扩展了 DaprException,以包含 Dapr 中添加的错误详细信息。
使用 Dapr Java SDK 处理 DaprException 并消费错误详细信息的示例:
...
try {
client . publishEvent ( "unknown_pubsub" , "mytopic" , "mydata" ). block ();
} catch ( DaprException exception ) {
System . out . println ( "Dapr 异常的错误代码: " + exception . getErrorCode ());
System . out . println ( "Dapr 异常的消息: " + exception . getMessage ());
// DaprException 现在通过 `getStatusDetails()` 提供来自 Dapr 运行时的更多错误详细信息。
System . out . println ( "Dapr 异常的原因: " + exception . getStatusDetails (). get (
DaprErrorDetails . ErrorDetailType . ERROR_INFO ,
"reason" ,
TypeRef . STRING ));
}
...
构建块 Java SDK 允许您与所有 Dapr 构建块 进行接口交互。
调用服务 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 调用 'GET' 方法 (HTTP) 跳过序列化: 返回类型为 Mono<byte[]>
// 对于 gRPC 设置 HttpExtension.NONE 参数
response = client . invokeMethod ( SERVICE_TO_INVOKE , METHOD_TO_INVOKE , "{\"name\":\"World!\"}" , HttpExtension . GET , byte [] . class ). block ();
// 调用 'POST' 方法 (HTTP) 跳过序列化: 返回类型为 Mono<byte[]>
response = client . invokeMethod ( SERVICE_TO_INVOKE , METHOD_TO_INVOKE , "{\"id\":\"100\", \"FirstName\":\"Value\", \"LastName\":\"Value\"}" , HttpExtension . POST , byte [] . class ). block ();
System . out . println ( new String ( response ));
// 调用 'POST' 方法 (HTTP) 带序列化: 返回类型为 Mono<Employee>
Employee newEmployee = new Employee ( "Nigel" , "Guitarist" );
Employee employeeResponse = client . invokeMethod ( SERVICE_TO_INVOKE , "employees" , newEmployee , HttpExtension . POST , Employee . class ). block ();
}
保存和获取应用程序状态 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.domain.State ;
import reactor.core.publisher.Mono ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 保存状态
client . saveState ( STATE_STORE_NAME , FIRST_KEY_NAME , myClass ). block ();
// 获取状态
State < MyClass > retrievedMessage = client . getState ( STATE_STORE_NAME , FIRST_KEY_NAME , MyClass . class ). block ();
// 删除状态
client . deleteState ( STATE_STORE_NAME , FIRST_KEY_NAME ). block ();
}
发布和订阅消息 发布消息 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.domain.Metadata ;
import static java.util.Collections.singletonMap ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
client . publishEvent ( PUBSUB_NAME , TOPIC_NAME , message , singletonMap ( Metadata . TTL_IN_SECONDS , MESSAGE_TTL_IN_SECONDS )). block ();
}
订阅消息 import com.fasterxml.jackson.databind.ObjectMapper ;
import io.dapr.Topic ;
import io.dapr.client.domain.BulkSubscribeAppResponse ;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry ;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus ;
import io.dapr.client.domain.BulkSubscribeMessage ;
import io.dapr.client.domain.BulkSubscribeMessageEntry ;
import io.dapr.client.domain.CloudEvent ;
import io.dapr.springboot.annotations.BulkSubscribe ;
import org.springframework.web.bind.annotation.PostMapping ;
import org.springframework.web.bind.annotation.RequestBody ;
import org.springframework.web.bind.annotation.RestController ;
import reactor.core.publisher.Mono ;
@RestController
public class SubscriberController {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
@Topic ( name = "testingtopic" , pubsubName = "${myAppProperty:messagebus}" )
@PostMapping ( path = "/testingtopic" )
public Mono < Void > handleMessage ( @RequestBody ( required = false ) CloudEvent <?> cloudEvent ) {
return Mono . fromRunnable (() -> {
try {
System . out . println ( "Subscriber got: " + cloudEvent . getData ());
System . out . println ( "Subscriber got: " + OBJECT_MAPPER . writeValueAsString ( cloudEvent ));
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
});
}
@Topic ( name = "testingtopic" , pubsubName = "${myAppProperty:messagebus}" ,
rule = @Rule ( match = "event.type == 'myevent.v2'" , priority = 1 ))
@PostMapping ( path = "/testingtopicV2" )
public Mono < Void > handleMessageV2 ( @RequestBody ( required = false ) CloudEvent envelope ) {
return Mono . fromRunnable (() -> {
try {
System . out . println ( "Subscriber got: " + cloudEvent . getData ());
System . out . println ( "Subscriber got: " + OBJECT_MAPPER . writeValueAsString ( cloudEvent ));
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
});
}
@BulkSubscribe ()
@Topic ( name = "testingtopicbulk" , pubsubName = "${myAppProperty:messagebus}" )
@PostMapping ( path = "/testingtopicbulk" )
public Mono < BulkSubscribeAppResponse > handleBulkMessage (
@RequestBody ( required = false ) BulkSubscribeMessage < CloudEvent < String >> bulkMessage ) {
return Mono . fromCallable (() -> {
if ( bulkMessage . getEntries (). size () == 0 ) {
return new BulkSubscribeAppResponse ( new ArrayList < BulkSubscribeAppResponseEntry > ());
}
System . out . println ( "Bulk Subscriber received " + bulkMessage . getEntries (). size () + " messages." );
List < BulkSubscribeAppResponseEntry > entries = new ArrayList < BulkSubscribeAppResponseEntry > ();
for ( BulkSubscribeMessageEntry <?> entry : bulkMessage . getEntries ()) {
try {
System . out . printf ( "Bulk Subscriber message has entry ID: %s\n" , entry . getEntryId ());
CloudEvent <?> cloudEvent = ( CloudEvent <?> ) entry . getEvent ();
System . out . printf ( "Bulk Subscriber got: %s\n" , cloudEvent . getData ());
entries . add ( new BulkSubscribeAppResponseEntry ( entry . getEntryId (), BulkSubscribeAppResponseStatus . SUCCESS ));
} catch ( Exception e ) {
e . printStackTrace ();
entries . add ( new BulkSubscribeAppResponseEntry ( entry . getEntryId (), BulkSubscribeAppResponseStatus . RETRY ));
}
}
return new BulkSubscribeAppResponse ( entries );
});
}
}
批量发布消息 注意: API 处于 Alpha 阶段
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.BulkPublishResponse ;
import io.dapr.client.domain.BulkPublishResponseFailedEntry ;
import java.util.ArrayList ;
import java.util.List ;
class Solution {
public void publishMessages () {
try ( DaprPreviewClient client = ( new DaprClientBuilder ()). buildPreviewClient ()) {
// 创建要发布的消息列表
List < String > messages = new ArrayList <> ();
for ( int i = 0 ; i < NUM_MESSAGES ; i ++ ) {
String message = String . format ( "This is message #%d" , i );
messages . add ( message );
System . out . println ( "Going to publish message : " + message );
}
// 使用批量发布 API 发布消息列表
BulkPublishResponse < String > res = client . publishEvents ( PUBSUB_NAME , TOPIC_NAME , "text/plain" , messages ). block ()
}
}
}
与输出绑定交互 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 发送带有消息的类; BINDING_OPERATION="create"
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , myClass ). block ();
// 发送纯字符串
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , message ). block ();
}
与输入绑定交互 import org.springframework.web.bind.annotation.* ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
@RestController
@RequestMapping ( "/" )
public class myClass {
private static final Logger log = LoggerFactory . getLogger ( myClass );
@PostMapping ( path = "/checkout" )
public Mono < String > getCheckout ( @RequestBody ( required = false ) byte [] body ) {
return Mono . fromRunnable (() ->
log . info ( "Received Message: " + new String ( body )));
}
}
检索秘密 import com.fasterxml.jackson.databind.ObjectMapper ;
importio . dapr . client . DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import java.util.Map ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
Map < String , String > secret = client . getSecret ( SECRET_STORE_NAME , secretKey ). block ();
System . out . println ( JSON_SERIALIZER . writeValueAsString ( secret ));
}
Actors actor 是一个具有单线程执行的隔离、独立的计算和状态单元。Dapr 提供了一种基于 虚拟 actor 模式 的 actor 实现,该模式提供了单线程编程模型,并且当 actor 不在使用时会被垃圾回收。使用 Dapr 的实现,您可以根据 actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性。
import io.dapr.actors.ActorMethod ;
import io.dapr.actors.ActorType ;
import reactor.core.publisher.Mono ;
@ActorType ( name = "DemoActor" )
public interface DemoActor {
void registerReminder ();
@ActorMethod ( name = "echo_message" )
String say ( String something );
void clock ( String message );
@ActorMethod ( returns = Integer . class )
Mono < Integer > incrementAndGet ( int delta );
}
获取和订阅应用程序配置 注意这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.ConfigurationItem ;
import io.dapr.client.domain.GetConfigurationRequest ;
import io.dapr.client.domain.SubscribeConfigurationRequest ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Mono ;
try ( DaprPreviewClient client = ( new DaprClientBuilder ()). buildPreviewClient ()) {
// 获取单个键的配置
Mono < ConfigurationItem > item = client . getConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY ). block ();
// 获取多个键的配置
Mono < Map < String , ConfigurationItem >> items =
client . getConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY_1 , CONFIG_KEY_2 );
// 订阅配置更改
Flux < SubscribeConfigurationResponse > outFlux = client . subscribeConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY_1 , CONFIG_KEY_2 );
outFlux . subscribe ( configItems -> configItems . forEach (...));
// 取消订阅配置更改
Mono < UnsubscribeConfigurationResponse > unsubscribe = client . unsubscribeConfiguration ( SUBSCRIPTION_ID , CONFIG_STORE_NAME )
}
查询保存的状态 注意这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.QueryStateItem ;
import io.dapr.client.domain.QueryStateRequest ;
import io.dapr.client.domain.QueryStateResponse ;
import io.dapr.client.domain.query.Query ;
import io.dapr.client.domain.query.Sorting ;
import io.dapr.client.domain.query.filters.EqFilter ;
try ( DaprClient client = builder . build (); DaprPreviewClient previewClient = builder . buildPreviewClient ()) {
String searchVal = args . length == 0 ? "searchValue" : args [ 0 ] ;
// 创建 JSON 数据
Listing first = new Listing ();
first . setPropertyType ( "apartment" );
first . setId ( "1000" );
...
Listing second = new Listing ();
second . setPropertyType ( "row-house" );
second . setId ( "1002" );
...
Listing third = new Listing ();
third . setPropertyType ( "apartment" );
third . setId ( "1003" );
...
Listing fourth = new Listing ();
fourth . setPropertyType ( "apartment" );
fourth . setId ( "1001" );
...
Map < String , String > meta = new HashMap <> ();
meta . put ( "contentType" , "application/json" );
// 保存状态
SaveStateRequest request = new SaveStateRequest ( STATE_STORE_NAME ). setStates (
new State <> ( "1" , first , null , meta , null ),
new State <> ( "2" , second , null , meta , null ),
new State <> ( "3" , third , null , meta , null ),
new State <> ( "4" , fourth , null , meta , null )
);
client . saveBulkState ( request ). block ();
// 创建查询和查询状态请求
Query query = new Query ()
. setFilter ( new EqFilter <> ( "propertyType" , "apartment" ))
. setSort ( Arrays . asList ( new Sorting ( "id" , Sorting . Order . DESC )));
QueryStateRequest request = new QueryStateRequest ( STATE_STORE_NAME )
. setQuery ( query );
// 使用预览客户端调用查询状态 API
QueryStateResponse < MyData > result = previewClient . queryState ( request , MyData . class ). block ();
// 查看查询状态响应
System . out . println ( "Found " + result . getResults (). size () + " items." );
for ( QueryStateItem < Listing > item : result . getResults ()) {
System . out . println ( "Key: " + item . getKey ());
System . out . println ( "Data: " + item . getValue ());
}
}
分布式锁 package io.dapr.examples.lock.grpc ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.LockRequest ;
import io.dapr.client.domain.UnlockRequest ;
import io.dapr.client.domain.UnlockResponseStatus ;
import reactor.core.publisher.Mono ;
public class DistributedLockGrpcClient {
private static final String LOCK_STORE_NAME = "lockstore" ;
/**
* 执行各种方法以检查不同的 API。
*
* @param args 参数
* @throws Exception 抛出异常
*/
public static void main ( String [] args ) throws Exception {
try ( DaprPreviewClient client = ( new DaprClientBuilder ()). buildPreviewClient ()) {
System . out . println ( "Using preview client..." );
tryLock ( client );
unlock ( client );
}
}
/**
* 尝试获取锁。
*
* @param client DaprPreviewClient 对象
*/
public static void tryLock ( DaprPreviewClient client ) {
System . out . println ( "*******尝试获取一个空闲的分布式锁********" );
try {
LockRequest lockRequest = new LockRequest ( LOCK_STORE_NAME , "resouce1" , "owner1" , 5 );
Mono < Boolean > result = client . tryLock ( lockRequest );
System . out . println ( "Lock result -> " + ( Boolean . TRUE . equals ( result . block ()) ? "SUCCESS" : "FAIL" ));
} catch ( Exception ex ) {
System . out . println ( ex . getMessage ());
}
}
/**
* 解锁。
*
* @param client DaprPreviewClient 对象
*/
public static void unlock ( DaprPreviewClient client ) {
System . out . println ( "*******解锁一个分布式锁********" );
try {
UnlockRequest unlockRequest = new UnlockRequest ( LOCK_STORE_NAME , "resouce1" , "owner1" );
Mono < UnlockResponseStatus > result = client . unlock ( unlockRequest );
System . out . println ( "Unlock result ->" + result . block (). name ());
} catch ( Exception ex ) {
System . out . println ( ex . getMessage ());
}
}
}
工作流 package io.dapr.examples.workflows ;
import io.dapr.workflows.client.DaprWorkflowClient ;
import io.dapr.workflows.client.WorkflowInstanceStatus ;
import java.time.Duration ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeoutException ;
/**
* 有关设置说明,请参阅 README。
*/
public class DemoWorkflowClient {
/**
* 主方法。
*
* @param args 输入参数(未使用)。
* @throws InterruptedException 如果程序被中断。
*/
public static void main ( String [] args ) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient ();
try ( client ) {
String separatorStr = "*******" ;
System . out . println ( separatorStr );
String instanceId = client . scheduleNewWorkflow ( DemoWorkflow . class , "input data" );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**GetInstanceMetadata:Running Workflow**" );
WorkflowInstanceStatus workflowMetadata = client . getInstanceState ( instanceId , true );
System . out . printf ( "Result: %s%n" , workflowMetadata );
System . out . println ( separatorStr );
System . out . println ( "**WaitForInstanceStart**" );
try {
WorkflowInstanceStatus waitForInstanceStartResult =
client . waitForInstanceStart ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForInstanceStartResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForInstanceStart has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**SendExternalMessage**" );
client . raiseEvent ( instanceId , "TestEvent" , "TestEventPayload" );
System . out . println ( separatorStr );
System . out . println ( "** Registering parallel Events to be captured by allOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "event1" , "TestEvent 1 Payload" );
client . raiseEvent ( instanceId , "event2" , "TestEvent 2 Payload" );
client . raiseEvent ( instanceId , "event3" , "TestEvent 3 Payload" );
System . out . printf ( "Events raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "** Registering Event to be captured by anyOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "e2" , "event 2 Payload" );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**WaitForInstanceCompletion**" );
try {
WorkflowInstanceStatus waitForInstanceCompletionResult =
client . waitForInstanceCompletion ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForInstanceCompletionResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForInstanceCompletion has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**purgeInstance**" );
boolean purgeResult = client . purgeInstance ( instanceId );
System . out . printf ( "purgeResult: %s%n" , purgeResult );
System . out . println ( separatorStr );
System . out . println ( "**raiseEvent**" );
String eventInstanceId = client . scheduleNewWorkflow ( DemoWorkflow . class );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , eventInstanceId );
client . raiseEvent ( eventInstanceId , "TestException" , null );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , eventInstanceId );
System . out . println ( separatorStr );
String instanceToTerminateId = "terminateMe" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , instanceToTerminateId );
System . out . printf ( "Started new workflow instance with specified ID: %s%n" , instanceToTerminateId );
TimeUnit . SECONDS . sleep ( 5 );
System . out . println ( "Terminate this workflow instance manually before the timeout is reached" );
client . terminateWorkflow ( instanceToTerminateId , null );
System . out . println ( separatorStr );
String restartingInstanceId = "restarting" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , restartingInstanceId );
System . out . printf ( "Started new workflow instance with ID: %s%n" , restartingInstanceId );
System . out . println ( "Sleeping 30 seconds to restart the workflow" );
TimeUnit . SECONDS . sleep ( 30 );
System . out . println ( "**SendExternalMessage: RestartEvent**" );
client . raiseEvent ( restartingInstanceId , "RestartEvent" , "RestartEventPayload" );
System . out . println ( "Sleeping 30 seconds to terminate the eternal workflow" );
TimeUnit . SECONDS . sleep ( 30 );
client . terminateWorkflow ( restartingInstanceId , null );
}
System . out . println ( "Exiting DemoWorkflowClient." );
System . exit ( 0 );
}
}
Sidecar API 等待 sidecar DaprClient
还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,请确保指定超时时间(以毫秒为单位)并使用 block() 来等待反应操作的结果。
// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
try ( DaprClient client = new DaprClientBuilder (). build ()) {
System . out . println ( "Waiting for Dapr sidecar ..." );
client . waitForSidecar ( 10000 ). block (); // 指定超时时间(以毫秒为单位)
System . out . println ( "Dapr sidecar is ready." );
...
}
// 在此处执行 Dapr 组件操作,例如获取秘密或保存状态。
关闭 sidecar try ( DaprClient client = new DaprClientBuilder (). build ()) {
logger . info ( "Sending shutdown request." );
client . shutdown (). block ();
logger . info ( "Ensuring dapr has stopped." );
...
}
了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包 。
相关链接 3.3 - 开始使用 Dapr 和 Spring Boot 如何开始使用 Dapr 和 Spring Boot
通过将 Dapr 和 Spring Boot 结合使用,我们可以创建不依赖于特定基础设施的 Java 应用程序,这些应用程序可以部署在不同的环境中,并支持多种本地和云服务提供商。
首先,我们将从一个简单的集成开始,涵盖 DaprClient
和 Testcontainers 的集成,然后利用 Spring 和 Spring Boot 的机制及编程模型来使用 Dapr API。这有助于团队消除连接到特定环境基础设施(如数据库、键值存储、消息代理、配置/密钥存储等)所需的客户端和驱动程序等依赖项。
注意 本页面解释的 Spring Boot 集成仍处于 alpha 阶段,因此大多数工件标记为 0.13.0。将 Dapr 和 Spring Boot 集成添加到您的项目中 如果您已经有一个 Spring Boot 应用程序(Spring Boot 3.x+),可以直接将以下依赖项添加到您的项目中:
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>0.13.1</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
通过添加这些依赖项,您可以:
自动装配一个 DaprClient
以在您的应用程序中使用 使用 Spring Data 和 Messaging 的抽象及编程模型,这些模型在底层使用 Dapr API 通过依赖 Testcontainers 来引导 Dapr 控制平面服务和默认组件,从而改善您的开发流程 一旦这些依赖项在您的应用程序中,您可以依赖 Spring Boot 自动配置来自动装配一个 DaprClient
实例:
@Autowired
private DaprClient daprClient ;
这将连接到默认的 Dapr gRPC 端点 localhost:50001
,需要您在应用程序外启动 Dapr。
您可以在应用程序中的任何地方使用 DaprClient
与 Dapr API 交互,例如在 REST 端点内部:
@RestController
public class DemoRestController {
@Autowired
private DaprClient daprClient ;
@PostMapping ( "/store" )
public void storeOrder ( @RequestBody Order order ){
daprClient . saveState ( "kvstore" , order . orderId (), order ). block ();
}
}
record Order ( String orderId , Integer amount ){}
如果您想避免在 Spring Boot 应用程序外管理 Dapr,可以依赖 Testcontainers 来在开发过程中引导 Dapr。为此,我们可以创建一个测试配置,使用 Testcontainers
来引导我们需要的所有内容,以使用 Dapr API 开发我们的应用程序。
通过使用 Testcontainers 和 Dapr 的集成,我们可以让 @TestConfiguration
为我们的应用程序引导 Dapr。注意,在此示例中,我们配置了一个名为 kvstore
的 Statestore 组件,该组件连接到由 Testcontainers 引导的 PostgreSQL
实例。
@TestConfiguration ( proxyBeanMethods = false )
public class DaprTestContainersConfig {
@Bean
@ServiceConnection
public DaprContainer daprContainer ( Network daprNetwork , PostgreSQLContainer <?> postgreSQLContainer ){
return new DaprContainer ( "daprio/daprd:1.14.1" )
. withAppName ( "producer-app" )
. withNetwork ( daprNetwork )
. withComponent ( new Component ( "kvstore" , "state.postgresql" , "v1" , STATE_STORE_PROPERTIES ))
. withComponent ( new Component ( "kvbinding" , "bindings.postgresql" , "v1" , BINDING_PROPERTIES ))
. dependsOn ( postgreSQLContainer );
}
}
在测试类路径中,您可以添加一个新的 Spring Boot 应用程序,使用此配置进行测试:
@SpringBootApplication
public class TestProducerApplication {
public static void main ( String [] args ) {
SpringApplication
. from ( ProducerApplication :: main )
. with ( DaprTestContainersConfig . class )
. run ( args );
}
}
现在您可以启动您的应用程序:
运行此命令将启动应用程序,使用提供的测试配置,其中包括 Testcontainers 和 Dapr 集成。在日志中,您应该能够看到 daprd
和 placement
服务容器已为您的应用程序启动。
除了之前的配置(DaprTestContainersConfig
),您的测试不应该测试 Dapr 本身,只需测试您的应用程序暴露的 REST 端点。
利用 Spring 和 Spring Boot 编程模型与 Dapr Java SDK 允许您与所有 Dapr 构建块 接口。但如果您想利用 Spring 和 Spring Boot 编程模型,可以使用 dapr-spring-boot-starter
集成。这包括 Spring Data 的实现(KeyValueTemplate
和 CrudRepository
)以及用于生产和消费消息的 DaprMessagingTemplate
(类似于 Spring Kafka 、Spring Pulsar 和 Spring AMQP for RabbitMQ )。
使用 Spring Data CrudRepository
和 KeyValueTemplate
您可以使用依赖于 Dapr 实现的知名 Spring Data 构造。使用 Dapr,您无需添加任何与基础设施相关的驱动程序或客户端,使您的 Spring 应用程序更轻量化,并与其运行的环境解耦。
在底层,这些实现使用 Dapr Statestore 和 Binding API。
配置参数 使用 Spring Data 抽象,您可以配置 Dapr 将用于连接到可用基础设施的 statestore 和 bindings。这可以通过设置以下属性来完成:
dapr.statestore.name = kvstore
dapr.statestore.binding = kvbinding
然后您可以像这样 @Autowire
一个 KeyValueTemplate
或 CrudRepository
:
@RestController
@EnableDaprRepositories
public class OrdersRestController {
@Autowired
private OrderRepository repository ;
@PostMapping ( "/orders" )
public void storeOrder ( @RequestBody Order order ){
repository . save ( order );
}
@GetMapping ( "/orders" )
public Iterable < Order > getAll (){
return repository . findAll ();
}
}
其中 OrderRepository
在一个扩展 Spring Data CrudRepository
接口的接口中定义:
public interface OrderRepository extends CrudRepository < Order , String > {}
注意,@EnableDaprRepositories
注解完成了在 CrudRespository
接口下连接 Dapr API 的所有工作。因为 Dapr 允许用户从同一个应用程序与不同的 StateStores 交互,作为用户,您需要提供以下 bean 作为 Spring Boot @Configuration
:
@Configuration
@EnableConfigurationProperties ({ DaprStateStoreProperties . class })
public class ProducerAppConfiguration {
@Bean
public KeyValueAdapterResolver keyValueAdapterResolver ( DaprClient daprClient , ObjectMapper mapper , DaprStateStoreProperties daprStatestoreProperties ) {
String storeName = daprStatestoreProperties . getName ();
String bindingName = daprStatestoreProperties . getBinding ();
return new DaprKeyValueAdapterResolver ( daprClient , mapper , storeName , bindingName );
}
@Bean
public DaprKeyValueTemplate daprKeyValueTemplate ( KeyValueAdapterResolver keyValueAdapterResolver ) {
return new DaprKeyValueTemplate ( keyValueAdapterResolver );
}
}
使用 Spring Messaging 生产和消费事件 类似于 Spring Kafka、Spring Pulsar 和 Spring AMQP,您可以使用 DaprMessagingTemplate
将消息发布到配置的基础设施。要消费消息,您可以使用 @Topic
注解(即将重命名为 @DaprListener
)。
要发布事件/消息,您可以在 Spring 应用程序中 @Autowired
DaprMessagingTemplate
。在此示例中,我们将发布 Order
事件,并将消息发送到名为 topic
的主题。
@Autowired
private DaprMessagingTemplate < Order > messagingTemplate ;
@PostMapping ( "/orders" )
public void storeOrder ( @RequestBody Order order ){
repository . save ( order );
messagingTemplate . send ( "topic" , order );
}
与 CrudRepository
类似,我们需要指定要使用哪个 PubSub 代理来发布和消费我们的消息。
因为使用 Dapr,您可以连接到多个 PubSub 代理,您需要提供以下 bean 以让 Dapr 知道您的 DaprMessagingTemplate
将使用哪个 PubSub 代理:
@Bean
public DaprMessagingTemplate < Order > messagingTemplate ( DaprClient daprClient ,
DaprPubSubProperties daprPubSubProperties ) {
return new DaprMessagingTemplate <> ( daprClient , daprPubSubProperties . getName ());
}
最后,因为 Dapr PubSub 需要您的应用程序和 Dapr 之间的双向连接,您需要使用一些参数扩展您的 Testcontainers 配置:
@Bean
@ServiceConnection
public DaprContainer daprContainer ( Network daprNetwork , PostgreSQLContainer <?> postgreSQLContainer , RabbitMQContainer rabbitMQContainer ){
return new DaprContainer ( "daprio/daprd:1.14.1" )
. withAppName ( "producer-app" )
. withNetwork ( daprNetwork )
. withComponent ( new Component ( "kvstore" , "state.postgresql" , "v1" , STATE_STORE_PROPERTIES ))
. withComponent ( new Component ( "kvbinding" , "bindings.postgresql" , "v1" , BINDING_PROPERTIES ))
. withComponent ( new Component ( "pubsub" , "pubsub.rabbitmq" , "v1" , rabbitMqProperties ))
. withAppPort ( 8080 )
. withAppChannelAddress ( "host.testcontainers.internal" )
. dependsOn ( rabbitMQContainer )
. dependsOn ( postgreSQLContainer );
}
现在,在 Dapr 配置中,我们包含了一个 pubsub
组件,该组件将连接到由 Testcontainers 启动的 RabbitMQ 实例。我们还设置了两个重要参数 .withAppPort(8080)
和 .withAppChannelAddress("host.testcontainers.internal")
,这允许 Dapr 在代理中发布消息时联系回应用程序。
要监听事件/消息,您需要在应用程序中暴露一个端点,该端点将负责接收消息。如果您暴露一个 REST 端点,可以使用 @Topic
注解让 Dapr 知道它需要将事件/消息转发到哪里:
@PostMapping ( "subscribe" )
@Topic ( pubsubName = "pubsub" , name = "topic" )
public void subscribe ( @RequestBody CloudEvent < Order > cloudEvent ){
events . add ( cloudEvent );
}
在引导您的应用程序时,Dapr 将注册订阅,以便将消息转发到您的应用程序暴露的 subscribe
端点。
如果您正在为这些订阅者编写测试,您需要确保 Testcontainers 知道您的应用程序将在端口 8080 上运行,以便 Testcontainers 启动的容器知道您的应用程序在哪里:
@BeforeAll
public static void setup (){
org . testcontainers . Testcontainers . exposeHostPorts ( 8080 );
}
您可以在此处查看并运行完整示例源代码 。
下一步 了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包 的信息。
相关链接 4 - JavaScript SDK 用于开发Dapr应用的JavaScript SDK
这是一个用于在JavaScript和TypeScript中构建Dapr应用的开发库。该库对Dapr的常用API进行了抽象,如服务调用、状态管理、发布订阅、密钥管理等,并提供了一个简单直观的API接口来帮助构建应用。
安装 要开始使用JavaScript SDK,请从NPM 安装Dapr JavaScript SDK:
npm install --save @dapr/dapr
结构 Dapr JavaScript SDK包含两个主要组件:
DaprServer :用于管理Dapr sidecar与应用之间的通信。DaprClient :用于管理应用与Dapr sidecar之间的通信。这些通信可以配置为使用gRPC或HTTP协议。
入门 为了帮助您快速上手,请查看以下资源:
客户端 创建一个JavaScript客户端,与Dapr sidecar和其他Dapr应用进行交互(例如,发布事件,支持输出绑定等)。
服务器 创建一个JavaScript服务器,让Dapr sidecar与您的应用进行交互(例如,订阅事件,支持输入绑定等)。
虚拟演员 创建具有状态、提醒/计时器和方法的虚拟演员。
示例 获取JavaScript SDK的源代码并尝试一些示例以快速入门。
4.1 - JavaScript 客户端 SDK 用于开发 Dapr 应用的 JavaScript 客户端 SDK
介绍 Dapr 客户端使您能够与 Dapr sidecar 进行通信,并访问其面向客户端的功能,如发布事件、调用输出绑定、状态管理、密钥管理等。
前置条件 安装和导入 Dapr 的 JS SDK 使用 npm
安装 SDK: 导入库: import { DaprClient , DaprServer , HttpMethod , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // 示例服务器的 Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 示例服务器的应用主机
const serverPort = "50051" ; // 示例服务器的应用端口
// HTTP 示例
const client = new DaprClient ({ daprHost , daprPort });
// GRPC 示例
const client = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocolEnum.GRPC });
运行 要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。
使用 HTTP(默认) import { DaprClient } from "@dapr/dapr" ;
const client = new DaprClient ({ daprHost , daprPort });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start
# 或者,使用 npm 脚本
npm run start:dapr-http
使用 gRPC 由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递一个额外的参数来实现这一点。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
const client = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocol.GRPC });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start
# 或者,使用 npm 脚本
npm run start:dapr-grpc
环境变量 Dapr sidecar 端点 您可以使用 DAPR_HTTP_ENDPOINT
和 DAPR_GRPC_ENDPOINT
环境变量分别设置 Dapr sidecar 的 HTTP 和 gRPC 端点。当这些变量被设置时,daprHost
和 daprPort
不需要在构造函数的选项参数中设置,客户端将自动从提供的端点中解析它们。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
// 使用 HTTP,当 DAPR_HTTP_ENDPOINT 被设置时
const client = new DaprClient ();
// 使用 gRPC,当 DAPR_GRPC_ENDPOINT 被设置时
const client = new DaprClient ({ communicationProtocol : CommunicationProtocol.GRPC });
如果环境变量被设置,但 daprHost
和 daprPort
值被传递给构造函数,后者将优先于环境变量。
Dapr API 令牌 您可以使用 DAPR_API_TOKEN
环境变量设置 Dapr API 令牌。当此变量被设置时,daprApiToken
不需要在构造函数的选项参数中设置,客户端将自动获取它。
通用 增加主体大小 您可以通过使用 DaprClient
的选项增加应用程序与 sidecar 通信时使用的主体大小。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
// 允许使用 10Mb 的主体大小
// 默认是 4Mb
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocol.HTTP ,
maxBodySizeMb : 10 ,
});
代理请求 通过代理请求,我们可以利用 Dapr 的 sidecar 架构带来的独特功能,如服务发现、日志记录等,使我们能够立即“升级”我们的 gRPC 服务。在 社区电话 41 中演示了 gRPC 代理的这一特性。
创建代理 要执行 gRPC 代理,只需通过调用 client.proxy.create()
方法创建一个代理:
// 一如既往,创建一个到我们 Dapr sidecar 的客户端
// 这个客户端负责确保 sidecar 已启动,我们可以通信,...
const clientSidecar = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocol.GRPC });
// 创建一个允许我们使用 gRPC 代码的代理
const clientProxy = await clientSidecar . proxy . create < GreeterClient >( GreeterClient );
我们现在可以调用在我们的 GreeterClient
接口中定义的方法(在这种情况下是来自 Hello World 示例 )
技术细节
gRPC 服务在 Dapr 中启动。我们通过 --app-port
告诉 Dapr 这个 gRPC 服务器运行在哪个端口,并通过 --app-id <APP_ID_HERE>
给它一个唯一的 Dapr 应用 ID 我们现在可以通过一个将连接到 sidecar 的客户端调用 Dapr sidecar 在调用 Dapr sidecar 时,我们提供一个名为 dapr-app-id
的元数据键,其值为在 Dapr 中启动的 gRPC 服务器(例如,在我们的示例中为 server
) Dapr 现在将调用转发到配置的 gRPC 服务器 构建块 JavaScript 客户端 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是客户端到 sidecar 的功能。
调用 API 调用服务 import { DaprClient , HttpMethod } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const serviceAppId = "my-app-id" ;
const serviceMethod = "say-hello" ;
// POST 请求
const response = await client . invoker . invoke ( serviceAppId , serviceMethod , HttpMethod . POST , { hello : "world" });
// 带有头部的 POST 请求
const response = await client . invoker . invoke (
serviceAppId ,
serviceMethod ,
HttpMethod . POST ,
{ hello : "world" },
{ headers : { "X-User-ID" : "123" } },
);
// GET 请求
const response = await client . invoker . invoke ( serviceAppId , serviceMethod , HttpMethod . GET );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关服务调用的完整指南,请访问 如何:调用服务 。
状态管理 API 保存、获取和删除应用状态 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const serviceStoreName = "my-state-store-name" ;
// 保存状态
const response = await client . state . save (
serviceStoreName ,
[
{
key : "first-key-name" ,
value : "hello" ,
metadata : {
foo : "bar" ,
},
},
{
key : "second-key-name" ,
value : "world" ,
},
],
{
metadata : {
ttlInSeconds : "3" , // 这应该覆盖状态项中的 ttl
},
},
);
// 获取状态
const response = await client . state . get ( serviceStoreName , "first-key-name" );
// 获取批量状态
const response = await client . state . getBulk ( serviceStoreName , [ "first-key-name" , "second-key-name" ]);
// 状态事务
await client . state . transaction ( serviceStoreName , [
{
operation : "upsert" ,
request : {
key : "first-key-name" ,
value : "new-data" ,
},
},
{
operation : "delete" ,
request : {
key : "second-key-name" ,
},
},
]);
// 删除状态
const response = await client . state . delete ( serviceStoreName , "first-key-name" );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关状态操作的完整列表,请访问 如何:获取和保存状态 。
查询状态 API import { DaprClient } from "@dapr/dapr" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const res = await client . state . query ( "state-mongodb" , {
filter : {
OR : [
{
EQ : { "person.org" : "Dev Ops" },
},
{
AND : [
{
EQ : { "person.org" : "Finance" },
},
{
IN : { state : [ "CA" , "WA" ] },
},
],
},
],
},
sort : [
{
key : "state" ,
order : "DESC" ,
},
],
page : {
limit : 10 ,
},
});
console . log ( res );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
PubSub API 发布消息 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// 以 text/plain 格式发布消息到主题
// 注意,内容类型是从消息类型推断的,除非明确指定
const response = await client . pubsub . publish ( pubSubName , topic , "hello, world!" );
// 如果发布失败,响应包含错误
console . log ( response );
// 以 application/json 格式发布消息到主题
await client . pubsub . publish ( pubSubName , topic , { hello : "world" });
// 将 JSON 消息作为纯文本发布
const options = { contentType : "text/plain" };
await client . pubsub . publish ( pubSubName , topic , { hello : "world" }, options );
// 以 application/cloudevents+json 格式发布消息到主题
// 您还可以使用 cloudevent SDK 创建云事件 https://github.com/cloudevents/sdk-javascript
const cloudEvent = {
specversion : "1.0" ,
source : "/some/source" ,
type : "example" ,
id : "1234" ,
};
await client . pubsub . publish ( pubSubName , topic , cloudEvent );
// 将 cloudevent 作为原始负载发布
const options = { metadata : { rawPayload : true } };
await client . pubsub . publish ( pubSubName , topic , "hello, world!" , options );
// 以 text/plain 格式批量发布多个消息到主题
await client . pubsub . publishBulk ( pubSubName , topic , [ "message 1" , "message 2" , "message 3" ]);
// 以 application/json 格式批量发布多个消息到主题
await client . pubsub . publishBulk ( pubSubName , topic , [
{ hello : "message 1" },
{ hello : "message 2" },
{ hello : "message 3" },
]);
// 使用显式批量发布消息批量发布多个消息
const bulkPublishMessages = [
{
entryID : "entry-1" ,
contentType : "application/json" ,
event : { hello : "foo message 1" },
},
{
entryID : "entry-2" ,
contentType : "application/cloudevents+json" ,
event : { ... cloudEvent , data : "foo message 2" , datacontenttype : "text/plain" },
},
{
entryID : "entry-3" ,
contentType : "text/plain" ,
event : "foo message 3" ,
},
];
await client . pubsub . publishBulk ( pubSubName , topic , bulkPublishMessages );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
Bindings API 调用输出绑定 输出绑定
import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const bindingName = "my-binding-name" ;
const bindingOperation = "create" ;
const message = { hello : "world" };
const response = await client . binding . send ( bindingName , bindingOperation , message );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关输出绑定的完整指南,请访问 如何:使用绑定 。
Secret API 检索 secrets import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const secretStoreName = "my-secret-store" ;
const secretKey = "secret-key" ;
// 从 secret 存储中检索单个 secret
const response = await client . secret . get ( secretStoreName , secretKey );
// 从 secret 存储中检索所有 secrets
const response = await client . secret . getBulk ( secretStoreName );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关 secrets 的完整指南,请访问 如何:检索 secrets 。
Configuration API 获取配置键 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort : process.env.DAPR_GRPC_PORT ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const config = await client . configuration . get ( "config-store" , [ "key1" , "key2" ]);
console . log ( config );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
示例输出:
{
items: {
key1: { key: 'key1', value: 'foo', version: '', metadata: {} },
key2: { key: 'key2', value: 'bar2', version: '', metadata: {} }
}
}
订阅配置更新 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort : process.env.DAPR_GRPC_PORT ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
// 订阅配置存储更改的键 "key1" 和 "key2"
const stream = await client . configuration . subscribeWithKeys ( "config-store" , [ "key1" , "key2" ], async ( data ) => {
console . log ( "订阅接收到来自配置存储的更新:" , data );
});
// 等待 60 秒并取消订阅。
await new Promise (( resolve ) => setTimeout ( resolve , 60000 ));
stream . stop ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
示例输出:
订阅接收到来自配置存储的更新: {
items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } }
}
订阅接收到来自配置存储的更新: {
items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } }
}
Cryptography API JavaScript SDK 中的 gRPC 客户端仅支持 cryptography API。
import { createReadStream , createWriteStream } from "node:fs" ;
import { readFile , writeFile } from "node:fs/promises" ;
import { pipeline } from "node:stream/promises" ;
import { DaprClient , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50050" ; // 示例服务器的 Dapr sidecar 端口
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
// 使用流加密和解密消息
await encryptDecryptStream ( client );
// 从缓冲区加密和解密消息
await encryptDecryptBuffer ( client );
}
async function encryptDecryptStream ( client : DaprClient ) {
// 首先,加密消息
console . log ( "== 使用流加密消息" );
console . log ( "将 plaintext.txt 加密为 ciphertext.out" );
await pipeline (
createReadStream ( "plaintext.txt" ),
await client . crypto . encrypt ({
componentName : "crypto-local" ,
keyName : "symmetric256" ,
keyWrapAlgorithm : "A256KW" ,
}),
createWriteStream ( "ciphertext.out" ),
);
// 解密消息
console . log ( "== 使用流解密消息" );
console . log ( "将 ciphertext.out 解密为 plaintext.out" );
await pipeline (
createReadStream ( "ciphertext.out" ),
await client . crypto . decrypt ({
componentName : "crypto-local" ,
}),
createWriteStream ( "plaintext.out" ),
);
}
async function encryptDecryptBuffer ( client : DaprClient ) {
// 读取 "plaintext.txt" 以便我们有一些内容
const plaintext = await readFile ( "plaintext.txt" );
// 首先,加密消息
console . log ( "== 使用缓冲区加密消息" );
const ciphertext = await client . crypto . encrypt ( plaintext , {
componentName : "crypto-local" ,
keyName : "my-rsa-key" ,
keyWrapAlgorithm : "RSA" ,
});
await writeFile ( "test.out" , ciphertext );
// 解密消息
console . log ( "== 使用缓冲区解密消息" );
const decrypted = await client . crypto . decrypt ( ciphertext , {
componentName : "crypto-local" ,
});
// 内容应该相等
if ( plaintext . compare ( decrypted ) !== 0 ) {
throw new Error ( "解密的消息与原始消息不匹配" );
}
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关 cryptography 的完整指南,请访问 如何:Cryptography 。
分布式锁 API 尝试锁定和解锁 API import { CommunicationProtocolEnum , DaprClient } from "@dapr/dapr" ;
import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse" ;
const daprHost = "127.0.0.1" ;
const daprPortDefault = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const storeName = "redislock" ;
const resourceId = "resourceId" ;
const lockOwner = "owner1" ;
let expiryInSeconds = 1000 ;
console . log ( `在 ${ storeName } , ${ resourceId } 上以所有者: ${ lockOwner } 获取锁` );
const lockResponse = await client . lock . lock ( storeName , resourceId , lockOwner , expiryInSeconds );
console . log ( lockResponse );
console . log ( `在 ${ storeName } , ${ resourceId } 上以所有者: ${ lockOwner } 解锁` );
const unlockResponse = await client . lock . unlock ( storeName , resourceId , lockOwner );
console . log ( "解锁 API 响应:" + getResponseStatus ( unlockResponse . status ));
}
function getResponseStatus ( status : LockStatus ) {
switch ( status ) {
case LockStatus.Success :
return "成功" ;
case LockStatus.LockDoesNotExist :
return "锁不存在" ;
case LockStatus.LockBelongsToOthers :
return "锁属于他人" ;
default :
return "内部错误" ;
}
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关分布式锁的完整指南,请访问 如何:使用分布式锁 。
Workflow API Workflow 管理 import { DaprClient } from "@dapr/dapr" ;
async function start() {
const client = new DaprClient ();
// 启动一个新的 workflow 实例
const instanceId = await client . workflow . start ( "OrderProcessingWorkflow" , {
Name : "Paperclips" ,
TotalCost : 99.95 ,
Quantity : 4 ,
});
console . log ( `启动了 workflow 实例 ${ instanceId } ` );
// 获取一个 workflow 实例
const workflow = await client . workflow . get ( instanceId );
console . log (
`Workflow ${ workflow . workflowName } , 创建于 ${ workflow . createdAt . toUTCString () } , 状态为 ${
workflow . runtimeStatus
} ` ,
);
console . log ( `附加属性: ${ JSON . stringify ( workflow . properties ) } ` );
// 暂停一个 workflow 实例
await client . workflow . pause ( instanceId );
console . log ( `暂停了 workflow 实例 ${ instanceId } ` );
// 恢复一个 workflow 实例
await client . workflow . resume ( instanceId );
console . log ( `恢复了 workflow 实例 ${ instanceId } ` );
// 终止一个 workflow 实例
await client . workflow . terminate ( instanceId );
console . log ( `终止了 workflow 实例 ${ instanceId } ` );
// 清除一个 workflow 实例
await client . workflow . purge ( instanceId );
console . log ( `清除了 workflow 实例 ${ instanceId } ` );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
相关链接 4.2 - JavaScript 服务器 SDK 用于开发 Dapr 应用的 JavaScript 服务器 SDK
介绍 Dapr 服务器使您能够接收来自 Dapr sidecar 的通信,并访问其面向服务器的功能,例如:事件订阅、接收输入绑定等。
准备条件 安装和导入 Dapr 的 JS SDK 使用 npm
安装 SDK: 导入库: import { DaprServer , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
// HTTP 示例
const server = new DaprServer ({
serverHost ,
serverPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP , // DaprClient 使用与 DaprServer 相同的通信协议,除非另有说明
clientOptions : {
daprHost ,
daprPort ,
},
});
// GRPC 示例
const server = new DaprServer ({
serverHost ,
serverPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
clientOptions : {
daprHost ,
daprPort ,
},
});
运行 要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。
使用 HTTP(内置 express 网络服务器) import { DaprServer } from "@dapr/dapr" ;
const server = new DaprServer ({
serverHost : appHost ,
serverPort : appPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server . start ();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start
# 或者,使用 npm 脚本
npm run start:dapr-http
ℹ️ 注意: 这里需要 app-port
,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。
使用 HTTP(自带 express 网络服务器) 除了使用内置的网络服务器进行 Dapr sidecar 到应用程序的通信,您还可以自带实例。这在构建 REST API 后端并希望直接集成 Dapr 时非常有用。
注意,这目前仅适用于 express
。
💡 注意:使用自定义网络服务器时,SDK 将配置服务器属性,如最大主体大小,并向其添加新路由。这些路由是独特的,以避免与您的应用程序发生任何冲突,但不能保证不发生冲突。
import { DaprServer , CommunicationProtocolEnum } from "@dapr/dapr" ;
import express from "express" ;
const myApp = express ();
myApp . get ( "/my-custom-endpoint" , ( req , res ) => {
res . send ({ msg : "My own express app!" });
});
const daprServer = new DaprServer ({
serverHost : "127.0.0.1" , // 应用主机
serverPort : "50002" , // 应用端口
serverHttp : myApp ,
clientOptions : {
daprHost ,
daprPort
}
});
// 在服务器启动前初始化订阅,Dapr sidecar 使用它。
// 这也将初始化应用服务器本身(无需调用 `app.listen`)。
await daprServer . start ();
配置完上述内容后,您可以像往常一样调用您的自定义端点:
const res = await fetch ( `http://127.0.0.1:50002/my-custom-endpoint` );
const json = await res . json ();
使用 gRPC 由于 HTTP 是默认的,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外的参数来实现这一点。
import { DaprServer , CommunicationProtocol } from "@dapr/dapr" ;
const server = new DaprServer ({
serverHost : appHost ,
serverPort : appPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
clientOptions : {
daprHost ,
daprPort ,
},
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server . start ();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start
# 或者,使用 npm 脚本
npm run start:dapr-grpc
ℹ️ 注意: 这里需要 app-port
,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。
构建块 JavaScript 服务器 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是 sidecar 到应用程序的功能。
调用 API 监听调用 import { DaprServer , DaprInvokerCallbackContent } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const callbackFunction = ( data : DaprInvokerCallbackContent ) => {
console . log ( "Received body: " , data . body );
console . log ( "Received metadata: " , data . metadata );
console . log ( "Received query: " , data . query );
console . log ( "Received headers: " , data . headers ); // 仅在 HTTP 中可用
};
await server . invoker . listen ( "hello-world" , callbackFunction , { method : HttpMethod.GET });
// 您现在可以使用您的应用 ID 和方法 "hello-world" 调用服务
await server . start ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关服务调用的完整指南,请访问 如何:调用服务 。
PubSub API 订阅消息 可以通过多种方式订阅消息,以提供接收主题消息的灵活性:
通过 subscribe
方法直接订阅 通过 subscribeWithOptions
方法直接订阅并带有选项 通过 susbcribeOnEvent
方法之后订阅 每次事件到达时,我们将其主体作为 data
传递,并将头信息作为 headers
传递,其中可以包含事件发布者的属性(例如,来自 IoT Hub 的设备 ID)
Dapr 要求在启动时设置订阅,但在 JS SDK 中,我们允许之后添加事件处理程序,为您提供编程的灵活性。
下面提供了一个示例
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// 为主题配置订阅者
// 方法 1:通过 `subscribe` 方法直接订阅
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` ),
);
// 方法 2:通过 `subscribeWithOptions` 方法直接订阅并带有选项
await server . pubsub . subscribeWithOptions ( pubSubName , topic , {
callback : async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` ),
});
// 方法 3:通过 `susbcribeOnEvent` 方法之后订阅
// 注意:我们使用默认值,因为如果没有传递路由(空选项),我们将使用 "default" 作为路由名称
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-1" , {});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "default" , async ( data : any , headers : object ) => {
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` );
});
// 启动服务器
await server . start ();
}
有关状态操作的完整列表,请访问 如何:发布和订阅 。
使用 SUCCESS/RETRY/DROP 状态订阅 Dapr 支持 重试逻辑的状态码 ,以指定消息处理后应执行的操作。
⚠️ JS SDK 允许在同一主题上有多个回调,我们处理状态优先级为 RETRY
> DROP
> SUCCESS
,默认为 SUCCESS
⚠️ 确保在应用程序中 配置弹性 以处理 RETRY
消息
在 JS SDK 中,我们通过 DaprPubSubStatusEnum
枚举支持这些消息。为了确保 Dapr 将重试,我们还配置了一个弹性策略。
components/resiliency.yaml
apiVersion : dapr.io/v1alpha1
kind : Resiliency
metadata :
name : myresiliency
spec :
policies :
retries :
# 全局重试策略用于入站组件操作
DefaultComponentInboundRetryPolicy :
policy : constant
duration : 500ms
maxRetries : 10
targets :
components :
messagebus :
inbound :
retry : DefaultComponentInboundRetryPolicy
src/index.ts
import { DaprServer , DaprPubSubStatusEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// 成功处理消息
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . SUCCESS ;
});
// 重试消息
// 注意:此示例将继续重试传递消息
// 注意 2:每个组件可以有自己的重试配置
// 例如,https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-redis-pubsub/
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . RETRY ;
});
// 丢弃消息
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . DROP ;
});
// 启动服务器
await server . start ();
}
基于规则订阅消息 Dapr 支持路由消息 到不同的处理程序(路由)基于规则。
例如,您正在编写一个需要根据消息的 “type” 处理消息的应用程序,使用 Dapr,您可以将它们发送到不同的路由 handlerType1
和 handlerType2
,默认路由为 handlerDefault
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// 为主题配置订阅者并设置规则
// 注意:默认路由和匹配模式是可选的
await server . pubsub . subscribe ( "pubsub-redis" , "topic-1" , {
default : "/default" ,
rules : [
{
match : `event.type == "my-type-1"` ,
path : "/type-1" ,
},
{
match : `event.type == "my-type-2"` ,
path : "/type-2" ,
},
],
});
// 为每个路由添加处理程序
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "default" , async ( data ) => {
console . log ( `Handling Default` );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "type-1" , async ( data ) => {
console . log ( `Handling Type 1` );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "type-2" , async ( data ) => {
console . log ( `Handling Type 2` );
});
// 启动服务器
await server . start ();
}
使用通配符订阅 支持流行的通配符 *
和 +
(请确保验证 pubsub 组件是否支持 )并可以按如下方式订阅:
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
// * 通配符
await server . pubsub . subscribe ( pubSubName , "/events/*" , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } ` ),
);
// + 通配符
await server . pubsub . subscribe ( pubSubName , "/events/+/temperature" , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } ` ),
);
// 启动服务器
await server . start ();
}
批量订阅消息 支持批量订阅,并可通过以下 API 获得:
通过 subscribeBulk
方法进行批量订阅:maxMessagesCount
和 maxAwaitDurationMs
是可选的;如果未提供,将使用相关组件的默认值。 在监听消息时,应用程序以批量方式从 Dapr 接收消息。然而,与常规订阅一样,回调函数一次接收一条消息,用户可以选择返回 DaprPubSubStatusEnum
值以确认成功、重试或丢弃消息。默认行为是返回成功响应。
请参阅 此文档 以获取更多详细信息。
import { DaprServer } from "@dapr/dapr" ;
const pubSubName = "orderPubSub" ;
const topic = "topicbulk" ;
const daprHost = process . env . DAPR_HOST || "127.0.0.1" ;
const daprHttpPort = process . env . DAPR_HTTP_PORT || "3502" ;
const serverHost = process . env . SERVER_HOST || "127.0.0.1" ;
const serverPort = process . env . APP_PORT || 5001 ;
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort : daprHttpPort ,
},
});
// 使用默认配置向主题发布多条消息。
await client . pubsub . subscribeBulk ( pubSubName , topic , ( data ) =>
console . log ( "Subscriber received: " + JSON . stringify ( data )),
);
// 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
await client . pubsub . subscribeBulk (
pubSubName ,
topic ,
( data ) => {
console . log ( "Subscriber received: " + JSON . stringify ( data ));
return DaprPubSubStatusEnum . SUCCESS ; // 如果应用程序没有返回任何内容,默认是 SUCCESS。应用程序还可以根据传入的消息返回 RETRY 或 DROP。
},
{
maxMessagesCount : 100 ,
maxAwaitDurationMs : 40 ,
},
);
}
死信主题 Dapr 支持 死信主题 。这意味着当消息处理失败时,它会被发送到死信队列。例如,当消息在 /my-queue
上处理失败时,它将被发送到 /my-queue-failed
。
例如,当消息在 /my-queue
上处理失败时,它将被发送到 /my-queue-failed
。
您可以使用 subscribeWithOptions
方法的以下选项:
deadletterTopic
:指定死信主题名称(注意:如果未提供,我们将创建一个名为 deadletter
的主题)deadletterCallback
:作为死信处理程序触发的方法在 JS SDK 中实现死信支持可以通过以下方式:
作为选项传递 deadletterCallback
通过 subscribeToRoute
手动订阅路由 下面提供了一个示例
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr sidecar 主机
const daprPort = "3500" ; // Dapr sidecar 端口
const serverHost = "127.0.0.1" ; // 应用主机
const serverPort = "50051" ; // 应用端口
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
// 方法 1(通过 subscribeWithOptions 直接订阅)
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-5" , {
callback : async ( data : any ) => {
throw new Error ( "Triggering Deadletter" );
},
deadLetterCallback : async ( data : any ) => {
console . log ( "Handling Deadletter message" );
},
});
// 方法 2(之后订阅)
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-1" , {
deadletterTopic : "my-deadletter-topic" ,
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "default" , async () => {
throw new Error ( "Triggering Deadletter" );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "my-deadletter-topic" , async () => {
console . log ( "Handling Deadletter message" );
});
// 启动服务器
await server . start ();
}
Bindings API 接收输入绑定 import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const bindingName = "my-binding-name" ;
const response = await server . binding . receive ( bindingName , async ( data : any ) =>
console . log ( `Got Data: ${ JSON . stringify ( data ) } ` ),
);
await server . start ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关输出绑定的完整指南,请访问 如何:使用绑定 。
Configuration API 💡 配置 API 目前仅通过 gRPC 可用
获取配置值 import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const config = await client . configuration . get ( "config-redis" , [ "myconfigkey1" , "myconfigkey2" ]);
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
订阅键更改 import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const stream = await client . configuration . subscribeWithKeys ( "config-redis" , [ "myconfigkey1" , "myconfigkey2" ], () => {
// 收到键更新
});
// 当您准备好停止监听时,调用以下命令
await stream . close ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
相关链接 4.3 - JavaScript SDK for Actors 如何使用 Dapr JavaScript SDK 快速上手 actor
Dapr actors 包允许您通过 JavaScript 应用程序与 Dapr 虚拟 actor 交互。以下示例展示了如何使用 JavaScript SDK 与虚拟 actor 进行交互。
有关 Dapr actor 的详细介绍,请访问 actor 概述页面 。
前置条件 场景 以下代码示例大致描述了一个停车场车位监控系统的场景,可以在 Mark Russinovich 的这个视频 中看到。
一个停车场由数百个停车位组成,每个停车位都配有一个传感器,该传感器向集中监控系统提供更新。停车位传感器(即我们的 actor)用于检测停车位是否被占用或可用。
要运行此示例,请克隆源代码,源代码位于 JavaScript SDK 示例目录 中。
Actor 接口 actor 接口定义了 actor 实现和调用 actor 的客户端之间共享的契约。在下面的示例中,我们为停车场传感器创建了一个接口。每个传感器都有两个方法:carEnter
和 carLeave
,它们定义了停车位的状态:
export default interface ParkingSensorInterface {
carEnter () : Promise < void >;
carLeave () : Promise < void >;
}
Actor 实现 actor 实现通过扩展基类型 AbstractActor
并实现 actor 接口(在此示例中为 ParkingSensorInterface
)来定义一个类。
以下代码描述了一个 actor 实现以及一些辅助方法。
import { AbstractActor } from "@dapr/dapr" ;
import ParkingSensorInterface from "./ParkingSensorInterface" ;
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
async carEnter () : Promise < void > {
// 实现更新停车位被占用的状态。
}
async carLeave () : Promise < void > {
// 实现更新停车位可用的状态。
}
private async getInfo () : Promise < object > {
// 实现从停车位传感器请求更新。
}
/**
* @override
*/
async onActivate () : Promise < void > {
// 由 AbstractActor 调用的初始化逻辑。
}
}
配置 Actor 运行时 要配置 actor 运行时,请使用 DaprClientOptions
。各种参数及其默认值记录在 如何:在 Dapr 中使用虚拟 actor 中。
注意,超时和间隔应格式化为 time.ParseDuration 字符串,这是一种用于表示时间段的格式。
import { CommunicationProtocolEnum , DaprClient , DaprServer } from "@dapr/dapr" ;
// 使用 DaprClientOptions 配置 actor 运行时。
const clientOptions = {
daprHost : daprHost ,
daprPort : daprPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP ,
actor : {
actorIdleTimeout : "1h" ,
actorScanInterval : "30s" ,
drainOngoingCallTimeout : "1m" ,
drainRebalancedActors : true ,
reentrancy : {
enabled : true ,
maxStackDepth : 32 ,
},
remindersStoragePartitions : 0 ,
},
};
// 在创建 DaprServer 和 DaprClient 时使用这些选项。
// 注意,DaprServer 内部创建了一个 DaprClient,需要使用 clientOptions 进行配置。
const server = new DaprServer ({ serverHost , serverPort , clientOptions });
const client = new DaprClient ( clientOptions );
注册 Actor 使用 DaprServer
包初始化并注册您的 actor:
import { DaprServer } from "@dapr/dapr" ;
import ParkingSensorImpl from "./ParkingSensorImpl" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50000" ;
const serverHost = "127.0.0.1" ;
const serverPort = "50001" ;
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
await server . actor . init (); // 让服务器知道我们需要 actor
server . actor . registerActor ( ParkingSensorImpl ); // 注册 actor
await server . start (); // 启动服务器
// 要获取已注册的 actor,可以调用 `getRegisteredActors`:
const resRegisteredActors = await server . actor . getRegisteredActors ();
console . log ( `Registered Actors: ${ JSON . stringify ( resRegisteredActors ) } ` );
调用 Actor 方法 在注册 actor 之后,使用 ActorProxyBuilder
创建一个实现 ParkingSensorInterface
的代理对象。您可以通过直接调用代理对象上的方法来调用 actor 方法。在内部,它会转换为对 actor API 的网络调用并获取结果。
import { ActorId , DaprClient } from "@dapr/dapr" ;
import ParkingSensorImpl from "./ParkingSensorImpl" ;
import ParkingSensorInterface from "./ParkingSensorInterface" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50000" ;
const client = new DaprClient ({ daprHost , daprPort });
// 创建一个新的 actor 构建器。它可以用于创建多种类型的 actor。
const builder = new ActorProxyBuilder < ParkingSensorInterface >( ParkingSensorImpl , client );
// 创建一个新的 actor 实例。
const actor = builder . build ( new ActorId ( "my-actor" ));
// 或者,使用随机 ID
// const actor = builder.build(ActorId.createRandomId());
// 调用方法。
await actor . carEnter ();
使用 actor 的状态 import { AbstractActor } from "@dapr/dapr" ;
import ActorStateInterface from "./ActorStateInterface" ;
export default class ActorStateExample extends AbstractActor implements ActorStateInterface {
async setState ( key : string , value : any ) : Promise < void > {
await this . getStateManager (). setState ( key , value );
await this . getStateManager (). saveState ();
}
async removeState ( key : string ) : Promise < void > {
await this . getStateManager (). removeState ( key );
await this . getStateManager (). saveState ();
}
// 使用特定类型获取状态
async getState < T >( key : string ) : Promise < T | null > {
return await this . getStateManager < T >(). getState ( key );
}
// 不指定类型获取状态为 `any`
async getState ( key : string ) : Promise < any > {
return await this . getStateManager (). getState ( key );
}
}
Actor 定时器和提醒 JS SDK 支持 actor 通过注册定时器或提醒来在自身上安排周期性工作。定时器和提醒之间的主要区别在于,Dapr actor 运行时在停用后不保留有关定时器的任何信息,但使用 Dapr actor 状态提供程序持久化提醒信息。
这种区别允许用户在轻量级但无状态的定时器与更耗资源但有状态的提醒之间进行权衡。
定时器和提醒的调度接口是相同的。有关调度配置的更深入了解,请参阅 actor 定时器和提醒文档 。
Actor 定时器 // ...
const actor = builder . build ( new ActorId ( "my-actor" ));
// 注册一个定时器
await actor . registerActorTimer (
"timer-id" , // 定时器的唯一名称。
"cb-method" , // 定时器触发时要执行的回调方法。
Temporal . Duration . from ({ seconds : 2 }), // DueTime
Temporal . Duration . from ({ seconds : 1 }), // Period
Temporal . Duration . from ({ seconds : 1 }), // TTL
50 , // 要发送到定时器回调的状态。
);
// 删除定时器
await actor . unregisterActorTimer ( "timer-id" );
Actor 提醒 // ...
const actor = builder . build ( new ActorId ( "my-actor" ));
// 注册一个提醒,它有一个默认回调:`receiveReminder`
await actor . registerActorReminder (
"reminder-id" , // 提醒的唯一名称。
Temporal . Duration . from ({ seconds : 2 }), // DueTime
Temporal . Duration . from ({ seconds : 1 }), // Period
Temporal . Duration . from ({ seconds : 1 }), // TTL
100 , // 要发送到提醒回调的状态。
);
// 删除提醒
await actor . unregisterActorReminder ( "reminder-id" );
要处理回调,您需要在 actor 中重写默认的 receiveReminder
实现。例如,从我们原来的 actor 实现中:
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
// ...
/**
* @override
*/
async receiveReminder ( state : any ) : Promise < void > {
// 在这里处理
}
// ...
}
有关 actor 的完整指南,请访问 如何:在 Dapr 中使用虚拟 actor 。
4.4 - JavaScript SDK中的日志记录 配置JavaScript SDK中的日志记录
介绍 JavaScript SDK自带一个内置的Console
日志记录器。SDK会生成各种内部日志,帮助用户理解事件流程并排查问题。用户可以自定义日志的详细程度,并提供自己的日志记录器实现。
配置日志级别 日志记录有五个级别,按重要性从高到低排列 - error
、warn
、info
、verbose
和debug
。设置日志级别意味着日志记录器将记录所有该级别及更高重要性的日志。例如,设置为verbose
级别意味着SDK不会记录debug
级别的日志。默认的日志级别是info
。
Dapr Client import { CommunicationProtocolEnum , DaprClient , LogLevel } from "@dapr/dapr" ;
// 创建一个日志级别设置为verbose的客户端实例。
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum . HTTP ,
logger : { level : LogLevel . Verbose },
});
有关如何使用Client的更多详细信息,请参见JavaScript Client 。
DaprServer import { CommunicationProtocolEnum , DaprServer , LogLevel } from "@dapr/dapr" ;
// 创建一个日志级别设置为error的服务器实例。
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
logger : { level : LogLevel.Error },
},
});
有关如何使用Server的更多详细信息,请参见JavaScript Server 。
自定义LoggerService JavaScript SDK使用内置的Console
进行日志记录。要使用自定义日志记录器,如Winston或Pino,可以实现LoggerService
接口。
基于Winston的日志记录: 创建LoggerService
的新实现。
import { LoggerService } from "@dapr/dapr" ;
import * as winston from "winston" ;
export class WinstonLoggerService implements LoggerService {
private logger ;
constructor () {
this . logger = winston . createLogger ({
transports : [ new winston . transports . Console (), new winston . transports . File ({ filename : "combined.log" })],
});
}
error ( message : any , ... optionalParams : any []) : void {
this . logger . error ( message , ... optionalParams );
}
warn ( message : any , ... optionalParams : any []) : void {
this . logger . warn ( message , ... optionalParams );
}
info ( message : any , ... optionalParams : any []) : void {
this . logger . info ( message , ... optionalParams );
}
verbose ( message : any , ... optionalParams : any []) : void {
this . logger . verbose ( message , ... optionalParams );
}
debug ( message : any , ... optionalParams : any []) : void {
this . logger . debug ( message , ... optionalParams );
}
}
将新的实现传递给SDK。
import { CommunicationProtocolEnum , DaprClient , LogLevel } from "@dapr/dapr" ;
import { WinstonLoggerService } from "./WinstonLoggerService" ;
const winstonLoggerService = new WinstonLoggerService ();
// 创建一个日志级别设置为verbose且日志服务为winston的客户端实例。
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP ,
logger : { level : LogLevel.Verbose , service : winstonLoggerService },
});
4.5 - JavaScript 示例 通过一些示例来学习如何使用 Dapr JavaScript SDK!
快速开始 相关文章 想要分享您的文章?告诉我们! 我们会将其添加到下面的列表中。
4.6 - 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流 如何使用 Dapr JavaScript SDK 快速启动和运行工作流
我们将创建一个 Dapr 工作流并通过控制台调用它。在这个示例中,您将:
此示例在自托管模式 下运行,使用 dapr init
的默认配置。
先决条件 设置环境 克隆 JavaScript SDK 仓库并进入其中。
git clone https://github.com/dapr/js-sdk
cd js-sdk
从 JavaScript SDK 根目录,导航到 Dapr 工作流示例。
cd examples/workflow/authoring
运行以下命令以安装运行此工作流示例所需的 Dapr JavaScript SDK 依赖。
运行 activity-sequence.ts
activity-sequence
文件在 Dapr 工作流运行时中注册了一个工作流和一个活动。工作流是按顺序执行的一系列活动。我们使用 DaprWorkflowClient 来调度一个新的工作流实例并等待其完成。
const daprHost = "localhost" ;
const daprPort = "50001" ;
const workflowClient = new DaprWorkflowClient ({
daprHost ,
daprPort ,
});
const workflowRuntime = new WorkflowRuntime ({
daprHost ,
daprPort ,
});
const hello = async ( _ : WorkflowActivityContext , name : string ) => {
return `Hello ${ name } !` ;
};
const sequence : TWorkflow = async function * ( ctx : WorkflowContext ) : any {
const cities : string [] = [];
const result1 = yield ctx . callActivity ( hello , "Tokyo" );
cities . push ( result1 );
const result2 = yield ctx . callActivity ( hello , "Seattle" );
cities . push ( result2 );
const result3 = yield ctx . callActivity ( hello , "London" );
cities . push ( result3 );
return cities ;
};
workflowRuntime . registerWorkflow ( sequence ). registerActivity ( hello );
// 将 worker 启动包装在 try-catch 块中以处理启动期间的任何错误
try {
await workflowRuntime . start ();
console . log ( "工作流运行时启动成功" );
} catch ( error ) {
console . error ( "启动工作流运行时出错:" , error );
}
// 调度一个新的编排
try {
const id = await workflowClient . scheduleNewWorkflow ( sequence );
console . log ( `编排已调度,ID: ${ id } ` );
// 等待编排完成
const state = await workflowClient . waitForWorkflowCompletion ( id , undefined , 30 );
console . log ( `编排完成!结果: ${ state ? . serializedOutput } ` );
} catch ( error ) {
console . error ( "调度或等待编排时出错:" , error );
}
在上面的代码中:
workflowRuntime.registerWorkflow(sequence)
将 sequence
注册为 Dapr 工作流运行时中的一个工作流。await workflowRuntime.start();
构建并启动 Dapr 工作流运行时中的引擎。await workflowClient.scheduleNewWorkflow(sequence)
在 Dapr 工作流运行时中调度一个新的工作流实例。await workflowClient.waitForWorkflowCompletion(id, undefined, 30)
等待工作流实例完成。在终端中,执行以下命令以启动 activity-sequence.ts
:
npm run start:dapr:activity-sequence
预期输出
你已启动并运行!Dapr 和您的应用程序日志将出现在这里。
...
== APP == 编排已调度,ID:dc040bea-6436-4051-9166-c9294f9d2201
== APP == 等待 30 秒以完成实例 dc040bea-6436-4051-9166-c9294f9d2201...
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 0 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Tokyo!" (14 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 3 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Seattle!" (16 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 6 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello London!" (15 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 9 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 编排完成,状态为 COMPLETED
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' 完成,状态为 COMPLETED。 app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3
== APP == 实例 dc040bea-6436-4051-9166-c9294f9d2201 完成
== APP == 编排完成!结果:["Hello Tokyo!","Hello Seattle!","Hello London!"]
下一步 5 - Dapr PHP SDK 用于开发Dapr应用的PHP SDK包
Dapr提供了一个SDK,帮助开发PHP应用程序。通过它,您可以使用Dapr创建PHP客户端、服务器和虚拟actor。
设置 先决条件 可选先决条件 初始化您的项目 在您希望创建服务的目录中,运行composer init
并回答提示的问题。
使用composer require dapr/php-sdk
安装此SDK以及您可能需要的其他依赖项。
配置您的服务 创建一个config.php文件,并复制以下内容:
<? php
use Dapr\Actors\Generators\ProxyFactory ;
use Dapr\Middleware\Defaults\ { Response\ApplicationJson , Tracing };
use Psr\Log\LogLevel ;
use function DI\ { env , get };
return [
// 设置日志级别
'dapr.log.level' => LogLevel :: WARNING ,
// 在每个请求上生成一个新的代理 - 推荐用于开发
'dapr.actors.proxy.generation' => ProxyFactory :: GENERATED ,
// 在此处放置任何订阅
'dapr.subscriptions' => [],
// 如果此服务将托管任何actor,请在此处添加它们
'dapr.actors' => [],
// 配置Dapr在多长时间后认为actor空闲
'dapr.actors.idle_timeout' => null ,
// 配置Dapr检查空闲actor的频率
'dapr.actors.scan_interval' => null ,
// 配置Dapr在关闭期间等待actor完成的时间
'dapr.actors.drain_timeout' => null ,
// 配置Dapr是否应等待actor完成
'dapr.actors.drain_enabled' => null ,
// 您可以在此处更改Dapr的端口设置
'dapr.port' => env ( 'DAPR_HTTP_PORT' , '3500' ),
// 添加任何自定义序列化例程
'dapr.serializers.custom' => [],
// 添加任何自定义反序列化例程
'dapr.deserializers.custom' => [],
// 以下设置为默认中间件,按指定顺序处理
'dapr.http.middleware.request' => [ get ( Tracing :: class )],
'dapr.http.middleware.response' => [ get ( ApplicationJson :: class ), get ( Tracing :: class )],
];
创建您的服务 创建index.php
并放入以下内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
use Dapr\App ;
$app = App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( __DIR__ . '/config.php' ));
$app -> get ( '/hello/{name}' , function ( string $name ) {
return [ 'hello' => $name ];
});
$app -> start ();
试用 使用dapr init
初始化Dapr,然后使用dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000
启动项目。
您现在可以打开一个网页浏览器并访问http://localhost:3000/hello/world ,将world
替换为您的名字、宠物的名字或您想要的任何内容。
恭喜,您已经创建了您的第一个Dapr服务!期待看到您会用它做些什么!
更多信息 5.1 - 虚拟Actor 如何构建actor
如果你对actor模式不熟悉,学习actor模式的最佳地方是Actor概述 。
在PHP SDK中,actor分为客户端和actor(也称为运行时)两部分。作为actor的客户端,你需要通过ActorProxy
类与远程actor进行交互。此类通过几种配置策略之一动态生成代理类。
编写actor时,系统可以为你管理状态。你可以接入actor的生命周期,并定义提醒和定时器。这为你处理适合actor模式的各种问题提供了强大的能力。
Actor代理 每当你想与actor通信时,你需要获取一个代理对象来进行通信。代理负责序列化你的请求,反序列化响应,并将其返回给你,同时遵循指定接口定义的契约。
为了创建代理,你首先需要一个接口来定义如何与actor发送和接收内容。例如,如果你想与一个仅跟踪计数的计数actor通信,你可以定义如下接口:
<? php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
function increment ( int $amount = 1 ) : void ;
function get_count () : int ;
}
将此接口放在actor和客户端都可以访问的共享库中是个好主意(如果两者都是用PHP编写的)。DaprType
属性告诉DaprClient要发送到的actor的名称。它应与实现的DaprType
匹配,尽管你可以根据需要覆盖类型。
<? php
$app -> run ( function ( \Dapr\Actors\ActorProxy $actorProxy ) {
$actor = $actorProxy -> get ( ICount :: class , 'actor-id' );
$actor -> increment ( 10 );
});
编写Actor 要创建actor,你需要实现之前定义的接口,并添加DaprType
属性。所有actor必须 实现IActor
,然而有一个Actor
基类实现了样板代码,使你的实现更简单。
这是计数器actor:
<? php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
function __construct ( string $id , private CountState $state ) {
parent :: __construct ( $id );
}
function increment ( int $amount = 1 ) : void {
$this -> state -> count += $amount ;
}
function get_count () : int {
return $this -> state -> count ;
}
}
构造函数是最重要的部分。它至少需要一个名为id
的参数,即actor的id。任何额外的参数都由DI容器注入,包括你想使用的任何ActorState
。
Actor生命周期 actor通过构造函数在每个针对该actor类型的请求中实例化。你可以使用它来计算临时状态或处理你需要的任何请求特定的启动,例如设置其他客户端或连接。
actor实例化后,可能会调用on_activation()
方法。on_activation()
方法在actor“唤醒”时或首次创建时调用。它不会在每个请求上调用。
接下来,调用actor方法。这可能来自定时器、提醒或客户端。你可以执行任何需要完成的工作和/或抛出异常。
最后,工作的结果返回给调用者。经过一段时间(取决于服务的配置方式),actor将被停用,并调用on_deactivation()
方法。如果主机崩溃、daprd崩溃或发生其他错误导致无法成功调用,则可能不会调用此方法。
Actor State actor状态是一个扩展ActorState
的“普通旧PHP对象”(POPO)。ActorState
基类提供了一些有用的方法。以下是一个示例实现:
<? php
class CountState extends \Dapr\Actors\ActorState {
public int $count = 0 ;
}
注册Actor Dapr期望在启动时知道服务可能托管的actor。你需要将其添加到配置中:
如果你想利用预编译的依赖注入,你需要使用工厂:
<? php
// 在config.php中
return [
'dapr.actors' => fn () => [ Counter :: class ],
];
启动应用所需的全部内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create (
configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ) -> enableCompilation ( __DIR__ )
);
$app -> start ();
<? php
// 在config.php中
return [
'dapr.actors' => [ Counter :: class ]
];
启动应用所需的全部内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
$app -> start ();
5.1.1 - 生产参考:actor 在生产环境中执行PHP角色
代理模式 actor代理有四种模式可供选择。每种模式都有不同的优缺点,您需要在开发和生产中进行权衡。
<? php
\Dapr\Actors\Generators\ProxyFactory :: GENERATED ;
\Dapr\Actors\Generators\ProxyFactory :: GENERATED_CACHED ;
\Dapr\Actors\Generators\ProxyFactory :: ONLY_EXISTING ;
\Dapr\Actors\Generators\ProxyFactory :: DYNAMIC ;
可以通过dapr.actors.proxy.generation
配置键进行设置。
GENERATED
GENERATED_CACHED
ONLY_EXISTING
DYNAMIC 这是默认模式。在此模式下,每个请求都会生成一个类并通过eval
执行。主要用于开发环境,不建议在生产中使用。
这与ProxyModes::GENERATED
相同,但类会存储在一个临时文件中,因此不需要在每个请求时重新生成。由于无法判断何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时可以使用。
在此模式下,如果代理类不存在,则会抛出异常。这对于不希望在生产中生成代码的情况很有用。您必须确保类已生成并预加载/自动加载。
生成代理 您可以创建一个composer脚本以按需生成代理,以利用ONLY_EXISTING
模式。
创建一个ProxyCompiler.php
<? php
class ProxyCompiler {
private const PROXIES = [
MyActorInterface :: class ,
MyOtherActorInterface :: class ,
];
private const PROXY_LOCATION = __DIR__ . '/proxies/' ;
public static function compile () {
try {
$app = \Dapr\App :: create ();
foreach ( self :: PROXIES as $interface ) {
$output = $app -> run ( function ( \DI\FactoryInterface $factory ) use ( $interface ) {
return \Dapr\Actors\Generators\FileGenerator :: generate ( $interface , $factory );
});
$reflection = new ReflectionClass ( $interface );
$dapr_type = $reflection -> getAttributes ( \Dapr\Actors\Attributes\DaprType :: class )[ 0 ] -> newInstance () -> type ;
$filename = 'dapr_proxy_' . $dapr_type . '.php' ;
file_put_contents ( self :: PROXY_LOCATION . $filename , $output );
echo "Compiled: $interface " ;
}
} catch ( Exception $ex ) {
echo "Failed to generate proxy for $interface\n{ $ex -> getMessage () } on line { $ex -> getLine () } in { $ex -> getFile () } \n " ;
}
}
}
然后在composer.json
中为生成的代理添加一个psr-4自动加载器和一个脚本:
{
"autoload" : {
"psr-4" : {
"Dapr\\Proxies\\" : "path/to/proxies"
}
},
"scripts" : {
"compile-proxies" : "ProxyCompiler::compile"
}
}
最后,配置dapr仅使用生成的代理:
<? php
// 在config.php中
return [
'dapr.actors.proxy.generation' => ProxyFactory :: ONLY_EXISTING ,
];
在此模式下,代理满足接口契约,但实际上并不实现接口本身(意味着instanceof
将为false
)。此模式利用PHP中的一些特性,适用于无法eval
或生成代码的情况。
请求 创建actor代理在任何模式下都是非常高效的。在创建actor代理对象时没有请求。
当您调用代理对象上的方法时,只有您实现的方法由您的actor实现服务。get_id()
在本地处理,而get_reminder()
、delete_reminder()
等由daprd
处理。
actor实现 每个PHP中的actor实现都必须实现\Dapr\Actors\IActor
并使用\Dapr\Actors\ActorTrait
特性。这允许快速反射和一些快捷方式。使用\Dapr\Actors\Actor
抽象基类可以为您做到这一点,但如果您需要覆盖默认行为,可以通过实现接口和使用特性来实现。
激活和停用 当actor激活时,会将一个令牌文件写入临时目录(默认情况下在Linux中为'/tmp/dapr_' + sha256(concat(Dapr type, id))
,在Windows中为'%temp%/dapr_' + sha256(concat(Dapr type, id))
)。这会一直保留到actor停用或主机关闭。这允许在Dapr在主机上激活actor时仅调用一次on_activation
。
性能 在使用php-fpm
和nginx
或Windows上的IIS的生产环境中,actor方法调用非常快。即使actor在每个请求中构建,actor状态键仅在需要时加载,而不是在每个请求中加载。然而,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这一问题,以速度换取一些可用性。建议不要从一开始就这样做,而是在需要时作为优化。
状态版本控制 ActorState
对象中的变量名称直接对应于存储中的键名。这意味着如果您更改变量的类型或名称,可能会遇到错误。为了解决这个问题,您可能需要对状态对象进行版本控制。为此,您需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能是这样的:
<? php
class VersionedState extends \Dapr\Actors\ActorState {
/**
* @var int 存储中状态的当前版本。我们给出当前版本的默认值。
* 然而,它可能在存储中有不同的值。
*/
public int $state_version = self :: VERSION ;
/**
* @var int 数据的当前版本
*/
private const VERSION = 3 ;
/**
* 当您的actor激活时调用。
*/
public function upgrade () {
if ( $this -> state_version < self :: VERSION ) {
$value = parent :: __get ( $this -> get_versioned_key ( 'key' , $this -> state_version ));
// 在更新数据结构后更新值
parent :: __set ( $this -> get_versioned_key ( 'key' , self :: VERSION ), $value );
$this -> state_version = self :: VERSION ;
$this -> save_state ();
}
}
// 如果您在上面的方法中根据需要升级所有键,则在加载/保存时不需要遍历以前的键,
// 您可以直接获取键的当前版本。
private function get_previous_version ( int $version ) : int {
return $this -> has_previous_version ( $version ) ? $version - 1 : $version ;
}
private function has_previous_version ( int $version ) : bool {
return $version >= 0 ;
}
private function walk_versions ( int $version , callable $callback , callable $predicate ) : mixed {
$value = $callback ( $version );
if ( $predicate ( $value ) || ! $this -> has_previous_version ( $version )) {
return $value ;
}
return $this -> walk_versions ( $this -> get_previous_version ( $version ), $callback , $predicate );
}
private function get_versioned_key ( string $key , int $version ) {
return $this -> has_previous_version ( $version ) ? $version . $key : $key ;
}
public function __get ( string $key ) : mixed {
return $this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __get ( $this -> get_versioned_key ( $key , $version )),
fn ( $value ) => isset ( $value )
);
}
public function __isset ( string $key ) : bool {
return $this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __isset ( $this -> get_versioned_key ( $key , $version )),
fn ( $isset ) => $isset
);
}
public function __set ( string $key , mixed $value ) : void {
// 可选:您可以取消设置键的以前版本
parent :: __set ( $this -> get_versioned_key ( $key , self :: VERSION ), $value );
}
public function __unset ( string $key ) : void {
// 取消设置此版本和所有以前版本
$this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __unset ( $this -> get_versioned_key ( $key , $version )),
fn () => false
);
}
}
有很多可以优化的地方,在生产中直接使用这个不是一个好主意,但您可以了解它的工作原理。很多将取决于您的用例,这就是为什么在SDK中没有这样的东西。例如,在这个示例实现中,保留了以前的值,以防在升级期间可能出现错误;保留以前的值允许再次运行升级,但您可能希望删除以前的值。
5.2 - 使用 PHP 实现发布和订阅 如何使用
通过 Dapr,您可以发布各种类型的内容,包括云事件。SDK 提供了一个简单的云事件实现,您也可以传递符合云事件规范的数组或使用其他库。
<? php
$app -> post ( '/publish' , function ( \Dapr\Client\DaprClient $daprClient ) {
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : [ 'something' => 'happened' ]);
});
有关发布/订阅的更多信息,请查看操作指南 。
数据的内容类型 PHP SDK 允许您在构建自定义云事件或发布原始数据时设置数据的内容类型。
<? php
$event = new \Dapr\PubSub\CloudEvent ();
$event -> data = $xml ;
$event -> data_content_type = 'application/xml' ;
<? php
/**
* @var \Dapr\Client\DaprClient $daprClient
*/
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : $raw_data , contentType : 'application/octet-stream' );
二进制数据 对于二进制数据,仅支持 <code>application/octet-stream</code>。
接收云事件 在您的订阅处理程序中,您可以让 DI 容器将 Dapr\PubSub\CloudEvent
或 array
注入到您的控制器中。使用 Dapr\PubSub\CloudEvent
时,会进行一些验证以确保事件的正确性。如果您需要直接访问数据,或者事件不符合规范,请使用 array
。
5.3 - 应用程序 使用 App 类
PHP 中没有默认的路由器。因此,提供了 \Dapr\App
类。它底层使用了 Nikic 的 FastRoute 。然而,你可以选择任何你喜欢的路由器或框架。只需查看 App
类中的 add_dapr_routes()
方法,了解 actor 和订阅是如何实现的。
每个应用程序都应该以 App::create()
开始,它接受两个参数,第一个是现有的 DI 容器(如果有的话),第二个是一个回调,用于挂钩到 ContainerBuilder
并添加你自己的配置。
接下来,你应该定义你的路由,然后调用 $app->start()
来执行当前请求的路由。
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个控制器用于 GET /test/{id},返回 id
$app -> get ( '/test/{id}' , fn ( string $id ) => $id );
$app -> start ();
从控制器返回 你可以从控制器返回任何内容,它将被序列化为一个 JSON 对象。你也可以请求 Psr Response 对象并返回它,这样你就可以自定义头信息,并控制整个响应:
<? php
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个控制器用于 GET /test/{id},返回 id
$app -> get ( '/test/{id}' ,
fn (
string $id ,
\Psr\Http\Message\ResponseInterface $response ,
\Nyholm\Psr7\Factory\Psr17Factory $factory ) => $response -> withBody ( $factory -> createStream ( $id )));
$app -> start ();
将应用程序用作客户端 当你只想将 Dapr 用作客户端时,比如在现有代码中,你可以调用 $app->run()
。在这些情况下,通常不需要自定义配置,不过,在生产环境中你可能希望使用编译的 DI 容器:
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> enableCompilation ( __DIR__ ));
$result = $app -> run ( fn ( \Dapr\DaprClient $client ) => $client -> get ( '/invoke/other-app/method/my-method' ));
在其他框架中使用 提供了一个 DaprClient
对象,实际上,App
对象使用的所有语法糖都是基于 DaprClient
构建的。
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$clientBuilder = \Dapr\Client\DaprClient :: clientBuilder ();
// 你可以自定义(反)序列化,或者注释掉以使用默认的 JSON 序列化器。
$clientBuilder = $clientBuilder -> withSerializationConfig ( $yourSerializer ) -> withDeserializationConfig ( $yourDeserializer );
// 你也可以传递一个日志记录器
$clientBuilder = $clientBuilder -> withLogger ( $myLogger );
// 并更改 sidecar 的 URL,例如,使用 https
$clientBuilder = $clientBuilder -> useHttpClient ( 'https://localhost:3800' )
在调用之前有几个函数可以使用
5.3.1 - 单元测试 单元测试
在 PHP SDK 中,单元测试和集成测试是非常重要的组成部分。通过使用依赖注入容器、模拟、存根以及提供的 \Dapr\Mocks\TestClient
,可以实现非常精细的测试。
测试 Actor 在测试 Actor 时,我们主要关注两个方面:
基于初始状态的返回结果 基于初始状态的结果状态 以下是一个简单的 Actor 测试示例,该 Actor 会更新其状态并返回特定值:
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
private \DI\Container $container ;
public function setUp () : void
{
parent :: setUp ();
// 创建一个默认应用并从中获取 DI 容器
$app = \Dapr\App :: create (
configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions (
[ 'dapr.actors' => [ TestActor :: class ]],
[ \Dapr\DaprClient :: class => \DI\autowire ( \Dapr\Mocks\TestClient :: class )]
));
$app -> run ( fn ( \DI\Container $container ) => $this -> container = $container );
}
public function testIncrementsWhenOdd ()
{
$id = uniqid ();
$runtime = $this -> container -> get ( \Dapr\Actors\ActorRuntime :: class );
$client = $this -> getClient ();
// 模拟从 http://localhost:1313/reference/api/actors_api/ 获取当前状态
$client -> register_get ( "/actors/TestActor/ $id /state/number" , code : 200 , data : 3 );
// 模拟从 http://localhost:1313/reference/api/actors_api/ 进行状态递增
$client -> register_post (
"/actors/TestActor/ $id /state" ,
code : 204 ,
response_data : null ,
expected_request : [
[
'operation' => 'upsert' ,
'request' => [
'key' => 'number' ,
'value' => 4 ,
],
],
]
);
$result = $runtime -> resolve_actor (
'TestActor' ,
$id ,
fn ( $actor ) => $runtime -> do_method ( $actor , 'oddIncrement' , null )
);
$this -> assertTrue ( $result );
}
private function getClient () : \Dapr\Mocks\TestClient
{
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
}
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
public function testNotIncrementsWhenEven () {
$container = new \DI\Container ();
$state = new TestState ( $container , $container );
$state -> number = 4 ;
$id = uniqid ();
$actor = new TestActor ( $id , $state );
$this -> assertFalse ( $actor -> oddIncrement ());
$this -> assertSame ( 4 , $state -> number );
}
}
测试事务 在构建事务时,您可能需要测试如何处理失败的事务。为此,您需要注入故障并确保事务按预期进行。
<? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
private \DI\Container $container ;
public function setUp () : void
{
parent :: setUp ();
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder )
=> $builder -> addDefinitions ([ \Dapr\DaprClient :: class => \DI\autowire ( \Dapr\Mocks\TestClient :: class )]));
$this -> container = $app -> run ( fn ( \DI\Container $container ) => $container );
}
private function getClient () : \Dapr\Mocks\TestClient {
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
public function testTransactionFailure () {
$client = $this -> getClient ();
// 模拟从 https://v1-16.docs.dapr.io/zh-hans/reference/api/state_api/ 创建响应
$client -> register_post ( '/state/statestore/bulk' , code : 200 , response_data : [
[
'key' => 'value' ,
// 没有先前的值
],
], expected_request : [
'keys' => [ 'value' ],
'parallelism' => 10
]);
$client -> register_post ( '/state/statestore/transaction' ,
code : 200 ,
response_data : null ,
expected_request : [
'operations' => [
[
'operation' => 'upsert' ,
'request' => [
'key' => 'value' ,
'value' => 'hello world'
]
]
]
]
);
$state = new MyState ( $this -> container , $this -> container );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
<? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
public function testTransactionFailure () {
$state = $this -> createStub ( MyState :: class );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
5.4 - 使用 PHP 进行状态管理 如何使用
Dapr 提供了一种模块化的状态管理方法,适用于您的应用程序。要学习基础知识,请访问
如何操作 。
元数据 许多状态组件允许您传递元数据给组件,以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递这些元数据:
<? php
// 使用状态管理器
$app -> run (
fn ( \Dapr\State\StateManager $stateManager ) =>
$stateManager -> save_state ( 'statestore' , new \Dapr\State\StateItem ( 'key' , 'value' , metadata : [ 'port' => '112' ])));
// 使用 DaprClient
$app -> run ( fn ( \Dapr\Client\DaprClient $daprClient ) => $daprClient -> saveState ( storeName : 'statestore' , key : 'key' , value : 'value' , metadata : [ 'port' => '112' ]))
这是一个将端口元数据传递给 Cassandra 的示例。
每个状态操作都允许传递元数据。
一致性与并发性 在 PHP SDK 中,有四个类代表 Dapr 中的四种不同类型的一致性和并发性:
<? php
[
\Dapr\consistency\StrongLastWrite :: class ,
\Dapr\consistency\StrongFirstWrite :: class ,
\Dapr\consistency\EventualLastWrite :: class ,
\Dapr\consistency\EventualFirstWrite :: class ,
]
将其中一个传递给 StateManager
方法或使用 StateStore()
属性可以让您定义状态存储应如何处理冲突。
并行性 进行批量读取或开始事务时,您可以指定并行度。如果必须一次读取一个键,Dapr 将从底层存储中“最多”读取这么多键。这有助于在性能的代价下控制状态存储的负载。默认值是 10
。
前缀 硬编码的键名很有用,但让状态对象更具可重用性会更好。在提交事务或将对象保存到状态时,您可以传递一个前缀,该前缀应用于对象中的每个键。
<? php
class TransactionObject extends \Dapr\State\TransactionalState {
public string $key ;
}
$app -> run ( function ( TransactionObject $object ) {
$object -> begin ( prefix : 'my-prefix-' );
$object -> key = 'value' ;
// 提交到键 `my-prefix-key`
$object -> commit ();
});
<? php
class StateObject {
public string $key ;
}
$app -> run ( function ( \Dapr\State\StateManager $stateManager ) {
$stateManager -> load_object ( $obj = new StateObject (), prefix : 'my-prefix-' );
// 原始值来自 `my-prefix-key`
$obj -> key = 'value' ;
// 保存到 `my-prefix-key`
$stateManager -> save_object ( $obj , prefix : 'my-prefix-' );
});
5.5 - 自定义序列化 如何配置序列化
Dapr 使用 JSON 进行序列化,因此在发送或接收数据时,复杂类型的信息可能会丢失。
序列化 当从控制器返回对象、将对象传递给 DaprClient
或将对象存储在状态存储中时,只有公共属性会被扫描和序列化。您可以通过实现 \Dapr\Serialization\ISerialize
接口来自定义此行为。例如,如果您想创建一个序列化为字符串的 ID 类型,可以这样实现:
<? php
class MyId implements \Dapr\Serialization\Serializers\ISerialize
{
public string $id ;
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// $value === $this
return $this -> id ;
}
}
这种方法适用于我们完全控制的类型,但不适用于库或 PHP 自带的类。对于这些情况,您需要在依赖注入容器中注册一个自定义序列化器:
<? php
// 在 config.php 中
class SerializeSomeClass implements \Dapr\Serialization\Serializers\ISerialize
{
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// 序列化 $value 并返回结果
}
}
return [
'dapr.serializers.custom' => [ SomeClass :: class => new SerializeSomeClass ()],
];
反序列化 反序列化的过程与序列化类似,只是使用的接口是 \Dapr\Deserialization\Deserializers\IDeserialize
。
6 - Dapr Python SDK 用于开发Dapr应用的Python SDK包
Dapr 提供了多种子包以帮助开发 Python 应用程序。通过这些子包,您可以使用 Dapr 创建 Python 客户端、服务器和虚拟 actor。
先决条件 安装 要开始使用 Python SDK,请安装主要的 Dapr Python SDK 包。
注意: 开发包包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 dapr-dev 包之前,请确保卸载任何稳定版本的 Python SDK。
可用子包 SDK 导入 Python SDK 导入是随主 SDK 安装一起包含的子包,但在使用时需要导入。Dapr Python SDK 提供的常用导入包括:
Client 编写 Python 应用以与 Dapr sidecar 和其他 Dapr 应用交互,包括 Python 中的有状态虚拟 actor。
Actors 创建和与 Dapr 的 actor 框架交互。
了解 所有可用的 Dapr Python SDK 导入 的更多信息。
SDK 扩展 SDK 扩展主要用于接收 pub/sub 事件、程序化创建 pub/sub 订阅和处理输入绑定事件。虽然这些任务可以在没有扩展的情况下完成,但使用 Python SDK 扩展会更加方便。
gRPC 使用 gRPC 服务器扩展创建 Dapr 服务。
FastAPI 使用 Dapr FastAPI 扩展与 Dapr Python 虚拟 actor 和 pub/sub 集成。
Flask 使用 Dapr Flask 扩展与 Dapr Python 虚拟 actor 集成。
Workflow 编写与其他 Dapr API 一起工作的 Python 工作流。
了解 Dapr Python SDK 扩展 的更多信息。
试用 克隆 Python SDK 仓库。
git clone https://github.com/dapr/python-sdk.git
通过 Python 快速入门、教程和示例来体验 Dapr 的实际应用:
SDK 示例 描述 快速入门 使用 Python SDK 在几分钟内体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始。 绑定教程 查看 Dapr Python SDK 如何与其他 Dapr SDK 一起工作以启用绑定。 分布式计算器教程 使用 Dapr Python SDK 处理方法调用和状态持久化功能。 Hello World 教程 学习如何在本地机器上使用 Python SDK 启动并运行 Dapr。 Hello Kubernetes 教程 在 Kubernetes 集群中使用 Dapr Python SDK 启动并运行。 可观测性教程 使用 Python SDK 探索 Dapr 的指标收集、跟踪、日志记录和健康检查功能。 Pub/sub 教程 查看 Dapr Python SDK 如何与其他 Dapr SDK 一起工作以启用 pub/sub 应用。
更多信息 Serialization 了解有关 Dapr SDK 中的序列化的更多信息。
6.1 - 使用 Dapr 客户端 Python SDK 入门 如何使用 Dapr Python SDK 快速上手
Dapr 客户端包使您能够从 Python 应用程序与其他 Dapr 应用程序进行交互。
注意 如果您还没有尝试过,
请尝试其中一个快速入门 ,以快速了解如何使用 Dapr Python SDK 和 API 构建块。
准备工作 在开始之前,安装 Dapr Python 包 。
导入客户端包 dapr
包包含 DaprClient
,用于创建和使用客户端。
from dapr.clients import DaprClient
初始化客户端 您可以通过多种方式初始化 Dapr 客户端:
默认值: 如果不提供参数初始化客户端,它将使用 Dapr sidecar 实例的默认值 (127.0.0.1:50001
)。
from dapr.clients import DaprClient
with DaprClient () as d :
# 使用客户端
在初始化时指定端点: 在构造函数中传递参数时,gRPC 端点优先于任何配置或环境变量。
from dapr.clients import DaprClient
with DaprClient ( "mydomain:50051?tls=true" ) as d :
# 使用客户端
配置选项: Dapr Sidecar 端点 您可以使用标准化的 DAPR_GRPC_ENDPOINT
环境变量来指定 gRPC 端点。当设置了此变量时,可以在没有任何参数的情况下初始化客户端:
export DAPR_GRPC_ENDPOINT = "mydomain:50051?tls=true"
from dapr.clients import DaprClient
with DaprClient () as d :
# 客户端将使用环境变量中指定的端点
旧的环境变量 DAPR_RUNTIME_HOST
、DAPR_HTTP_PORT
和 DAPR_GRPC_PORT
也被支持,但 DAPR_GRPC_ENDPOINT
优先。
Dapr API 令牌 如果您的 Dapr 实例配置为需要 DAPR_API_TOKEN
环境变量,您可以在环境中设置它,客户端将自动使用它。 您可以在这里 阅读更多关于 Dapr API 令牌认证的信息。
健康检查超时 客户端初始化时,会对 Dapr sidecar (/healthz/outbound
) 进行健康检查。客户端将在 sidecar 启动并运行后继续。
默认的健康检查超时时间为 60 秒,但可以通过设置 DAPR_HEALTH_TIMEOUT
环境变量来覆盖。
重试和超时 如果从 sidecar 收到特定错误代码,Dapr 客户端可以重试请求。这可以通过 DAPR_API_MAX_RETRIES
环境变量进行配置,并自动获取,不需要任何代码更改。
DAPR_API_MAX_RETRIES
的默认值为 0
,这意味着不会进行重试。
您可以通过创建 dapr.clients.retry.RetryPolicy
对象并将其传递给 DaprClient 构造函数来微调更多重试参数:
from dapr.clients.retry import RetryPolicy
retry = RetryPolicy (
max_attempts = 5 ,
initial_backoff = 1 ,
max_backoff = 20 ,
backoff_multiplier = 1.5 ,
retryable_http_status_codes = [ 408 , 429 , 500 , 502 , 503 , 504 ],
retryable_grpc_status_codes = [ StatusCode . UNAVAILABLE , StatusCode . DEADLINE_EXCEEDED , ]
)
with DaprClient ( retry_policy = retry ) as d :
...
或对于 actor:
factory = ActorProxyFactory ( retry_policy = RetryPolicy ( max_attempts = 3 ))
proxy = ActorProxy . create ( 'DemoActor' , ActorId ( '1' ), DemoActorInterface , factory )
超时 可以通过环境变量 DAPR_API_TIMEOUT_SECONDS
为所有调用设置。默认值为 60 秒。
注意:您可以通过将 timeout
参数传递给 invoke_method
方法来单独控制服务调用的超时。
错误处理 最初,Dapr 中的错误遵循 标准 gRPC 错误模型 。然而,为了提供更详细和信息丰富的错误消息,在版本 1.13 中引入了一个增强的错误模型,与 gRPC 更丰富的错误模型 对齐。作为回应,Python SDK 实现了 DaprGrpcError
,一个旨在改善开发者体验的自定义异常类。 需要注意的是,过渡到使用 DaprGrpcError
处理所有 gRPC 状态异常仍在进行中。目前,SDK 中的每个 API 调用尚未更新以利用此自定义异常。我们正在积极进行此增强,并欢迎社区的贡献。
使用 Dapr python-SDK 处理 DaprGrpcError
异常的示例:
try :
d . save_state ( store_name = storeName , key = key , value = value )
except DaprGrpcError as err :
print ( f '状态代码: { err . code () } ' )
print ( f "消息: { err . message () } " )
print ( f "错误代码: { err . error_code () } " )
print ( f "错误信息(原因): { err . error_info . reason } " )
print ( f "资源信息 (资源类型): { err . resource_info . resource_type } " )
print ( f "资源信息 (资源名称): { err . resource_info . resource_name } " )
print ( f "错误请求 (字段): { err . bad_request . field_violations [ 0 ] . field } " )
print ( f "错误请求 (描述): { err . bad_request . field_violations [ 0 ] . description } " )
构建块 Python SDK 允许您与所有 Dapr 构建块 进行接口交互。
调用服务 Dapr Python SDK 提供了一个简单的 API,用于通过 HTTP 或 gRPC(已弃用)调用服务。可以通过设置 DAPR_API_METHOD_INVOCATION_PROTOCOL
环境变量来选择协议,默认情况下未设置时为 HTTP。Dapr 中的 GRPC 服务调用已弃用,建议使用 GRPC 代理作为替代。
from dapr.clients import DaprClient
with DaprClient () as d :
# 调用方法 (gRPC 或 HTTP GET)
resp = d . invoke_method ( 'service-to-invoke' , 'method-to-invoke' , data = '{"message":"Hello World"}' )
# 对于其他 HTTP 动词,必须指定动词
# 调用 'POST' 方法 (仅限 HTTP)
resp = d . invoke_method ( 'service-to-invoke' , 'method-to-invoke' , data = '{"id":"100", "FirstName":"Value", "LastName":"Value"}' , http_verb = 'post' )
HTTP API 调用的基本端点在 DAPR_HTTP_ENDPOINT
环境变量中指定。
如果未设置此变量,则端点值从 DAPR_RUNTIME_HOST
和 DAPR_HTTP_PORT
变量派生,其默认值分别为 127.0.0.1
和 3500
。
gRPC 调用的基本端点是用于客户端初始化的端点(如上所述 )。
保存和获取应用程序状态 from dapr.clients import DaprClient
with DaprClient () as d :
# 保存状态
d . save_state ( store_name = "statestore" , key = "key1" , value = "value1" )
# 获取状态
data = d . get_state ( store_name = "statestore" , key = "key1" ) . data
# 删除状态
d . delete_state ( store_name = "statestore" , key = "key1" )
查询应用程序状态 (Alpha) from dapr import DaprClient
query = '''
{
"filter": {
"EQ": { "state": "CA" }
},
"sort": [
{
"key": "person.id",
"order": "DESC"
}
]
}
'''
with DaprClient () as d :
resp = d . query_state (
store_name = 'state_store' ,
query = query ,
states_metadata = { "metakey" : "metavalue" }, # 可选
)
发布和订阅 发布消息 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . publish_event ( pubsub_name = 'pubsub' , topic_name = 'TOPIC_A' , data = '{"message":"Hello World"}' )
订阅消息 from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import json
app = App ()
# 默认订阅一个主题
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' )
def mytopic ( event : v1 . Event ) -> None :
data = json . loads ( event . Data ())
print ( f '接收到: id= { data [ "id" ] } , message=" { data [ "message" ] } "'
' content_type=" {event.content_type} "' , flush = True )
# 使用 Pub/Sub 路由的特定处理程序
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' ,
rule = Rule ( "event.type == \" important \" " , 1 ))
def mytopic_important ( event : v1 . Event ) -> None :
data = json . loads ( event . Data ())
print ( f '接收到: id= { data [ "id" ] } , message=" { data [ "message" ] } "'
' content_type=" {event.content_type} "' , flush = True )
流式消息订阅 您可以使用 subscribe
或 subscribe_handler
方法创建对 PubSub 主题的流式订阅。
subscribe
方法返回一个 Subscription
对象,允许您通过调用 next_message
方法从流中提取消息。这将在等待消息时阻塞主线程。完成后,您应该调用 close 方法以终止订阅并停止接收消息。
subscribe_with_handler
方法接受一个回调函数,该函数针对从流中接收到的每条消息执行。它在单独的线程中运行,因此不会阻塞主线程。回调应返回一个 TopicEventResponse
(例如 TopicEventResponse('success')
),指示消息是否已成功处理、应重试或应丢弃。该方法将根据返回的状态自动管理消息确认。对 subscribe_with_handler
方法的调用返回一个关闭函数,完成后应调用该函数以终止订阅。
以下是使用 subscribe
方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError
counter = 0
def process_message ( message ):
global counter
counter += 1
# 在此处处理消息
print ( f '处理消息: { message . data () } 来自 { message . topic () } ...' )
return 'success'
def main ():
with DaprClient () as client :
global counter
subscription = client . subscribe (
pubsub_name = 'pubsub' , topic = 'TOPIC_A' , dead_letter_topic = 'TOPIC_A_DEAD'
)
try :
while counter < 5 :
try :
message = subscription . next_message ()
except StreamInactiveError as e :
print ( '流不活跃。重试...' )
time . sleep ( 1 )
continue
if message is None :
print ( '在超时时间内未收到消息。' )
continue
# 处理消息
response_status = process_message ( message )
if response_status == 'success' :
subscription . respond_success ( message )
elif response_status == 'retry' :
subscription . respond_retry ( message )
elif response_status == 'drop' :
subscription . respond_drop ( message )
finally :
print ( "关闭订阅..." )
subscription . close ()
if __name__ == '__main__' :
main ()
以下是使用 subscribe_with_handler
方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse
counter = 0
def process_message ( message ):
# 在此处处理消息
global counter
counter += 1
print ( f '处理消息: { message . data () } 来自 { message . topic () } ...' )
return TopicEventResponse ( 'success' )
def main ():
with ( DaprClient () as client ):
# 这将启动一个新线程,该线程将监听消息
# 并在 `process_message` 函数中处理它们
close_fn = client . subscribe_with_handler (
pubsub_name = 'pubsub' , topic = 'TOPIC_A' , handler_fn = process_message ,
dead_letter_topic = 'TOPIC_A_DEAD'
)
while counter < 5 :
time . sleep ( 1 )
print ( "关闭订阅..." )
close_fn ()
if __name__ == '__main__' :
main ()
与输出绑定交互 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . invoke_binding ( binding_name = 'kafkaBinding' , operation = 'create' , data = '{"message":"Hello World"}' )
检索秘密 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . get_secret ( store_name = 'localsecretstore' , key = 'secretKey' )
配置 获取配置 from dapr.clients import DaprClient
with DaprClient () as d :
# 获取配置
configuration = d . get_configuration ( store_name = 'configurationstore' , keys = [ 'orderId' ], config_metadata = {})
订阅配置 import asyncio
from time import sleep
from dapr.clients import DaprClient
async def executeConfiguration ():
with DaprClient () as d :
storeName = 'configurationstore'
key = 'orderId'
# 在 20 秒内等待 sidecar 启动。
d . wait ( 20 )
# 通过键订阅配置。
configuration = await d . subscribe_configuration ( store_name = storeName , keys = [ key ], config_metadata = {})
while True :
if configuration != None :
items = configuration . get_items ()
for key , item in items :
print ( f "订阅键= { key } 值= { item . value } 版本= { item . version } " , flush = True )
else :
print ( "尚无内容" )
sleep ( 5 )
asyncio . run ( executeConfiguration ())
分布式锁 from dapr.clients import DaprClient
def main ():
# 锁参数
store_name = 'lockstore' # 在 components/lockstore.yaml 中定义
resource_id = 'example-lock-resource'
client_id = 'example-client-id'
expiry_in_seconds = 60
with DaprClient () as dapr :
print ( '将尝试从名为 [ %s ] 的锁存储中获取锁' % store_name )
print ( '锁是为名为 [ %s ] 的资源准备的' % resource_id )
print ( '客户端标识符是 [ %s ]' % client_id )
print ( '锁将在 %s 秒后过期。' % expiry_in_seconds )
with dapr . try_lock ( store_name , resource_id , client_id , expiry_in_seconds ) as lock_result :
assert lock_result . success , '获取锁失败。中止。'
print ( '锁获取成功!!!' )
# 此时锁已释放 - 通过 `with` 子句的魔力 ;)
unlock_result = dapr . unlock ( store_name , resource_id , client_id )
print ( '我们已经释放了锁,因此解锁将不起作用。' )
print ( '我们仍然尝试解锁它,并得到了 [ %s ]' % unlock_result . status )
加密 from dapr.clients import DaprClient
message = 'The secret is "passw0rd"'
def main ():
with DaprClient () as d :
resp = d . encrypt (
data = message . encode (),
options = EncryptOptions (
component_name = 'crypto-localstorage' ,
key_name = 'rsa-private-key.pem' ,
key_wrap_algorithm = 'RSA' ,
),
)
encrypt_bytes = resp . read ()
resp = d . decrypt (
data = encrypt_bytes ,
options = DecryptOptions (
component_name = 'crypto-localstorage' ,
key_name = 'rsa-private-key.pem' ,
),
)
decrypt_bytes = resp . read ()
print ( decrypt_bytes . decode ()) # The secret is "passw0rd"
工作流 from dapr.ext.workflow import WorkflowRuntime , DaprWorkflowContext , WorkflowActivityContext
from dapr.clients import DaprClient
instanceId = "exampleInstanceID"
workflowComponent = "dapr"
workflowName = "hello_world_wf"
eventName = "event1"
eventData = "eventData"
def main ():
with DaprClient () as d :
host = settings . DAPR_RUNTIME_HOST
port = settings . DAPR_GRPC_PORT
workflowRuntime = WorkflowRuntime ( host , port )
workflowRuntime = WorkflowRuntime ()
workflowRuntime . register_workflow ( hello_world_wf )
workflowRuntime . register_activity ( hello_act )
workflowRuntime . start ()
# 启动工作流
start_resp = d . start_workflow ( instance_id = instanceId , workflow_component = workflowComponent ,
workflow_name = workflowName , input = inputData , workflow_options = workflowOptions )
print ( f "start_resp { start_resp . instance_id } " )
# ...
# 暂停测试
d . pause_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "从 { workflowName } 获取暂停调用后的响应: { getResponse . runtime_status } " )
# 恢复测试
d . resume_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "从 { workflowName } 获取恢复调用后的响应: { getResponse . runtime_status } " )
sleep ( 1 )
# 触发事件
d . raise_workflow_event ( instance_id = instanceId , workflow_component = workflowComponent ,
event_name = eventName , event_data = eventData )
sleep ( 5 )
# 清除测试
d . purge_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
try :
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
except DaprInternalError as err :
if nonExistentIDError in err . _message :
print ( "实例成功清除" )
# 启动另一个工作流以进行终止
# 这也将测试在旧实例被清除后在新工作流上使用相同的实例 ID
start_resp = d . start_workflow ( instance_id = instanceId , workflow_component = workflowComponent ,
workflow_name = workflowName , input = inputData , workflow_options = workflowOptions )
print ( f "start_resp { start_resp . instance_id } " )
# 终止测试
d . terminate_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
sleep ( 1 )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "从 { workflowName } 获取终止调用后的响应: { getResponse . runtime_status } " )
# 清除测试
d . purge_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
try :
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
except DaprInternalError as err :
if nonExistentIDError in err . _message :
print ( "实例成功清除" )
workflowRuntime . shutdown ()
相关链接 Python SDK 示例
6.2 - 使用 Dapr actor Python SDK 入门 如何使用 Dapr Python SDK 快速上手
Dapr actor 包使您能够从 Python 应用程序与 Dapr 虚拟 actor 交互。
先决条件 actor 接口 接口定义了 actor 实现和调用 actor 的客户端之间共享的协议。由于客户端可能依赖于此协议,通常将其定义在与 actor 实现分开的模块中是有意义的。
from dapr.actor import ActorInterface , actormethod
class DemoActorInterface ( ActorInterface ):
@actormethod ( name = "GetMyData" )
async def get_my_data ( self ) -> object :
...
actor 服务 actor 服务负责托管虚拟 actor。它是一个从基类 Actor
派生并实现 actor 接口中定义的类。
可以使用以下 Dapr actor 扩展之一创建 actor:
actor 客户端 actor 客户端用于实现调用 actor 接口中定义的方法。
import asyncio
from dapr.actor import ActorProxy , ActorId
from demo_actor_interface import DemoActorInterface
async def main ():
# 创建代理客户端
proxy = ActorProxy . create ( 'DemoActor' , ActorId ( '1' ), DemoActorInterface )
# 在客户端上调用方法
resp = await proxy . GetMyData ()
示例 访问此页面 获取可运行的 actor 示例。
6.3 - Dapr Python SDK 插件 用于开发 Dapr 应用的 Python SDK 工具
6.3.1 - 开始使用 Dapr Python gRPC 服务扩展 如何启动并运行 Dapr Python gRPC 扩展
Dapr Python SDK 提供了一个用于创建 Dapr 服务的内置 gRPC 服务器扩展 dapr.ext.grpc
。
安装 您可以通过以下命令下载并安装 Dapr gRPC 服务器扩展:
pip install dapr-ext-grpc
注意 开发包包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何稳定版本的 Python SDK 扩展。
pip3 install dapr-ext-grpc-dev
示例 您可以使用 App
对象来创建一个服务器。
监听服务调用请求 可以使用 InvokeMethodRequest
和 InvokeMethodResponse
对象来处理传入的请求。
以下是一个简单的服务示例,它会监听并响应请求:
from dapr.ext.grpc import App , InvokeMethodRequest , InvokeMethodResponse
app = App ()
@app.method ( name = 'my-method' )
def mymethod ( request : InvokeMethodRequest ) -> InvokeMethodResponse :
print ( request . metadata , flush = True )
print ( request . text (), flush = True )
return InvokeMethodResponse ( b 'INVOKE_RECEIVED' , "text/plain; charset=UTF-8" )
app . run ( 50051 )
完整示例可以在这里 找到。
订阅主题 在订阅主题时,您可以指示 dapr 事件是否已被接受,或者是否应该丢弃或稍后重试。
from typing import Optional
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
from dapr.clients.grpc._response import TopicEventResponse
app = App ()
# 默认的主题订阅
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' )
def mytopic ( event : v1 . Event ) -> Optional [ TopicEventResponse ]:
print ( event . Data (), flush = True )
# 返回 None(或不显式返回)等同于返回 TopicEventResponse("success")。
# 您还可以返回 TopicEventResponse("retry") 以便 dapr 记录消息并稍后重试交付,
# 或者返回 TopicEventResponse("drop") 以丢弃消息
return TopicEventResponse ( "success" )
# 使用发布/订阅路由的特定处理程序
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' ,
rule = Rule ( "event.type == \" important \" " , 1 ))
def mytopic_important ( event : v1 . Event ) -> None :
print ( event . Data (), flush = True )
# 禁用主题验证的处理程序
@app.subscribe ( pubsub_name = 'pubsub-mqtt' , topic = 'topic/#' , disable_topic_validation = True ,)
def mytopic_wildcard ( event : v1 . Event ) -> None :
print ( event . Data (), flush = True )
app . run ( 50051 )
完整示例可以在这里 找到。
设置输入绑定触发器 from dapr.ext.grpc import App , BindingRequest
app = App ()
@app.binding ( 'kafkaBinding' )
def binding ( request : BindingRequest ):
print ( request . text (), flush = True )
app . run ( 50051 )
完整示例可以在这里 找到。
相关链接 6.3.2 - Dapr Python SDK 与 FastAPI 集成指南 如何使用 FastAPI 扩展创建 Dapr Python actor 和发布订阅功能
Dapr Python SDK 通过 dapr-ext-fastapi
扩展实现与 FastAPI 的集成。
安装 您可以通过以下命令下载并安装 Dapr FastAPI 扩展:
pip install dapr-ext-fastapi
注意 开发版包含与 Dapr 运行时预发布版本兼容的功能。在安装 <code>dapr-dev</code> 包之前,请先卸载任何稳定版本的 Python SDK 扩展。
pip install dapr-ext-fastapi-dev
示例 订阅不同类型的事件 import uvicorn
from fastapi import Body , FastAPI
from dapr.ext.fastapi import DaprApp
from pydantic import BaseModel
class RawEventModel ( BaseModel ):
body : str
class User ( BaseModel ):
id : int
name = 'Jane Doe'
class CloudEventModel ( BaseModel ):
data : User
datacontenttype : str
id : str
pubsubname : str
source : str
specversion : str
topic : str
traceid : str
traceparent : str
tracestate : str
type : str
app = FastAPI ()
dapr_app = DaprApp ( app )
# 处理任意结构的事件(简单但不够可靠)
# dapr publish --publish-app-id sample --topic any_topic --pubsub pubsub --data '{"id":"7", "desc": "good", "size":"small"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'any_topic' )
def any_event_handler ( event_data = Body ()):
print ( event_data )
# 为了更稳健,根据发布者是否使用 CloudEvents 选择以下之一
# 处理使用 CloudEvents 发送的事件
# dapr publish --publish-app-id sample --topic cloud_topic --pubsub pubsub --data '{"id":"7", "name":"Bob Jones"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'cloud_topic' )
def cloud_event_handler ( event_data : CloudEventModel ):
print ( event_data )
# 处理未使用 CloudEvents 发送的原始事件
# curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/raw_topic?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"body": "345"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'raw_topic' )
def raw_event_handler ( event_data : RawEventModel ):
print ( event_data )
if __name__ == "__main__" :
uvicorn . run ( app , host = "0.0.0.0" , port = 30212 )
创建一个 actor from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from demo_actor import DemoActor
app = FastAPI ( title = f ' { DemoActor . __name__ } 服务' )
# 添加 Dapr actor 扩展
actor = DaprActor ( app )
@app.on_event ( "startup" )
async def startup_event ():
# 注册 DemoActor
await actor . register_actor ( DemoActor )
@app.get ( "/GetMyData" )
def get_my_data ():
return "{'message': 'myData'}"
6.3.3 - Dapr Python SDK 与 Flask 集成 如何使用 Flask 扩展创建 Dapr Python 虚拟 actor
Dapr Python SDK 使用 flask-dapr
扩展来实现与 Flask 的集成。
安装 您可以通过以下命令下载并安装 Dapr Flask 扩展:
注意 开发版包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何已安装的稳定版 Python SDK 扩展。
pip install flask-dapr-dev
示例 from flask import Flask
from flask_dapr.actor import DaprActor
from dapr.conf import settings
from demo_actor import DemoActor
app = Flask ( f ' { DemoActor . __name__ } Service' )
# 启用 DaprActor Flask 扩展
actor = DaprActor ( app )
# 注册 DemoActor
actor . register_actor ( DemoActor )
# 设置方法路由
@app.route ( '/GetMyData' , methods = [ 'GET' ])
def get_my_data ():
return { 'message' : 'myData' }, 200
# 运行应用程序
if __name__ == '__main__' :
app . run ( port = settings . HTTP_APP_PORT )
6.3.4 - Dapr Python SDK 与 Dapr Workflow 扩展集成 如何使用 Dapr Workflow 扩展快速上手
注意 Dapr Workflow 目前处于初始测试阶段(alpha)。Dapr Python SDK 内置了一个 Dapr Workflow 扩展,dapr.ext.workflow
,用于创建 Dapr 服务。
安装 您可以通过以下命令下载并安装 Dapr Workflow 扩展:
pip install dapr-ext-workflow
注意 开发包包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何已安装的稳定版 Python SDK 扩展。
pip3 install dapr-ext-workflow-dev
下一步 开始使用 Dapr Workflow Python SDK 6.3.4.1 - 使用 Dapr Workflow Python SDK 入门 如何使用 Dapr Python SDK 开始并运行工作流
注意 Dapr Workflow 目前处于 alpha 阶段。我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的 hello world 工作流示例 ,您将会:
此示例使用 dapr init
的默认配置在本地模式 下运行。
在 Python 示例项目中,app.py
文件包含应用程序的设置,其中包括:
先决条件 设置环境 运行以下命令以安装使用 Dapr Python SDK 运行此工作流示例的必要依赖。
pip3 install -r demo_workflow/requirements.txt
克隆 [Python SDK 仓库]。
git clone https://github.com/dapr/python-sdk.git
从 Python SDK 根目录导航到 Dapr 工作流示例。
cd examples/demo_workflow
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 Python 程序和一个 Dapr 辅助进程。在终端中运行:
dapr run --app-id orderapp --app-protocol grpc --dapr-grpc-port 50001 --resources-path components --placement-host-address localhost:50005 -- python3 app.py
注意: 由于 Windows 中未定义 Python3.exe,您可能需要使用 python app.py
而不是 python3 app.py
。
预期输出
== APP == ==========根据输入开始计数器增加==========
== APP == start_resp exampleInstanceID
== APP == 你好,计数器!
== APP == 新的计数器值是:1!
== APP == 你好,计数器!
== APP == 新的计数器值是:11!
== APP == 你好,计数器!
== APP == 你好,计数器!
== APP == 在暂停调用后从 hello_world_wf 获取响应:已暂停
== APP == 你好,计数器!
== APP == 在恢复调用后从 hello_world_wf 获取响应:运行中
== APP == 你好,计数器!
== APP == 新的计数器值是:111!
== APP == 你好,计数器!
== APP == 实例成功清除
== APP == start_resp exampleInstanceID
== APP == 你好,计数器!
== APP == 新的计数器值是:1112!
== APP == 你好,计数器!
== APP == 新的计数器值是:1122!
== APP == 在终止调用后从 hello_world_wf 获取响应:已终止
== APP == 在终止调用后从 child_wf 获取响应:已终止
== APP == 实例成功清除
发生了什么? 当您运行 dapr run
时,Dapr 客户端:
注册了工作流 (hello_world_wf
) 及其活动 (hello_act
) 启动了工作流引擎 def main ():
with DaprClient () as d :
host = settings . DAPR_RUNTIME_HOST
port = settings . DAPR_GRPC_PORT
workflowRuntime = WorkflowRuntime ( host , port )
workflowRuntime = WorkflowRuntime ()
workflowRuntime . register_workflow ( hello_world_wf )
workflowRuntime . register_activity ( hello_act )
workflowRuntime . start ()
print ( "==========根据输入开始计数器增加==========" )
start_resp = d . start_workflow ( instance_id = instanceId , workflow_component = workflowComponent ,
workflow_name = workflowName , input = inputData , workflow_options = workflowOptions )
print ( f "start_resp { start_resp . instance_id } " )
然后 Dapr 暂停并恢复了工作流:
# 暂停
d . pause_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "在暂停调用后从 { workflowName } 获取响应: { getResponse . runtime_status } " )
# 恢复
d . resume_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "在恢复调用后从 { workflowName } 获取响应: { getResponse . runtime_status } " )
一旦工作流恢复,Dapr 触发了一个工作流事件并打印了新的计数器值:
# 触发事件
d . raise_workflow_event ( instance_id = instanceId , workflow_component = workflowComponent ,
event_name = eventName , event_data = eventData )
为了从您的状态存储中清除工作流状态,Dapr 清除了工作流:
# 清除
d . purge_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
try :
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
except DaprInternalError as err :
if nonExistentIDError in err . _message :
print ( "实例成功清除" )
然后示例演示了通过以下步骤终止工作流:
使用与已清除工作流相同的 instanceId
启动一个新的工作流。 在关闭工作流之前终止并清除工作流。 # 启动另一个工作流
start_resp = d . start_workflow ( instance_id = instanceId , workflow_component = workflowComponent ,
workflow_name = workflowName , input = inputData , workflow_options = workflowOptions )
print ( f "start_resp { start_resp . instance_id } " )
# 终止
d . terminate_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
sleep ( 1 )
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
print ( f "在终止调用后从 { workflowName } 获取响应: { getResponse . runtime_status } " )
# 清除
d . purge_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
try :
getResponse = d . get_workflow ( instance_id = instanceId , workflow_component = workflowComponent )
except DaprInternalError as err :
if nonExistentIDError in err . _message :
print ( "实例成功清除" )
下一步 7 - Dapr Rust SDK 用于开发Dapr应用的Rust SDK包
注意 Dapr Rust-SDK目前处于Alpha版本阶段。我们正在努力使其达到稳定版本,可能会涉及重大更改。这是一个帮助开发者使用Rust构建Dapr应用的客户端库。该客户端旨在支持所有公共的Dapr API,同时注重提供符合Rust习惯的开发体验和提升开发者的工作效率。
客户端 使用Rust客户端SDK调用公共的Dapr API
[**了解更多关于Rust客户端SDK的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/rust/rust-client/)
7.1 - 使用 Dapr 客户端 Rust SDK 入门 如何使用 Dapr Rust SDK 快速上手
Dapr 客户端库使您能够从 Rust 应用程序与其他 Dapr 应用程序进行交互。
注意 Dapr Rust-SDK 目前处于 Alpha 阶段。我们正在努力将其推向稳定版本,这可能会涉及重大更改。前提条件 引入客户端库 在您的 cargo.toml
文件中添加 Dapr
[ dependencies ]
# 其他依赖项
dapr = "0.13.0"
您可以引用 dapr::Client
,或者将其完整路径绑定到一个新名称,如下所示:
use dapr ::Client as DaprClient
实例化 Dapr 客户端 const addr : String = "https://127.0.0.1" ;
const port : String = "50001" ;
let mut client = dapr ::Client ::< dapr ::client ::TonicClient > ::connect ( addr ,
port ). await ? ;
功能模块 Rust SDK 允许您与 Dapr 功能模块 进行交互。
服务调用 要在运行 Dapr sidecar 的另一个服务上调用特定方法,Dapr 客户端 Go SDK 提供了以下选项:
调用服务
let response = client
. invoke_service ( "service-to-invoke" , "method-to-invoke" , Some ( data ))
. await
. unwrap ();
有关服务调用的完整指南,请访问 如何:调用服务 。
状态管理 Dapr 客户端提供对状态管理方法的访问:save_state
、get_state
、delete_state
,可以像这样使用:
let store_name = "store-name" ;
let state_key = "state-key" ;
let states = vec! [( state_key , ( "state-value" ). as_bytes (). to_vec ())];
// 使用键 "state-key" 和值 "state-value" 保存状态
client . save_state ( store_name , states ). await ? ;
// 获取键 "state-key" 的状态
let response = client . get_state ( store_name , state_key , None ). await . unwrap ();
// 删除键 "state-key" 的状态
client . delete_state ( store_name , state_key , None ). await ? ;
注意: save_state
方法目前执行的是批量保存,但未来可能会进行重构
有关状态管理的完整指南,请访问 如何:保存和获取状态 。
发布消息 要将数据发布到主题上,Dapr Go 客户端提供了一种简单的方法:
let pubsub_name = "pubsub-name" . to_string ();
let pubsub_topic = "topic-name" . to_string ();
let pubsub_content_type = "text/plain" . to_string ();
let data = "content" . to_string (). into_bytes ();
client
. publish_event ( pubsub_name , pubsub_topic , pubsub_content_type , data , None )
. await ? ;
有关发布/订阅的完整指南,请访问 如何:发布和订阅 。
相关链接 Rust SDK 示例
8 - Dapr SDK中的序列化 Dapr如何在SDK中序列化数据
Dapr的SDK应该提供两种用例的序列化功能。首先是通过请求和响应负载发送的API对象。其次是需要持久化的对象。对于这两种用例,SDK提供了默认的序列化。在Java SDK中,使用DefaultObjectSerializer 类来进行JSON序列化。
服务调用 DaprClient client = ( new DaprClientBuilder ()). build ();
client . invokeService ( "myappid" , "saySomething" , "My Message" , HttpExtension . POST ). block ();
在上面的示例中,应用程序会收到一个针对saySomething
方法的POST
请求,请求负载为"My Message"
- 引号是因为序列化器会将输入字符串序列化为JSON格式。
POST /saySomething HTTP/1.1
Host: localhost
Content-Type: text/plain
Content-Length: 12
"My Message"
状态管理 DaprClient client = ( new DaprClientBuilder ()). build ();
client . saveState ( "MyStateStore" , "MyKey" , "My Message" ). block ();
在此示例中,My Message
将被保存。它没有加引号,因为Dapr的API会在内部解析JSON请求对象后再保存它。
[
{
"key" : "MyKey" ,
"value" : "My Message"
}
]
发布订阅 DaprClient client = ( new DaprClientBuilder ()). build ();
client . publishEvent ( "TopicName" , "My Message" ). block ();
事件被发布,内容被序列化为byte[]
并发送到Dapr sidecar。订阅者将以CloudEvent 的形式接收它。CloudEvent定义data
为字符串。Dapr SDK还为CloudEvent
对象提供了内置的反序列化器。
@PostMapping ( path = "/TopicName" )
public void handleMessage ( @RequestBody ( required = false ) byte [] body ) {
// Dapr的事件符合CloudEvent。
CloudEvent event = CloudEvent . deserialize ( body );
}
绑定 在这种情况下,对象也被序列化为byte[]
,输入绑定接收原始的byte[]
并将其反序列化为预期的对象类型。
DaprClient client = ( new DaprClientBuilder ()). build ();
client . invokeBinding ( "sample" , "My Message" ). block ();
@PostMapping ( path = "/sample" )
public void handleInputBinding ( @RequestBody ( required = false ) byte [] body ) {
String message = ( new DefaultObjectSerializer ()). deserialize ( body , String . class );
System . out . println ( message );
}
它应该打印:
My Message
actor方法调用 actor方法调用的对象序列化和反序列化与服务方法调用相同,唯一的区别是应用程序不需要手动反序列化请求或序列化响应,因为这些操作都由SDK自动完成。
对于actor的方法,SDK仅支持具有零个或一个参数的方法。
public static void main () {
ActorProxyBuilder builder = new ActorProxyBuilder ( "DemoActor" );
String result = actor . invokeActorMethod ( "say" , "My Message" , String . class ). block ();
}
public String say ( String something ) {
System . out . println ( something );
return "OK" ;
}
它应该打印:
My Message
actor的状态管理 actor也可以有状态。在这种情况下,状态管理器将使用状态序列化器来序列化和反序列化对象,并自动处理这些操作。
public String actorMethod ( String message ) {
// 从键读取状态并将其反序列化为字符串。
String previousMessage = super . getActorStateManager (). get ( "lastmessage" , String . class ). block ();
// 在序列化后为键设置新状态。
super . getActorStateManager (). set ( "lastmessage" , message ). block ();
return previousMessage ;
}
默认序列化器 Dapr的默认序列化器是一个JSON序列化器,具有以下期望:
使用基本的JSON数据类型 以实现跨语言和跨平台的兼容性:字符串、数字、数组、布尔值、null和另一个JSON对象。应用程序可序列化对象中的每个复杂属性类型(例如DateTime)都应表示为JSON的基本类型之一。 使用默认序列化器持久化的数据也应保存为JSON对象,没有额外的引号或编码。下面的示例显示了字符串和JSON对象在Redis存储中的样子。 redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
" This is a message to be saved and retrieved."
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
{" value":" My data value."}
自定义序列化器必须将对象序列化为byte[]
。 自定义序列化器必须将byte[]
反序列化为对象。 当用户提供自定义序列化器时,它应作为byte[]
传输或持久化。持久化时,也要编码为Base64字符串。这是大多数JSON库本地完成的。 redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
" VGhpcyBpcyBhIG1lc3NhZ2UgdG8gYmUgc2F2ZWQgYW5kIHJldHJpZXZlZC4 = "
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
" eyJ2YWx1ZSI6Ik15IGRhdGEgdmFsdWUuIn0 = "
截至目前,Java SDK 是唯一实现此规范的Dapr SDK。在不久的将来,其他SDK也将实现相同的功能。