java與es8實戰之三:Java API Client有關的知識點串講

2023-08-28 15:14:38

歡迎存取我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本篇是《java與es8實戰》系列的第三篇,將一些重要的知識點在這裡梳理清楚,為後面的實踐奠定基礎
  • 一共有七個與Java API Client有關的重要知識點
  1. 關於namespace:每個feature都有自己的package
  2. 命名規則:介紹Java API Client中物件的方法的命名規則
  3. 集合不為空:Java API Client中物件返回的集合,到底要不要做判空?
  4. variant type:繁多的場景和物件,可以通過variant type進行簡化
  5. 通過JSON字串建立API物件:通過builder建立複雜的物件,會導致程式碼也很複雜,這裡提供了一種更簡單的方式
  6. 關於異常:有哪些異常型別,各自會在什麼場景丟擲
  • 接下來逐個去看

名稱空間

  • 在REST API檔案中,數量眾多API是按照特性(feature)來分組的,如下圖

  • 在ES的Java庫Java API Client中,上圖中的各種feature被稱為namespace

  • 在ES的Java庫Java API Client中,與REST API對應的的類和介面都在統一的包名co.elastic.clients.elasticsearch之下,然後再通過下一級package進行分類,這個分類與上圖的feature相對應,例如索引相關的,在REST API中的feature是Index APIs,那麼在Java API Client中,完整的package就是co.elastic.clients.elasticsearch.indices,這裡面有索引操作所需的請求、響應、服務等各種類,如下圖

  • 每一個namespace(也就是REST API中的feature),都有自己的client,例如索引相關的操作都有索引專用的client類負責,範例程式碼如下,client.indices()返回的是ElasticsearchIndicesClient物件,這是索引操作專用的範例

ElasticsearchClient client = ...
client.indices().create(c -> c.index("products"));
  • 展開上述程式碼的indices()方法,看看其內部實現,如下所示,每次呼叫indices方法,都會建立一個ElasticsearchIndicesClient物件,對於其他namespace,例如ingest、license亦是如此,都會建立新的範例
  • 看到這裡,經驗豐富的您應該發現了問題:在大量並行頻繁執行各種namespace操作時,會建立大量client物件,這樣會影響系統效能嗎?
  • es預判了咱們的預判,如下圖,官方說這是輕量級物件(very lightweight),所以,理論上可以放心建立,不必擔心其對系統造成的壓力
  • 儘管每個namespace都有自己的client,但也有例外,就是searchdocument,它們的程式碼不在search或者document這樣的package下面,而是在core下面,而且可以通過ElasticsearchClient來操作,如下圖

命名規則

  • Java API Client是個庫,也是個java工程,工程裡有自己的內部設計,這算是Java API Client自己的框架部分(framework),另一部分就是專門為使用者提供的大量API
  • 對於API部分,方法的命名規則都是駝峰式(camelCaseNaming),例如查詢請求ElasticsearchClient.search()、查詢結果的最高評分SearchResponse.maxScore()
  • 對於framework部分,方法命令是下劃線作為字首,例如獲取查詢型別Query._kind()

五種物件

  • 官方將Java API Client中的物件分為五種
  1. Object mapper:序列化和反序列化工具,這類物件是執行緒安全、無狀態的,通常是單例模式存在於應用中,常在啟動時建立
  2. Transport:傳輸工具,此類物件執行緒安全,藉助底層HTTP使用者端工具維護著網路資源,例如負責與es伺服器端建立連線,在需要關閉連線的時候負責釋放所有底層網路資源
  3. Clients:實際處理每個namespace的使用者端類,例如負責索引的是ElasticsearchIndicesClient,它們的特點:不可變物件、無狀態、執行緒安全、輕量級(類似於普通bean的資源開銷),之所以輕量級,是因為其結構實際上就是對一些API endpoint的包裝
  4. Builders:這個在《開篇》中已經詳細說明了,就不多贅述,用過builder的您應該會發現,builder當然是可變類,至於是否執行緒安全似乎和builder沒什麼關係,因為每建立一次範例時,都要建立一個builder範例,而且,一旦執行完build方法後,這個builder範例就沒用了
  5. Requests & other API objects:和請求相關的物件,都是不可變的、執行緒安全的

集合不會為空

  • 對於單值屬性,我們在使用的時候判斷是否為空是個常規操作,這樣是為了避免直接使用時可能出現的空指標異常
  • 而對於集合,Java API Client 已經確保了API返回的集合非空,我們只需要檢查集合中是否有內容,而不必擔心集合自身是否等於null的問題,官方給出的演示程式碼如下
NodeStatistics stats = NodeStatistics.of(b -> b
    .total(1)
    .failed(0)
    .successful(1)
);

// The `failures` list was not provided.
// - it's not null
assertNotNull(stats.failures());
  • 出於好奇,去看看NodeStatistics原始碼,構造方法如下,failures來自ApiTypeHelper.unmodifiable
	private NodeStatistics(Builder builder) {

		this.failures = ApiTypeHelper.unmodifiable(builder.failures);
		this.total = ApiTypeHelper.requireNonNull(builder.total, this, "total");
		this.successful = ApiTypeHelper.requireNonNull(builder.successful, this, "successful");
		this.failed = ApiTypeHelper.requireNonNull(builder.failed, this, "failed");

	}
  • 再去看ApiTypeHelper.unmodifiable,如下,已確保了failures不為空
    public static <T> List<T> unmodifiable(@Nullable List<T> list) {
        if (list == null) {
            return undefinedList();
        }
        if (list == UNDEFINED_LIST) {
            return list;
        }
        return Collections.unmodifiableList(list);
    }
  • 因此,再使用API返回的集合時,集合物件自身始終非空

