异步 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类,以每周或每月为间隔自动执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 接着上节 condition_varible ,本节主要介绍future的内容,练习代码地址。本文参考http:/...
    jorion阅读 14,793评论 1 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,106评论 25 707
  • 图片发自简书App 你恐惧的心跳正是我最需要的,你的不安正是我期待的,狰狞的笑在每个转身,即使如此,我不得不一而再...
    miss_suge阅读 200评论 0 1
  • 对于这个问题,地理知识与旅行资讯专家——地理答啦认为,这个问题根本不用回答,你去看看地理答啦头条号就行啦。哈哈。 ...
    地理答啦阅读 1,043评论 0 2