当前位置: 首页 > article >正文

API设计指南:详解HTTP状态码错误解析、HTTP方法及参数命名规则

目录

    • 1、HTTP API规范
      • 1.1 原则
      • 1.2 协议
      • 1.3 版本
      • 1.4 路径
      • 1.5 HTTP 方法(Method)
      • 1.6 过滤信息
      • 1.7 参数命名
      • 1.8 HTTP 状态码(Response Code)
      • 1.9 鉴权
    • 2、状态码
      • 2.1 API返回基础规范
      • 2.2 常见的 HTTP 状态码
      • 2.3 API错误信息应该放到响应实体中返回
      • 2.4 列举我们遇到较多的错误码
        • 2.4.1 200 OK
        • 2.4.2 201 Created
        • 2.4.3 202 Accepted
        • 2.4.4 204 No Content
        • 2.4.5 3XX 重定向
        • 2.4.6 400 Bad Request
        • 2.4.7 401 Unauthorized
        • 2.4.8 403 Forbidden
        • 2.4.9 404 Not Found
        • 2.4.10 405 Method Not Allowd
        • 2.4.11 406 Not Acceptable
        • 2.4.12 408 Request Timeout
        • 2.4.13 409 Gonfilct
        • 2.4.14 410 Gone
        • 2.4.15 413 Request Entity Too Large
        • 2.4.16 414 Request-URI Too Long
        • 2.4.17 415 Unsupported Media Type
        • 2.4.18 429 Too Many Request
        • 2.4.19 500 Internal Server Error
        • 2.4.20 503 Service Unavailable
    • 3、API的调用方式
      • 3.1 HTTP调用
      • 3.2 Feign调用
      • 3.3 Dubbo调用
      • 3.4 RPC调用

1、HTTP API规范

1.1 原则

友好,容易输入
简单,容易使用
灵活兼容上层UI、客户端

1.2 协议

在通过 API 与后端服务通信的过程中,应该使用 HTTPS 协议。
示例:https://api.niaonao.com/xxx

1.3 版本

针对产品正常迭代升级或需求变更需要改造升级接口时,需要保证升级新版本接口后,旧版本的接口仍然可用,直至调用旧版接口的所有服务全部下线,监控旧版接口为零请求量时,排期下线旧版接口。
解决方案:在URL中嵌入版本编号,版本号从 v1 开始。
版本不是强要求,根据实际产品进行规划,不会对接口频繁的迭代升级就不需要引入版本号来区分不同版本的接口。
例:https://api.niaonao.com/v1/xxx

1.4 路径

路径(Path)就是指向特定资源或资源集合的URL。
例如:path = /{版本}/{具体的业务功能}/{表名or实体}/{行为}

在路径的设计中,需要遵守以下约定:

  • URL存在多个单词的方法名称时,使用驼峰式命名
  • URL中资源的命名应该是名词
  • URL是易读的
  • URL不要暴漏服务器架构
  • URL推荐使用动词,如getXXX、findXXX、deleteXXX。对外开放的服务,因很多浏览器和框架不支持PUT、DELETE,所以不推荐使用PUT、DELETE请求方式,推荐POST和GET。加动词见名知意

1.5 HTTP 方法(Method)

对于资源的具体操作类型,有下面五个方法

HTTP方法对应SQL命令方法说明
GETSELECT从服务器查询资源
POSTCREATE在服务器新建资源
PUTUPDATE在服务器更新资源,客户端提供改变后的完整资源
PATCHUPDATE在服务器更新资源,客户端提供改变的属性,服务端根据属性转换为改变后的资源
DELETEDELETE从服务器删除资源

注:对外开放服务不要使用REST的PUT和DELETE,因为很多浏览器不支持,很多框架也不支持。内部服务可以使用PUT和DELETE。

1.6 过滤信息

如果记录数量较多,服务器不能一次性返回所有记录给用户。API应该提供过滤参数,过滤返回结果。
常见的分页参数是pageNum、pageSize,从第几页读取指定条数数据。nextFrom,size从第几条数据开始读取几条数据。

  • ?nextFrom=10&limit=5:指定从第10条记录开始返回5条记录
  • ?offset=10:指定返回记录的开始位置。
  • ?pageNum=2&pageSize=100:指定第2页,读取100个记录。
  • ?sortBy=id&order=asc:指定返回结果按照id属性排序,升序排序。
  • ?id=1:指定筛选条件,id为1的记录

