# 处理事件

通过事件订阅功能，应用可以及时接收飞书中资源数据的变化，并根据变化情况做对应的业务处理，详情参见[事件概述](https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM)。在应用内订阅事件时，还需要在本地服务端建立与应用的连接，以便接收事件数据。服务端 SDK 封装了长连接方式，可以快速建立数据通道处理事件；你也可以选择自建 HTTP 服务器处理事件。两种方式介绍如下：

订阅方式 | 介绍
---|---
使用长连接接收事件 | 该方式是飞书 SDK 内提供的能力，你可以通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道（你的服务器需要能够访问公网）。后续当应用订阅的事件发生时，开放平台会通过该通道向你的服务器发送消息。相较于传统的 Webhook 模式，长连接模式大大降低了接入成本，将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下：测试阶段无需使用内网穿透工具，通过长连接模式在本地开发环境中即可接收事件回调。SDK 内封装了鉴权逻辑，只在建连时进行鉴权，后续事件推送均为明文数据，无需再处理解密和验签逻辑。只需保证运行环境具备访问公网能力即可，无需提供公网 IP 或域名。无需部署防火墙和配置白名单。
将事件发送至开发者服务器 | 传统的 Webhook 模式，该方式需要你提供用于接收事件消息的服务器公网地址。后续当应用订阅的事件发生时，开放平台会向服务器的公网地址发送 HTTP POST 请求，请求内包含事件数据。

## （推荐）方式一：使用长连接接收事件

如果事件订阅方式需要选择 **使用长连接接收事件**，则需要先使用 SDK 建立与应用的连接。本章节提供建立长连接的示例代码与代码解析，通过 SDK 建立长连接之后，你才能在应用的事件订阅方式中保存 **使用长连接接收事件** 方式。关于应用内配置事件订阅方式的介绍，参考[配置事件订阅方式](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case)。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/c5a72c50a0d1caf40bdc8718c911b8cd_MHMSb9cxef.png?height=580&lazyload=true&maxWidth=550&width=1224)

### 注意事项

在开始配置之前，你需要确保已了解以下注意事项：
- 目前长连接模式仅支持企业自建应用。
- 与 **将事件发送至开发者服务器** 方式（即自建服务器方式）的要求相同，长连接模式下接收到消息后，需要在 3 秒内处理完成，否则会触发超时重推机制。
- 每个应用最多建立 50 个连接（在配置长连接时，每初始化一个 client 就是一个连接）。
- 长连接模式的消息推送为 **集群模式**，不支持广播，即如果同一应用部署了多个客户端（client），那么只有其中随机一个客户端会收到消息。

### 长连接代码

```java
package com.lark.oapi.sample.ws;
import com.lark.oapi.core.request.EventReq;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.CustomEventHandler;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.service.im.ImService;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import com.lark.oapi.ws.Client;
import java.nio.charset.StandardCharsets;
public class Sample {
          // onP2MessageReceiveV1 为接收消息 v2.0；onCustomizedEvent 内的 message 为接收消息 v1.0。
    private static final EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "")
            .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
                @Override
                public void handle(P2MessageReceiveV1 event) throws Exception {
                    System.out.printf("[ onP2MessageReceiveV1 access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                }
            })
            .onCustomizedEvent("这里填入你要自定义订阅的 event 的 key，例如 out_approval", new CustomEventHandler() {
                @Override
                public void handle(EventReq event) throws Exception {
                    System.out.printf("[ onCustomizedEvent access ], type: message, data: %s\n", new String(event.getBody(), StandardCharsets.UTF_8));
                }
            })
            .build();
    public static void main(String[] args) {
        Client cli = new Client.Builder("YOUR_APP_ID", "YOUR_APP_SECRET")
                .eventHandler(EVENT_HANDLER)
                .build();
        cli.start();
    }
}
```

