-
Notifications
You must be signed in to change notification settings - Fork 27
2.云引擎 1 云函数
云函数是云引擎({{productName}})的一个子模块,请确保在阅读本文档之前已经阅读了 云引擎服务概览。
当你开发移动端应用时,可能会有下列需求:
- 应用在多平台客户端(Android、iOS、Windows Phone、浏览器等)中很多逻辑都是一样的,希望将这部分逻辑抽取出来只维护一份。
- 有些逻辑希望能够较灵活的调整(比如某些个性化列表的排序规则),但又不希望频繁的更新和发布移动客户端。
- 有些逻辑需要的数据量很大,或者运算成本高(比如某些统计汇总需求),不希望在移动客户端进行运算,因为这样会消耗大量的网络流量和手机运算能力。
- 当应用执行特定操作时,由云端系统自动触发一段逻辑(称为 Hook 函数),例如用户注册后对该用户增加一些信息记录用于统计;或某业务数据发生变化后希望做一些别的业务操作。
- 客户端提供能够越过 ACL 或表权限的限制,对数据进行操作。
- 需要定时任务,比如每天凌晨清理垃圾注册账号等。
这时,你可以使用云引擎的云函数,云函数是一段部署在服务端的代码,编写 JavaScript、Python、PHP 或 Java 代码,并部署到我们的平台上,可以很好的完成上述需求。
如果还不知道如何创建云引擎项目、本地调试并部署到云端,请阅读 云引擎快速入门。
云引擎支持多种语言的运行环境,你可以选择自己熟悉的语言开发应用:
云引擎应用有「生产环境」和「预备环境」之分,在客户端的切换方法为:
AVCloud.setProductionMode(false); // 调用预备环境
免费版云引擎 应用只有「生产环境」,因此请不要切换到预备环境。
示例项目 中 "$PROJECT_DIR/src/main/java/cn/leancloud/demo/todo/Cloud.java" 文件定义了一个很简单的 hello
云函数。现在让我们看一个较复杂的例子来展示云引擎的用途。在云端进行计算的一个重要理由是,你不需要将大量的数据发送到设备上做计算,而是将这些计算放到服务端,并返回结果这一点点信息就好。
例如,你写了一个应用,让用户对电影评分,一个评分对象大概是这样:
{
"movie": "夏洛特烦恼",
"stars": 5,
"comment": "夏洛一梦,笑成麻花"
}
stars
表示评分,1-5。如果你想查找《夏洛特烦恼》这部电影的平均分,你可以找出这部电影的所有评分,并在设备上根据这个查询结果计算平均分。但是这样一来,尽管你只是需要平均分这样一个数字,却不得不耗费大量的带宽来传输所有的评分。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。
Cloud 函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。整个 Storage-Core SDK 都在云引擎运行环境上有效,可以直接使用,所以我们可以使用它来查询所有的评分。结合在一起,实现 averageStars
函数的代码如下:
@EngineFunction("averageStars")
public static float getAverageStars(@EngineFunctionParam("movie") String movie)
throws AVException {
AVQuery<AVObject> query = new AVQuery("Review");
query.whereEqualTo("movie", movie);
List<AVObject> reviews = query.find();
int sum = 0;
if (reviews == null && reviews.isEmpty()) {
return 0;
}
for (AVObject review : reviews) {
sum += review.getInt("star");
}
return sum / reviews.size();
}
云函数的信息是通过 @EngineFunctionParam 来指定传入参数的名字和对应的类型的. AVUser.getCurrentUser() 则可以获取与每个请求关联(根据客户端发送的 LC-Session 头)的用户信息 EngineRequestContext 则可以获取额外的一些 metaData 信息
LeanCloud 各个语言版本的 SDK 都提供了调用云函数的接口。
// AVCloud 提供了一系列的静态方法来实现客户端调用云函数
// 构建参数
Map<String, String> dicParameters = new HashMap<String, String>();
dicParameters.put("movie", "夏洛特烦恼");
// 调用云函数 averageStars
AVCloud.callFunctionInBackground("averageStars", dicParameters)
.subscribe(ObserverBuilder.buildSingleObserver(new FunctionCallback() {
public void done(Object object, AVException e) {
if (e == null) {
// 处理返回结果
} else {
// 处理报错
}
}
}));
请查看我们的 云引擎 REST API 使用指南。
在云引擎中可以使用 AVCloud.callFunction 调用 @EngineFunction 定义的云函数:
Map<String, Object> params = new HashMap<String, Object>();
params.put("movie", "夏洛特烦恼");
try {
float result = AVCloud.callFunction("averageStars", params);
} catch (AVException e) {
e.printStackTrace();
}
RPC 调用云函数是指:云引擎会在这种调用方式下自动为 Http Response Body 做序列化,而 SDK 调用之后拿回的返回结果就是一个完整的 AVObject
。
// 构建参数
Map<String, String> dicParameters = new HashMap<String, String>();
dicParameters.put("movie", "夏洛特烦恼");
AVCloud.rpcFunctionInBackground("averageStars", dicParameters,
new FunctionCallback<AVObject>() {
@Override
public void done(AVObject object, AVException e) {
Assert.assertNull(e);
}
});
错误响应码允许自定义。可以在云函数中间 throw AVException 来指定 code 和 error 消息,如果是普通的 Exception,code 值则是默认的1 。
@EngineFunction("me")
public static AVUser getCurrentUser() throws Exception {
AVUser u = AVUser.getCurrentUser();
if (u == null) {
throw new AVException(211, "Could not find user");
} else {
return u;
}
}
客户端收到的响应:{"code":211,"error":"Could not find user"}
@EngineFunction()
public static void customErrorCode() throws Exception {
throw new AVException(123,"custom error message");
}
客户端收到的响应: {"code":123,"error":"自定义错误信息"}
Hook 函数本质上是云函数,但它有固定的名称,定义之后会由系统在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时自动触发,而不是由开发者来控制其触发时机。
需要注意:
- 使用 Hook 函数需要 防止死循环调用。
-
_Installation
表暂不支持 Hook 函数。 - Hook 函数只对当前应用的 Class 生效,对绑定后的目标 Class 无效。
对于 before 类的 Hook(也包括 onLogin
),如果返回了一个错误的话,这个操作就会被中断,你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作;对于 after 类的 Hook(也包括 onVerified
),返回错误并不会影响操作的执行(因为其实操作已经执行完了)。
为了认证 Hook 调用者的身份,我们的 SDK 内部会确认请求确实是从云引擎内网的云存储组件发出的,如果认证失败,可能会出现 Hook key check failed
的提示,如果在本地调试时出现这样的错误,请确保是通过命令行工具启动调试的。
在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符:
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static AVObject reviewBeforeSaveHook(AVObject review) throws Exception {
if (AVUtils.isBlankString(review.getString("comment"))) {
throw new Exception("No Comment");
} else if (review.getString("comment").length() > 140) {
review.put("comment", review.getString("comment").substring(0, 137) + "...");
}
return review;
}
在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数:
@EngineHook(className = "Review", type = EngineHookType.afterSave)
public static void reviewAfterSaveHook(AVObject review) throws Exception {
AVObject post = review.getAVObject("post");
post.fetch();
post.increment("comments");
post.save();
}
再如,在用户注册成功之后,给用户增加一个新的属性 from 并保存:
@EngineHook(className = "_User", type = EngineHookType.afterSave)
public static void userAfterSaveHook(AVUser user) throws Exception {
user.put("from", "LeanCloud");
user.save();
}
在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改:
@EngineHook(className = "Review", type = EngineHookType.beforeUpdate)
public static AVObject reviewBeforeUpdateHook(AVObject review) throws Exception {
List<String> updateKeys = EngineRequestContext.getUpdateKeys();
for (String key : updateKeys) {
if ("comment".equals(key) && review.getString("comment").length()>140) {
throw new Exception("comment 长度不得超过 140 字符");
}
}
return review;
}
注意:传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。
在更新已存在的对象后执行特定的动作,比如每次修改文章后记录下日志:
@EngineHook(className = "Article", type = EngineHookType.afterUpdate)
public static void articleAfterUpdateHook(AVObject article) throws Exception {
LogUtil.avlog.d("updated article,the id is:" + article.getObjectId());
}
在删除一个对象之前做一些检查工作,比如在删除一个相册 Album 前,先检查一下该相册中还有没有照片 Photo:
@EngineHook(className = "Album", type = EngineHookType.beforeDelete)
public static AVObject albumBeforeDeleteHook(AVObject album) throws Exception {
AVQuery query = new AVQuery("Photo");
query.whereEqualTo("album", album);
int count = query.count();
if (count > 0) {
throw new Exception("无法删除非空相簿");
} else {
return album;
}
}
在被删一个对象后执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片:
@EngineHook(className = "Album", type = EngineHookType.afterDelete)
public static void albumAfterDeleteHook(AVObject album) throws Exception {
AVQuery query = new AVQuery("Photo");
query.whereEqualTo("album", album);
List<AVObject> result = query.find();
if (result != null && !result.isEmpty()) {
AVObject.deleteAll(result);
}
}
当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如:
@EngineHook(className = "_User", type = EngineHookType.onVerified)
public static void userOnVerifiedHook(AVUser user) throws Exception {
LogUtil.avlog.d("onVerified: sms,user:" + user.getObjectId());
}
函数的第一个参数是验证类型。短信验证为 sms
,邮箱验证为 email
。另外,数据库中相关的验证字段,如 emailVerified
不需要修改,系统会自动更新。
在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录:
@EngineHook(className = "_User", type = EngineHookType.onVerified)
public static AVUser userOnLoginHook(AVUser user) throws Exception {
if ("noLogin".equals(user.getUsername())) {
throw new Exception("Forbidden");
} else {
return user;
}
}
请阅读 实时通信概览 · 云引擎 Hook 来了解以下函数的相关参数和用法。
在消息达到服务器、群组成员已解析完成、发送给收件人之前触发。例如,提前过滤掉聊天内容中的一些广告类的关键词:
@IMHook(type = IMHookType.messageReceived)
public static Map<String, Object> onMessageReceived(Map<String, Object> params) {
// 打印整个 Hook 函数的参数
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
// 获取消息内容
String content = (String)params.get("content");
// 转化成 Map 格式
Map<String,Object> contentMap = (Map<String,Object>)JSON.parse(content);
// 读取文本内容
String text = (String)(contentMap.get("_lctext").toString());
// 过滤广告内容
String processedContent = text.replace("XX中介", "**");
// 将过滤之后的内容发还给服务端
result.put("content",processedContent);
return result;
}
在消息发送完成时触发、对话中某些用户却已经下线,此时可以根据发送的消息来生成离线消息推送的标题等等。例如截取所发送消息的前 6 个字符作为推送的标题:
@IMHook(type = IMHookType.receiversOffline)
public static Map<String, Object> onReceiversOffline(Map<String, Object> params) {
String alert = (String)params.get("content");
if(alert.length() > 6){
alert = alert.substring(0, 6);
}
System.out.println(alert);
Map<String, Object> result = new HashMap<String, Object>();
JSONObject object = new JSONObject();
object.put("badge", "Increment");
object.put("sound", "default");
object.put("_profile", "dev");
object.put("alert", alert);
result.put("pushMessage", object.toString());
return result;
}
消息发送完成之后触发,例如消息发送完后,在云引擎中打印一下日志:
@IMHook(type = IMHookType.messageSent)
public static Map<String, Object> onMessageSent(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
// ...
return result;
}
创建对话,在签名校验(如果开启)之后、实际创建之前触发。例如对话创建时,在云引擎中打印一下日志:
@IMHook(type = IMHookType.conversationStart)
public static Map<String, Object> onConversationStart(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
// 如果创建者是 black 可以拒绝创建对话
if ("black".equals(params.get("initBy"))) {
result.put("reject", true);
// 这个数字是由开发者自定义的
result.put("code", 9890);
}
return result;
}
创建对话完成触发。例如对话创建之后,在云引擎打印一下日志:
@IMHook(type = IMHookType.conversationStarted)
public static Map<String, Object> onConversationStarted(Map<String, Object> params) throws Exception {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
String convId = (String)params.get("convId");
System.out.println(convId);
return result;
}
向对话添加成员,在签名校验(如果开启)之后、实际加入之前,包括主动加入和被其他用户加入两种情况都会触发,**注意在创建对话时传入了其他用户的 Client Id 作为 Member 参数,不会触发 {{hook_conversation_add}} **。例如在云引擎中打印成员加入时的日志:
@IMHook(type = IMHookType.conversationAdd)
public static Map<String, Object> onConversationAdd(Map<String, Object> params) {
System.out.println(params);
String[] members = (String[])params.get("members");
Map<String, Object> result = new HashMap<String, Object>();
System.out.println("members");
System.out.println(members);
// 以下代码表示此次操作的发起人如果是 black 就拒绝此次操作,members 不会被加入到当前对话中
if ("black".equals(params.get("initBy"))) {
result.put("reject", true);
// 这个数字是由开发者自定义
result.put("code", 9890);
}
return result;
}
从对话中踢出成员,在签名校验(如果开启)之后、实际踢出之前触发,用户自己退出对话不会触发。例如在踢出某一个成员时,在云引擎日志中打印出该成员的 Client Id:
@IMHook(type = IMHookType.conversationRemove)
public static Map<String, Object> onConversationRemove(Map<String, Object> params) {
System.out.println(params);
String[] members = (String[])params.get("members");
Map<String, Object> result = new HashMap<String, Object>();
System.out.println("members");
// 以下代码表示此次操作的发起人如果是 black 就拒绝此次操作,members 不会被删除
if ("black".equals(params.get("initBy"))) {
result.put("reject", true);
// 这个数字是由开发者自定义
result.put("code", 9892);
}
return result;
}
修改对话属性、设置或取消对话消息提醒,在实际修改之前触发。例如在更新发生时,在云引擎日志中打印出对话的名称:
@IMHook(type = IMHookType.conversationUpdate)
public static Map<String, Object> onConversationUpdate(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
Map<String,Object> attr = (Map<String,Object>)params.get("attr");
System.out.println(attr);
//Map<String,Object> attrMap = (Map<String,Object>)JSON.parse(attr);
String name = (String)attr.get("name");
//System.out.println(attrMap);
System.out.println(name);
// 以下代码表示此次操作的发起人如果是 black 就拒绝此次操作,对话的属性不会被修改
if ("black".equals(params.get("initBy"))) {
result.put("reject", true);
result.put("code", 9893);
}
return result;
}
为 beforeSave
这类的 hook 函数定义错误码,需要这样:
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static AVObject reviewBeforeSaveHook(AVObject review) throws Exception {
throw new AVException(123,"自定义错误信息");
}
客户端收到的响应为:Cloud Code validation failed. Error detail : {"code":123, "message": "自定义错误信息"}
,然后通过截取字符串的方式取出错误信息,再转换成需要的对象。
Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为 save 对象而触发 beforeSave
和 afterSave
),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。
例如,如果一个 beforeSave
函数是被一个已经运行了 13 秒的云函数触发,那么 beforeSave
函数就只剩下 2 秒的时间来运行。同时请参考 云函数超时及处理方案。
{% block online_editor %}{% endblock %}
定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是秒,正常情况下时间误差都可以控制在秒级别。
定时任务是普通的云函数,也会遇到 超时问题,具体请参考 超时处理方案。
一个定时器如果在 24 小时内收到了超过 30 次的 400 (Bad Request) 或 502 (Bad Gateway) 的应答,它将会被云引擎禁用,同时系统会向开发者发出相关的禁用通知邮件。在控制台的日志中,对应的错误信息为
{{ errorMessage_shortCircuited }}
。
部署云引擎之后,进入 控制台 > 存储 > 云引擎 > 定时任务,点击 创建定时器,然后设定执行的函数名称、执行环境等等。
定时器创建后,其状态为未运行,需要点击 启用 来激活。之后其执行日志可以在 日志 页面中查看。
定时任务分为两类:
- 使用 Cron 表达式安排调度
- 以秒为单位的简单循环调度
以 Cron 表达式为例,比如每周一早上 8 点准时发送推送消息给用户:
创建定时器的时候,选择 Cron 表达式 并填入 0 0 8 ? * MON
。
Cron 表达式的基本语法为:
<秒> <分钟> <小时> <日期 day-of-month> <月份> <星期 day-of-week> <年>
位置 | 字段 | 约束 | 取值 | 可使用的特殊字符 |
---|---|---|---|---|
1 | 秒 | 必须 | 0-59 | , - * / |
2 | 分钟 | 必须 | 0-59 | , - * / |
3 | 小时 | 必须 | 0-23(0 为午夜) | , - * / |
4 | 日期 | 必须 | 1-31 | , - * ? / L W |
5 | 月份 | 必须 | 1-12、JAN-DEC | , - * / |
6 | 星期 | 必须 | 1-7、SUN-SAT | , - ? / L # |
7 | 年 | 可选 | 空、1970-2099 | , - * / |
特殊字符的用法:
字符 | 含义 | 用法 |
---|---|---|
* |
所有值 | 代表一个字段的所有可能取值。如将 <分钟> 设为 *,表示每一分钟。 |
? |
不指定值 | 用于可以使用该字符的两个字段中的一个,在一个表达式中只能出现一次。如任务执行时间为每月 10 号,星期几无所谓,那么表达式中 <日期> 设为 10,<星期> 设为 ?。 |
- |
范围 | 如 <小时> 为 10-12,即10 点、11 点、12 点。 |
, |
分隔多个值 | 如 <星期> 为 MON,WED,FRI,即周一、周三、周五。 |
/ |
增量 | 如 <秒> 设为 0/15,即从 0 秒开始,以 15 秒为增量,包括 0、15、30、45 秒;5/15 即 5、20、35、50 秒。*/ 与 0/ 等效,如 <日期> 设为 1/3,即从每个月的第一天开始,每 3 天(即每隔 2 天)执行一次任务。 |
L |
最后 | 其含义随字段的不同而不同。 <日期> 中使用 L 代表每月最后一天,如 1 月 31 号、2 月 28 日(非闰年);<星期> 中单独使用 L,则与使用 7 或 SAT 等效,若前面搭配其他值使用,如 6L,则表示每月的最后一个星期五。注意,在 L 之前不要使用多个值或范围,如 1,2L、1-2L,否则会产生错误结果。 |
W |
weekday | 周一到周五的任意一天,离指定日期最近的非周末的那一天。<日期> 为 15W 即离 15 号最近的非周末的一天;如果 15 号是周六,任务则会在 14 号周五触发,如果 15 号是周日,则在 16 号周一触发,如果 15 号是周二,则周二当天触发。<日期> 为 1W,如果 1 号是周六,任务则会在 3 号周一触发,因为不能向前跨月来计算天数。在 <日期> 中 W 之前只能使用一个数值,不能使用多个值或范围。LW 可在 <日期> 中组合使用,表示每月最后一个非周末的一天。 |
# |
第 N 次 | 如 <星期> 为 6#3 代表每月第三个周五,2#1 为每月头一个周一,4#5 为每月第五个周三;如果当月没有第五周,则 #5 不会产生作用。 |
各字段以空格或空白隔开。JAN-DEC、SUN-SAT 这些值不区分大小写,比如 MON 和 mon 效果一样。更详细的使用方法请参考 {{quartz_docs_link}} 。
举例如下:
表达式 | 说明 |
---|---|
0 0/5 * * * ? |
每隔 5 分钟执行一次 |
10 0/5 * * * ? |
每隔 5 分钟执行一次,每次执行都在分钟开始的 10 秒,例如 10:00:10、10:05:10 等等。 |
0 30 10-13 ? * WED,FRI |
每周三和每周五的 10:30、11:30、12:30、13:30 执行。 |
0 0/30 8-9 5,20 * ? |
每个月的 5 号和 20 号的 8 点和 10 点之间每隔 30 分钟执行一次,也就是 8:00、8:30、9:00 和 9:30。 |
生产环境和预备环境的定时器数量都限制在 5 个以内,也就是说你总共最多可以创建 10 个定时器。
定时器执行后的日志会记录在 控制台 > 存储 > 云引擎 > 其它 > 日志
中,以下为常见的错误信息及原因。
-
timerAction timed-out and no fallback available
某个定时器触发的云函数,因 15 秒内没有响应而超时(可参考 对云函数调用超时的处理)。 -
{{ errorMessage_shortCircuited }}
某个定时器触发的云函数,因为太多次超时而停止触发。
因为云引擎运行在可信的服务器端环境中,所以你可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也允许调用一些仅供 masterKey 使用的 API。开启 Master Key 的方法如下:
// 通常位于 src/**/AppInitListener.java
JavaRequestSignImplementation.instance().setUseMasterKey(true);
如果没有添加这些代码,默认是没有超级权限的,这意味着在云引擎中你也不能修改被 ACL 保护的数据,你需要在进行操作时手动指定 sessionToken,让操作以这个用户的权限来执行:
那么究竟是否应该使用超级权限呢,我们的建议如下:
- 如果你的云引擎代码中特权操作比较多、操作不属于用户的全局数据比较多,那么建议全局开启 Master Key,并自行做好对于用户请求的权限检查。
- 如果你的云引擎代码中的请求通常和单个用户自己的数据相关、需要遵守 ACL,那么建议不开启 Master Key,将用户请求的 sessionToken 传入数据修改的相关操作(如
save
)。 {% endif %}
关于云引擎上的权限问题,还可以参考 ACL 与数据安全 和 在云引擎中使用 ACL。