部署 Apex

您无法在 Salesforce 生产组织中开发 Apex。您的开发工作是 在沙盒或 Developer Edition 组织中完成。您可以使用以下方法部署 Apex:

  • 更改集
  • Salesforce 扩展 针对 Visual Studio Code
  • 蚂蚁迁徙 工具
  • 肥皂 应用程序接口
  • 使用元数据 API 或工具 API 的第三方工具
  • 带有 Salesforce DX 插件的 VS Code

部署时编译

从 18 年夏季开始,每个组织的 Apex 代码现在都会自动重新编译 完成元数据部署、包安装或包升级。部署时编译 为生产组织自动启用,以确保用户不会 部署后立即体验性能下降,而您不能 禁用它。对于沙盒、开发人员、试用和临时组织,此功能是 默认情况下禁用,但您可以在“设置”中的“Apex 设置”下启用。

此功能会导致部署到组织调用 Apex 编译器并保存 生成的字节码作为部署的一部分。部署时间的增加最小 ,但 Apex 不需要在首次运行时重新编译。所以轻微的 增加部署时间可以防止首次运行时出现性能问题。考虑 在多个用户共享的沙盒或临时组织中启用此功能 功能测试或由持续集成流程使用。

有关使用元数据 API 设置此组织首选项的信息,请参阅《元数据 API 开发人员指南》中的 OrgPreferenceSettings。

  1. 使用更改集部署 Apex
  2. 使用适用于 Visual Studio Code 的 Salesforce 扩展部署 Apex
  3. 使用 Ant 迁移工具部署更改
  4. 使用 SOAP API 部署Apex
    默认情况下,这些 Salesforce 对象和 SOAP API 调用和标头可用于 Apex。有关所有其他 SOAP API 调用的信息,包括可用于扩展或实现任何现有 Apex IDE 的调用,请联系您的 Salesforce 代表。

使用更改集部署 Apex

适用于:Salesforce Classic
提供 EnterprisePerformanceUnlimited 和 Database.com Edition

您可以部署 Apex 连接组织之间的类和触发器,例如,从沙盒组织到 您的生产组织。您可以在 Salesforce 用户界面中创建出站更改集 并添加要上传并部署到目标的 Apex 组件 组织。要了解有关更改集的更多信息,请参阅 Salesforce Online 中的“更改集” 帮助。

使用适用于 Visual Studio Code 的 Salesforce 扩展进行部署 顶点

适用于 Visual Studio Code 的 Salesforce 扩展包括用于在 轻量级、可扩展的 VS Code 编辑器中的 Salesforce 平台。这些工具提供 用于与开发组织(临时组织、沙盒和 DE 组织)、Apex、Aura 合作的功能 组件和 Visualforce。

注意

如果部署到生产组织:

  • 单元测试必须至少覆盖 75% 的 Apex 代码,并且所有这些测试都必须 成功完成。
  • 每个触发器都必须具有一定的测试覆盖率。
  • 所有类和触发器都必须成功编译。

请注意以下事项。

  • 将 Apex 部署到生产组织时,每个单元测试 默认情况下,将执行 Organization 命名空间。
  • 调用不计入 Apex 代码的一部分 覆盖。System.debug
  • 测试方法和测试类不计为 Apex 代码的一部分 覆盖。
  • 虽然只有 75% 的 Apex 代码必须被测试覆盖,但不要专注于 所覆盖代码的百分比。相反,请确保每次使用 涵盖您的申请案例,包括正面和负面案例, 以及批量和单条记录。这种方法可确保 75% 或更多 的代码包含在单元测试中。

有关如何使用 Visual Studio Code 部署到 Salesforce 组织的详细信息,请参阅开发模型。

使用 Ant 迁移工具部署更改

除了适用于 Visual Studio Code 的 Salesforce 扩展之外,您还可以使用脚本来执行以下操作 部署Apex。

下载 Ant 迁移工具以执行基于文件的元数据更改部署,以及 Apex 类从 Developer Edition 或沙盒组织到使用 Apache 的 Ant 构建工具。

注意

Ant 迁移工具是 Salesforce 提供的免费资源 支持其用户和合作伙伴,但出于目的不被视为我们服务的一部分 Salesforce 主要服务协议。要使用 Ant 迁移工具,请执行以下操作:

  1. 访问 http://www.oracle.com/technetwork/java/javase/downloads/index.html 并安装 Java JDK。注意Ant Migration Tool 版本 51.0 及更高版本需要 Java 版本 11 或更高版本。如果使用 Ant 迁移工具版本 36.0 到 50.0,为了增强安全性,我们 建议使用 Java 7 或更高版本以及最新版本的 Ant 迁移工具(版本 36.0 或更高版本)。从版本 36.0 开始,Ant 迁移工具使用 TLS 1.2 来确保安全 当 Salesforce 检测到 Java 版本 7 (1.7) 时与 Salesforce 进行通信。工具 显式启用 Java 7 的 TLS 1.1 和 1.2。如果您使用的是 Java 8 (1.8)、TLS 1.2 被使用。对于 Java V6,使用 TLS 1.0,而 Salesforce的。或者,如果您使用的是 Java 7,而不是升级 Ant 迁移工具 对于版本 36.0 或更高版本,您可以将以下内容添加到环境变量中:ANT_OPTS-Dhttps.protocols=TLSv1.1,TLSv1.2此设置还会对本地的任何其他 Ant 工具强制实施 TLS 1.1 和 1.2 系统。
  2. 访问 http://ant.apache.org/ 并安装 Apache Ant 版本 1.6 或更高版本,在部署计算机上。
  3. 按照《Ant 安装指南》第 http://ant.apache.org/manual/install.html 页中指定的方式设置环境变量(如 、 和 )。ANT_HOMEJAVA_HOMEPATH
  4. 通过打开命令提示符来验证 JDK 和 Ant 是否已正确安装,然后 进入。您的输出 必须看起来像 这:ant –versionApache Ant version 1.7.0 compiled on December 13 2006
  5. 下载21年夏季蚂蚁迁徙的.zip文件 工具。下载链接没有 要求对 Salesforce 进行身份验证。如果您已登录 Salesforce,我们建议您 在访问浏览器中的链接之前注销。
  6. 将下载的文件解压缩到您选择的目录。Zip 文件包含以下内容:
    • 说明如何使用这些工具的自述文件 .html 文件
    • 包含 ant 任务的 Jar 文件:ant-salesforce.jar
    • 包含以下内容的示例文件夹:
      • 一个 codepkg\classes 文件夹,其中包含SampleDeployClass.clsSampleFailingTestClass.cls
      • 包含 SampleAccountTrigger.trigger 的 codepkg\triggers 文件夹
      • 包含自定义对象的 mypkg\objects 文件夹 在示例中使用
      • 一个 removecodepkg 文件夹,其中包含要删除的 XML 文件 来自您组织的示例
      • 您必须编辑的示例 build.properties 文件, 指定您的凭据,以便在 build.xml 中运行示例 Ant 任务
      • 一个示例 build.xml 文件,用于执行 和 API 调用deployretrieve
  7. Ant 迁移工具使用 ant-salesforce.jar 文件 分发.zip文件。如果您安装了该工具的早期版本,并且 将 ant-salesforce.jar 复制到 Ant lib 目录,删除之前的 jar 文件。lib 目录位于 Ant 的根文件夹中 安装。您无需将新的 jar 文件复制到 Ant lib 目录。
  8. 打开解压缩文件中的示例子目录。
  9. 编辑 build.properties 文件:
    1. 分别在 sf.user 和 sf.password 字段中输入您的 Salesforce 生产组织用户名和密码。注意
      • 您指定的用户名必须具有编辑 Apex 的权限。
      • 如果您从不受信任的网络使用 Ant 迁移工具,请附加 安全令牌添加到密码中。了解有关安全性的详细信息 令牌,请参阅“重置” 您的安全令牌“在 Salesforce 帮助中。
    2. 如果要部署到沙盒组织,请将 sf.serverurl 字段更改为 .https://test.salesforce.com
  10. 在示例目录中打开命令窗口。
  11. 进入。这将使用示例类和 Account 运行 API 调用 触发器随 Ant 迁移工具提供。ant deployCodedeploy调用 build.xml 文件中命名的 Ant 目标。ant deployCodedeploy<!-- Shows deploying code & running tests for package 'codepkg' --> <target name="deployCode"> <!-- Upload the contents of the "codepkg" package, running the tests for just 1 class --> <sf:deploy username="${sf.username}" password="${sf.password}" serverurl="${sf.serverurl}" deployroot="codepkg"> <runTest>SampleDeployClass</runTest> </sf:deploy> </target>有关详细信息,请参阅了解部署。
  12. 要删除在执行过程中添加的测试类和触发器,请在命令窗口中输入以下内容: 。ant deployCodeant undeployCodeant undeployCode调用 build.xml 文件中命名的 Ant 目标。undeployCode<target name="undeployCode"> <sf:deploy username="${sf.username}" password="${sf.password}" serverurl= "${sf.serverurl}" deployroot="removecodepkg"/> </target>

查看 Ant 迁移工具 有关 Ant 迁移工具的完整详细信息的指南。

  1. 了解部署
    Ant 迁移工具提供了该任务,该任务可以合并到您的部署脚本中。deploy
  2. 了解检索

了解部署

Ant 迁移工具提供了该任务,该任务可以合并到您的部署脚本中。

deploy

您可以修改 build.xml 示例以包含组织的 类和触发器。有关部署任务的属性的完整列表,请参见《Ant 迁移工具指南》。该任务的一些属性包括:deployusername用于登录 Salesforce 生产组织的用户名。password与登录 Salesforce 生产组织关联的密码。serverURL您正在登录的 Salesforce 服务器的 URL。我们建议您指定 贵组织的 My Domain 登录 URL,列在“My Domain 设置”页面上。如果你不这样做 指定一个值,默认值为 。login.salesforce.comdeployRoot包含 Apex 类和触发器的本地目录,以及任何其他 元数据。创建必要文件结构的最佳方法是 从您的组织或沙盒中检索它。有关详细信息,请参阅了解检索。

  • Apex 类文件必须位于名为 classes 的子目录中。您必须有两个 每个类的文件,命名如下:
    • classname.CLS公司
    • classname.cls-meta.xml
    例如,和 .-meta.xml 文件包含 API 版本和类的状态(活动/非活动)。MyClass.clsMyClass.cls-meta.xml
  • Apex 触发器文件必须位于名为 triggers 的子目录中。你必须有 每个触发器有两个文件,命名如下:
    • triggername.触发
    • triggername.触发器元.xml
    例如,和 .-meta.xml 文件 包含触发器的 API 版本和状态(活动/非活动)。MyTrigger.triggerMyTrigger.trigger-meta.xml
  • 根目录包含一个 XML 文件包 .xml,其中列出了 要部署的所有类、触发器和其他对象。
  • 根目录(可选)包含一个 XML 文件 destructiveChanges.xml,其中列出了所有类、触发器、 以及要从组织中删除的其他对象。

checkOnly指定是将类和触发器部署到目标环境,还是 不。此属性采用布尔值:if 否则,您不希望将类和触发器保存到组织中。如果未指定值,则 缺省值为 。truefalsefalserunTest可选的子元素。包含部署后运行的测试的 Apex 类的列表。 要使用此选项,请将 testLevel 设置为 。RunSpecifiedTeststestLevel自选。指定在部署过程中运行哪些测试。测试级别为 无论部署包中存在的组件类型如何,都强制执行。 有效值为:

  • NoTestRun– 不运行任何测试。此测试 级别仅适用于部署到开发环境,例如沙盒、 Developer Edition 或试用组织。此测试级别是 开发环境。
  • RunSpecifiedTests– 仅运行您在 runTests 选项中指定的测试。代码覆盖率要求不同于 使用此测试级别时的默认覆盖率要求。每个类和触发器 在部署包中,必须至少覆盖 75% 的已执行测试 代码覆盖率。此覆盖率是针对每个类计算的,并单独触发,并且 与总体覆盖率不同。
  • RunLocalTests组织中的所有测试都是 运行,但源自已安装、托管和解锁的那些除外 包。此测试级别是生产部署的默认级别,该级别符合以下条件 包括 Apex 类或触发器。
  • RunAllTestsInOrg运行所有测试。 这些测试包括组织中的所有测试,包括托管测试 包。

如果未指定测试级别,则使用默认测试执行行为。 请参阅元数据 API 开发人员的 指南。

此字段在 API 版本 34.0 中可用,并且 后。runAllTests(已弃用,仅在 API 版本 33.0 及更早版本中可用。此参数是 可选,缺省值为 。设置为在部署后运行所有 Apex 测试,包括测试 源自已安装的托管软件包。falsetrue

了解检索

使用目标检索类和 来自沙盒或生产组织的触发器。在正常的部署周期中,您将在 之前运行,以便为新类和 触发器。但是,对于此示例,首先用于 确保有要检索的内容。retrieveCoderetrieveCodedeploydeploy

若要从现有组织中检索类和触发器, 使用检索 Ant 任务,如示例构建目标所示:ant retrieveCode

<target name="retrieveCode">
   <!-- Retrieve the contents listed in the file codepkg/package.xml into the codepkg directory -->
   <sf:retrieve username="${sf.username}" password="${sf.password}" 
        serverurl="${sf.serverurl}" retrieveTarget="codepkg" unpackaged="codepkg/package.xml"/>
</target>

文件 codepkg/package.xml 列出了元数据 要检索的组件。在此示例中,它检索两个类 和一个触发器。检索到的文件被放入目录 codepkg 中,覆盖目录中已有的所有内容。

检索任务的属性如下:

描述
用户名如果 sessionId 不是,则为必填项 指定。用于登录的 Salesforce 用户名。与此关联的用户名 连接必须具有通过元数据 API 函数修改元数据 许可。
密码如果 sessionId 不是,则为必填项 指定。用于登录与此关联的组织的密码 项目。如果您使用的是安全令牌,请将 25 位令牌值粘贴到 密码末尾。
会话 ID如果未指定用户名和密码,则为必填项。活动 Salesforce 会话或 OAuth 的 ID 访问令牌。用户成功登录 Salesforce 后将创建会话 使用用户名和密码。使用会话 ID 登录到现有会话 而不是创建新会话。或者,将访问令牌用于 OAuth 认证。有关更多信息,请参阅使用 Salesforce 帮助中的 OAuth。
服务器网址自选。Salesforce 服务器 URL(如果为空, 默认值为 )。连接 更改为沙盒实例,请将此值更改为 。login.salesforce.comtest.salesforce.com
检索目标必填。目录结构的根目录 元数据文件被检索到其中。
包名称如果未指定 unpackaged,则为必填项。逗号分隔 要检索的包的名称列表。指定 packageNames 或 unpackaged,但不指定 双。
api版本自选。要用于 检索到的元数据文件。默认值为 59.0。
轮询WaitMillis自选。缺省值为 .数量 轮询检索结果时两次尝试之间等待的毫秒数 请求。客户端继续轮询服务器,直至达到 maxPoll 定义的限制。10000
最大投票自选。缺省值为 .轮询服务器以获取结果的次数 检索请求。200连续轮询尝试之间的等待时间由 pollWaitMillis 定义。
单包自选。缺省值为 .设置此参数 以检索多个包。 如果设置为 ,则检索到的 zip 文件包括 额外的顶级目录,包含每个包的子目录。truefalsefalse
跟踪自选。缺省值为 . 将 SOAP 请求和响应打印到控制台。此选项显示用户的 登录时以纯文本形式输入密码。false
未包装如果未指定 packageNames,则为必需。路径和名称 指定要检索的组件的文件清单。指定 unpackaged 或 packageNames,但不能同时指定两者。
解 压缩自选。缺省值为 .如果设置为 ,则检索到的 组件已解压缩。如果设置为 ,则 检索到的组件将作为 zip 文件保存在 retrieveTarget 目录中。truetruefalse

使用 SOAP API 部署Apex

这些 Salesforce 对象和 SOAP API 调用和标头是 默认情况下可用于 Apex。有关所有其他 SOAP API 调用的信息,包括那些 可用于扩展或实施任何现有的 Apex IDE,请联系您的 Salesforce 代表。

Apex 类方法可以公开为自定义 SOAP Web 服务调用。这允许外部 应用程序调用 Apex Web 服务以在 Salesforce 中执行操作。使用关键字来定义这些方法。为 有关详细信息,请参阅使用 webservice 关键字的注意事项。webservice使用 SOAP API 调用保存的任何 Apex 代码都使用与端点相同的 SOAP API 版本 的请求。例如,如果要使用 SOAP API 版本 59.0 ,请使用端点 59.0:

https://MyDomain.salesforce.com/services/Soap/s/59.0

这些 Salesforce 对象可用于 Apex。

  • ApexTestQueueItem
  • ApexTestResult(阿佩克斯测试结果)
  • ApexTestResultLimits
  • ApexTestRunResult

使用这些 SOAP API 调用来部署 Apex。

  • compileAndTest()
  • compileClasses()
  • compileTriggers()
  • executeanonymous()
  • runTests()

所有这些调用都采用包含类或触发器的 Apex 代码,以及 需要设置的任何字段的值。这些 SOAP 标头在 Apex 的 SOAP API 调用中可用。

  • DebuggingHeader
  • PackageVersionHeader

另请参阅元数据 API 开发人员指南,了解另外两个调用:

  • deploy()
  • retrieve()

使用托管软件包分发 Apex

作为 ISV 或 Salesforce 合作伙伴,您可以将 Apex 代码分发给客户 使用包的组织。在这里,我们将介绍包和包 版本控制。

重要

如果一个类有一个 依赖于 Chatter,代码可以编译并安装在没有 启用 Chatter。但是,如果未启用 Chatter,则代码会在运行时抛出错误 时间。请参阅打包 ConnectApi 类。ConnectApi

  1. 什么是套餐?
  2. 软件包版本
  3. 弃用 Apex
  4. 包版本中的行为

什么是套餐?

是一个容器,用于容纳小到单个组件或 大作为一组相关应用。创建包后,可以将其分发给其他 Salesforce 用户和组织,包括公司外部的用户和组织。组织可以 创建一个可由多个不同用户下载和安装的托管软件包 组织。托管包与非托管包的不同之处在于具有一些锁定的组件, 允许以后升级托管包。非托管包不包括锁定的包 组件,并且无法升级。

软件包版本

包版本是一个数字,用于标识包中上载的组件集。这 版本号的格式为 (对于 例如,2.1.3)。在每个主要数字中,主要数字和次要数字增加到选定的值 释放。仅针对补丁版本生成和更新。majorNumber.minorNumber.patchNumberpatchNumber

非托管包不可升级,因此每个包版本只是一组组件 用于分发。包版本对于托管包具有更重要的意义。包 对于不同的版本,可以表现出不同的行为。发布者可以使用包 版本,通过发布来优雅地发展其托管包中的组件 后续软件包版本,而不会破坏现有客户集成,使用 包。

当现有订阅者安装新的包版本时,仍然只有一个实例 包中的每个组件,但这些组件可以模拟旧版本。为 例如,订阅者可以使用包含 Apex 类的托管包。如果 发布者决定弃用 Apex 类中的方法并发布一个新包 版本,订阅者在安装后仍然只能看到 Apex 类的一个实例 新版本。但是,此 Apex 类仍然可以模拟任何 在旧版本中引用已弃用方法的代码。

在托管软件包中开发 Apex 时,请注意以下事项:

  • 包含在 Apex 类、触发器或 Visualforce 组件中的代码,该组件是 托管软件包被模糊处理,无法在安装组织中查看。唯一的例外是 声明为 global 的方法。您可以在安装组织中查看全局方法签名。在 此外,具有“查看和调试托管 Apex”权限的许可证管理组织用户可以查看 当通过订阅者登录订阅者组织时,他们的软件包会混淆 Apex 类 支持控制台。
  • 托管包接收唯一的命名空间。此命名空间在类名之前, 方法、变量等,这有助于防止安装程序组织中出现重复名称。
  • 在单个事务中,只能引用 10 个唯一命名空间。例如,假设 您有一个对象,该对象在更新对象时执行托管包中的类。 然后,该类更新第二个对象,该对象又在不同的 包。即使第一个包没有直接访问第二个包,访问 发生在同一事务中。因此,它包含在 单笔交易。
  • 包开发人员可以使用已弃用的注释来标识 无法再引用的方法、类、异常、枚举、接口和变量 在它们所在的托管包的后续版本中。这在以下情况下很有用 随着需求的发展重构托管包中的代码。
  • 可以编写测试方法,将包版本上下文更改为其他包 版本,使用系统方法。runAs
  • 在以下情况下,不能将方法添加到全局接口,也不能将抽象方法添加到全局类 接口或类已在托管 – 已发布包版本中上传。如果 Managed – Released 包中的类是虚拟的,您可以添加该方法 它也必须是虚拟的,并且必须有一个实现。如果 托管 – 发布包扩展了另一个类,现有类协定不能 被删除。
  • 非托管包中包含的 Apex 代码 显式引用命名空间无法上传。

弃用 Apex

包开发人员可以使用已弃用的注释来标识 无法再引用的方法、类、异常、枚举、接口和变量 在它们所在的托管包的后续版本中。这在以下情况下很有用 随着需求的发展重构托管包中的代码。 将另一个包版本上传为托管版本后 – 已发布,安装最新包版本的新订阅者看不到已弃用的 元素,而这些元素继续为现有订阅者和 API 运行 集成。弃用的项(如方法或类)仍可引用 由包开发人员在内部进行。

注意

您不能在 Apex 类中使用注释,也不能在非托管包中使用触发器。deprecated

包开发人员可以将托管 – Beta 包版本用于 对不同 Salesforce 组织中的一组试点用户进行评估和反馈。如果 开发人员弃用 Apex 标识符,然后将包的版本上传为托管 – Beta 版,安装包版本的订阅者仍会在其中看到已弃用的标识符 包版本。如果包开发人员随后上传托管 – 已发布的包版本, 订阅者在订阅后将不再在包版本中看到已弃用的标识符 安装它。

包版本中的行为

包组件可以在不同的 包版本。此行为版本控制允许您添加新组件 到您的包装中并优化您现有的组件,同时仍然确保 您的代码继续为现有订阅者无缝工作。 如果包开发人员将新组件添加到包中并上传 新的软件包版本,新组件可供订阅者使用 安装新的软件包版本。

  1. 对 Apex 代码行为进行版本控制
  2. 未进行版本控制的 Apex 代码项
  3. 在包版本中测试行为

对 Apex 代码行为进行版本控制

包开发人员可以在 Apex 类和触发器中使用条件逻辑 以显示不同版本的不同行为。条件逻辑允许包 开发人员支持以前包版本中的类和触发器中的现有行为 同时改进代码。

当订阅者安装包的多个版本并编写引用 Apex 的代码时 包中的类或触发器,它们必须选择 他们引用的版本。在 包中引用的 Apex 代码,您可以有条件地执行 基于调用 Apex 代码的版本设置的不同代码路径 进行引用。调用代码的包版本设置可以是 通过调用该方法在包代码中确定。通过这种方式,包开发人员可以 确定请求上下文并为不同版本指定不同的行为 的包。System.requestVersion

以下示例使用该方法并实例化该类,以在 Apex 触发器中为不同的包版本定义不同的行为。System.requestVersionSystem.Version

trigger oppValidation on Opportunity (before insert, before update) {

    for (Opportunity o : Trigger.new){
    
        // Add a new validation to the package
        // Applies to versions of the managed package greater than 1.0
        if (System.requestVersion().compareTo(new Version(1,0)) > 0) {
            if (o.Probability >= 50 && o.Description == null) {
                o.addError('All deals over 50% require a description');
            }
        }

        // Validation applies to all versions of the managed package.
        if (o.IsWon == true && o.LeadSource == null) {
            o.addError('A lead source must be provided for all Closed Won deals');
        }
    }
}

有关使用包版本的方法的完整列表,请参阅 Version 类和 System Class 中的方法。System.requestVersion

如果已安装包中的某个类调用另一个包中的方法,则会保留请求上下文 类。例如,订阅者安装了一个 GeoReports 包,该包 包含 CountryUtil 和 ContinentUtil Apex 类。订阅者创建一个新的 GeoReportsEx 类,并使用版本设置将其绑定到 2.3 版 GeoReports 包。如果 GeoReportsEx 在 ContinentUtil 中调用内部的方法 调用 CountryUtil 中的方法,请求上下文从 ContinentUtil 传播到 CountryUtil 和方法 在 CountryUtil 中返回 GeoReports 包的 2.3 版本。System.requestVersion

未进行版本控制的 Apex 代码项

您可以跨包版本更改某些 Apex 项目的行为。例如,您可以 弃用某个方法,以便新订阅者无法再在后续中引用该包 版本。

但是,无法对以下修饰符、关键字和批注列表进行版本控制。如果 包开发人员对以下修饰符、关键字或注释之一进行更改, 这些更改将反映在所有包版本中。

当这些项目出现以下情况时,您可以对其中某些项目进行的更改存在限制 在托管包的 Apex 代码中使用。

包开发人员可以添加或删除以下项:

  • @future
  • @isTest
  • with sharing
  • without sharing
  • transient

包开发人员可以对以下项进行有限的更改:

  • private—可以更改为global
  • public—可以更改为global
  • protected—可以更改为global
  • abstract– 可以更改为,但不能删除virtual
  • final– 可以删除,但不能删除 添加

程序包开发人员无法删除或更改以下项:

  • global
  • virtual

包开发人员可以添加关键字, 但是一旦添加,就无法删除。

webservice

注意

不能弃用托管方法或变量 软件包代码。webservice

在包版本中测试行为

当您更改不同包版本的 Apex 类或触发器中的行为时,这很重要 测试代码在不同包版本中是否按预期运行。您可以编写更改包的测试方法 使用系统将上下文版本更改为不同的包版本 方法。runAs您可以 仅在测试中使用 方法。runAs

以下示例显示了具有不同行为的触发器 不同的包版本。

trigger oppValidation on Opportunity (before insert, before update) {

    for (Opportunity o : Trigger.new){
    
        // Add a new validation to the package
        // Applies to versions of the managed package greater than 1.0
        if (System.requestVersion().compareTo(new Version(1,0)) > 0) {
            if (o.Probability >= 50 && o.Description == null) {
                o.addError('All deals over 50% require a description');
            }
        }

        // Validation applies to all versions of the managed package.
        if (o.IsWon == true && o.LeadSource == null) {
            o.addError('A lead source must be provided for all Closed Won deals');
        }
    }
}

以下测试类使用该方法验证触发器的行为(有和没有) 特定版本:runAs

@isTest
private class OppTriggerTests{

   static testMethod void testOppValidation(){
   
      // Set up 50% opportunity with no description
      Opportunity o = new Opportunity();
      o.Name = 'Test Job';
      o.Probability = 50;
      o.StageName = 'Prospect';
      o.CloseDate = System.today();
      
      // Test running as latest package version
      try{
          insert o;
      }
      catch(System.DMLException e){
          System.assert(
              e.getMessage().contains(
                'All deals over 50% require a description'),
                  e.getMessage());
      }
      
      // Run test as managed package version 1.0
      System.runAs(new Version(1,0)){
          try{
              insert o;
          }
          catch(System.DMLException e){
              System.assert(false, e.getMessage());
          }
      }

      // Set up a closed won opportunity with no lead source
      o = new Opportunity();
      o.Name = 'Test Job';
      o.Probability = 50;
      o.StageName = 'Prospect';
      o.CloseDate = System.today();
      o.StageName = 'Closed Won';
      
      // Test running as latest package version
      try{
          insert o;
      }
      catch(System.DMLException e){
          System.assert(
            e.getMessage().contains(
              'A lead source must be provided for all Closed Won deals'),
                e.getMessage());
      }

      // Run test as managed package version 1.0
      System.runAs(new Version(1,0)){
          try{
              insert o;
          }
          catch(System.DMLException e){
              System.assert(
                  e.getMessage().contains(
                    'A lead source must be provided for all Closed Won deals'),
                        e.getMessage());
          }
      }
   }
}

测试 Apex

Apex 提供了一个测试框架,允许您编写单元测试,运行 测试,检查测试结果,并有代码覆盖率结果。

让我们来谈谈单元测试、测试的数据可见性以及可用的工具 在 Lightning 平台上用于测试 Apex。我们还将介绍测试最佳实践 以及一个测试示例。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个 Exception 子类,其中包含保存个人数据的新属性。然后 不要在异常的消息字符串中包含子类属性信息。

  • 了解 Apex 中的测试
  • 在 Apex 中测试什么
  • 什么是 Apex 单元测试?
  • 了解测试数据
    Apex 测试数据是暂时性的,不会提交到数据库。
  • 运行单元测试方法
    若要验证 Apex 代码的功能,请执行单元测试。您可以在开发人员控制台、设置、Visual Studio Code 的 Salesforce 扩展中或使用 API 运行 Apex 测试方法。
  • 测试最佳实践
  • 测试实例
  • 测试和代码覆盖率
    Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并在您每次运行一个或多个测试时触发。代码覆盖率指示测试方法已执行类和触发器中有多少行可执行代码。编写测试方法来测试触发器和类,然后运行这些测试以生成代码覆盖率信息。
  • 代码覆盖率最佳实践 请考虑以下代码覆盖率提示和最佳实践
  • 使用 Stub API 构建模拟框架 Apex 提供了一个用于实现模拟框架的存根 API
    。模拟框架有很多好处。它可以简化和改进测试,并帮助您创建更快、更可靠的测试。您可以使用它来单独测试类,这对于单元测试非常重要。使用存根 API 构建模拟框架也是有益的,因为存根对象是在运行时生成的。由于这些对象是动态生成的,因此不必打包和部署测试类。您可以构建自己的模拟框架,也可以使用其他人构建的模拟框架。

了解 Apex 中的测试

测试是长期成功发展的关键,是一种 开发过程的关键组成部分。强烈建议使用测试驱动开发过程,即同时进行的测试开发 时间即代码开发。

为什么要测试 Apex?

测试是应用程序成功的关键,尤其是当您的应用程序要 部署到客户。如果验证应用程序是否按预期工作,则有 没有意外行为,您的客户会更加信任您。

有两种方法可以测试应用程序。一种是通过Salesforce用户界面, 很重要,但仅仅通过用户界面进行测试并不能捕获所有用例 您的应用程序。另一种方法是测试批量功能:最多可以测试 200 条记录 如果代码是使用 SOAP API 或 Visualforce 标准集调用的,则通过代码进行传递 控制器。

应用程序很少完成。您将拥有它的其他版本,您可以在其中进行更改 并扩展功能。如果你已经编写了全面的测试,你可以确保 回归不会随任何新功能一起引入。在为 Salesforce 部署代码或打包代码之前 AppExchange,则必须满足以下条件。

  • 单元测试必须至少覆盖 75% 的 Apex 代码,并且所有这些测试都必须 成功完成。请注意以下事项。
    • 将 Apex 部署到生产组织时,每个单元测试 默认情况下,将执行 Organization 命名空间。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计为 Apex 代码的一部分 覆盖。
    • 虽然只有 75% 的 Apex 代码必须被测试覆盖,但不要专注于 所覆盖代码的百分比。相反,请确保每次使用 涵盖您的申请案例,包括正面和负面案例, 以及批量和单条记录。这种方法可确保 75% 或更多 的代码包含在单元测试中。
  • 每个触发器都必须具有一定的测试覆盖率。
  • 所有类和触发器都必须成功编译。

Salesforce 在具有 Apex 代码的所有组织中运行所有测试,以验证没有行为 已因任何服务升级而更改。

在 Apex 中测试什么

Salesforce 建议您为以下内容编写测试:单次操作测试以验证单个记录是否生成正确的预期结果。批量操作任何 Apex 代码,无论是触发器、类还是扩展,都可以为 1 到 200 调用 记录。您不仅必须测试单个记录案例,还必须测试批量案例。积极行为测试以验证预期行为是否通过每个预期的排列发生,即 是,用户正确填写了所有内容并且没有超出限制。负面行为您的应用程序可能存在限制,例如无法添加未来日期、 无法指定负数,依此类推。您必须测试阴性情况 并验证错误消息是否正确生成,以及对于正错误消息,在 限制情况。受限用户测试对代码中使用的 sObject 具有受限访问权限的用户是否看到 预期行为。也就是说,它们是否可以运行代码或接收错误消息。

注意

条件运算符和三元运算符不被视为执行,除非两者都为正 并执行负分支。

有关这些类型的测试的示例,请参阅测试 示例

什么是 Apex 单元测试?

为了促进健壮、无错误的代码的开发,Apex 支持创建和 执行单元测试。单元测试是验证 特定代码段工作正常。单元测试方法不带任何参数, 不向数据库提交任何数据,也不发送电子邮件。此类方法在方法定义中使用注释进行标记。 单元测试方法必须在测试类中定义,即用 批注的类。@IsTest@IsTest例如:

@IsTest
private class myClass {
    @IsTest
    static void myTest() {
        // code_block
    }
}

使用注解定义类和 仅包含用于测试应用程序的代码的方法。注释可以采用多个修饰符 括号内,并用空格分隔。@IsTest@IsTest

注意

上的注释 methods 等同于关键字。如 最佳做法,Salesforce 建议您使用 而不是 .关键字可以在 未来版本。@IsTesttestMethod@IsTesttestMethodtestMethod

此测试类示例包含两个测试方法。

@IsTest
private class MyTestClass {

   // Methods for testing
   @IsTest
   static void test1() {
      // Implement test code
   }

   @IsTest
   static void test2() {
      // Implement test code
   }

}

类和方法定义为 can be 或 。测试类方法的访问级别 无所谓。在定义测试类或 测试方法。Apex 中的默认访问级别是私有的。测试框架可以 始终找到测试方法并执行它们,无论其访问级别如何。@IsTestprivatepublic

定义为必须是顶级的类 类,不能是接口或枚举。@IsTest

测试类的方法只能从测试方法或测试调用的代码中调用 方法;非测试请求无法调用它。

此示例显示要测试的类及其对应的测试类。它包含两个 方法和构造函数。

public class TVRemoteControl {
    // Volume to be modified
    Integer volume;
    // Constant for maximum volume value
    static final Integer MAX_VOLUME = 50;    
    
    // Constructor
    public TVRemoteControl(Integer v) {
        // Set initial value for volume
        volume = v;
    }
        
    public Integer increaseVolume(Integer amount) {
        volume += amount;
        if (volume > MAX_VOLUME) {
            volume = MAX_VOLUME;
        } 
        return volume;
    }
    
    public Integer decreaseVolume(Integer amount) {
        volume -= amount;
        if (volume < 0) {
            volume = 0;
        }  
        return volume;
    }    
    
    public static String getMenuOptions() {
        return 'AUDIO SETTINGS - VIDEO SETTINGS';
    }
       
}

此示例包含具有四个测试方法的相应测试类。中的每个方法 调用上一个类。尽管有足够的测试覆盖率,但测试 Test 类中的方法执行额外的测试以验证边界条件。

@IsTest
class TVRemoteControlTest {
    @IsTest 
    static void testVolumeIncrease() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(15);
        System.assertEquals(25, newVolume);
    }
    
    @IsTest
    static void testVolumeDecrease() {
        TVRemoteControl rc = new TVRemoteControl(20);
        Integer newVolume = rc.decreaseVolume(15);
        System.assertEquals(5, newVolume);        
    } 
        
    @IsTest
    static void testVolumeIncreaseOverMax() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(100);
        System.assertEquals(50, newVolume);        
    }
    
    @IsTest
    static void testVolumeDecreaseUnderMin() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.decreaseVolume(100);
        System.assertEquals(0, newVolume);        
    }
    
    @IsTest
    static void testGetMenuOptions() {
        // Static method call. No need to create a class instance.
        String menu = TVRemoteControl.getMenuOptions();
        System.assertNotEquals(null, menu);
        System.assertNotEquals('', menu);
    }
}

单元测试注意事项

以下是有关单元测试的一些注意事项。

  • 从 Salesforce API 28.0 开始,测试方法不能再驻留在非测试中 类,并且必须是用 注释的类的一部分。请参阅 TestVisible 注解以了解 如何从测试类访问私有类成员。IsTest
  • 测试方法不能用于测试 Web 服务标注。相反,请使用 模拟标注。请参阅测试 Web 服务标注和测试 HTTP 标注。
  • 不能从测试方法发送电子邮件。
  • 由于测试方法不会提交在测试中创建的数据,因此 不必在完成后删除测试数据。
  • 如果测试类中静态成员变量的值在 testSetup 或 test 方法,则不会保留新值。其他测试方法 此类获取静态成员变量的原始值。此行为 当静态成员变量在另一个类中定义时也适用,并且 在测试方法中访问。
  • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。
  • Chatter 摘要中记录(FeedTrackedChange 记录)的跟踪更改不是 当测试方法修改关联记录时可用。FeedTracked更改 记录要求对与其关联的父记录进行更改 在创建数据库之前提交到数据库。由于测试方法没有 提交数据,它们不会导致创建 FeedTrackedChange 记录。 同样,无法在测试方法中创建字段历史记录跟踪记录 因为它们需要首先提交其他 sObject 记录。例如 无法在测试方法中创建 AccountHistory 记录,因为 Account 记录 必须首先提交。
  • 如果您的测试包括 DML,请确保不超过MAX_DML_ROWS 限制。请参阅执行调控器和限制中的“其他顶点限制”
  1. 访问私有测试类成员

访问私有测试类成员

测试方法是在测试类中定义的,与它们测试的类分开。这可以 当必须从测试中访问私有类成员变量时出现问题 方法,或者在调用私有方法时。因为这些是私有的,所以它们不是 对测试类可见。您可以修改类中的代码以公开公共 将使用这些私有类成员的方法,或者您可以简单地注释 这些带有 . 当您使用此批注对私有成员或受保护成员进行批注时,它们可以 由测试方法访问,并且仅在测试上下文中运行代码。TestVisible

此示例说明如何与 私有成员变量,带有构造函数的私有内部类,私有方法, 和私有自定义例外。所有这些都可以在测试类中访问,因为 它们带有 注释。这 类首先列出,后跟一个包含测试方法的测试类。TestVisibleTestVisible

public class VisibleSampleClass {
    // Private member variables
    @TestVisible private Integer recordNumber = 0;
    @TestVisible private String areaCode = '(415)';
    // Public member variable
    public Integer maxRecords = 1000;
    
    // Private inner class
    @TestVisible class Employee {
        String fullName;
        String phone;
        
        // Constructor
        @TestVisible Employee(String s, String ph) {
            fullName = s;
            phone = ph;
        }
    }
       