代码实现说明：
1. 通过 EventDispatcher.*newBuilder*() 初始化事件处理器（*EVENT_HANDLER*），注意**两个参数必须填空字符串**。
1. 通过 *EVENT_HANDLER* 的 onXXXX() 方法处理不同的事件。上述示例中分别监听了「接收消息 v1.0」和「接收消息 v2.0」两个事件。

**说明**：开放平台提供的事件包括 v1.0 和 v2.0 两个版本。两个版本的结构不同。在处理事件时，可通过[事件列表](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list)文档或在开发者后台[添加事件](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/subscription-event-case)时，获取事件的类型与版本信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4dbc1cd97d95f1af949354ce3c350461_4PX46ts1Hs.png?height=380&lazyload=true&maxWidth=650&width=1666)

如果是 v1.0 事件，则注册服务器时需要查找并使用 onP1xxxx 开头的方法；如果是 v2.0 事件，则注册服务器时需要查找并使用 onP2xxxx 开头的方法。
例如，使用 `onP2MessageReceiveV1` 注册接收消息事件回调时，`P2`便对应的是 v2.0 版本事件结构。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/8bbadcadbadcf70bb9cb4c44afea25cd_z7bPrzvx1l.png?height=286&lazyload=true&maxWidth=650&width=1598)

1. 通过 new Client.Builder() 初始化长连接客户端，必填参数为应用的 APP_ID 和 APP_SECRET，需登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** > **应用凭证** 区域获取。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_1BhhLr3htD.png?height=524&lazyload=true&maxWidth=550&width=3594)

1. 可选参数传入 *EVENT_HANDLER*，同时可设置日志级别，可在项目的日志框架（log4j2、logback等）配置中修改。
1. 通过 cli.start() 启动客户端，如连接成功，控制台会打印 "connected to wss://xxxxx"，主线程将阻塞，直到进程结束。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/90d8aab594e3fa8ca22bf4b291723efd_8BitTMiPGo.png?height=176&lazyload=true&maxWidth=550&width=2992)

## 方式二：集成 Servlet 容器，将事件发送至开发者服务器

如果事件订阅方式选择 **将事件发送至开发者服务器**，则需要设置事件请求地址，后续在事件发生时，开放平台会向该地址发送包含事件 JSON 数据的 HTTP POST 请求。为此你需要在项目中启动一个 HTTP 服务，然后把 HTTP 服务的 URL 绑定到应用的事件订阅中，以接收事件。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/2b355c6bb9405407832987a20a3c1c9b_ngBEyaxYW2.png?height=629&lazyload=true&maxWidth=550&width=962)

本章节介绍如何集成基于 Servlet 技术栈实现的 SpringBoot Web 框架。

1. 安装集成包

要实现 SDK 集成已有的 SpringBoot 框架，你需要在项目 pom.xml 文件中引入集成包。相应的 maven 坐标如下：

```xml
    <dependency>
      <artifactId>oapi-sdk-servlet-ext</artifactId>
      <groupId>com.larksuite.oapi</groupId>
      <version>1.0.0-rc3</version>
      <exclusions>
        <exclusion>
          <artifactId>oapi-sdk</artifactId>
          <groupId>com.larksuite.oapi</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    ```

1. 通过代码集成 Servlet 容器。以下提供了集成示例：

1. 注入 ServletAdapter 实例到 IOC 容器。

```java
        import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.context.annotation.Bean;

@SpringBootApplication
        public class AppStartup {

public static void main(String[] args) {
            SpringApplication.run(AppStartup.class, args);
          }

// 注入扩展实例到 IOC 容器
          @Bean
          public ServletAdapter getServletAdapter() {
            return new ServletAdapter();
          }
        }
        ```

1. 编写 Controller 注册事件处理器。

