异步 Apex 类
异步Apex类
一个Apex类可以定义为异步类,用于异步执行。
异步类可以通过多种方式实现:
- Future注解
- 批处理
- Queueable接口
- Schedulable接口
Future注解
使用Future注解可以将一个Apex函数定义为异步执行类。该类会拥有自己的线程,并在此线程中独立运行,实现异步效果。
Future注解的应用示例:
global class ExampleClass {
@future
public static void exampleFutureFunction(List<Id> recordIds) {
// ..
}
}
要注意的是,在Future函数中不可以用sObject对象作为参数,因为Future函数是独立运行的,如果将sObject对象传入其中,该sObject对象在Future函数运行的同时有可能被改变,这样有可能会造成错误。
定义为Future的函数必须是静态的(static),并且返回类型必须是void。
由于异步函数的特点,在多个Future函数同时执行的时候,执行的顺序是不确定的,完成的顺序也不确定。所以作为开发者,我们不能设想用一个Future函数的执行结果来影响另一个Future函数,也不能从Future函数中调用另一个Future函数。
在Apex函数中调用外部网络服务
在Apex函数中调用外部网络服务时,可以定义该函数为Future,并加入“callout=true”。比如:
@future(callout=true)
public static void callWebService() {
String result = ExampleWebServiceClass.getWebServiceResult();
}
通过这种方式,此函数不需要等待网络服务的回应,从而可以继续执行其他的功能。
Future函数单元测试
在对Future函数进行单元测试时,必须将测试的代码放入“startTest()”和“stopTest()”函数中间。“stopTest()”函数可以确保在异步执行的函数得到结果之后再继续执行后面的代码。比如:
@isTest
static void testExampleFutureFunction() {
// ...
Test.startTest();
ExampleClass.exampleFutureFunction();
Test.stopTest();
// ...
}
最佳实践
由于Salesforce将所有异步函数保存在一个队列中执行,所以如果同时运行的Future函数过多,会导致异步执行的效率降低。
使用Future函数时,尽量保证该函数被尽快执行。
如果需要同时调用若干外部网络服务请求,尽量将这些请求放在一个Future函数中,而不要对每一个请求建立单独的Future函数。
批处理Apex
当开发者想在代码中处理大批量的数据时,很容易会使数据库查询的次数达到上限。为了避免这种情况,Apex提供了“Database.Batchable”接口,可以让开发者实现批处理Apex类,用于异步执行大批量数据的查询操作。
“Database.Batchable”接口包含了如下函数:
- start():初始化函数,对要处理的数据进行初始化,并将要处理的数据作为返回值,分批传入execute()函数。返回的类型可以是“Database.QueryLocator”或“Iterable”
- execute():从start()函数中分批接收数据,并对每批数据进行处理。数据被处理的顺序并不一定和从start()函数传入的顺序一致
- finish():当execute()函数执行完成后,对数据进行最后的处理
一个标准的批处理类的结构:
global class ExampleBatchClass implements Database.Batchable<sObject> {
global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
// 得到需要处理的数据
}
global void execute(Database.BatchableContext bc, List<P> records){
// 处理数据
}
global void finish(Database.BatchableContext bc){
// 收尾函数
}
}
执行批处理Apex类
使用“Database.executeBatch()”函数可以执行一个批处理Apex类。与此同时,还可以设定一个参数,指定从start()函数传入execute()函数的每批记录的最大数量(如果不设置则为200)。每次执行的时候,会返回一个ID类型的结果,通过这个ID可以在Salesforce中的AsyncApexJob对象中查询当前批处理的执行情况。
示例代码:
// 执行批处理类
ExampleBatchClass exampleBatchClass = new ExampleBatchClass();
// 不设定每批记录的最大数量并执行批处理操作
Id batchId = Database.executeBatch(exampleBatchClass);
// 不设定每批记录的最大数量并执行批处理操作
Id batchIdWithVolumn = Database.executeBatch(exampleBatchClass, 300);
// 查询批处理任务的进度
AsyncApexJob job = [SELECT
Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors
FROM AsyncApexJob
WHERE ID = :batchId];
在批处理Apex类中记录数据的状态
通过实现“Database.Stateful”接口可以在批处理类中记录数据被处理的状态,并可以在类中设定非静态变量,该变量的值在处理每批数据时不会自动刷新。
示例代码:
global class ExampleBatchClass implements Database.Batchable<sObject>, Database.Stateful {
// 定义一个非静态变量,记录execute执行的次数
global Integer executeProcessed = 0;
global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
}
global void execute(Database.BatchableContext bc, List<P> records){
// 每次执行execute()函数,便增加1
executeProcessed += 1;
}
global void finish(Database.BatchableContext bc){
// 在此可以显示总共执行了多少次execute()函数
System.debug(executeProcessed);
}
}
批处理Apex类的单元测试
对批处理Apex类进行单元测试时,与Future函数类似,执行代码要包含在“Test.startTest()”和“Test.stopTest()”函数中。并且,在单元测试中,只能处理一个批次的记录,所以在定义测试数据时,不要让测试数据总数超过200条。
Queueable接口
Queueable接口的作用和Future函数类似,但是实现了Queueable的类可以使用sObject对象作为参数进行操作,这一点Future函数做不到。
Queueable接口中定义了“execute()”函数。它必须是global或者public的。
“System.enqueueJob()”函数可以将实现了Queueable接口的类加入系统的队列,异步执行。
代码结构如下:
public class ExampleClass implements Queueable {
public void execute(QueueableContext context) {
// ...
}
}
示例代码:
首先定义一个Apex类,实现Queueable接口。该类的功能是对于每一个Account对象,设置parentId字段并更新:
public class UpdateParentAccount implements Queueable {
private List<Account> accounts;
private ID parent;
public UpdateParentAccount(List<Account> records, ID id) {
this.accounts = records;
this.parent = id;
}
public void execute(QueueableContext context) {
for (Account account : accounts) {
account.parentId = parent;
}
update accounts;
}
}
然后在代码中调用此类,实现异步操作。假设已经有了一个Account列表和一个ID值:
// 设定参数
UpdateParentAccount updateJob = new UpdateParentAccount(accountList, parentId);
// 执行该操作
ID jobID = System.enqueueJob(updateJob);
通过此代码,即可实现异步将accountList列表中的Account对象的parentId字段更新为参数parentId的值。
实现Queueable的类的单元测试
对实现了Queueable的Apex类进行单元测试时,与Future函数类似,执行代码要包含在“Test.startTest()”和“Test.stopTest()”函数中。比如:
UpdateParentAccount updateJob = new UpdateParentAccount(accountList, parentId);
Test.startTest();
// 执行操作的代码要放在startTest()和stopTest()之间
ID jobID = System.enqueueJob(updateJob);
Test.stopTest();
任务的连续处理
Queueable接口的另一个优点是可以在execute()函数中调用另一个实现了Queueable的类,实现任务的连续处理。比如:
public class FirstJob implements Queueable {
public void execute(QueueableContext context) {
// 连接下一个任务
System.enqueueJob(new SecondJob());
}
}
任务的连续处理最多可以同时处理50个任务,并且在连接任务时,每个execute()函数中只能连接一个任务。
Schedulable接口
当一个类实现了Schedulable接口之后,可以被作为计划任务,在特定的时间执行。
Schedulable接口中定义了“execute()”函数。它必须是global或者public的。
代码结构如下:
global class SchedulableExampleClass implements Schedulable {
global void execute(SchedulableContext ctx) {
// ...
}
}
“System.Schedule()”或“System.scheduleBatch()”函数可以设置实现了Schedulable的类在某个特定时间执行。调用Schedulable类和调用Queueable类的方式类似:
示例代码:定义Apex类的计划执行
String CRON_EXP = '0 0 06 * * ?';
String jobId = System.schedule('Scheduled Job Name', CRON_EXP, new SchedulableExampleClass());
在上述代码中,使用了System.schedule()函数。它包含三个参数:
- 第一个参数是计划任务的名字,可以自己定义,不能与已经存在的计划任务名字重复
- 第二个参数是cron job的表达式,具体可以参考维基百科或crontab。一个基本的解释:表达式包含7个部分,从左到右依次是:秒、分钟、小时、日、月、星期几、年,其中后两个是可选项。“*”表示所有可能的值,比如对于“小时”就是包括了0点到23点。“?”表示任一可能的值,比如对于“星期几”就是每周任意一天。
- 第三个参数是要被计划的实现了Schedulable接口的类
关于cron job的表达式
其格式为:
Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
可以用问号(?)来表示“每一个”。比如上面代码中的“0 0 06 * * ?”就代表了每天6点。“0 0 10 ? * MON-FRI”就代表了周一到周五早上10点。
Apex类计划执行的特点
- 所有计划都要定义一个执行的时间点,比如早上8点。如果在同一时间有多个Apex类被设定为计划执行,则它们其中的一部分的执行有可能有延迟
- Salesforce中可以定义计划执行的Apex类最多是100个
用CronTrigger追踪计划任务
System.schedule()函数返回的值是ID类型,代表了当前计划任务的ID。使用CronTrigger对象可以追踪此任务的情况。
在知道计划任务ID的时候,可以使用以下查询语句:
CronTrigger ct = [SELECT TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :jobId];
如果在实现了Schedulable接口的Apex类中,在execute()函数里需要查询当前计划任务的ID,可以使用SchedulableContext的getTriggerId()函数。
CronTrigger ct = [SELECT TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :sc.getTriggerId()];
关于SchedulableContext的详细信息可以参考官方文档。
用CronJobDetail得到计划任务的详细信息
CronJobDetail对象和CronTrigger对象相关联。可以使用如下查询得到计划任务的详细信息:
CronTrigger job = [SELECT Id, CronJobDetail.Id, CronJobDetail.Name, CronJobDetail.JobType
FROM CronTrigger
ORDER BY CreatedDate DESC
LIMIT 10];
注意这里的CronJobDetail.JobType字段,此字段代表了所有任务的类型,对于Apex计划任务,此字段的值为7。
比如:
SELECT COUNT() FROM CronTrigger WHERE CronJobDetail.JobType = '7'
单元测试Apex计划任务
对实现了Schedulable的Apex类进行单元测试时,与Future函数类似,执行代码要包含在“Test.startTest()”和“Test.stopTest()”函数中。比如:
// 定义计划任务类
global class SchedulableExampleClass implements Schedulable {
global void execute(SchedulableContext sc) {
Account a = [SELECT Id, Name FROM Account WHERE Name = 'TEST ACCOUNT'];
a.Name = 'CHANGED NAME';
update a;
}
}
// 单元测试类
@isTest
class TestScheduleExample {
@isTest
static void testExample() {
// 开始测试
Test.startTest();
// 建立测试数据
Account a = new Account();
a.Name = 'TEST ACCOUNT';
insert a;
// 建立计划任务的执行
String jobId = System.schedule('Scheduled Job', '0 0 0 3 9 ? 2022', new SchedulableExampleClass());
// 得到计划任务的信息
CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
FROM CronTrigger
WHERE Id = :jobId];
// 检查计划任务的Cron表达式是否相同
System.assertEquals('0 0 0 3 9 ? 2022', ct.CronExpression);
// 检查计划任务是否尚未运行
System.assertEquals(0, ct.TimesTriggered);
// 检查计划任务下次运行时间
System.assertEquals('2022-09-03 00:00:00', String.valueOf(ct.NextFireTime));
// 检查数据是否尚未改变
System.assertNotEquals('CHANGED NAME', [SELECT Id, Name FROM Account WHERE Id = :a.Id].Name);
// 完成测试
Test.stopTest();
// 在stopTest()执行之后,已经计划的任务会忽略Cron表达式定义的时间立即执行,并且是同步执行,所以可以保证在执行接下来的语句之前,计划的Apex类的功能已经完成了
// 检查数据是否改变了
System.assertEquals('CHANGED NAME', [SELECT Id, Name FROM Account WHERE Id = :a.Id].Name);
}
}
通过设置界面设置Apex类计划执行
在“设置”界面中,搜索“Apex 类”,可以进入“Apex 类”界面。在此界面中列出了所有系统中存在的Apex类。点击“计划Apex”按钮,即可计划一个实现了Schedulable接口的Apex类,以每周或每月为间隔自动执行。