    // Private method
    @TestVisible private String privateMethod(Employee e) {
        System.debug('I am private.');
        recordNumber++;
        String phone = areaCode + ' ' + e.phone;
        String s = e.fullName + '\'s phone number is ' + phone;
        System.debug(s);
        return s;
    }
    
    // Public method
    public void publicMethod() {
        maxRecords++;
        System.debug('I am public.');    
    }
    
    // Private custom exception class
    @TestVisible private class MyException extends Exception {}
}
// Test class for VisibleSampleClass
@isTest
private class VisibleSampleClassTest {

    // This test method can access private members of another class 
    // that are annotated with @TestVisible.
    static testmethod void test1() {
        VisibleSampleClass sample = new VisibleSampleClass ();

        // Access private data members and update their values
        sample.recordNumber = 100;
        sample.areaCode = '(510)';
        
        // Access private inner class
        VisibleSampleClass.Employee emp = 
            new VisibleSampleClass.Employee('Joe Smith', '555-1212');
        
        // Call private method
        String s = sample.privateMethod(emp);
        
        // Verify result
        System.assert(
            s.contains('(510)') &&
            s.contains('Joe Smith') &&
            s.contains('555-1212'));
    }
    
    // This test method can throw private exception defined in another class
    static testmethod void test2() {
        // Throw private exception.
        try {
            throw new VisibleSampleClass.MyException('Thrown from a test.');
        } catch(VisibleSampleClass.MyException e) {
            // Handle exception 
        }
    }
    
    static testmethod void test3() {
        // Access public method.
        // No @TestVisible is used.
        VisibleSampleClass sample = new VisibleSampleClass ();
        sample.publicMethod();
    }   

}

当您 升级 Salesforce API 包含混合测试代码和非测试代码的现有类的版本。因为测试 从 API 版本 28.0 开始,非测试类中不允许使用方法,您必须 升级 API 版本 时,将测试方法从旧类移动到新的测试类(用 批注的类) 你的班级。在访问私有方法或 原始类的成员变量。在这种情况下,只需注释这些私有 具有 .TestVisibleisTestTestVisible

了解测试数据

Apex 测试数据是暂时性的,不会提交到数据库。

这意味着在测试方法完成执行后,测试插入的数据 不会保留在数据库中。因此,无需删除任何测试数据 测试的结论。同样,对现有记录的所有更改,例如更新或 删除,不要持久。测试数据的这种瞬态行为使得管理 数据更容易,因为您不必执行任何测试数据清理。同时,如果你 测试访问组织数据,这可以防止意外删除或修改现有数据 记录。

默认情况下,现有组织数据对测试方法不可见,但例外 某些设置对象。应尽可能为测试方法创建测试数据。 但是,针对 Salesforce API 版本 23.0 或更早版本保存的测试代码可以访问组织中的所有数据。数据可见性 下一节将更详细地介绍测试。

  • 在单元测试中将测试数据与组织数据隔离
  • 使用 isTest(SeeAllData=True) 批注 批注
    测试类或测试方法,以打开对组织中记录的数据访问。IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建或更改,包括删除。即使在使用注释时,新记录和更改的记录仍会在 Apex 测试中回滚。IsTest(SeeAllData=true)
  • 加载测试数据 使用该方法,可以在测试方法中填充数据
    ,而无需编写许多代码行。Test.loadData
  • 用于创建
    测试数据的公共测试实用程序类 公共测试实用程序类是公共测试类,其中包含用于创建测试数据的可重用代码。
  • 使用测试设置方法 使用测试设置方法(用 批注的方法)创建一次测试记录,然后在测试类中的每个测试方法
    中访问它们。当您需要为所有测试方法创建参考或先决条件数据,或者需要为所有测试方法操作的一组通用记录创建时,测试设置方法可以节省时间。@testSetup

将测试数据与单元中的组织数据隔离 测试

默认情况下,Apex 测试方法(API 版本 24.0 及更高版本)不能 访问预先存在的组织数据,例如标准对象、自定义对象和自定义设置 数据。他们只能访问他们创建的数据。但是,用于管理 仍然可以在测试中访问您的组织或元数据对象。这些是一些 此类对象的示例。

  • 用户
  • 轮廓
  • 组织
  • Cron触发器
  • 记录类型
  • 顶点类
  • Apex触发器
  • Apex组件
  • ApexPage(顶点页面)

尽可能为每个测试创建测试数据。您可以通过以下方式禁用此限制 使用注释注释测试类或测试方法。IsTest(SeeAllData=true)

使用 Salesforce API 版本 23.0 或更早版本保存的测试代码 继续有权访问组织中的所有数据,其数据访问权限为 变。数据访问注意事项

  • 使用数据孤岛 Apex 测试时,不支持使用该关系的跨对象字段引用。由于 此限制在数据孤岛 Apex 测试中运行时返回 null。OwnerSELECT Owner.IsActive FROM Account
  • 如果使用 Salesforce API 版本 24.0 或更高版本保存的新测试方法调用方法 在使用版本 23.0 或更低版本保存的另一个类中,数据访问限制 调用方在被调用的方法中强制执行。调用的方法无法访问 组织数据,因为调用方无法访问它,即使它保存在 早期版本。
  • 注解具有 添加到使用 Salesforce API 版本 23.0 保存的 Apex 代码时不起作用,并且 早些时候。IsTest(SeeAllData=true)
  • 此对测试数据的访问限制适用于所有代码 在测试上下文中运行。例如,如果测试方法导致触发器执行 并且测试无法访问组织数据,触发器将无法 也。
  • 如果测试发出 Visualforce 请求,则执行的测试将停留在测试上下文中,但 在不同的线程中运行。因此,不再强制执行测试数据隔离。在 在这种情况下,测试将能够访问组织中的所有数据 发起 Visualforce 请求。但是,如果 Visualforce 请求执行 callback,例如 JavaScript remoting 调用,则回调插入的任何数据都不是 对测试可见。
  • 在 API 版本 27.0 及更早版本中,VLOOKUP 验证规则函数始终查找 当由正在运行的 Apex 测试触发时,除了测试数据外,还会向上组织数据。开头为 版本 28.0,VLOOKUP 验证规则函数不再访问组织 来自正在运行的 Apex 测试的数据。该函数仅查找测试创建的数据, 除非测试类或方法用 注释。IsTest(SeeAllData=true)
  • 在某些情况下,您可能无法从测试中创建某些类型的数据 由于特定的限制,方法。以下是此类限制的一些示例。
    • 某些标准对象不可创建。有关这些对象的详细信息, 请参阅对象参考 Salesforce的。
    • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。无论您的测试是否被批注,都会发生此错误 有 ,或没有。IsTest(SeeAllData=true)
    • 仅在将相关记录提交到 数据库,例如 Chatter 中跟踪的更改。记录的跟踪更改 (FeedTrackedChange 记录)在测试方法时不可用 修改关联的记录。FeedTrackedChange 记录需要更改为 之前要提交到数据库的与它们关联的父记录 它们被创造出来。由于测试方法不提交数据,因此它们不会导致 创建 FeedTrackedChange 记录。同样,字段历史记录跟踪记录 无法在测试方法中创建,因为它们需要其他 sObject 记录 首先承诺。例如,无法在测试中创建 AccountHistory 记录 方法,因为必须首先提交帐户记录。

使用 isTest(SeeAllData=True) 注解

对测试类或测试方法进行批注,以打开对组织中记录的数据访问。这 IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建 或更改,包括删除。新记录和更改的记录仍会在 Apex 测试中回滚 即使在使用注释时。

IsTest(SeeAllData=true)

警告

通过用 注释类,可以允许测试方法访问所有组织记录。这 但是,最佳做法是使用 .取决于 API 版本 您正在使用时,默认注释可能会有所不同。@isTest(SeeAllData=true)@isTest(SeeAllData=false)此示例演示如何使用注释定义测试类。所有 此类中的测试方法可以访问 组织。

@IsTest(SeeAllData=true)

// All test methods in this class can access all data.
@IsTest(SeeAllData=true)
public class TestDataAccessClass {

    // This test accesses an existing account. 
    // It also creates and accesses a new test account.
    @IsTest
    static void myTestMethod1() {
        // Query an existing account in the organization. 
        Account a = [SELECT Id, Name FROM Account WHERE Name='Acme' LIMIT 1];
        System.assert(a != null);
        
        // Create a test account based on the queried account.
        Account testAccount = a.clone();
        testAccount.Name = 'Acme Test';
        insert testAccount;
        
        // Query the test account that was inserted.
        Account testAccount2 = [SELECT Id, Name FROM Account 
                                WHERE Name='Acme Test' LIMIT 1];
        System.assert(testAccount2 != null);
    }
       
    
    // Like the previous method, this test method can also access all data
    // because the containing class is annotated with @IsTest(SeeAllData=true).
    @IsTest
    static void myTestMethod2() {
        // Can access all data in the organization.
   }
  
}

第二个示例演示如何在测试上应用注释 方法。由于测试方法的类没有注释,因此必须注释 方法,以便能够访问该方法的所有数据。第二种测试方法没有 具有此注释,因此它只能访问它创建的数据。此外,它还可以 访问用于管理组织的对象,例如 用户。

@IsTest(SeeAllData=true)

// This class contains test methods with different data access levels.
@IsTest
private class ClassWithDifferentDataAccess {

    // Test method that has access to all data.
    @IsTest(SeeAllData=true)
    static void testWithAllDataAccess() {
        // Can query all data in the organization.      
    }
    
    // Test method that has access to only the data it creates
    // and organization setup and metadata objects.
    @IsTest
    static void testWithOwnDataAccess() {
        // This method can still access the User object.
        // This query returns the first user object.
        User u = [SELECT UserName,Email FROM User LIMIT 1]; 
        System.debug('UserName: ' + u.UserName);
        System.debug('Email: ' + u.Email);
        
        // Can access the test account that is created here.
        Account a = new Account(Name='Test Account');
        insert a;      
        // Access the account that was just created.
        Account insertedAcct = [SELECT Id,Name FROM Account 
                                WHERE Name='Test Account'];
        System.assert(insertedAcct != null);
    }
}

注释的注意事项@IsTest(SeeAllData=true)

  • 如果测试类是用注解定义的,则 未显式设置关键字的测试方法。@IsTest(SeeAllData=true)SeeAllData=trueSeeAllData
  • 注解用于打开 在类或方法级别应用时的数据访问。但是,如果 包含类已用 、 对于方法,将忽略对方法进行批注。 在这种情况下,该方法仍然可以访问 组织。使用重写对方法进行注释,对于该方法,对 类。@IsTest(SeeAllData=true)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)
  • @IsTest(SeeAllData=true)并且不能使用注释 一起使用相同的 Apex 方法。@IsTest(IsParallel=true)

加载测试数据

使用该方法,可以在测试方法中填充数据,而无需 编写多行代码。

Test.loadData请按照下列步骤操作:

  1. 在 .csv 文件中添加数据。
  2. 为此文件创建静态资源。
  3. 在测试中调用 方法,并向其传递 sObject 类型令牌和静态资源名称。Test.loadData

例如,对于帐户记录和静态资源名称 ,进行以下调用:

myResource

List<sObject> ls = Test.loadData(Account.sObjectType, 'myResource');

方法 返回与插入的每条记录相对应的 sObject 列表。Test.loadData

在调用此方法之前,必须创建静态资源。 静态资源是以逗号分隔的文件,以 .csv 扩展名结尾。 该文件包含测试记录的字段名称和值。这 文件的第一行必须包含字段名称,随后的行 行是字段值。若要了解有关静态资源的详细信息,请执行以下操作: 请参阅 Salesforce 联机帮助中的“定义静态资源”。创建后 .csv 文件的静态资源,静态资源将是 分配了 MIME 类型。支持的 MIME 类型包括:

  • 文本/CSV
  • 应用程序/vnd.ms-excel
  • application/octet-stream
  • 文本/纯文本

Test.loadData 示例

以下是步骤 用于创建示例 .csv 文件和静态资源,并调用以插入测试记录。

Test.loadData

  1. 创建一个包含测试记录数据的 .csv 文件。此示例 .csv 文件有三个 帐户记录。您可以使用此示例内容创建 .csv 文件。Name,Website,Phone,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry sForceTest1,http://www.sforcetest1.com,(415) 901-7000,The Landmark @ One Market,San Francisco,CA,94105,US sForceTest2,http://www.sforcetest2.com,(415) 901-7000,The Landmark @ One Market Suite 300,San Francisco,CA,94105,US sForceTest3,http://www.sforcetest3.com,(415) 901-7000,1 Market St,San Francisco,CA,94105,US
  2. 为 .csv 文件创建静态资源:
    1. 在“设置”中,输入“快速查找”框,然后选择“静态资源”。Static Resources
    2. 单击“新建”。
    3. 将静态资源命名为 。testAccounts
    4. 选择您创建的文件。
    5. 点击保存
  3. 呼入 用于填充测试帐户的测试方法。Test.loadData@isTest private class DataUtil { static testmethod void testLoadData() { // Load the test accounts from the static resource List<sObject> ls = Test.loadData(Account.sObjectType, 'testAccounts'); // Verify that all 3 test accounts were created System.assert(ls.size() == 3); // Get first test account Account a1 = (Account)ls[0]; String acctName = a1.Name; System.debug(acctName); // Perform some testing using the test records } }

测试数据的通用测试实用程序类 创造

常见的测试实用程序类是公共测试类,它们 包含用于创建测试数据的可重用代码。

公共测试实用程序类使用注释进行定义,因此被排除在组织代码大小限制之外并执行 在测试上下文中。它们可以由测试方法调用,但不能由非测试代码调用。IsTest

公共测试实用程序类中的方法的定义方式与非测试中的方法相同 类。它们可以接受参数并返回值。必须声明这些方法 作为公共或全局,以便对其他测试类可见。这些常用方法可以是 由 Apex 类中的任何测试方法调用,以在运行 测试。虽然您可以在常规 Apex 中创建用于创建测试数据的公共方法 类,如果没有注解,你 不要从组织代码大小中排除此代码的好处 限制。IsTest

这是测试实用程序类的一个示例。它包含一个方法,该方法接受 要创建的帐户数和每个帐户的联系人数。 下一个示例演示一个测试方法,该方法调用此方法以创建 一些数据。createTestRecords

@IsTest
public class TestDataFactory {
    public static void createTestRecords(Integer numAccts, Integer numContactsPerAcct) {
        List<Account> accts = new List<Account>();
        
        for(Integer i=0;i<numAccts;i++) {
            Account a = new Account(Name='TestAccount' + i);
            accts.add(a);
        }
        insert accts;
        
        List<Contact> cons = new List<Contact>();
        for (Integer j=0;j<numAccts;j++) {
            Account acct = accts[j];            
            // For each account just inserted, add contacts
            for (Integer k=numContactsPerAcct*j;k<numContactsPerAcct*(j+1);k++) {
                cons.add(new Contact(firstname='Test'+k,
                                     lastname='Test'+k,
                                     AccountId=acct.Id));
            }
        }
        // Insert all contacts for all accounts
        insert cons;
    }
}

此类中的测试方法调用测试实用程序方法 ,以创建 5 个 测试帐户,每个帐户有三个联系人。createTestRecords

@IsTest
private class MyTestClass {
    static testmethod void test1() {
        TestDataFactory.createTestRecords(5,3);
        // Run some tests
    }
}

使用测试设置方法

使用测试设置方法(带有 注释的方法)创建一次测试记录,然后再创建一次 在测试类的每个测试方法中访问它们。测试设置方法可以是 当您需要为所有测试方法创建参考或先决条件数据时,可以节省时间, 或所有测试方法都操作的一组通用记录。@testSetup

测试设置方法可以减少测试执行时间,尤其是在 您正在处理许多记录。测试设置方法使您能够创建通用测试 轻松高效地获取数据。通过为班级设置一次记录,您不需要 为每个测试方法重新创建记录。此外,因为回滚的记录 在测试设置期间创建,发生在整个类执行结束时, 减少回滚的记录数。因此,系统资源是 与创建这些记录并回滚这些记录相比,使用效率更高 每种测试方法。

如果测试类包含测试设置方法,则测试 Framework 首先执行测试设置方法,然后再执行类中的任何测试方法。 在测试设置方法中创建的记录可用于 测试类,并在测试类执行结束时回滚。如果测试方法 更改这些记录,例如记录字段更新或记录删除,这些更改 在每个测试方法完成执行后回滚。下一个执行测试 方法获取对这些记录的原始未修改状态的访问。

语法

测试设置方法是在测试类中定义的,不带任何参数,也不返回 价值。以下是测试设置方法的语法。

@testSetup static void methodName() {

}

以下示例演示如何创建一次测试记录,然后在 多种测试方法。此外,该示例还显示了如何在 第一个测试方法将回滚,并且不适用于第二个测试 方法。

@isTest
private class CommonTestSetup {

    @testSetup static void setup() {
        // Create common test accounts
        List<Account> testAccts = new List<Account>();
        for(Integer i=0;i<2;i++) {
            testAccts.add(new Account(Name = 'TestAcct'+i));
        }
        insert testAccts;        
    }
    
    @isTest static void testMethod1() {
        // Get the first test account by using a SOQL query
        Account acct = [SELECT Id FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Modify first account
        acct.Phone = '555-1212';
        // This update is local to this test method only.
        update acct;
        
        // Delete second account
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // This deletion is local to this test method only.
        delete acct2;
        
        // Perform some testing
    }

    @isTest static void testMethod2() {
        // The changes made by testMethod1() are rolled back and 
        // are not visible to this test method.        
        // Get the first account by using a SOQL query
        Account acct = [SELECT Phone FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Verify that test account created by test setup method is unaltered.
        System.assertEquals(null, acct.Phone);
        
        // Get the second account by using a SOQL query
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // Verify test account created by test setup method is unaltered.
        System.assertNotEquals(null, acct2);
        
        // Perform some testing
    }

}

测试设置方法注意事项

  • 测试设置方法仅支持 测试类的默认数据隔离模式。如果测试类或测试 方法可以使用注释访问组织数据, 此类不支持测试设置方法。因为数据隔离 for tests 可用于 API 版本 24.0 及更高版本的测试设置方法 也仅适用于这些版本。@isTest(SeeAllData=true)
  • 每个测试类只能有一种测试设置方法。
  • 如果在执行测试设置方法期间发生致命错误,例如 由 DML 操作或断言失败导致的异常,整个 测试类失败,并且不会执行该类中的其他测试。
  • 如果测试设置方法调用另一个类的非测试方法,则无代码 覆盖率是针对非测试方法计算的。

运行单元测试方法

若要验证 Apex 代码的功能,请执行单元测试。您可以运行 Apex 在开发人员控制台、设置和 Salesforce Visual 扩展中测试方法 Studio Code,或使用 API。可以运行这些单元测试分组。

  • 特定类中的部分或全部方法
  • 一组类中的部分或全部方法
  • 预定义的类套件,称为测试套件
  • 组织中的所有单元测试

若要运行测试,请使用以下任一方法:

  • Salesforce 用户界面
  • 适用于 Visual Studio 的 Salesforce 扩展 法典
  • 闪电平台开发人员 安慰
  • The API

从 Salesforce 用户界面启动的所有 Apex 测试(包括 Developer Console)异步并行运行。Apex 测试类放置在 用于执行的 Apex 作业队列。可以运行的最大测试类数 每 24 小时是 500 或 10 乘以测试类数的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高,并且是 500 或 20 中的较大值乘以组织中的测试类数。

注意

作为部署的一部分运行的 Apex 测试始终同步运行,并且 串行。

通过 Salesforce 用户运行测试 接口

您可以在 Apex 测试执行页面上运行单元测试。在此页面上开始测试 异步运行,也就是说,您不必等待测试类执行 完成。“Apex Test Execution”(Apex 测试执行)页面刷新测试状态,并显示 测试完成后的结果。

Apex 测试执行页面
  1. 在“设置”中,输入“快速查找”框,然后选择“Apex 测试” 执行Apex Test Execution
  2. 单击“选择测试…”。注意如果你 具有从托管包安装的 Apex 类,您必须 首先通过单击“全部编译”来编译这些类 Apex 类页面上的类,以便它们显示在 列表。
  3. 选择要运行的测试。测试列表仅包括 包含测试方法。
    • 若要从已安装的托管包中选择测试,请选择 下拉列表中托管包的相应命名空间。 仅具有所选命名空间的托管包的类 出现在列表中。
    • 若要选择组织中本地存在的测试,请从下拉列表中选择“[我的命名空间]”。 只有不是来自托管包的本地类才会出现在 列表。
    • 要选择任何测试,请从下拉列表中选择[所有命名空间]。将显示组织中的所有类, 无论它们是否来自托管包。
    注意当前正在运行测试的类不会出现在 列表。
  4. 若要选择不在测试运行期间收集代码覆盖率信息,请选择“跳过代码覆盖率”。
  5. 单击运行

使用 Apex Test Execution 页面运行测试后,您可以查看代码覆盖率 开发者控制台中的详细信息。

在“设置”中,输入“快速查找”框,选择“Apex 测试执行”,然后单击“查看” 测试历史记录,用于查看组织的所有测试结果,而不是 只是您运行的测试。测试结果在测试后保留 30 天 完成运行,除非清除。Apex

使用 Salesforce 扩展运行测试 针对 Visual Studio Code

可以使用 Visual Studio Code 执行测试。请参阅适用于 Visual Studio 的 Salesforce 扩展 代码。

使用 Lightning 运行测试 平台开发人员控制台

在开发者控制台中,您可以在特定的测试类中执行部分或全部测试, 设置和运行测试套件,或运行所有测试。开发者控制台运行测试 在后台异步运行,除非测试运行仅包含一个类和 你尚未在“测试”菜单中选择“始终异步运行”。 通过异步运行测试,您可以在开发者控制台的其他区域工作 在测试运行时。测试完成执行后,可以检查测试 结果在开发者控制台中。此外,还可以检查整体代码覆盖率 对于测试涵盖的课程。

有关更多信息,请参阅 Salesforce 中的开发人员控制台文档 帮助。

使用 API 运行测试

您可以使用来自 SOAP 的调用 用于运行测试的 API 同步。

runTests()

RunTestsResult[] runTests(RunTestsRequest ri)

此调用允许您运行所有类中的所有测试,以及特定类中的所有测试 命名空间,或指定特定命名空间中类子集中的所有测试 在 RunTestsRequest 对象中。它返回以下内容。

  • 运行的测试总数
  • 代码覆盖率统计
  • 每个失败测试的错误信息
  • 每个成功的测试的信息
  • 运行测试所花费的时间

有关 的更多信息,请参见 SOAP API 中的 runTests() 开发人员指南。runTests()

还可以使用工具 REST API 运行测试。使用 和 终结点运行测试 异步或同步。有关用法的详细信息,请参阅工具 API:REST 资源。/runTestsAsynchronous//runTestsSynchronous/

使用 ApexTestQueueItem 运行测试

可以使用 ApexTestQueueItem 和 ApexTestResult 异步运行测试。这些对象允许您 将测试添加到 Apex 作业队列,并检查已完成的测试运行的结果。 此过程不仅使你能够异步启动测试,还使你能够安排 使用 Apex 调度程序在特定时间执行的测试。有关详细信息,请参阅 Apex Scheduler。

插入一个对象以放置其 Apex 作业队列中相应的 Apex 类以供执行。Apex 作业执行 类中的测试方法。作业执行后,包含每个测试方法的结果 作为测试的一部分执行。ApexTestQueueItemApexTestResult

要中止 Apex 作业队列中的类,请在 ApexTestQueueItem 对象,并将其 Status 字段设置为 。Aborted

如果在单个批量操作中插入多个 Apex 测试队列项,则队列 项目共享相同的父作业。这意味着测试运行可以包括 如果插入了所有测试队列项,则执行多个类的测试 在相同的批量操作中。

可以在 Apex 作业队列是 500 或 10 乘以测试类数中的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高 并且是 500 或 20 中的较大值乘以 组织。此示例使用 DML 操作插入和查询 和 对象。该方法为以 测试。然后,它返回一个队列项的父作业 ID,该 ID 对于所有队列项都是相同的 对项目进行排队,因为它们是批量插入的。该方法检索对应于 指定的作业 ID。然后,它查询并输出名称、作业状态和传递 每个班级的费率。该方法获取作为 工作。

ApexTestQueueItemApexTestResultenqueueTestscheckClassStatuscheckMethodStatus

public class TestUtil {

    // Enqueue all classes ending in "Test". 
    public static ID enqueueTests() {
        ApexClass[] testClasses = 
           [SELECT Id FROM ApexClass 
            WHERE Name LIKE '%Test'];
        if (testClasses.size() > 0) {
            ApexTestQueueItem[] queueItems = new List<ApexTestQueueItem>();
            for (ApexClass cls : testClasses) {
                queueItems.add(new ApexTestQueueItem(ApexClassId=cls.Id));
            }

            insert queueItems;

            // Get the job ID of the first queue item returned.
            ApexTestQueueItem item = 
               [SELECT ParentJobId FROM ApexTestQueueItem 
                WHERE Id=:queueItems[0].Id LIMIT 1];
            return item.parentjobid;
        }
        return null;
    }

    // Get the status and pass rate for each class
    // whose tests were run by the job.
    // that correspond to the specified job ID.
    public static void checkClassStatus(ID jobId) {
        ApexTestQueueItem[] items = 
           [SELECT ApexClass.Name, Status, ExtendedStatus 
            FROM ApexTestQueueItem 
            WHERE ParentJobId=:jobId];
        for (ApexTestQueueItem item : items) {
            String extStatus = item.extendedstatus == null ? '' : item.extendedStatus;
            System.debug(item.ApexClass.Name + ': ' + item.Status + extStatus);
        }
    }

    // Get the result for each test method that was executed.
    public static void checkMethodStatus(ID jobId) {
        ApexTestResult[] results = 
           [SELECT Outcome, ApexClass.Name, MethodName, Message, StackTrace 
            FROM ApexTestResult 
            WHERE AsyncApexJobId=:jobId];
        for (ApexTestResult atr : results) {
            System.debug(atr.ApexClass.Name + '.' + atr.MethodName + ': ' + atr.Outcome);
            if (atr.message != null) {
                System.debug(atr.Message + '\n at ' + atr.StackTrace);
            }
        }
    }
}
  1. 使用 runAs 方法
  2. 使用 Limits、startTest 和 stopTest
  3. 将 SOSL 查询添加到单元测试

使用 runAs 方法

通常,所有 Apex 代码都运行在系统模式下,其中权限和记录共享 的当前用户不被考虑在内。使用系统方法,可以编写更改的测试方法 现有用户或新用户的用户上下文,以便用户的记录 强制执行共享。该方法强制执行 记录 共享。 用户权限和字段级权限将应用于新的上下文用户,因为 在强制执行对象和字段权限中进行了描述。runAsrunAs

注意

无论测试类的共享模式如何,都会在块内强制执行用户的共享权限。如果 在块中调用用户定义的方法, 强制执行的共享模式是定义方法的类的共享模式。runAsrunAs

您只能在测试方法中使用。这 完成所有测试方法后,将再次启动原始系统上下文。runAsrunAs

该方法忽略用户许可证限制。你 能 创造 即使你的组织没有 其他用户许可证。runAsrunAs

注意

每个调用都很重要 相对于流程中发出的 DML 语句总数。runAs

在以下示例中,将创建一个新的测试用户,然后以该用户身份运行代码,其中 该用户的记录共享访问权限:

@isTest
private class TestRunAs {
   public static testMethod void testRunAs() {
        // Setup test data
        // Create a unique UserName
        String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';
        // This code runs as the system user
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
        EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
        LocaleSidKey='en_US', ProfileId = p.Id,
        TimeZoneSidKey='America/Los_Angeles',
         UserName=uniqueUserName);

        System.runAs(u) {
              // The following code runs as user 'u'
              System.debug('Current User: ' + UserInfo.getUserName());
              System.debug('Current Profile: ' + UserInfo.getProfileId());
          }
    }
}

可以嵌套多个方法。为 例:runAs

@isTest
private class TestRunAs2 {

   public static testMethod void test2() {

      Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
      User u2 = new User(Alias = 'newUser', Email='newuser@testorg.com',
         EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
         LocaleSidKey='en_US', ProfileId = p.Id,
         TimeZoneSidKey='America/Los_Angeles', UserName='newuser@testorg.com');

      System.runAs(u2) {
         // The following code runs as user u2.
         System.debug('Current User: ' + UserInfo.getUserName());
         System.debug('Current Profile: ' + UserInfo.getProfileId());

         // The following code runs as user u3.
         User u3 = [SELECT Id FROM User WHERE UserName='newuser@testorg.com'];
         System.runAs(u3) {
            System.debug('Current User: ' + UserInfo.getUserName());
            System.debug('Current Profile: ' + UserInfo.getProfileId());
         }

         // Any additional code here would run as user u2.
      }
   }
}

runAs 的其他用途

您还可以使用该方法来执行 通过将 DML 操作封闭在块中,在测试中混合 DML 操作。这样,您就可以绕过混合 DML 在插入或更新安装对象时返回的错误 其他 sObjects。请参见不能一起使用的 sObjects 在 DML 操作中。runAsrunAs

runAs 方法 (runAs(System.Version)) 的另一个重载将包版本作为参数。这 方法导致使用特定版本的托管包的代码。为 有关使用 runAs 方法和 指定包版本上下文,请参阅测试包中的行为 版本。

使用 Limits、startTest 和 stopTest

Limits 方法返回特定调控器的特定限制,例如数字 方法的调用次数或剩余的堆大小量。

每种方法都有两个版本。第一个版本返回具有 在当前上下文中使用。第二个版本包含“限制”一词 并返回可用于该上下文的资源总量。例如,将标注数返回到 已在当前上下文中处理的外部服务,而返回标注的总数 在给定的上下文中可用。getCalloutsgetLimitCallouts

除了 Limits 方法之外,还可以使用 和 方法来验证代码的接近程度 是达到调速器限制。startTeststopTest

该方法标记测试中的点 在测试实际开始时编写代码。每个测试方法只允许调用此方法 一次。此方法之前的所有代码都应用于初始化变量、填充数据 结构等,允许您设置运行测试所需的一切。任何代码 在调用之后执行,并在调用之前执行,分配了一组新的调控器 限制。startTeststartTeststopTest

该方法不会刷新 测试:它为测试添加上下文。例如,如果您的类进行 98 次 SOQL 查询 在它调用之前,第一个重要的 后面的语句是 DML 语句, 程序现在可以进行额外的 100 次查询。但是,一旦被调用,程序就会返回到原始上下文,并且 在达到 100 个限制之前,只能进行 2 个额外的 SOQL 查询。startTeststartTeststartTeststopTest

当测试结束时,该方法在测试代码中标记该点。stopTest在 与方法结合使用。每次测试 方法只允许调用此方法一次。在为方法分配原始限制后执行的任何代码 实际上之前被调用了。都 在该方法之后进行的异步调用是 由系统收集。startTeststopTeststartTeststartTest执行时,将运行所有异步进程 同步。stopTest

将 SOSL 查询添加到单元测试

为确保测试方法始终以可预测的方式运行,任何 Salesforce 对象 添加到 Apex 测试方法的搜索语言 (SOSL) 查询将返回一组空的 测试方法执行时的搜索结果。如果您不希望查询返回 结果列表为空,您可以使用系统方法定义记录 ID 列表,该 由搜索返回。稍后在测试方法中发生的所有 SOSL 查询 返回方法指定的记录 ID 列表。此外,测试方法可以多次调用 为不同的 SOSL 查询定义不同的结果集。如果未在测试方法中调用该方法,或者 如果在未指定记录 ID 列表的情况下调用此方法,则任何 SOSL 查询 place later in the test 方法返回一个空的结果列表。Test.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResults

该方法指定的记录 ID 列表将替换通常 如果 SOSL 查询不受任何 OR 子句的约束,则由 SOSL 查询返回。如果这些 子句存在于 SOSL 查询中,它们应用于固定搜索结果列表。为 例:Test.setFixedSearchResultsWHERELIMIT

@isTest
private class SoslFixedResultsTest1 {

    public static testMethod void testSoslFixedResults() {
       Id [] fixedSearchResults= new Id[1];
       fixedSearchResults[0] = '001x0000003G89h';
       Test.setFixedSearchResults(fixedSearchResults);
       List<List<SObject>> searchList = [FIND 'test' 
                                         IN ALL FIELDS RETURNING 
                                            Account(id, name WHERE name = 'test' LIMIT 1)];
    }
}

注意

ContentDocument (File) 或 ContentNote (Note) 实体的 SOSL 查询需要与 ContentVersion ID 一起使用,以便与 Salesforce 索引的方式保持一致 并搜索文件和笔记。setFixedSearchResults

尽管 ID 为 的客户记录可能与 FIND 子句 () 中的查询字符串不匹配,但该记录将传递到 SOSL 语句的子句中。如果记录 如果 ID 与子句筛选器匹配,则返回记录。如果是这样 不匹配子句,无记录 返回。001x0000003G89h‘test’RETURNING001x0000003G89hWHEREWHERE

测试最佳实践

好的测试可以做到以下几点:

  • 覆盖尽可能多的代码行。在部署 Apex 或 将其打包为 AppExchange,则必须满足以下条件。重要
    • 单元测试必须覆盖至少 75% 的 Apex 代码,以及所有 这些测试必须成功完成。
    • 每个触发器都必须具有一定的测试覆盖率。
    • 所有类和触发器都必须成功编译。
    请注意以下事项。
    • 将 Apex 部署到生产组织时,每个 组织命名空间中的单元测试由 违约。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计入 Apex 代码覆盖率。
    • 虽然只有 75% 的 Apex 代码必须被 测试,不要关注代码的百分比 覆盖。相反,请确保你的每个用例 涵盖应用,包括正面和负面 案例,以及批量和单条记录。这种方法 确保 75% 或更多的代码被单元覆盖 测试。
  • 如果代码使用条件逻辑(包括三元运算符),请执行每个分支。
  • 使用有效和无效输入调用方法。
  • 成功完成而不会引发任何异常,除非这些错误是预期的 并被困在一个街区。try…catch
  • 始终处理捕获的所有异常,而不仅仅是捕获 异常。
  • 使用方法证明代码 行为正常。System.assert
  • 使用该方法测试应用程序 在不同的用户上下文中。runAs
  • 练习批量触发器功能 – 在测试中使用至少 20 条记录。
  • 使用关键字确保 记录按预期顺序返回。ORDER BY
  • 不假定记录 ID 按顺序排列。记录 ID 不是在 升序,除非您使用同一请求插入多条记录。例如 如果您创建了一个帐户 A,并接收了 ID,则创建帐户 B,则帐户 B 的 ID 可能会,也可能不会 依次走高。001D000000IEEmT
  • 设置测试数据:
    • 在测试类中创建必要的数据,以便测试不必依赖数据 在特定组织中。
    • 在调用方法之前创建所有测试数据。Test.startTest
    • 由于测试不提交,因此无需删除任何数据。
  • 写评论,不仅要说明应该测试的内容,还要说明假设 测试人员对数据、预期结果等进行了测试。
  • 单独测试应用程序中的类。永远不要测试你的整个应用程序 一次测试。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个具有新属性的 Exception 子类,用于保存个人数据。然后 不要在异常的消息字符串中包含子类属性信息。

如果要运行许多测试,请测试组织中的类 在 Salesforce 用户界面中单独使用,而不是使用“全部运行” “测试”按钮将它们一起运行。

并行测试执行的最佳实践

从 Salesforce 用户界面(包括开发人员控制台)启动的测试 并行运行。并行测试执行可以加快测试运行时间。有时,并行 测试执行会导致数据争用问题,您可以在 那些情况。具体而言,在以下情况下可能会发生数据争用问题和错误:

UNABLE_TO_LOCK_ROW

  • 当测试同时更新相同的记录时。更新相同的记录 通常在测试不创建自己的数据并关闭数据时发生 隔离以访问组织的数据。
  • 当并行运行并尝试创建 具有重复索引字段值的记录。当两个正在运行的测试发生时,会发生死锁 等待彼此回滚数据。如果插入两个测试,则可能会发生这种等待 以不同顺序具有相同唯一索引字段值的记录。

您可以通过在 Salesforce 用户界面:

  1. 在“设置”中,输入 。Apex Test
  2. 单击“选项…”。
  3. 在“Apex 测试执行选项”对话框中,选择“禁用并行 Apex” “测试”,然后单击“确定”。

带批注的测试类指示测试类可以同时运行,并且超过默认数量 并发测试类。此批注将覆盖默认设置。IsTest(IsParallel=true)

测试实例

以下示例包括以下类型的案例 测试:

  • 具有单条和多条记录的阳性病例
  • 单条和多条记录的负数大小写
  • 与其他用户一起测试

该测试与简单的里程跟踪应用程序一起使用。现有代码 应用程序验证一天内输入的里程不超过 500 英里。这 主要对象是名为 Mileage__c 的自定义对象。该测试创建一条包含 300 的记录 英里并验证仅记录了 300 英里。然后循环创建 200 条记录 每人一英里。最后,它验证总共记录了 500 英里( 原来的 300 加上新的)。这是整个测试类。以下各节 单步执行代码的特定部分。

@isTest
private class MileageTrackerTestSuite {

    static testMethod void runPositiveTestCases() {
        
        Double totalMiles = 0;
        final Double maxtotalMiles = 500;
        final Double singletotalMiles = 300;
        final Double u2Miles = 100;
  
        
        //Set up user
        User u1 = [SELECT Id FROM User WHERE Alias='auser'];
        
        //Run As U1
        System.RunAs(u1){

            
        System.debug('Inserting 300  miles... (single record validation)');
        
        Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today());
        insert testMiles1;
        
        //Validate single insert
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c 
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(singletotalMiles, totalMiles);
    
    
        //Bulk validation   
        totalMiles = 0; 
        System.debug('Inserting 200 mileage records... (bulk validation)');
        
        List<Mileage__c> testMiles2 = new List<Mileage__c>();
        for(integer i=0; i<200; i++) {
            testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) );
        }
        insert testMiles2;
       
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(maxtotalMiles, totalMiles);

        }//end RunAs(u1)


       //Validate additional user:
       totalMiles = 0;
       //Setup RunAs
       User u2 = [SELECT Id FROM User WHERE Alias='tuser'];
       System.RunAs(u2){
        
        Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today());
        insert testMiles3;
        
                for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u2.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        //Validate 
        Assert.areEqual(u2Miles, totalMiles);
        
       } //System.RunAs(u2)

      
    } // runPositiveTestCases()
   
    static testMethod void runNegativeTestCases() {

        User u3 = [SELECT Id FROM User WHERE Alias='tuser'];
        System.RunAs(u3) {

           System.debug('Inserting a record with 501 miles... (negative test case)'); 

           Mileage__c testMiles3 = new Mileage__c( Miles__c = 501, Date__c = System.today() );

           try {
               insert testMiles3;
               Assert.fail('DmlException expected');
           } catch (DmlException e) {
               //Assert Status Code
               Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0));
            
               //Assert field
               Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]);
                        
               //Assert Error Message
               Assert.isTrue(e.getMessage().contains(
                'Mileage request exceeds daily limit(500): [Miles__c]'),
                'DMLException did not contain expected validation message:' + e.getMessage() );
            
             } //catch
           } //RunAs(u3) 
    } // runNegativeTestCases() 
  
    
} // class MileageTrackerTestSuite