1.7 参数命名

参数单词应该使用小写,多个单词拼接的方法使用驼峰式命名。包括请求体body和请求头header里面的自定义参数。

1.8 HTTP 状态码(Response Code)

接口状态码和业务状态码应该互相独立。服务端正确处理后返回http code 200/201,返回的json数据统一增加状态描述。说明业务处理成功还是失败。

{
    "logId":123456,//提供日志追踪ID
    "code":1,      //1:成功,其他为失败
    "message":"success", //失败或成功描述
    "data" :{      //接口正确处理时的信息
     }
}

业务code 按业务功能提供统一错误编码枚举。
不要直接将异常抛给客户端处理,一般需要一个统一的异常处理类,并且以统一格式将异常信息返回前端。

1.9 鉴权

API的身份认证应该使用OAuth 2.0框架。服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
使用 OAuth2.0 的方式为 API 调用者提供登录认证。应该先通过登录接口获取Access Token后再通过token令牌调用需要身份认证的API。

Oauth 的路径设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时应该在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少秒后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,应该在请求头 Authorization 中带上 access_token。
Authorization: Bearer token…
当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端应该返回 invalid_token 的错误或 401 错误码。

httpHTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
    "error": "invalid_token"
}

2、状态码

2.1 API返回基础规范

所有的API响应应该遵守HTTP设计规范,应该选择合适的HTTP状态码。不可以所有的接口都返回状态码为200的HTTP响应。
响应示例

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com
{
"code": 0,
"msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com
{
    "code": -1,
    "msg": "该活动不存在",
}

2.2 常见的 HTTP 状态码

状态码补充说明
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误

只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端必须认定该请求已处理成功。

2.3 API错误信息应该放到响应实体中返回

当 API 发生错误时,应该返回出错时的详细信息。目前常见返回错误信息的方法有两种:“将错误详细放入 HTTP 响应首部”和“将错误详细放入响应实体中”。
考虑到易读性和客户端的易处理性,我们应该把错误信息直接放到响应实体中,并且错误格式应该满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}
  • 将错误详细放入 HTTP 响应首部
httpX-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication
  • 将错误详细放入响应实体中
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 10:02:59 GMT
Connection: keep-alive
 
{"error_code":40100,"message":"Unauthorized"}

其中错误码error_code应该和 HTTP 状态码对应,也方便错误码归类,例如:

  • 429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
  • 403 Forbidden
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 10:19:27 GMT
Connection: keep-alive
 
{"error_code":403002,"message":"用户已禁用"}

另外,应该在返回错误信息中,同时包含面向开发者和面向用户的提示信息。面向开发者的信息方便开发人员调试对接;面向用户的提示信息,可以直接展示给终端用户查看。

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

2.4 列举我们遇到较多的错误码

2.4.1 200 OK

200 状态码是最常见的 HTTP 状态码,在所有成功的 GET 请求中,应该返回此状态码。HTTP 响应实体部分应该直接就是数据,不要做多余的包装。
返回示例:
获取单个用户

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

获取集合

[{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
},{
    "id": 2,
    "username": "foo",
    "age": 88,
}]
2.4.2 201 Created

当服务器创建数据成功时,应该返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

可以返回201状态码。

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2024 09:12:20 GMT
Connection: keep-alive

客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。响应体可为空

2.4.3 202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;
返回该状态码时,响应实体应该为空。

HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2024 09:25:15 GMT
Connection: keep-alive
2.4.4 204 No Content

该状态码表示响应实体不包含任何数据,其中:
在使用 DELETE 方法删除资源成功时,必须返回该状态码
使用 PUT、PATCH 方法更新数据成功时,也应该返回此状态码

HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2024 09:29:12 GMT
Connection: keep-alive
2.4.5 3XX 重定向

所有API不应该返回3XX类型的状态码。因为返回格式包含html。API不要返回纯 HTML 结构的响应;若一定要使用重定向功能,应该返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段。

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 09:41:50 GMT
Location: https://example.com
Connection: keep-alive
 
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=https://example.com" />
 
