Durable Functions运行时会自动将函数参数、返回值和其他状态保存到 task 中心以提供可靠的执行。 但是,保留到持久存储的数据量和频率可能会影响应用程序性能和存储事务成本。 可能还需要考虑数据保留和隐私策略,具体取决于应用程序存储的数据的类型。
本文介绍保留哪些数据、如何处理大型有效负载和敏感数据,以及如何为每个受支持的语言自定义序列化。
本文内容:
任务中心内容
任务枢纽存储实例的当前状态,以及所有等待处理的消息:
-
实例状态存储实例的当前状态和历史记录。 对于业务流程实例,此状态包括运行时状态、业务流程历史记录、输入、输出和自定义状态。 对于实体实例,它包括实体状态。
-
消息存储函数输入或输出、事件负载以及出于内部目的(如路由和端到端关联)使用的元数据。
处理后会删除消息,但实例状态会保留,除非应用程序或操作员将其显式删除。 具体而言,即使编排完成,编排历史记录仍会保留在存储中。
有关状态和消息如何表示业务流程进度的示例,请参阅任务中心执行示例。
存储中表示状态和消息的位置和方式取决于存储提供程序。 使用 持久任务调度器 ,因为它为任务中心提供管理后端,并帮你处理底层状态存储。 然而,Azure 存储 依然是现有工作负载和希望自行管理存储资源的应用的可靠选择。
| 存储提供商 |
状态如何存储 |
建议用途 |
| 持久任务计划程序 |
编排状态和实体状态存储在任务中心资源背后的托管调度程序后端中。 |
新的 Durable Functions 应用和托管式部署的首选选项。 |
| Azure 存储 |
状态和消息以队列、表和 Azure 存储 账户中的 blob 表示。 |
非常适合已经依赖Azure 存储的现有应用或部署。 |
序列化和持久化数据类型
以下列表显示了使用Durable Functions功能时将序列化和保留的不同类型的数据:
- 协调器、活动函数和实体函数的所有输入和输出,包括任何 ID 和未处理的异常
- 编排器、活动和实体函数的名称
- 外部事件名称和有效负载
- 自定义业务流程状态有效负载
- 业务流程终止消息
- 持久计时器负载
- 耐用的 HTTP 请求和响应 URL、头信息和有效负载
- 实体呼叫和信号负载
- 实体状态信息载荷
有关管理有效负载大小和保护此列表中的敏感项的指导,请参阅以下部分。
如果向 Durable Functions API 提供大型输入和输出,则可能会遇到内存问题。 输入和输出被序列化到编排历史中,这意味着大型有效负载可能会随着时间的推移显著促进无限制的历史增长。 此增长风险会导致 重播期间出现内存异常。
若要缓解大型输入和输出的影响,可以:
- 将工作委托给子业务流程协调程序,以跨多个业务流程协调程序对历史记录内存负担进行负载均衡,使单个历史记录的内存占用量保持较小。
- 将大型数据存储在外部存储(如Azure Blob 存储)中,并传递轻量标识符,以便在需要时在活动函数内检索该数据。
对于持久任务调度器,利用大负载支持将较大的负载卸载到Azure Blob 存储。 对于新应用,当编排需要在持久操作之间传递大型负载时,推荐采用这种模式。 如果你使用 Azure 存储 提供者,仍然可以应用下节所示的申领-检查模式,并在操作间传递轻量级引用。
Tip
处理大数据的最佳做法是将其保留在外部存储中,并在需要时仅在活动内具体化该数据。
传递大型负载的引用
选择适合你存储服务商的图案。
持久任务计划程序
如果你使用Durable Task Scheduler,请启用大负载支持,这样运行时会写入更大的负载到Azure Blob 存储,并通过调度器发送一个小的引用。 调度器文档中展示了典型配置:
{
"version": "2.0",
"extensions": {
"durableTask": {
"storageProvider": {
"type": "azureManaged",
"connectionStringName": "DTS_CONNECTION_STRING",
"payloadStorageEnabled": true,
"payloadStorageThresholdBytes": 262144
},
"hubName": "%TASKHUB_NAME%"
}
}
}
Azure 存储
通过 Azure 存储 提供商,你可以使用申报检查模式,保持编排历史较小,同时仍能处理大型负载。 编排器传递一个包含 blob 容器和 blob 名称的轻量级引用,活动根据需要从 Azure Blob 存储 读取或写入有效载荷。
以下示例假定您的存储账户中已存在一个 blob。 以诸如 {"container":"large-payloads","blobName":"input/job-123.json"} 这样的引用开始编排。 活动在必要时创建 processed-payloads 输出容器。 采样处理步骤不变地复制输入字节;用你的应用逻辑替换它。
Important
引用中切勿包含存储凭证或共享访问签名(SAS)。 系统会在编排历史记录中持久保存该引用。 这些例子使用了一个名为 PAYLOAD_STORAGE_CONNECTION_STRING App 的设置来保持存储代码简洁。 对于生产工作负载,可以使用 Microsoft Entra ID 对Blob 数据的访问进行授权。
这个例子需要 Azure。Storage.Blobs NuGet 包。
using System;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
public record BlobReference(string Container, string BlobName);
public static class LargePayloadFunctions
{
[Function("ProcessLargePayload")]
public static async Task<BlobReference> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
BlobReference inputReference = context.GetInput<BlobReference>()
?? throw new InvalidOperationException("A blob reference is required.");
return await context.CallActivityAsync<BlobReference>(
nameof(ProcessLargePayloadActivity), inputReference);
}
[Function(nameof(ProcessLargePayloadActivity))]
public static async Task<BlobReference> ProcessLargePayloadActivity(
[ActivityTrigger] BlobReference inputReference)
{
string connectionString =
Environment.GetEnvironmentVariable("PAYLOAD_STORAGE_CONNECTION_STRING")
?? throw new InvalidOperationException("Payload storage is not configured.");
BlobServiceClient service = new BlobServiceClient(connectionString);
BlobClient inputBlob = service
.GetBlobContainerClient(inputReference.Container)
.GetBlobClient(inputReference.BlobName);
BinaryData inputData = (await inputBlob.DownloadContentAsync()).Value.Content;
BlobContainerClient outputContainer =
service.GetBlobContainerClient("processed-payloads");
await outputContainer.CreateIfNotExistsAsync();
string outputName = $"processed/{Guid.NewGuid():N}.json";
await outputContainer.GetBlobClient(outputName)
.UploadAsync(inputData, overwrite: true);
return new BlobReference(outputContainer.Name, outputName);
}
}
这个例子需要 Azure。Storage.Blobs NuGet 包。
using System;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
public class BlobReference
{
public string Container { get; set; } = string.Empty;
public string BlobName { get; set; } = string.Empty;
}
public static class LargePayloadFunctions
{
[FunctionName("ProcessLargePayload")]
public static async Task<BlobReference> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
BlobReference inputReference = context.GetInput<BlobReference>()
?? throw new InvalidOperationException("A blob reference is required.");
return await context.CallActivityAsync<BlobReference>(
"ProcessLargePayloadActivity", inputReference);
}
[FunctionName("ProcessLargePayloadActivity")]
public static async Task<BlobReference> RunActivity(
[ActivityTrigger] BlobReference inputReference)
{
string connectionString =
Environment.GetEnvironmentVariable("PAYLOAD_STORAGE_CONNECTION_STRING")
?? throw new InvalidOperationException("Payload storage is not configured.");
BlobServiceClient service = new BlobServiceClient(connectionString);
BlobClient inputBlob = service
.GetBlobContainerClient(inputReference.Container)
.GetBlobClient(inputReference.BlobName);
BinaryData inputData = (await inputBlob.DownloadContentAsync()).Value.Content;
BlobContainerClient outputContainer =
service.GetBlobContainerClient("processed-payloads");
await outputContainer.CreateIfNotExistsAsync();
string outputName = $"processed/{Guid.NewGuid():N}.json";
await outputContainer.GetBlobClient(outputName)
.UploadAsync(inputData, overwrite: true);
return new BlobReference
{
Container = outputContainer.Name,
BlobName = outputName,
};
}
}
这个例子需要 @azure/storage-blob npm 包。
const { randomUUID } = require("crypto");
const { BlobServiceClient } = require("@azure/storage-blob");
const df = require("durable-functions");
df.app.orchestration("processLargePayload", function* (context) {
const inputReference = context.df.getInput();
return yield context.df.callActivity(
"processLargePayloadActivity", inputReference);
});
df.app.activity("processLargePayloadActivity", {
handler: async (inputReference) => {
const connectionString =
process.env.PAYLOAD_STORAGE_CONNECTION_STRING;
if (!connectionString) {
throw new Error(
"PAYLOAD_STORAGE_CONNECTION_STRING is not set.");
}
const service = BlobServiceClient.fromConnectionString(
connectionString);
const inputBlob = service
.getContainerClient(inputReference.container)
.getBlockBlobClient(inputReference.blobName);
const inputData = await inputBlob.downloadToBuffer();
const outputContainer =
service.getContainerClient("processed-payloads");
await outputContainer.createIfNotExists();
const outputName = `processed/${randomUUID()}.json`;
await outputContainer.getBlockBlobClient(outputName)
.uploadData(inputData);
return {
container: outputContainer.containerName,
blobName: outputName,
};
},
});
这个例子需要 azure-storage-blob 包。 加到 requirements.txt。
import os
import uuid
import azure.functions as func
import azure.durable_functions as df
from azure.core.exceptions import ResourceExistsError
from azure.storage.blob import BlobServiceClient
app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.orchestration_trigger(context_name="context")
def process_large_payload(context: df.DurableOrchestrationContext):
input_reference: dict = context.get_input()
return (yield context.call_activity(
"process_large_payload_activity", input_reference))
@app.activity_trigger(input_name="input_reference")
def process_large_payload_activity(input_reference: dict) -> dict:
connection_string = os.getenv("PAYLOAD_STORAGE_CONNECTION_STRING")
if not connection_string:
raise ValueError(
"PAYLOAD_STORAGE_CONNECTION_STRING is not set.")
service = BlobServiceClient.from_connection_string(connection_string)
input_blob = service.get_blob_client(
container=input_reference["container"],
blob=input_reference["blobName"])
input_data = input_blob.download_blob().readall()
output_container = service.get_container_client("processed-payloads")
try:
output_container.create_container()
except ResourceExistsError:
pass
output_name = f"processed/{uuid.uuid4()}.json"
output_container.upload_blob(
name=output_name, data=input_data, overwrite=True)
return {
"container": output_container.container_name,
"blobName": output_name,
}
本示例需要 Az.Storage 模块。 把它添加到 requirements.psd1 里,并确保在 host.json中启用了托管依赖。
编曲者 run.ps1:
param($Context)
$OutputReference = Invoke-DurableActivity `
-FunctionName 'ProcessLargePayloadActivity' `
-Input $Context.Input
return $OutputReference
活动:run.ps1
param($InputReference)
if ([string]::IsNullOrEmpty(
$env:PAYLOAD_STORAGE_CONNECTION_STRING)) {
throw 'The PAYLOAD_STORAGE_CONNECTION_STRING app setting is not configured.'
}
$StorageContext = New-AzStorageContext `
-ConnectionString $env:PAYLOAD_STORAGE_CONNECTION_STRING
$InputFile = New-TemporaryFile
try {
Get-AzStorageBlobContent `
-Container $InputReference.container `
-Blob $InputReference.blobName `
-Destination $InputFile.FullName `
-Context $StorageContext `
-Force | Out-Null
# Process $InputFile here. This example uploads it unchanged.
$OutputContainer = 'processed-payloads'
New-AzStorageContainer `
-Name $OutputContainer `
-Context $StorageContext `
-ErrorAction SilentlyContinue | Out-Null
$OutputName = "processed/$([guid]::NewGuid()).json"
Set-AzStorageBlobContent `
-File $InputFile.FullName `
-Container $OutputContainer `
-Blob $OutputName `
-Context $StorageContext `
-Force | Out-Null
return @{
container = $OutputContainer
blobName = $OutputName
}
}
finally {
Remove-Item $InputFile.FullName -Force
}
使用orchestrationTrigger中描述的标准 activityTrigger 和 绑定。
此示例需要 com.azure:azure-storage-blob 和 com.microsoft:durabletask-azure-functions Maven 软件包。
import com.azure.core.util.BinaryData;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.durabletask.TaskOrchestrationContext;
import com.microsoft.durabletask.azurefunctions.DurableActivityTrigger;
import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger;
import java.util.Objects;
import java.util.UUID;
public class LargePayloadFunctions {
public static class BlobReference {
public String container;
public String blobName;
public BlobReference() {
}
public BlobReference(String container, String blobName) {
this.container = container;
this.blobName = blobName;
}
}
@FunctionName("ProcessLargePayload")
public BlobReference runOrchestrator(
@DurableOrchestrationTrigger(name = "ctx")
TaskOrchestrationContext ctx) {
BlobReference inputReference = Objects.requireNonNull(
ctx.getInput(BlobReference.class),
"A blob reference is required.");
return ctx.callActivity(
"ProcessLargePayloadActivity",
inputReference,
BlobReference.class).await();
}
@FunctionName("ProcessLargePayloadActivity")
public BlobReference runActivity(
@DurableActivityTrigger(name = "inputReference")
BlobReference inputReference) {
String connectionString = Objects.requireNonNull(
System.getenv("PAYLOAD_STORAGE_CONNECTION_STRING"),
"PAYLOAD_STORAGE_CONNECTION_STRING is not configured.");
BlobServiceClient service = new BlobServiceClientBuilder()
.connectionString(connectionString)
.buildClient();
BinaryData inputData = service
.getBlobContainerClient(inputReference.container)
.getBlobClient(inputReference.blobName)
.downloadContent();
BlobContainerClient outputContainer =
service.getBlobContainerClient("processed-payloads");
outputContainer.createIfNotExists();
String outputName =
"processed/" + UUID.randomUUID() + ".json";
outputContainer.getBlobClient(outputName)
.upload(inputData, true);
return new BlobReference(
outputContainer.getBlobContainerName(), outputName);
}
}
如果并行活动产生多个大型结果,返回一份引用列表,并将该列表传递给最终的聚合活动。 聚合活动应加载并合并有效载荷,然后写入最终输出块。 不要在编排器中加载或串接大型结果。
处理敏感数据
输入和输出(包括异常)被持久地保存在您选择的 Durable Functions 存储服务提供商中。 如果这些输入、输出或异常包含敏感数据(例如机密、连接字符串或个人身份信息),则对存储提供程序的资源具有读取访问权限的任何人都可以获取它们。
若要安全地处理敏感数据,请从Azure 密钥保管库或环境变量中提取活动函数中的数据,并且永远不会将数据直接与业务流程协调程序或实体通信。 此方法有助于防止敏感数据泄漏到存储资源中。
同样,必须严格控制对存储资源的写入访问,因为存储中被篡改的数据可能会改变业务流程行为。 有关保护任务中心存储的详细信息,请参阅 保护任务中心存储。
Tip
本指南也适用于 CallHttp 业务流程协调程序 API,该 API 在存储中保留其请求和响应有效负载。 如果目标 HTTP 终结点需要身份验证,请在活动内实现 HTTP 调用,或使用 CallHttp 提供的内置托管标识支持,该支持不会将凭据保存到存储中。
注意
为了防止拥有日志读取访问权限的人员(例如,在 Application Insights 中的人员)获取这些机密,避免记录包含机密的数据。
静止数据加密
使用 Azure 存储提供程序时,会自动静态加密所有数据。 但是,有权访问存储帐户的任何人都可以读取采用未加密格式的数据。 如果需要对敏感数据提供更强的保护,请考虑首先使用你自己的加密密钥对数据进行加密,从而以预加密的形式保存数据。
另外,.NET 用户也可以选择实现提供自动加密的自定义序列化提供程序。 可以在此 GitHub 示例中找到一个包含加密功能的自定义序列化的示例。
注意
如果决定实施应用程序级别的加密,请注意,业务流程和实体可以存在无限长的时间。 这一点在需要轮换加密密钥时很重要,因为业务流程或实体运行的时间可能比密钥轮换策略要长。 如果发生密钥轮换,则下一次执行业务流程或实体时,用于加密数据的密钥也许不再可用于对数据进行解密。 因此,仅当业务流程和实体预计在相对较短时间内运行时,才建议使用自定义加密。
保护任务中心存储
托管任务中心的存储后端是关键信任边界。 Durable Task Framework 信任它在业务流程重播和消息处理期间从存储中读取的数据。 任何对任务中心存储具有写入权限的人都可以篡改编排状态、挂起的消息或存储的负载数据。 这可能会更改应用程序行为、触发意外操作或在函数应用的上下文中实现远程代码执行。
Important
不要公开任务中心存储凭据或授予对不受信任的参与方的写入访问权限。 对任务中心存储的写入访问权限可用于更改应用程序行为,包括触发任意代码执行。
共担责任
保护存储后端是你的责任,与保护存储应用程序状态或代码的任何数据库相同。 Durable Task Framework 不会对存储的数据执行完整性验证,因此它依赖于存储层的访问控制来防止未经授权的修改。
| Backend |
安全责任 |
Guidance |
| 持久任务计划程序 |
Microsoft负责管理底层存储后台。 你管理身份、任务中心访问和应用级安全。 |
新建 Durable Functions 应用的首选默认设置。 |
| Azure 存储 及其他 BYO 提供商 |
你管理存储账户或数据库及其安全控制。 |
非常适合已经依赖Azure 存储的现有工作负载或部署。 |
注意
不要在不受信任的租户之间共享单个任务中心。 任务中心不会在其用户之间强制实施访问边界,因此任何可读取或写入任务中心的租户都可能会影响其中的所有业务流程和实体。 同样,不要将同一后端中的独立任务中心作为安全边界来依赖。 虽然 Durable Task Scheduler 支持 范围限定为单个任务中心的 RBAC,但网络控制(如 IP 允许列表和专用终结点)仅适用于计划程序级别,因此计划程序中的任务中心不是安全隔离边界。 BYO 存储提供程序也是如此,任何有权访问存储帐户或数据库的租户都可以访问该后端上的所有任务中心。 若需在不同租户之间实现安全隔离,请为各租户独立预配基础结构:对于自有组件提供商,需分配独立存储帐户或数据库;也可部署独立的持久任务计划程序实例。
存储加固检查清单
应用以下最佳做法来保护任务中心存储:
使用基于身份的连接来连接你选择的后端。
- 使用持久任务调度器时,可以依赖托管身份和RBAC来管理调度器和任务中心。
- 对于Azure 存储和其他BYO提供商,尽量优先使用托管身份而非连接字符串。
请参阅为Durable Functions配置托管标识。
应用最小权限 RBAC 角色。 仅授予所需的最低权限。 避免将广泛存储权限授予不需要的用户或服务。
通过使用私有终端或服务终端限制对存储账户或调度器部署的网络访问。 这一限制有助于防止未授权的网络层访问任务中心数据。
通过为存储帐户启用 Azure Monitor 资源日志(尤其是 日志类别)来StorageWrite。 将这些日志路由到受监控的存储帐户外部的目标位置,例如 Log Analytics,从而防止这些日志被篡改。 请参阅 存储日志。
如果使用连接字符串,请定期轮换凭据。 像对待任何其他高权限凭据一样谨慎保管存储账户密钥。
请考虑托管存储后端。
Durable Task Scheduler 自动处理存储安全,包括认证、RBAC 和网络隔离,而 Azure 存储 则提供显式的存储控制。
自定义序列化和反序列化
序列化自定义选项因语言而异。 选择语言选项卡以查看可用选项。
默认序列化逻辑
适用于 .NET(进程内)的 Durable Functions 在内部使用 Json.NET,以便将业务流程和实体数据序列化为 JSON。 使用的默认 Json.NET 设置是:
输入、输出和状态:
JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None,
DateParseHandling = DateParseHandling.None,
}
异常:
JsonSerializerSettings
{
ContractResolver = new ExceptionResolver(),
TypeNameHandling = TypeNameHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
}
在JsonSerializerSettings阅读有关 的详细文档。
使用.NET属性自定义序列化
在序列化期间,Json.NET 查找类和属性上的各种属性,这些属性控制数据如何从 JSON 序列化和反序列化。 如果你拥有传递到 Durable Functions API 的数据类型的源代码,请考虑将这些特性添加到类型以自定义序列化和反序列化。
通过依赖注入自定义序列化
面向 .NET 并在 Functions V3 运行时上运行的函数应用可以使用依赖项注入 (DI) 来自定义数据和异常的序列化方式。 以下示例代码演示如何使用 DI 通过 IMessageSerializerSettingsFactory 和 IErrorSerializerSettingsFactory 服务接口的自定义实现替代默认 Json.NET序列化设置。
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using System.Collections.Generic;
[assembly: FunctionsStartup(typeof(MyApplication.Startup))]
namespace MyApplication
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMessageSerializerSettingsFactory, CustomMessageSerializerSettingsFactory>();
builder.Services.AddSingleton<IErrorSerializerSettingsFactory, CustomErrorSerializerSettingsFactory>();
}
/// <summary>
/// A factory that provides the serialization for all inputs and outputs for activities and
/// orchestrations, as well as entity state.
/// </summary>
internal class CustomMessageSerializerSettingsFactory : IMessageSerializerSettingsFactory
{
public JsonSerializerSettings CreateJsonSerializerSettings()
{
// Return your custom JsonSerializerSettings here
}
}
/// <summary>
/// A factory that provides the serialization for all exceptions thrown by activities
/// and orchestrations
/// </summary>
internal class CustomErrorSerializerSettingsFactory : IErrorSerializerSettingsFactory
{
public JsonSerializerSettings CreateJsonSerializerSettings()
{
// Return your custom JsonSerializerSettings here
}
}
}
}
序列化和反序列化逻辑
Azure Functions Node 应用程序使用 JSON.stringify() 进行序列化,使用 JSON.Parse() 进行反序列化。 大多数类型都会无缝地进行序列化和反序列化。 如果默认逻辑不够用,则可以通过在对象上定义一个toJSON()方法来覆盖序列化逻辑。 但是,对于对象反序列化,不存在模拟。
若要对序列化/反序列化管道进行完全的自定义,请考虑使用你自己的代码来处理序列化和反序列化,并将数据作为字符串进行传递。
序列化和反序列化逻辑
Durable Functions PowerShell 应用使用默认的 PowerShell JSON 处理方式来处理业务流程和活动的有效负载。 在大多数情况下,兼容 JSON 的普通对象能够正确序列化和反序列化,无需额外配置。
如果你需要为负载形状自定义行为,可以在函数内自己序列化或反序列化数据,并在编排器、活动和实体之间传递兼容 JSON 的值。 这种方法保持数据格式的明确性,避免依赖未为PowerShell暴露的串行器定制API。
对于更复杂的场景,可以考虑将数据表示为简单的字符串、数组或哈希表,并在函数代码中转换不同格式。
序列化和反序列化逻辑
建议使用类型注解以确保 Durable Functions 正确地序列化和反序列化数据。 虽然许多内置类型是自动处理的,但是某些内置数据类型需要类型注解才能在反序列化期间保留类型。
对于自定义数据类型,可以通过在数据类型类中定义类方法to_json和from_json来实现 JSON 序列化和反序列化。 请注意,这些方法不会在业务流程协调程序函数的返回值上调用,这意味着返回值必须是本机 JSON 可序列化的。 有关详细信息,请参阅 绑定。
要验证反序列化的负载是否符合预期类型,并可选选择加入硬化严格模式,请参见 Python 的 Durable Functions 中的“迁移到类型安全序列化”。
后续步骤