Apex 部署与托管包完全指南
Apex部署与托管包完全指南:Compile On Deploy(生产+Full Sandbox默认/Developer+Sandbox可选/减少部署后性能影响)、部署方法(Change Sets连接org/VS Code+CLI源驱动/Metadata API异步+package.xml/Tooling API单元素增量/DevOps Center流水线)、托管包类型(1GP+2GP命名空间分离)、Apex版本化(global不可改/webservice不可弃用/@Deprecated标记/public+private可改)、安全审查认证(独立Governor Limit)、最佳实践(@isTest(OnInstall=true)/向后兼容/命名空间/@TestVisible/Stub API)、包升级流程(开发→Beta→安全审查→发布→订阅者升级/Major需授权/不可回滚)。
本课程共有 8 个章节
同学们,咱们来聊一个特别重要的前提:你绝对不能在Salesforce的生产环境里直接写Apex代码。这就像你不能在正在飞行的飞机上修引擎一样,太危险了。所以,咱们所有开发、调试的工作都要放在安全的“实验室”里完成——也就是沙箱、Scratch组织,或者免费的Developer Edition组织。 这一章,我会带大家完整走一遍从开发到上线的整个生命周期。我们会讲到各种部署方法,比如用变更集、用SFDX命令怎么推上去。然后重点介绍托管包的生命周期,因为如果你们将来要做AppExchange上的应用,必须得懂这个。这里会涉及版本化的Apex代码行为——同一个类在不同版本里怎么共存,怎么平滑升级。 另外,我们还会讨论代码的弃用策略,也就是你怎么优雅地告诉用户“这个方法马上要退休了,请用新的”。测试当然不能少,安全稳定的发布一定得靠高覆盖率的测试护航。 接着,我会分享一些写全球通用Apex的最佳实践,比如怎么处理不同地区的日期、数字格式,让代码在全世界跑起来都不出错。还有如果你是在给订阅者开发安装包,有哪些特别的注意事项,比如怎么保护API、怎么管理权限。最后,我们会重点讲安全升级策略,确保你每次推送新版本时,用户的数据和自定义都不受影响,平稳过渡。 好,做好准备,我们这就开始深入这一整套部署和生命周期的学问。
今天我们来聊聊 Salesforce 里的一个编译选项,叫做“部署时编译”——英文是 Compile On Deploy。听名字就知道,它跟部署有关。 简单说,当你把代码部署到某个环境,或者安装、升级一个托管包的时候,Salesforce 会在完成之前,自动把你所有的 Apex 代码重新编译一遍。这个编译过程是编译成字节码,并且把结果保存下来。这样,之后用户在使用系统的时候,就不需要再临时编译,直接就能运行了。 你可能会担心,那部署时间会不会变长?其实增加的时间非常少,几乎可以忽略,但它换来的好处很明显:部署之后,活跃用户和那些自动化流程不会遇到因为要实时编译而导致的性能问题,体验会顺滑很多。 在生产环境和完整沙箱里,这个功能是始终开启的,你没法关掉。但对于其他类型的组织,比如开发者沙箱或者部分复制沙箱,你需要在 Apex 设置里手动打开它。 如果你的共享组织是用于功能测试或者 CI/CD 流水线的,特别建议你把这个编译选项打开。这样每次部署后,运行测试或自动化验证的时候,就不会因为首次编译而出现不稳定的延迟。 总结一下,Compile On Deploy 就是帮你把编译的工作提前到部署阶段完成,用户使用时直接用编译好的字节码,又快又稳。在生产环境它强制开启,其他环境记得手动打开,对测试和持续集成都很有帮助。
同学们,今天我们聊一聊怎么把写好的Apex代码部署到其他环境里。在Salesforce里,最传统的方式就是用“变更集”。你可以在开发环境里创建一个“传出变更集”,把相关的Apex组件和它们的依赖项都勾选上,然后上传到目标环境。目标环境的管理员会看到“入站变更集”,他们审核之后点一下部署,变更就过去了。 不过在现代化的开发里,很多人更喜欢用VS Code或者Code Builder直接开发。这些工具通过Salesforce CLI(也就是命令行接口)进行源代码驱动的部署,可以更灵活地直接从本地上传代码,不需要变更集那套步骤。 无论是哪种方式,部署的流程大概是这样:我们首先在沙箱或者scratch org里进行开发,功能写完后,一定要编写测试类,而且测试覆盖率至少达到75%。接下来,根据你的项目需求选择一种部署方法,把代码部署到目标环境。在部署到生产环境的过程中,测试会自动运行,只有测试通过,部署才算成功。最后,别忘了去目标环境里验证一下功能是否正常,确保一切顺利。这样就完成了一次安全的Apex部署。
好,同学们,我们今天花几分钟来聊聊 Salesforce 里三种部署和管理变更的方式,弄清楚它们分别适合什么场景。你以后在做发布的时候,就不会再纠结该用哪个工具了。 首先,我们来看,元数据 API,。你可以把它想象成一个编程接口,专门用来把你的自定义配置,比如 Apex 类、触发器,还有那些复杂的自定义元数据类型,以代码的方式部署出去。简单说,如果你希望写脚本,自动化地把整个元数据包推送到其他环境,那元数据 API 就是你的好帮手。 接着是,工具 API,。相比之下,它走的是精细路线。假如你刚刚只修改了一个 Apex 类,或者只想更新一个触发器,你不需要动整个大包,只要通过工具 API,就可以只部署那一个元素。一次只改一点点,这非常适合那种紧急修复、或者不想因为一个小变动而影响其他组件的场景,控制力度非常细。 最后,,DevOps 中心, 带来的是整个发布管理体验的提升。它让你可以搭建一条管道,把你的工作项从一个阶段推到下一个阶段——比如先开发,再到测试,接着是阶段环境,最后上生产。而且它天生就和版本控制(比如 Git)集成在一起,让你可以把代码和配置的变更都管得井井有条。如果你的团队想要一套从源码到上线的完整流水线,DevOps 中心就很对路。 总结下来,这三种方法其实对应了不同的需求: - 如果你只是需要,简单传输,,或者倾向于,源驱动开发,,元数据 API 这种编程方式很灵活; - 如果要,自动化,,你可以基于元数据 API 写脚本; - 想要,细粒度控制,,一次只动一个文件,那工具 API 最顺手; - 要是你追求,完整的管道管理,,从版本控制到多阶段发布,DevOps 中心就是为此设计的。 你看,工具没有绝对的好坏,看你当下的需求更像哪一种,选对方法,部署和发布就能省掉很多麻烦。 好了,这节课就到这里,我们下次继续。
咱们今天来聊聊 Salesforce 里托管包的三种类型,以及怎么在 Apex 里根据包的版本做不同的事情。你可能听说过 1GP、2GP 还有 Unlocked 包,但别担心,我用最简单的话给你讲清楚。 首先,1GP,全称是第一代托管包,其实就是咱们在 AppExchange 上最常见的那种传统包。它跟组织绑定得比较紧,开发流程大多是直接在打包组织里点点改改。这个方式已经很多年了,稳定但不够灵活。 接着是 2GP,第二代托管包。它是从源代码驱动的,就是说你的代码放在版本控制系统里,比如 Git,然后 Salesforce 根据你源码的版本来创建包版本。它的一个很大改进是命名空间的可见性控制更精细,而且允许你构建模块化的包,把功能拆分开来,更适合现代开发流程。你可能会同时维护同一个包的不同主要版本,让订阅者可以选择升级到什么版本。 最后是 Unlocked 包,这个其实主要不是给 AppExchange 上架用的,而是用于企业内部,尤其是想在多个 org 间共享组件,但又不想走很重的 AppExchange 审核流程。Unlocked 包也是从源代码创建的,但它没有命名空间,或者可以使用一个没有发布到 AppExchange 的命名空间,很适合内部开发和分发。 现在关键来了:无论你是作为包的开发者,还是作为使用包的订阅者写 Apex 代码,都需要有能力知道“我当前运行的代码是属于哪个版本的包”。因为你的逻辑可能需要根据包版本来走不同的分支。这时候,System.requestVersion() 方法就派上用场了。 System.requestVersion() 返回一个 Version 对象,它描述的是当前正在执行的代码所属的包版本。注意,它不是订阅者安装的包的版本,而是具体正在调用的那部分代码所在的包版本。比如,你的扩展包或者订阅者代码调用了托管包里某个类的方法,那个方法里调用的 System.requestVersion() 返回的就是托管包自己的版本。 然后你可以用 Version 对象的 compareTo 方法来做比较。假设你作为包开发者,想对 2.0 版本以上的包启用一套新逻辑,而对旧版本保持兼容,你就可以这样写: ```apex if (System.requestVersion().compareTo(new Version(2, 0)) >= 0) { // 执行新版本的逻辑 } else { // 执行旧版本的逻辑 } ``` compareTo 方法会返回一个整数:如果当前版本比传入的版本大,返回正数;相等返回 0;小的话返回负数。所以 >=0 就代表当前版本大于等于 2.0。 还有个重要概念:请求上下文。System.requestVersion() 返回的版本在整个请求的调用链里是保持一致的,哪怕你的方法内部调用了同一个包里的其他方法,这个版本号也不会变。但它只持续到当前请求结束,下一个请求就会重新根据实际调用的包确定。 简单总结一下:1GP 是传统打包,2GP 是现代源码驱动且更适合模块化,Unlocked 包用于内部共享。你需要用 System.requestVersion() 拿到当前执行代码的包版本,然后通过 compareTo 来编写版本兼容条件。好啦,今天的知识点就这么些,下回咱们来动手写写代码试试看。
同学们,我们花几分钟来看一下这张幻灯片,它讲的是一些在Salesforce开发中,特别是做托管包的时候,关于Apex代码版本化和修改限制的重要规则。 首先,大家一定要理解一个点:并不是所有的Apex项目都“版本化”了。什么意思呢?如果你是在一个普通的Salesforce环境中直接写代码,比如你在开发者版里练手,那其实你的每一次代码改动,都会立刻反映到所有引用它的地方,没有版本号的概念。但是,一旦你开始创建托管包,而且这个包发布了不同的版本,那情况就完全不一样了。对于托管包里的Apex类、方法这些东西,它们就和包的版本绑定了。这样做的好处是,你可以让老版本的客户继续使用旧的行为,而不受你最新代码改动的影响——这就是“版本化”的意义。 那这个幻灯片告诉我们,托管包里的一些Apex项目,注意,是一些,并没有被版本化。具体来说,有哪些呢?比如你自己定义的自定义异常类(异常类是不会被版本化的),还有Apex里定义的枚举类型(枚举也是),另外还有那些被标记为 @Deprecated 的东西,也就是我们接下来要讲的“废弃”标记。这些项目,一旦你在新版本包里做了修改,它就会影响到所有已经安装了这个包的任何版本的订阅者。所以对这些非版本化的东西,改动真的要特别小心,因为你的改动直接跨版本生效了。 接着我们来看访问修饰符的更改规则。在编写包的时候,我们可能会想在以后的新版本中扩大某个方法的可见性,或者改变它的特性。但是,这里有一堆限制,而且这些限制是不可逆的。首先,你可以“扩大”访问权限。比如,你在第一个版本里把一个方法声明成 private 的,那么后续版本你可以把它改成 protected,这没问题,再往后你甚至能把它改成 public,最后甚至改成 global。注意这里的顺序:私人 → 受保护 → 公开 → 全局。只能从小到大扩,不能从大到小缩。为什么呢?因为你一旦公开了某个API,就有订阅者可能依赖它,你突然缩小权限,订阅者的代码就报错了。所以,只能扩大,不能缩小。还有,如果你当初定义了一个抽象类(abstract),你以后可以把它改成虚拟的(virtual),因为 virtual 是 abstract 的超集,virtual 类允许自己有实现,而 abstract 强制子类实现,所以把它从抽象变虚拟相当于放宽了限制,这是允许的。但是反过来,你绝对不能把 virtual 改成 abstract,也不能把 abstract 直接删掉。更严格的是,一旦声明为 global 和 virtual,这两个修饰符永远不能删除。因为 global 表示跨命名空间的公开API,你一删,所有外部调用全挂,所以Salesforce禁止你删。virtual 也一样,一旦声明,就永远不能收回,因为可能有子类在依赖着这种可重写的行为。抽象也不能删除,理由类似。 再来看 @Deprecated 这个注解。它的作用就是:当你准备在未来的版本里移除某个标识符(类、方法、变量等),但你现在还不想让它立刻消失,你可以先标记它为“废弃”。对于已经安装了旧版本包的现有订阅者来说,这些被废弃的东西依然完全有效,代码能正常运行。但重点是,对于新的订阅者,也就是那些首次安装你新版本包的人,这些被 @Deprecated 标记的东西对他们是不可见的——他们无法在代码提示里看到,也无法调用。这给了老客户一个缓冲期,让他们修改自己的代码,同时又能防止新用户不小心使用即将淘汰的API。不过注意,在托管包的Beta测试版本里,@Deprecated 的东西还是可见的,这是为了让你在发布前能充分测试过渡效果。只有在你正式发布(上传)了托管已发布版本后,它们才真正对新订阅者隐藏。 最后还有几个不能做弃用操作的特殊情况:你不能弃用 Web Service(也就是那些被声明为 webservice 关键字的方法),因为 web service 一般是同步 SOAP API 的一部分,你直接弃用它,会导致所有依赖它的集成系统突然找不到方法,这风险很大,所以 Salesforce 不允许。还有,你不能单独弃用枚举类型里的某一个值,要是真想弃用整个枚举,你必须整体弃用这个枚举类型。同样的,接口里的某个方法不能单独弃用,抽象类里的某个抽象方法也不能单独弃用。因为这些都属于一个整体定义的一部分,部分弃用会破坏整个类型契约的完整性。 总结一下,为了包的安全演进,你要记住:非版本化的项目(异常、枚举、废弃项)改动全局生效;访问修饰符只能扩大不能缩小,global 和 virtual 一加就永远不能撤;@Deprecated 是优雅迁移的利器,但要注意它对新订阅者的隐藏,以及它不能用于 web service、枚举值、接口方法和抽象方法。好了,这一页内容虽然不多,但规则密集,希望大家结合托管包开发的实践经验来体会这些限制存在的意义。下一节我们继续。
今天我们来聊一下Salesforce Apex开发中关于包版本化行为的一些关键点,特别是在你给客户做托管包,或者你自己就是订阅者的时候,怎么稳妥地管理版本。 首先,测试版本化的行为,你可以用 System.runAs 方法,但注意参数里可以传入一个 System.Version 对象,这样就能模拟代码在某个特定包版本的上下文中运行。听起来有点绕,简单说就是:你写了个托管包,有不同版本,你想测试老版本下的行为有没有被新版本破坏,就可以用这个办法模拟出来。 接下来是一些最佳实践。第一,谨慎使用 global 访问修饰符。因为一旦你把一个类或方法声明为 global,它就成了公开的API,删不掉了,不然订阅者的代码就会崩。所以设计合约的时候要非常小心,这些是永久性的,别一拍脑袋就 global 出去。每次变更都要记录版本的变化,这样出了事也好追溯。 还有一个好用的工具叫 @namespaceAccessible 注解(注意你这里可能写成了 @spaceupsible,但实际是 @namespaceAccessible)。对于第二代托管包(2GP),如果你想跨包访问一些资源,就可以用这个注解,而不用非得把东西全部设为 global,这样更安全也更灵活。 再说到 Agentforce 相关,如果你写的可调用方法会被 Agentforce 调用,那一定要做得稳健:能够处理批量请求,并且文档要写充分,让别人或者AI知道怎么用、边界条件是什么。 最后从订阅者的角度看,当你的行为确实会影响到对包中 Apex 类的依赖时,你应该把关键的 Apex 类锁定到某个特定的包版本上。升级之前,必须做完整的测试,避免新版本悄悄改了你不期望的东西。另外,可以用“显示客观性(Show Objectiveness)”这个功能(可能指“显示依赖”或某种引用查看器),来搞清楚你的代码到底引用了包的哪些部分,这样版本变化时一目了然。 总结一下:版本管理靠的是克制和记录,不要随手 global,用了就删不掉;测试要覆盖不同版本上下文;订阅方也要主动锁定版本并测试,不要被动升级。这样整个生态才会更稳定。
好,今天我们来讲一下安全升级演练,它非常直观地展示了Salesforce开发中一个很重要的概念——共享责任模型。 想象一下这个场景:我们作为开发者,发布了一个托管包的1.0版本,里面有一些类和方法,订阅者拿到了这个版本后,为了让功能更贴合自己的业务,可能会去重写、或者说覆盖掉我们原本实现的一部分逻辑。这完全没问题,也是平台提供的灵活能力。 但接下来,如果我们发布了2.0版本,把原来那些被覆盖的功能,在底层做了新的、本地的实现,情况就不同了。对于订阅者来说,如果他们直接升级,但没有做任何锁定版本的动作,那他们之前在1.0上精心调整的重写逻辑,可能就和新版本的代码打架了,出现错误或者不符合预期的结果。这是很头疼的事。 这个问题的根源,就在于升级时缺少一个版本固定的保护。所以,解决方案其实分两层,需要开发者和订阅者各负其责,这就是共享责任。 对订阅者而言,最稳妥的做法是,把自己写的Apex类明确固定到1.0版本上。这样,哪怕底层包升级到了2.0,这些类运行的时候,还是会继续调用1.0版本的上下文,你之前做的重写依然有效,不会被新版本意外冲掉。这是订阅者这边的责任。 那开发者呢?我们不能只让订阅者去兜底。我们自己的代码,也要主动实现向后兼容。这里就要用到一个很关键的方法,叫 System.requestVersion( )。通过在代码里使用这个方法和适当的条件逻辑,我们可以判断出当前包正在被哪个版本调用。比如说,如果发现调用方仍然基于1.0版本,就继续走老的兼容路径;如果是2.0的新调用,就走新的优化逻辑。这样两个版本就能和谐共存,不会突然崩掉。 最后,要实现顺利升级,光靠代码还不够。双方还需要通过清晰的文档、发行说明来提前沟通。开发者在发版说明里就要讲清楚:新版改了哪些底层实现,哪些地方容易被覆盖影响到,推荐订阅者怎么做版本固定。订阅者看到这些说明后,也可以有的放矢地去检查和管理自己的固定版本。这样互相配合,整个升级过程才会平顺,不出意外。 所以,总结一下,安全升级演练告诉我们:版本升级不是一个人的事,开发者和订阅者必须像齿轮一样咬合起来,开发者用System.requestVersion做好向下兼容,订阅者用版本固定保住自己的定制,再配合上清晰的文档同步,就能一起完成一次漂亮、平稳的升级。