<title>Redirecting to https://example.com</title>
</head>
<body>
Redirecting to <a href="https://example.com">https://example.com</a>.
</body>
</html>

加了重定向路径Location属性的空响应体3XX响应

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2024 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive
2.4.6 400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器应该放弃该请求。
当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都必须返回该状态码。

HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 13:22:36 GMT
Connection: keep-alive
 
{"error_code":40000,"message":"无效的签名"}
2.4.7 401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都必须返回该状态码。
未认证用户访问需要认证的 API
access_token 无效/过期
客户端在收到 401 响应后,都应该提示用户进行下一步的登录操作。

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 13:17:02 GMT
Connection: keep-alive
 
{"message":"Token Signature could not be verified.","error_code": "40100"}
2.4.8 403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。
如当普通用户请求操作管理员用户时,必须返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 13:05:34 GMT
Connection: keep-alive
 
{"error_code":40301,"message":"权限不足"}
2.4.9 404 Not Found

该状态码表示用户请求的资源不存在,如
获取不存在的用户信息
访问不存在的路径
都必须返回该状态码,若该资源已永久不存在,则应该返回 410 响应。

2.4.10 405 Method Not Allowd

当客户端使用的 HTTP 请求方法不被服务器允许时,必须返回该状态码。
如客户端调用了 POST 方法来访问只支持 GET 方法的 API
该响应必须返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 12:30:57 GMT
Connection: keep-alive
 
{"message":"405 Method Not Allowed","error_code": 40500}
2.4.11 406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSON 和 XML 输出的 API 被指定返回 YAML 格式的数据时。
Http 协议一般通过请求首部的 Accept 来指定数据格式

2.4.12 408 Request Timeout

客户端请求超时时必须返回该状态码,需要注意的时,该状态码表示客户端请求超时,在涉及第三方 API 调用超时时,一定不可返回该状态码。

2.4.13 409 Gonfilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 12:19:04 GMT
Connection: keep-alive
 
{"error_code":40900,"message":"手机号已存在"}
2.4.14 410 Gone

和 404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端应该停止再次请求该资源。

2.4.15 413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。
此种情况下,服务器可以关闭连接以免客户端继续发送此请求。
如果这个状况是临时的,服务器应该返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

2.4.16 414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

2.4.17 415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都应该返回该状态码。
该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时应该返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 12:09:40 GMT
Connection: keep-alive
 
{"error_code":41500,"message":"不允许上传的图片格式"}
2.4.18 429 Too Many Request

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都应该返回该状态码。并且也应该在响应首部中加上下列头部:
X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 11:19:32 GMT
Connection: keep-alive
 
{"message":"You have exceeded your rate limit.","error_code":42900}

必须为所有的 API 设置 Rate Limit 支持。

2.4.19 500 Internal Server Error

该状态码必须在服务器出错时抛出,对于所有的 500 错误,都应该提供完整的错误信息支持,也方便跟踪调试。

2.4.20 503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都应该返回该状态码,其中若是主动关闭 API 服务,应该在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2024 10:56:20 GMT
Retry-After: 60
Connection: keep-alive
 
{"error_code":50300,"message":"服务维护中"}

3、API的调用方式

HTTP:适用于广泛的 Web 应用和服务间通信,尤其适合跨语言和跨平台交互。它具有良好的互操作性和安全性。
RPC:提供高效的远程调用机制,适用于高性能要求的内部服务间通信。它通常需要特定的协议和序列化格式。
Feign:简化了 RESTful API 的调用,特别适合微服务架构中的服务间调用,能够显著减少开发工作量。
Dubbo:专注于企业级分布式系统的高性能和高可用性,提供了丰富的服务治理功能,适用于大规模分布式环境。

3.1 HTTP调用

(1)HTTP说明

  • 协议基础:HTTP 是一种应用层协议,用于客户端和服务器之间的通信。它基于请求/响应模型,支持多种方法如 GET、POST 等。
  • 无状态性:HTTP 是无状态的,每个请求都是独立的,服务器不会保留任何上下文信息。
  • 广泛支持:几乎所有编程语言都支持 HTTP 协议,且有丰富的库和框架可以使用。
  • 灵活性:HTTP 可以传输多种形式的数据,如 JSON、XML、HTML 等,并且可以通过 RESTful API 实现资源导向的设计。
  • 安全性:通过 HTTPS 提供加密通信,确保数据安全。