variant type

  • variant type是Java API Client中常見的物件型別,這個該如何翻譯呢,個人覺得是不確定型別的意思,不專業,期待您的指正
  • 舉個例子,查詢是最常見的操作了,下面列舉三種查詢,第一個是普通的不分詞查詢
{
  "query":{"term":{ "interests":"youyong"}}
}
  • 分詞查詢
{
  "query":{"match":{"interests": "changge"}}
}
  • 以及複雜的全文字查詢
{
  "query": {
    "intervals" : {
      "my_text" : {
        "all_of" : {
          "ordered" : false,
          "intervals" : [
            {
              "match" : {
                "query" : "my favorite books",
                "max_gaps" : 0,
                "ordered" : true
              }
            },
            {
              "any_of" : {
                "intervals" : [
                  { "match" : { "query" : "java tutorials" } },
                  { "match" : { "query" : "cold porridge" } }
                ]
              }
            }
          ]
        }
      }
    }
  }
}
  • 對查詢來說,在Java API Client中,有個Query物件代表了查詢行為,這就是個典型的variant type,至於對應的真實query是哪種,可以在builder時指定,例如下面指定了型別是term
Query query = new Query.Builder()
    .term(t -> t                          
        .field("name")                    
        .value(v -> v.stringValue("foo"))
    )
    .build();  
  • 上述query有對應的方法返回其值,例如上面的value可以這樣獲取
query.term().value().stringValue()
  • 如果在設定的時候,並非用stringValue方法,而是其他型別,那麼上面的程式碼在獲取String型別的值時會丟擲IllegalStateException異常

  • variant type配有對應的isXXX方法返回其是否屬於某個型別,例如Query就有query.isTerm()表示自己是不是term查詢

  • 還可以用_kind()返回當前型別,下面是範例

switch(query._kind()) { 
    case Term:
        doSomething(query.term());
        break;
    case Intervals:
        doSomething(query.intervals());
        break;
    default:
        doSomething(query._kind(), query._get()); 
}
  • 可見有了variant type,在 queries, aggregations, field mappings, analyzers等多種場景下,我們不需要使用各種具體的類,只要用最抽象的variant type,再設定builder pattern即可,這對服務提供者和服務消費者都是有效的簡化

通過JSON字串建立API物件

  • 下面是kibana頁面上,用JSON建立索引的操作截圖
  • 如果要在程式碼中實現上述效果,該如何做呢?一層一層的建立mapping、proterties、field物件?那可真是麻煩...
  • 在Java API Client中,可以通過json字串反序列化為API物件,首先,將上述JSON放入名為some-index.json的檔案中,然後執行以下程式碼,即可用json檔案建立req物件
InputStream input = this.getClass()
    .getResourceAsStream("some-index.json"); 

CreateIndexRequest req = CreateIndexRequest.of(b -> b
    .index("some-index")
    .withJson(input) 
);

boolean created = client.indices().create(req).acknowledged();
  • 再來段更復雜的,一個API物件,既通過JSON反序列化生成,同時又能呼叫物件的方法設定一些屬性
Reader queryJson = new StringReader(
    "{" +
    "  \"query\": {" +
    "    \"range\": {" +
    "      \"@timestamp\": {" +
    "        \"gt\": \"now-1w\"" +
    "      }" +
    "    }" +
    "  }," +
    "  \"size\": 100" + 
    "}");

Reader aggregationJson = new StringReader(
    "{" +
    "  \"size\": 0, " + 
    "  \"aggregations\": {" +
    "    \"hours\": {" +
    "      \"date_histogram\": {" +
    "        \"field\": \"@timestamp\"," +
    "        \"interval\": \"hour\"" +
    "      }," +
    "      \"aggregations\": {" +
    "        \"max-cpu\": {" +
    "          \"max\": {" +
    "            \"field\": \"host.cpu.usage\"" +
    "          }" +
    "        }" +
    "      }" +
    "    }" +
    "  }" +
    "}");

SearchRequest aggRequest = SearchRequest.of(b -> b
    .withJson(queryJson) 
    .withJson(aggregationJson) 
    .ignoreUnavailable(true) 
);

Map<String, Aggregate> aggs = client
    .search(aggRequest, Void.class)
    .aggregations();

關於異常

  • 在Java API Client中一共有兩大類異常
  1. 第一類是由es伺服器端返回的錯誤引發的,例如es伺服器端的校驗未通過,或者es伺服器端自己內部出現異常等,這些情況下丟擲的異常是ElasticsearchException
  2. 第二類是因為請求未能成功到達es伺服器端而引發的,例如網路故障,es服務不可用等,這些情況下丟擲的異常是TransportException,這些是lower-level implementation丟擲的,有個例外:如果這些問題發生在RestClientTransport物件的方法中,那麼丟擲的異常型別是ResponseException
  • 以上就是Java API Client相關的重要知識點,在寫程式碼之前先了解它們算是打好基礎,然後,接下來精彩的實戰篇即將開幕

歡迎關注部落格園:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...