整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          ES-Search API之請求主體查詢(上)

          節將詳細介紹es Search API的查詢主體,定制化查詢條件的實現主體。

          query

          搜索請求體中查詢條件使用es DSL查詢語法來定義。通過使用query來定義查詢體。

          1GET /_search
          2{
          3   "query" : {
          4        "term" : { "user" : "kimchy" }
          5    }
          6}

          From / Size

          es的一種分頁語法。通過使用from和size參數來對結果集進行分頁。

          from設置第一條數據的偏移量。size設置返回的條數(針對每個分片生效),由于es天生就是分布式的,通過設置主分片個數來進行數據水平切分,一個查詢請求通常需要從多個后臺節點(分片)進行數據匯聚。

          From/Size方式會遇到分布式存儲的一個共性問題:深度分頁,也就是頁數越大需要訪問的數據則越大。es提供了另外一種分頁方式,滾動API(Scroll),后續會詳細分析。

          注意:from + size 不能超過index.max_result_window配置項的值,其默認值為10000。

          sort (排序)

          與傳統關系型數據庫類似,es支持根據一個或多個字段進行排序,同時支持asc升序或desc降序。另外es可以按照_score(基于得分)的排序,默認值。

          如果使用了排序,響應結果中每一條命中數據將包含一個響應字段sort,其類型為Object[],表示該文檔當前的排序值,該值在ES支持的第三種分頁方式Search After中會使用到。

          排序順序

          es提供了兩種排序順序,SortOrder.ASC(asc)升序、SortOrder.DESC(desc)降序,如果排序類型為_score,其默認排序順序為降序(desc),如果排序類型為字段,則默認排序順序為升序(asc)。

          排序模型選型

          es支持按數組或多值字段進行排序。模式選項控制選擇的數組值,以便對它所屬的文檔進行排序。模式選項可以有以下值:

          • min 使用數組中最小的值參與排序
          • max 使用數組中最大的值參與排序
          • sum 使用數組中的總和參與排序
          • avg 使用數組中的平均值參與排序
          • median 使用數組中的中位數參與排序

          如果是一個數組類型的值參與排序,通常會對該數組元素進行一些計算得出一個最終參與排序的值,例如取平均數、最大值、最小值、求和等運算。es通過排序模型mode來指定。

          嵌套字段排序

          es還支持在一個或多個嵌套對象內部的字段進行排序。一個嵌套查詢提包含如下選項(參數):

          • path定義要排序的嵌套對象。排序字段必須是這個嵌套對象中的一個直接字段(非嵌套字段),并且排序字段必須存在。
          • filter定義過濾上下文,定義排序環境中的過濾上下文。
          • max_children排序是要考慮根文檔下子屬性文檔的最大個數,默認為無限制。
          • nested排序體支持嵌套。
           1"sort" : [
           2  {
           3    "parent.child.age" : {      // @1
           4        "mode" :  "min",
           5         "order" : "asc",
           6         "nested": {                // @2
           7            "path": "parent",
           8            "filter": {
           9                "range": {"parent.age": {"gte": 21}}
          10            },
          11            "nested": {                            // @3
          12                "path": "parent.child",
          13                "filter": {
          14                    "match": {"parent.child.name": "matt"}
          15                }
          16            }
          17         }
          18    }
          19  }
          20]

          代碼@1:排序字段名,支持級聯表示字段名。代碼@2:通過nested屬性定義排序嵌套語法,其中path定義當前的嵌套層級,filter定義過濾上下文。@3內部可以再通過nested屬性再次嵌套定義。

          missing values

          由于es的索引,類型下的字段可以在索引文檔時動態增加,那如果有些文檔沒有包含排序字段,這部分文檔的順序如何確定呢?es通過missing屬性來確定,其可選值為:

          • _last默認值,排在最后。
          • _first排在最前。

          ignoring unmapped fields

          默認情況下,如果排序字段為未映射的字段將拋出異常。可通過unmapped_type來忽略該異常,該參數指定一個類型,也就是告訴ES如果未找該字段名的映射,就認為該字段是一個unmapped_type指定的類型,所有文檔都未存該字段的值。

          Geo sorting

          地圖類型排序,該部分將在后續專題介紹geo類型時講解。

          字段過濾

          默認情況下,對命中的結果會返回_source字段下的所有內容。字段過濾機制允許用戶按需要返回_source字段里面部分字段。其過濾設置機制已在Elasticsearch Document Get API詳解、原理與示例中已詳細介紹,在這里就不重復介紹了。

          Doc Value Fields

          使用方式如下:

           1GET /_search
           2{
           3    "query" : {
           4        "match_all": {}
           5    },
           6    "docvalue_fields" : [
           7        {
           8            "field": "my_date_field",   
           9            "format": "epoch_millis" 
          10
          11        }
          12    ]
          13}

          通過使用docvalue_fields指定需要轉換的字段與格式,它對于在映射文件中定義stored=false的字段同樣生效。字段支持用通配符,例如"field":"myfield*"。

          docvalue_fields中指定的字段并不會改變_souce字段中的值,而是使用fields返回值進行額外返回。

          java實例代碼片段如下(完整的Demo示例將在文末給出):

          1SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
          2sourceBuilder.query(QueryBuilders.termQuery("user", "dingw"))
          3        .sort(new FieldSortBuilder("post_date").order(SortOrder.DESC))
          4        .docValueField("post_date", "epoch_millis")

          其返回結果如下:

          {
              "took":88,
              "timed_out":false,
              "_shards":{
                  "total":5,
                  "successful":5,
                  "skipped":0,
                  "failed":0
              },
              "hits":{
                  "total":2,
                  "max_score":null,
                  "hits":[
                      {
                          "_index":"twitter",
                          "_type":"_doc",
                          "_id":"11",
                          "_score":null,
                          "_source":{
                              "post_date":"2009-11-19T14:12:12",
                              "message":"test bulk update",
                              "user":"dingw"
                          },
                          "fields":{
                              "post_date":[
                                  "1258639932000"
                              ]
                          },
                          "sort":[
                              1258639932000
                          ]
                      },
                      {
                          "_index":"twitter",
                          "_type":"_doc",
                          "_id":"12",
                          "_score":null,
                          "_source":{
                              "post_date":"2009-11-18T14:12:12",
                              "message":"test bulk",
                              "user":"dingw"
                          },
                          "fields":{
                              "post_date":[
                                  "1258553532000"
                              ]
                          },
                          "sort":[
                              1258553532000
                          ]
                      }
                  ]
              }
          }

          Post Filter

          post filter對查詢條件命中后的文檔再做一次篩選。

           1GET /shirts/_search
           2{
           3  "query": {
           4    "bool": {
           5      "filter": {
           6        "term": { "brand": "gucci" }      // @1
           7      }
           8    }
           9  },
          10  "post_filter": {     // @2
          11    "term": { "color": "red" }
          12  }
          13}

          首先根據@1條件對索引進行檢索,然后得到匹配的文檔后,再利用@2過濾條件對結果再一次篩選。

          Highlighting

          查詢結果高亮顯示。

          Es支持的高亮分析器

          用于對查詢結果中對查詢關鍵字進行高亮顯示,高亮顯示查詢條件在查詢結果中匹配的部分。

          注意:高亮顯示器在提取要高亮顯示的術語時不能反映查詢的布爾邏輯。因此對于一些復雜的布爾查詢(例如嵌套的布爾查詢,或使用minimum_should_match等查詢)可能高亮顯示會出現一些誤差。

          高亮顯示需要字段的實際內容。如果字段沒有存儲(映射沒有將store設置為true),則從_source中提取相關字段。

          es支持三種高亮顯示工具,通過為每個字段指定type來使用。

          • unified highlighter使用Lucene unified高亮顯示器。首先將文本分解成句子并使用BM25算法對單個句子進行評分。支持精確的短語和多術語(模糊、前綴、正則表達式)高亮顯示。這是es默認的高亮顯示器。
          • plain highlighter使用Lucene標準高亮顯示器。plain highlighter最適合單個字段的匹配高亮顯示需求。為了準確地反映查詢邏輯,它在內存中創建一個很小的索引,并通過Lucene的查詢執行計劃重新運行原來的查詢條件,以便獲取當前文檔的更低級別的匹配信息。如果需要對多個字段進行高亮顯示,建議還是使用unified高亮顯示器或term_vector fields。plain高亮顯示器是一個實時分析處理高亮器。即用戶在查詢的時候,搜索引擎查詢到了目標數據docid后,將需要高亮的字段數據提取到內存,再調用該字段的分析器進行處理,分析完后采用相似度算法計算得分最高的前n組并高亮段返回數據。plain高亮器是實時分析高亮器,這種實時分析機制會讓ES占用較少的IO資源同時也占用較少的存儲空間(詞庫較全的話相比fvh方式能節省一半的存儲空間),其策略是采用cpu資源來換取磁盤IO壓力,在需要高亮字段較短(比如高亮文章的標題)時候速度較快,同時因IO訪問的次數少,IO壓力較小,有利于提高系統吞吐量。
          • fast vector highlighter(fvh)使用lucene fast vector highlingter,基于詞向量,該高亮處理器必須開啟term_vector=with_positions_offsets,存儲詞向量、即位置與偏移量。為解決大文本字段上高亮速度性能的問題,lucene高亮模塊提供了基于向量的高亮方式 fvh。fvh高亮顯示器利用建索引時候保存好的詞向量來直接計算高亮段落,在高亮過程中比plain高亮方式少了實時分析過程,取而代之的是直接從磁盤中將分詞結果直接讀取到內存中進行計算。故要使用fvh的前置條件就是在建索引時候,需要配置存儲詞向量,詞向量需要包含詞位置信息、詞偏移量信息。注意:fvh高亮器不支持span查詢。如果您需要對span查詢的支持,請嘗試其他高亮顯示,例如unified highlighter。

          Offsets Strategy

          獲取偏移量策略。高亮顯示要解決的一個核心就是高亮顯示的詞根以及該詞根的位置(位置與偏移量)。

          ES中提供了3中獲取偏移量信息(Offsets)的策略:

          • The postings list如果將index_options設置為offsets,unified高亮器將使用該信息突出顯示文檔,而無需重新分析文本。它直接對索引重新運行原始查詢,并從索引中提取匹配偏移量。如果字段很大,這一點很重要,因為它不需要重新分析需要高亮顯示的文本。比term_vector方式占用更少的磁盤空間。
          • Term vectors如果在字段映射中將term_vector設置為with_positions_offset,unified highlighter將自動使用term_vector來高亮顯示字段。它特別適用于大字段和高亮顯示多詞根查詢(如前綴或通配符),因為它可以訪問每個文檔的術語字典。fvh高亮器必須將字段映射term_vector設置為with_positions_offset時才能生效。
          • Plain highlighting當沒有其他選擇時,統一使用這種模式。它在內存中創建一個很小的索引,并通過Lucene的查詢執行計劃重新運行原來的查詢條件,以訪問當前文檔上的低級匹配信息。對于每個需要突出顯示的字段和文檔,都要重復此操作。Plain高亮顯示器就是這種模式。注意:對于大型文本,Plain顯示器可能需要大量的時間消耗和內存。為了防止這種情況,在下一個版本中,對要分析的文本字符的最大數量將限制在100萬。6.x版本默認無限制,但是可以使用索引設置參數index.highlight.max_analyzed_offset為特定索引設置。

          高亮顯示配置項

          高亮顯示的全局配置會被字段級別的覆蓋。

          • boundary_chars設置邊界字符串集合,默認包含:.,!? \t\n
          • boundary_max_scan掃描邊界字符。默認為20。
          • boundary_scanner指定如何分解高亮顯示的片段,可選值為chars、sentence、word。
          • chars字符。使用由bordery_chars指定的字符作為高亮顯示邊界。通過boundary_max_scan控制掃描邊界字符的距離。該掃描方式只適用于fvh。
          • sentence句子,使用Java的BreakIterator確定的下一個句子邊界處的突出顯示片段。您可以使用boundary_scanner_locale指定要使用的區域設置。unified 高亮器默認行為。
          • word單詞,由Java的BreakIterator確定的下一個單詞邊界處高亮顯示的片段。
          • boundary_scanner_locale區域設置。該參數采用語言標記的形式,例如。“en-us”、“- fr”、“ja-JP”。更多信息可以在Locale語言標記文檔中找到。默認值是local.roo-t。
          • encoder指示代碼段是否應該編碼為HTML:默認(無編碼)或HTML (HTML-轉義代碼段文本,然后插入高亮標記)。
          • fields指定要檢索高亮顯示的字段,支持通配符。例如,您可以指定comment_*來獲得以comment_開頭的所有文本和關鍵字字段的高亮顯示。注意:當您使用通配符時,只會匹配text、keyword類型字段。
          • force_source是否強制從_source高亮顯示,默認為false。其實默認情況就是根據源字段內容(_source)內容高亮顯示,即使字段是單獨存儲的。
          • fragmenter指定如何在高亮顯示代碼片段中拆分文本:可選值為simple、span。僅適用于Plain高亮顯示器。默認為span。
          • simple將文本分成大小相同的片段。
          • span將文本分割成大小相同的片段,但盡量避免在突出顯示的術語之間分割文本。這在查詢短語時很有用。
          • fragment_offset控制開始高亮顯示的margin(空白),僅適用于fvh。
          • fragment_size高亮顯示的片段,默認100。
          • highlight_query高亮顯示匹配搜索查詢以外的查詢。如果您使用rescore查詢,這尤其有用,因為默認情況下高亮顯示并不會考慮這些查詢。通常,應該將搜索查詢包含在highlight_query中。
          • matched_fields組合多個字段上的匹配項以突出顯示單個字段。對于以不同方式分析相同字符串的多個字段,這是最直觀的。所有matched_fields必須將term_vector設置為with_positions_offset,但是只加載匹配項組合到的字段,所以建議該字段store設置為true。只適用于fvh。
          • no_match_size如果沒有要高亮顯示的匹配片段,則希望從字段開頭返回的文本數量。默認值為0(不返回任何內容)。
          • number_of_fragments返回的高亮顯示片段的最大數量。如果片段的數量設置為0,則不返回片段。默認為5。
          • order該值默認為none,按照字段的順序返回高亮文檔,可以設置為score(按相關性排序)。
          • phrase_limit控制要考慮的文檔中匹配短語的數量。防止fvh分析太多的短語和消耗太多的內存。在使用matched_fields時,將考慮每個匹配字段的phrase_limit短語。提高限制會增加查詢時間并消耗更多內存。只支持fvh。默認為256。
          • pre_tags用于高亮顯示HTML標簽,與post_tags一起使用,默認用高亮顯示文本。
          • post_tags用于高亮顯示HTML標簽,與pre_tags一起使用,默認用高亮顯示文本。
          • require_field_match默認情況下,只有包含查詢匹配的字段才會高亮顯示。將require_field_match設置為false以突出顯示所有字段。默認值為true。
          • tags_schema定義高亮顯示樣式,例如。
          • type指定高亮顯示器,可選值:unified、plain、fvh。默認值為unified。

          高亮顯示demo

           1public static void testSearch_highlighting() {
           2        RestHighLevelClient client = EsClient.getClient();
           3        try {
           4            SearchRequest searchRequest = new SearchRequest();
           5            searchRequest.indices("map_highlighting_01");
           6            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
           7            sourceBuilder.query(
           8            //      QueryBuilders.matchAllQuery()
           9                    QueryBuilders.termQuery("context", "身份證")
          10                    );
          11
          12            HighlightBuilder highlightBuilder = new HighlightBuilder();
          13            highlightBuilder.field("context");
          14
          15            sourceBuilder.highlighter(highlightBuilder);
          16            searchRequest.source(sourceBuilder);
          17            System.out.println(client.search(searchRequest, RequestOptions.DEFAULT));
          18        } catch (Exception e) {
          19            // TODO: handle exception
          20        }
          21    }

          其返回值如下:

           1{
          10    "hits":{
          11       
          13        "hits":[
          14            {
          19                "_source":{
          20                    "context":"城中西路可以受理外地二代身份證的辦理。"
          21                },
          22                "highlight":{   // @1
          23                    "context":[
          24                        "城中西路可以受理外地二代<em>身份證</em>的辦理。"
          25                    ]
          26                }
          27             }
          28        ]
          29    }
          30}

          這里主要對highlight再做一次說明,其中每一個字段返回的內容是對應原始數據的子集,最多fragmentSize個待關鍵字的匹配條目,通常,在頁面上顯示文本時,應該用該字段取代原始值,這樣才能有高亮顯示的效果。

          Rescoring

          重打分機制。一個查詢首先使用高效的算法查找文檔,然后對返回結果的top n 文檔運用另外的查詢算法,通常這些算法效率低效但能提供匹配精度。resoring查詢與原始查詢分數的合計方式如下:

          • total 兩個評分相加
          • multiply 將原始分數乘以rescore查詢分數。用于函數查詢重定向。
          • avg取平均數
          • max取最大值
          • min取最小值。


          Search Type

          查詢類型,默認值為query_then_fetch。

          • QUERY_THEN_FETCH
            首先根據路由算法向相關分片(多個)發送請求,此時只返回docid與一些必要信息(例如用于排序等),然后對各個分片的結果進行匯聚,排序,然后選取客戶端指定需要獲取的數據條數前N條數據,然后根據docid再向各個分片請求具體的文檔信息。
          • QUERY_AND_FETCH
            在5.4.x版本開始廢棄,是直接向各個分片節點請求數據,每個分片返回客戶端請求數量的文檔信息,然后匯聚全部返回給客戶端,返回的數據為客戶端請求數量size * (路由后的分片數量)。
          • DFS_QUERY_THEN_FETCH
            在開始向各個節點發送請求之前,會進行一次詞頻、相關性的計算,后續流程與QUERY_THEN_FETCH相同,可以看出,該查詢類型的文檔相關性會更高,但性能比QUERY_THEN_FETCH要差。

          scroll

          滾動查詢。es另外一種分頁方式。雖然搜索請求返回結果的單個頁面,但scroll API可以用于從單個搜索請求檢索大量結果(甚至所有結果),這與在傳統數據庫上使用游標的方式非常相似。

          scroll api不用于實時用戶請求,而是用于處理大量數據,例如為了將一個索引的內容重新索引到具有不同配置的新索引中。

          如何使用scroll API

          scroll API使用分為兩步:

          1、第一步,首先通過scroll參數,指定該滾動查詢(類似于數據庫的游標的存活時間)

          1POST /twitter/_search?scroll=1m
          2{
          3    "size": 100,
          4    "query": {
          5        "match" : {
          6            "title" : "elasticsearch"
          7        }
          8    }
          9}

          該方法會返回一個重要的參數scrollId。

          2、第二步,使用該scrollId去es服務器拉取下一批(下一頁數據)

          1POST  /_search/scroll 
          2{
          3    "scroll" : "1m", 
          4    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
          5}

          循環第三步,可以循環批量處理數據。

          3、第三步,清除scrollId,類似于清除數據庫游標,快速釋放資源。

          1DELETE /_search/scroll
          2{
          3    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
          4}

          下面給出scoll api的java版本示例程序

           1public static void testScoll() {
           2        RestHighLevelClient client = EsClient.getClient();
           3        String scrollId = null;
           4        try {
           5            System.out.println("step 1 start ");
           6            // step 1 start
           7            SearchRequest searchRequest = new SearchRequest();
           8            searchRequest.indices("map_highlighting_01");
           9            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
          10            sourceBuilder.query(
          11                    QueryBuilders.termQuery("context", "身份證")
          12                    );
          13            searchRequest.source(sourceBuilder);
          14            searchRequest.scroll(TimeValue.timeValueMinutes(1));
          15            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
          16            scrollId = result.getScrollId();
          17            // step 1 end
          18
          19            // step 2 start
          20            if(!StringUtils.isEmpty(scrollId)) {
          21                System.out.println("step 2 start ");
          22                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
          23                scrollRequest.scroll(TimeValue.timeValueMinutes(1));
          24                while(true) { //循環遍歷
          25                    SearchResponse scollResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
          26                    if(scollResponse.getHits().getHits() == null ||
          27                            scollResponse.getHits().getHits().length < 1) {
          28                        break;
          29                    }
          30                    scrollId = scollResponse.getScrollId();
          31                    // 處理文檔
          32                    scrollRequest.scrollId(scrollId);
          33                }
          34            // step 2 end   
          35            }
          36            System.out.println(result);
          37        } catch (Exception e) {
          38            e.printStackTrace();
          39        } finally {
          40            if(!StringUtils.isEmpty(scrollId)) {
          41                System.out.println("step 3 start ");
          42                // step 3 start
          43                ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
          44                clearScrollRequest.addScrollId(scrollId);
          45                try {
          46                    client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
          47                } catch (IOException e) {
          48                    // TODO Auto-generated catch block
          49                    e.printStackTrace();
          50                }
          51            // step 3 end
          52            }
          53        } 
          54
          55    }

          這里重點闡述一下第一次查詢時,不僅返回scrollId,也會返回第一批數據。

          Keeping the search context alive

          scroll參數(傳遞給搜索請求和每個滾動請求)告訴es它應該保持搜索上下文活動多長時間。只需要足夠長的時間來處理前一批結果。

          每個scroll請求(帶有scroll參數)設置一個新的過期時間。如果scroll請求沒有傳入scroll,那么搜索上下文將作為scroll請求的一部分被釋放。

          scroll其內部實現類似于快照,當第一次收到一個scroll請求時,就會為該搜索上下文所匹配的結果創建一個快照,隨后文檔的變化并不會反映到該API的結果。

          sliced scroll

          對于返回大量文檔的scroll查詢,可以將滾動分割為多個可以獨立使用的片,通過slice指定。例如:

           1GET /twitter/_search?scroll=1m     // @1
           2{
           3    "slice": {                                      // @11
           4        "id": 0,                                    // @12
           5        "max": 2                                 // @13
           6    },
           7    "query": {
           8        "match" : {
           9            "title" : "elasticsearch"
          10        }
          11    }
          12}
          13GET /twitter/_search?scroll=1m        // @2
          14{
          15    "slice": {
          16        "id": 1,
          17        "max": 2
          18    },
          19    "query": {
          20        "match" : {
          21            "title" : "elasticsearch"
          22        }
          23    }
          24}

          @1,@2兩個并列的查詢,按分片去查詢。
          @11:通過slice定義分片查詢。
          @12:該分片查詢的ID。
          @13:本次查詢總片數。
          這個機制非常適合多線程處理數據。
          具體分片機制是,首先將請求轉發到各分片節點,然后在每個節點使用匹配到的文檔(hashcode(_uid)%slice片數),然后各分片節點返回數據到協調節點。也就是默認情況下,分片是根據文檔的_uid,為了提高分片過程,可以通過如下方式進行優化,并指定分片字段。

          • 分片字段類型為數值型。
          • 字段的doc_values設置為true。
          • 每個文檔中都索引了該字段。
          • 該字段值只在創建時賦值,并不會更新。
          • 字段的基數應該很高(相當于數據庫索引選擇度),這樣能確保每個片返回的數據相當,數據分布較均勻。

          注意,默認slice片數最大為1024,可以通過索引設置項index.max_slices_per_scroll來改變默認值。例如:

          目管理微服務開發

          從本章開始,我們將根據電商平臺的各個實例項目進行具體的微服務開發,主要包括類目管理、庫存管理、訂單管理等。在這幾個實例項目中,我們將根據項目本身的特點,使用不同的數據庫進行開發。對于類目管理來說,我們將使用二級分類設計,即數據實體之間存在一定的關聯關系,因此最好的選擇就是使用Spring Data JPA進行開發。Spring Data JPA是Spring Boot開發框架中一個默認推薦使用的數據庫開發方法,同時,JPA 也是領域驅動設計的一種具體應用。

          本章的項目工程可以通過本文的源代碼在IDEA中使用Git檢出。該項目由三個模塊組成:

          • catalog-object:類目公共對象設計。
          • catalog-restapi:類目接口開發。
          • catalog-web:類目管理的Web應用。

          了解領域驅動設計

          領域驅動設計(Domain-Driven Design,DDD)是一種面向對象建模,以業務模型為核心展開的軟件開發方法。面向對象建模的設計方法,相比于面向過程和面向數據結構的設計方法,從根本上解耦了系統分析與系統設計之間相互隔離的狀態,從而提高了軟件開發的工作效率。

          我們將使用JPA來實現領域驅動設計的開發方法。JPA通過實體定義建立了領域業務對象的數據模型,然后通過使用存儲庫賦予實體操作行為,從而可以快速進行領域業務功能的開發。

          DDD的分層結構

          DDD將系統分為用戶接口層、應用層、領域層和基礎設施層,如圖6-1所示。

          應用層是很薄的一層,負責接收用戶接口層傳來的參數和路由到對應的領域層,系統的業務邏輯主要集中在領域層中,所以領域層在系統架構中占據了很大的面積。上下層之間應該通過接口進行通信,這樣接口定義的位置就決定了上下層之間的依賴關系。

          DDD的基本元素

          DDD的基本元素有Entity、Value Object、Service、Aggregate、Repository、Factory、DomainEvent和Moudle等。

          • Entity:可以表示一個實體。
          • Value Object:表示一個沒有狀態的對象。Service:可以包含對象的行為。
          • Aggregate:一組相關對象的集合。Repository:一個存儲倉庫。
          • Factory:一個生成聚合對象的工廠。Domain Event:表示領域事件。
          • Moudle:表示模塊。

          Spring Data JPA

          JPA(Java Persistence API)即Java持久層API,是Java持久層開發的接口規范。Hibernate、TopLink和 OpenJPA等ORM框架都提供了JPA的實現。Spring Data JPA 的實現使用了Hibernate框架,所以在設計上與直接使用 Hibernate差別不大。但JPA 并不等同于Hibernate,它是在Hibernate之上的一個通用規范。

          接下來,我們通過模塊catalog-restapi來說明Spring Data JPA的開發方法。

          Druid數據源配置

          Druid是阿里巴巴開源的一個數據源服務組件,不僅具有很好的性能,還提供了監控和安全過濾的功能。

          我們可以創建一個配置類DruidConfiguration來啟用Druid 的監控和過濾功能,代碼如下所示:

          @Configuration
          public class DruidConfiguration {
          @Bean
          public ServletRegistrationBean statviewServle({
          ServletRegistrationBean servletRegistrationBean = new
          ServletRegistrationBean(new StatViewServlet (), "/druid/*");
          //IP地址白名單
          servletRegistrationBean.addInitParameter("allow","192.168.0.1,
          127.0.0.1");
          //IP地址黑名單(共同存在時,deny優先于allow)
          servletRegistrationBean.addInitParameter ("deny", "192.168.110.100");//控制臺管理用戶
          servletRegistrationBean.addInitParameter ("loginUsername" , "druid");servletRegistrationBean.addInitParameter ("loginPassword","12345678");//是否能夠重置數據
          servletRegistrationBean.addInitParameter("resetEnable" ,"false");return servletRegistrationBean;
          }
          @Bean
          public FilterRegistrationBean statFilter(){
          FilterRegistrationBean filterRegistrationBean = new
          FilterRegistrationBean (new webStatFilter());
          //添加過濾規則
          filterRegistrationBean.addUrlPatterns("/*");//忽略過濾的格式
          filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,* .jpg,* .png,*.Css,* .ico,/druid/* ")F
          return filterRegistrationBean;
          }
          }

          在使用這個監控配置后,當應用運行時,例如,我們啟動catalog-restapi模塊,即可通過下列鏈接打開監控控制臺頁面:

          http://localhost:9095/druid

          在登錄認證中輸入前面代碼中配置的用戶和密碼“druid/12345678”,即可打開如圖6-2所示的操作界面。注意,本地的P地址不在前面代碼設置的黑名單之中。

          在使用這個監控控制臺之后,通過查看“SQL監控”的結果,即可為我們對應用的SQL設計和優化提供有價值的參考依據。

          我們可以使用項目中的配置文件 application.yml 來設置Druid連接數據源,代碼如下所示:

          spring:
          datasource:
          driver-class-name: com.mysql.jdbc.Driverurl:
          jdbc:mysql://localhost:3306/catalogdb?characterEncoding=utf8&useSSL=false
          username: root
          password:12345678
          #初始化大小,最小值為5,最大值為120initialSize: 5
          minIdle: 5
          maxActive: 20
          #配置獲取連接等待超時的時間maxwait: 60000
          #配置間隔多久進行一次檢測,檢測需要關閉的空閑連接,單位是mstimeBetweenEvictionRunsMillis: 60000
          #配置一個連接在池中最小生存時間,單位是msminEvictableIdleTimeMillis:300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: true
          testOnBorrow: falsetestOnReturn: false
          #打開PSCache,指定每個連接上 PSCache的大小poolPreparedStatements: true
          maxPoolPreparedStatementPerConnectionSize: 20
          #配置監控統計攔截的filters,如果去掉,則監控界面SQL將無法統計, 'wall'用于防火墻filters: stat, wall, log4j
          #通過connectProperties屬性打開mergeSql功能;慢sQL記錄connectionProperties:
          druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

          在上面的配置中,主要設定了所連接的數據庫,以及數據庫的用戶名和密碼,確保數據庫的用戶配置信息正確,并且具有讀寫權限。其他一些配置可由 Druid 的通用參數來設定。

          數據源的配置同時適用于MyBatis的開發。

          JPA 初始化和基本配置

          首先,我們新建一個配置類JpaConfiguration,初始化一些JPA的參數,代碼如下所示:

          econfiguration
          @EnableTransactionManagement (proxyTargetClass = true)
          @EnableJpaRepositories (basePackages = "com.** .repository")CEntityScan(basePackages = "com.* *.entity")
          public class JpaConfiguration{
          @Bean
          PersistenceExceptionTranslationPostProcessorpersistenceExceptionTranslationPostProcessor(){
          return new PersistenceExceptionTranslationPostProcessor();
          }
          }

          在這里,我們設置存儲庫的存放位置為“com.**.repository”,同時設置實體的存放位置為“com.**.entity”,這樣就能讓JPA找到我們定義的存儲庫和實體對象了。

          然后,在應用程序的配置文件中,增加如下配置:

          spring:
          jpa:
          database: MYSQLshow-sql: false
          ## Hibernate ddl auto (validate lcreate l create-drop l update)hibernate:
          ddl-auto: update
          #naming-strategy:org.hibernate.cfg. ImprovedNamingStrategynaming.physical-strategy:
          org.hibernate.boot.model.naming. PhysicalNamingStrategystandardImpl
          properties:
          hibernate:
          dialect: org.hibernate.dialect.MySQL5Dialectenable_lazy_load_no_trans: true

          其中,“ddl-auto”設置為“update”,表示當實體屬性更改時,將會更新表結構。如果表結構不存在,則創建表結構。注意,不要把“ddl-auto”設置為“create”,否則程序每次啟動時都會重新創建表結構,而之前的數據也會丟失。如果不使用自動功能,則可以設置為“none”。上面配置中的最后一行代碼開啟了Hibernate的延遲加載功能,這可以提高關聯關系查詢時的訪問性能。

          實體建模

          在使用Spring Data JPA進行實體建模時,主要使用Hibernate的對象關系映射(ORM)來實現。在類目管理項目中我們需要創建兩個實體,分別為主類和二級分類。

          主類由名稱、操作者和創建日期等屬性組成,實現代碼如下所示:

          @Entity
          @Table(name ="tsorts")
          public class Sorts implements java.io.Serializable {
          CId
          @Generatedvalue(strategy = GenerationType. IDENTITY)private Long id;
          private string name;
          private string operator;
          @DateTimeFormat (pattern = "Yyyy-MM-dd HH:mm:ss")
          eColumn (name = "created",columnDefinition = "timestamp defaultcurrent timestamp")
          @Temporal (TemporalType.TIMESTAMP)private Date created;
          coneToMany(cascade = CascadeType.REMOVE)@OrderBy("created asc")
          CJoincolumn (name= "sorts id")
          private Set<Subsorts> subsortses = new HashSet<>();

          從上面代碼中可以看出,我們使用了表“t_sorts”來存儲數據,并且它與二級分類以一對多的方式建立了關聯關系。建立關聯關系的是“sorts_id”,它將被保存在二級分類的表格中。另外,在查詢這種關系時,我們指定了以創建時間“created”進行排序。

          二級分類實體由名稱、操作者和創建日期等屬性組成,代碼如下所示:

          @Entity
          @Table(name ="tsubsorts")
          public class Subsorts implements java.io.Serializable t
          @Id
          @Generatedvalue(strategy = GenerationType. IDENTITY)private Long id;
          private string name;
          private String operator;
          @DateTimeFormat (pattern = "Yyyy-MM-dd HH:mm:ss")
          @Column (name = "created",columnDefinition = "timestamp defaultcurrent_timestamp")
          @Temporal (TemporalType.TIMESTAMP)private Date created;
          ...
          }

          二級分類使用了表結構“t_subsorts”來存儲數據,字段定義與主類的定義幾乎相同。

          在上面兩個實體對象的設計中,我們通過主類使用一對多的方式與二級分類實現關聯設計,這樣,當在主類中進行查詢時,將可以同時獲取二級分類的數據;而對主類的存儲和更新,也將自動涉及分類的相關操作。

          有關實體建模的設計,特別是關聯關系的設計,我們主要說明以下幾個重要的功能。

          (1)實體對象必須有一個唯一標識。

          這里使用Long類型定義對象的身份標識“id”,并且這個“id”將由數據庫自動生成。在實際應用中,推薦使用UUID作為對象的唯一標識,這樣不僅可以保持這一字段長度的一致性,還能保證這一標識在整個數據庫中的唯一性,而且還將非常有利于數據庫的集群設計。

          (2)日期屬性要使用正確的格式。

          使用注解“@DateTimeFormat”對日期進行格式化,不僅可以保證日期正常顯示,還能保證在參數傳遞中日期的正確性。注意,上面的創建日期“created”使用了默認值設置。

          (3)使用合理的關聯設置。

          關聯設置是實體設計的關鍵,為了避免引起遞歸調用,最好使用單向關聯設置,即在互相關聯的兩個對象之中,只在一個對象中進行關聯設置。

          一般來說,多對多的關聯可以使用中間表來存儲關聯關系,而一對多或多對一的關聯關系可以使用一個字段來存儲關聯對象的外鍵。例如,在上面的實體設計中,我們使用“sorts_id"作為二級分類與主類關聯的外鍵。

          在主類實體的關聯設置中,我們還使用了級聯的操作設置:“CascadeType.REMOVE”。這樣,當主類中的一個類別被刪除時,將會自動刪除與其關聯的所有分類。

          有關級聯的設置,可以使用的選項如下所示:

          • CascadeType.PERSIST:級聯保存。
          • CascadeType.REMOVE:級聯刪除。
          • CascadeType.MERGE:級聯合并(更新)。
          • CascadeType.DETACH:級聯脫管/游離。
          • CascadeType.ALL:以上所有級聯操作。

          查詢對象設計

          我們將查詢對象設計放在一個公共模塊catalog-object中,這樣,其他兩個模塊都可以進行調用。使用查詢對象(Query Object,qo)是為了與vo進行區分。有人把vo看成值對象(ValueObject),也有人把vo看成視圖對象(View Object),所以很容易引起誤解。這兩種對象的意義和用途是不一樣的,值對象表示的是與實體不同的一些數據,它可以作為視圖顯示;而視圖對象是只能作為視圖顯示的一種數據。

          因為實體是有生命周期和狀態的,并且它的狀態會直接影響存儲的數據,所以我們使用一個無狀態的數據對象來存儲實體的數據。這些數據的使用和更改不會直接影響數據存儲,因此它的使用是安全的,也可以用之即棄。

          我們既可以將查詢對象作為值對象使用,也可以將查詢對象作為視圖對象使用,還可以將查詢對象作為查詢參數的一個集合來使用,即相當于一個數據傳輸對象(Data Transfer Object, dto)。

          我們只要使用一個查詢對象qo,就可以包含vo、dto等對象的功能,這是一種簡化設計。qo有時會包含一些冗余數據,但這對于使用方來說影響不大。例如,在我們的查詢對象中,將會包含分頁所需的頁碼和頁大小等分頁屬性數據,而在視圖顯示中并不需要這些數據,所以它可以不用理會這些數據。

          相對于主類實體,它的查詢對象的設計如下所示:

          public class SortsQ0 extends PageQ0 {
          private Long id;
          private String name;
          private String operator;
          @DateTimeFormat (pattern = "yyyY-MM-dd HH:mm :ss")private Date created;
          private List<SubsortsQ0> subsortses = new ArrayList<>();
          ...
          }

          其中,它所繼承的PageQo查詢對象將提供兩個分頁查詢參數,實現代碼如下所示:

          public class PageQ0 [
          private Integer page = 0;private Integer size = 10;
          ...
          }

          在分頁參數中,只有一個頁碼和每頁大小的設定兩個字段。

          數據持久化設計

          使用JPA進行實體數據持久化設計是比較容易的,只要為實體創建一個存儲庫接口,將實體對象與JPA的存儲庫接口進行綁定,就可以實現實體的數據持久化設計,相當于給實體賦予了一些訪問數據庫的操作行為,包括基本的增刪改查等操作。

          除數據存儲的基本操作外,我們還可以根據實體的字段名稱來聲明查詢接口,而對于一些復雜的查詢,也可以使用SQL查詢語言設計。實體主類的存儲接口設計如下所示:

          @Repository
          public interface SortsRepository extends JpaRepository<Sorts,Long>,JpaSpecificationExecutor<Sorts> {
          Page<Sorts> findByNameLike (@Param ( "name ") string name,PageablepageRequest);
          @Query("select t from Sorts t where t.name like :name and t.created=:created")
          Page<Sorts> findByNameAndCreated(@Param ("name") String name,@Param ("created") Date created, Pageable pageRequest);
          Sorts findByName (@Param("name") String name);
          @Query ( "select s from Sorts s"+
          "left join s.subsortses b"+"where b.id= :id")
          Sorts findBySubsortsId(@Param( "id") Long id);
          }

          這個接口定義是不用我們實現的,只要方法定義符合JPA的規則,后續的工作就可以交給JPA來完成。

          在JPA中,可以根據以下方法自定義聲明方法的規則,即在接口中使用關鍵字findBy.readBy、getBy等作為方法名的前綴,然后拼接實體類中的屬性字段(首個字母大寫),最后拼接一些SQL查詢關鍵字(也可不拼接),組成一個查詢方法。下面是一些查詢關鍵字的使用實例:

          • And,例如findByIdAndName(Long id, String name);
          • Or,例如findByldOrName (Long id, String name);
          • Between,例如 findByCreatedBetween(Date start,Date end); LessThan,例如findByCreatedLessThan(Date start);
          • GreaterThan,例如findByCreatedGreaterThan(Date start); IsNull,例如findByNameIsNull();
          • IsNotNull,例如 findByNamelsNotNull();
          • NotNull,與IsNotNull等價;
          • Like,例如 findByNameLike(String name);
          • NotLike,例如 findByNameNotLike(String name);
          • OrderBy,例如findByNameOrderByIdAsc(String name); Not,例如 findByNameNot(String name);
          • In,例如 findByNameIn(Collection<String> nameList);
          • NotIn,例如 findByNameNotIn(Collection<String> nameList)。

          通過注解@Query使用SQL查詢語言設計的查詢,基本與數據庫的查詢相同,這里只是使用實體對象的名字代替了數據庫表的名字。

          在上面的存儲庫接口定義中,我們不但繼承了JPA的基礎存儲庫JpaRepository,還繼承了一個比較特別的存儲庫JpaSpecificationExecutor,通過這個存儲庫可以進行一些復雜的分頁設計。

          數據管理服務設計

          前面的持久化設計已經在實體與數據庫之間建立了存取關系。為了更好地對外提供數據訪問服務,我們需要對存儲庫的調用再進行一次封裝。在這次封裝中,我們可以實現統一事務管理及其分頁的查詢設計。分類的數據管理服務設計代碼如下所示:

          @Service
          @Transactional
          public class SortsService {
          @Autowired
          private SortsRepository sortsRepository;
          public Sorts findOne (Long id){
          Sorts sorts =sortsRepository.findById(id).get();return sorts;
          public Sorts findByName (string name){
          return sortsRepository.findByName (name) ;
          }
          public String save (Sorts sorts){
          tryi
          sortsRepository.save(sorts);
          return sorts.getId() .toString();}catch(Exception e){
          e.printStackTrace();return e.getMessage();
          }
          public String delete (Long id){
          try{
          sortsRepository.deleteById(id);return id.toString();
          Jcatch(Exception e){
          e.printStackTrace();return e.getMessage ();
          public Page<Sorts> findAll (SortsQo sortsQo){
          Sort sort = new Sort (Sort.Direction. DESC, "created");
          Pageable pageable = PageRequest.of (sortsQo.getPage (), sortsQo.getSize(),
          sort);
          return sortsRepository.findAll (new Specification<Sorts>()1
          @override
          public Predicate toPredicate (Root<Sorts> root, CriteriaQuery<?>query,
          CriteriaBuilder criteriaBuilder) {
          List<Predicate> predicatesList =new ArrayList<Predicate>();
          if(CommonUtils.isNotNull (sortsQo.getName()){
          predicatesList.add (criteriaBuilder.like(root.get ("name"),
          "號"+sorts0o.getName()+"%"));
          }
          if(CommonUtils.isNotNull (sortsQo.getCreated())){
          predicatesList.add (criteriaBuilder.greaterThan (root.get ("created"),sortsQo.getCreated()));
          }
          query.where(predicatesList.toArray (new
          Predicate[predicatesList.size()]));
          return query.getRestriction();
          }, pageable);
          }
          }

          在上面的代碼中,使用注解@Transactional 實現了隱式事務管理,對于一些基本的數據操作,可直接調用存儲庫接口的方法。

          在上述代碼中,使用findAll方法實現了分頁查詢的設計。在這個設計中,可以定義排序的方法和字段,以及對頁碼和每頁行數的設定,同時,還可以根據查詢參數動態地設置查詢條件。在這里,我們既可以按分類的名稱進行模糊查詢,也可以按分類的創建時間進行限定查詢。

          單元測試

          在完成上節的設計之后,我們可以寫一個測試用例驗證領域服務的設計。需要注意的是,因為在前面的JPA配置中已經有了更新表結構的配置,所以如果表結構不存在,則會自動生成;如果表結構更新,則啟動程序也會自動更新。下面的測試用例演示了如何插入分類和主類的數據:

          @RunWith(SpringJUnit4ClassRunner.class)
          eContextConfiguration (classes = {UpaConfiguration.class,SortsRestApiApplication.class})
          @SpringBootTest
          public class SortsTest{
          private static Logger logger = LoggerFactory.getLogger (SortsTest.class);
          CAutowired
          private SortsService sortsService;@Autowired
          private SubsortsService subsortsService;
          @Test
          public void insertData() {
          Sorts sorts =new Sorts();sorts.setName("圖書");
          sorts.setOperator( "editor");sorts.setCreated(new Date());
          //
          Sorts sorts = sortsService.findByName("圖書");
          Subsorts subsorts = new Subsorts();subsorts.setName("計算機");
          subsorts.setOperator("editor");subsorts.setCreated(new Date());
          subsortsService.save (subsorts);
          Assert.notNull(subsorts.getId(), "insert sub error");
          sorts.addSubsorts(subsorts);
          sortsService.save(sorts);
          Assert.notNull (sorts.getId(), "not insert sorts");
          }
          ...
          }

          其他查詢的測試用例可以參照這個方法設計,如果斷言沒有錯誤,則說明測試符合預期,即不會提示任何錯誤信息。在調試環境中,還可以借助控制臺信息分析測試的過程。

          類目接口微服務開發

          類目接口微服務是一個獨立的微服務應用,它將使用基于REST 協議的方式,對外提供一些有關類目查詢和類目數據管理的接口服務。這個接口服務,既可以用于商家后臺進行類目管理的設計之中,也可以用于移動端、App或其他客戶端程序的設計之中。

          當上面的單元測試完成之后,我們就可以使用上面設計中提供的數據服務進行類目接口微服務的開發了。

          RESTful接口開發

          我們將遵循REST協議的規范設計基于RESTful的接口開發,例如,對于分類來說,我們可以設計如下請求:

          • GET/sorts/{id}:根據ID獲取一個分類的詳細信息;GET /sorts:查詢分類的分頁列表;
          • POST /sorts:創建一個新分類; PUT /sorts:更新一個分類;
          • DELETE /sorts/{id}:根據ID刪除一個分類。

          下面的代碼展示了分類接口設計的部分實現,完整的代碼可以查看項目工程的相關源代碼:

          @RestController
          @RequestMapping("/sorts")
          public class SortsController {
          private static Logger logger = LoggerFactory.getLogger(SortsController.class);
          @Autowired
          private SortsService sortsService;
          @GetMapping (value="/{id] ")
          public String fnidById(@PathVariable Long id){
          return new Gson().toJson (sortsService.findOne (id));
          )
          @GetMapping ()
          public String findAll(Integer index,Integer size, String name)
          try {
          SortsQo sortsQ0 = new SortsQ0();if(CommonUtils.isNotNul1(index)){
          sortsQo .setPage(index);
          }
          if(CommonUtils.isNotNull(size)){
          sortsQ0.setsize(size);
          }
          if(CommonUtils.isNotNul1 (name)){
          sortsQo. setName(name);
          }
          Page<Sorts> orderses = sortsService.findAll(sortsQo);
          Map<String, 0bject> page = new HashMap<>();
          page.put( "content", orderses.getContent();
          page.put ("totalPages", orderses.getTotalPages());
          page.put ("totalelements",orderses.getTotalElements());
          return new Gson() .toJson (page);
          }
          Jcatch(Exception e){
          e.printStackTrace();
          }
          return null;
          )
          @PostMapping()
          public String save (CRequestBody SortsQo sortsQo) throws Exception{t
          Sorts sorts =new sorts();
          BeanUtils.copyProperties (sortsQ0,sorts);sorts.setCreated (new Date());
          List<Subsorts> subsortsList = new ArrayList<>();//轉換每個分類,然后加入主類的分類列表中
          for(SubsortsQo subsortsQ0 : sortsQo.getSubsortses()){
          Subsorts subsorts =new Subsorts();
          BeanUtils.copyProperties(subsortsQ0,subsorts);subsortsList.add(subsorts);
          )
          sorts. setSubsortses (subsortsList);
          String ret =sortsService.save(sorts);logger.info("新增="+ ret);
          return ret;
          }
                                                                             ...
                                                                             
                                                                            }

          在上面微服務接口設計中,使用RestController 定義了對外提供服務的URL 接口,而接口之中有關數據的訪問則通過調用SortsService的各種方法來實現。其中,在接口調用中,都使用JSON方式的數據結構來傳輸數據,所以在上面代碼中,顯式或隱式地使用了JSON 的數據結構。對于一個數據對象來說,為了保證其數據的完整性,我們一般使用GSON 工具對數據進行顯式轉換。

          需要注意的是,因為在數據傳輸中使用的是查詢對象,所以當進行數據保存和更新操作時,需要將查詢對象轉換為實體對象。

          微服務接口調試

          當微服務接口開發完成之后,即可啟動項目的應用程序進行簡單調試。對于類目微服務接口,我們可以啟動catalog-restapi模塊中的主程序SortsRestApiApplication進行調試。

          在啟動成功之后,對于一些GET請求,可以直接通過瀏覽器進行調試。

          例如,通過下列鏈接地址,可以根據分類ID查看一個分類的信息:

          http://localhost:9091/sorts/1

          如果數據存在,則返回如圖6-3所示的JSON數據。

          使用如下鏈接地址可以查詢分頁第一頁的數據:

          http://localhost:9091/sorts

          如果查詢成功,則可以看到如圖6-4所示的信息。

          因為POST 和 PUT等請求在調試時需要傳輸參數,所以不能直接使用瀏覽器進行測試,但是可以通過Postman等工具進行調試。

          基于RESTful的微服務接口調用

          我們可以使用多種方法調用基于RESTful接口的服務。例如,可以使用HTTP 訪問(例如HttpClient),或者使用RestTemplate的方式進行調用,等等。但是,在微服務應用中,最好的方法是使用聲明式的FeignClient。

          因為 FeignClient是為其他微服務進行調用的,所以這里將這些設計都放在模塊catalog-object中進行開發。

          聲明式FeignClient 設計

          FeignClient是一個聲明式的客戶端,為了使用這個工具組件,我們需要在項目對象模型中引入 FeignClient的依賴,代碼如下所示:

          <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

          針對主類的接口調用,我們可以定義一個接口程序SortsClient,根據微服務catalogapi提供的接口服務,使用如下所示的方法聲明一些調用方法:

          @FeignClient ("catalogapi")
          public interface SortsClient {
          @RequestMapping (method = RequestMethod.GET, value = "/sorts/{id}")String findById(CRequestParam("id") Long id);
          @RequestMapping (method = RequestMethod.GET,value = "/sorts/findAll")String findList();
          CRequestMapping (method = RequestMethod.GET, value = "/sorts",
          consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
          produces =MediaType.APPLICATION_JSON_UTF8_VALUE)
          String findPage (CRequestParam("index") Integer index, @RequestParam ("size")Integer size,
          @RequestParam( "name") String name);
          @RequestMapping (method =RequestMethod.GET, value = "/sorts/findAll",
          consumes = MediaType.APPLICATION JSON UTF8_VALUE,
          produces = MediaType.APPLICATION_ JSON_UTF8_VALUE)
          String findAll();
          @RequestMapping (method = RequestMethod. POST, value = "/sorts",
          consumes = MediaType.APPLICATION_JSON UTF8 VALUE,
          produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
          String create(@RequestBody SortsQ0 sortsQ0) ;
          @RequestMapping (method = RequestMethod.PUT,value = "/sorts",
          consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
          produces = MediaType.APPLICATION_JSON_ UTF8_VALUE)
          String update(@RequestBody SortsQo sortsQo);
          @RequestMapping (method = RequestMethod. DELETE,value = "/sorts/{id}")String delete (@RequestParam("id") Long id);
          }

          在這個實現代碼中,首先通過注解@FeignClient引用微服務catalogapi,然后使用其暴露出來的URL直接聲明調用方法。需要注意的是,這里的數據傳輸,即數據的生產和消費,都是通過JSON格式進行的,所以為了保證中文字符的正確性,我們使用UTF8編碼。

          斷路器的使用

          基于SortsClient的聲明方法,我們可以創建一個服務類SortsRestService進行調用。然后,使用SortsRestService提供的功能,就可以像使用本地方法一樣使用微服務catalogapi 提供的接口方法。服務類SortsRestService的實現代碼如下所示:

          @service
          public class SortsRestService {
          CAutowired
          private SortsClient sortsClient;
          @HystrixCommand (fallbackMethod = "findByIdFallback")public String findById(Long id){
          return sortsClient.findById(id);
          private String findByIdFal1back (Long id){
          SortsQo sortsQo = new SortsQ0();
          return new Gson() .toJson (sortsQo);
          }
          ...
          }

          在上面的代碼中,我們實現了對SortsClient的調用,同時增加了一個注解@HystrixCommand。通過這個注解,定義了一個回退方法。而這一回退方法的設計,就是SpringCloud組件提供的斷路器功能的實現方法。斷路器的含義是,當服務調用過載或不可用時,通過降級調用或故障轉移的方法,減輕服務的負載。這里我們使用了回退方法設計,以快速響應來自客戶端的訪問,并保障客戶端對微服務的訪問不會因為出現故障而崩潰。斷路器的設計就像電路的保護開關一樣,對系統服務起到一定的保護作用。與保護開關不同的是,當系統恢復正常時,斷路器會自動失效,不用人為干預。

          類目管理Web應用微服務開發

          這里 類目管理是一個基于 PC 端的 Web 應用,它也是一個獨立的微服務應用。這個應用在項目工程的模塊catalog-web 中實現,可以把它看成一個獨立的項目。

          在這個應用中,我們將演示如何使用類目管理微服務接口提供的服務,進行相關應用功能的開發,從而實現在PC端提供一個對類目進行操作管理的友好操作界面。

          接口調用引用的相關配置

          上面的接口調用服務是在模塊catalog-object 中進行開發的,想要在模塊“catalog-web”中使用這些服務,就必須先在項目對象模型中進行引用配置,代碼如下所示:

          <dependency>
          <groupId>com.demo</groupId>
          <artifactId>catalog-object</artifactId><version>${project.version}</version></dependency>

          因為兩個模塊處于同一個項目工程之中,所以上面引用配置的版本直接使用了項目的版本。這樣,當接口服務啟動之后,我們就可以在接下來的 Web應用中進行相關調用了。

          需要注意的是,如果有多個FeignClient程序調用了同一個微服務接口服務,則必須在項目的配置文件中使用如下所示的配置進行設置,以支持這種調用方式。因為這個Spring Cloud版本的默認配置是不開啟這種調用方式的:

          #允許多個接口使用相同的服務

          spring:
          main:
          allow-bean-definition-overriding: true

          Spring MVC控制器設計

          Spring MVC是 Web應用開發的一個基礎組件,下面我們使用這一組設計一個控制器。在Web應用的主類控制器設計中,我們直接使用上面設計的服務類:SortsRestService。我們可以像使用本地方法一樣使用SortsRestService類,直接調用微服務提供的接口服務,代碼如下所示:

          @GRestController
          @RequestMapping ( "/sorts")
          public class SortsController {
          private static Logger logger =
          LoggerFactory.getLogger(SortsController.class);
          @Autowired
          private SortsRestService sortsRestService;
          @GetMapping(value=" /index")
          public Mode1AndView index(){
          return new ModelAndview( "sorts/index");
          @GetMapping (value="/{id]")
          public ModelAndView findById(@PathVariable Long id){
          return new ModelAndView("sorts/show", "sorts",
          new Gson() .fromJson (sortsRestService.findById(id),
          SortsQo.class));
          }
          ...
          }

          上面代碼中的findByld方法是一個使用頁面來顯示分類信息的設計。在這個設計中,一方面引用了上面設計的服務類SortsRestService,并調用了它的findByld 方法,進行數據查詢;另一方面將查詢數據通過一個 show頁面顯示出來。這個設計與一般的本地調用不同的是,查詢數據時得到的返回值是一種ISON結構,所以必須將它轉化為一個查詢對象,這樣才能方便使用。

          接下來的頁面設計將會用到Thymeleaf模板的功能。

          使用 Thymeleaf模板

          在 Web應用的頁面設計中,我們將使用Thymeleaf 這個模板,因此,必須在catolog-web模塊中引入Thymeleaf 的依賴,代碼如下所示:

          <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
          <dependency>
          <groupId>nz.net.ultraq.thymeleaf</groupId>
          <artifactId>thymeleaf-layout-dialect</artifactId><version>2.3.0</version>
          </dependency>

          有關 Thymeleaf 的配置,使用其默認配置即可,即只要在程序的資源目錄中有static和templates這兩個目錄就可以了。這兩個目錄分別用來存放靜態文件和模板設計及其頁面設計文件,頁面文件的后綴默認使用html。

          HTML頁面設計

          在6.10節控制器的設計中,類目信息輸出的是一個show頁面,它的設計在show.html文件中,代碼如下所示:

          <html xmlns:th="http://www.thymeleaf.org"><div class="addInfBtn">
          <h3 class="itemTit"><span>類目信息</span></h3><table class="addNewInfList">
          <tr>
          <th>名稱</th>
          <td width="240"><input class="inp-list w-200 clear-mr f-left"
          type="text" th:value="$ {sorts.namel" readonly="true"/></td>
          <th>操作者</th>
          <td><input class="inp-list w-200 clear-mr f-left" type="text"
          th:value="$ {sorts.operator}" readonly="true"/></td>
          </tr>
          <tr>
          <th>子類</th><td>
          <select multiple= "multiple" readonly="true">
          <option th:each="subsorts:${sorts.subsortses] "
          th:text="${#strings. length(subsorts.name)
          >20?#strings.substring (subsorts.name,0,20)+'...':subsorts.name} "
          th:selected="true"
          ></option>
          </select>
          </td>
          <th>日期</th><td>
          <input onfocus="WdatePicker ({dateFmt:'yyyy-MiM-dd HH :mm:ss'))"
          type="text" class="inp-list w-200 clear-mr f-left" th:value="${sorts.created)?$ {#dates.format(sorts.created, 'vyvy-MM-dd HlH:mm:ss')}:''" readonly="true"/>
          </td>
          </tr></table>
          <div class="bottomBtnBox">
          <a class="btn-93x38 backBtn" href="javascript:closeDialog (0)">返回</a></div>
          </div>

          從上面的代碼可以看出,除用到Thymeleaf特有的地方外,其他設計都與一般的HTML標簽語言相同。設計之后,這個頁面的最終效果如圖6-5所示。

          統一風格模板設計

          Thymeleaf更強大的功能是提供了一個統一風格的模板設計,即整個網站可以使用統一風格的框架結構。在類目管理這個項目中,使用了總體頁面框架設計layout.html,代碼如下所示:

          <! DOCTYPE html>
          <html xmlns:th="http://www.thymeleaf.org"
          xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
          <body>
          <div class="headerBox">
          <div class="topBox">
          <div class="topLogo f-left">
          <a href="#"><img th:src="@{/images/logo.pngl "/></a></div>
          </div></div>
          <div class="locationLine" layout:fragment="prompt">
          當前位置:首頁 > <em>頁面</em>
          </div>
          <table class="globalMainBox" style="position:relative;z-index:1">
          <tr>
          <td class="columnLeftBoX" valign="top">
          <div th:replace="fragments/nav ::nav"></div></td>
          <td class="whiteSpace"></td>
          <td class="rightColumnBox" valign="top"><div layout: fragment="content"></div></td>
          </tr></table>
          <form th:action="@{/logout}" method="post" id="logoutform"></form>
          <div class="footBox" th:replace="fragments/footer :: footer"></div></body>
          </html>

          頁面上方是狀態欄,頁面左側是導航欄,中間部分是內容顯示區域,底端還有一個頁腳設計。在引用這個模板之后,只需對需要更改的區域進行覆蓋就可以了,而不需要更改的地方使用模板的默認設計即可。一般來說,在使用這個模板時,只要更改狀態欄和內容顯示區域就可以了,而導航欄和頁腳,則可以使用通用的頁面設計。

          在這個例子中,分類的主頁是通過index.html這個頁面設計來引用這個模板的,代碼如下所示:

          <! DOCTYPE html>
          <html xmlns:th="http://www.thymeleaf.org"
          xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
          layout:decorator="fragments/layout">
          <body>
          <!--狀態欄-->
          <div class="locationLine" layout: fragment="prompt">
          當前位置:首頁> <em >類目管理</em>
          </div>
          <!--主要內容區域-->
          <div class="statisticBoX w-782"layout:fragment="content">
          ...
          </div></body></html>

          可以看出,在上面的代碼中,我們只更新了狀態欄和主要內容顯示區域的設計,其他部分都沿用了模板的設計。

          在上面的一些設計講解和演示中,我們只說明了主類的設計,二級分類的設計與主類的設計大同小異,不再贅述。

          至此,類目管理的微服務應用的開發工作就基本完成了。

          現在我們可以體驗微服務之間的調用了,因為使用了Spring Cloud工具組件來開發,所以在各個方面的實現都是非常方便的。當然,對于微服務的調用,不僅僅是Web應用的調用,還有其他如App應用、微信公眾號或小程序客戶端,或者其他語言的設計、異構環境的調用,等。不管使用哪種工具來設計,只要能用HTTP,就可以輕易實現對微服務的調用。

          總體測試

          在類目管理的微服務接口及其Web微服務應用都開發完成之后,我們就可以進行一個總體測試了。首先確認Consul已經運行就緒,然后先后啟動catalog-restapi和 catalog-web兩個模塊。啟動成功之后,通過瀏覽器訪問如下鏈接地址:

          http://localhost:8091

          如果一切正常,則可以進入如圖6-6所示的類目管理的主頁。在這里,我們可以分別對主類和二級分類中的所有類目進行增刪改查的所有操作。

          有關項目的打包與部署

          在使用IDEA開發工具執行打包時,可以使用 Maven項目管理器執行打包操作,如圖6-7所示。

          如果是模塊化的項目,請務必在項目的根(root)目錄中執行打包操作,這樣才能將其所依賴的模塊同時打包在一起。

          當打包完成之后,可以使用命令終端,分別切換到catalog-restapi和 catalog-web模塊的 target目錄中執行下列命令,啟動應用進行調試:

          java -jar catalog*.jar

          以這種方式啟動應用,與上面使用IDEA工具進行調試時的效果是一樣的。如果啟動正常,則可以進行與上面一樣的測試。

          這種啟動方式也可以作為一種普通的方式來發布微服務,在生產環境中,可以在上面指令的基礎上增加一些內存和日志存儲方面的參數。

          有關微服務應用的部署,將在運維部署部分進行詳細介紹。

          小結

          本章介紹了電商平臺的類目管理接口和Web類目管理后臺兩個微服務的開發實例,通過這個項目的開發和演示,我們清楚了微服務之間快速通信和相互調用的方法。在類目管理接口開發中,我們通過Spring Data JPA開發工具,了解了DDD開發方法在Spring 開發框架中的工作原理和實現方法。通過類目管理接口的實現,我們將有狀態的數據訪問行為,轉變成沒有狀態的接口服務。

          下一章,我們將介紹另一種數據庫開發工具 MyBatis,體驗不同的數據庫開發工具在Spring項目工程中的應用方法。

          本文給大家講解的內容是SpringCloud微服務架構實戰:類目管理微服務開發

          1. 下篇文章給大家講解的是SpringCloud微服務架構實戰:庫存管理與分布式文件系統;
          2. 覺得文章不錯的朋友可以轉發此文關注小編;
          3. 感謝大家的支持!

          擊“了解更多”獲取工具

          DevExpress WinForms Subscription擁有180+組件和UI庫,能為Windows Forms平臺創建具有影響力的業務解決方案。DevExpress WinForms能完美構建流暢、美觀且易于使用的應用程序,無論是Office風格的界面,還是分析處理大批量的業務數據,它都能輕松勝任!

          DevExpress Winforms v20.2日前全新發布,此版本加強了Scheduler、Spreadsheet控件功能,歡迎下載最新版體驗!

          Year View

          WinForms Scheduler控件附帶Year View顯示選項,它旨在可視化跨越數天和數周的事件/約會。

          Year View包含“ MonthCount”屬性,其他與視圖相關的設置與Month View設置相同。

          下拉日歷和視圖選擇器

          在日期導航欄中添加了兩個新的UI元素。

          • Dropdown Calendar

          • View Selector

          這兩個UI元素最初都是隱藏的,激活DateNavigationBar.CalendarButton和DateNavigationBar.ShowViewSelectorButton選項以使其可見。

          在Timeline View中新的單元格自動高度模式

          新版本將CellsAutoHeightOptions.Enabled屬性重命名為AutoHeightMode,AutoHeightMode屬性接受枚舉值,而不是布爾值。'None'和 'Limited' 對應于'false' 和 'true',第三個值 - “ Full”- 激活新的AutoHeight模式。

          使用AutoHeight時,時間單元將忽略ResourcesPerPage屬性值,并根據內容調整大小,這還允許用戶對Timeline View進行像素滾動。

          Spreadsheet

          Excel 2016圖表(CTP)

          WinForms Spreadsheet控件現在支持以下Excel 2016圖表類型:

          • Box & Whisker
          • Funnel
          • Histogram
          • Waterfall
          • Pareto

          全面的Spreadsheet API可讓您根據需要創建和編輯Excel 2016圖表,WinForms Spreadsheet控件可以使用Excel 2016圖表打開、打印和導出(導出為PDF)現有工作簿。

          其他

          HTML格式

          現在,您可以使用以下標準HTML標記來格式化字符串:

          • <a> - 在標題、工具提示、標簽等中插入超鏈接。
            <a href=https://www.devexpress.com>www.devexpress.com</a>
            要響應對鏈接的單擊,請處理控件的HyperlinkClick事件。
          • <br> - 插入換行符,您可以使用此標記在控件中顯示多行文本,您也可以使用<br/>語法。

          大多數控件現在都支持<image>標簽,要指定圖像集合,請使用控件的“ HtmlImages”屬性。

          疊加層 - 支持DirectX

          疊加層現在支持DirectX硬件加速,現在動畫在高分辨率顯示器上的呈現更加流暢(并且內存使用效率更高)。

          MVVM - MessageBox表單樣式

          我們向MessageBoxService類添加一個新的MessageBoxFormStyle屬性,此屬性使您可以指定MessageBox表單的外觀設置。

          C#

          var flyoutMsgService = MessageBoxService.CreateFlyoutMessageBoxService();
          flyoutMsgService.MessageBoxFormStyle = (form) => {
          FlyoutDialog msgFrm = form as FlyoutDialog;
          msgFrm.Properties.AppearanceButtons.FontStyleDelta = FontStyle.Bold;
          };
          mvvmContext1.RegisterService(flyoutMsgService);

          Docking - 浮動面板始終位于頂部

          浮動面板和DocumentManager文檔(在Tabbed和Widget Views中)可以顯示在其父窗體的上方或下方。 以下新選項使您始終可以將浮動窗口置于最上方:

          • DockingOptions.FloatPanelsAlwaysOnTop
          • BaseView.FloatDocumentsAlwaysOnTop
          • BarAndDockingController.DockingOptions.FloatWindowsAlwaysOnTop

          如果單獨使用浮動窗口(禁用了FloatPanelsAlwaysOnTop選項),它將顯示Minimize按鈕,該按鈕會將窗口折疊到Windows任務欄。 要隱藏Minimize按鈕,請禁用ShowMinimizeButton選項。


          主站蜘蛛池模板: 激情内射亚州一区二区三区爱妻| 精品人妻一区二区三区浪潮在线| 国产激情视频一区二区三区| 女人18毛片a级毛片一区二区| 久久精品国产AV一区二区三区| 亚洲AV无码一区二区二三区入口| 国产精品被窝福利一区 | 国产一区二区三区电影| 国产吧一区在线视频| 日本伊人精品一区二区三区| 亚洲视频一区在线| 国产成人久久一区二区三区 | 成人国内精品久久久久一区| 香蕉视频一区二区三区| 久久精品动漫一区二区三区| 国产91大片精品一区在线观看| 无码视频免费一区二三区| 精品免费久久久久国产一区| 日韩免费视频一区二区| 无码精品一区二区三区| 色综合视频一区二区三区| 一区二区三区高清在线 | 多人伦精品一区二区三区视频| 亚洲av成人一区二区三区观看在线| 日韩精品一区二区三区中文3d| 久久国产高清一区二区三区| 亚洲AV无码一区二区三区在线| 日韩精品国产一区| 亚洲天堂一区二区三区| 无码人妻一区二区三区在线水卜樱| 无码一区二区三区免费| 亚洲AⅤ无码一区二区三区在线| 精品国产一区二区三区不卡| 中日韩一区二区三区| 波多野结衣中文字幕一区| 久久亚洲一区二区| 99久久精品国产免看国产一区| 尤物精品视频一区二区三区| 亚洲一区二区三区高清| 在线精品动漫一区二区无广告| 精品国产日韩一区三区|