(2)示例

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class HttpExample {
    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("https://api.example.com/data")
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response);

            System.out.println(response.body().string());
        }
    }
}

3.2 Feign调用

(1)Feign说明

  • 声明式 Web 服务客户端:Feign 是一个声明式的 HTTP 客户端,简化了与 RESTful 服务的交互。它- 基于注解的方式,使得编写 HTTP 请求变得非常直观。
  • 集成方便:Feign 很容易与其他框架集成,如 Spring Cloud,提供了自动配置和支持。
  • 简洁易用:不需要手动构造 HTTP 请求和解析响应,只需要定义接口即可

(2)示例

@FeignClient(name = "example-service", url = "http://api.example.com")
public interface ExampleServiceClient {

    @GetMapping("/data")
    String getData();
}

// 使用 Feign 客户端
@Autowired
private ExampleServiceClient exampleServiceClient;

public void fetchData() {
    String data = exampleServiceClient.getData();
    System.out.println(data);
}

3.3 Dubbo调用

(1)Dubbo说明

  • 分布式服务框架:Dubbo 是阿里巴巴开源的一个高性能的 Java RPC 框架,旨在提供高可用、高性能的服务治理能力。
  • 服务发现与注册:支持多种注册中心(如 ZooKeeper, Nacos),使得服务可以动态地发现和注册。
  • 负载均衡:内置了多种负载均衡策略,如随机、轮询等,保证服务调用的可靠性。
  • 监控和管理:提供了详细的监控和管理功能,便于运维人员了解系统运行状态。

(2)示例

// 接口定义
public interface HelloService {
    String sayHello(String name);
}

// 消费者配置
<dubbo:reference id="helloService" interface="com.example.HelloService"/>

// 使用 Dubbo 服务
@Resource
private HelloService helloService;

public void callService() {
    String result = helloService.sayHello("World");
    System.out.println(result);
}

3.4 RPC调用

(1)远程过程调用说明

  • 透明性:RPC 的目标是让远程方法调用像本地方法调用一样简单,开发者无需关心底层网络细节。
  • 高效性:相比 HTTP,RPC 通常更高效,因为它专注于特定的二进制协议,减少了不必要的开销。
  • 强类型:大多数 RPC 框架支持强类型的接口定义,有助于编译时检查错误。
  • 依赖于序列化:RPC 需要序列化和反序列化参数和返回值,因此对性能有一定影响。

(2)示例

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class RpcExample {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        
        // 创建存根并进行远程调用
        // ...
        
        channel.shutdown();
    }
}

Powered by niaonao


http://www.kler.cn/a/427473.html

相关文章:

  • Uniapp的vue、nvue、uvue后缀名区别
  • CSS 实现视差滚动:详解 background-attachment 与 transform:translate3D 及应用
  • matlab Delaunay三角剖分提取平面点云的边界
  • 【随笔笔记】将mysql数据迁移到群晖NAS
  • 阿拉丁论文助手:一键点亮学术之路
  • 仿真键盘输入遇到Edge环境不识别 回车符如何处理
  • PHP使用RabbitMQ(正常连接与开启SSL验证后的连接)
  • 零基础学鸿蒙开发--第九篇--网络请求
  • lvgl9 消息框控件Message(lv_message)使用指南
  • macOS 15.1.1 (24B2091) 系统中快捷键符号及其代表的按键的对照表
  • 学习23种设计模式
  • 刷算法心得
  • 人工智能在云计算中的运维优化:智能化的新时代
  • 本文介绍麒麟信安服务器系统(kylinsec)的安装。
  • R语言使用“纽约市数据集中的优步皮卡”数据创建不同年度时间范围的可视化
  • https/http访问接口工具类,附带ssl忽略证书验证,以及head头部的添加-java版
  • [241206] X-CMD 发布 v0.4.15:env 升级,mirror 支持华为/腾讯 npm 镜像,pb-wayland 剪贴板
  • Python编程语言学习书籍和课程建议
  • Java Serializable 序列化
  • 【Copilot 】TAB keybinding not working on JetBrains Client