阳性测试用例

以下步骤通过上面的代码,特别是, 单条记录和多条记录的阳性测试用例。

  1. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 300 more miles...single record validation');
  2. 创建一个Mileage__c对象并将其插入到数据库中。Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today() ); insert testMiles1;
  3. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :createdbyId and miles__c != null]) { totalMiles += m.miles__c; }
  4. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(singletotalMiles, totalMiles);
  5. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  6. 通过创建 200 条记录的批量插入来验证代码。首先,在调试日志中添加文本,指示下一步 法典:System.debug('Inserting 200 Mileage records...bulk validation');
  7. 然后插入 200 条Mileage__c记录:List<Mileage__c> testMiles2 = new List<Mileage__c>(); for(Integer i=0; i<200; i++){ testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) ); } insert testMiles2;
  8. 用于验证预期结果 是 返回:Assert.areEqualfor(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :CreatedbyId and miles__c != null]) { totalMiles += m.miles__c; } Assert.areEqual(maxtotalMiles, totalMiles);

阴性测试用例

以下步骤通过上面的代码,特别是, 阴性测试用例。

  1. 创建一个名为 :runNegativeTestCasesstatic testMethod void runNegativeTestCases(){
  2. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 501 miles... negative test case');
  3. 创建 501 英里的Mileage__c记录。Mileage__c testMiles3 = new Mileage__c(Miles__c = 501, Date__c = System.today());
  4. 将语句放在 / 块中。这允许您捕获验证异常和 断言生成的错误消息。使用该方法明确断言您期望 验证异常。inserttrycatchAssert.failtry { insert testMiles3; Assert.fail('DmlException expected'); } catch (DmlException e) {
  5. 现在使用 和 方法进行测试。 将以下代码添加到之前的块中 创建:Assert.areEqualAssert.isTruecatch//Assert Status Code Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0)); //Assert field Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]); //Assert Error Message Assert.isTrue(e.getMessage().contains( 'Mileage request exceeds daily limit(500): [Miles__c]'), 'DMLException did not contain expected validation message:' + e.getMessage() );

以第二用户身份进行测试

以下步骤通过上面的代码,特别是, 以第二个用户身份运行。

  1. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  2. 设置下一个用户。User u2 = [SELECT Id FROM User WHERE Alias='tuser']; System.RunAs(u2){
  3. 将文本添加到调试日志中,指示代码的下一步:System.debug('Setting up testing - deleting any mileage records for ' + UserInfo.getUserName() + ' from today');
  4. 然后插入一条Mileage__c记录:Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today()); insert testMiles3;
  5. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :u2.Id and miles__c != null]) { totalMiles += m.miles__c; }
  6. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(u2Miles, totalMiles);

测试和代码覆盖率

Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并且 每次运行一个或多个测试时触发。代码覆盖率指示有多少可执行行 类和触发器中的代码已由测试方法执行。将测试方法写入 测试触发器和类,然后运行这些测试以生成代码覆盖率 信息。

测试方法涵盖的顶点触发器和类

测试方法涵盖的顶点触发器和类

除了确保代码的质量外,单元测试还使您能够满足代码要求 部署或打包 Apex 的覆盖率要求。要部署 Apex 或将其打包为 Salesforce AppExchange,单元测试必须至少覆盖 75% 的 Apex 代码,并且这些测试 必须通过。

代码覆盖率是测试有效性的一个指标,但不能保证测试 有效性。测试的质量也很重要,但您可以使用代码覆盖率作为工具来 评估是否需要添加更多测试。虽然您需要满足最低代码覆盖率 部署或打包 Apex 代码的要求,代码覆盖率不应是唯一的目标 您的测试。测试应断言应用的行为,并确保代码的质量。

代码覆盖率是如何计算的?

代码覆盖率百分比是计算 覆盖线除以覆盖线数和未覆盖线数之和。只 包括可执行代码行。(注释和空行不计算在内。 语句和大括号在以下情况下被排除在外 它们单独出现在一行上。一行上的多个语句计为一行 代码覆盖率的目的。如果语句由多个表达式组成,这些表达式写在 多行,每行都计入代码覆盖率。System.debug()

下面是一个具有一个方法的类的示例。这门课的测试是 run,并在开发人员控制台中为此类选择了显示代码覆盖率的选项。 蓝线表示测试覆盖的线。未突出显示的行 被排除在代码覆盖率计算之外。红线表示未覆盖的线 通过测试。为了实现全面覆盖,需要更多的测试。测试必须使用不同的输入进行调用,并验证返回的 价值。getTaskPriority()

这是测试方法部分涵盖的类。对应的测试类 未显示。

开发者控制台中具有代码覆盖率视图的示例类

测试类(带有 注释的类) 从代码覆盖率计算中排除。此排除项适用于所有测试类 无论它们包含什么 – 用于测试的测试方法或实用方法。@isTest

注意

Apex 编译器有时会优化语句中的表达式。例如,如果多个 字符串常量与运算符 编译器在内部将这些表达式替换为一个字符串常量。如果字符串 串联表达式位于单独的行上,附加行不计为 优化后的代码覆盖率计算。为了说明这一点,字符串变量是 分配给两个连接的字符串常量。第二个字符串常量位于 分开 线。+

String s = 'Hello'
    + ' World!';
String s = 'Hello World!';

检查代码覆盖率

运行测试后,可以在“开发人员”的“测试”选项卡中查看代码覆盖率信息 安慰。代码覆盖率窗格包括每个 Apex 类的覆盖率信息,以及 全面覆盖组织中的所有 Apex 代码。

此外,代码覆盖率存储在两个 Lightning Platform Tooling API 对象中: ApexCodeCoverageAggregate 和 ApexCodeCoverage。ApexCodeCoverageAggregate 存储 在检查测试类的所有测试方法后,为该类覆盖了行。ApexCodeCoverage 存储 每个单独的测试方法覆盖和未覆盖的线。出于这个原因,一个 类在 ApexCodeCoverage 中可以有多个覆盖率结果,每个测试方法一个覆盖率结果 这已经测试了它。可以使用 SOQL 和工具 API 查询这些对象以检索 覆盖范围信息。将 SOQL 查询与工具 API 结合使用是检查代码的另一种方法 覆盖范围和获取更多详细信息的快速方法。

例如,此 SOQL 查询获取类的代码覆盖率。覆盖率是从执行 此类中的方法。TaskUtil

SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered 
FROM ApexCodeCoverageAggregate 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

注意

此 SOQL 查询需要工具 API。您可以使用查询编辑器运行此查询 ,然后选中 Use Tooling API

下面是测试部分覆盖的类的示例查询结果:

ApexClassOrTrigger.NameNumLines覆盖NumLines未覆盖
任务实用性82

下一个示例演示如何确定哪些测试方法涵盖了该类。查询 从存储覆盖范围的其他对象 ApexCodeCoverage 获取覆盖率信息 按测试类和方法列出的信息。

SELECT ApexTestClass.Name,TestMethodName,NumLinesCovered,NumLinesUncovered 
FROM ApexCodeCoverage 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

下面是一个示例查询结果。

ApexTestClass.NameTestMethodName (测试方法名称)NumLines覆盖NumLines未覆盖
TaskUtilTesttestTaskPriority(测试任务优先级)73
TaskUtilTesttestTaskHighPriority(测试任务高优先级)64

ApexCodeCoverage 中的 NumLinesUncovered 值与 聚合结果为 ApexCodeCoverageAggregate,因为它们表示与 测试方法各。例如,测试方法覆盖了整个班级的 7 行,总共 10 行 行,因此未覆盖的行数为 3 行 (10–7)。因为聚合覆盖率存储在 ApexCodeCoverageAggregate 包括所有测试方法的覆盖率,其中 和 的覆盖率被包括在内,只剩下 2 行未被覆盖 任何测试方法。testTaskPriority()testTaskPriority()testTaskPriority()testTaskHighPriority()

代码覆盖率最佳实践

请考虑以下代码覆盖率提示和最佳做法。

代码覆盖率一般提示

  • 运行测试以刷新代码覆盖率。在以下情况下不会刷新代码覆盖率数字 除非重新运行测试,否则将对组织中的 Apex 代码进行更新。
  • 如果组织自上次测试运行以来已更新,则代码覆盖率估计可以 不正确。重新运行 Apex 测试以获得正确的估计值。
  • 组织中的总体代码覆盖率百分比不包括 托管包测试。唯一的例外是托管包测试导致触发器 火灾。有关更多信息,请参见托管包测试。
  • 覆盖率基于组织中的代码行总数。添加或删除 代码行更改覆盖率百分比。例如,假设一个组织有 50 个 测试方法涵盖的代码行。如果添加的触发器包含 50 行代码,则不 通过测试,代码覆盖率从 100% 下降到 50%。触发器增加 组织中的总代码行从 50 行到 100 行,其中只有 50 行由 测试。

为什么沙盒和生产环境之间的代码覆盖率数字不同

当 Apex 部署到生产环境或作为软件包的一部分上传到 Salesforce 时 AppExchange、Salesforce 在目标组织中运行本地测试。沙盒和生产 环境通常不包含相同的数据和元数据,因此代码覆盖率结果不会 始终匹配。如果代码在生产环境中的覆盖率低于 75%,请增加覆盖率以便能够 以部署或上传代码。以下是代码差异的常见原因 开发或沙盒环境与生产环境之间的覆盖率。这 信息可以帮助您排查和协调这些差异。测试失败如果一个环境中的测试结果不同,则总体代码覆盖率百分比 不匹配。在比较沙盒和生产环境之间的代码覆盖率之前,请使 确保要部署或打包的代码的所有测试都在组织中通过 第一。参与代码覆盖率计算的测试必须全部通过 部署或包上传。数据依赖关系如果测试使用批注访问组织数据,则测试结果可以 根据组织中可用的数据而有所不同。如果 测试中引用的记录不存在或已更改,测试失败或 在 Apex 方法中执行不同的代码路径。修改测试,以便 他们创建测试数据,而不是访问组织数据。@IsTest(SeeAllData=true)元数据依赖关系元数据中的更改(例如用户配置文件设置中的更改)可能会导致测试 失败或执行不同的代码路径。确保沙盒和生产环境中的元数据 匹配,或确保元数据更改不是导致不同测试执行的原因 行为。托管包测试在用户界面中运行所有 Apex 测试后计算的代码覆盖率,例如 开发人员控制台,可能与在 部署。如果运行所有测试,包括托管包测试,请在 用户界面,组织中的整体代码覆盖率没有 包括托管包代码的覆盖率。虽然托管包测试 覆盖托管包中的代码行,此覆盖率不是 组织的代码覆盖率计算为总行数和覆盖行数。 相比之下,在运行所有 测试通过测试 级别包括托管包代码的覆盖率。如果您运行的是托管 通过测试级别在部署中打包测试,建议运行此 首先在沙盒中部署或执行验证部署以验证 代码覆盖率。RunAllTestsInOrgRunAllTestsInOrg部署导致整体覆盖率低于 75%将具有 100% 覆盖率的新组件部署到生产环境时,部署失败 如果新代码和现有代码之间的平均覆盖率未达到 75% 的阈值。如果 在目标组织中测试运行返回的覆盖率结果小于 75%,修改 现有的测试方法或编写额外的测试方法以提高代码覆盖率 75%.单独部署修改后的或新的测试方法,或使用具有 100% 的新代码部署 覆盖。生产环境中的代码覆盖率降至 75% 以下有时,生产中的整体覆盖率会下降到75%以下,即使至少是 当组件从沙盒部署时,为 75%。依赖于 组织的数据和元数据可能会导致代码覆盖率下降。如果数据和元数据 已经进行了充分的更改以改变相关测试方法的结果,某些方法可能会失败 或行为不同。在这种情况下,某些行将不再被覆盖。

为生产环境匹配代码覆盖率数字的推荐过程

  • 使用完整沙盒作为生产部署的暂存沙盒环境。一个完整的 沙盒模拟生产中的元数据和数据,并有助于减少代码差异 两个环境之间的覆盖数。
  • 若要减少对沙盒和生产组织中数据的依赖性,请在 您的 Apex 测试。
  • 如果由于代码覆盖率不足而导致部署到生产环境失败,请编写更多测试 将整体代码覆盖率提高到尽可能高的覆盖率或 100%。重试 部署。
  • 如果在沙盒中提高代码覆盖率后部署到生产环境失败, 从生产组织运行本地测试。确定低于 75% 的类 覆盖。在沙盒中为这些类编写其他测试以提高代码覆盖率。

使用 Stub API 构建模拟框架

Apex 提供了一个用于实现模拟框架的存根 API。模拟框架 有很多好处。它可以简化和改进测试,并帮助您更快、更快速地创建更多 可靠的测试。您可以使用它来单独测试类,这对单元很重要 测试。使用存根 API 构建模拟框架也是有益的,因为 存根对象在运行时生成。由于这些对象是动态生成的,因此 不必打包和部署测试类。您可以构建自己的模拟框架,或者 您可以使用其他人构建的。

您可以定义存根对象的行为,这些对象在运行时创建为匿名对象 Apex 类的子类。存根 API 由接口和方法组成。System.StubProviderSystem.Test.createStub()

注意

此功能适用于高级 Apex 开发人员。使用它需要彻底 了解单元测试和模拟框架。如果你认为嘲笑 框架是取笑你的东西,你可能想多做一点 在进一步阅读之前进行研究。

让我们看一个示例来说明存根 API 的工作原理。此示例不是故意的 来演示模拟框架的广泛可能用途。它 有意简单专注于使用 Apex 存根 API 的机制。

假设我们想在下面的类中测试格式化方法。

public class DateFormatter {    
    // Method to test    
    public String getFormattedDate(DateHelper helper) {
        return 'Today\'s date is ' + helper.getTodaysDate();
    }
}

通常,当我们调用此方法时,我们会传入一个帮助程序类,该类具有一个方法 返回今天的日期。

public class DateHelper {   
    // Method to stub    
    public String getTodaysDate() {
        return Date.today().format();
    }
}

下面的代码调用该方法。

DateFormatter df = new DateFormatter();
DateHelper dh = new DateHelper();
String dateStr = df.getFormattedDate(dh);

为了进行测试,我们希望隔离该方法以确保格式有效 适当地。该方法的返回值通常因日期而异。然而,在这个 情况下,我们希望返回一个恒定的、可预测的值,以将我们的测试隔离到 格式。而不是编写类的“假”版本,该方法返回 一个常量值,我们创建类的存根版本。创建存根对象 在运行时动态,我们可以指定其方法的“存根”行为。getFormattedDate()getTodaysDate()要使用 Apex 类的存根版本,请执行以下操作:

  1. 通过实现接口来定义存根类的行为。System.StubProvider
  2. 使用该方法实例化存根对象。System.Test.createStub()
  3. 从测试类中调用存根对象的相关方法。

实现 StubProvider 接口

下面是接口的实现。StubProvider

@isTest
public class MockProvider implements System.StubProvider {
    
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
        Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
        List<Object> listOfArgs) {
        
        // The following debug statements show an example of logging 
        // the invocation of a mocked method.
       
        // You can use the method name and return type to determine which method was called.
        System.debug('Name of stubbed method: ' + stubbedMethodName);
        System.debug('Return type of stubbed method: ' + returnType.getName());
        
        // You can also use the parameter names and types to determine which method 
        // was called.
        for (integer i =0; i < listOfParamNames.size(); i++) {
            System.debug('parameter name: ' + listOfParamNames.get(i));
            System.debug('  parameter type: ' + listOfParamTypes.get(i).getName());
        }
        
        // This shows the actual parameter values passed into the stubbed method at runtime.
        System.debug('number of parameters passed into the mocked call: ' + 
            listOfArgs.size());
        System.debug('parameter(s) sent into the mocked call: ' + listOfArgs);
        
        // This is a very simple mock provider that returns a hard-coded value 
        // based on the return type of the invoked.
        if (returnType.getName() == 'String')
            return '8/8/2016';
        else 
            return null;
    }
}

StubProvider是一个回调接口。它 指定需要实现的单个方法:。当调用存根方法时,将调用。您可以定义 此方法中存根类的行为。该方法具有以下特点 参数。handleMethodCall()handleMethodCall()

  • stubbedObject:存根对象
  • stubbedMethodName:调用方法的名称
  • returnType:被调用方法的返回类型
  • listOfParamTypes:参数类型的列表 invoked 方法
  • listOfParamNames:参数名称的列表 invoked 方法
  • listOfArgs:传递到此参数的实际参数值 方法在运行时

您可以使用这些参数来确定调用了类的哪个方法,以及 然后,您可以定义每个方法的行为。在这种情况下,我们检查退货 用于标识它并返回硬编码值的方法的类型。

实例化类的存根版本

下一步是实例化该类的存根版本。以下实用程序 类返回一个可用作模拟的存根对象。

public class MockUtil {
    private MockUtil(){}

    public static MockProvider getInstance() {
        return new MockProvider();
    }
    
     public static Object createMock(Type typeToMock) {
        // Invoke the stub API and pass it our mock provider to create a 
        // mock class of typeToMock.
        return Test.createStub(typeToMock, MockUtil.getInstance());
    }
}

该类包含方法, 调用该方法。 该方法采用 Apex 类 类型和我们之前创建的接口的实例。它返回一个存根对象,我们可以在 测试。createMock()Test.createStub()createStub()StubProvider

调用 Stub 方法

最后,我们从测试中调用存根类的相关方法 类。

@isTest 
public class DateFormatterTest {   
    @isTest 
    public static void testGetFormattedDate() {
        // Create a mock version of the DateHelper class.
        DateHelper mockDH = (DateHelper)MockUtil.createMock(DateHelper.class);
        DateFormatter df = new DateFormatter();
        
        // Use the mocked object in the test.
        System.assertEquals('Today\'s date is 8/8/2016', df.getFormattedDate(mockDH));
    }
}

在这个测试中,我们调用 创建类的存根版本。然后,我们可以在存根对象上调用该方法,该方法返回我们的 硬编码日期。使用硬编码日期允许我们在 隔离。createMock()DateHelpergetTodaysDate()getFormattedDate()

Apex 存根 API 限制

使用 Apex 存根 API 时,请记住以下限制。

  • 被模拟的对象必须与对方法的调用位于同一命名空间中。但是, 接口的实现可以在另一个命名空间中。Test.createStub()StubProvider
  • 您无法模拟以下 Apex 元素。
    • 静态方法(包括将来的方法)
    • 私有方法
    • 属性(getter 和 setter)
    • 触发器
    • 内部类
    • 系统类型
    • 实现接口的类Batchable
    • 只有私有构造函数的类
  • 迭代器不能用作返回类型或参数类型。

Apex 中的异常

异常会记录中断正常代码流的错误和其他事件 执行。 语句用于 生成异常,而 、 和 语句用于从异常中正常恢复。throwtrycatchfinally

有许多方法可以处理代码中的错误,包括使用断言(如调用)或返回错误 代码或布尔值,那么为什么要使用异常呢?使用异常的优点是 它们简化了错误处理。异常从被调用的方法冒泡到 caller,根据需要设置多个级别,直到找到处理错误的语句。这种冒泡缓解了 您不必在每个方法中编写错误处理代码。此外,通过使用语句,您可以在一个位置进行恢复 从异常,例如重置变量和删除数据。System.assertcatchfinally

发生异常时会发生什么情况?

发生异常时,代码执行将停止。任何 在异常之前处理的 DML 操作将回滚,并且 未提交到数据库。异常记录在调试日志中。为 Salesforce 发送的未处理异常(代码未捕获的异常) 包含异常信息的电子邮件。最终用户看到错误 Salesforce 用户界面中的消息。

未经处理的异常电子邮件

当发生未经处理的 Apex 异常时,将发送电子邮件 包括 Apex 堆栈跟踪、异常消息以及客户的组织和用户 ID。报表不会返回其他数据。默认情况下,未处理的异常电子邮件会发送给指定的开发人员 在失败类的 LastModifiedBy 字段中,或者 触发。此外,您还可以将电子邮件发送给 Salesforce 的用户 组织和任意电子邮件地址。这些电子邮件收件人还可以 接收流程或流程错误电子邮件。要设置这些电子邮件通知, 从“设置”中,输入“快速查找”框,然后选择“Apex” 例外电子邮件。然后,输入的电子邮件地址将应用于 客户组织中的所有托管软件包。您还可以配置 Apex 使用 Tooling API 对象的异常电子邮件 Apex电子邮件通知。Apex Exception Email

注意

  • 如果同步运行的 Apex 代码中出现重复异常,或者 异步地,后续异常电子邮件将被禁止,并且只有 发送第一封电子邮件。此电子邮件抑制可防止泛滥 开发人员的收件箱中包含有关相同错误的电子邮件。
  • 对于匿名 Apex 遇到的异常,不会发送电子邮件 执行。
  • 每个应用程序每小时限制为 10 封电子邮件 Apex 例外电子邮件 服务器。由于此限制不是基于每个组织,因此电子邮件传递到 特定的组织可能不可靠。

用户界面中未处理的异常

如果最终用户在使用 标准用户界面,则显示错误消息。错误消息包含文本 与此处显示的通知类似。

“新商品”页面中未处理的异常
  • 异常语句
  • 异常处理示例
  • 内置异常和常用方法
  • 捕获不同的异常类型
  • 创建自定义例外

异常语句

Apex 使用异常来记录错误和其他事件 扰乱代码执行的正常流程。 语句可用于生成异常,而 、 和 可以 用于从异常中正常恢复。throwtrycatchfinally

抛出语句

语句允许您发出信号 发生错误。若要引发异常,请使用该语句并为其提供 一个 Exception 对象,用于提供有关特定错误的信息。 例如:

throwthrow

throw exceptionObject;

尝试-捕捉-最后 语句

、 和 语句可用于从引发的异常中正常恢复:

trycatchfinally

  • 该语句标识一个 可能发生异常的代码块。try
  • 该语句标识一个 可以处理特定类型异常的代码块。单个语句可以有零或 更多相关语句。 每个语句必须具有 唯一的异常类型。此外,一旦捕获到特定的异常类型 一个区块,其余区块(如果有的话), 不执行。catchtrycatchcatchcatchcatch
  • 该语句标识一个 保证执行的代码块,并允许您清理您的 法典。单个语句可以 最多有一个关联的语句。块中的代码 无论是否引发异常或类型如何,始终执行 引发的异常。由于该块始终执行,因此请将其用于清理代码,例如 至于释放资源。finallytryfinallyfinallyfinally

语法

、 和 语句的语法 如下所示。trycatchfinally

try {
  // Try block
 code_block
} catch (exceptionType variableName) {
  // Initial catch block.
  // At least the catch block or the finally block must be present. 
 code_block
} catch (Exception e) {
  // Optional additional catch statement for other exception types.
  // Note that the general exception type, 'Exception',
  // must be the last catch block when it is used.
 code_block
} finally {
  // Finally block.
  // At least the catch block or the finally block must be present.
 code_block
}

至少一个块或一个块必须与块一起存在。以下是 try-catch 块。catchfinallytry

try {
 code_block
} catch (exceptionType variableName) {
 code_block
}
// Optional additional catch blocks

以下是 try-finally 块的语法。

try {
 code_block
} finally {
 code_block
}

这是 try-catch-finally 的骨架示例 块。

try {
    // Perform some operation that 
    //   might cause an exception.
} catch(Exception e) {
    // Generic exception handling code here.
} finally {
    // Perform some clean up.
}

无法捕获的异常

某些特殊类型的内置异常无法捕获。这些例外与 闪电平台中的危急情况。这些情况需要堕胎 的代码执行,并且不允许通过异常恢复执行 处理。其中一个异常是限制异常 (),如果调控器限制具有 已超过,例如,当发出的最大 SOQL 查询数 超过。其他示例是断言语句失败时引发的异常 (通过方法)或许可证 异常。System.LimitExceptionSystem.assert

当异常无法捕获时,块以及块(如果有)不会捕获 执行。catchfinally

版本化行为更改

在 API 版本 41.0 及更高版本中,代码中的 unreachable 语句将导致 编译错误。例如,以下代码块生成编译时 API 版本 41.0 及更高版本中的错误。永远无法达到第三种说法 因为前面的语句抛出了一个无条件的 例外。

Boolean x = true; 
throw new NullPointerException();
x = false;

异常处理示例

若要查看操作中的异常,请执行一些导致引发 DML 异常的代码。 在 Developer 中执行以下命令 安慰:

Merchandise__c m = new Merchandise__c();
insert m;

DML 语句 在示例中导致 DmlException,因为我们插入 未设置任何必填字段的商品项。这 是您在调试日志中看到的异常错误。insert

System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Description, Price, Total Inventory]: [Description, Price, Total Inventory]接下来,在开发者控制台中执行此代码段。它基于前面的示例,但 包括一个 try-catch 块。

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

请注意,开发者控制台中的请求状态现在报告成功。这是因为 代码处理异常。

在异常之后发生的 try 块中的任何语句都是 跳过,不执行。例如,如果添加语句 之后,此语句 不会被执行。执行以下命令:insert m;

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
    // This doesn't execute since insert causes an exception
    System.debug('Statement after insert.');
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

在新的调试日志条目中,请注意,您没有看到 的调试消息。这是因为此调试语句发生在异常之后 由插入引起,并且永远不会被执行。继续执行 的代码语句,将语句放在 在 try-catch 块之后。执行这个修改后的代码片段,然后 请注意,调试日志现在具有 的调试消息。Statement after insertStatement after insert

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}
// This will get executed
System.debug('Statement after insert.');

或者,您可以包含其他 try-catch 块。这 代码片段在第二个 try-catch 块中具有该语句。执行它以查看 你得到和以前一样的结果。System.debug

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

try {
    System.debug('Statement after insert.');
    // Insert other records
}
catch (Exception e) {
    // Handle this exception here
}

无论出现什么异常,finally 块始终执行 被抛出,即使没有抛出异常。让我们来看看吧 在行动中使用。执行以下命令:

// Declare the variable outside the try-catch block
// so that it will be in scope for all blocks.
XmlStreamWriter w = null;
try {
    w = new XmlStreamWriter();
    w.writeStartDocument(null, '1.0');
    w.writeStartElement(null, 'book', null);
    w.writeCharacters('This is my book');
    w.writeEndElement(); 
    w.writeEndDocument();

    // Perform some other operations
    String s;
    // This causes an exception because
    // the string hasn't been assigned a value.
    Integer i = s.length();
} catch(Exception e) {
    System.debug('An exception occurred: ' + e.getMessage());
} finally {
    // This gets executed after the exception is handled
    System.debug('Closing the stream writer in the finally block.');
    // Close the stream writer
    w.close();
}

前面的代码片段创建一个 XML 流编写器,并将 一些 XML 元素。接下来,由于访问 null 字符串变量。这 catch 块处理此异常。然后执行 finally 块。 它写入调试消息并关闭流编写器,从而释放 任何关联的资源。检查调试日志中的调试输出。 异常后,你将看到调试消息 错误。这告诉您,在异常之后执行的 finally 块 被抓住了。sClosing the stream writer in the finally block.

内置异常和常用方法

Apex 提供了许多内置的异常类型,运行时引擎会在以下情况下抛出这些异常类型 在执行过程中遇到错误。你已在前面的 例。下面是一些其他内置异常的示例。有关完整列表 内置异常类型,请参阅 Exception 类和内置异常。DmlExceptionDML 语句的任何问题,例如语句在记录中缺少必填字段。insert此示例使用 DmlException。此示例中的 DML 语句会导致 DmlException,因为 它插入商品项目而不设置任何所需的内容 领域。此异常在块中捕获,异常消息将写入调试日志 使用语句。insertcatchSystem.debug

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

