# 调用服务端 API

本文档介绍如何通过 Java SDK，自行构建 API Client、构造 API 请求、最终成功调用服务端 API。你可前往[ API 调试台](https://open.feishu.cn/api-explorer?from=op_doc)，直接获取指定服务端 API 相关示例代码，然后参考本文档了解调用 API 的全面流程。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/0758be526cbabc9012632268de8b0cc3_iudhkSRU44.png?height=764&lazyload=true&maxWidth=750&width=1889)

## 步骤一：构建 API Client

通过 SDK 调用飞书开放接口之前，你需要先在代码中创建一个 API Client。该 API Client 支持指定当前使用的应用信息、日志级别、HTTP 请求超时时间等基本信息。以下为支持的配置项及其具体含义。

```java
Client client=Client.newBuilder("appId","appSecret") // 默认配置为自建应用
    .marketplaceApp() // 设置应用类型为商店应用
    .openBaseUrl(BaseUrlEnum.FeiShu) // 设置域名，默认为飞书
    .helpDeskCredential("helpDeskId","helpDeskSecret") // 服务台应用才需要设置
    .requestTimeout(3,TimeUnit.SECONDS) // 设置httpclient 超时时间，默认永不超时
    .logReqAtDebug(true) // 在 debug 模式下会打印 http 请求和响应的 headers、body 等信息。
    .build();
```

配置选项 | 配置方式 | 是否必填 | 描述
---|---|---|---
app_id 和 app_secret | <code>Client.newBuilder("appId","appSecret")</code> | 是 | 应用凭证 App ID 和 App Secret。可在[开发者后台](https://open.feishu.cn/app) > 应用详情页 > **凭证与基础信息** > **应用凭证** 区域获取。![图片名称](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_YeHS0mGtI7.png?height=524&lazyload=true&width=3594)
appType | <code>client.marketplaceApp()</code> | 否 | 设置 App 类型为商店应用。如果你是 ISV 开发者，则必须设置该选项。关于商店应用的开发指南，可参见 [ISV（商店应用）开发指南-oapi-sdk-java](https://bytedance.feishu.cn/docx/G4lndQqsgoenFhxcPlIc0Klinte)。
logReqAtDebug | <code>client.logReqAtDebug(boolean logReqAtDebug)</code> | 否 | 设置是否开启 HTTP 请求参数和响应参数的日志打印开关。开启后，在 debug 模式下会打印 HTTP 请求和响应的 headers、body 等信息。在排查问题时开启该选项，有利于问题的排查。
BaseUrl | <code>client.openBaseUrl(BaseUrlEnum baseUrl)</code> | 否 | 设置飞书域名，默认为 FeishuBaseUrl。可用域名如下：<br>```java<br>public enum BaseUrlEnum {<br>FeiShu("https://open.feishu.cn"),<br>LarkSuite("https://open.larksuite.com"),<br>;<br>}<br>```
tokenCache | <code>client.tokenCache(ICache cache)</code> | 否 | 设置 Token 缓存器，用于缓存 Token 和 appTIcket，默认实现为内存。<br>```java<br>public interface ICache {<br>// 获取缓存值<br>String get(String key);<br>// 设置缓存值<br>void set(String key, String value, int expire, TimeUnit timeUnit);<br>}<br>```<br>对于商店应用的开发者而言，如果需要 SDK 来缓存 appTicket，则需要实现该接口，以提供分布式缓存。
disableTokenCache | <code>client.disableTokenCache()</code> | 否 | 设置是否开启 TenantAccessToken （[应用访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)）的自动获取与缓存。<br>若配置该选项，表示关闭自动获取与缓存 TenantAccessToken；若不配置则为开启。
helpDeskId、helpDeskToken | <code>client.helpDeskCredential(String helpDeskId, String helpDeskToken)</code> | 否 | 服务台的 ID 和 token。仅在调用服务台业务的 API 时需要配置。可在[服务台管理后台](https://feishu.cn/helpdesk/admin)**设置中心** > **API 凭证** 处获取，详情参见 [服务台接入指南](https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN)。<br>**注意**：服务台的 ID、Token 只有服务台创建者可以查看到。![图片名称](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/dcc3b0ac14729354c2bc4b44af26c4f9_mXmcHyDfTy.png?height=693&lazyload=true&width=1916)
requestTimeout | <code>client.requestTimeout(long timeout, TimeUnit timeUnit)</code> | 否 | 设置 SDK 内置的 Http Client 的请求超时时间。默认为 0 表示永不超时。
httpTransport | <code>client.httpTransport(IHttpTransport httpTransport)</code> | 否 | 设置传输层实现，用于替换 SDK 提供的默认实现。你可通过实现下面的 IHttpTransport 接口来设置自定义的传输实现：<br>```java<br>public interface IHttpTransport {<br>RawResponse execute(RawRequest request) throws Exception;<br>}<br>```<br>目前提供了以下两种实现：<br>- 基于 [OKhttp](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/larksuite-oapi/src/main/java/com/lark/oapi/core/httpclient/OkHttpTransport.java) 的实现，使用方式参见[示例代码](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/sample/src/main/java/com/lark/oapi/sample/api/ClientSample.java)。<br>- 基于 [Apache HttpClient](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/larksuite-oapi/src/main/java/com/lark/oapi/core/httpclient/ApacheHttpClientTransport.java) 的实现，使用方式参见[示例代码](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/sample/src/main/java/com/lark/oapi/sample/api/ClientSample.java)。

示例配置：

- 对于自建应用，使用以下代码创建 API Client。

```java
     Client client=Client.newBuilder("appId","appSecret").build();  // 默认配置为自建应用
     ```

- 对于商店应用，需在创建 API Client 时，使用 `marketplaceApp()` 方法指定 AppType 为商店应用，代码配置如下。了解更多可参考 [ISV（商店应用）开发指南-oapi-sdk-java](https://bytedance.feishu.cn/docx/G4lndQqsgoenFhxcPlIc0Klinte)。

```java
  Client client = Client.newBuilder("appId", "appSecret")
      .marketplaceApp() // 设置应用为商店应用
      .build();
  ```

## 步骤二：构造 API 请求

在项目内创建好一个 API Client 后，即可开始调用[飞书开放平台接口](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/server-api-list)。SDK 使用 **client.** **业务域.版本.资源** **.方法名称** 来定位具体的 API 方法。如下图示例，你可前往[ API 调试台](https://open.feishu.cn/api-explorer?from=op_doc)，选择指定 API，在示例代码处直接获取 API 方法。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/a47afd75dae54efd8db446bd507a5f10_ESdzhEww0g.png?height=755&lazyload=true&maxWidth=650&width=1877)

如下代码示例，你可通过 client 调用文档资源的 create 方法，创建一个文档。

该示例需要你在开发者后台为应用开通[创建及编辑新版文档]或[创建新版文档]权限，否则接口将报 99991672 错误码。
```java
import com.lark.oapi.Client;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.service.docx.v1.model.CreateDocumentReq;
import com.lark.oapi.service.docx.v1.model.CreateDocumentReqBody;
import com.lark.oapi.service.docx.v1.model.CreateDocumentResp;
public class DocxSample {

public static void main(String arg[]) throws Exception {
    // 构建client
    Client client = Client.newBuilder("appId", "appSecret").build();
    // 发起请求
    CreateDocumentResp resp = client.docx().document()
        .create(CreateDocumentReq.newBuilder()
            .createDocumentReqBody(CreateDocumentReqBody.newBuilder()
                .title("title")   // 文档标题
                .folderToken("")   // 文件夹 token，传空表示在根目录创建文档
                .build())
            .build()
        );
    // 处理服务端错误
    if (!resp.success()) {
      System.out.println(String.format("code:%s,msg:%s,reqId:%s"
          , resp.getCode(), resp.getMsg(), resp.getRequestId()));
      return;
    }
    // 业务数据处理
    System.out.println(Jsons.DEFAULT.toJson(resp.getData()));
  }
}
```
其他 API 调用示例请参考 GitHub 代码仓库中的 [Im Java 示例](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/sample/src/main/java/com/lark/oapi/sample/api/ImSample.java)。

## （可选）步骤三：设置请求选项

在每次发起 API 调用时，你可以设置请求级别的相关参数，例如传递 userAccessToken（用户访问凭证）、自定义 headers 等。所有请求级别可设置的选项如下表所示。

配置选项 | 配置方式 | 描述
---|---|---
headers | <code>requestOptions.headers(Map&lt;String, List&lt;String&gt;&gt; headers)</code> | 设置自定义请求头。在发起请求时，这些请求头会被透传到飞书开放平台服务端。
userAccessToken | <code>requestOptions.userAccessToken(String userAccessToken)</code> | 设置用户 token，当你需要以用户身份发起 API 调用时，需要设置该选项的值。
tenantAccessToken | <code>requestOptions.tenantAccessToken(String tenantAccessToken)</code> | 设置企业或组织 token，当你自己维护企业或组织 token 时（即创建 client 时 EnableTokenCache 设置为 false），需通过该选项传递企业或组织 token。
tenantKey | <code>requestOptions.tenantKey(tenantKey string)</code> | 设置企业或组织 key, 当你开发商店应用时，必须设置该选项。
requestId | <code>requestOptions.requestId(requestId string)</code> | 设置请求 ID，作为请求的唯一标识。该 ID 会被透传到飞书开放平台服务端。

设置自定义请求头的示例代码如下所示。

```java
import com.lark.oapi.Client;
import com.lark.oapi.core.request.RequestOptions;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.core.utils.Lists;
import com.lark.oapi.service.docx.v1.model.CreateDocumentReq;
import com.lark.oapi.service.docx.v1.model.CreateDocumentReqBody;
import com.lark.oapi.service.docx.v1.model.CreateDocumentResp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DocxSample {
  public static void main(String arg[]) throws Exception {
    // 创建 API Client。你需在此传入你的应用的实际 App ID 和 App Secret
    Client client = Client.newBuilder("appId", "appSecret").build();
    // 设置自定义请求头
    Map<String, List<String>> headers = new HashMap<>();
    headers.put("key1", Lists.newArrayList("value1"));
    headers.put("key2", Lists.newArrayList("value2"));
    // 发起请求
    CreateDocumentResp resp = client.docx().document()
        .create(CreateDocumentReq.newBuilder()
                .createDocumentReqBody(CreateDocumentReqBody.newBuilder()
                    .title("title")   // 文档标题
                    .folderToken("")  // 文件夹 token，传空表示在根目录创建文档
                    .build())
                .build()
            , RequestOptions.newBuilder()
                .userAccessToken("u-2GxFH7ysh8E9lj9UJp8XAG0k0gh1h5KzM800khEw2G6e") // 传递用户token
                .headers(headers) // 传递自定义请求头
                .build());
    // 处理服务端错误
    if (!resp.success()) {
      System.out.println(String.format("code:%s,msg:%s,reqId:%s"
          , resp.getCode(), resp.getMsg(), resp.getRequestId()));
      return;
    }
    // 业务数据处理
    System.out.println(Jsons.DEFAULT.toJson(resp.getData()));
  }
}
```

## 步骤四：运行代码

完成以上步骤后，即可运行代码调用创建文档 API。若请求成功，预计将返回以下数据。若失败，将返回错误码、错误信息和 Log ID，你可前往开发文档搜索解决方案。

```json
{
  Document: {
    DocumentId: "IPI4dqnbfoPxL3xhAEhcjXabcef",
    RevisionId: 1,
    Title: "title"
  }
}
```

## 常见问题

### 如何调用历史版本 API ？

服务端 API 中存在部分历史版本的开放接口，由于没有元数据信息，所以不能使用 SDK 内封装好的方法快速调用，此时你可以使用 SDK 提供的原生模式调用 API。以[发送消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create)接口为例，调用示例如下所示：

```java
package com.lark.oapi.sample.rawapi;
import com.lark.oapi.Client;
import com.lark.oapi.core.enums.AppType;
import com.lark.oapi.core.response.RawResponse;
import com.lark.oapi.core.token.AccessTokenType;
import com.lark.oapi.core.utils.Jsons;
import java.util.HashMap;
import java.util.Map;
/**
 * 原生http 调用方式
 */
public class RawApiCall {
  public static void main(String arg[]) throws Exception {
    // 构建client
    Client client = Client.newBuilder("appId", "appSecret").build();
    // 构建http body
    Map<String, Object> body = new HashMap<>();
    body.put("receive_id", "ou_c245b0a7dff2725cfa2fb104f8b48b9d");
    body.put("content", MessageText.newBuilder()
        .atUser("ou_155184d1e73cbfb8973e5a9e698e74f2", "Tom")
        .text("test content")
        .build());
    body.put("msg_type", MsgTypeEnum.MSG_TYPE_TEXT);
    // 发起请求
    RawResponse resp = client.post(
        "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id"
        , body
        , AccessTokenType.Tenant);
    // 处理结果
    System.out.println(resp.getStatusCode());
    System.out.println(Jsons.DEFAULT.toJson(resp.getHeaders()));
    System.out.println(new String(resp.getBody()));
    System.out.println(resp.getRequestID());
  }
}
```
了解更多 API 调用示例，参考 GitHub 代码仓库中的 [RawApiCall Java 示例](https://github.com/larksuite/oapi-sdk-java/blob/v2_main/sample/src/main/java/com/lark/oapi/sample/rawapi/RawApiCall.java)。

### 如何快速获取接口对应的示例代码？

飞书开放平台提供了 [API 调试台](https://open.feishu.cn/api-explorer)，通过该平台可以快速调试服务端 API，快速获取资源 ID 及生成多语言示例代码的能力，为您节省开发成本。例如，通过 API 调试台调用 [发送消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create) 接口，在调试台成功完成测试后，可通过 **示例代码** 页面查阅 Java SDK 对应的接口调用代码。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/ca14b04adb4859f7971208d4f0128f08_aSSX48s0zA.png?height=768&lazyload=true&maxWidth=766&width=1266)

### 如何准确选择 API？

使用 API Client 调用 API 时，对应的方法建议你借助 [API 调试台](https://open.feishu.cn/api-explorer/)获取，可通过指定接口的地址栏参数拼接方法，也可以直接参考接口提供的示例代码。以[通过手机号或邮箱获取用户 ID](https://open.feishu.cn/api-explorer/cli_a61e4f821889d00c?apiName=batch_get_id&from=op_doc_tab&project=contact&resource=user&version=v3) 接口为例，获取方式如下图所示。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/5b42cd293c1e26079d8ec616349f25b1_aU0FkMhXG3.png?height=1684&lazyload=true&maxWidth=766&width=2882)

### 开放接口的 HTTP GET 请求需要携带请求体 Body 参数，如何传参？

由于默认的客户端实现（OkHttp3Client）不支持这种方式，因此你需要切换成 ApacheHttpClient。参考以下代码：

```java
Client.newBuilder(appId, appSecret)
      .httpTransport(
          ApacheHttpClientTransport.newBuilder().httpclient(HttpClients.createDefault()).build()
      )
```

### 接口超时并报错 ClientTimeoutException，如何解决？

该报错是因为构建 API Client 时未配置超时时间引起的，你需要在 Client 内配置超时时间，参考如下代码配置：

```java
@Test
void init() {
    Client client = Client.newBuilder("appId", "appSecret")
        .httpTransport(new OkHttpTransport(
            new OkHttpClient().newBuilder()
                .readTimeout(3, TimeUnit.MINUTES)  // 设置超时时间，单位必须为分钟
                .callTimeout(3, TimeUnit.MINUTES)  // 设置超时时间，单位必须为分钟
                .build()
        ))
        .tokenCache(LocalCache.getInstance())      // 默认实现，本地带时间过期的缓存；可以自己实现ICache的接口，例如Redis缓存等
        .logReqAtDebug(true)                       // 在 debug 模式下会打印 http 请求和响应的 headers,body 等信息。 
        .build();
}
```

### 示例代码运行后，Client 正常发起请求并返回响应结果，但程序仍然一直运行了一段时间才自动停止是什么原因？

在使用 OkHttp 作为 HTTP 客户端库时，OkHttp 会在内部维护一个连接池（Connection Pool），用于复用已经建立的 HTTP 连接，以提高性能。连接池中的连接有 5 分钟的存活时间（TTL），进程可能不会立即结束，而是会保持活跃一段时间，直到所有连接的 TTL 到期或被手动关闭。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/9cd8264533cd83f153590c6f3046cfd4_I0Cskcoa9i.png?height=1488&lazyload=true&maxWidth=766&width=1794)

如果希望进程立即结束，可以通过设置 `Connection: close` 请求头来禁用 OkHttp 的连接复用能力，但该方式会导致网络性能下降，请谨慎操作。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4ba4c401008e159139380944260c574c_tmoEIPSilp.png?height=456&lazyload=true&maxWidth=766&width=1456)

### MessageCardElement 缺少 tag 为 table 的实现类。
**回答：**

Java SDK 中的 `MessageCardElement` 类已停止维护。建议使用 **飞书卡片搭建工具** 设计卡片，并导出 **JSON 模板**，直接通过 JSON 字符串或 Map 结构构建卡片内容。

### com.lark.oapi.core.exception.IllegalAccessTokenTypeException: null 错误
**回答：**

先核查接口是否仅支持 `UserAccessToken`；若为仅支持的类型，需进一步检查调用接口时是否已传入该 Token，未传入则补充传入即可。

### 本地启动长连接后，没有打印 "connected to wss://xxxxx"信息，也没有其他报错。
**回答：**

检查 `logback.xml` 配置文件，确认是否关闭了 `oapi` 包的日志打印

### 上传审批文件示例代码报错，或不知道如何构造请求？
**参考文档**：[上传审批文件](https://open.larkoffice.com/document/server-docs/approval-v4/file/upload-files)

**正确的示例代码**：
```java
FormData formData = new FormData();
formData.addField("name", file.getName());
formData.addField("type", "attachment");
FormDataFile formDataFile = new FormDataFile();
formDataFile.setFieldName("content");
formDataFile.setFile(file);
formData.addFile("content", formDataFile);
```

### ERROR com.lark.oapi.ws.Listener [Listener.java:83] - java.io.EOFException错误
**回答**：

该错误通常由网络连接中断或服务端主动断开连接导致。SDK 内部已实现自动重连机制，偶发该错误可忽略；如频繁出现，请检查服务器网络稳定性。

### 批量请求接口超时、client time out、SocketTimeoutException: timeout、java.io.IOException: Canceled：
- **批量接口超时示例**：

![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/fba9bb1dfb6fd1166e6d9edf4bea673d_gNXtjTQjdO.png?height=273&lazyload=true&width=1280)
- **Client time out示例**：
```
com.lark.oapi.core.exception.ClientTimeoutException: client time out
at com.lark.oapi.core.Transport.send(Transport.java:189)
at com.lark.oapi.service.contact.v3.resource.User.batchGetId(User.java:162)
```
- **SocketTimeoutException: timeout 示例：**

![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/e8d113469298cb0dda6f1771502d49c8_Qv70VDJKVb.png?height=423&lazyload=true&width=1280)
**原因：**

OkHttp 中 `CallTimeout` **未手动设置时理论上无超时限制**，但实际请求的connect（连接）、read（读取）、write（写入） 各阶段均有默认超时时间（均为 10s）；服务端接口调用的超时问题，一般均发生在 `readtimeout`（读取）阶段，可通过以下方式调整各阶段超时配置：

```java
import com.lark.oapi.Client;
import com.lark.oapi.core.cache.LocalCache;
import com.lark.oapi.core.httpclient.OkHttpTransport;
import com.lark.oapi.okhttp.OkHttpClient;    
import java.net.Proxy;
import java.util.concurrent.TimeUnit;

@Test
void init() {
    Client client = Client.newBuilder("appId", "appSecret")
        .httpTransport(
            new OkHttpTransport(
                new OkHttpClient().newBuilder()
                    .readTimeout(3, TimeUnit.MINUTES) // 等待服务端返回响应数据阶段的超时设置
                    .callTimeout(3, TimeUnit.MINUTES) // 从请求发起到响应完全接收的全生命周期的超时时间设置
                    .build()
        ))
        .tokenCache(LocalCache.getInstance())     // 默认实现，本地带时间过期的缓存；可以自己实现ICache的接口，例如Redis缓存等
        .logReqAtDebug(true)                      // 在 debug 模式下会打印 http 请求和响应的 headers,body 等信息。
        .build();
}
```

### 下载素材接口 Java 示例代码运行报错：java.lang.IllegalArgumentException: The result returned by the server is illegal  
**参考文档**：[下载素材](https://open.larkoffice.com/document/server-docs/docs/drive-v1/media/download)

**示例代码：**
```java
public static void main(String arg[]) throws Exception {
    Client client = Client.newBuilder("xxx", "xxx").build();

DownloadMediaReq req = DownloadMediaReq.newBuilder()
            .fileToken("xxx")
            .extra("%7B%22bitablePerm%22%3A%7B%22tableId%22%3A%22tblQ7MgglSH9q3NE%22%2C%22rev%22%3A5%7D%7D")
            .build();

DownloadMediaResp resp = client.drive().v1().media().download(req);

if(!resp.success()) {
        System.out.println(String.format("code:%s,msg:%s,reqId:%s, resp:%s",
                resp.getCode(), resp.getMsg(), resp.getRequestId(), Jsons.createGSON(true, false).toJson(JsonParser.parseString(new String(resp.getRawResponse().getBody(), StandardCharsets.UTF_8)))));
        return;
    }

// 业务数据处理
    resp.writeFile("c:/filepath/filename");
}
```
**原因：**

用户传入的 `extra` 为已编码的参数，但 Java SDK 内部会对 `extra` 字段值自动执行一次编码操作，导致该参数被重复编码，最终引发参数传入异常。

**解决方式：**

传入 `extra` 参数时，请直接使用编码前的原始参数，示例传入方式如下：
```java
DownloadMediaReq req = DownloadMediaReq.newBuilder()
        .fileToken("xxxx")
        .extra("{\"bitablePerm\":{\"tableId\":\"xxxx\",\"rev\":5}}")
        .build();
```
