已过时,请使用 oapi-sdk-java
Appframework-java 是飞书开放平台应用开发接口Java版本SDK,用来帮助开发者快速搭建飞书app。 SDK中封装了飞书开放台的主要的接口,提供了事件回调框架,让开发着只需要关注自己的业务逻辑,减少在api、协议、安全校验等问题上花费的精力。
<dependency>
<artifactId>appframework-sdk</artifactId>
<groupId>com.larksuite.appframework</groupId>
<version>${version}</version>
</dependency>
每个飞书应用都有一个唯一的appId和对应的appSecret. AppId是一个飞书应用的唯一标识,但是其是开放平台随机生成的一个字符串,不利于开发者用来记忆和区分应用。 所以我们让开发者为自己的app设定一个短名字,后续开发中对app的标识主要使用此名字。
开始开发之前,我们需要先从开放平台开发者后台收集app相关的配置项,生成一个AppConfiguration对象。
AppConfiguration ac = new AppConfiguration();
ac.setAppShortName("myAppName"); // app name, will be used to identify a app, should be unique
ac.setAppId(appId);
ac.setAppSecret(appSecret);
ac.setEncryptKey(encryptKey);
ac.setVerificationToken(verificationToken);
ac.setIsIsv(Boolean.parseBoolean(isIsv));
以下为一种稍简单的方式用来从配置文件读取配置并生成AppConfiguration对象。
AppConfiguration conf = AppConfiguration.loadFromProperties(String appShortName, Properties properties);
配置文件示例如下,配置中需要使用到自己定义的应用名
# larksuite.appframework.${appShortName}.appId
larksuite.appframework.test-app1-isv.appId=cli_xxxxxxxxxx
larksuite.appframework.test-app1-isv.appSecret=xxxxxxxxxxxxxxxx
larksuite.appframework.test-app1-isv.encryptKey=xxxxxxxxxxxx
larksuite.appframework.test-app1-isv.verificationToken=xxxxxxxxxxxx
larksuite.appframework.test-app1-isv.isIsv=true
LarkAppInstance对应的是一个app应用实例,是几乎所有应用操作的入口,可以认为其为整个框架入口。 通常我们使用LarkAppInstanceFactory来构建一个LarkAppInstance,示例代码如下
AppConfiguration appConfiguration = AppConfiguration.loadFromProperties("test-app1-isv", properties); //test-app1-isv is a app short name
LarkAppInstanceFactory.AppEventListener myTestAppListener1 = LarkAppInstanceFactory
.createAppEventCallbackListener()
.onMessageEvent(new App1MessageEventHandler())
.onBotInvitedEvent(new App1AddRobotEventHandler());
LarkAppInstance ins = LarkAppInstanceFactory
.builder(appConfiguration)
.appTicketStorage(new RedisAppTicketStorage())
.registerAppEventCallbackListener(myTestAppListener1)
.create();
如果要开发ISV应用,开发者需要自己提供一个AppTicketStorage接口的实现。App ticket是用来获取应用access token的必要参数,是开放平台主动推送给应用的。 所以开发者需要使用一个持久化(通常Redis或Mysql)的实现,保证接收到的app ticket能被存储。对于内部应用,忽略这个配置项即可。
appframework默认对应的是海外lark环境,如果是飞书,则需要手动设置 .feishu()
来切换到飞书 API 服务器。
如果是企业私有化部署,可以手动设置 .apiBasePath("example.com")
切换到私有化部署的 API 服务器。
BotAppsServerFactory.AppEventListener提供几乎全部飞书事件处理逻辑的注册接口,如果没有提供对应事件的处理逻辑,当应用接收到相应事件时,框架会静默地忽略掉。 构造完AppEventListener后设置到LarkAppInstance中即可。
下面是接收飞书开放平台事件的一个例子。开发框架没有提供http服务的能力,开发者需要将代码运行在一款web server上,可以按java j2ee标准的HttpServlet的接口提供一个url来接收webhook的事件回调。 LarkAppInstance.receiveLarkNotify方法接收原始的报文,完成业务处理后,会同步返回一些数据给开发平台,需要返回的数据是依具体接口而不同。
public class App1EventServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestString = com.google.common.io.CharStreams.toString(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8));
System.out.println("LarkNotifyApp1Servlet requestString: " + requestString);
String respData = Global.botApp1.receiveLarkNotify(requestString);
resp.setStatus(HttpStatus.OK_200);
resp.getWriter().println(respData);
}
}
LarkClient封装了所有主动向开放平台发送请求的方法,LarkAppInstance.getLarkClient可以获得一个全局唯一的client实例,handler中提供的InstanceContext参数也可以通过getLarkClient方法获得该实例。 以下例子展示了向id为"123456"的用户发送文本消息的方式。
Message msg = new TextMessage("Hello");
larkAppInstance.getLarkClient().sendChatMessage(MessageDestinations.UserId("123456"), msg);
目前为止,已支持的回调事件如下
Event type | Event class |
---|---|
app_open | AppEnabledEvent |
approval | ApprovalEvent |
app_status_change | AppStatusChangeEvent |
app_ticket | AppTicketEvent |
add_bot | BotInvitedEvent |
remove_bot | BotRemovedEvent |
user_add | ContactsUpdatesEvent |
leave_approval | LeaveApprovalEvent |
message | MessageEvent |
order_paid | OrderPaidEvent |
work_approval | OvertimeApprovalEvent |
p2p_chat_create | P2pChatCreateEvent |
remedy_approval | RemedyApprovalEvent |
shift_approval | ShiftApprovalEvent |
trip_approval | TripApprovalEvent |
Function name | Remark |
---|---|
app_open | AppOpenEvent |
approval | ApprovalEvent |
app_status_change | AppStatusChangeEvent |
app_ticket | AppTicketEvent |
add_bot | AddBotEvent |
remove_bot | RemovedBotEvent |
user_add | UserAddEvent |
leave_approval | LeaveApprovalEvent |
message | MessageEvent |
order_paid | OrderPaidEvent |
work_approval | workApprovalEvent |
p2p_chat_create | P2pChatCreateEvent |
remedy_approval | RemedyApprovalEvent |
shift_approval | ShiftApprovalEvent |
trip_approval | TripApprovalEvent |
TextMessage
ImageMessage
ShareGroupMessage
PostMessage
Usage example
PostBuilder.Post post = PostBuilder.newPost();
PostBuilder.Language zhCn = post.createZhCnLanguage("I'm the title");
zhCn.createLine()
.createTextTag("Line one", false) // unEscape: false
.creatATag("go to github", "https://github.com");
zhCn.createLine()
.createTextTag("Line two", false)
.creatAtTag("123456") // @Someone useId: 123456
.createImgTag("xxxxxx", 300, 400); // append a image of key: xxxxxx, width:300, height: 400
Message msg = new PostMessage(post.toContent());
CardMessage
卡片消息可能会很复杂,这里展示了一个构建卡片消息的简单例子
Card card = new Card(new Config(true), new Header(new Text(Text.Mode.PLAIN_TEXT, "I am the title"))); // config and header are necessary
Button button = new Button("TestCard.btn", new Text(Text.Mode.PLAIN_TEXT, "submit btn")).setValue(ImmutableMap.of("k1", "v1"));
Action action = new Action(Lists.newArrayList(button));
card.setModules(Lists.newArrayList(
div,
new Hr(),
action
));
Message msg = new CardMessage(card.toObjectForJson());
交互式的卡片元素(按钮、选择列表、日历等)的构造方法第一个参数都是该元素的名字,这类似于html form中的元素name,用来区分提交到服务端的数据的。 当接收到Card交互事件时,可以通过CardActionUtils.getActionMethodName(CardEvent cardEvent)方式获得元素name。
Spring boot是目前主流的Java开发框架,我们提供了SpringBoot风格的插件以便开发者更容易地在SpringBoot框架中开发和集成飞书应用,减少一些重复代码。 开发者直接引入appframework的springboot starter包即可。
<dependency>
<artifactId>appframework-spring-boot-starter</artifactId>
<groupId>com.larksuite.appframework</groupId>
<version>${version}</version>
</dependency>
版本号一般取最新版本号即可。
为了支持单个应用中实现多个app,配置文件只能支持yml格式,以下是一个完整的配置文件例子,一般写在application.yml中。
spring:
application:
name: example
server:
port: 7070
larksuite:
appframework:
feishu: true
# apiBasePath: https://example.com 针对企业私有化部署
notify:
basePath: /notify
apps[0]:
appShortName: app1Name
appId: cli_xxx
appSecret: xxxxxxxxxxxx
encryptKey: xxxxxxxxxxxx
verificationToken: xxxxxxxxxxxx
isIsv: false
apps[1]:
appShortName: app2Name
appId: cli_xxx
appSecret: xxxxxxxxxxxx
encryptKey:
verificationToken:
isIsv: false
appframework默认对应的是海外lark环境,飞书用户通过配置"larksuite.appframework.feishu"来选择环境,设置该配置项为"true",即为国内飞书环境。
如果是企业私有化部署,可以通过设置"larksuite.appframework.apiBasePath"将请求指向私有化部署环境。
larksuite.appframework.notify.basePath配置后会开启一个http servlet,接收以该地址开头的所有请求,但只处理固定格式uri的请求。 如果设置该值为"/notify",如下两个uri的请求会被处理。如果进程内配置了多个app,就会有多组url的请求被监听。
- "/notify/event/${appName}" 回调事件
- "/notify/card/${appName}" 卡片事件
项目启动时,可以在启动日志里找到对应的url信息。找到后加上http(s)和域名,组成完整url后,配置到开发者后台即可(事件订阅和机器人消息卡片)。
@LarkEventHandlers(appName="someAppName")
public class EventHandlers {
@Handler
public Object onRobotAdd(AddBotEvent event, LarkClient larkClient) {
try {
larkClient.sendChatMessage(
MessageDestinations.ChatId(event.getOpenChatId()),
new TextMessage("Hello, I'm Echo Robot, try say to me."));
} catch (LarkClientException e) {
e.printStackTrace();
}
return null;
}
@Handler
public Object onMessageEvent(TextMessageEvent event, InstanceContext ic) {
return null;
}
}
LarkEventHandlers注解可以加在class上,会激活spring包扫描,从此class下找到所有被"Handler" "CardAction"注解的方法,并转换成EventHandler。 LarkEventHandlers如果指定了appName,该class下所有转换而成的EventHandler都会绑定到该app。Handler CardAction都可以单独指定appName,并会覆盖LarkEventHandlers中的appName。 对于单app的应用程序,可以完全忽略这个参数。但是多app时,必须让包扫描程序能为每个Handler/CardAction找到一个存在的appName。
标识了"@Handler"的方法,会根据方法参数列表中的Event类型来响应不同类型的事件。如果需要用到LarkClient/InstanceContext实例,在参数列表中增加其声明即可。
@LarkEventHandlers(appName="someAppName")
public class CardEventHandlers {
@Handler
public Card onCardEvent(CardEvent event) {
return null;
}
@CardAction(methodName = "testCard.btn1")
public Card onActionMethod1(CardEvent cardEvent, LarkClient larkClient) {
return null;
}
@CardAction(methodName = "testCard.btn2")
public Card onActionMethod2(CardEvent cardEvent, LarkClient larkClient) {
return null;
}
}
以上例子提供了两种方式来响应卡片事件。
- @CardAction CardAction注解需要methodName,那么该方法会响应此methodName对应的交互元素的事件。
- @Handler + CardEvent 所有没有被CardAction根据methodName匹配到的事件都会被此方法处理。
Card testCard = new Card(new Config(true), new Header(new Text(Text.Mode.PLAIN_TEXT, "I am the title"))); // config and header are necessary
Action action = new Action(Lists.newArrayList(
new Button("testCard.btn1", new Text(Text.Mode.PLAIN_TEXT, "button-1")),
new Button("testCard.btn2", new Text(Text.Mode.PLAIN_TEXT, "button-2")),
new Button("testCard.btn3", new Text(Text.Mode.PLAIN_TEXT, "button-3"))
));
card.setModules(Lists.newArrayList(action));
Message msg = new CardMessage(card.toObjectForJson());
上述例子中,我们为卡片设置了3个按钮,并想为这3个按钮分别响应其点击事件。在以上CardEventHandlers类中,onActionMethod1方法会处理"testCard.btn1"按钮的点击事件, onActionMethod2方法处理"testCard.btn1"按钮的点击事件,剩下的事件都会由onCardEvent方法处理。
在一些场景中,我们需要主动获取LarkClient实例。回调事件handler的参数列表中提供了LarkClient实例,但对于非回调事件的场景,我们可以用spring context注入的方式获得实例。 例子如下:
@Component
public class MyBusinessService {
@Autowired
private LarkClient larkClient;
@Autowired
private LarkAppInstance larkAppInstance;
// ...
}
对于多app的项目,上面的代码会在启动中出错。原因也很明显,因为自动注入过程存在多个候选者。所以我们需要指定注入的bean的名字。例如,如果我们有两个app:"app1","app2"。 下面是一组例子,bean的规则为"${appName}${ClassName}"。LarkClient的注入我们声明的名字为app1LarkClient和app2LarkClient,LarkAppInstance的注入,声明的名字为app1LarkAppInstance和app2LarkAppInstance。
@Component
public class MyBusinessService {
@Autowired
private LarkClient app1LarkClient; // variable name must be: "${appName}LarkClient"
@Resource(name="app2LarkAppInstance") // "${appName}LarkAppInstance"
private LarkAppInstance app2LarkAppInstance; // arbitrary variable name
// ...
}
appframework-jetty-example
appframework-springboot-example