列表异常列表的任何问题,例如尝试访问超出 边界。此示例创建一个列表并向其添加一个元素。然后,进行尝试 访问两个元素,一个位于索引 0(存在),另一个位于索引 1(其中 导致引发 ListException,因为此索引处不存在任何元素。 此异常在 catch 块中被捕获。catch 块中的语句将以下内容写入 调试日志:。System.debugThe following exception has occurred: List index out of bounds: 1

try {
    List<Integer> li = new List<Integer>();
    li.add(15);
    // This list contains only one element,
    // but we're attempting to access the second element
    // from this zero-based list.
    Integer i1 = li[0]; 
    Integer i2 = li[1]; // Causes a ListException
} catch(ListException le) {
    System.debug('The following exception has occurred: ' + le.getMessage());
}

NullPointerException取消引用变量的任何问题。null此示例创建一个名为 String 的变量,但我们不将其初始化为值,因此它是 null。 在我们的 null 上调用该方法 变量导致 NullPointerException。例外情况在我们的捕获中被发现 块,这是写入调试日志的内容:。scontainsThe following exception has occurred: Attempt to de-reference a null object

try {
    String s;
    Boolean b = s.contains('abc'); // Causes a NullPointerException
} catch(NullPointerException npe) {
    System.debug('The following exception has occurred: ' + npe.getMessage());
}

查询异常SOQL 查询的任何问题,例如分配不返回任何记录的查询 或多条记录到一个单一实例 sObject 变量。此示例中的第二个 SOQL 查询会导致 QueryException。示例 将 Merchandise 对象分配给从查询返回的内容。请注意在查询中使用。这确保了 从数据库中最多返回一个对象,因此我们可以将其分配给 单个对象,而不是列表。但是,在这种情况下,我们没有 商品名为 XYZ,因此不会返回任何内容,并且尝试将 单个对象的返回值会导致 QueryException。例外情况是 捕获到我们的 catch 块中,这就是您将在调试日志中看到的内容:.LIMIT 1The following exception has occurred: List has no rows for assignment to SObject

try {
    // This statement doesn't cause an exception, even though 
    // we don't have a merchandise with name='XYZ'.
    // The list will just be empty.
    List<Merchandise__c> lm = [SELECT Name FROM Merchandise__c WHERE Name = 'XYZ'];
    // lm.size() is 0 
    System.debug(lm.size());
    
    // However, this statement causes a QueryException because 
    // we're assiging the return value to a Merchandise__c object
    // but no Merchandise is returned.
    Merchandise__c m = [SELECT Name FROM Merchandise__c WHERE Name = 'XYZ' LIMIT 1];
} catch(QueryException qe) {
    System.debug('The following exception has occurred: ' + qe.getMessage());    
}

SObjectExceptionsObject 记录的任何问题,例如尝试更改语句中的字段,该字段只能是 期间更改。updateinsert此示例在 try 块中生成 SObjectException,该块在 catch 块。该示例查询发票对帐单,并仅选择其 名称字段。然后,它尝试获取查询的 Description__c 字段 sObject,它不可用,因为它不在 在 SELECT 语句中查询的字段。这会导致 SObjectException。 这个异常在我们的 catch 块中被捕获,这就是你将看到的 在调试日志中:。The following exception has occurred: SObject row was retrieved via SOQL without querying the requested field: Invoice_Statement__c.Description__c

try {
    Invoice_Statement__c inv = new Invoice_Statement__c(
        Description__c='New Invoice');
    insert inv;

    // Query the invoice we just inserted
    Invoice_Statement__c v = [SELECT Name FROM Invoice_Statement__c WHERE Id = :inv.Id];
    // Causes an SObjectException because we didn't retrieve
    // the Description__c field.
    String s = v.Description__c;
} catch(SObjectException se) {
    System.debug('The following exception has occurred: ' + se.getMessage());
}

常见异常方法

您可以使用常见的异常方法来获取有关异常的详细信息,例如 作为异常错误消息或堆栈跟踪。前面的示例调用该方法,该方法返回 与异常关联的错误消息。还有其他异常方法 也可用。以下是一些有用方法的说明:

getMessage

  • getCause:返回 exception 作为异常对象。
  • getLineNumber:返回该行 引发异常的编号。
  • getMessage:返回错误 为用户显示的消息。
  • getStackTraceString:返回 以字符串形式引发异常的堆栈跟踪。
  • getTypeName:返回 异常,例如 DmlException、ListException、MathException 等 上。

若要了解一些常见方法返回的内容,请尝试运行此示例。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(Exception e) {
    System.debug('Exception type caught: ' + e.getTypeName());    
    System.debug('Message: ' + e.getMessage());    
    System.debug('Cause: ' + e.getCause());    // returns null
    System.debug('Line number: ' + e.getLineNumber());    
    System.debug('Stack trace: ' + e.getStackTraceString());    
}

所有语句的输出 如下所示:System.debug

17:38:04:149 USER_DEBUG [7]|DEBUG|Exception type caught: System.SObjectException

17:38:04:149 USER_DEBUG [8]|DEBUG|Message: SObject row was retrieved via SOQL without querying the requested field: Merchandise__c.Total_Inventory__c

17:38:04:150 USER_DEBUG [9]|DEBUG|Cause: null

17:38:04:150 USER_DEBUG [10]|DEBUG|Line number: 5

17:38:04:150 USER_DEBUG [11]|DEBUG|Stack trace: AnonymousBlock: line 5, column 1

catch 语句参数类型是泛型 Exception 类型。它抓住了更多 特定的 SObjectException。您可以通过检查退货来验证是否如此 的值 输出。输出还包含 SObjectException 的其他属性,如 错误消息、发生异常的行号和堆栈跟踪。 您可能想知道为什么返回 null。这是因为在我们的样本中,之前没有异常(内部 exception),导致此异常。在 Create Custom Exceptions 中,您将看到一个示例,其中 的返回值是实际值 例外。e.getTypeName()getCausegetCause

更多异常方法

某些异常类型(如 DmlException)具有特定的异常方法,这些方法 仅适用于它们,不适用于其他异常类型:

  • getDmlFieldNames(Index of the failed record):返回导致错误的字段的名称 指定的失败记录。
  • getDmlId(Index of the failed record): 返回导致指定错误的失败记录的 ID 失败的记录。
  • getDmlMessage(Index of the failed record):返回指定失败的错误消息 记录。
  • getNumDml:返回失败的次数 记录。

此代码片段利用 DmlException 方法获取有关 插入 Merchandise 对象列表时返回的异常。项目清单 要插入包含三个项目,其中最后两个不是必需的 字段和原因 异常。

Merchandise__c m1 = new Merchandise__c(
    Name='Coffeemaker',
    Description__c='Kitchenware',
    Price__c=25,
    Total_Inventory__c=1000);
// Missing the Price and Total_Inventory fields
Merchandise__c m2 = new Merchandise__c(
    Name='Coffeemaker B',
    Description__c='Kitchenware');
// Missing all required fields
Merchandise__c m3 = new Merchandise__c();
Merchandise__c[] mList = new List<Merchandise__c>();
mList.add(m1);
mList.add(m2);
mList.add(m3);

try {
    insert mList;
} catch (DmlException de) {
    Integer numErrors = de.getNumDml();
    System.debug('getNumDml=' + numErrors);
    for(Integer i=0;i<numErrors;i++) {
        System.debug('getDmlFieldNames=' + de.getDmlFieldNames(i));
        System.debug('getDmlMessage=' + de.getDmlMessage(i));  
    }
}

请注意,上面的示例在尝试中没有包含所有初始代码 块。只有可能生成异常的代码部分才会被包装 在块内,在这种情况下,该语句可能会在 如果输入数据无效。操作导致的异常由其后面的块捕获。执行后 此示例中,你将看到类似于以下内容的语句输出:tryinsertinsertcatchSystem.debug

14:01:24:939 USER_DEBUG [20]|DEBUG|getNumDml=2

14:01:24:941 USER_DEBUG [23]|DEBUG|getDmlFieldNames=(Price, Total Inventory)

14:01:24:941 USER_DEBUG [24]|DEBUG|getDmlMessage=Required fields are missing: [Price, Total Inventory]

14:01:24:942 USER_DEBUG [23]|DEBUG|getDmlFieldNames=(Description, Price, Total Inventory)

14:01:24:942 USER_DEBUG [24]|DEBUG|getDmlMessage=Required fields are missing: [Description, Price, Total Inventory]

DML 失败数正确报告为 2,因为我们列表中有两个项目 插入失败。此外,导致失败的字段名称和错误消息 对于每个失败的记录,将写入输出。

捕获不同的异常类型

在前面的示例中,我们使用了 catch 块。我们也可以捕获通用异常 键入所有示例,这将捕获所有异常类型。例如 尝试运行此示例,该示例会引发 SObjectException 并具有 catch 语句,参数类型为 Exception。The SObjectException 被 catch 块卡住。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(Exception e) {
    System.debug('The following exception has occurred: ' + e.getMessage());    
}

或者,您可以有多个 catch 块 – 一个 catch 块,以及捕获的最终 catch 块 泛型 Exception 类型。请看这个例子。请注意,它有 三个捕获块。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(DmlException e) {
    System.debug('DmlException caught: ' + e.getMessage());    
} catch(SObjectException e) {
    System.debug('SObjectException caught: ' + e.getMessage());    
} catch(Exception e) {
    System.debug('Exception caught: ' + e.getMessage());    
}

请记住,只执行一个 catch 块,其余的 catch 块 那些被绕过。此示例与上一个示例类似,但 它有更多的捕获块。运行此代码段时,一个 SObjectException 在以下行上抛出:。每个捕获块 按指定的顺序进行检查,以查找抛出的匹配项 exception 和 catch 块参数中指定的异常类型:

Double inventory = m.Total_Inventory__c;

  1. 第一个 catch 块参数的类型为 DmlException,其类型为 与引发的异常 (SObjectException) 不匹配。
  2. 第二个 catch 块参数的类型为 SObjectException,其 与我们的异常匹配,因此此块被执行并执行以下 消息将写入调试日志:。SObjectException caught: SObject row was retrieved via SOQL without querying the requested field: Merchandise__c.Total_Inventory__c
  3. 最后一个 catch 块被忽略,因为一个 catch 块已经 执行。

最后一个 catch 块很方便,因为它可以捕获任何异常 类型,从而捕获上一个未捕获的任何异常 catch 块。假设我们修改了上面的代码以导致 NullPointerException 要抛出,此异常将在最后一个 catch 块中捕获。 执行此修改后的示例。你将看到以下调试You’ll see the following debug 消息:。Exception caught: Attempt to de-reference a null object

try {
    String s;
    Boolean b = s.contains('abc'); // Causes a NullPointerException
} catch(DmlException e) {
    System.debug('DmlException caught: ' + e.getMessage());    
} catch(SObjectException e) {
    System.debug('SObjectException caught: ' + e.getMessage());    
} catch(Exception e) {
    System.debug('Exception caught: ' + e.getMessage());    
}

创建自定义例外

您不能抛出内置的 Apex 异常。你只能抓住他们。但是有定制 异常,您可以在方法中抛出和捕获它们。自定义异常使您能够 指定详细的错误消息,并在捕获中具有更多自定义错误处理 块。

异常可以是顶级类,也就是说,它们可以具有成员变量、方法和 构造函数,它们可以实现接口,等等。

若要创建自定义异常类,请扩展内置类,并确保类名以单词结尾,例如“MyException”或 “PurchaseException”。所有异常类都扩展了系统定义的基类,因此继承了所有公共类 异常方法。ExceptionExceptionException此示例定义了一个名为 的自定义异常。

MyException

public class MyException extends Exception {}

与 Java 类一样,用户定义的异常类型可以形成继承树,并捕获 块可以捕获此继承树中的任何对象。例如:

public class ExceptionExample {
    public virtual class BaseException extends Exception {}
    public class OtherException extends BaseException {}

    public static void testExtendedException() {
        try {
            Integer i=0;
            // Your code here
            if (i < 5) throw new OtherException('This is bad');
        } catch (BaseException e) {  
            // This catches the OtherException
            System.debug(e.getMessage());
        }  
    }
}

您可以通过以下几种方式创建异常对象,然后可以抛出这些对象。您可以构造异常:

  • 没有 参数:new MyException();
  • 使用指定错误的单个 String 参数 消息:new MyException('This is bad');
  • 使用单个 Exception 参数,该参数指定原因并显示在 任何堆栈 跟踪:new MyException(e);
  • 同时显示 String 错误消息和链式异常原因 任何堆栈 跟踪:new MyException('This is bad', e);

重新引发异常和内部异常

在 catch 块中捕获异常后,您可以选择重新抛出 捕获异常变量。如果方法被另一个方法调用,这将非常有用 并且您希望将异常的处理委托给调用方方法。您可以 将捕获的异常作为内部异常重新抛出,并具有 main 方法捕获自定义异常类型。

下面的示例演示如何将异常重新引发为内部异常。这 example 定义了两个自定义异常和 ,并生成一个堆栈跟踪,其中包含有关两者的信息。My1ExceptionMy2Exception

// Define two custom exceptions
public class My1Exception extends Exception {} 
public class My2Exception extends Exception {} 

try { 
    // Throw first exception
    throw new My1Exception('First exception'); 
} catch (My1Exception e) { 
    // Throw second exception with the first 
    // exception variable as the inner exception
    throw new My2Exception('Thrown with inner exception', e);
}

这是运行上述代码后生成的堆栈跟踪的样子:

15:52:21:073 EXCEPTION_THROWN [7]|My1Exception: First exception

15:52:21:077 EXCEPTION_THROWN [11]|My2Exception: Throw with inner exception

15:52:21:000 FATAL_ERROR AnonymousBlock: line 11, column 1

15:52:21:000 FATAL_ERROR Caused by

15:52:21:000 FATAL_ERROR AnonymousBlock: line 7, column 1

下一节中的示例演示如何使用 异常。getCause

内部异常示例

现在,您已经了解了如何创建自定义异常类以及如何构造 您的异常对象,让我们创建并运行一个示例来演示 自定义异常的有用性。

  1. 在开发者控制台中,创建一个名为 内容。MerchandiseExceptionpublic class MerchandiseException extends Exception { }你会 在创建的第二个类中使用此异常类。大括号 最后将异常类的主体括起来,我们将其留空,因为 我们得到了一些免费代码——我们的类继承了所有的构造函数和公共 异常方法,例如 来自内置类。getMessageException
  2. 接下来,创建名为 .MerchandiseUtilitypublic class MerchandiseUtility { public static void mainProcessing() { try { insertMerchandise(); } catch(MerchandiseException me) { System.debug('Message: ' + me.getMessage()); System.debug('Cause: ' + me.getCause()); System.debug('Line number: ' + me.getLineNumber()); System.debug('Stack trace: ' + me.getStackTraceString()); } } public static void insertMerchandise() { try { // Insert merchandise without required fields Merchandise__c m = new Merchandise__c(); insert m; } catch(DmlException e) { // Something happened that prevents the insertion // of Employee custom objects, so throw a more // specific exception. throw new MerchandiseException( 'Merchandise item could not be inserted.', e); } } }这 class 包含调用 . 后者通过插入不需要的商品来导致异常 领域。catch 块捕获此异常并抛出一个新异常,即 您之前创建的自定义 MerchandiseException。请注意,我们调用了 接受两个参数的异常的构造函数:错误消息和 原始异常对象。您可能想知道为什么我们要传递原始版本 例外?因为它是有用的信息 – 当 MerchandiseException 在第一种方法中捕获,原始异常(称为内部 exception) 确实是导致此异常的原因,因为它发生在 MerchandiseException。mainProcessinginsertMerchandisemainProcessing
  3. 现在让我们看看所有这些的实际效果,以便更好地理解。执行 以后:MerchandiseUtility.mainProcessing();
  4. 检查调试日志输出。您应该看到类似于 以后:18:12:34:928 USER_DEBUG [6]|DEBUG|Message: Merchandise item could not be inserted.18:12:34:929 USER_DEBUG [7]|DEBUG|Cause: System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Description, Price, Total Inventory]: [Description, Price, Total Inventory]18:12:34:929 USER_DEBUG [8]|DEBUG|Line number: 2218:12:34:930 USER_DEBUG [9]|DEBUG|Stack trace: Class.EmployeeUtilityClass.insertMerchandise: line 22, column 1一些感兴趣的项目:
    • MerchandiseException 的原因是 DmlException。您可以看到 DmlException 消息还指出必填字段是 失踪。
    • 堆栈跟踪是第 22 行,这是第二次出现异常 扔。它对应于 throw 语句 MerchandiseException。throw new MerchandiseException('Merchandise item could not be inserted.', e);

调试、测试和部署 Apex

在沙盒中开发 Apex 代码,并使用开发人员控制台对其进行调试,然后进行调试 原木。对代码进行单元测试,然后使用包将其分发给客户。

  • 调试Apex
    Apex 提供调试支持。您可以使用开发人员控制台和调试日志来调试 Apex 代码。
  • 测试 Apex Apex
    提供了一个测试框架,允许您编写单元测试、运行测试、检查测试结果以及获得代码覆盖率结果。
  • 部署 Apex
    您无法在 Salesforce 生产组织中开发 Apex。您的开发工作是在沙盒或 Developer Edition 组织中完成的。
  • 使用托管软件包
    分发 Apex 作为 ISV 或 Salesforce 合作伙伴,您可以使用软件包将 Apex 代码分发给客户组织。在这里,我们将介绍包和包版本控制。

调试Apex

Apex 提供调试支持。您可以使用开发人员调试 Apex 代码 控制台和调试日志。

为了帮助在代码中进行调试,Apex 支持异常语句和自定义异常。 此外,Apex 还会向开发人员发送电子邮件,以解决未处理的异常。

  1. 调试日志
  2. Apex 中的异常

调试日志

调试日志可以记录数据库操作、系统进程和以下情况下发生的错误 执行事务或运行单元测试。调试日志可以包含以下信息:

  • 数据库更改
  • HTTP 标注
  • 顶点错误
  • Apex 使用的资源
  • 自动化工作流程,例如:
    • 工作流规则
    • 分配规则
    • 审批流程
    • 验证规则
    注意调试日志不包括来自基于时间的触发的操作的信息 工作流。

您可以保留和管理特定用户(包括您自己)和类的调试日志 和触发器。设置类和触发器跟踪标志不会导致生成日志或 保存。类和触发器跟踪标志覆盖其他日志记录级别,包括日志记录级别 由用户跟踪标志设置,但它们不会导致日志记录发生。如果在以下情况下启用日志记录 类或触发器执行,日志在执行时生成。

若要查看调试日志,请从“设置”中输入“快速查找”框,然后选择“调试日志”。然后 单击要检查的调试日志旁边的“查看”。单击下载”将日志下载为 XML 文件。Debug Logs

调试日志具有以下限制。

  • 每个调试日志必须小于或等于 20 MB。减少大于 20 MB 的调试日志 通过删除较旧的日志行(例如早期语句的日志行)来调整大小。日志行可以从任何 位置,而不仅仅是调试日志的开头。System.debug
  • 系统调试日志将保留 24 小时。监视调试日志保留 7 个 日。
  • 如果在 15 分钟的时段内生成超过 1,000 MB 的调试日志,则跟踪标志为 禁用。我们向上次修改跟踪标志的用户发送一封电子邮件,通知他们 他们可以在 15 分钟内重新启用跟踪标志。警告如果调试日志跟踪 标志在经常访问的 Apex 类上启用,或者为经常执行请求的用户启用, 无论时间窗口和调试大小如何,请求都可能导致失败 原木。
  • 当您的组织累积的调试日志超过 1,000 MB 时,我们会阻止组织中的用户 添加或编辑跟踪标志。添加或编辑跟踪标志以便生成更多日志 达到限制后,请删除一些调试日志。

检查调试日志部分

生成调试日志后,列出的信息类型和数量取决于为用户设置的筛选器值。 但是,调试日志的格式始终相同。

注意

会话 ID 在 Apex 调试日志中替换为“SESSION_ID_REMOVED”调试日志包含以下部分。页眉标头包含以下信息。

  • 事务期间使用的 API 版本。
  • 用于生成 日志。例如:

下面是一个示例 页眉。

59.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;
WORKFLOW,INFO

本示例中,API版本为59.0,并设置了以下调试日志类别和级别。

Apex 代码调试
Apex 分析信息
标注信息
数据库信息
系统调试
验证信息
视觉力信息
工作流程信息

警告

如果 Apex Code 日志级别设置为 FINEST,则调试 日志包含所有 Apex 变量分配的详细信息。确保 Apex 代码 被跟踪不会处理敏感数据。在启用 FINEST 日志级别之前,请 确保了解组织的 Apex 处理的敏感数据级别。是 特别注意社区用户自助注册等流程,其中 可以将用户密码分配给 Apex 字符串变量。执行单元执行单元等同于事务。它包含以下一切 在事务中发生。 并分隔执行单元。EXECUTION_STARTEDEXECUTION_FINISHED代码单元代码单元是事务中的离散工作单元。例如,触发器 是一个代码单元,方法也是一个 或验证规则。webservice

注意

不是离散的代码单元。CODE_UNIT_STARTED并分隔代码单位。工作单元 可以嵌入其他工作单元。例如:CODE_UNIT_FINISHED

EXECUTION_STARTED
CODE_UNIT_STARTED|[EXTERNAL]execute_anonymous_apex
CODE_UNIT_STARTED|[EXTERNAL]MyTrigger on Account trigger event BeforeInsert for [new]|__sfdc_trigger/MyTrigger
CODE_UNIT_FINISHED <-- The trigger ends
CODE_UNIT_FINISHED <-- The executeAnonymous ends
EXECUTION_FINISHED

代码单元包括但不限于以下内容:

  • 触发器
  • 工作流调用和基于时间的工作流
  • 验证规则
  • 审批流程
  • Apex 潜在客户转换
  • @future方法调用
  • Web 服务调用
  • executeAnonymous调用
  • Apex 控制器上的 Visualforce 属性访问
  • Apex 控制器上的 Visualforce 操作
  • 批量执行 Apex 和方法,以及每次执行 方法startfinishexecute
  • Apex 方法的执行System.Schedule execute
  • 传入电子邮件处理

日志行日志行包含在代码单元中,并指示哪些代码或规则是 正在执行。日志行也可以是写入调试日志的消息。例如:

Debug Log Line Example

日志行由一组字段组成,由竖线 () 分隔。格式为:|

  • timestamp:由事件发生的时间和一个值组成 括号之间。时间采用用户的时区,格式为 。括号中的值表示时间 自请求开始以来经过的纳秒。运行时间值为 从使用执行时在开发者控制台中查看的日志中排除 日志视图。但是,当您使用“原始日志”视图时,您可以看到经过的时间。自 打开 Raw Log 视图,在 Developer Console 的 Logs 选项卡中,右键单击名称 ,然后选择打开原始日志HH:mm:ss.SSS
  • event identifier:指定触发调试日志的事件 条目(例如 或 )。SAVEPOINT_RESETVALIDATION_RULE还包括其他 与该事件一起记录的信息,例如方法名称或行,以及 执行代码的字符号。如果无法找到行号,则改为记录。为 示例,为内置记录 托管包中的 Apex 类或代码。[EXTERNAL][EXTERNAL]对于某些事件(、、、、和),事件结束 标识符包括一个管道 (),后跟 Apex 类或触发器的 typeRef。CODE_UNIT_STARTEDCODE_UNIT_FINISHEDVF_APEX_CALL_STARTVF_APEX_CALL_ENDCONSTRUCTOR_ENTRYCONSTRUCTOR_EXIT|对于触发器,typeRef 开始 替换为 SFDC 触发器前缀。例如,或 .__sfdc_trigger/__sfdc_trigger/YourTriggerName__sfdc_trigger/YourNamespace/YourTriggerName为 一个类,typeRef 使用格式 或 。YourClassYourClass$YourInnerClass,YourNamespace/YourClass$YourInnerClass

更多日志数据此外,日志还包含以下信息。

  • 累积资源使用情况记录在许多代码单元的末尾。其中 代码单元是触发器、 批处理 Apex 消息处理、方法、Apex 测试方法、Apex Web 服务方法和 Apex 潜在客户转换。executeAnonymous@future
  • 累积分析信息在事务结束时记录一次 并包含有关 DML 调用、成本高昂的查询等的信息。 “昂贵”的查询会大量使用资源。

下面是一个调试示例 日志。

37.0 APEX_CODE,FINEST;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;
    VALIDATION,INFO;VISUALFORCE,INFO;WORKFLOW,INFO
Execute Anonymous: System.debug('Hello World!');
16:06:58.18 (18043585)|USER_INFO|[EXTERNAL]|005D0000001bYPN|devuser@example.org|
    Pacific Standard Time|GMT-08:00
16:06:58.18 (18348659)|EXECUTION_STARTED
16:06:58.18 (18383790)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
16:06:58.18 (23822880)|HEAP_ALLOCATE|[72]|Bytes:3
16:06:58.18 (24271272)|HEAP_ALLOCATE|[77]|Bytes:152
16:06:58.18 (24691098)|HEAP_ALLOCATE|[342]|Bytes:408
16:06:58.18 (25306695)|HEAP_ALLOCATE|[355]|Bytes:408
16:06:58.18 (25787912)|HEAP_ALLOCATE|[467]|Bytes:48
16:06:58.18 (26415871)|HEAP_ALLOCATE|[139]|Bytes:6
16:06:58.18 (26979574)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:1
16:06:58.18 (27384663)|STATEMENT_EXECUTE|[1]
16:06:58.18 (27414067)|STATEMENT_EXECUTE|[1]
16:06:58.18 (27458836)|HEAP_ALLOCATE|[1]|Bytes:12
16:06:58.18 (27612700)|HEAP_ALLOCATE|[50]|Bytes:5
16:06:58.18 (27768171)|HEAP_ALLOCATE|[56]|Bytes:5
16:06:58.18 (27877126)|HEAP_ALLOCATE|[64]|Bytes:7
16:06:58.18 (49244886)|USER_DEBUG|[1]|DEBUG|Hello World!
16:06:58.49 (49590539)|CUMULATIVE_LIMIT_USAGE
16:06:58.49 (49590539)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

16:06:58.49 (49590539)|CUMULATIVE_LIMIT_USAGE_END

16:06:58.18 (52417923)|CODE_UNIT_FINISHED|execute_anonymous_apex
16:06:58.18 (54114689)|EXECUTION_FINISHED

为 Apex 类设置调试日志过滤器和 触发器

调试日志筛选提供了一种机制,用于在触发器时微调日志详细程度 和班级水平。这在调试 Apex 时特别有用 逻辑。例如,若要评估复杂进程的输出,可以引发日志 给定类的详细程度,同时关闭其他类或触发器的日志记录 单个请求。

当您重写类或触发器的调试日志级别时, 这些调试级别也适用于您的类或触发器调用的类方法,以及 因此被执行的触发器。执行路径中的所有类方法和触发器 从其调用方继承调试日志设置,除非他们具有这些设置 重写。

下图演示了在类和触发器中重写调试日志级别 水平。对于这种情况,假设导致 您想仔细研究的一些问题。为此,调试日志级别 的被提升到最细的粒度。 不会覆盖这些日志级别,并且 因此继承了 的粒度日志过滤器。但是,有 已经过测试并且已知可以正常工作,因此它已关闭其日志过滤器。 同样,不在导致 一个问题,因此它的日志记录最小化,只记录 Apex 代码的错误 类别。 从 继承这些日志设置。Class1Class1Class3Class1UtilityClassClass2Trigger2Class2

微调类和触发器的调试日志记录Debug log filters for classes and triggers下面是该关系图所基于的伪代码示例。

  1. Trigger1调用 的方法和 的另一个方法。为 例:Class1Class2trigger Trigger1 on Account (before insert) { Class1.someMethod(); Class2.anotherMethod(); }
  2. Class1调用 的方法,而 的方法又调用实用程序的方法 类。为 例:Class3public class Class1 { public static void someMethod() { Class3.thirdMethod(); } } public class Class3 { public static void thirdMethod() { UtilityClass.doSomething(); } }
  3. Class2导致触发器 执行。为 例:Trigger2public class Class2 { public static void anotherMethod() { // Some code that causes Trigger2 to be fired. } }
  • 在开发者控制台中使用日志
  • 调试Apex API 调用
  • 调试日志 优先顺序 记录
    哪些事件取决于各种因素。这些因素包括跟踪标志、默认日志记录级别、API 标头、基于用户的系统日志启用以及入口点设置的日志级别。

在开发者控制台中使用日志

使用开发者控制台中的“日志”选项卡打开调试日志。

使用“日志”选项卡打开相关调试日志以供查看

日志在 Log Inspector 中打开。Log Inspector 是 开发者控制台。它显示操作的来源、触发操作的内容以及内容 接下来发生了。使用此工具检查调试日志,其中包括数据库事件、Apex 处理、 工作流和验证逻辑。

要了解有关在开发人员控制台中使用日志的更多信息,请参阅 Salesforce 联机帮助中的日志检查器。使用开发人员控制台或监控调试日志时,您可以指定 日志中包含的信息。日志类别记录的信息类型,例如来自 Apex 或工作流规则的信息。日志级别记录的信息量。事件类型日志类别和日志级别的组合,用于指定要记录的事件。每 事件可以记录其他信息,例如事件所在的行号和字符号 已开始、与事件关联的字段以及事件的持续时间。

调试日志类别

每个调试级别都包括以下每个日志类别的调试日志级别。这 每个类别记录的信息量取决于日志级别。

日志类别描述
数据库包括有关数据库活动的信息,包括每个数据操作 语言 (DML) 语句或内联 SOQL 或 SOSL 查询。
工作流程包括工作流规则、流和流程的信息,例如规则名称 以及所采取的行动。
NBA(英语:包括有关 Einstein Next Best Action 活动的信息,包括策略 来自策略生成器的执行详细信息。
验证包括有关验证规则的信息,例如规则的名称以及是否 规则的计算结果为 true 或 false。
标注包括服务器所在的请求-响应 XML 从外部 Web 服务发送和接收。在调试与以下内容相关的问题时很有用 使用 Lightning 平台 Web 服务 API 调用或对用户访问外部进行故障排除 对象通过 Salesforce Connect。
Apex 代码包括有关 Apex 代码的信息。可以包含日志消息等信息 由 DML 语句、内联 SOQL 或 SOSL 查询生成,任何 触发器,以及任何测试方法的开始和完成。
Apex 分析包括累积分析信息,例如命名空间的限制和 发送的电子邮件数量。
视觉力包括有关 Visualforce 事件的信息,包括序列化和 在 Visualforce 中反序列化视图状态或计算公式字段 页。
系统包括有关调用所有系统方法(如方法)的信息。System.debug

调试日志级别

对于每个日志类别,每个调试级别都包括以下日志级别之一。关卡 按从低到高的顺序列出。根据以下组合记录特定事件 类别和级别。大多数事件开始在 INFO 级别记录。水平是累积的, 也就是说,如果选择 FINE,则日志还包括在 DEBUG、INFO、WARN、 和 ERROR 级别。

注意

并非所有级别都适用于所有类别。只有级别 对应于一个或多个事件可用。

  • NONE
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • FINE
  • FINER
  • FINEST

重要

在运行部署之前,请验证是否未设置 Apex Code 日志级别 到FINEST。否则,部署所需的时间可能比预期的要长。如果开发者 控制台处于打开状态,开发者控制台中的日志级别会影响所有日志,包括创建的日志 在部署期间。

调试事件类型

以下是写入调试日志的内容的示例。该事件是 。格式为 |:

USER_DEBUGtimestampevent identifier

  • timestamp:由事件发生的时间和 括弧。时间采用用户的时区,格式为 。括号中的值表示经过的时间 自请求开始以来的纳秒数。运行时间值将从日志中排除 当您使用“执行日志”视图时,在开发人员控制台中查看。但是,您可以看到 使用“原始日志”视图时经过的时间。要打开“原始日志”视图,请从 Developer 控制台的“日志”选项卡,右键单击日志的名称,然后选择“打开原始日志” 日志HH:mm:ss.SSS
  • event identifier:指定触发调试日志条目的事件(例如 作为 或 )。SAVEPOINT_RESETVALIDATION_RULE还包括与该事件一起记录的其他信息, 例如方法名称或执行代码的行号和字符号。如果 无法找到行号,已记录 相反。例如,为内置记录 托管包中的 Apex 类或代码。[EXTERNAL][EXTERNAL]对于某些事件(、、、、和)、 事件标识符的末尾包括一个管道 () 后跟 Apex 类或触发器的 typeRef。CODE_UNIT_STARTEDCODE_UNIT_FINISHEDVF_APEX_CALL_STARTVF_APEX_CALL_ENDCONSTRUCTOR_ENTRYCONSTRUCTOR_EXIT|对于触发器,typeRef 开始 替换为 SFDC 触发器前缀。为 example,或 .__sfdc_trigger/__sfdc_trigger/YourTriggerName__sfdc_trigger/YourNamespace/YourTriggerName为 一个类,typeRef 使用格式 或 。YourClassYourClass$YourInnerClass,YourNamespace/YourClass$YourInnerClass

以下是调试日志行的示例。

调试日志行示例调试日志行示例在此示例中,事件标识符由以下部分组成:

  • 活动名称:USER_DEBUG
  • 代码中事件的行号:[2]
  • 设置方法的日志记录级别 自:System.DebugDEBUG
  • 用户为该方法提供的字符串:System.DebugHello world!

此代码片段触发以下日志行示例。

调试日志行代码片段调试日志行代码片段当测试到达第 5 行时,将记录以下日志行 法典。

15:51:01.071 (55856000)|DML_BEGIN|[5]|Op:Insert|Type:Invoice_Statement__c|Rows:1

在此示例中,事件标识符由以下部分组成。

  • 活动名称:DML_BEGIN
  • 代码中事件的行号:[5]
  • DML 操作类型—:InsertOp:Insert
  • 对象名称:Type:Invoice_Statement__c
  • 传递到 DML 的行数 操作:Rows:1

将记录以下事件类型。该表列出了哪些字段或其他信息是 记录每个事件,以及日志级别和类别的哪个组合导致事件 登录。

活动名称与事件一起记录的字段或信息记录的类别记录的级别
BULK_HEAP_ALLOCATE分配的字节数Apex 代码最好
CALLOUT_REQUEST行号和请求标头标注INFO 及以上
CALLOUT_REQUEST(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)外部终结点和方法标注INFO 及以上
CALLOUT_RESPONSE行号和响应正文标注INFO 及以上
CALLOUT_RESPONSE(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)状态和状态代码标注INFO 及以上
CODE_UNIT_FINISHED行号、代码单元名称,例如 和:MyTrigger on Account trigger event BeforeInsert for [new]对于 Apex 方法,命名空间(如果适用)、类名和方法名;为 示例,或者YourNamespace.YourClass.yourMethod()YourClass.yourMethod()对于 Apex 触发器,typeRef;例如,或__sfdc_trigger/YourNamespace.YourTrigger__sfdc_trigger/YourTriggerApex 代码ERROR 及以上
CODE_UNIT_STARTED行号、代码单元名称,例如 和:MyTrigger on Account trigger event BeforeInsert for [new]对于 Apex 方法,命名空间(如果适用)、类名和方法名;为 示例,或者YourNamespace.YourClass.yourMethod()YourClass.yourMethod()对于 Apex 触发器,typeRef;例如__sfdc_trigger/YourTriggerApex 代码ERROR 及以上
CONSTRUCTOR_ENTRY行号、Apex 类 ID、括号内包含参数类型(如果有)的字符串,以及 类型Ref;例如,或<init>()YourClassYourClass.YourInnerClassApex 代码FINE 及以上
CONSTRUCTOR_EXIT行号,带有 括号之间的参数类型(如果有)和 typeRef;例如,或<init>()YourClassYourClass.YourInnerClassApex 代码FINE 及以上
CUMULATIVE_LIMIT_USAGE没有Apex 分析INFO 及以上
CUMULATIVE_LIMIT_USAGE_END没有Apex 分析INFO 及以上
CUMULATIVE_PROFILING没有Apex 分析FINE 及以上
CUMULATIVE_PROFILING_BEGIN没有Apex 分析FINE 及以上
CUMULATIVE_PROFILING_END没有Apex 分析FINE 及以上
DML_BEGIN行号、操作(如或)、记录名称或类型以及行数 传递到 DML 操作InsertUpdate分贝INFO 及以上
DML_END行号分贝INFO 及以上
EMAIL_QUEUE行号Apex 代码INFO 及以上
ENTERING_MANAGED_PKG包命名空间Apex 代码FINE 及以上
EVENT_SERVICE_PUB_BEGIN事件类型工作流程INFO 及以上
EVENT_SERVICE_PUB_DETAIL订阅 ID、发布事件的用户的 ID 和事件消息 数据工作流程FINER 及以上
EVENT_SERVICE_PUB_END事件类型工作流程INFO 及以上
EVENT_SERVICE_SUB_BEGIN事件类型和操作(订阅或取消订阅)工作流程INFO 及以上
EVENT_SERVICE_SUB_DETAIL订阅 ID、订阅实例 ID、引用数据(如 进程 API 名称)、激活或停用订阅的用户的 ID 和事件 消息数据工作流程FINER 及以上
EVENT_SERVICE_SUB_END事件类型和操作(订阅或取消订阅)工作流程INFO 及以上
EXCEPTION_THROWN行号、异常类型和消息Apex 代码INFO 及以上
EXECUTION_FINISHED没有Apex 代码ERROR 及以上
EXECUTION_STARTED没有Apex 代码ERROR 及以上
FATAL_ERROR异常类型、消息和堆栈跟踪Apex 代码ERROR 及以上
FLOW_ACTIONCALL_DETAIL采访 ID、元素名称、操作类型、操作枚举或 ID,无论是操作调用 成功,并显示错误消息工作流程FINER 及以上
FLOW_ASSIGNMENT_DETAIL采访 ID、引用、运算符和值工作流程FINER 及以上
FLOW_BULK_ELEMENT_BEGIN采访 ID 和元素类型工作流程FINE 及以上
FLOW_BULK_ELEMENT_DETAIL采访 ID、元素类型、元素名称、记录数工作流程FINER 及以上
FLOW_BULK_ELEMENT_END采访 ID、元素类型、元素名称、记录数和执行 时间工作流程FINE 及以上
FLOW_BULK_ELEMENT_LIMIT_USAGE将使用量递增,以达到此批量元素的限制。每个事件都显示使用情况 对于以下限制之一:SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_BULK_ELEMENT_NOT_SUPPORTED不支持批量操作的操作、元素名称和实体名称工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_BEGIN组织 ID、定义 ID 和版本 ID工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_END面试 ID 和流名称工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_ERROR消息、组织 ID、定义 ID 和版本 ID工作流程ERROR 及以上
FLOW_ELEMENT_BEGIN采访 ID、元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_DEFERRED元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_END采访 ID、元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(流运行时异常)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(未找到 spark)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(设计器例外)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(超出设计器限制)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(设计器运行时异常)工作流程ERROR 及以上
FLOW_ELEMENT_FAULT消息、元素类型和元素名称(采用的错误路径)工作流程警告及以上
FLOW_ELEMENT_LIMIT_USAGE将使用量递增,以达到此元素的限制。每个事件都显示 这些限制之一。SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_INTERVIEW_FINISHED_LIMIT_USAGEUsage toward a limit when the interview finishes. Each event displays the usage for one of these limits.SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_INTERVIEW_PAUSED采访 ID、流名称以及用户暂停的原因工作流程INFO 及以上
FLOW_INTERVIEW_RESUMED面试 ID 和流名称工作流程INFO 及以上
FLOW_LOOP_DETAIL访谈 ID、索引和值索引是集合变量中的位置 对于循环正在操作的项。工作流程FINER 及以上
FLOW_RULE_DETAIL面试 ID、规则名称和结果工作流程FINER 及以上
FLOW_START_INTERVIEW_BEGIN面试 ID 和流名称工作流程INFO 及以上
FLOW_START_INTERVIEW_END面试 ID 和流名称工作流程INFO 及以上
FLOW_START_INTERVIEWS_BEGIN请求工作流程INFO 及以上
FLOW_START_INTERVIEWS_END请求工作流程INFO 及以上
FLOW_START_INTERVIEWS_ERROR消息、采访 ID 和流名称工作流程ERROR 及以上
FLOW_START_INTERVIEW_LIMIT_USAGE在面试开始时达到限制的使用量。每个事件都显示 以下限制之一:SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_START_SCHEDULED_RECORDS运行流的消息和记录数工作流程INFO 及以上
FLOW_SUBFLOW_DETAIL采访 ID、姓名、定义 ID 和版本 ID工作流程FINER 及以上
FLOW_VALUE_ASSIGNMENT面试 ID、键和值工作流程FINER 及以上
FLOW_WAIT_EVENT_RESUMING_DETAIL采访 ID、元素名称、事件名称和事件类型工作流程FINER 及以上
FLOW_WAIT_EVENT_WAITING_DETAIL采访 ID、元素名称、事件名称、事件类型以及条件是否 遇到工作流程FINER 及以上
FLOW_WAIT_RESUMING_DETAIL采访 ID、元素名称和持久采访 ID工作流程FINER 及以上
FLOW_WAIT_WAITING_DETAIL采访 ID、元素名称、元素正在等待的事件数,以及 持久化采访 ID工作流程FINER 及以上
HEAP_ALLOCATE行号和字节数Apex 代码FINER 及以上
HEAP_DEALLOCATE行号和释放的字节数Apex 代码FINER 及以上
IDEAS_QUERY_EXECUTE行号分贝最好
LIMIT_USAGE_FOR_NS命名空间和以下内容 限制:Number of SOQL queries Number of query rows Number of SOSL queries Number of DML statements Number of DML rows Number of code statements Maximum heap size Number of callouts Number of Email Invocations Number of fields describes Number of record type describes Number of child relationships describes Number of picklist describes Number of future calls Number of find similar calls Number of System.runAs() invocationsApex 分析最好
METHOD_ENTRY行号、类的 Lightning 平台 ID 和方法签名(带有 命名空间(如果适用)Apex 代码FINE 及以上
METHOD_EXIT行号、类的 Lightning 平台 ID 和方法签名(带有 命名空间(如果适用)对于构造函数,将记录以下信息: 行 编号和类名。Apex 代码FINE 及以上
NAMED_CREDENTIAL_REQUEST命名凭据 ID、命名凭据名称、终结点、方法、外部凭据 类型、Http 标头授权、请求大小字节数和重试 401。如果使用 出站网络连接,还会记录以下附加字段: 出站网络 连接 ID、出站网络连接名称、出站网络连接状态、主机 类型、主机区域和专用连接出站每小时数据使用百分比。标注INFO 及以上
NAMED_CREDENTIAL_RESPONSE从 NamedCredential 返回的响应正文的截断部分 标注。标注INFO 及以上
NAMED_CREDENTIAL_RESPONSE_DETAIL命名凭据 ID、命名凭据名称、状态代码、响应大小字节、总体 标注时间毫秒和连接时间毫秒。如果使用出站网络连接,则这些 还会记录其他字段:出站网络连接 ID、出站网络 连接名称和专用连接出站每小时数据使用百分比。标注FINER 及以上
NBA_NODE_BEGIN元素名称、元素类型NBA(英语:FINE 及以上
NBA_NODE_DETAIL元素名称、元素类型、消息NBA(英语:FINE 及以上
NBA_NODE_END元素名称、元素类型、消息NBA(英语:FINE 及以上
NBA_NODE_ERROR元素名称、元素类型、错误消息NBA(英语:ERROR 及以上
NBA_OFFER_INVALID姓名、ID、原因NBA(英语:FINE 及以上
NBA_STRATEGY_BEGIN策略名称NBA(英语:FINE 及以上
NBA_STRATEGY_END策略名称、输出计数NBA(英语:FINE 及以上
NBA_STRATEGY_ERROR策略名称、错误消息NBA(英语:ERROR 及以上
POP_TRACE_FLAGS行号,具有其日志级别的类或触发器的 Lightning 平台 ID 设置,进入范围,此类或触发器的名称以及日志级别 离开此作用域后生效的设置系统INFO 及以上
PUSH_NOTIFICATION_INVALID_APP应用命名空间、应用名称当 Apex 代码尝试发送 通知组织中不存在或未启用推送的应用。Apex 代码错误
PUSH_NOTIFICATION_INVALID_CERTIFICATE应用命名空间、应用名称此事件表示证书无效。为 例如,它已过期。Apex 代码错误
PUSH_NOTIFICATION_INVALID_NOTIFICATION应用命名空间、应用名称、服务类型(Apple 或 Android GCM)、用户 ID、设备、有效负载 (子字符串),有效负载长度。当通知有效负载太 长。Apex 代码错误
PUSH_NOTIFICATION_NO_DEVICES应用命名空间、应用名称当我们尝试的所有用户都没有时,会发生此事件 发送通知以注册设备。Apex 代码调试
PUSH_NOTIFICATION_NOT_ENABLED当组织中未启用推送通知时,会发生此事件。Apex 代码信息
PUSH_NOTIFICATION_SENT应用命名空间、应用名称、服务类型(Apple 或 Android GCM)、用户 ID、设备、有效负载 (子字符串)此事件记录已接受发送通知。我们没有 保证通知的送达。Apex 代码调试
PUSH_TRACE_FLAGS行号、设置了日志级别的类或触发器的 Salesforce ID,以及 超出范围、此类或触发器的名称以及日志级别设置 进入此范围后生效的系统INFO 及以上
QUERY_MORE_BEGIN行号分贝INFO 及以上
QUERY_MORE_END行号分贝INFO 及以上
QUERY_MORE_ITERATIONS行号和迭代次数queryMore分贝INFO 及以上
SAVEPOINT_ROLLBACK行号和保存点名称分贝INFO 及以上
SAVEPOINT_SET行号和保存点名称分贝INFO 及以上
SLA_END案例数、加载时间、处理时间、要插入的案例里程碑数、 更新、删除和新建触发器工作流程INFO 及以上
SLA_EVAL_MILESTONE里程碑 ID工作流程INFO 及以上
SLA_NULL_START_DATE没有工作流程INFO 及以上
SLA_PROCESS_CASE案例 ID工作流程INFO 及以上
SOQL_EXECUTE_BEGIN行号、聚合数和查询源分贝INFO 及以上
SOQL_EXECUTE_END行号、行数和持续时间(以毫秒为单位)分贝INFO 及以上
SOQL_EXECUTE_EXPLAIN已执行的 SOQL 查询的查询计划详细信息。有关查看查询计划的信息 使用开发人员控制台,请参阅检索查询计划。获取反馈 有关查询性能的信息,请参阅获取有关 查询性能。分贝最好
SOSL_EXECUTE_BEGIN行号和查询源分贝INFO 及以上
SOSL_EXECUTE_END行号、行数和持续时间(以毫秒为单位)分贝INFO 及以上
STACK_FRAME_VARIABLE_LIST帧号和变量列表的形式:| .例如:Variable numberValuevar1:50 var2:'Hello World'Apex 分析FINE 及以上
STATEMENT_EXECUTE行号Apex 代码FINER 及以上
STATIC_VARIABLE_LIST格式为:| .例如:Variable numberValuevar1:50 var2:'Hello World'Apex 分析FINE 及以上
SYSTEM_CONSTRUCTOR_ENTRY行号和带有 括号之间的参数类型(如果有)<init>()系统FINE 及以上
SYSTEM_CONSTRUCTOR_EXIT行号和带有 括号之间的参数类型(如果有)<init>()系统FINE 及以上
SYSTEM_METHOD_ENTRY行号和方法签名系统FINE 及以上
SYSTEM_METHOD_EXIT行号和方法签名系统FINE 及以上
SYSTEM_MODE_ENTER模式名称系统INFO 及以上
SYSTEM_MODE_EXIT模式名称系统INFO 及以上
TESTING_LIMITS没有Apex 分析INFO 及以上
TOTAL_EMAIL_RECIPIENTS_QUEUED发送的电子邮件数量Apex 分析FINE 及以上
USER_DEBUG行号、日志记录级别和用户提供的字符串Apex 代码默认为 DEBUG 及以上版本。如果用户设置了方法的日志级别,则会在该级别记录事件 相反。System.Debug
USER_INFO行号、用户 ID、用户名、用户时区和用户时区(以 GMT 为单位)Apex 代码ERROR 及以上
VALIDATION_ERROR错误信息验证INFO 及以上
VALIDATION_FAIL没有验证INFO 及以上
VALIDATION_FORMULA公式来源和值验证INFO 及以上
VALIDATION_PASS没有验证INFO 及以上
VALIDATION_RULE规则名称验证INFO 及以上
VARIABLE_ASSIGNMENT行号、变量名称(包括变量的命名空间,如果适用)、 变量值和变量地址的字符串表示形式Apex 代码最好
VARIABLE_SCOPE_BEGIN行号、变量名称(包括变量的命名空间,如果适用)、类型、 一个值,指示是否可以引用变量,以及一个值,指示 变量是否为静态变量Apex 代码最好
VARIABLE_SCOPE_END没有Apex 代码最好
VF_APEX_CALL_STARTVisualforce 控制器的元素名称、方法名称、返回类型和 typeRef (例如,YourApexClass)Apex 代码INFO 及以上
VF_APEX_CALL_ENDVisualforce 控制器的元素名称、方法名称、返回类型和 typeRef (例如,YourApexClass)Apex 代码INFO 及以上
VF_DESERIALIZE_VIEWSTATE_BEGIN查看状态 ID视觉力INFO 及以上
VF_DESERIALIZE_VIEWSTATE_END没有视觉力INFO 及以上
VF_EVALUATE_FORMULA_BEGIN查看状态 ID 和公式视觉力FINER 及以上
VF_EVALUATE_FORMULA_END没有视觉力FINER 及以上
VF_PAGE_MESSAGE消息文本Apex 代码INFO 及以上
VF_SERIALIZE_VIEWSTATE_BEGIN查看状态 ID视觉力INFO 及以上
VF_SERIALIZE_VIEWSTATE_END没有视觉力INFO 及以上
WF_ACTION操作说明工作流程INFO 及以上
WF_ACTION_TASK任务主题、操作 ID、规则名称、规则 ID、所有者和截止日期工作流程INFO 及以上
WF_ACTIONS_END所执行操作的摘要工作流程INFO 及以上
WF_APPROVAL转换类型、 和 进程节点名称EntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_REMOVEEntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_SUBMITEntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_SUBMITTER提交者 ID、提交者全名和错误消息工作流程INFO 及以上
WF_ASSIGN所有者和受托人模板 ID工作流程INFO 及以上
WF_CRITERIA_BEGINEntityName: NameField Id、规则名称、规则 ID 和 (如果规则遵循触发器类型) 触发器类型和递归计数工作流程INFO 及以上
WF_CRITERIA_END指示成功的布尔值(true 或 false)工作流程INFO 及以上
WF_EMAIL_ALERT操作 ID、规则名称和规则 ID工作流程INFO 及以上
WF_EMAIL_SENT电子邮件模板 ID、收件人和抄送电子邮件工作流程INFO 及以上
WF_ENQUEUE_ACTIONS排队的操作摘要工作流程INFO 及以上
WF_ESCALATION_ACTION箱 ID 和升级日期工作流程INFO 及以上
WF_ESCALATION_RULE没有工作流程INFO 及以上
WF_EVAL_ENTRY_CRITERIA进程名称、电子邮件模板 ID 和指示结果的布尔值(true 或 false)工作流程INFO 及以上
WF_FIELD_UPDATEEntityName: NameField Id和对象或字段 名字工作流程INFO 及以上
WF_FLOW_ACTION_BEGIN流触发器的 ID工作流程INFO 及以上
WF_FLOW_ACTION_DETAIL流触发器的 ID、对象类型和记录的 ID,其创建或更新导致了 要触发的工作流规则、工作流规则的名称和 ID,以及流的名称和值 变量工作流程FINE 及以上
WF_FLOW_ACTION_END流触发器的 ID工作流程INFO 及以上
WF_FLOW_ACTION_ERROR流触发器 ID、流定义的 ID、流版本的 ID 和流错误 消息工作流程ERROR 及以上
WF_FLOW_ACTION_ERROR_DETAIL详细的流错误消息工作流程ERROR 及以上
WF_FORMULA公式来源和值工作流程INFO 及以上
WF_HARD_REJECT没有工作流程INFO 及以上
WF_NEXT_APPROVER所有者、下一个所有者类型和字段工作流程INFO 及以上
WF_NO_PROCESS_FOUND没有工作流程INFO 及以上
WF_OUTBOUND_MSGEntityName: NameField Id、操作 ID、规则名称、 和规则 ID工作流程INFO 及以上
WF_PROCESS_FOUND进程定义 ID 和进程标签工作流程INFO 及以上
WF_PROCESS_NODE进程名称工作流程INFO 及以上
WF_REASSIGN_RECORDEntityName: NameField Id和所有者工作流程INFO 及以上
WF_RESPONSE_NOTIFY通知程序名称、通知程序电子邮件、通知程序模板 ID 和回复电子邮件工作流程INFO 及以上
WF_RULE_ENTRY_ORDER表示顺序的整数工作流程INFO 及以上
WF_RULE_EVAL_BEGIN规则类型工作流程INFO 及以上
WF_RULE_EVAL_END没有工作流程INFO 及以上
WF_RULE_EVAL_VALUE价值工作流程INFO 及以上
WF_RULE_FILTER筛选条件工作流程INFO 及以上
WF_RULE_INVOCATIONEntityName: NameField Id工作流程INFO 及以上
WF_RULE_NOT_EVALUATED没有工作流程INFO 及以上
WF_SOFT_REJECT进程名称工作流程INFO 及以上
WF_SPOOL_ACTION_BEGIN节点类型工作流程INFO 及以上
WF_TIME_TRIGGEREntityName: NameField Id、时间动作、时间 操作容器和计算日期时间工作流程INFO 及以上
WF_TIME_TRIGGERS_BEGIN没有工作流程INFO 及以上
XDS_DETAIL(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)对于 OData 适配器,自定义 HTTP 的 POST 正文和名称以及计算公式 头标注FINER 及以上
XDS_RESPONSE(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)外部数据源、外部对象、请求详细信息、返回记录数、 和系统使用情况标注INFO 及以上
XDS_RESPONSE_DETAIL(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)来自外部系统的截断响应,包括返回的记录标注FINER 及以上
XDS_RESPONSE_ERROR(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)错误信息标注ERROR 及以上

调试Apex API 调用

调用 Apex 的所有 API 调用都支持允许访问 有关代码执行的详细信息,包括对 的任何调用。SOAP 输入标头的 categories 字段允许您 根据中概述的级别设置日志记录粒度 桌子。System.debug()DebuggingHeader

元素名称类型描述
类别日志类别指定 中返回的信息类型 调试日志。有效值为:DbWorkflowValidationCalloutApex_codeApex_profilingVisualforceSystemAll
水平日志类别级别指定 调试日志。有效的日志级别包括(从 从低到高):NONEERRORWARNINFODEBUGFINEFINERFINEST

此外,作为 for backwards 的一部分,仍支持以下日志级别 兼容性。DebuggingHeader

日志级别描述
NONE不包含任何日志消息。
DEBUGONLY包括较低级别的消息和消息 由对方法的调用生成。System.debug
DB包括调用 方法和每个数据 操作语言 (DML) 语句或内联 SOQL 或 SOSL 查询。System.debug
PROFILE包括调用 方法,每个 DML 语句 或内联 SOQL 或 SOSL 查询,以及入口和 退出每个用户定义的方法。另外 调试日志的末尾包含整个 各部分的分析信息 使用最多资源的请求。这 分析信息以 SOQL 和 SOSL 语句、DML 操作和 Apex 方法调用。这三个部分列出了 代码中消耗时间最多的位置, 按总累积时间的降序排列。也 列出的是类别的次数 执行。System.debug
CALLOUT包括请求-响应 XML 服务器正在从外部发送和接收 Web 服务。在调试相关问题时很有用 使用 Lightning 平台 Web 服务 API 调用 或对用户访问外部对象进行故障排除 通过 Salesforce Connect。
DETAIL包括级别和以下级别生成的所有消息。PROFILE变量声明语句循环执行的开始所有循环控制,例如 break 和 继续引发的异常 *静态和类初始化代码 *上下文中的任何更改with sharing

相应的输出标头 ,包含生成的调试日志。查看更多 信息,请参阅 SOAP API 开发人员指南 中的 DebuggingHeaderDebuggingInfo

调试日志优先顺序

记录哪些事件取决于各种因素。这些因素包括您的跟踪 标志、默认日志记录级别、API 标头、基于用户的系统日志启用和日志 由您的入口点设置的级别。调试日志级别的优先顺序为:

  1. 跟踪标志将覆盖所有其他日志记录逻辑。开发者控制台在以下情况下设置跟踪标志 它将加载,并且该跟踪标志将一直有效,直到它过期。您可以在 开发人员控制台或在安装程序中或使用 和 工具 API 对象。TraceFlagDebugLevel注意设置类和触发器跟踪标志不会导致 要生成或保存的日志。类和触发器跟踪标志覆盖其他日志记录级别, 包括由用户跟踪标志设置的日志记录级别,但它们不会导致日志记录发生。如果 日志记录在类或触发器执行时启用,日志在执行时生成 执行。
  2. 如果您没有活动跟踪标志,则同步和异步 Apex 测试将使用 默认日志记录级别。默认日志记录级别为:分贝信息APEX_CODE调试APEX_PROFILING信息工作流程信息验证信息标注信息视觉力信息系统调试
  3. 如果没有相关的跟踪标志处于活动状态,并且没有运行任何测试,则 API 标头将 日志记录级别。在没有调试标头的情况下发送的 API 请求会生成暂时性 日志 – 未保存的日志 – 除非其他日志记录规则生效。
  4. 如果入口点设置了日志级别,则使用该日志级别。例如,Visualforce 请求可以包含用于设置日志级别的调试参数。

如果这些情况都不适用,则不会生成或保留日志。

保护您的数据

可以使用类提供的方法保护数据。

Crypto

类中的方法提供标准 用于创建摘要、消息身份验证代码和签名的算法 作为加密和解密信息。这些可用于保护 Salesforce,或与 Google 或 Amazon 等外部服务集成 网络服务 (AWS)。Crypto

示例:集成 Amazon WebServices

以下示例演示了 Amazon WebServices 与 销售人员:

public class HMacAuthCallout {

   public void testAlexaWSForAmazon() {
 
   // The date format is yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
      DateTime d = System.now();
      String timestamp = ''+ d.year() + '-' +
      d.month() + '-' +
      d.day() + '\'T\'' +
      d.hour() + ':' +
      d.minute() + ':' +
      d.second() + '.' +
      d.millisecond() + '\'Z\'';
      String timeFormat = d.formatGmt(timestamp);

      String urlEncodedTimestamp = EncodingUtil.urlEncode(timestamp, 'UTF-8');
      String action = 'UrlInfo';
      String inputStr = action + timeFormat;
      String algorithmName = 'HMacSHA1';
      Blob mac = Crypto.generateMac(algorithmName,  Blob.valueOf(inputStr), 
                                                    Blob.valueOf('your_signing_key'));
      String macUrl = EncodingUtil.urlEncode(EncodingUtil.base64Encode(mac), 'UTF-8');
 
      String urlToTest = 'amazon.com';
      String version = '2005-07-11'; 
      String endpoint = 'http://awis.amazonaws.com/';
      String accessKey = 'your_key';
 
      HttpRequest req = new HttpRequest();
      req.setEndpoint(endpoint +
                      '?AWSAccessKeyId=' + accessKey +
                      '&Action=' + action +
                      '&ResponseGroup=Rank&Version=' + version +
                      '&Timestamp=' + urlEncodedTimestamp +
                      '&Url=' + urlToTest +
                      '&Signature=' + macUrl);
 
      req.setMethod('GET');
      Http http = new Http();
      try {
         HttpResponse res = http.send(req);
         System.debug('STATUS:'+res.getStatus());
         System.debug('STATUS_CODE:'+res.getStatusCode());
         System.debug('BODY: '+res.getBody());
      } catch(System.CalloutException e) {
         System.debug('ERROR: '+ e);
      }
   }
}

示例:加密和 解密

下面的示例使用 和 方法以及类的方法。encryptWithManagedIVdecryptWithManagedIVgenerateAesKeyCrypto

// Use generateAesKey to generate the private key
Blob cryptoKey = Crypto.generateAesKey(256);

// Generate the data to be encrypted.
Blob data = Blob.valueOf('Test data to encrypted');

// Encrypt the data and have Salesforce generate the initialization vector 
Blob encryptedData = Crypto.encryptWithManagedIV('AES256', cryptoKey, data);

// Decrypt the data
Blob decryptedData = Crypto.decryptWithManagedIV('AES256', cryptoKey, encryptedData);

下面是为 和 Crypto 方法编写单元测试的示例。

encryptWithManagedIVdecryptWithManagedIV

@isTest
private class CryptoTest {
    static testMethod void testValidDecryption() {

        // Use generateAesKey to generate the private key
        Blob key = Crypto.generateAesKey(128);
        // Generate the data to be encrypted.
        Blob data = Blob.valueOf('Test data');
        // Generate an encrypted form of the data using base64 encoding
        String b64Data = EncodingUtil.base64Encode(data);
        // Encrypt and decrypt the data
        Blob encryptedData = Crypto.encryptWithManagedIV('AES128', key, data);
        Blob decryptedData = Crypto.decryptWithManagedIV('AES128', key, encryptedData);
        String b64Decrypted = EncodingUtil.base64Encode(decryptedData);
        // Verify that the strings still match
        System.assertEquals(b64Data, b64Decrypted);
    }
    static testMethod void testInvalidDecryption() {
        // Verify that you must use the same key size for encrypting data
        // Generate two private keys, using different key sizes
        Blob keyOne = Crypto.generateAesKey(128);
        Blob keyTwo = Crypto.generateAesKey(256);
        // Generate the data to be encrypted.
        Blob data = Blob.valueOf('Test data');
        // Encrypt the data using the first key 
        Blob encryptedData = Crypto.encryptWithManagedIV('AES128', keyOne, data);
        try {
         // Try decrypting the data using the second key   
            Crypto.decryptWithManagedIV('AES256', keyTwo, encryptedData);
            System.assert(false);
        } catch(SecurityException e) {
            System.assertEquals('Given final block not properly padded', e.getMessage());
        }
    }
}

对数据进行编码

您可以对 URL 进行编码和解码,并将字符串转换为十六进制 使用类提供的方法设置格式。

EncodingUtil

此示例演示如何对 UTF-8 中的时间戳值进行 URL 编码 通过调用 .urlEncode

DateTime d = System.now();
String timestamp = ''+ d.year() + '-' +
    d.month() + '-' +
    d.day() + '\'T\'' +
    d.hour() + ':' +
    d.minute() + ':' +
    d.second() + '.' +
    d.millisecond() + '\'Z\'';
System.debug(timestamp);
String urlEncodedTimestamp = EncodingUtil.urlEncode(timestamp, 'UTF-8');
System.debug(urlEncodedTimestamp);

下一个示例演示如何使用 HTTP 摘要计算客户端响应 身份验证 (RFC2617)。convertToHex

@isTest
private class SampleTest {
   static testmethod void testConvertToHex() {
      String myData = 'A Test String';
      Blob hash = Crypto.generateDigest('SHA1',Blob.valueOf(myData));
      String hexDigest = EncodingUtil.convertToHex(hash);
      System.debug(hexDigest);  
    } 
}

使用模式和匹配器

Apex 提供模式和匹配器,使您能够使用 正则表达式。

模式是正则表达式的编译表示形式。 匹配器使用模式对字符执行匹配操作 字符串。

正则表达式是用于匹配的字符串 另一个字符串,使用特定语法。Apex 支持通过其 Pattern 和 Matcher 类使用正则表达式。

注意

在 Apex 中,Patterns 和 Matchers 以及正则表达式都是基于 在 Java 中的对应物上。请参见 http://java.sun.com/j2se/1.5.0/docs/api/index.html?java/util/regex/Pattern.html。

许多 Matcher 对象可以共享同一个 Pattern 对象,如下所示 在下图中:

可以从同一个 Pattern 对象创建许多 Matcher 对象A flow chart showing flow from Regular Expression to Matcher object

Apex 中的正则表达式遵循 Java 中使用的正则表达式的标准语法。 任何基于 Java 的正则表达式字符串都可以轻松导入到 您的 Apex 代码。

注意

Salesforce 限制常规输入序列的次数 表达式可以访问 1,000,000 次。如果达到该限制, 收到运行时错误。

所有正则表达式都指定为字符串。最常规 表达式首先编译到 Pattern 对象中:只有 String 方法采用正则表达式 这没有被编译。split通常,在将正则表达式编译为 Pattern 之后 对象,则只需使用 Pattern 对象一次即可创建 Matcher 对象。 然后,使用 Matcher 对象执行所有进一步的操作。为 例:

// First, instantiate a new Pattern object "MyPattern"
Pattern MyPattern = Pattern.compile('a*b');

// Then instantiate a new Matcher object "MyMatcher"
Matcher MyMatcher = MyPattern.matcher('aaaaab');

// You can use the system static method assert to verify the match
System.assert(MyMatcher.matches());

如果只打算使用一次正则表达式,请使用 class 方法编译表达式 并在单个调用中将字符串与其匹配。例如 以下内容等效于上面的代码:Patternmatches

Boolean Test = Pattern.matches('a*b', 'aaaaab');
  • 使用区域
  • 使用匹配操作
  • 使用边界
  • 了解捕获组
  • 模式和匹配器示例

使用区域

Matcher 对象在其输入字符串的子集中查找匹配项 称为区域。Matcher 对象的默认区域 始终是输入字符串的整个部分。但是,您可以更改 使用该方法获取某个区域的起点和终点,您可以查询 使用 和 方法的区域终结点。regionregionStartregionEnd

该方法需要 起始值和结束值。下表提供了示例 如何在不设置另一个值的情况下设置另一个值。region

区域开始区域结束代码示例
显式指定保持不变MyMatcher.region(start, MyMatcher.regionEnd());
保持不变显式指定MyMatcher.region(MyMatcher.regionStart(), end);
重置为默认值显式指定MyMatcher.region(0, end);

使用匹配操作

Matcher 对象对角色执行匹配操作 序列,通过解释 Pattern。

Matcher 对象是通过 Pattern 的方法从 Pattern 实例化的。创建后,匹配器 对象可用于执行以下类型的匹配操作:matcher

  • 将 Matcher 对象的整个输入字符串与模式进行匹配 使用方法matches
  • 将 Matcher 对象的输入字符串与模式匹配,从 在开始时,但不匹配整个区域,使用该方法lookingAt
  • 扫描 Matcher 对象的输入字符串以查找下一个子字符串 与使用方法的模式匹配find

这些方法中的每一个都返回一个指示成功或失败的布尔值。

使用这些方法中的任何一种后,您可以找到更多信息 关于上一个匹配项,即找到的内容,通过使用以下命令 Matcher 类方法:

  • end:一旦匹配 made,此方法返回匹配字符串中 匹配的最后一个字符。
  • start:一旦匹配 made,此方法返回第一个字符串中的位置 匹配的字符。
  • group:一旦匹配 made,此方法返回匹配的子序列。

使用边界

默认情况下,区域由定位边界分隔,这意味着线锚点(例如 或 )在区域边界处匹配,即使区域边界也是如此 已从输入字符串的开头和结尾移开。您可以 指定区域是否对方法使用定位边界。默认情况下, 区域始终使用定位边界。如果设置为 ,则线锚点仅匹配 输入字符串的真正末尾。^$useAnchoringBoundsuseAnchoringBoundsfalse

默认情况下,不会搜索位于区域之外的所有文本, 也就是说,该区域具有不透明的边界。但是,使用透明边界可以搜索外部的文本 一个区域。仅当区域不再使用透明边界时 包含整个输入字符串。您可以指定边界的类型 区域具有使用该方法。useTransparentBounds

假设您正在搜索以下字符串,以及您所在的地区 只是“STRING”这个词:

This is a concatenated STRING of cats and dogs.

如果你搜索“猫”这个词,你不会 接收匹配项,除非您设置了透明边界。

了解捕获组

在匹配操作期间,输入字符串的每个子字符串 与模式匹配的将被保存。这些匹配的子字符串称为捕获组

捕获组通过计算其左括号进行编号 从左到右。例如,在正则表达式字符串中,有四个捕获 组:((A)(B(C)))

  1. ((A)(B(C)))
  2. (A)
  3. (B(C))
  4. (C)

组 0 始终代表整个表达式。

与组关联的捕获输入始终是子字符串 最近匹配的组,即返回的组 Matcher 类匹配操作之一。

如果使用其中一个匹配操作第二次评估组, 如果第二次评估,则保留其先前捕获的值(如果有) 失败。

模式和匹配器示例

Matcher 类方法 返回匹配字符串中最后一个字符之后的位置 那是匹配的。在解析字符串时,您将使用它 并希望在找到匹配项后对其进行其他工作, 比如找到下一个匹配项。end

在正则表达式语法中,表示匹配一次或根本不匹配,表示匹配 1 次或多次。?+

在以下示例中,使用 Matcher 传入的字符串 对象与模式匹配,因为匹配字符串 – 后跟一次。然后它与最后一个匹配 – 然后是根本不匹配。(a(b)?)‘ab’‘a’‘b’‘a’‘a’‘b’

pattern myPattern = pattern.compile('(a(b)?)+'); 
matcher myMatcher = myPattern.matcher('aba');
System.assert(myMatcher.matches() && myMatcher.hitEnd());

// We have two groups: group 0 is always the whole pattern, and group 1 contains 
// the substring that most recently matched--in this case, 'a'. 
// So the following is true:

System.assert(myMatcher.groupCount() == 2 &&
              myMatcher.group(0) == 'aba' && 
              myMatcher.group(1) == 'a');
 
// Since group 0 refers to the whole pattern, the following is true:

System.assert(myMatcher.end() == myMatcher.end(0));

// Since the offset after the last character matched is returned by end, 
// and since both groups used the last input letter, that offset is 3
// Remember the offset starts its count at 0. So the following is also true:

System.assert(myMatcher.end() == 3 && 
              myMatcher.end(0) == 3 && 
              myMatcher.end(1) == 3);

在以下示例中,电子邮件地址被规范化并重复 如果存在不同的顶级域名或子域名,则会报告 对于相似的电子邮件地址。例如,规范化为 。john@fairway.smithcojohn@smithco

class normalizeEmailAddresses{

    public void hasDuplicatesByDomain(Lead[] leads) {
           // This pattern reduces the email address to 'john@smithco' 
           // from 'john@*.smithco.com' or 'john@smithco.*'
        Pattern emailPattern = Pattern.compile('(?<=@)((?![\\w]+\\.[\\w]+$)
                                               [\\w]+\\.)|(\\.[\\w]+$)');
           // Define a set for emailkey to lead:
        Map<String,Lead> leadMap = new Map<String,Lead>();
                for(Lead lead:leads) {
                    // Ignore leads with a null email
                    if(lead.Email != null) {
                           // Generate the key using the regular expression
                       String emailKey = emailPattern.matcher(lead.Email).replaceAll('');
                           // Look for duplicates in the batch
                       if(leadMap.containsKey(emailKey)) 
                            lead.email.addError('Duplicate found in batch');
                       else {
                           // Keep the key in the duplicate key custom field
                            lead.Duplicate_Key__c = emailKey;
                            leadMap.put(emailKey, lead);
                       }
                 }
             }
                // Now search the database looking for duplicates 
                for(Lead[] leadsCheck:[SELECT Id, duplicate_key__c FROM Lead WHERE 
                duplicate_key__c IN :leadMap.keySet()]) {
               for(Lead lead:leadsCheck) {
               // If there's a duplicate, add the error.
                   if(leadMap.containsKey(lead.Duplicate_Key__c)) 
                      leadMap.get(lead.Duplicate_Key__c).email.addError('Duplicate found 
                         in salesforce(Id: ' + lead.Id + ')');
            }
        }
    }
 }

XML 支持

Apex 提供了实用程序类,用于创建和解析 使用流和 DOM 的 XML 内容。

本节包含有关 XML 支持的详细信息。

  • 使用 Streams
    读取和写入 XML Apex 提供了用于使用流读取和写入 XML 内容的类。
  • 使用 DOM
    读取和写入 XML Apex 提供了使您能够使用 DOM(文档对象模型)处理 XML 内容的类。

使用流读取和写入 XML

Apex 提供了用于使用流读取和写入 XML 内容的类。

使用 XMLStreamReader 类可以读取 XML 内容和 XMLStreamWriter 类使您能够编写 XML 内容。

  • 使用流
    读取 XML XMLStreamReader 类方法支持对 XML 数据的正向只读访问。
  • 使用流
    编写 XML XmlStreamWriter 类方法允许写入 XML 数据。

使用流读取 XML

XMLStreamReader 类方法支持对 XML 的正向只读访问 数据。这些方法与 HTTP 标注结合使用,以解析 XML 数据或跳过 不需要的事件。您可以分析最

50节点深度。下面的示例演示如何 实例化新的 XmlStreamReader 对象:

String xmlString = '<books><book>My Book</book><book>Your Book</book></books>';
XmlStreamReader xsr = new XmlStreamReader(xmlString);

这些方法适用于以下 XML 事件:

  • 为特定元素指定属性事件。例如 该元素具有属性:。<book>title<book title=”Salesforce.com for Dummies”>
  • start 元素事件是元素的开始标记,例如 。<book>
  • 结束元素事件是元素的结束标记,例如 。</book>
  • 开始文档事件是文档的开始标记。
  • 结束文档事件是文档的结束标记。
  • 实体引用是代码中的实体引用,例如 。!ENTITY title = “My Book Title”
  • 字符事件是文本字符
  • 注释事件是 XML 文件中的注释。

使用 and 方法循环访问 XML 数据。使用 方法(如 the 方法)访问 XML 中的数据。nexthasNextgetgetNamespace

循环访问 XML 数据时,请始终在调用之前检查流数据是否可用,以避免尝试读取 XML 数据。hasNextnext

XmlStreamReader 示例

下面的示例处理一个 XML 字符串。

public class XmlStreamReaderDemo {

    // Create a class Book for processing
    public class Book {
        String name;
        String author;
    }

    public Book[] parseBooks(XmlStreamReader reader) {
        Book[] books = new Book[0];
        boolean isSafeToGetNextXmlElement = true;
        while(isSafeToGetNextXmlElement) {
            // Start at the beginning of the book and make sure that it is a book
            if (reader.getEventType() == XmlTag.START_ELEMENT) {
                if ('Book' == reader.getLocalName()) {
                    // Pass the book to the parseBook method (below) 
                    Book book = parseBook(reader);
                    books.add(book);
                }
            }
            // Always use hasNext() before calling next() to confirm 
            // that we have not reached the end of the stream
            if (reader.hasNext()) {
                reader.next();
            } else {
                isSafeToGetNextXmlElement = false;
                break;
            }
        }
        return books;
    }

    // Parse through the XML, determine the author and the characters
    Book parseBook(XmlStreamReader reader) {
        Book book = new Book();
        book.author = reader.getAttributeValue(null, 'author');
        boolean isSafeToGetNextXmlElement = true;
        while(isSafeToGetNextXmlElement) {
            if (reader.getEventType() == XmlTag.END_ELEMENT) {
                break;
            } else if (reader.getEventType() == XmlTag.CHARACTERS) {
                book.name = reader.getText();
            }
            // Always use hasNext() before calling next() to confirm 
            // that we have not reached the end of the stream
            if (reader.hasNext()) {
                reader.next();
            } else {
                isSafeToGetNextXmlElement = false;
                break;
            }
        }
        return book;
    }
}
@isTest
private class XmlStreamReaderDemoTest {
    // Test that the XML string contains specific values
    static testMethod void testBookParser() {

        XmlStreamReaderDemo demo = new XmlStreamReaderDemo();

        String str = '<books><book author="Chatty">Alpha beta</book>' +
            '<book author="Sassy">Baz</book></books>';

        XmlStreamReader reader = new XmlStreamReader(str);
        XmlStreamReaderDemo.Book[] books = demo.parseBooks(reader);

        System.debug(books.size());

        for (XmlStreamReaderDemo.Book book : books) {
            System.debug(book);
        }
    }
}

使用流编写 XML

XmlStreamWriter 类方法允许编写 XML 数据。这些方法与 HTTP 标注结合使用以构造 要在标注请求中发送到外部服务的 XML 文档。 下面的示例演示如何实例化新的 XmlStreamReader 对象:

String xmlString = '<books><book>My Book</book><book>Your Book</book></books>';
XmlStreamReader xsr = new XmlStreamReader(xmlString);

XML 编写器方法 例

下面的示例编写一个 XML 文档 并测试其有效性。

此 Hello World 示例需要自定义对象。您可以 您可以自行创建这些代码,或将对象和 Apex 代码下载为非托管包 来自 Salesforce AppExchange。要获取组织中的示例资产,请安装 Apex 教程包。此套餐 还包含装运发票示例的示例代码和对象。

public class XmlWriterDemo {

     public String getXml() {
          XmlStreamWriter w = new XmlStreamWriter();
          w.writeStartDocument(null, '1.0');
          w.writeProcessingInstruction('target', 'data');
          w.writeStartElement('m', 'Library', 'http://www.book.com');
          w.writeNamespace('m', 'http://www.book.com');
          w.writeComment('Book starts here');
          w.setDefaultNamespace('http://www.defns.com');
          w.writeCData('<Cdata> I like CData </Cdata>');
          w.writeStartElement(null, 'book', null);
          w.writedefaultNamespace('http://www.defns.com');
          w.writeAttribute(null, null, 'author', 'Manoj');
          w.writeCharacters('This is my book');
          w.writeEndElement(); //end book
          w.writeEmptyElement(null, 'ISBN', null);
          w.writeEndElement(); //end library
          w.writeEndDocument();
          String xmlOutput = w.getXmlString();
          w.close();
          return xmlOutput;
        }
}
@isTest
private class XmlWriterDemoTest {
    static TestMethod void basicTest() {
        XmlWriterDemo demo = new XmlWriterDemo();
        String result = demo.getXml();
        String expected = '<?xml version="1.0"?><?target data?>' +
            '<m:Library xmlns:m="http://www.book.com">' + 
            '<!--Book starts here-->' +
            '<![CDATA[<Cdata> I like CData </Cdata>]]>' +
'<book xmlns="http://www.defns.com" author="Manoj">This is my book</book><ISBN/></m:Library>';
        
        System.assert(result == expected);
    }
}

使用 DOM 读取和写入 XML

Apex 提供了使您能够使用 DOM 处理 XML 内容的类 (文档对象模型)。

DOM 类可帮助您解析或生成 XML 内容。您可以使用这些类来工作 替换为任何 XML 内容。一个常见的应用是使用类来生成正文 由 HttpRequest 创建的请求或解析 HttpResponse 访问的响应。The DOM 将 XML 文档表示为节点层次结构。某些节点可能是分支节点 并且有子节点,而其他节点是没有子节点的叶节点。您可以 分析嵌套的 XML 内容,该内容最多50节点 深。

DOM 类包含在命名空间中。Dom

使用 Document 类处理 XML 文档正文中的内容。

使用 XmlNode 类处理 XML 中的节点 公文。

使用 Document 类类处理 XML 内容。一个常见的应用是使用它 为 HttpRequest 创建请求的正文,或分析 HttpResponse 访问的响应。

XML 命名空间

XML 命名空间是由 URI 引用标识的名称集合,用于 XML 文档,用于唯一标识元素类型和属性名称。XML 中的名称 命名空间可能显示为限定名称,其中包含一个冒号,分隔 名称转换为命名空间前缀和本地部分。前缀,映射到 URI 引用,选择命名空间。通用管理 URI 的组合 命名空间和文档自己的命名空间生成通用的标识符 独特。

以下 XML 元素的命名空间和前缀为 。http://my.name.spacemyprefix

<sampleElement xmlns:myprefix="http://my.name.space" />

在下面的示例中,XML 元素具有两个属性:

  • 第一个属性的键为 ; 值为 。dimension2
  • 第二个属性的键命名空间为 http://ns1;这 value 命名空间为 http://ns2;关键是;值为 。exampletest
<square dimension="2" ns1:example="ns2:test" xmlns:ns1="http://ns1" xmlns:ns2="http://ns2" />

公文例

对于下面的示例,假定传递到方法中的参数返回此 XML 响应:urlparseResponseDom

<address>
    <name>Kirk Stevens</name>
    <street1>808 State St</street1>
    <street2>Apt. 2</street2>
    <city>Palookaville</city>
    <state>PA</state>
    <country>USA</country>
</address>

下面的示例演示如何使用 DOM 类来分析 XML 响应 在请求正文中返回:GET

public class DomDocument {
 
    // Pass in the URL for the request
    // For the purposes of this sample,assume that the URL
    // returns the XML shown above in the response body
    public void parseResponseDom(String url){
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        // url that returns the XML in the response body
        req.setEndpoint(url);
        req.setMethod('GET');
        HttpResponse res = h.send(req);
        Dom.Document doc = res.getBodyDocument();
        
        //Retrieve the root element for this document.
        Dom.XMLNode address = doc.getRootElement();
        
        String name = address.getChildElement('name', null).getText();
        String state = address.getChildElement('state', null).getText();
        // print out specific elements
        System.debug('Name: ' + name);
        System.debug('State: ' + state);
        
        // Alternatively, loop through the child elements.
        // This prints out all the elements of the address
        for(Dom.XMLNode child : address.getChildElements()) {
           System.debug(child.getText());
        }
    }
}

使用 XML 节点

使用该类处理 XML 文档。DOM 将 XML 文档表示为节点的层次结构。一些 节点可以是分支节点并具有子节点,而其他节点是没有子节点的叶节点 孩子。XmlNode

Apex 中有不同类型的 DOM 节点可用。 是这些不同类型的枚举。 这些值为:XmlNodeType

  • 评论
  • 元素
  • 发短信

区分 XML 文档中的元素和节点非常重要。这 下面是一个简单的 XML 示例:

<name>
    <firstName>Suvain</firstName>
    <lastName>Singh</lastName>
</name>

此示例包含三个 XML 元素:、 和 。它包含五个节点:三个节点 、 和 元素 节点,以及两个文本节点 – 和 .请注意,元素中的文本 节点被视为单独的文本节点。namefirstNamelastNamenamefirstNamelastNameSuvainSingh

有关所有枚举共享的方法的详细信息,请参阅枚举方法。

XmlNode的例

此示例演示如何使用方法 和命名空间来创建 XML 请求。XmlNode

public class DomNamespaceSample
{
    public void sendRequest(String endpoint)
    {
        // Create the request envelope
        DOM.Document doc = new DOM.Document();
        
        String soapNS = 'http://schemas.xmlsoap.org/soap/envelope/';
        String xsi = 'http://www.w3.org/2001/XMLSchema-instance';
        String serviceNS = 'http://www.myservice.com/services/MyService/';
        
        dom.XmlNode envelope
            = doc.createRootElement('Envelope', soapNS, 'soapenv');
        envelope.setNamespace('xsi', xsi);
        envelope.setAttributeNS('schemaLocation', soapNS, xsi, null);
        
        dom.XmlNode body
            = envelope.addChildElement('Body', soapNS, null);
        
        body.addChildElement('echo', serviceNS, 'req').
           addChildElement('category', serviceNS, null).
           addTextNode('classifieds');
        
        System.debug(doc.toXmlString());
        
        // Send the request
        HttpRequest req = new HttpRequest();
        req.setMethod('POST');
        req.setEndpoint(endpoint);
        req.setHeader('Content-Type', 'text/xml');
        
        req.setBodyDocument(doc);
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        System.assertEquals(200, res.getStatusCode());
        
        dom.Document resDoc = res.getBodyDocument();
        
        envelope = resDoc.getRootElement();
        
        String wsa = 'http://schemas.xmlsoap.org/ws/2004/08/addressing';
        
        dom.XmlNode header = envelope.getChildElement('Header', soapNS);
        System.assert(header != null);
        
        String messageId
            = header.getChildElement('MessageID', wsa).getText();
        
        System.debug(messageId);
        System.debug(resDoc.toXmlString());
        System.debug(resDoc);
        System.debug(header);
        
        System.assertEquals(
         'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
         header.getChildElement(
           'ReplyTo', wsa).getChildElement('Address', wsa).getText());
        
        
        System.assertEquals(
          envelope.getChildElement('Body', soapNS).
              getChildElement('echo', serviceNS).
              getChildElement('something', 'http://something.else').
              getChildElement(
                'whatever', serviceNS).getAttribute('bb', null),
                'cc');
        
        System.assertEquals('classifieds',
          envelope.getChildElement('Body', soapNS).
              getChildElement('echo', serviceNS).
              getChildElement('category', serviceNS).getText());
    }
}

JSON 支持

Apex 中的 JavaScript 对象表示法 (JSON) 支持可实现 Apex 的序列化 对象转换为JSON格式,并对序列化的JSON内容进行反序列化。Apex 提供了一组类,用于公开 JSON 序列化的方法和 反序列化。下表描述了可用的类。

描述
System.JSON包含用于将 Apex 对象序列化为 JSON 格式的方法,以及 反序列化使用此类中的方法序列化的 JSON 内容。serialize
System.JSONGenerator包含用于使用标准将对象序列化为 JSON 内容的方法 JSON 编码。
System.JSONParser表示 JSON 编码内容的分析器。

枚举包含标记 用于 JSON 解析。System.JSONToken

这些类中的方法会抛出一个 if 在执行过程中遇到问题。JSONExceptionJSON 支持注意事项

  • JSON 序列化和反序列化支持可用于 sObjects(标准 对象和自定义对象)、Apex 原语和集合类型、返回类型 数据库方法(例如 SaveResult 和 DeleteResult)和 Apex 实例 类。
  • 只有托管包的自定义对象(类型)才能从以下代码进行序列化: 托管包的外部。作为 中定义的 Apex 类实例的对象 无法序列化托管包。sObject
  • 仅当 Map 对象使用 以下数据类型作为键。
    • 布尔
    • 日期
    • 日期时间
    • 十进制
    • 枚举
    • 同上
    • 整数
    • 字符串
    • 时间
  • 当对象被声明为父类型,但被设置为 子类型,某些数据可能会丢失。该对象被序列化和反序列化为 父类型和特定于子类型的任何字段都将丢失。
  • 具有自身引用的对象不会被序列化,并导致抛出。JSONException
  • 两次引用同一对象的引用图将被反序列化并导致 要生成的引用对象的多个副本。
  • 数据类型不是 序列 化。如果尝试创建可序列化类的实例,例如 Visualforce 控制器,其成员变量类型为 ,您会收到异常。若要在可序列化类中使用,请使用 local 变量。System.JSONParserSystem.JSONParserJSONParser

版本化行为更改

在 API 版本 53.0 及更高版本中,DateTime 格式和处理已更新。The API 正确处理 JSON 请求中使用 3 位以上数字的 JSON请求中的DateTime值 小数点。使用不支持的 DateTime 格式(如 )的请求会导致错误。Salesforce 建议您 严格遵守 https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_valid_date_formats.htm 中指定的 DateTime 格式。123456000

  • 往返序列化和反序列化
    使用类方法对 JSON 内容执行往返序列化和反序列化。通过这些方法,可以将对象序列化为 JSON 格式的字符串,并将 JSON 字符串反序列化回对象。JSON
  • JSON 生成器
    使用类方法,可以生成标准的 JSON 编码内容。JSONGenerator
  • JSON 解析
    使用类方法解析 JSON 编码的内容。通过这些方法,可以分析从对外部服务(如 Web 服务标注)的调用返回的 JSON 格式的响应。JSONParser

往返序列化和反序列化

使用类方法执行往返 JSON 内容的序列化和反序列化。这些方法使您能够 将对象序列化为 JSON 格式的字符串,并将 JSON 字符串反序列化回 对象。

JSON

示例:序列化和反序列化发票列表

此示例创建一个对象列表并序列化该列表。接下来,序列化 JSON 字符串用于再次反序列化列表,示例验证 新列表包含与原始列表中相同的发票 列表。

InvoiceStatement

public class JSONRoundTripSample {
  
    public class InvoiceStatement {
        Long invoiceNumber;
        Datetime statementDate;
        Decimal totalPrice;
        
        public InvoiceStatement(Long i, Datetime dt, Decimal price)
        {
            invoiceNumber = i;
            statementDate = dt;
            totalPrice = price;
        }
    }
    
    public static void SerializeRoundtrip() {
        Datetime dt = Datetime.now(); 
        // Create a few invoices.
        InvoiceStatement inv1 = new InvoiceStatement(1,Datetime.valueOf(dt),1000);
        InvoiceStatement inv2 = new InvoiceStatement(2,Datetime.valueOf(dt),500);
        // Add the invoices to a list.
        List<InvoiceStatement> invoices = new List<InvoiceStatement>();
        invoices.add(inv1);
        invoices.add(inv2);
              
        // Serialize the list of InvoiceStatement objects.
        String JSONString = JSON.serialize(invoices);
        System.debug('Serialized list of invoices into JSON format: ' + JSONString);
        
        // Deserialize the list of invoices from the JSON string.
        List<InvoiceStatement> deserializedInvoices = 
          (List<InvoiceStatement>)JSON.deserialize(JSONString, List<InvoiceStatement>.class);
        System.assertEquals(invoices.size(), deserializedInvoices.size());
        Integer i=0;
        for (InvoiceStatement deserializedInvoice :deserializedInvoices) {
            system.debug('Deserialized:' + deserializedInvoice.invoiceNumber + ',' 
            + deserializedInvoice.statementDate.formatGmt('MM/dd/yyyy  HH:mm:ss.SSS')
            + ', ' + deserializedInvoice.totalPrice); 
            system.debug('Original:' + invoices[i].invoiceNumber + ',' 
            + invoices[i].statementDate.formatGmt('MM/dd/yyyy  HH:mm:ss.SSS') 
            + ', ' + invoices[i].totalPrice); 
            i++;
        }
    }
}

JSON 序列化注意事项

该方法的行为不同 取决于保存的 Apex 代码的 Salesforce API 版本。serialize使用设置其他字段的查询 sObject 的序列化对于使用 Salesforce API 版本 27.0 及更早版本保存的 Apex,如果查询 sObjects 设置了其他字段,这些字段不包含在 方法返回的序列化 JSON 字符串。从使用 Salesforce 保存的 Apex 开始 API 版本 28.0,其他字段包含在序列化的 JSON 中 字符串。serialize本示例在查询联系人后向该联系人添加一个字段,然后 序列化联系人。断言语句验证 JSON string 包含附加字段。断言传递 Apex 保存 使用 Salesforce API 版本 28.0 和 后。

Contact con = [SELECT Id, LastName, AccountId FROM Contact LIMIT 1]; 
// Set additional field
con.FirstName = 'Joe'; 
String jsonstring = Json.serialize(con); 
System.debug(jsonstring); 
System.assert(jsonstring.contains('Joe') == true);

聚合查询结果字段的序列化对于使用 Salesforce API 版本 27.0 保存的 Apex,聚合结果 在以下情况下,查询不包括 SELECT 语句中的字段 使用该方法序列化。对于早期 API 版本或 API 版本 28.0 及更高版本, 序列化聚合查询结果包括 SELECT 中的所有字段 陈述。serialize此聚合查询返回两个字段:ID 字段计数和 帐户名称。

String jsonString = JSON.serialize(
    Database.query('SELECT Count(Id),Account.Name FROM Contact WHERE Account.Name != null GROUP BY Account.Name LIMIT 1'));
    System.debug(jsonString);

// Expected output in API v 26 and earlier or v28 and later
// [{"attributes":{"type":"AggregateResult"},"expr0":2,"Name":"acct1"}]

空字段的序列化从 API 版本 28.0 开始,null 字段不会序列化,也不会 包含在 JSON 字符串中,这与早期版本不同。此更改不会 影响使用 JSON 方法(例如 Json.deserialize())反序列化 JSON 字符串。此更改是 检查 JSON 字符串时会注意到这一点。为 例:

String jsonString = JSON.serialize(
                 [SELECT Id, Name, Website FROM Account WHERE Website = null LIMIT 1]);
System.debug(jsonString);

// In v27.0 and earlier, the string includes the null field and looks like the following.
// {"attributes":{...},"Id":"001D000000Jsm0WIAR","Name":"Acme","Website":null}

// In v28.0 and later, the string doesn’t include the null field and looks like 
//  the following.
// {"attributes":{...},"Name":"Acme","Id":"001D000000Jsm0WIAR"}}

ID 序列化在 API 版本 34.0 及更早版本中,对于已通过 往返 JSON 序列化和反序列化。==

JSON 反序列化注意事项

聚合结果中的 JSON 无法反序列化回 Apex AggregateResult 对象,因为它们没有命名字段。

JSON生成器

使用类方法,可以生成标准的 JSON 编码内容。

JSONGenerator

您可以使用标准逐个元素构造 JSON 内容 JSON 编码。为此,请使用类中的方法。JSONGenerator

JSONGenerator 示例

此示例生成 使用类的方法以漂亮的打印格式创建 JSON 字符串。首先举个例子 添加一个数字字段和一个字符串字段,然后添加一个要包含的字段 整数列表的 object 字段,该字段已正确反序列化。 接下来,它添加对象 进入现场,其中 也会被反序列化。JSONGeneratorAObject A

public class JSONGeneratorSample{

    public class A { 
        String str;
        
        public A(String s) { str = s; }
    }

    static void generateJSONContent() {
        // Create a JSONGenerator object.
        // Pass true to the constructor for pretty print formatting.
        JSONGenerator gen = JSON.createGenerator(true);
        
        // Create a list of integers to write to the JSON string.
        List<integer> intlist = new List<integer>();
        intlist.add(1);
        intlist.add(2);
        intlist.add(3);
        
        // Create an object to write to the JSON string.
        A x = new A('X');
        
        // Write data to the JSON string.
        gen.writeStartObject();
        gen.writeNumberField('abc', 1.21);
        gen.writeStringField('def', 'xyz');
        gen.writeFieldName('ghi');
        gen.writeStartObject();
        
        gen.writeObjectField('aaa', intlist);
        
        gen.writeEndObject();
        
        gen.writeFieldName('Object A');
        
        gen.writeObject(x);
        
        gen.writeEndObject();
        
        // Get the JSON string.
        String pretty = gen.getAsString();
        
        System.assertEquals('{\n' +
        '  "abc" : 1.21,\n' +
        '  "def" : "xyz",\n' +
        '  "ghi" : {\n' +
        '    "aaa" : [ 1, 2, 3 ]\n' +
        '  },\n' +
        '  "Object A" : {\n' +
        '    "str" : "X"\n' +
        '  }\n' +
        '}', pretty);
    }
}

JSON解析

使用类方法进行解析 JSON 编码的内容。这些方法使你能够分析 JSON 格式的响应,该响应是 从对外部服务(如 Web 服务标注)的调用返回。

JSONParser

以下示例演示如何分析 JSON 字符串。

示例:解析来自 Web 服务标注的 JSON 响应

此示例使用方法分析 JSON 格式的响应。它对返回 JSON 格式的响应。接下来,解析响应以从 api 构建映射 版本号添加到版本标签中。JSONParser

public class JSONParserUtil {
    public static void parseJSONResponse() {        
        
        // Create HTTP request to send.
        HttpRequest request = new HttpRequest();
        // Set the endpoint URL.
        String endpoint = URL.getOrgDomainUrl().toExternalForm() + '/services/data';
        request.setEndPoint(endpoint);
        // Set the HTTP verb to GET.
        request.setMethod('GET');
        // Set the request header for JSON content type
        request.setHeader('Accept', 'application/json');
        
        // Send the HTTP request and get the response.
        // The response is in JSON format.
        Http httpProtocol = new Http();
        HttpResponse response = httpProtocol.send(request);
        System.debug(response.getBody());
        /* The JSON response returned is the following:
            {"label":"Summer '14","url":"/services/data/v31.0","version":"31.0"},
            {"label":"Winter '15","url":"/services/data/v32.0","version":"32.0"},
            {"label":"Spring '15","url":"/services/data/v33.0","version":"33.0"},
        */
        // Parse JSON response to build a map from API version numbers to labels
        JSONParser parser = JSON.createParser(response.getBody());
        Map<double, string> apiVersionToReleaseNameMap = new Map<double, string>();
        
        string label = null;
        double version = null;
        
        while (parser.nextToken() != null) {
            
            if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                switch on parser.getText() {
                    when 'label' {
                    // Advance to the label value.
                    parser.nextToken();
                        label = parser.getText();
                    }
                    when 'version' {
                        // Advance to the version value.
                        parser.nextToken();
                        version = Double.valueOf(parser.getText());
                    }
                }
            }
            
            if(version != null && String.isNotEmpty(label)) {
                apiVersionToReleaseNameMap.put(version, label);
                version = null;
                label = null; 
            }
        }
        system.debug('Release with Rainbow logo = ' +
            apiVersionToReleaseNameMap.get(39.0D));
    }
}

示例:解析 JSON 字符串并将其反序列化为对象

此示例使用硬编码的 JSON 字符串,该字符串与 返回的 JSON 字符串相同 上一示例中的标注。在此示例中,将分析整个字符串 使用该方法添加到对象中。 此代码还使用 跳过子数组和子对象,并解析 列表。分析的对象是定义为内部类的类的实例。因为每张发票 包含行项,表示相应行项类型的类,该类,也被定义为 内部类。将此示例代码添加到要使用的类中 它。

InvoicereadValueAsskipChildrenInvoiceLineItem

public static void parseJSONString() {
    String jsonStr = 
        '{"invoiceList":[' +
        '{"totalPrice":5.5,"statementDate":"2011-10-04T16:58:54.858Z","lineItems":[' +
            '{"UnitPrice":1.0,"Quantity":5.0,"ProductName":"Pencil"},' +
            '{"UnitPrice":0.5,"Quantity":1.0,"ProductName":"Eraser"}],' +
                '"invoiceNumber":1},' +
        '{"totalPrice":11.5,"statementDate":"2011-10-04T16:58:54.858Z","lineItems":[' +
            '{"UnitPrice":6.0,"Quantity":1.0,"ProductName":"Notebook"},' +
            '{"UnitPrice":2.5,"Quantity":1.0,"ProductName":"Ruler"},' +
            '{"UnitPrice":1.5,"Quantity":2.0,"ProductName":"Pen"}],"invoiceNumber":2}' +
        ']}';

    // Parse entire JSON response.
    JSONParser parser = JSON.createParser(jsonStr);
    while (parser.nextToken() != null) {
        // Start at the array of invoices.
        if (parser.getCurrentToken() == JSONToken.START_ARRAY) {
            while (parser.nextToken() != null) {
                // Advance to the start object marker to
                //  find next invoice statement object.
                if (parser.getCurrentToken() == JSONToken.START_OBJECT) {
                    // Read entire invoice object, including its array of line items.
                    Invoice inv = (Invoice)parser.readValueAs(Invoice.class);
                    system.debug('Invoice number: ' + inv.invoiceNumber);
                    system.debug('Size of list items: ' + inv.lineItems.size());
                    // For debugging purposes, serialize again to verify what was parsed.
                    String s = JSON.serialize(inv);
                    system.debug('Serialized invoice: ' + s);

                    // Skip the child start array and start object markers.
                    parser.skipChildren();
                }
            }
        }
    }
} 

// Inner classes used for serialization by readValuesAs(). 

public class Invoice {
    public Double totalPrice;
    public DateTime statementDate;
    public Long invoiceNumber;
    List<LineItem> lineItems;
    
    public Invoice(Double price, DateTime dt, Long invNumber, List<LineItem> liList) {
        totalPrice = price;
        statementDate = dt;
        invoiceNumber = invNumber;
        lineItems = liList.clone();
    }
}  

public class LineItem {
    public Double unitPrice;
    public Double quantity;
    public String productName;
}

集成和 Apex 实用程序

Apex 允许您使用标注与外部 SOAP 和 REST Web 服务集成。 您可以使用用于 JSON、XML、数据安全和编码的实用程序。通用实用程序 对于带有文本字符串的正则表达式,也提供了。

  • 使用 Apex 调用标注
  • JSON 支持 Apex 中的 JavaScript 对象表示法 (JSON) 支持
    将 Apex 对象序列化为 JSON 格式,并对序列化的 JSON 内容进行反序列化。
  • XML Support
    Apex 提供实用程序类,这些类支持使用流和 DOM 创建和解析 XML 内容。
  • 保护数据
    可以使用类提供的方法保护数据。Crypto
  • 对数据
    进行编码 可以使用类提供的方法对 URL 进行编码和解码,并将字符串转换为十六进制格式。EncodingUtil
  • 使用模式和匹配器 Apex 提供了模式和匹配器
    ,使您能够使用正则表达式搜索文本。

使用 Apex 调用标注

借助 Apex 标注,您可以通过以下方式将 Apex 与外部服务紧密集成 调用外部 Web 服务或从 Apex 代码发送 HTTP 请求,然后接收 响应。Apex 提供与利用 SOAP 和 WSDL 或 HTTP 的 Web 服务的集成 服务(RESTful 服务)。

注意

在任何 Apex 标注可以调用外部站点之前,该站点 必须在“远程站点设置”页中注册,否则标注将失败。Salesforce的 防止调用未经授权的网络地址。

如果标注指定了命名 凭据作为终结点,则无需配置远程站点设置。一个命名的 credential 指定标注端点的 URL 及其所需的身份验证 一个定义中的参数。若要设置命名凭据,请参阅“定义命名凭据” 在 Salesforce 帮助中。

若要详细了解标注类型,请参阅:

  • SOAP 服务:从 WSDL 文档定义类
  • 调用 HTTP 标注
  • 异步 长时间运行的请求的标注

提示

标注使 Apex 能够调用外部 Web 或 HTTP 服务。Apex Web 服务允许外部应用程序调用 Apex 方法 通过 Web 服务。

  1. 添加远程站点设置
  2. 命名凭据作为标注端点 命名凭据在一个定义中指定标注端点
    的 URL 及其所需的身份验证参数。Salesforce 管理将指定命名凭据作为标注端点的 Apex 标注的所有身份验证,以便您的代码不必这样做。对于命名凭据中定义的站点,还可以跳过远程站点设置,否则,外部站点的标注将需要这些设置。
  3. SOAP 服务:从 WSDL 文档定义类
  4. 调用 HTTP 标注
  5. 使用证书
  6. 标注限制和局限性
  7. 使用延续创建长时间运行的标注
    使用异步标注从 Visualforce 页面或 Lightning 组件向外部 Web 服务发出长时间运行的请求,并在回调方法中处理响应。

添加远程站点设置

在任何 Apex 标注可以调用外部站点之前,该站点 必须在“远程站点设置”页中注册,否则标注将失败。Salesforce的 防止调用未经授权的网络地址。

注意

如果标注指定命名凭据作为终结点,则无需配置 远程站点设置。命名凭据指定标注端点的 URL 及其 一个定义中所需的身份验证参数。若要设置命名凭据,请参阅“定义” 命名凭据”。

若要添加远程站点设置,请执行以下操作:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“远程站点设置”。Remote Site Settings
  2. 单击“新建远程站点”。
  3. 输入“远程站点名称”的描述性术语。
  4. 输入远程站点的 URL。
  5. (可选)输入站点的描述。
  6. 点击保存

提示

为了获得最佳性能,请验证远程 HTTPS 加密站点是否具有 OCSP (联机证书状态协议)装订已打开。

作为标注端点的命名凭据

命名凭据指定标注端点的 URL 及其所需的 URL 一个定义中的身份验证参数。Salesforce 管理 Apex 的所有身份验证 将命名凭据指定为标注终结点的标注,以便代码没有 自。您还可以跳过远程站点设置,否则,外部标注将需要这些设置 sites,用于命名凭据中定义的站点。

命名凭据还包括可用于路由的 OutboundNetworkConnection 字段 通过专用连接进行标注。通过将端点 URL 和身份验证与 标注定义、命名凭据使标注更易于维护。例如,如果 终结点 URL 更改时,仅更新命名凭据。引用命名的所有标注 凭据只是继续工作。

如果您有多个组织,则可以创建具有相同名称但具有 每个组织中不同的端点 URL。然后,您可以在所有 orgs – 一个标注定义,引用这些命名凭据的共享名称。 例如,每个组织中的命名凭据可以具有不同的终结点 URL 来容纳 开发和生产环境的差异。如果 Apex 标注指定了共享的 这些命名凭据的名称,定义标注的 Apex 类可以打包,并且 部署在所有这些组织上,而无需以编程方式检查环境。

若要从标注定义中引用命名凭据,请使用命名凭据 URL。一个 命名凭证 URL 包含方案、名称 的命名凭据和可选路径。例如:。callout:callout:My_Named_Credential/some_path

您可以将查询字符串追加到命名凭据 URL。使用问号 (?) 作为命名凭据 URL 之间的分隔符 和查询字符串。例如:。callout:My_Named_Credential/some_path?format=json

在以下 Apex 代码中,命名凭据和附加路径指定标注的 端点。

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/some_path');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());

引用的命名凭据指定终结点 URL 和外部凭据 指定身份验证设置。

命名凭据详细信息页

无论您使用哪种身份验证,Apex 代码都保持不变。这 外部凭据中的身份验证设置不同,外部凭据引用身份验证 组织中定义的提供程序。

具有 OAuth 选项的命名凭据身份验证设置

在 相比之下,让我们看看没有命名凭据的 Apex 代码是什么样子的。请注意, 代码处理身份验证变得更加复杂,即使我们坚持使用基本密码 认证。编码 OAuth 甚至更加复杂,是命名 凭据。

HttpRequest req = new HttpRequest();
req.setEndpoint('https://my_endpoint.example.com/some_path');
req.setMethod('GET');

// Because we didn't set the endpoint as a named credential, 
// our code has to specify:
// - The required username and password to access the endpoint
// - The header and header information
 
String username = 'myname';
String password = 'mypwd';
  
Blob headerValue = Blob.valueOf(username + ':' + password);
String authorizationHeader = 'BASIC ' +
EncodingUtil.base64Encode(headerValue);
req.setHeader('Authorization', authorizationHeader);
   
// Create a new http object to send the request object
// A response object is generated as a result of the request  
  
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());
  1. 使用命名凭据
    的 Apex 标注的自定义标头和正文 Salesforce 为命名凭据定义的端点的每个标注生成一个标准授权标头,但您可以禁用此选项。您的 Apex 代码还可以使用合并字段来构造每个标注的 HTTP 标头和正文。
  2. 使用命名凭证
    的 Apex 标注的合并字段 要构造 HTTP 标头并向指定为命名凭证的端点发出标注正文,请在 Apex 代码中使用这些合并字段。

使用 named 的 apex 标注的自定义标头和正文 凭据

Salesforce 为每个标注生成一个标准授权标头,以 named-credential-defined 端点,但您可以禁用此选项。您的 Apex 代码也可以使用 merge 字段来构造每个标注的 HTTP 标头和正文。

这种灵活性使您能够在特殊情况下使用命名凭据。例如,一些 远程终结点需要在请求标头中使用安全令牌或加密凭据。一些偏远的 终结点需要 XML 或 JSON 消息正文中的用户名和密码。自定义标注 标题和正文。

Salesforce 管理员必须设置命名凭据,以允许 Apex 代码构造标头 或在 HTTP 标头或正文中使用合并字段。下表描述了这些标注 命名凭据的选项。

描述
生成授权 页眉默认情况下,Salesforce 会生成一个授权标头,并将其应用于每个标注 引用命名凭据。仅当出现以下情况之一时才取消选择此选项 声明适用。远程终结点不支持授权标头。授权标头通过其他方式提供。例如,在 Apex 标注中, 开发人员可以让代码为每个 标注。如果从外部引用命名凭据,则此选项是必需的 数据源。
允许在 HTTP 中合并字段 页眉允许合并 HTTP 正文中的字段在每个 Apex 标注中,代码指定 HTTP 标头和请求正文的 构建。例如,Apex 代码可以在授权中设置 cookie 的值 页眉。这些选项使 Apex 代码能够使用合并字段来填充 HTTP 标头 并在进行标注时请求带有组织数据的正文。这些选项不是 如果从外部数据源引用命名凭据,则可用。

合并使用命名凭证的 Apex 标注的字段

构造 HTTP 标头和指向端点的标注正文 指定为命名凭据,请在 Apex 代码中使用这些合并字段。

合并字段描述
{!$Credential.Username}{!$Credential.Password}正在运行的用户的用户名和密码。仅当命名凭据使用 密码 认证。// non-standard authentication req.setHeader('X-Username', '{!$Credential.Username}'); req.setHeader('X-Password', '{!$Credential.Password}');
{!$Credential.OAuthToken}正在运行的用户的 OAuth 令牌。仅当命名凭据使用 OAuth 时才可用 认证。req.setHeader('Authorization', '{!$Credential.OAuthToken}');
{!$Credential.AuthorizationMethod}有效值取决于 命名凭据的身份验证协议。Basic—密码验证Bearer– OAuth 2.0null– 无身份验证
{!$Credential.AuthorizationHeaderValue}有效值取决于命名凭据的身份验证协议。Base-64 encoded username and password—密码验证OAuth token– OAuth 2.0null– 无身份验证
{!$Credential.OAuthConsumerKey}使用者密钥。仅当命名凭据使用 OAuth 身份验证时才可用。

注意

  • 在标注的 HTTP 请求正文中使用这些合并字段时,可以应用公式函数来转义特殊字符。 不支持其他公式函数,并且不能用于 HTTP 标头中的合并字段。以下示例对特殊字符进行转义 在 凭据。HTMLENCODEHTMLENCODEreq.setBody('Username:{!HTMLENCODE($Credential.Username)}') req.setBody('Password:{!HTMLENCODE($Credential.Password)}')
  • 在 SOAP API 调用中使用这些合并字段时,OAuth 访问令牌不会 刷新。

SOAP 服务:从 WSDL 文档定义类

可以从 WSDL 文档自动生成类,该文档具有 存储在本地硬盘驱动器或网络上。通过使用 WSDL 创建类 文档允许开发人员在其 Apex 代码中对外部 Web 服务进行标注。

注意

如果可能,请使用出站消息传递来处理集成解决方案。使用标注可以 仅在必要时才使用第三方 Web 服务。

要从 WSDL 生成 Apex 类,请执行以下操作:

  1. 在应用程序中,从“设置”中输入“快速查找”框,然后选择“Apex 类”。Apex Classes
  2. 单击“从 WSDL 生成”。
  3. 单击 Browse 导航到本地硬盘上的 WSDL 文档 驱动器或网络,或键入完整路径。此 WSDL 文档是 Apex 的基础 您正在创建的类。注意您指定的 WSDL 文档可能包含一个 SOAP 端点位置,该位置 引用出站端口。出于安全原因,Salesforce 限制了出站端口 您可以指定以下选项之一:
    • 80:此端口仅接受 HTTP 连接。
    • 443:此端口仅接受 HTTPS 连接。
    • 1024–66535(含):这些端口接受 HTTP 或 HTTPS 连接。
  4. 单击解析 WSDL 以验证 WSDL 文档内容。这 应用程序为 WSDL 文档中的每个名称空间生成一个缺省类名,并且 报告任何错误。如果 WSDL 包含以下模式类型或构造,则解析将失败 不受 Apex 类支持,或者如果生成的类超过 100 万个字符 对 Apex 类的限制。例如,无法解析 Salesforce SOAP API WSDL。
  5. 根据需要修改类名。虽然您可以保存多个 通过对每个命名空间 Apex 使用相同的类名,将 WSDL 命名空间转换为单个类 类的总长度不能超过 100 万个字符。
  6. 单击 Generate Apex。向导的最后一页显示哪个 已成功生成类,以及来自其他类的任何错误。该页面还 提供用于查看成功生成的代码的链接。

成功生成的 Apex 类包括用于调用 由 WSDL 文档表示的第三方 Web 服务。这些类允许您调用 来自 Apex 的外部 Web 服务。对于每个生成的类,将创建第二个类,并使用 同名且前缀为 .第一类是 用于同步标注。第二类用于异步标注。更多信息 关于异步标注,请参阅使用延续创建长时间运行的标注。Async

关于生成的 Apex,请注意以下几点:

  • 如果 WSDL 文档包含 Apex 保留字,则在生成 Apex 类时会追加该字。例如,在 WSDL 文档中转换为在生成的 Apex 类中。请参阅保留关键字。详情请见 处理 WSDL 中 Apex 变量不支持的元素名称中的字符 名称,请参阅使用 WSDL 的注意事项。_xlimitlimit_x
  • 如果 WSDL 中的操作具有包含多个元素的输出消息,那么 生成的 Apex 将元素包装在内部类中。表示 WSDL 操作返回内部类,而不是单个元素。
  • 由于 Apex 类中不允许使用句点 () names,则用于生成 Apex 类的 WSDL 名称中的任何句点都将替换为下划线 () 在生成的 Apex 代码中。._

从 WSDL 生成类后,可以调用外部服务 由 WSDL 引用。

注意

在使用本主题其余部分中的示例之前,必须从生成的 WSDL2Apex 代码中复制 Apex 类,并将其添加到 组织。docSampleClass

调用外部服务

在使用外部服务的 WSDL 文档后调用外部服务,以执行以下操作 生成一个 Apex 类,在 Apex 代码中创建存根的实例,然后调用 方法。例如,要从 Apex 调用 StrikeIron IP 地址查找服务,您可以 编写类似于以下内容的代码:

  // Create the stub
  strikeironIplookup.DNSSoap dns = new strikeironIplookup.DNSSoap();

  // Set up the license header
  dns.LicenseInfo = new strikeiron.LicenseInfo();
  dns.LicenseInfo.RegisteredUser = new strikeiron.RegisteredUser();
  dns.LicenseInfo.RegisteredUser.UserID = 'you@company.com';
  dns.LicenseInfo.RegisteredUser.Password = 'your-password';

  // Make the Web service call
  strikeironIplookup.DNSInfo info = dns.DNSLookup('www.myname.com');

HTTP 标头支持

可以在 Web 服务标注上设置 HTTP 标头。例如,您可以使用此功能 在授权标头中设置 Cookie 的值。要设置 HTTP 标头,请将 和 添加到存根。inputHttpHeaders_xoutputHttpHeaders_x

注意

在 API 版本 16.0 及更早版本中,HTTP 响应 标注始终使用 UTF-8 进行解码,而不考虑 Content-Type 标头。在 API 中 版本 17.0 及更高版本中,HTTP 响应使用 Content-Type 标头。

以下示例使用生成的 WSDL2Apex 代码中的示例 WSDL 文件:

在 Web 服务标注上发送 HTTP 标头

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.inputHttpHeaders_x = new Map<String, String>();

//Setting a basic authentication header

stub.inputHttpHeaders_x.put('Authorization', 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==');

//Setting a cookie header
stub.inputHttpHeaders_x.put('Cookie', 'name=value');

//Setting a custom HTTP header
stub.inputHttpHeaders_x.put('myHeader', 'myValue');

String input = 'This is the input string';
String output = stub.EchoString(input);

如果指定了 for 的值,则 覆盖标准标头集。inputHttpHeaders_x

从 Web 服务标注响应访问 HTTP 响应标头

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.outputHttpHeaders_x = new Map<String, String>();
String input = 'This is the input string';
String output = stub.EchoString(input);

//Getting cookie header
String cookie = stub.outputHttpHeaders_x.get('Set-Cookie');

//Getting custom header
String myHeader = stub.outputHttpHeaders_x.get('My-Header');

的值为 null 违约。您必须在之前设置 您可以访问响应中标头的内容。outputHttpHeaders_xoutputHttpHeaders_x

支持的 WSDL 功能

Apex 仅支持文档文本包装的 WSDL 样式以及以下基元和 内置数据类型:

架构类型顶点类型
xsd:anyURI字符串
xsd:boolean布尔
xsd:date日期
xsd:dateTime日期时间
xsd:double
xsd:float
xsd:int整数
xsd:integer整数
xsd:language字符串
xsd:long
xsd:Name字符串
xsd:NCName字符串
xsd:nonNegativeInteger整数
xsd:NMTOKEN字符串
xsd:NMTOKENS字符串
xsd:normalizedString字符串
xsd:NOTATION字符串
xsd:positiveInteger整数
xsd:QName字符串
xsd:short整数
xsd:string字符串
xsd:time日期时间
xsd:token字符串
xsd:unsignedInt整数
xsd:unsignedLong
xsd:unsignedShort整数

注意

Salesforce 数据类型 anyType 在用于 生成使用 API 版本 15.0 及更高版本保存的 Apex 代码。对于使用 API 保存的代码 版本 14.0 及更早版本,anyType 映射到 String。

Apex 还支持以下架构构造:

  • xsd:all,在使用 API 版本保存的 Apex 代码中 15.0 及更高版本
  • xsd:annotation,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:attribute,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:choice,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:element.在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本,该属性也是 受以下限制支持:ref
    • 您不能在不同的 命名空间。ref
    • 全局元素不能使用 。ref
    • 如果一个元素包含 ,则它不能也 contain 或 .refnametype
  • xsd:sequence

仅当用作呼入时才支持以下数据类型,即 当外部 Web 服务调用 Apex Web 服务方法时。这些数据类型不是 支持作为标注,即当 Apex Web 服务方法调用 外部 Web 服务。

  • 斑点
  • 十进制
  • 枚举

Apex 不支持任何其他 WSDL 构造、类型或服务,包括:

  • RPC/编码服务
  • 具有多个 、 多个 的 WSDL 文件 服务或多个绑定portTypes
  • 导入外部模式的 WSDL 文件。例如,以下 WSDL 片段 导入外部架构,该架构不是 支持:<wsdl:types> <xsd:schema elementFormDefault="qualified" targetNamespace="http://s3.amazonaws.com/doc/2006-03-01/"> <xsd:include schemaLocation="AmazonS3.xsd"/> </xsd:schema> </wsdl:types>但是,支持在同一架构中导入。在以下内容中 例如,外部 WSDL 被粘贴到您所在的 WSDL 中 转换:<wsdl:types> <xsd:schema xmlns:tns="http://s3.amazonaws.com/doc/2006-03-01/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://s3.amazonaws.com/doc/2006-03-01/"> <xsd:element name="CreateBucket"> <xsd:complexType> <xsd:sequence> [...] </xsd:schema> </wsdl:types>
  • 上表中未记录的任何架构类型
  • 超过大小限制的 WSDL,包括 Salesforce WSDL
  • 不使用文档文本换行样式的 WSDL。以下 WSDL 代码段不使用文档文字换行样式,并导致“无法 查找 complexType“错误时 洋。<wsdl:types> <xsd:schema targetNamespace="http://test.org/AccountPollInterface/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="SFDCPollAccountsResponse" type="tns:SFDCPollResponse"/> <xsd:simpleType name="SFDCPollResponse"> <xsd:restriction base="xsd:string" /> </xsd:simpleType> </xsd:schema> </wsdl:types>此修改后的版本将元素包装为 包含元素序列。这遵循文档文本样式,并且是 支持。simpleTypecomplexType<wsdl:types> <xsd:schema targetNamespace="http://test.org/AccountPollInterface/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="SFDCPollAccountsResponse" type="tns:SFDCPollResponse" /> <xsd:complexType name="SFDCPollResponse"> <xsd:sequence> <xsd:element name="SFDCOutput" type="xsd:string" /> </xsd:sequence> </xsd:complexType> </xsd:schema> </wsdl:types>
  1. 生成的 WSDL2Apex 代码
    您可以使用 WSDL2Apex 工具从 WSDL 文档生成 Apex 类。WSDL2Apex 工具是开源的,可在 GitHub 上找到。
  2. 测试 Web 服务标注
    生成的代码将另存为 Apex 类,其中包含可用于调用 Web 服务的方法。若要部署或打包此 Apex 类和其他随附代码,75% 的代码必须具有测试覆盖率,包括生成的类中的方法。默认情况下,测试方法不支持 Web 服务标注,并且执行 Web 服务标注的测试会失败。为了防止测试失败并增加代码覆盖率,Apex 提供了内置接口和方法。在测试方法中使用和接收虚假响应。WebServiceMockTest.setMockWebServiceMockTest.setMock
  3. 执行 DML 操作和模拟标注
  4. 使用 WSDL 的注意事项

生成的 WSDL2Apex 代码

您可以使用 WSDL2Apex 工具从 WSDL 文档生成 Apex 类。这 WSDL2Apex 工具是开源的,可在 GitHub 上找到。

您可以在 GitHub 上的 WSDL2Apex 存储库中找到并参与 WSDL2Apex 源代码。

下面的示例演示如何从 WSDL 文档创建 Apex 类。顶点 类是在导入 WSDL 时自动生成的。下面的代码显示了一个示例 WSDL 公文。

<wsdl:definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tns="http://doc.sample.com/docSample"
targetNamespace="http://doc.sample.com/docSample"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">

<!-- Above, the schema targetNamespace maps to the Apex class name. -->


<!-- Below, the type definitions for the parameters are listed. 
     Each complexType and simpleType parameteris mapped to an Apex class inside the parent class for the WSDL.  Then, each element in the complexType is mapped to a public field inside the class. -->

<wsdl:types>
<s:schema elementFormDefault="qualified"
targetNamespace="http://doc.sample.com/docSample">
<s:element name="EchoString">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="input" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="EchoStringResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="EchoStringResult"
type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</wsdl:types>

<!--The stub below defines operations. -->

<wsdl:message name="EchoStringSoapIn">
<wsdl:part name="parameters" element="tns:EchoString" />
</wsdl:message>
<wsdl:message name="EchoStringSoapOut">
<wsdl:part name="parameters" element="tns:EchoStringResponse" />
</wsdl:message>
<wsdl:portType name="DocSamplePortType">
<wsdl:operation name="EchoString">
<wsdl:input message="tns:EchoStringSoapIn" />
<wsdl:output message="tns:EchoStringSoapOut" />
</wsdl:operation>
</wsdl:portType>

<!--The code below defines how the types map to SOAP. -->

<wsdl:binding name="DocSampleBinding" type="tns:DocSamplePortType">
<wsdl:operation name="EchoString">
<soap:operation soapAction="urn:dotnet.callouttest.soap.sforce.com/EchoString"
style="document" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>

<!-- Finally, the code below defines the endpoint, which maps to the endpoint in the class -->

<wsdl:service name="DocSample">
<wsdl:port name="DocSamplePort" binding="tns:DocSampleBinding">
<soap:address location="http://YourServer/YourService" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>

在此 WSDL 文档中,自动生成以下 Apex 类。类名是您在以下情况下指定的名称 导入 WSDL.docSample

//Generated by wsdl2apex

public class docSample {
    public class EchoStringResponse_element {
        public String EchoStringResult;
        private String[] EchoStringResult_type_info = new String[]{
                            'EchoStringResult',
                            'http://doc.sample.com/docSample',
                             null,'0','1','false'};
        private String[] apex_schema_type_info = new String[]{
                             'http://doc.sample.com/docSample',
                             'true','false'};
        private String[] field_order_type_info = new String[]{
                             'EchoStringResult'};
    }
    public class EchoString_element {
        public String input;
        private String[] input_type_info = new String[]{
                              'input',
                              'http://doc.sample.com/docSample',
                               null,'0','1','false'};
        private String[] apex_schema_type_info = new String[]{
                               'http://doc.sample.com/docSample',
                               'true','false'};
        private String[] field_order_type_info = new String[]{'input'};
    }
    public class DocSamplePort {
        public String endpoint_x = 'http://YourServer/YourService';
        public Map<String,String> inputHttpHeaders_x;
        public Map<String,String> outputHttpHeaders_x;
        public String clientCertName_x;
        public String clientCert_x;
        public String clientCertPasswd_x;
        public Integer timeout_x;
        private String[] ns_map_type_info = new String[]{
                          'http://doc.sample.com/docSample', 'docSample'};
        public String EchoString(String input) {
            docSample.EchoString_element request_x = new 
                                           docSample.EchoString_element();
            request_x.input = input;
            docSample.EchoStringResponse_element response_x;
            Map<String, docSample.EchoStringResponse_element> response_map_x = 
                       new Map<String, docSample.EchoStringResponse_element>();
            response_map_x.put('response_x', response_x);
            WebServiceCallout.invoke(
              this,
              request_x,
              response_map_x,
              new String[]{endpoint_x,
              'urn:dotnet.callouttest.soap.sforce.com/EchoString',
              'http://doc.sample.com/docSample',
              'EchoString',
              'http://doc.sample.com/docSample',
              'EchoStringResponse',
              'docSample.EchoStringResponse_element'}
            );
            response_x = response_map_x.get('response_x');
            return response_x.EchoStringResult;
        }
    }
}

请注意原始 WSDL 文档中的以下映射:

  • WSDL 目标命名空间映射到 Apex 类名。
  • 每个复杂类型都成为一个类。类型中的每个元素都是 类。
  • WSDL 端口名称映射到存根类。
  • WSDL 中的每个操作都映射到一个公共方法。

您可以使用自动生成的类来 调用外部 Web 服务。下面的代码在外部服务器上调用该方法。docSampleechoString

docSample.DocSamplePort stub = new docSample.DocSamplePort();
String input = 'This is the input string';
String output = stub.EchoString(input);

测试 Web 服务标注

生成的代码将另存为 Apex 类,其中包含您可以调用的方法 调用 Web 服务。若要部署或打包此 Apex 类和其他随附代码, 75% 的代码必须具有测试覆盖率,包括生成的类中的方法。由 默认情况下,测试方法不支持 Web 服务标注和执行 Web 的测试 服务标注失败。为了防止测试失败并增加代码覆盖率,Apex 提供内置接口和 方法。在测试方法中使用和接收虚假响应。

WebServiceMockTest.setMockWebServiceMockTest.setMock

指定用于测试 Web 服务标注的模拟响应

从 WSDL 创建 Apex 类时,自动生成的类中的方法 调用 ,其中 执行对外部服务的标注。测试这些方法时,您可以 指示 Apex 运行时在调用时生成虚假响应。为此, 实现接口和 指定要发送的 Apex 运行时的虚假响应。以下是更多步骤 细节。WebServiceCallout.invokeWebServiceCallout.invokeWebServiceMock

首先,实现接口 并在方法中指定 false 响应。WebServiceMockdoInvoke

global class YourWebServiceMockImpl implements WebServiceMock {
   global void doInvoke(
           Object stub,
           Object request,
           Map<String, Object> response,
           String endpoint,
           String soapAction,
           String requestName,
           String responseNS,
           String responseName,
           String responseType) {

        // Create response element from the autogenerated class.
        // Populate response element.
        // Add response element to the response parameter, as follows:
        response.put('response_x', responseElement); 
   }
}

注意

  • 实现接口的类可以是全局类,也可以是公共类。WebServiceMock
  • 您可以使用@isTest对此类进行批注,因为它仅在测试上下文中使用。 这样,您就可以将其从组织的代码大小限制 6 中排除 MB的。

现在,您已经指定了假响应的值,请指示 Apex 运行时通过调用测试方法发送此虚假响应。对于第一个参数,传递 ,对于第二个参数 参数,则传递 的接口实现的新实例,如下所示:Test.setMockWebServiceMock.classWebServiceMock

Test.setMock(WebServiceMock.class, new YourWebServiceMockImpl());

在此之后,如果在测试上下文中调用 Web 服务标注,则标注为 不是制造的。您会收到方法实现中指定的模拟响应。doInvoke

注意

如果执行标注的代码位于 一个托管包,从 测试方法。Test.setMock

此示例演示如何测试 Web 服务标注。列出了接口的实现 第一。此示例实现该方法,该方法返回您指定的响应。在本例中,响应元素 的自动生成的类被创建并分配一个值。接下来,响应映射 参数填充了此虚假响应。此示例基于 WSDL 在生成的 WSDL2Apex 代码中列出。导入此 WSDL 并生成 保存之前调用的类 这个班级。WebServiceMockdoInvokedocSample

@isTest
global class WebServiceMockImpl implements WebServiceMock {
   global void doInvoke(
           Object stub,
           Object request,
           Map<String, Object> response,
           String endpoint,
           String soapAction,
           String requestName,
           String responseNS,
           String responseName,
           String responseType) {
       docSample.EchoStringResponse_element respElement = 
           new docSample.EchoStringResponse_element();
       respElement.EchoStringResult = 'Mock response';
       response.put('response_x', respElement); 
   }
}

此方法生成 Web 服务标注。

public class WebSvcCallout {
    public static String callEchoString(String input) {
        docSample.DocSamplePort sample = new docSample.DocSamplePort();
        sample.endpoint_x = 'https://example.com/example/test';
        
        // This invokes the EchoString method in the generated class
        String echo = sample.EchoString(input);
        
        return echo;
    }   
}

此测试类包含设置模拟标注模式的测试方法。它调用 上一个方法 类,并验证是否收到模拟响应。callEchoString

@isTest
private class WebSvcCalloutTest {
    @isTest static void testEchoString() {              
        // This causes a fake response to be generated
        Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
        
        // Call the method that invokes a callout
        String output = WebSvcCallout.callEchoString('Hello World!');
        
        // Verify that a fake result is returned
        System.assertEquals('Mock response', output); 
    }
}

执行 DML 操作和模拟标注

默认情况下,标注不是 允许在同一事务中执行 DML 操作后,因为 DML 操作 导致待处理的未提交工作,从而阻止标注执行。 有时,您可能希望在测试方法中插入测试数据 在进行标注之前使用 DML。要启用此功能,请将部分括起来 在 AND 语句中执行标注的代码。语句必须出现 在声明之前。 此外,对 DML 操作的调用不得是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTest

DML 操作 在模拟标注被允许且不需要之后发生 测试方法的任何变化。

在模拟标注之前执行 DML

此示例基于前面的示例。该示例显示 如何使用 AND 语句允许 在模拟标注之前在测试方法中执行的 DML 操作。 测试方法()首先插入一个测试帐户,调用,使用设置模拟标注模式,调用一个方法 执行标注,验证模拟响应值,最后 调用。Test.startTestTest.stopTesttestEchoStringTest.startTestTest.setMockTest.stopTest

@isTest
private class WebSvcCalloutTest {
    @isTest static void testEchoString() {              
        // Perform some DML to insert test data
        Account testAcct = new Account('Test Account');
        insert testAcct;

        // Call Test.startTest before performing callout
        // but after setting test data.
        Test.startTest();

        // Set mock callout class 
        Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
        
        // Call the method that invokes a callout
        String output = WebSvcCallout.callEchoString('Hello World!');
        
        // Verify that a fake result is returned
        System.assertEquals('Mock response', output);

        Test.stopTest();
    }
}

异步顶点和模拟标注

与 DML 类似,异步 Apex 操作会导致挂起的未提交工作,从而阻止 稍后在同一事务中执行的标注。示例 异步 Apex 操作是对未来方法、批处理 Apex 或计划的调用 顶点。这些异步调用通常包含在测试方法的 and 语句中,以便 它们在 之后执行。在这个 情况下,可以在异步调用后执行模拟标注,并且不会进行任何更改 必要。但是,如果异步调用没有包含在 and 语句中,你将得到一个 异常,因为未提交的工作处于待处理状态。要防止此异常,请执行以下任一操作 其中:

Test.startTestTest.stopTestTest.stopTestTest.startTestTest.stopTest

  • 将异步调用包含在 and 语句中。Test.startTestTest.stopTestTest.startTest(); MyClass.asyncCall(); Test.stopTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout();
  • 遵循与 DML 调用相同的规则:将代码部分括起来 在 AND 语句中执行标注。语句必须出现在语句之前。此外, 异步调用不能是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTestMyClass.asyncCall(); Test.startTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout(); Test.stopTest();

模拟标注之后发生的异步调用是 允许并且不需要对测试方法进行任何更改。

使用 WSDL 的注意事项

从 WSDL 生成 Apex 类时,请注意以下事项。

映射标头

WSDL 文档中定义的标头将成为生成的类中存根上的公共字段。 这类似于 AJAX 工具包和 .NET 的工作方式。

了解运行时事件

当 Apex 代码对外部服务进行标注时,将执行以下检查。

  • 有关发出 HTTP 请求或 Web 服务调用时的超时限制的信息,请参见标注限制和局限性。
  • 不允许在 Apex 类中使用循环引用。
  • 不允许与 Salesforce 域建立多个环回连接。
  • 若要允许访问终结点,请从安装程序中输入“快速查找”框,然后选择“远程站点设置”来注册该终结点。Remote Site Settings
  • 为了防止数据库连接被阻止,没有事务 可以打开。

了解变量名称中不支持的字符

WSDL 文件可以包含 Apex 变量名称中不允许的元素名称。这 从 WSDL 生成 Apex 变量名称时,以下规则适用 文件:

  • 如果元素名称的第一个字符不是字母顺序,则会在生成的 Apex 前面附加一个字符 变量名称。x
  • 如果 Apex 变量名称中不允许使用元素名称的最后一个字符,则会在 生成的 Apex 变量名称。x
  • 如果元素名称包含 Apex 变量名称中不允许的字符,则 字符替换为下划线 () 字符。_
  • 如果元素名称在一行中包含两个 Apex 变量中不允许的字符 name,第一个字符替换为下划线 () 字符,第二个字符 替换为字符。这样可以避免生成具有两个连续变量名称的变量名称 下划线,这在 Apex 中是不允许的。_x
  • 假设您有一个操作,该操作采用两个参数和 . 生成的 Apex 有两个变量,均命名为 .该类不编译。 手动编辑 Apex 并更改其中一个变量名称。a_a_xa_x

调试从 WSDL 文件生成的类

Salesforce 使用 SOAP API、.NET 和 Axis 测试代码。如果您使用其他工具,您可能会遇到 问题。

您可以使用调试标头在请求和响应 SOAP 消息中返回 XML 以帮助 您诊断问题。有关更多信息,请参阅使用 SOAP API 部署 Apex。

调用 HTTP 标注

Apex 提供了几个内置类来处理 HTTP 服务并创建 HTTP 请求,例如 GET、POST、PUT 和 DELETE。

您可以使用这些 HTTP 类集成到基于 REST 的服务。它们还允许您 集成到基于 SOAP 的 Web 服务中,作为从 WSDL 生成 Apex 代码的替代选项。 通过使用 HTTP 类,而不是从 WSDL 开始,您可以承担更多的责任 处理请求和响应的 SOAP 消息的构造。

  1. HTTP 类
  2. 测试 HTTP 标注
    要部署或打包 Apex,75% 的代码必须具有测试覆盖率。默认情况下,测试方法不支持 HTTP 标注,因此执行标注的测试会失败。通过使用指示 Apex 在测试中生成模拟响应来启用 HTTP 标注测试。Test.setMock

HTTP 类

这些类公开 HTTP 请求和响应功能。

  • Http的 类。使用此类来启动 HTTP 请求和 响应。
  • HttpRequest 类:使用此类可以 以编程方式创建 HTTP 请求,如 GET、POST、PATCH、PUT 和 DELETE。
  • HttpResponse 类:使用此类可以 处理 返回的 HTTP 响应。HTTP

和类支持这些元素。

HttpRequestHttpResponse

  • Http请求
    • HTTP 请求类型,例如 GET、POST、PATCH、PUT、DELETE、TRACE、 连接、头部和选项
    • 请求标头(如果需要)
    • 读取和连接超时
    • 必要时重定向
    • 邮件正文的内容
  • HttpResponse
    • HTTP 状态代码
    • 响应标头(如果需要)
    • 响应正文的内容

此示例向外部服务器发出 HTTP GET 请求,该请求传递给参数中的方法。此示例还访问 返回的响应。getCalloutResponseContentsurl

public class HttpCalloutSample {

  // Pass in the endpoint to be used using the string url
  public String getCalloutResponseContents(String url) {

    // Instantiate a new Http object
    Http h = new Http();

     // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
    HttpRequest req = new HttpRequest();
    req.setEndpoint(url);
    req.setMethod('GET');

    // Send the request, and return a response
    HttpResponse res = h.send(req);
    return res.getBody();
  }
}

前面的示例是同步运行的,这意味着没有进一步的处理 在外部 Web 服务返回响应之前发生。或者,您可以使用 @future注解,使 标注异步运行。

此示例向外部服务器发出 HTTP POST 请求,该请求传递给参数中的方法。替换为 要在标注中发送的 JSON 内容。getPostCalloutResponseContentsurlYour_JSON_Content

public class HttpPostCalloutSample {

  // Pass in the endpoint to be used using the string url
  public String getPostCalloutResponseContents(String url) {

    // Instantiate a new Http object
    Http h = new Http();

    // Instantiate a new HTTP request
    // Specify request properties such as the endpoint, the POST method, etc. 
    HttpRequest req = new HttpRequest();
    req.setEndpoint(url);
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setBody('{Your_JSON_Content}');

    // Send the request, and return a response
    HttpResponse res = h.send(req);
    return res.getBody();
  }
}

若要从终结点或重定向终结点访问外部服务器,请添加远程站点 添加到授权远程站点的列表中。登录到 Salesforce,然后从“设置”的“快速”中 “查找”框,输入 ,然后选择“远程站点设置”。Remote Site Settings

使用 XML 类或 JSON 类来解析 XML 或 JSON 中的内容 由 HttpRequest 创建的请求的正文,或由 HttpResponse 访问的响应。

考虑

  • AJAX 代理处理重定向和身份验证质询 (401/407 responses) 自动。有关 AJAX 代理的详细信息,请参阅 AJAX 工具包 文档。
  • 您可以将终结点设置为命名凭据 URL。命名凭据 URL 包含方案,名称 命名凭据和可选路径。例如:。一个命名的 credential 指定标注端点的 URL 及其所需的 URL 一个定义中的身份验证参数。Salesforce 管理所有 对指定命名凭据作为标注的 Apex 标注进行身份验证 端点,这样您的代码就不必这样做了。您还可以跳过远程站点 设置,否则,向外部站点进行标注时需要这些设置,用于 命名凭据中定义的站点。请参阅作为标注的命名凭据 端点。callout:callout:My_Named_Credential/some_path
  • 在标注中设置请求正文时,请将方法设置为 。如果设置了请求正文和请求 方法为 ,则执行请求。POSTGETPOST
  • 如果您有来自 DML 的待处理未提交交易,则会阻止标注 操作、可排队作业(与 、 或将来的方法一起排队)。System.enqueueJobDatabase.executeBatch

测试 HTTP 标注

要部署或打包 Apex,75% 的代码必须具有测试覆盖率。默认情况下,test 方法不支持 HTTP 标注,因此执行标注的测试会失败。启用 HTTP 通过指示 Apex 在测试中生成模拟响应来进行标注测试,使用 .

Test.setMock通过以下方式之一指定模拟响应。

  • 通过实现 HttpCalloutMock 接口
  • 通过将静态资源与 StaticResourceCalloutMock 或 MultiStaticResourceCalloutMock 一起使用

若要在测试方法中的模拟标注之前启用运行 DML 操作,请参阅执行 DML 操作和模拟标注。

  • 通过实现 HttpCalloutMock 接口测试 HTTP 标注
  • 使用静态资源测试 HTTP 标注
  • 执行 DML 操作和模拟标注

通过实现 HttpCalloutMock 接口测试 HTTP 标注

为接口提供一个实现,以指定 Apex 运行时调用的方法中发送的响应 发送标注的响应。HttpCalloutMockrespond

global class YourHttpCalloutMockImpl implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest req) {
        // Create a fake response.
        // Set response values, and 
        // return response.
    }
}

注意

  • 实现接口的类可以是全局类,也可以是公共类。HttpCalloutMock
  • 您可以使用@isTest注释此类,因为它将仅在测试上下文中使用。 这样,就可以将其从组织的代码大小中排除 限制为 6 MB。

现在,您已经指定了假响应的值,请指示 Apex 运行时通过调用测试方法发送此虚假响应。对于第一个参数,传递 ,对于第二个参数 参数,则传递 的接口实现的新实例,如下所示:Test.setMockHttpCalloutMock.classHttpCalloutMock

Test.setMock(HttpCalloutMock.class, new YourHttpCalloutMockImpl());

在此之后,如果在测试上下文中调用 HTTP 标注,则标注不会 made 时,您会收到在方法实现中指定的模拟响应。respond

注意

如果执行标注的代码位于托管包中,则要模拟标注, 从 测试方法调用 具有相同命名空间的相同包。Test.setMock

这是一个完整的示例,演示如何测试 HTTP 标注。界面 实现 () 列在最前面。它后面是一个包含测试方法的类和另一个 包含测试调用的方法。测试方法通过在调用之前调用来设置模拟标注模式。然后,它会验证 返回的响应是已实现的方法发送的响应。单独保存每个类,并在 中运行测试。MockHttpResponseGeneratortestCalloutTest.setMockgetInfoFromExternalServicerespondCalloutClassTest

@isTest
global class MockHttpResponseGenerator implements HttpCalloutMock {
    // Implement this interface method
    global HTTPResponse respond(HTTPRequest req) {
        // Optionally, only send a mock response for a specific endpoint
        // and method.
        System.assertEquals('https://example.com/example/test', req.getEndpoint());
        System.assertEquals('GET', req.getMethod());
        
        // Create a fake response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"example":"test"}');
        res.setStatusCode(200);
        return res;
    }
}
public class CalloutClass {
    public static HttpResponse getInfoFromExternalService() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://example.com/example/test');
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutClassTest {
     @isTest static void testCallout() {
        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
        
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = CalloutClass.getInfoFromExternalService();
        
        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"example":"test"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());
    }
}

使用静态资源测试 HTTP 标注

您可以通过指定 您希望在静态资源中接收的响应,并使用 两个内置类之一 – StaticResourceCalloutMock 或 MultiStaticResourceCalloutMock

使用 StaticResourceCalloutMock 测试 HTTP 标注

Apex 提供了内置类,您可以通过在静态中指定响应正文来测试标注 资源。使用此类时,您不必提供自己的 接口的实现。相反,只需创建一个实例并设置静态资源以用于 响应正文以及其他响应属性,如状态代码和 内容类型。StaticResourceCalloutMockHttpCalloutMockStaticResourceCalloutMock首先,必须从要包含的文本文件创建静态资源 响应正文:

  1. 创建一个包含要返回的响应正文的文本文件。 响应正文可以是任意字符串,但它必须与 内容类型(如果指定)。例如,如果您的回复没有内容 指定类型,则该文件可以包含任意字符串。如果指定 application/json 的内容类型 对于响应,文件内容应为 JSON 字符串,例如 {“呵呵”:“骗了你”}。abc
  2. 为文本文件创建静态资源:
    1. 在“设置”中,输入“快速查找”框,然后选择“静态资源”。Static Resources
    2. 单击“新建”。
    3. 为静态资源命名。
    4. 选择要上传的文件。
    5. 点击保存

要了解有关静态资源的更多信息,请参阅 Salesforce 联机帮助。

接下来,创建一个实例并设置静态资源,然后 任何其他属性。StaticResourceCalloutMock

StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('myStaticResourceName');
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json');

在测试方法中,调用以设置模拟标注模式并将其作为第一个传递 参数,以及您为其创建的第二个变量名称 论点。Test.setMockHttpCalloutMock.classStaticResourceCalloutMock

Test.setMock(HttpCalloutMock.class, mock);

在此之后,如果您的测试方法执行标注,则不会进行标注,并且 Apex 运行时发送您在 实例中指定的模拟响应。StaticResourceCalloutMock

注意

如果执行标注的代码位于 一个托管包,从 测试方法。Test.setMock

这是一个完整的示例,包含测试方法 () 和 它所测试的方法 () 执行标注。在运行此示例之前,请创建 名为 based 的静态资源 在包含以下内容的文本文件上。单独保存每个类,并在 中运行测试。testCalloutWithStaticResourcesgetInfoFromExternalServicemockResponse{"hah":"fooled you"}CalloutStaticClassTest

public class CalloutStaticClass {
    public static HttpResponse getInfoFromExternalService(String endpoint) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutStaticClassTest {
    @isTest static void testCalloutWithStaticResources() {
        // Use StaticResourceCalloutMock built-in class to
        // specify fake response and include response body 
        // in a static resource.
        StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
        mock.setStaticResource('mockResponse');
        mock.setStatusCode(200);
        mock.setHeader('Content-Type', 'application/json');
        
        // Set the mock callout mode
        Test.setMock(HttpCalloutMock.class, mock);
        
        // Call the method that performs the callout
        HTTPResponse res = CalloutStaticClass.getInfoFromExternalService(
            'https://example.com/example/test');
        
        // Verify response received contains values returned by
        // the mock response.
        // This is the content of the static resource.
        System.assertEquals('{"hah":"fooled you"}', res.getBody());
        System.assertEquals(200,res.getStatusCode());
        System.assertEquals('application/json', res.getHeader('Content-Type'));   
    }
}

使用 MultiStaticResourceCalloutMock 测试 HTTP 标注

Apex 提供了内置类,您可以通过在静态中指定响应正文来测试标注 资源。此类类似于 除了它允许您指定 多个响应机构。使用此类时,您不必提供 您自己的接口实现。相反,只需创建一个实例并将 每个终结点要使用的静态资源。您还可以设置其他响应属性,例如 状态代码和内容类型。MultiStaticResourceCalloutMockStaticResourceCalloutMockHttpCalloutMockMultiStaticResourceCalloutMock

首先,必须从文本文件创建静态资源以包含响应正文。请参阅 使用 StaticResourceCalloutMock 测试 HTTP 标注中概述的过程。

接下来,创建 和 的实例 设置静态资源和任何其他属性。MultiStaticResourceCalloutMock

MultiStaticResourceCalloutMock multimock = new MultiStaticResourceCalloutMock();
multimock.setStaticResource('https://example.com/example/test', 'mockResponse');
multimock.setStaticResource('https://example.com/example/sfdc', 'mockResponse2');
multimock.setStatusCode(200);
multimock.setHeader('Content-Type', 'application/json');

在测试方法中,调用以设置模拟标注模式并将其作为第一个传递 参数,以及您为 AS 创建的变量名称 第二个参数。Test.setMockHttpCalloutMock.classMultiStaticResourceCalloutMock

Test.setMock(HttpCalloutMock.class, multimock);

在此之后,如果测试方法对某个终结点执行 HTTP 标注 https://example.com/example/test 或 https://example.com/example/sfdc,则不会进行标注 并且 Apex 运行时会发送您在 的实例。MultiStaticResourceCalloutMock

这是一个完整的示例,其中包含测试方法 () 和它正在测试的执行标注的方法 ()。在运行此示例之前,请创建 名为 based 的静态资源 在一个文本文件上,其中包含内容,另一个基于 包含以下内容的文本文件。单独保存每个类,并在 中运行测试。testCalloutWithMultipleStaticResourcesgetInfoFromExternalServicemockResponse{"hah":"fooled you"}mockResponse2{"hah":"fooled you twice"}CalloutMultiStaticClassTest

public class CalloutMultiStaticClass {
    public static HttpResponse getInfoFromExternalService(String endpoint) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutMultiStaticClassTest {
    @isTest static void testCalloutWithMultipleStaticResources() {
        // Use MultiStaticResourceCalloutMock to
        // specify fake response for a certain endpoint and 
        // include response body in a static resource.    
        MultiStaticResourceCalloutMock multimock = new MultiStaticResourceCalloutMock();
        multimock.setStaticResource(
            'https://example.com/example/test', 'mockResponse');
        multimock.setStaticResource(
            'https://example.com/example/sfdc', 'mockResponse2');
        multimock.setStatusCode(200);
        multimock.setHeader('Content-Type', 'application/json');
        
        // Set the mock callout mode
        Test.setMock(HttpCalloutMock.class, multimock);
        
        // Call the method for the first endpoint
        HTTPResponse res = CalloutMultiStaticClass.getInfoFromExternalService(
            'https://example.com/example/test');
        // Verify response received 
        System.assertEquals('{"hah":"fooled you"}', res.getBody());
        
        // Call the method for the second endpoint
        HTTPResponse res2 = CalloutMultiStaticClass.getInfoFromExternalService(
            'https://example.com/example/sfdc');
        // Verify response received 
        System.assertEquals('{"hah":"fooled you twice"}', res2.getBody());
    }
}

执行 DML 操作和模拟标注

默认情况下,标注不是 允许在同一事务中执行 DML 操作后,因为 DML 操作 导致待处理的未提交工作,从而阻止标注执行。 有时,您可能希望在测试方法中插入测试数据 在进行标注之前使用 DML。要启用此功能,请将部分括起来 在 AND 语句中执行标注的代码。语句必须出现 在声明之前。 此外,对 DML 操作的调用不得是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTest

DML 操作 在模拟标注被允许且不需要之后发生 测试方法的任何变化。

DML 操作支持适用于所有实现 使用接口和静态资源(或 )。以下示例使用已实现的接口,但您 在使用静态资源时可以应用相同的技术。HttpCalloutMockStaticResourceCalloutMockMultiStaticResourceCalloutMockHttpCalloutMock

在模拟标注之前执行 DML

此示例基于前面提供的 HttpCalloutMock 示例。该示例演示如何使用 和 语句来允许 在模拟标注之前在测试方法中执行的 DML 操作。 测试方法()首先插入一个测试帐户,调用,使用设置模拟标注模式,调用一个方法 执行标注,验证模拟响应值,最后 调用。Test.startTestTest.stopTesttestCalloutTest.startTestTest.setMockTest.stopTest

@isTest
private class CalloutClassTest {
     @isTest static void testCallout() {
        // Perform some DML to insert test data
        Account testAcct = new Account('Test Account');
        insert testAcct;

        // Call Test.startTest before performing callout
        // but after setting test data.
        Test.startTest();

        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
        
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = CalloutClass.getInfoFromExternalService();
        
        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"example":"test"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());

        Test.stopTest();
    }
}

异步顶点和模拟标注

与 DML 类似,异步 Apex 操作会导致挂起的未提交工作,从而阻止 稍后在同一事务中执行的标注。示例 异步 Apex 操作是对未来方法、批处理 Apex 或计划的调用 顶点。这些异步调用通常包含在测试方法的 and 语句中,以便 它们在 之后执行。在这个 情况下,可以在异步调用后执行模拟标注,并且不会进行任何更改 必要。但是,如果异步调用没有包含在 and 语句中,你将得到一个 异常,因为未提交的工作处于待处理状态。要防止此异常,请执行以下任一操作 其中:

Test.startTestTest.stopTestTest.stopTestTest.startTestTest.stopTest

  • 将异步调用包含在 and 语句中。Test.startTestTest.stopTestTest.startTest(); MyClass.asyncCall(); Test.stopTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout();
  • 遵循与 DML 调用相同的规则:将代码部分括起来 在 AND 语句中执行标注。语句必须出现在语句之前。此外, 异步调用不能是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTestMyClass.asyncCall(); Test.startTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout(); Test.stopTest();

模拟标注之后发生的异步调用是 允许并且不需要对测试方法进行任何更改。

使用证书

要使用双向 SSL 身份验证,请发送一个证书,其中包含您的标注,该证书是 在 Salesforce 中生成或由证书颁发机构 (CA) 签名。发送证书 增强了安全性,因为标注的目标接收证书并可以使用 它根据其密钥库对请求进行身份验证。

要为标注启用双向 SSL 身份验证,请执行以下操作:

  1. 生成一个 证书。
  2. 将证书与代码集成。请参阅对 SOAP 服务使用证书和对 HTTP 请求使用证书。
  3. 如果要连接到第三方并使用自签名证书,请共享 Salesforce 证书,以便他们可以将证书添加到他们的 密钥库。如果要连接到组织内的其他应用程序, 配置 Web 服务器或应用程序服务器以请求客户端证书。这 进程取决于您使用的 Web 或应用程序服务器的类型。
  4. 配置远程站点设置 标注。在任何 Apex 标注可以调用外部站点之前,该站点必须 在“远程站点设置”页中注册,否则标注失败。如果标注 指定命名凭据作为终结点,无需配置远程 网站设置。若要设置命名凭据,请参阅《定义命名凭据》中的“定义命名凭据” Salesforce 帮助。
  1. 生成证书
  2. 将证书用于 SOAP 服务
    要支持对 SOAP Web 服务的标注进行双向身份验证,请在 Salesforce 中生成证书或将密钥对从密钥库导入 Salesforce。然后将证书与您的 Apex 集成。
  3. 对 HTTP 请求使用证书

生成证书

您可以使用在 Salesforce 中生成的自签名证书或由 证书颁发机构 (CA)。要为标注生成证书,请参阅生成证书。

成功保存 Salesforce 证书后,证书和相应的 密钥是自动生成的。

创建 CA 签名证书后,必须先上传签名证书 你可以使用它。请参阅“生成由证书签名的证书 权限“在 Salesforce 在线帮助中。

将证书用于 SOAP 服务

要支持 SOAP Web Service 标注的双向身份验证,请生成 证书或将密钥对从密钥库导入 Salesforce。然后 将证书与您的 Apex 集成。

重要

我们建议存储外部的相互身份验证证书 Java 密钥库中的 Web 服务。有关详细信息,请参阅证书和密钥。

要将证书与您的 Apex 集成,请执行以下操作:

  1. 从第三方接收 Web Service 的 WSDL,或从 要连接到的应用程序。
  2. 从 Web 服务的 WSDL 生成 Apex 类。请参阅 SOAP 服务:从 WSDL 文档定义类。
  3. 生成的 Apex 类包括用于调用第三方 Web 服务的存根 由 WSDL 文档表示。编辑 Apex 类,并为实例上的变量赋值 存根类。该值必须与 在“证书和密钥管理”页面上生成的证书。clientCertName_x

此示例演示如何编辑 Apex 类,并使用生成的 WSDL2Apex 代码中的示例 WSDL 文件。示例 假定您生成的证书的唯一名称为 。DocSampleCert

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.clientCertName_x = 'DocSampleCert';
String input = 'This is the input string';
String output = stub.EchoString(input);

对 HTTP 请求使用证书

在 Salesforce 中生成证书后,您可以使用它来支持双向 对 HTTP 请求的标注进行身份验证。

要将证书与您的 Apex 集成,请执行以下操作:

  1. 生成证书。记下证书的唯一名称。
  2. 在 Apex 中,使用 类。用于参数的值 对于此方法,必须与证书的唯一名称匹配 在上一步中生成。setClientCertificateNameHttpRequest

以下示例演示了上一步的最后一步 程序。此示例假定您之前生成了一个证书 唯一名称为 。DocSampleCert

HttpRequest req = new HttpRequest();
req.setClientCertificateName('DocSampleCert');

标注限制和局限性

当 Apex 代码对 HTTP 请求进行标注时,存在以下限制和限制 或 Web 服务调用。Web Service 调用可以是 SOAP API 调用,也可以是任何外部 Web 服务调用。

  • 单个 Apex 事务最多可以对 HTTP 请求或 API 发出 100 个标注 叫。
  • 在 Developer Edition 组织,您最多只能对外部的端点进行 20 个并发标注 您的 Salesforce 组织的域。此限制不适用于非开发人员版 组织。
  • 默认超时为 10 秒。可以为每个标注定义自定义超时。这 最小值为 1 毫秒,最大值为 120,000 毫秒。请参阅 下一节了解如何为 Web 服务或 HTTP 标注设置自定义超时。
  • 单个 Apex 事务的标注的最大累积超时为 120 秒。 此时间是 Apex 事务调用的所有标注的累加时间。
  • 每个组织对运行时间超过 5 秒的长时间运行请求都有限制(总计 执行时间)。计算此限制时,不包括 HTTP 标注处理时间。 我们暂停标注的计时器,并在标注完成后恢复。请参阅 Lightning Platform Apex 的执行调控器和限制 限制。
  • 当同一事务中有待处理的操作时,无法进行标注。 导致挂起操作的内容是 DML 语句、异步 Apex(例如 未来方法和批处理 Apex 作业)、计划的 Apex 或发送电子邮件。您可以进行标注 在执行这些类型的操作之前。
  • 挂起的操作可能在同一事务中的模拟标注之前发生。请参阅对基于 WSDL 的标注执行 DML 操作和模拟标注或对 HTTP 标注执行 DML 操作和模拟标注。
  • 当标头添加到 标注请求和响应 未由外部服务器返回,则会发生超时。Expect: 100-ContinueHTTP/1.1 100 Continue

只读模式下的 Apex 标注

在只读模式下,外部服务的 Apex 标注会执行,并且不会被 系统。通常,您会在以下情况下在同一事务中执行一些后续操作 接收来自标注的响应。例如,您可以进行 DML 调用以更新 Salesforce 记录。但 Salesforce 中的写入操作(如记录更新)被阻止 在只读模式下。只读模式下的这种行为不一致可能会破坏 程序流程和原因问题。为避免不正确的程序行为,我们建议您 防止在只读模式下进行标注。要检查组织是否处于只读模式, 叫。System.getApplicationReadWriteMode()

下面的示例检查 的返回值。如果返回值等于枚举值,则组织 在只读模式下,将跳过标注。否则 ( value),则执行标注。System.getApplicationReadWriteMode()ApplicationReadWriteMode.READ_ONLYApplicationReadWriteMode.DEFAULT

注意

此类使用 Apex HTTP 类作为示例进行标注。您还可以制作一个 通过 WSDL2Apex 使用导入的 WSDL 进行标注。检查只读的过程 无论哪种情况,模式都是相同的。

public class HttpCalloutSampleReadOnly {
    public class MyReadOnlyException extends Exception {}

    // Pass in the endpoint to be used using the string url
    public String getCalloutResponseContents(String url) {
        
        // Get Read-only mode status
        ApplicationReadWriteMode mode = System.getApplicationReadWriteMode();
        String returnValue = '';
        
        if (mode == ApplicationReadWriteMode.READ_ONLY) {
            // Prevent the callout
            throw new MyReadOnlyException('Read-only mode. Skipping callouts!');
        } else if (mode == ApplicationReadWriteMode.DEFAULT) {
            // Instantiate a new http object
            Http h = new Http();
            
            // Instantiate a new HTTP request, specify the method (GET) 
            // as well as the endpoint.
            HttpRequest req = new HttpRequest();
            req.setEndpoint(url);
            req.setMethod('GET');
            
            // Send the request, and return a response
            HttpResponse res = h.send(req);
            returnValue = res.getBody();                        
        }
        return returnValue;
    }
}

在某些 Salesforce 维护活动期间,您的 Salesforce 组织处于只读模式。 例如计划的站点切换和实例刷新。作为连续站点切换的一部分, 您的 Salesforce 组织大约每六个月切换到一次其就绪站点。为 有关站点切换的详细信息,请参阅连续站点切换。

要在沙盒中测试只读模式,请联系 Salesforce 以启用只读模式测试 选择。启用测试选项后,您可以打开只读模式并验证您的 应用程序。

设置注解超时

下面的示例为 Web 服务标注设置自定义超时。该示例有效 替换为示例 WSDL 文件和生成的类,如生成的 WSDL2Apex 代码中所述。设置超时值(以毫秒为单位) 为特殊变量赋值 在 存根。

DocSamplePorttimeout_x

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.timeout_x = 2000; // timeout in milliseconds

以下是为 HTTP 标注设置自定义超时的示例:

HttpRequest req = new HttpRequest();
req.setTimeout(2000); // timeout in milliseconds

使用延续进行长时间的标注

使用异步标注从 Visualforce 页面或 Lightning 组件到外部 Web 服务,并在 回调方法。

异步标注是从 Visualforce 页面或 Lightning 制作的标注 通过回调方法返回响应的组件。异步 标注也称为延续

Visualforce 示例

下图显示了异步标注的执行路径。 从 Visualforce 页面开始。用户在 Visualforce 页面上调用一个操作,该操作 从 Web 服务请求信息(步骤 1)。应用服务器发出标注请求 复制到 Continuation 服务器,然后返回到 Visualforce 页面(步骤 2-3)。这 Continuation 服务器将请求发送到 Web 服务并接收响应(步骤 4–7),然后将响应交还给应用服务器(步骤 8)。最后,回应 返回到 Visualforce 页面(步骤 9)。

异步标注的执行流程

延续的执行流程图示

受益于异步标注的典型 Salesforce 应用程序 包含一个带有按钮的 Visualforce 页面。用户单击该按钮可从 外部 Web 服务。例如,获取 Web 服务中的某些产品。组织中数以千计的代理可以使用此功能 页。因此,一百个代理商可以点击同一个按钮来处理保修 同时提供产品信息。这一百个同时动作超过了 并发长时间运行请求数限制为 10。但是通过使用异步标注, 请求不受此限制的约束,可以执行。

在以下示例应用程序中,按钮操作是在 Apex 中实现的 controller 方法。action 方法创建一个并返回它。将请求发送到服务后, Visualforce 请求已暂停。用户必须等待返回响应,然后才能返回 继续使用页面并调用新操作。当外部服务返回 响应时,Visualforce 请求将恢复,页面将收到此响应。Continuation

这是我们示例应用程序的 Visualforce 页面。此页面包含一个按钮,用于 调用控制器的方法 与此页面相关联。返回延续结果并回调后 方法,按钮将再次呈现组件以显示响应的主体。startRequestoutputText

<apex:page controller="ContinuationController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequest}" 
              value="Start Request" reRender="result"/> 
   </apex:form>

   <!-- This output text component displays the callout response body. -->
   <apex:outputText id="result" value="{!result}" />
</apex:page>

以下是与 Visualforce 页面关联的 Apex 控制器。这 controller 包含 action 和 callback 方法。

注意

在调用外部服务之前,必须将远程站点添加到 Salesforce 用户界面中授权的远程站点。在“设置”中,在“快速查找”框中输入, ,然后选择“远程站点设置”,然后单击“新建” 远程站点Remote Site Settings

如果标注将命名凭据指定为终结点, 无需配置远程站点设置。命名凭据指定 URL 一个定义中的标注端点及其所需的身份验证参数。设置 命名凭据,请参阅 Salesforce 帮助中的“定义命名凭据”。在代码中, 指定命名凭据 URL,而不是长时间运行的服务 URL。一个命名的 credential URL 包含方案、名称 的命名凭据和可选路径。例如:。callout:callout:My_Named_Credential/some_path

public with sharing class ContinuationController {
    // Unique label corresponding to the continuation
    public String requestLabel;
    // Result of callout
    public String result {get;set;}
    // Callout endpoint as a named credential URL 
    // or, as shown here, as the long-running service URL
    private static final String LONG_RUNNING_SERVICE_URL = 
        '<Insert your service URL>';
   
   // Action method
    public Object startRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(40);
      // Set callback method
      con.continuationMethod='processResponse';
      
      // Create callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL);
      
      // Add callout request to continuation
      this.requestLabel = con.addHttpRequest(req);
      
      // Return the continuation
      return con;  
    }
    
    // Callback method 
    public Object processResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel);
      // Set the result variable that is displayed on the Visualforce page
      this.result = response.getBody();
      
      // Return null to re-render the original Visualforce page
      return null;
    }
}

注意

  • 在单个延续中最多可以创建三个异步标注。添加这些 使用类的方法对同一延续的标注请求。标注并行运行,用于此延续和 暂停 Visualforce 请求。只有在外部服务返回所有标注后, Visualforce 进程将恢复。addHttpRequestContinuation
  • 异步标注仅通过 Visualforce 页面受支持。制作一个 通过在 Visualforce 页面外部调用操作方法进行异步标注,例如 在开发者控制台中,不受支持。
  • 异步标注可用于保存的 Apex 控制器和 Visualforce 页面 在版本 30.0 及更高版本中。如果使用 JavaScript 远程处理,则版本 31.0 或更高版本是 必填。
  • 专用连接不支持异步标注。
  • 使用异步标注的过程
    若要使用异步标注,请在控制器的操作方法中创建一个对象,并实现回调方法。Continuation
  • 测试异步标注
    编写测试以测试控制器并满足部署或打包 Apex 的代码覆盖率要求。由于 Apex 测试不支持进行标注,因此您可以模拟标注请求和响应。模拟标注时,请求不会发送到外部服务,而是使用模拟响应。
  • 异步标注限制
    执行延续时,将应用特定于延续的限制。当延续返回并且请求恢复时,将启动新的 Apex 事务。所有 Apex 和 Visualforce 限制均适用,并在新事务中重置,包括 Apex 标注限制。
  • 创建多个异步标注
    要从 Visualforce 页面同时对长时间运行的服务进行多个标注,您最多可以向 Continuation 实例添加三个请求。例如,当您向服务发出独立请求时,例如获取两种产品的库存统计信息,何时同时进行宣传。
  • 链接异步标注
    如果标注的顺序很重要,或者当一个标注以另一个标注的响应为条件时,则可以链接标注请求。链接标注意味着只有在前一个标注的响应返回后才会进行下一个标注。例如,您可能需要在保修服务响应指示保修过期后链接标注以获取保修延期信息。您最多可以链接三个标注。
  • 从导入的 WSDL
    创建异步标注 除了基于 的标注之外,从 WSDL 生成的类进行的 Web 服务调用中还支持异步标注。从 WSDL 生成的类创建异步标注的过程与使用该类的过程类似。HttpRequestHttpRequest

使用异步标注的过程

若要使用异步标注,请在控制器的操作方法中创建一个对象,并实现回调 方法。

Continuation

在操作方法中调用异步标注

若要调用异步标注,请使用 Visualforce 操作方法中的实例调用外部服务。 创建延续时,可以指定超时值和回调的名称 方法。例如,以下代码将创建一个具有 60 秒超时和 processResponse 的回调方法名称。Continuation

Continuation cont = new Continuation(60);
cont.continuationMethod = 'processResponse';

接下来,将对象关联到 外部标注。为此,请创建 HTTP 请求,然后将此请求添加到 延续如下:Continuation

String requestLabel = cont.addHttpRequest(request);

注意

此过程基于使用 HttpRequest 类进行标注。举个例子 使用基于 WSDL 的类,请参阅从导入的 WSDL 创建异步标注。

调用标注的方法(操作方法)必须返回对象,以指示 Visualforce 挂起 系统发送标注并等待标注响应后的当前请求。该对象包含 要执行的标注。ContinuationContinuation

这是调用标注的方法的签名。Object 返回类型 表示 .Continuation

public Object calloutActionMethodName()

定义回调方法

在外部服务完成标注处理后,将返回响应。你 可以指定 callout 返回后异步执行的回调方法。这 回调方法必须在 Controller 类中定义,其中 callout 调用方法 是定义的。您可以定义一个回调方法来处理返回的响应,例如 检索响应以显示在 Visualforce 页面上。

回调方法不接受任何参数,并具有此签名。

public Object callbackMethodName()

Object 返回类型表示 、 或 。渲染原始 Visualforce 页面并完成 Visualforce request,在回调方法中返回。ContinuationPageReferencenullnull

如果操作方法使用 JavaScript 远程处理(带有 注释),则回调方法必须是静态的,并且具有 支持以下签名。@RemoteAction

public static Object callbackMethodName(List< String> labels, Object state)

艺术

public static Object callbackMethodName(Object state)

该参数由系统在调用 callback 方法,并保存与发出的标注请求关联的标签。该参数是通过在控制器中设置 Continuation.state 属性来提供的。labelsstate

下表列出了回调方法的返回值。每个返回值对应 到不同的行为。

Callback 方法返回值请求生命周期和结果
null系统完成 Visualforce 页面请求并呈现原始 Visualforce 页面(或其中的一部分)。
PageReference系统完成 Visualforce 页面请求并重定向到新的 Visualforce 页面。(使用 中的查询参数将 的结果传递到新页面。PageReferenceContinuation
Continuation系统再次暂停 Visualforce 请求,并等待 新的标注。返回新的 用于链接异步标注的回调方法。Continuation

注意

如果未为延续设置属性,则使用相同的操作方法 这使得标注在标注响应返回时再次调用。continuationMethod

测试异步标注

编写测试以测试控制器并满足部署的代码覆盖率要求 或包装 Apex。由于 Apex 测试不支持进行标注,因此您可以模拟 标注请求和响应。模拟标注时,请求不会得到 发送到外部服务,并使用模拟响应。

下面的示例演示如何在 Web 测试中调用模拟异步标注 使用 的服务调用。模拟 标注,调用类的以下方法:Test.setContinuationResponse() 和 Test.invokeContinuationMethod()。HTTPRequestTest

首先列出要测试的控制器类,然后是测试类。控制器 此处重用了 Make Long-Running Callouts with Continuations 中的类。

public with sharing class ContinuationController {
    // Unique label corresponding to the continuation request
    public String requestLabel;
    // Result of callout
    public String result {get;set;}
    // Endpoint of long-running service
    private static final String LONG_RUNNING_SERVICE_URL = 
        '<Insert your service URL>';
   
   // Action method
    public Object startRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(40);
      // Set callback method
      con.continuationMethod='processResponse';
      
      // Create callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL);
      
      // Add callout request to continuation
      this.requestLabel = con.addHttpRequest(req);
      
      // Return the continuation
      return con;  
    }
    
    // Callback method 
    public Object processResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel);
      // Set the result variable that is displayed on the Visualforce page
      this.result = response.getBody();
      
      // Return null to re-render the original Visualforce page
      return null;
    }
}

此示例显示与控制器对应的测试类。此测试类 包含用于测试异步标注的测试方法。在测试方法中,设置一个模拟响应, 并导致 callback 方法。该测试确保回调 方法通过验证控制器的结果变量 设置为预期的响应。Test.setContinuationResponseTest.invokeContinuationMethod

@isTest
public class ContinuationTestingForHttpRequest {
    public static testmethod void testWebService() {
        ContinuationController controller = new ContinuationController();
        // Invoke the continuation by calling the action method
        Continuation conti = (Continuation)controller.startRequest();
        
        // Verify that the continuation has the proper requests
        Map<String, HttpRequest> requests = conti.getRequests();
        system.assert(requests.size() == 1);
        system.assert(requests.get(controller.requestLabel) != null);
        
        // Perform mock callout 
        // (i.e. skip the callout and call the callback method)
        HttpResponse response = new HttpResponse();
        response.setBody('Mock response body');   
        // Set the fake response for the continuation     
        Test.setContinuationResponse(controller.requestLabel, response);
        // Invoke callback method
        Object result = Test.invokeContinuationMethod(controller, conti);
        // result is the return value of the callback
        System.assertEquals(null, result);
        // Verify that the controller's result variable
        //   is set to the mock response.
        System.assertEquals('Mock response body', controller.result);
    }
}

异步标注限制

执行延续时,将适用特定于延续的限制。当 延续返回,请求恢复,新的 Apex 事务开始。所有 Apex 和 Visualforce 限制适用,并在新事务中重置,包括 Apex 标注 限制。

延续特定限制

以下是特定于延续的 Apex 和 Visualforce 限制。

描述限制
单个延续中并行 Apex 标注的最大数量3
链式 Apex 标注的最大数量3
单个延续的最大超时1120 秒
最大 Visualforce 控制器状态大小280 KB
最大 HTTP 响应大小1 兆字节
最大 HTTP POST 表单大小 – 所有键和值的大小 形式31 兆字节
HTTP POST 表单中的最大密钥数3500

1在自动生成的 Web 服务存根和 HttpRequest 对象将被忽略。对于延续,仅强制执行此超时限制。

2执行延续时,将对 Visualforce 控制器进行序列化。 延续完成后,控制器被反序列化,回调 调用。使用 Apex 修饰符可以 指定不序列化的变量。该框架仅使用序列化 成员,当它恢复时。控制器状态大小限制与视图状态是分开的 限制。请参见延续控制器状态和 Visualforce 视图状态。transient

3此限制适用于具有以下内容类型标头的HTTP POST表单:content-type=’application/x-www-form-urlencoded’和content-type=’multipart/form-data’

延续控制器状态和 Visualforce 视图状态

控制器状态和视图状态是不同的。延续的控制器状态包括 请求中涉及的所有控制器的序列化,而不仅仅是 调用延续的控制器。序列化控制器包括控制器 扩展,以及自定义和内部组件控制器。控制器状态大小为 作为事件记录在调试日志中。USER_DEBUG

视图状态比控制器状态保存的数据更多,并且具有更高的最大大小 (170KB)。 视图状态包含状态和组件结构。状态是所有 控制器和页面上每个组件的所有属性,包括子页面和 子组件。组件结构是组件的父子关系 在页面中。您可以在开发者控制台或页脚中监控视图状态大小 启用开发模式时的 Visualforce 页面。有关详细信息,请参阅“查看 状态选项卡“,或参考 Visualforce 开发人员的 指南。

进行多个异步标注

从 Visualforce 同时对长时间运行的服务进行多个标注 页面上,您最多可以向 Continuation 实例添加三个请求。何时 “同时标注”是指向服务发出独立请求,例如 获取两种产品的库存统计信息。

当您在同一延续中制作多个标注时,标注请求会运行 并行并挂起 Visualforce 请求。仅在返回所有标注响应后 Visualforce 进程是否恢复。

以下 Visualforce 和 Apex 示例显示了如何进行两个异步标注 同时使用单个延续。首先显示 Visualforce 页面。这 Visualforce 页面包含一个按钮,用于调用控制器中的操作方法。当 Visualforce 进程恢复,outputPanel 组件被渲染 再。此面板显示两个异步标注的响应。startRequestsInParallel

<apex:page controller="MultipleCalloutController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequestsInParallel}" value="Start Request" reRender="panel"/>  
   </apex:form>

   <apex:outputPanel id="panel">
       <!-- Displays the response body of the initial callout. -->   
       <apex:outputText value="{!result1}" />
       
       <br/>
       <!-- Displays the response body of the chained callout. -->
       <apex:outputText value="{!result2}" />
   </apex:outputPanel> 
   
</apex:page>

此示例显示 Visualforce 页面的控制器类。该方法将两个请求添加到 延续。返回所有标注响应后,将调用回调方法 () 并处理 反应。startRequestsInParallelprocessAllResponses

public with sharing class MultipleCalloutController {

    // Unique label for the first request
    public String requestLabel1;
    // Unique label for the second request
    public String requestLabel2;
    // Result of first callout
    public String result1 {get;set;}
   // Result of second callout
    public String result2 {get;set;}
    // Endpoints of long-running service
    private static final String LONG_RUNNING_SERVICE_URL1 = 
        '<Insert your first service URL>';
    private static final String LONG_RUNNING_SERVICE_URL2 = 
        '<Insert your second service URL>';
           
    // Action method
    public Object startRequestsInParallel() {
      // Create continuation with a timeout
      Continuation con = new Continuation(60);
      // Set callback method
      con.continuationMethod='processAllResponses';
      
      // Create first callout request
      HttpRequest req1 = new HttpRequest();
      req1.setMethod('GET');
      req1.setEndpoint(LONG_RUNNING_SERVICE_URL1);
      
      // Add first callout request to continuation
      this.requestLabel1 = con.addHttpRequest(req1);     
      
      // Create second callout request
      HttpRequest req2 = new HttpRequest();
      req2.setMethod('GET');
      req2.setEndpoint(LONG_RUNNING_SERVICE_URL2);
      
      // Add second callout request to continuation
      this.requestLabel2 = con.addHttpRequest(req2);     
      
      // Return the continuation
      return con;  
    }
    
    // Callback method.
    // Invoked only when responses of all callouts are returned.
    public Object processAllResponses() {   
      // Get the response of the first request
      HttpResponse response1 = Continuation.getResponse(this.requestLabel1);
      this.result1 = response1.getBody();

      // Get the response of the second request
      HttpResponse response2 = Continuation.getResponse(this.requestLabel2);
      this.result2 = response2.getBody();
                 
      // Return null to re-render the original Visualforce page
      return null;
    }
}

链接异步标注

如果标注的顺序很重要,或者标注以响应为条件 在另一个标注中,您可以链接标注请求。链接标注意味着下一个 只有在前一个标注的响应返回后,才会进行标注。例如,您 可能需要在保修服务后链接标注以获取保修延期信息 响应表示保修已过期。您最多可以链接三个标注。

以下 Visualforce 和 Apex 示例演示如何将一个标注链接到另一个标注。 首先显示 Visualforce 页面。Visualforce 页面包含一个按钮,用于调用 操作方法 控制器。每次返回延续时,Visualforce 进程都会暂停。 Visualforce 进程在返回每个响应后恢复,并呈现每个响应 outputPanel 组件中的响应。invokeInitialRequest

<apex:page controller="ChainedContinuationController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!invokeInitialRequest}" value="Start Request" reRender="panel"/>  
   </apex:form>

   <apex:outputPanel id="panel">
       <!-- Displays the response body of the initial callout. -->   
       <apex:outputText value="{!result1}" />
       
       <br/>
       <!-- Displays the response body of the chained callout. -->
       <apex:outputText value="{!result2}" />
   </apex:outputPanel> 
   
</apex:page>

此示例显示 Visualforce 页面的控制器类。该方法创建第一个 延续。回调方法 () 处理第一个标注的响应。如果 此响应满足特定条件,该方法通过返回来链接另一个标注 第二个延续。返回链式延续的响应后, 第二个回调方法 () 被调用并处理第二个响应。invokeInitialRequestprocessInitialResponseprocessChainedResponse

public with sharing class ChainedContinuationController {

    // Unique label for the initial callout request
    public String requestLabel1;
    // Unique label for the chained callout request
    public String requestLabel2;
    // Result of initial callout
    public String result1 {get;set;}
    // Result of chained callout
    public String result2 {get;set;}
    // Endpoint of long-running service
    private static final String LONG_RUNNING_SERVICE_URL1 = 
        '<Insert your first service URL>';
    private static final String LONG_RUNNING_SERVICE_URL2 = 
        '<Insert your second service URL>';
           
    // Action method
    public Object invokeInitialRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(60);
      // Set callback method
      con.continuationMethod='processInitialResponse';
      
      // Create first callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL1);
      
      // Add initial callout request to continuation
      this.requestLabel1 = con.addHttpRequest(req);              
      
      // Return the continuation
      return con;  
    }
    
    // Callback method for initial request
    public Object processInitialResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel1);
      // Set the result variable that is displayed on the Visualforce page
      this.result1 = response.getBody();
           
      Continuation chainedContinuation = null;
      // Chain continuation if some condition is met
      if (response.getBody().toLowerCase().contains('expired')) {
          // Create a second continuation 
          chainedContinuation = new Continuation(60);
          // Set callback method
          chainedContinuation.continuationMethod='processChainedResponse';
          
          // Create callout request
          HttpRequest req = new HttpRequest();
          req.setMethod('GET');
          req.setEndpoint(LONG_RUNNING_SERVICE_URL2);
          
          // Add callout request to continuation
          this.requestLabel2 = chainedContinuation.addHttpRequest(req); 
      }
      
      // Start another continuation 
      return chainedContinuation;  
    }    
    
    // Callback method for chained request
    public Object processChainedResponse() {   
      // Get the response for the chained request
      HttpResponse response = Continuation.getResponse(this.requestLabel2);
      // Set the result variable that is displayed on the Visualforce page
      this.result2 = response.getBody();
           
      // Return null to re-render the original Visualforce page
      return null;
    }
}

注意

在创建新的延续之前,必须检索延续的响应 以及 Visualforce 请求再次暂停之前。您无法检索旧的 来自延续链中较早延续的回应。

从导入的 WSDL 进行异步标注

除了基于 -的标注之外, 在从 WSDL 生成的 Web 服务调用中支持异步标注 类。从 WSDL 生成的类进行异步标注的过程与此类似 到使用类的过程。

HttpRequestHttpRequest

在 Salesforce 中导入 WSDL 时,Salesforce 会为每个类自动生成两个 Apex 类 命名空间。一个类是同步的服务类 服务,另一个是异步服务的修改版本。这 自动生成的异步类名以 Async 前缀开头 其格式为 AsyncServiceName。 是原始未修改的服务类的名称。 异步类在以下方面与标准类不同。ServiceName

  • 公共服务方法包含一个额外的参数作为第一个参数。Continuation
  • Web 服务操作是异步调用的,其响应是 用 response 元素。getValue
  • 和 习惯于 分别调用服务和获取响应。WebServiceCallout.beginInvokeWebServiceCallout.endInvoke

您可以在 Salesforce 用户界面中从 WSDL 生成 Apex 类。从设置中, 在“快速查找”框中输入 Apex 类, 然后选择 Apex 类

若要进行异步 Web 服务标注,请在自动生成的 异步类,方法是将实例传递给这些方法。以下示例基于假设的股票报价 服务。此示例假定组织具有一个名为 的类,该类是自动生成的 通过 WSDL 导入。该示例演示如何对服务进行异步标注 通过使用自动生成的类。首先,此示例创建一个 以 60 秒的超时时间继续,并设置回调方法。接下来,代码 示例调用方法 将 Continuation 实例传递给它。方法调用对应于异步标注 执行。ContinuationAsyncSOAPStockQuoteServiceAsyncSOAPStockQuoteServicebeginStockQuotebeginStockQuote

public Continuation startRequest() {
   Integer TIMEOUT_INT_SECS = 60;  
   Continuation cont = new Continuation(TIMEOUT_INT_SECS);
   cont.continuationMethod = 'processResponse';
   
   AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap 
      stockQuoteService = 
        new AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap();
   stockQuoteFuture = stockQuoteService.beginStockQuote(cont,'CRM');    

   return cont;   
}

当外部服务返回异步标注(方法)的响应时,此回调方法是 执行。它通过调用响应对象上的方法来获取响应。beginStockQuotegetValue

public Object processResponse() {
   result = stockQuoteFuture.getValue();
   return null; 
}

以下是带有 action 和 callback 方法的整个控制器。

public class ContinuationSOAPController {
 
    AsyncSOAPStockQuoteService.GetStockQuoteResponse_elementFuture
           stockQuoteFuture;
    public String result {get;set;}

    // Action method
    public Continuation startRequest() {    
       Integer TIMEOUT_INT_SECS = 60;  
       Continuation cont = new Continuation(TIMEOUT_INT_SECS);
       cont.continuationMethod = 'processResponse';
       
       AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap 
          stockQuoteService = 
            new AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap();
           stockQuoteFuture = stockQuoteService.beginGetStockQuote(cont,'CRM');     
       return cont;   
    }    
    
    // Callback method
    public Object processResponse() {    
       result = stockQuoteFuture.getValue();
       // Return null to re-render the original Visualforce page
       return null; 
    }
}

此示例显示了调用该方法并显示结果的相应 Visualforce 页面 田。startRequest

<apex:page controller="ContinuationSOAPController" showChat="false" showHeader="false">
   <apex:form >      
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequest}" 
              value="Start Request" reRender="result"/> 
   </apex:form>

   <!-- This output text component displays the callout response body. -->
   <apex:outputText value="{!result}" />
</apex:page>

测试基于 WSDL 的异步标注

测试基于 WSDL 中的 Apex 类的异步标注与此类似 添加到与基于类的标注一起使用的流程。在测试 ContinuationSOAPController.cls 之前,请创建一个 实现。本类 启用对 ContinuationTestForWSDL.cls 的安全测试,该测试 我们稍后将通过启用模拟延续并确保 测试没有实际效果。HttpRequestWebServiceMock

public class AsyncSOAPStockQuoteServiceMockImpl implements WebServiceMock {
    public void doInvoke(
        Object stub, 
        Object request, 
        Map<String, Object> response,
        String endpoint, 
        String soapAction, 
        String requestName,
        String responseNS, 
        String responseName, 
        String responseType) {
        // do nothing
    }
}

此示例是对应于控制器的测试类。类中的测试方法 设置一个假响应并调用一个模拟的延续。标注不会发送到 外部服务。为了执行模拟标注,测试调用类的以下方法:Test.setContinuationResponse() 和 Test.invokeContinuationMethod()。ContinuationSOAPControllerTest

@isTest
public class ContinuationTestingForWSDL {
    public static testmethod void testWebService() {

        ContinuationSOAPController demoWSDLClass = 
            new ContinuationSOAPController();

        // Invoke the continuation by calling the action method
        Continuation conti = demoWSDLClass.startRequest();

        // Verify that the continuation has the proper requests
        Map<String, HttpRequest> requests = conti.getRequests();
        System.assertEquals(requests.size(), 1);

        // Perform mock callout 
        // (i.e. skip the callout and call the callback method)
        HttpResponse response = new HttpResponse();
        response.setBody('<SOAP:Envelope'
            + ' xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">'
            + '<SOAP:Body>'
            + '<m:getStockQuoteResponse '
            + 'xmlns:m="http://soap.sforce.com/schemas/class/StockQuoteServiceSoap">'
            + '<m:result>Mock response body</m:result>' 
            + '</m:getStockQuoteResponse>' 
            + '</SOAP:Body>'
            + '</SOAP:Envelope>');

        // Set the fake response for the continuation
        String requestLabel = requests.keyset().iterator().next();
        Test.setContinuationResponse(requestLabel, response);

        // Invoke callback method
        Object result = Test.invokeContinuationMethod(demoWSDLClass, conti);
        System.debug(demoWSDLClass);

        // result is the return value of the callback
        System.assertEquals(null, result);

        // Verify that the controller's result variable
        //   is set to the mock response.
        System.assertEquals('Mock response body', demoWSDLClass.result);
    }
}

支持类

支持类允许您与支持中心常用的记录进行交互, 例如营业时间和案例。

与企业合作 小时

营业时间用于指定营业时间 您的客户支持团队运作的地方,包括多项业务 多个时区的小时数。本示例查找时间 1 从 startTime 开始的工作时间,返回本地的 Datetime 时区。它通过查询 BusinessHours 来获取默认工作时间。 此外,它还调用该方法。

BusinessHoursadd

// Get the default business hours
BusinessHours bh = [SELECT Id FROM BusinessHours WHERE IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Find the time it will be one business hour from May 28, 2008, 1:06:08 AM using the
// default business hours.  The returned Datetime will be in the local timezone.
Datetime nextTime = BusinessHours.add(bh.id, startTime, 60 * 60 * 1000L);

本示例查找距离 1 个工作小时的时间 startTime,以 GMT 为单位返回日期时间:

// Get the default business hours
BusinessHours bh = [SELECT Id FROM BusinessHours WHERE IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Find the time it will be one business hour from May 28, 2008, 1:06:08 AM using the
// default business hours.  The returned Datetime will be in GMT.
Datetime nextTimeGmt = BusinessHours.addGmt(bh.id, startTime, 60 * 60 * 1000L);

下一个示例查找 startTime 之间的差异 和 nextTime:

// Get the default business hours
BusinessHours bh = [select id from businesshours where IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Create Datetime on May 28, 2008 at 4:06:08 PM in local timezone.
Datetime endTime = Datetime.newInstance(2008, 5, 28, 16, 6, 8);

// Find the number of business hours milliseconds between startTime and endTime as
// defined by the default business hours.  Will return a negative value if endTime is
// before startTime, 0 if equal, positive value otherwise.
Long diff = BusinessHours.diff(bh.id, startTime, endTime);

使用案例

传入和传出 可以使用以下方法将电子邮件与其相应的案例相关联 类方法。 此方法与 Email-to-Case(一个自动化过程)一起使用 这将从客户那里收到的电子邮件转变为客户服务案例。CasesgetCaseIdFromEmailThreadId

以下示例使用电子邮件线程 ID 来检索 相关案例 ID。

public class GetCaseIdController {

   public static void getCaseIdSample() {
        // Get email thread ID 
        String emailThreadId = '_00Dxx1gEW._500xxYktg';
        // Call Apex method to retrieve case ID from email thread ID 
        ID caseId = Cases.getCaseIdFromEmailThreadId(emailThreadId);
    
    }
}

区域管理2.0

通过对 Territory2 和 UserTerritory2Association 标准对象的触发器支持, 您可以自动执行与这些区域管理中的更改相关的操作和流程 记录。

Territory2 的示例触发器

此示例触发器在创建或删除 Territory2 记录后触发。 此示例触发器假定组织具有一个名为 TerritoryCount__c 的自定义字段,该字段在 Territory2Model 对象上定义为 跟踪每个区域模型中的区域净数量。触发器代码 每次创建或删除区域时,递增或递减TerritoryCount__c字段中的值。

trigger maintainTerritoryCount on Territory2 (after insert, after delete) {
    // Track the effective delta for each model
    Map<Id, Integer> modelMap = new Map<Id, Integer>();
    for(Territory2 terr : (Trigger.isInsert ? Trigger.new : Trigger.old)) {
       Integer offset = 0;
       if(modelMap.containsKey(terr.territory2ModelId)) {
           offset = modelMap.get(terr.territory2ModelId);
       }
       offset += (Trigger.isInsert ? 1 : -1);
       modelMap.put(terr.territory2ModelId, offset);
    }
    // We have a custom field on Territory2Model called TerritoryCount__c
    List<Territory2Model> models = [SELECT Id, TerritoryCount__c FROM 
                            Territory2Model WHERE Id IN :modelMap.keySet()];
    for(Territory2Model tm : models) {
       // In case the field is not defined with a default of 0
       if(tm.TerritoryCount__c == null) {
           tm.TerritoryCount__c = 0;
       }
       tm.TerritoryCount__c += modelMap.get(tm.Id);
    }
    // Bulk update the field on all the impacted models
    update(models);
}

UserTerritory2Association 的示例触发器

此示例触发器在创建 UserTerritory2Association 记录后触发。 此示例触发器向 Sales Operations 组发送电子邮件通知 让他们知道用户已添加到区域。它标识用户 谁将用户添加到区域。然后,它标识每个添加的用户以及 用户被添加到的区域以及区域所属的区域模型 自。

trigger notifySalesOps on UserTerritory2Association (after insert) {
    // Query the details of the users and territories involved
    List<UserTerritory2Association> utaList = [SELECT Id, User.FirstName, User.LastName, 
       Territory2.Name, Territory2.Territory2Model.Name 
       FROM UserTerritory2Association WHERE Id IN :Trigger.New];
            
    // Email message to send
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    mail.setToAddresses(new String[]{'salesOps@acme.com'}); 
    mail.setSubject('Users added to territories notification');     
            
    // Build the message body   
    List<String> msgBody = new List<String>();
    String addedToTerrStr = '{0}, {1} added to territory {2} in model {3} \n';    
    msgBody.add('The following users were added to territories by ' +  
        UserInfo.getFirstName() + ', ' + UserInfo.getLastName() + '\n');
    for(UserTerritory2Association uta : utaList) {
       msgBody.add(String.format(addedToTerrStr, 
           new String[]{uta.User.FirstName, uta.User.LastName,
                        uta.Territory2.Name, uta.Territory2.Territory2Model.Name}));    
    } 
    
    // Set the message body and send the email
    mail.setPlainTextBody(String.join(msgBody,''));
    Messaging.sendEmail(new Messaging.Email[] { mail });
}

Salesforce 站点

Salesforce Sites 允许您通过继承来构建自定义页面和 Web 应用程序 闪电平台功能,包括分析、工作流程和审批,以及可编程 逻辑。

您可以使用 和 类的方法在 Apex 中管理您的 Salesforce 站点。SiteCookie

  • 重写 Salesforce 站点的 URL 站点提供内置逻辑,可帮助您显示用户友好的 URL 和指向站点
    访问者的链接。创建规则以重写在地址栏中键入的、从书签启动的或从外部网站链接的 URL 请求。您还可以创建规则来重写网站页面中链接的 URL。URL 重写不仅使 URL 对用户更具描述性和直观性,还允许搜索引擎更好地索引您的网站页面。

重写 Salesforce 站点的 URL

协作平台提供了内置逻辑,可帮助您显示用户友好的网址和指向 网站访问者。

创建规则以重写键入的 URL 请求 进入地址栏,从书签启动,或从外部网站链接。您可以 此外,还要创建规则来重写网站页面中链接的 URL。URL 重写不 只会使 URL 对用户更具描述性和直观性,它允许搜索引擎 更好地为您的网站页面编制索引。

例如,假设您有一个博客网站。如果没有 URL 重写,博客条目的 URL 可能如下所示: https://myblog.my.salesforce-sites.com/posts?id=003D000000Q0PcN

通过 URL 重写,您的用户可以按日期和标题访问博客文章,而不是 按记录 ID。您的新年前夜帖子的网址可能是:https://myblog.my.salesforce-sites.com/posts/2019/12/31/auld-lang-syne

您还可以重写网站页面中显示的链接的 URL。如果你的新年前夜帖子 包含指向您的情人节帖子的链接,链接网址可能会显示:https://myblog.my.salesforce-sites.com/posts/2019/02/14/last-minute-roses

若要重写站点的 URL,请创建一个 Apex 类,该类将原始 URL 映射到 用户友好的 URL,然后将 Apex 类添加到您的网站。

若要了解 中的方法,请参阅 UrlRewriter 接口。Site.UrlRewriter interface

创建 Apex 类

您创建的 Apex 类必须实现提供的接口。通常,它必须具有 以后 形式:

Site.UrlRewriter

global class yourClass implements Site.UrlRewriter {
    global PageReference mapRequestUrl(PageReference
            yourFriendlyUrl)
    global PageReference[] generateUrlFor(PageReference[]
            yourSalesforceUrls);
}

在创建 Apex 类时,请考虑以下限制和建议:类和方法必须是全局的Apex 类和方法必须全部为 。global类必须同时包含这两种方法Apex 类必须同时实现 和 方法。如果您不想使用以下之一 方法,只需让它返回即可。mapRequestUrlgenerateUrlFornull重写仅适用于 Visualforce 网站页面传入的 URL 请求只能映射到关联的 Visualforce 页面 与您的网站。无法映射到标准页面、图像或其他 实体。要重写网站网页上链接的网址,请使用带有 merge 变量的函数。例如 以下链接到名为 我的页面:!URLFOR$Page

<apex:outputLink value="{!URLFOR($Page.myPage)}"></apex:outputLink>

注意

Visualforce 元素 不受 的影响。<apex:form>forceSSL=”true”urlRewriter请参阅 Visualforce 开发人员的 指南。编码的 URL使用该接口获取的 URL 已编码。如果需要 访问 URL 的未编码值,请使用 EncodingUtil 类的方法。Site.urlRewriterurlDecode受限制的字符用户友好的 URL 必须与 Salesforce URL 不同。带有 3 个字符的实体前缀或 15 或 18 个字符的 ID 不是 重写。您不能在用户友好网址或重写的网址中使用句点,但以下情况除外 对于路径 组件,不能在 URL 末尾使用。.well-known受限制的字符串不能使用以下保留字符串作为第一个路径组件 在用户友好的 URL 或重写的站点基本 URL 之后 网址。网站基本 URL 之后的第一个过去组件的一些示例 是 baseURL https://.my.salesforce-sites.com/baseURL, https://.my.salesforce-sites.com/pathPrefix/baseURL, https://custom-domain/pathPrefix/baseURL,以及 https://.my.salesforce-sites.com/pathPrefix/baseURL/another/path。MyDomainNameMyDomainNameMyDomainName

  • apexcomponent
  • apexpages
  • aura
  • chatter
  • chatteranswers
  • chatterservice
  • cometd
  • ex
  • faces
  • flash
  • flex
  • google
  • home
  • id
  • ideas
  • idp
  • images
  • img
  • javascript
  • js
  • knowledge
  • lightning
  • login
  • m
  • mobile
  • ncsphoto
  • nui
  • push
  • resource
  • saml
  • sccommunities
  • search
  • secur
  • services
  • servlet
  • setup
  • sfc
  • sfdc
  • sfdc_ns
  • sfsites
  • site
  • style
  • vote
  • WEB-INF
  • widg

不能在重写的末尾使用以下保留字符串 URL 路径:

  • /光环
  • /auraFW
  • /auraResource
  • /AuraJLoggingRPCService
  • /AuraJLVRPC服务
  • /AuraJRPC服务
  • /dbcthumbnail
  • /HelpAndTrainingDoor
  • /htmldbcthumbnail
  • /升
  • /米
  • /移动

仅相对路径PageReference.getUrl() 方法 仅返回紧跟在主机名后面的 URL 部分,或者 站点前缀(如果有)。例如,如果您的 URL 是 https://mycompany.my.salesforce-sites.com/sales/MyPage?id=12345, 其中“sales”是网站前缀,仅返回 /MyPage?id=12345。您无法重写域或网站前缀。仅限唯一路径您无法将 URL 映射到与您的网站同名的目录 前缀。例如,如果您的网站网址为 https://acme.my.salesforce-sites.com/help, 其中“help”是站点前缀,则不能指向 URL 到帮助/页面。生成的路径 https://acme.my.salesforce-sites.com/help/help/page、 将作为 https://acme.my.salesforce-sites.com/help/page 返回。批量查询为了提高页面生成性能,请批量执行任务 而不是一次一个的方法。generateUrlFor强制执行字段唯一性确保您选择用于重写 URL 的字段是唯一的。用 SOQL 中用于查询的唯一字段或索引字段可能会得到改进 性能。

向站点添加 URL 重写

创建 URL 重写 Apex 类后,请按照以下步骤将其添加到 您的网站:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“站点”。Sites
  2. 单击“新建”或单击“编辑” 现有站点。
  3. 在“站点编辑”页上,为“URL 重写器”选择一个 Apex 类 类。
  4. 点击保存

注意

如果您的网站上启用了 URL 重写,则所有 PageReferences 都是 通过 URL 重写器传递。设置为 且不是 0 的 PageReferences 返回重定向的 URL,而不是 重写的 URL。redirecttrueredirectCode

代码示例

在此示例中,我们有一个由两个 Visualforce 页面组成的简单站点:mycontact 和 myaccount。确保您为两者启用了“读取”权限 在尝试示例之前。每个页面都使用标准控制器作为其对象 类型。联系页面包括指向父帐户的链接以及联系人 详。

在实现重写之前,地址栏和链接 URL 显示记录 ID(a 随机 15 位字符串),如“之前”图所示。启用重写后, 地址栏和链接显示更多用户友好的重写 URL,如“之后”所示 图。

用于重写这些页面的 URL 的 Apex 类显示在 URL 重写 Apex 类示例中,并附有详细的注释。

示例网站页面

本部分显示此部分中使用的客户和联系人页面的 Visualforce 例。帐户页面对帐户使用标准控制器,只不过是一个 标准详情页面。此页面应命名为 我的帐户。

<apex:page standardController="Account">
    <apex:detail relatedList="false"/>
</apex:page>

联系人页面使用标准控制器进行联系人,由两部分组成。 第一部分使用函数和合并变量链接到父帐户;第二个只是提供联系方式。请注意, Visualforce 页面不包含任何重写逻辑,但 .此页面应命名为 mycontact。

URLFOR$PageURLFOR

<apex:page standardController="contact">
    <apex:pageBlock title="Parent Account">
        <apex:outputLink value="{!URLFOR($Page.mycontact,null, 
                [id=contact.account.id])}">{!contact.account.name}
                </apex:outputLink>
    </apex:pageBlock>
    <apex:detail relatedList="false"/>
</apex:page>

示例 URL 重写 Apex 类

用作站点的 URL 重写器的 Apex 类使用该方法来映射传入的 URL 请求到正确的 Salesforce 记录。它还使用该方法重写 链接到帐户页面更加人性化 形式。

mapRequestUrlgenerateUrlFor

global with sharing class myRewriter implements Site.UrlRewriter {

    //Variables to represent the user-friendly URLs for
    //account and contact pages
    String ACCOUNT_PAGE = '/myaccount/';
    String CONTACT_PAGE = '/mycontact/';
    //Variables to represent my custom Visualforce pages
    //that display account and contact information
    String ACCOUNT_VISUALFORCE_PAGE = '/myaccount?id=';
    String CONTACT_VISUALFORCE_PAGE = '/mycontact?id=';

    global PageReference mapRequestUrl(PageReference
            myFriendlyUrl){
        String url = myFriendlyUrl.getUrl();

        if(url.startsWith(CONTACT_PAGE)){
            //Extract the name of the contact from the URL
            //For example: /mycontact/Ryan returns Ryan
            String name = url.substring(CONTACT_PAGE.length(),
                    url.length());

            //Select the ID of the contact that matches
            //the name from the URL
            Contact con = [SELECT Id FROM Contact WHERE Name =:
                    name LIMIT 1];

            //Construct a new page reference in the form
            //of my Visualforce page
            return new PageReference(CONTACT_VISUALFORCE_PAGE + con.id);
        }
        if(url.startsWith(ACCOUNT_PAGE)){
            //Extract the name of the account
            String name = url.substring(ACCOUNT_PAGE.length(),
                    url.length());

            //Query for the ID of an account with this name
            Account acc = [SELECT Id FROM Account WHERE Name =:name LIMIT 1];

           //Return a page in Visualforce format
            return new PageReference(ACCOUNT_VISUALFORCE_PAGE + acc.id);
        }
        //If the URL isn't in the form of a contact or
        //account page, continue with the request
        return null;
    }
    global List<PageReference> generateUrlFor(List<PageReference> 
            mySalesforceUrls){
        //A list of pages to return after all the links 
        //have been evaluated
        List<PageReference> myFriendlyUrls = new List<PageReference>();
        
        //a list of all the ids in the urls
        List<id> accIds = new List<id>();
        
        // loop through all the urls once, finding all the valid ids
        for(PageReference mySalesforceUrl : mySalesforceUrls){
        //Get the URL of the page
        String url = mySalesforceUrl.getUrl();
 
            //If this looks like an account page, transform it
            if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
                //Extract the ID from the query parameter
                //and store in a list
                //for querying later in bulk.
                        String id= url.substring(ACCOUNT_VISUALFORCE_PAGE.length(),
                        url.length());
                        accIds.add(id);
            }
        }

    // Get all the account names in bulk
    List <account> accounts = [SELECT Name FROM Account WHERE Id IN :accIds];

    // make the new urls
    Integer counter = 0;

    // it is important to go through all the urls again, so that the order
    // of the urls in the list is maintained. 
    for(PageReference mySalesforceUrl : mySalesforceUrls) {

       //Get the URL of the page
       String url = mySalesforceUrl.getUrl();

       if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
         myFriendlyUrls.add(new PageReference(ACCOUNT_PAGE + accounts.get(counter).name));
         counter++;
       } else {
         //If this doesn't start like an account page,
         //don't do any transformations
         myFriendlyUrls.add(mySalesforceUrl);
       }
    }
   
    //Return the full list of pages
    return myFriendlyUrls;
  }

}

重写之前和之后

下面是实现 Apex 类以重写 原始站点 URL。请注意第一张图中基于 ID 的 URL,以及 第二个用户友好的 URL。

重写之前的网站 URL之前的网站网址此图中编号的元素是:

  1. 重写之前联系人页面的原始 URL
  2. 从联系页面到父帐户页面的链接
  3. 重写前指向帐户页面的链接的原始 URL,如 浏览器的状态栏

重写后的网站 URL之后的网站网址此图中编号的元素是:

  1. 重写后联系人页面的重写 URL
  2. 从联系页面到父帐户页面的链接
  3. 重写后指向帐户页面的链接的重写 URL,如 浏览器的状态栏