```java
        import com.lark.oapi.core.utils.Jsons;
        import com.lark.oapi.event.EventDispatcher;
        import com.lark.oapi.service.contact.v3.ContactService;
        import com.lark.oapi.service.contact.v3.model.P2UserCreatedV3;
        import com.lark.oapi.service.im.v1.ImService;
        import com.lark.oapi.service.im.v1.model.P1MessageReadV1;
        import com.lark.oapi.service.im.v1.model.P2MessageReadV1;
        import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
        import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RestController;

@RestController
        public class EventController {

//1. 注册消息处理器
          private final EventDispatcher EVENT_DISPATCHER = EventDispatcher.newBuilder("verificationToken",
                  "encryptKey")
              .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
                @Override
                public void handle(P2MessageReceiveV1 event) {
                  System.out.println(Jsons.DEFAULT.toJson(event));
                  System.out.println(event.getRequestId());
                }
              }).onCustomizedEvent("这里填入你要自定义订阅的 event 的 key,例如 out_approval", new CustomEventHandler() {
                        @Override
                        public void handle(EventReq event) {
                            System.out.println("body:" + new String(event.getBody()));
                            System.out.println("plain:" + event.getPlain());
                        }
                    })
              .build();

//2. 注入 ServletAdapter 实例
          @Autowired
          private ServletAdapter servletAdapter;

//3. 创建路由处理器
          @RequestMapping("/webhook/event")
          public void event(HttpServletRequest request, HttpServletResponse response)
              throws Throwable {
            //3.1 回调扩展包提供的事件回调处理器
            servletAdapter.handleEvent(request, response, EVENT_DISPATCHER);
          }
        }
        ```

其中需要注意：EventDispatcher.newBuilder 方法的参数用于签名验证和消息解密。默认可传递空串，但如果你在[开发者后台](https://open.feishu.cn/app)对应的应用中配置了加密信息（Encrypt Key 和 Verification Token），则必须将加密信息的值传递到 EventDispatcher.newBuilder 方法的参数中。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/529c6adc9193d752eac5f87e4cb0b90e_UatK248qhJ.png?height=773&lazyload=true&maxWidth=550&width=1495)

了解更多事件订阅示例，可参见 [GitHub 代码仓库](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/sample/src/main/java/com/lark/oapi/sample/event/EventController.java)。

## （可选）在消息处理器内向指定用户发送消息

如果你是商店应用的开发者，当需要在消息处理器内给对应企业或组织内的用户发送消息时，需要先从消息事件内获取企业或组织 key，然后使用以下方式调用消息 API 进行消息发送。

```java
package com.lark.oapi.sample.event;
import com.lark.oapi.core.request.RequestOptions;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.service.im.v1.ImService.P2MessageReceiveV1Handler;
import com.lark.oapi.service.im.v1.enums.ReceiveIdTypeEnum;
import com.lark.oapi.service.im.v1.model.CreateMessageReq;
import com.lark.oapi.service.im.v1.model.CreateMessageReqBody;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EventController {
  //1. 注册消息处理器
  private final EventDispatcher EVENT_DISPATCHER = EventDispatcher.newBuilder("", "")
      .onP2MessageReceiveV1(new P2MessageReceiveV1Handler() {
        @Override
        public void handle(P2MessageReceiveV1 event) throws Exception {
          // 处理消息
          System.out.println(Jsons.DEFAULT.toJson(event));
          System.out.println(event.getRequestId());
          // 获取企业或组织 key
          String tenantKey = event.getTenantKey();
          // 发送请求
          client.im().message().create(CreateMessageReq.newBuilder()
                  .receiveIdType(ReceiveIdTypeEnum.OPEN_ID)
                  .createMessageReqBody(CreateMessageReqBody.newBuilder()
                      .content("text")
                      .build())
                  .build()
              , RequestOptions.newBuilder()
                  .tenantKey(tenantKey)
                  .build());
        }
      }).build();
  //2. 注入 ServletAdapter 实例
  @Autowired
  private ServletAdapter servletAdapter;
  //3. 创建路由处理器
  @RequestMapping("/webhook/event")
  public void event(HttpServletRequest request, HttpServletResponse response)
      throws Throwable {
    //3.1 回调扩展包提供的事件回调处理器
    servletAdapter.handleEvent(request, response, EVENT_DISPATCHER);
  }
}
```
