整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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选项。