Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
節(jié)將詳細(xì)介紹es Search API的查詢主體,定制化查詢條件的實(shí)現(xiàn)主體。
搜索請求體中查詢條件使用es DSL查詢語法來定義。通過使用query來定義查詢體。
1GET /_search
2{
3 "query" : {
4 "term" : { "user" : "kimchy" }
5 }
6}
es的一種分頁語法。通過使用from和size參數(shù)來對結(jié)果集進(jìn)行分頁。
from設(shè)置第一條數(shù)據(jù)的偏移量。size設(shè)置返回的條數(shù)(針對每個(gè)分片生效),由于es天生就是分布式的,通過設(shè)置主分片個(gè)數(shù)來進(jìn)行數(shù)據(jù)水平切分,一個(gè)查詢請求通常需要從多個(gè)后臺(tái)節(jié)點(diǎn)(分片)進(jìn)行數(shù)據(jù)匯聚。
From/Size方式會(huì)遇到分布式存儲(chǔ)的一個(gè)共性問題:深度分頁,也就是頁數(shù)越大需要訪問的數(shù)據(jù)則越大。es提供了另外一種分頁方式,滾動(dòng)API(Scroll),后續(xù)會(huì)詳細(xì)分析。
注意:from + size 不能超過index.max_result_window配置項(xiàng)的值,其默認(rèn)值為10000。
與傳統(tǒng)關(guān)系型數(shù)據(jù)庫類似,es支持根據(jù)一個(gè)或多個(gè)字段進(jìn)行排序,同時(shí)支持asc升序或desc降序。另外es可以按照_score(基于得分)的排序,默認(rèn)值。
如果使用了排序,響應(yīng)結(jié)果中每一條命中數(shù)據(jù)將包含一個(gè)響應(yīng)字段sort,其類型為Object[],表示該文檔當(dāng)前的排序值,該值在ES支持的第三種分頁方式Search After中會(huì)使用到。
es提供了兩種排序順序,SortOrder.ASC(asc)升序、SortOrder.DESC(desc)降序,如果排序類型為_score,其默認(rèn)排序順序?yàn)榻敌?desc),如果排序類型為字段,則默認(rèn)排序順序?yàn)樯?asc)。
es支持按數(shù)組或多值字段進(jìn)行排序。模式選項(xiàng)控制選擇的數(shù)組值,以便對它所屬的文檔進(jìn)行排序。模式選項(xiàng)可以有以下值:
如果是一個(gè)數(shù)組類型的值參與排序,通常會(huì)對該數(shù)組元素進(jìn)行一些計(jì)算得出一個(gè)最終參與排序的值,例如取平均數(shù)、最大值、最小值、求和等運(yùn)算。es通過排序模型mode來指定。
es還支持在一個(gè)或多個(gè)嵌套對象內(nèi)部的字段進(jìn)行排序。一個(gè)嵌套查詢提包含如下選項(xiàng)(參數(shù)):
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:排序字段名,支持級聯(lián)表示字段名。代碼@2:通過nested屬性定義排序嵌套語法,其中path定義當(dāng)前的嵌套層級,filter定義過濾上下文。@3內(nèi)部可以再通過nested屬性再次嵌套定義。
由于es的索引,類型下的字段可以在索引文檔時(shí)動(dòng)態(tài)增加,那如果有些文檔沒有包含排序字段,這部分文檔的順序如何確定呢?es通過missing屬性來確定,其可選值為:
默認(rèn)情況下,如果排序字段為未映射的字段將拋出異常。可通過unmapped_type來忽略該異常,該參數(shù)指定一個(gè)類型,也就是告訴ES如果未找該字段名的映射,就認(rèn)為該字段是一個(gè)unmapped_type指定的類型,所有文檔都未存該字段的值。
地圖類型排序,該部分將在后續(xù)專題介紹geo類型時(shí)講解。
默認(rèn)情況下,對命中的結(jié)果會(huì)返回_source字段下的所有內(nèi)容。字段過濾機(jī)制允許用戶按需要返回_source字段里面部分字段。其過濾設(shè)置機(jī)制已在Elasticsearch Document Get API詳解、原理與示例中已詳細(xì)介紹,在這里就不重復(fù)介紹了。
使用方式如下:
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指定需要轉(zhuǎn)換的字段與格式,它對于在映射文件中定義stored=false的字段同樣生效。字段支持用通配符,例如"field":"myfield*"。
docvalue_fields中指定的字段并不會(huì)改變_souce字段中的值,而是使用fields返回值進(jìn)行額外返回。
java實(shí)例代碼片段如下(完整的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")
其返回結(jié)果如下:
{
"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對查詢條件命中后的文檔再做一次篩選。
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}
首先根據(jù)@1條件對索引進(jìn)行檢索,然后得到匹配的文檔后,再利用@2過濾條件對結(jié)果再一次篩選。
查詢結(jié)果高亮顯示。
用于對查詢結(jié)果中對查詢關(guān)鍵字進(jìn)行高亮顯示,高亮顯示查詢條件在查詢結(jié)果中匹配的部分。
注意:高亮顯示器在提取要高亮顯示的術(shù)語時(shí)不能反映查詢的布爾邏輯。因此對于一些復(fù)雜的布爾查詢(例如嵌套的布爾查詢,或使用minimum_should_match等查詢)可能高亮顯示會(huì)出現(xiàn)一些誤差。
高亮顯示需要字段的實(shí)際內(nèi)容。如果字段沒有存儲(chǔ)(映射沒有將store設(shè)置為true),則從_source中提取相關(guān)字段。
es支持三種高亮顯示工具,通過為每個(gè)字段指定type來使用。
獲取偏移量策略。高亮顯示要解決的一個(gè)核心就是高亮顯示的詞根以及該詞根的位置(位置與偏移量)。
ES中提供了3中獲取偏移量信息(Offsets)的策略:
高亮顯示的全局配置會(huì)被字段級別的覆蓋。
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再做一次說明,其中每一個(gè)字段返回的內(nèi)容是對應(yīng)原始數(shù)據(jù)的子集,最多fragmentSize個(gè)待關(guān)鍵字的匹配條目,通常,在頁面上顯示文本時(shí),應(yīng)該用該字段取代原始值,這樣才能有高亮顯示的效果。
重打分機(jī)制。一個(gè)查詢首先使用高效的算法查找文檔,然后對返回結(jié)果的top n 文檔運(yùn)用另外的查詢算法,通常這些算法效率低效但能提供匹配精度。resoring查詢與原始查詢分?jǐn)?shù)的合計(jì)方式如下:
查詢類型,默認(rèn)值為query_then_fetch。
滾動(dòng)查詢。es另外一種分頁方式。雖然搜索請求返回結(jié)果的單個(gè)頁面,但scroll API可以用于從單個(gè)搜索請求檢索大量結(jié)果(甚至所有結(jié)果),這與在傳統(tǒng)數(shù)據(jù)庫上使用游標(biāo)的方式非常相似。
scroll api不用于實(shí)時(shí)用戶請求,而是用于處理大量數(shù)據(jù),例如為了將一個(gè)索引的內(nèi)容重新索引到具有不同配置的新索引中。
scroll API使用分為兩步:
1、第一步,首先通過scroll參數(shù),指定該滾動(dòng)查詢(類似于數(shù)據(jù)庫的游標(biāo)的存活時(shí)間)
1POST /twitter/_search?scroll=1m
2{
3 "size": 100,
4 "query": {
5 "match" : {
6 "title" : "elasticsearch"
7 }
8 }
9}
該方法會(huì)返回一個(gè)重要的參數(shù)scrollId。
2、第二步,使用該scrollId去es服務(wù)器拉取下一批(下一頁數(shù)據(jù))
1POST /_search/scroll
2{
3 "scroll" : "1m",
4 "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
5}
循環(huán)第三步,可以循環(huán)批量處理數(shù)據(jù)。
3、第三步,清除scrollId,類似于清除數(shù)據(jù)庫游標(biāo),快速釋放資源。
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) { //循環(huán)遍歷
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 }
這里重點(diǎn)闡述一下第一次查詢時(shí),不僅返回scrollId,也會(huì)返回第一批數(shù)據(jù)。
scroll參數(shù)(傳遞給搜索請求和每個(gè)滾動(dòng)請求)告訴es它應(yīng)該保持搜索上下文活動(dòng)多長時(shí)間。只需要足夠長的時(shí)間來處理前一批結(jié)果。
每個(gè)scroll請求(帶有scroll參數(shù))設(shè)置一個(gè)新的過期時(shí)間。如果scroll請求沒有傳入scroll,那么搜索上下文將作為scroll請求的一部分被釋放。
scroll其內(nèi)部實(shí)現(xiàn)類似于快照,當(dāng)?shù)谝淮问盏揭粋€(gè)scroll請求時(shí),就會(huì)為該搜索上下文所匹配的結(jié)果創(chuàng)建一個(gè)快照,隨后文檔的變化并不會(huì)反映到該API的結(jié)果。
對于返回大量文檔的scroll查詢,可以將滾動(dòng)分割為多個(gè)可以獨(dú)立使用的片,通過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兩個(gè)并列的查詢,按分片去查詢。
@11:通過slice定義分片查詢。
@12:該分片查詢的ID。
@13:本次查詢總片數(shù)。
這個(gè)機(jī)制非常適合多線程處理數(shù)據(jù)。
具體分片機(jī)制是,首先將請求轉(zhuǎn)發(fā)到各分片節(jié)點(diǎn),然后在每個(gè)節(jié)點(diǎn)使用匹配到的文檔(hashcode(_uid)%slice片數(shù)),然后各分片節(jié)點(diǎn)返回?cái)?shù)據(jù)到協(xié)調(diào)節(jié)點(diǎn)。也就是默認(rèn)情況下,分片是根據(jù)文檔的_uid,為了提高分片過程,可以通過如下方式進(jìn)行優(yōu)化,并指定分片字段。
注意,默認(rèn)slice片數(shù)最大為1024,可以通過索引設(shè)置項(xiàng)index.max_slices_per_scroll來改變默認(rèn)值。例如:
從本章開始,我們將根據(jù)電商平臺(tái)的各個(gè)實(shí)例項(xiàng)目進(jìn)行具體的微服務(wù)開發(fā),主要包括類目管理、庫存管理、訂單管理等。在這幾個(gè)實(shí)例項(xiàng)目中,我們將根據(jù)項(xiàng)目本身的特點(diǎn),使用不同的數(shù)據(jù)庫進(jìn)行開發(fā)。對于類目管理來說,我們將使用二級分類設(shè)計(jì),即數(shù)據(jù)實(shí)體之間存在一定的關(guān)聯(lián)關(guān)系,因此最好的選擇就是使用Spring Data JPA進(jìn)行開發(fā)。Spring Data JPA是Spring Boot開發(fā)框架中一個(gè)默認(rèn)推薦使用的數(shù)據(jù)庫開發(fā)方法,同時(shí),JPA 也是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的一種具體應(yīng)用。
本章的項(xiàng)目工程可以通過本文的源代碼在IDEA中使用Git檢出。該項(xiàng)目由三個(gè)模塊組成:
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain-Driven Design,DDD)是一種面向?qū)ο蠼#詷I(yè)務(wù)模型為核心展開的軟件開發(fā)方法。面向?qū)ο蠼5脑O(shè)計(jì)方法,相比于面向過程和面向數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)方法,從根本上解耦了系統(tǒng)分析與系統(tǒng)設(shè)計(jì)之間相互隔離的狀態(tài),從而提高了軟件開發(fā)的工作效率。
我們將使用JPA來實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的開發(fā)方法。JPA通過實(shí)體定義建立了領(lǐng)域業(yè)務(wù)對象的數(shù)據(jù)模型,然后通過使用存儲(chǔ)庫賦予實(shí)體操作行為,從而可以快速進(jìn)行領(lǐng)域業(yè)務(wù)功能的開發(fā)。
DDD的分層結(jié)構(gòu)
DDD將系統(tǒng)分為用戶接口層、應(yīng)用層、領(lǐng)域?qū)雍突A(chǔ)設(shè)施層,如圖6-1所示。
應(yīng)用層是很薄的一層,負(fù)責(zé)接收用戶接口層傳來的參數(shù)和路由到對應(yīng)的領(lǐng)域?qū)樱到y(tǒng)的業(yè)務(wù)邏輯主要集中在領(lǐng)域?qū)又校灶I(lǐng)域?qū)釉谙到y(tǒng)架構(gòu)中占據(jù)了很大的面積。上下層之間應(yīng)該通過接口進(jìn)行通信,這樣接口定義的位置就決定了上下層之間的依賴關(guān)系。
DDD的基本元素
DDD的基本元素有Entity、Value Object、Service、Aggregate、Repository、Factory、DomainEvent和Moudle等。
JPA(Java Persistence API)即Java持久層API,是Java持久層開發(fā)的接口規(guī)范。Hibernate、TopLink和 OpenJPA等ORM框架都提供了JPA的實(shí)現(xiàn)。Spring Data JPA 的實(shí)現(xiàn)使用了Hibernate框架,所以在設(shè)計(jì)上與直接使用 Hibernate差別不大。但JPA 并不等同于Hibernate,它是在Hibernate之上的一個(gè)通用規(guī)范。
接下來,我們通過模塊catalog-restapi來說明Spring Data JPA的開發(fā)方法。
Druid數(shù)據(jù)源配置
Druid是阿里巴巴開源的一個(gè)數(shù)據(jù)源服務(wù)組件,不僅具有很好的性能,還提供了監(jiān)控和安全過濾的功能。
我們可以創(chuàng)建一個(gè)配置類DruidConfiguration來啟用Druid 的監(jiān)控和過濾功能,代碼如下所示:
@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地址黑名單(共同存在時(shí),deny優(yōu)先于allow)
servletRegistrationBean.addInitParameter ("deny", "192.168.110.100");//控制臺(tái)管理用戶
servletRegistrationBean.addInitParameter ("loginUsername" , "druid");servletRegistrationBean.addInitParameter ("loginPassword","12345678");//是否能夠重置數(shù)據(jù)
servletRegistrationBean.addInitParameter("resetEnable" ,"false");return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean statFilter(){
FilterRegistrationBean filterRegistrationBean = new
FilterRegistrationBean (new webStatFilter());
//添加過濾規(guī)則
filterRegistrationBean.addUrlPatterns("/*");//忽略過濾的格式
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,* .jpg,* .png,*.Css,* .ico,/druid/* ")F
return filterRegistrationBean;
}
}
在使用這個(gè)監(jiān)控配置后,當(dāng)應(yīng)用運(yùn)行時(shí),例如,我們啟動(dòng)catalog-restapi模塊,即可通過下列鏈接打開監(jiān)控控制臺(tái)頁面:
http://localhost:9095/druid
在登錄認(rèn)證中輸入前面代碼中配置的用戶和密碼“druid/12345678”,即可打開如圖6-2所示的操作界面。注意,本地的P地址不在前面代碼設(shè)置的黑名單之中。
在使用這個(gè)監(jiān)控控制臺(tái)之后,通過查看“SQL監(jiān)控”的結(jié)果,即可為我們對應(yīng)用的SQL設(shè)計(jì)和優(yōu)化提供有價(jià)值的參考依據(jù)。
我們可以使用項(xiàng)目中的配置文件 application.yml 來設(shè)置Druid連接數(shù)據(jù)源,代碼如下所示:
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
#配置獲取連接等待超時(shí)的時(shí)間maxwait: 60000
#配置間隔多久進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是mstimeBetweenEvictionRunsMillis: 60000
#配置一個(gè)連接在池中最小生存時(shí)間,單位是msminEvictableIdleTimeMillis:300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: true
testOnBorrow: falsetestOnReturn: false
#打開PSCache,指定每個(gè)連接上 PSCache的大小poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
#配置監(jiān)控統(tǒng)計(jì)攔截的filters,如果去掉,則監(jiān)控界面SQL將無法統(tǒng)計(jì), 'wall'用于防火墻filters: stat, wall, log4j
#通過connectProperties屬性打開mergeSql功能;慢sQL記錄connectionProperties:
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
在上面的配置中,主要設(shè)定了所連接的數(shù)據(jù)庫,以及數(shù)據(jù)庫的用戶名和密碼,確保數(shù)據(jù)庫的用戶配置信息正確,并且具有讀寫權(quán)限。其他一些配置可由 Druid 的通用參數(shù)來設(shè)定。
數(shù)據(jù)源的配置同時(shí)適用于MyBatis的開發(fā)。
JPA 初始化和基本配置
首先,我們新建一個(gè)配置類JpaConfiguration,初始化一些JPA的參數(shù),代碼如下所示:
econfiguration
@EnableTransactionManagement (proxyTargetClass = true)
@EnableJpaRepositories (basePackages = "com.** .repository")CEntityScan(basePackages = "com.* *.entity")
public class JpaConfiguration{
@Bean
PersistenceExceptionTranslationPostProcessorpersistenceExceptionTranslationPostProcessor(){
return new PersistenceExceptionTranslationPostProcessor();
}
}
在這里,我們設(shè)置存儲(chǔ)庫的存放位置為“com.**.repository”,同時(shí)設(shè)置實(shí)體的存放位置為“com.**.entity”,這樣就能讓JPA找到我們定義的存儲(chǔ)庫和實(shí)體對象了。
然后,在應(yīng)用程序的配置文件中,增加如下配置:
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”設(shè)置為“update”,表示當(dāng)實(shí)體屬性更改時(shí),將會(huì)更新表結(jié)構(gòu)。如果表結(jié)構(gòu)不存在,則創(chuàng)建表結(jié)構(gòu)。注意,不要把“ddl-auto”設(shè)置為“create”,否則程序每次啟動(dòng)時(shí)都會(huì)重新創(chuàng)建表結(jié)構(gòu),而之前的數(shù)據(jù)也會(huì)丟失。如果不使用自動(dòng)功能,則可以設(shè)置為“none”。上面配置中的最后一行代碼開啟了Hibernate的延遲加載功能,這可以提高關(guān)聯(lián)關(guān)系查詢時(shí)的訪問性能。
在使用Spring Data JPA進(jìn)行實(shí)體建模時(shí),主要使用Hibernate的對象關(guān)系映射(ORM)來實(shí)現(xiàn)。在類目管理項(xiàng)目中我們需要?jiǎng)?chuàng)建兩個(gè)實(shí)體,分別為主類和二級分類。
主類由名稱、操作者和創(chuàng)建日期等屬性組成,實(shí)現(xiàn)代碼如下所示:
@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”來存儲(chǔ)數(shù)據(jù),并且它與二級分類以一對多的方式建立了關(guān)聯(lián)關(guān)系。建立關(guān)聯(lián)關(guān)系的是“sorts_id”,它將被保存在二級分類的表格中。另外,在查詢這種關(guān)系時(shí),我們指定了以創(chuàng)建時(shí)間“created”進(jìn)行排序。
二級分類實(shí)體由名稱、操作者和創(chuàng)建日期等屬性組成,代碼如下所示:
@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;
...
}
二級分類使用了表結(jié)構(gòu)“t_subsorts”來存儲(chǔ)數(shù)據(jù),字段定義與主類的定義幾乎相同。
在上面兩個(gè)實(shí)體對象的設(shè)計(jì)中,我們通過主類使用一對多的方式與二級分類實(shí)現(xiàn)關(guān)聯(lián)設(shè)計(jì),這樣,當(dāng)在主類中進(jìn)行查詢時(shí),將可以同時(shí)獲取二級分類的數(shù)據(jù);而對主類的存儲(chǔ)和更新,也將自動(dòng)涉及分類的相關(guān)操作。
有關(guān)實(shí)體建模的設(shè)計(jì),特別是關(guān)聯(lián)關(guān)系的設(shè)計(jì),我們主要說明以下幾個(gè)重要的功能。
(1)實(shí)體對象必須有一個(gè)唯一標(biāo)識。
這里使用Long類型定義對象的身份標(biāo)識“id”,并且這個(gè)“id”將由數(shù)據(jù)庫自動(dòng)生成。在實(shí)際應(yīng)用中,推薦使用UUID作為對象的唯一標(biāo)識,這樣不僅可以保持這一字段長度的一致性,還能保證這一標(biāo)識在整個(gè)數(shù)據(jù)庫中的唯一性,而且還將非常有利于數(shù)據(jù)庫的集群設(shè)計(jì)。
(2)日期屬性要使用正確的格式。
使用注解“@DateTimeFormat”對日期進(jìn)行格式化,不僅可以保證日期正常顯示,還能保證在參數(shù)傳遞中日期的正確性。注意,上面的創(chuàng)建日期“created”使用了默認(rèn)值設(shè)置。
(3)使用合理的關(guān)聯(lián)設(shè)置。
關(guān)聯(lián)設(shè)置是實(shí)體設(shè)計(jì)的關(guān)鍵,為了避免引起遞歸調(diào)用,最好使用單向關(guān)聯(lián)設(shè)置,即在互相關(guān)聯(lián)的兩個(gè)對象之中,只在一個(gè)對象中進(jìn)行關(guān)聯(lián)設(shè)置。
一般來說,多對多的關(guān)聯(lián)可以使用中間表來存儲(chǔ)關(guān)聯(lián)關(guān)系,而一對多或多對一的關(guān)聯(lián)關(guān)系可以使用一個(gè)字段來存儲(chǔ)關(guān)聯(lián)對象的外鍵。例如,在上面的實(shí)體設(shè)計(jì)中,我們使用“sorts_id"作為二級分類與主類關(guān)聯(lián)的外鍵。
在主類實(shí)體的關(guān)聯(lián)設(shè)置中,我們還使用了級聯(lián)的操作設(shè)置:“CascadeType.REMOVE”。這樣,當(dāng)主類中的一個(gè)類別被刪除時(shí),將會(huì)自動(dòng)刪除與其關(guān)聯(lián)的所有分類。
有關(guān)級聯(lián)的設(shè)置,可以使用的選項(xiàng)如下所示:
我們將查詢對象設(shè)計(jì)放在一個(gè)公共模塊catalog-object中,這樣,其他兩個(gè)模塊都可以進(jìn)行調(diào)用。使用查詢對象(Query Object,qo)是為了與vo進(jìn)行區(qū)分。有人把vo看成值對象(ValueObject),也有人把vo看成視圖對象(View Object),所以很容易引起誤解。這兩種對象的意義和用途是不一樣的,值對象表示的是與實(shí)體不同的一些數(shù)據(jù),它可以作為視圖顯示;而視圖對象是只能作為視圖顯示的一種數(shù)據(jù)。
因?yàn)閷?shí)體是有生命周期和狀態(tài)的,并且它的狀態(tài)會(huì)直接影響存儲(chǔ)的數(shù)據(jù),所以我們使用一個(gè)無狀態(tài)的數(shù)據(jù)對象來存儲(chǔ)實(shí)體的數(shù)據(jù)。這些數(shù)據(jù)的使用和更改不會(huì)直接影響數(shù)據(jù)存儲(chǔ),因此它的使用是安全的,也可以用之即棄。
我們既可以將查詢對象作為值對象使用,也可以將查詢對象作為視圖對象使用,還可以將查詢對象作為查詢參數(shù)的一個(gè)集合來使用,即相當(dāng)于一個(gè)數(shù)據(jù)傳輸對象(Data Transfer Object, dto)。
我們只要使用一個(gè)查詢對象qo,就可以包含vo、dto等對象的功能,這是一種簡化設(shè)計(jì)。qo有時(shí)會(huì)包含一些冗余數(shù)據(jù),但這對于使用方來說影響不大。例如,在我們的查詢對象中,將會(huì)包含分頁所需的頁碼和頁大小等分頁屬性數(shù)據(jù),而在視圖顯示中并不需要這些數(shù)據(jù),所以它可以不用理會(huì)這些數(shù)據(jù)。
相對于主類實(shí)體,它的查詢對象的設(shè)計(jì)如下所示:
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查詢對象將提供兩個(gè)分頁查詢參數(shù),實(shí)現(xiàn)代碼如下所示:
public class PageQ0 [
private Integer page = 0;private Integer size = 10;
...
}
在分頁參數(shù)中,只有一個(gè)頁碼和每頁大小的設(shè)定兩個(gè)字段。
使用JPA進(jìn)行實(shí)體數(shù)據(jù)持久化設(shè)計(jì)是比較容易的,只要為實(shí)體創(chuàng)建一個(gè)存儲(chǔ)庫接口,將實(shí)體對象與JPA的存儲(chǔ)庫接口進(jìn)行綁定,就可以實(shí)現(xiàn)實(shí)體的數(shù)據(jù)持久化設(shè)計(jì),相當(dāng)于給實(shí)體賦予了一些訪問數(shù)據(jù)庫的操作行為,包括基本的增刪改查等操作。
除數(shù)據(jù)存儲(chǔ)的基本操作外,我們還可以根據(jù)實(shí)體的字段名稱來聲明查詢接口,而對于一些復(fù)雜的查詢,也可以使用SQL查詢語言設(shè)計(jì)。實(shí)體主類的存儲(chǔ)接口設(shè)計(jì)如下所示:
@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);
}
這個(gè)接口定義是不用我們實(shí)現(xiàn)的,只要方法定義符合JPA的規(guī)則,后續(xù)的工作就可以交給JPA來完成。
在JPA中,可以根據(jù)以下方法自定義聲明方法的規(guī)則,即在接口中使用關(guān)鍵字findBy.readBy、getBy等作為方法名的前綴,然后拼接實(shí)體類中的屬性字段(首個(gè)字母大寫),最后拼接一些SQL查詢關(guān)鍵字(也可不拼接),組成一個(gè)查詢方法。下面是一些查詢關(guān)鍵字的使用實(shí)例:
通過注解@Query使用SQL查詢語言設(shè)計(jì)的查詢,基本與數(shù)據(jù)庫的查詢相同,這里只是使用實(shí)體對象的名字代替了數(shù)據(jù)庫表的名字。
在上面的存儲(chǔ)庫接口定義中,我們不但繼承了JPA的基礎(chǔ)存儲(chǔ)庫JpaRepository,還繼承了一個(gè)比較特別的存儲(chǔ)庫JpaSpecificationExecutor,通過這個(gè)存儲(chǔ)庫可以進(jìn)行一些復(fù)雜的分頁設(shè)計(jì)。
前面的持久化設(shè)計(jì)已經(jīng)在實(shí)體與數(shù)據(jù)庫之間建立了存取關(guān)系。為了更好地對外提供數(shù)據(jù)訪問服務(wù),我們需要對存儲(chǔ)庫的調(diào)用再進(jìn)行一次封裝。在這次封裝中,我們可以實(shí)現(xiàn)統(tǒng)一事務(wù)管理及其分頁的查詢設(shè)計(jì)。分類的數(shù)據(jù)管理服務(wù)設(shè)計(jì)代碼如下所示:
@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 實(shí)現(xiàn)了隱式事務(wù)管理,對于一些基本的數(shù)據(jù)操作,可直接調(diào)用存儲(chǔ)庫接口的方法。
在上述代碼中,使用findAll方法實(shí)現(xiàn)了分頁查詢的設(shè)計(jì)。在這個(gè)設(shè)計(jì)中,可以定義排序的方法和字段,以及對頁碼和每頁行數(shù)的設(shè)定,同時(shí),還可以根據(jù)查詢參數(shù)動(dòng)態(tài)地設(shè)置查詢條件。在這里,我們既可以按分類的名稱進(jìn)行模糊查詢,也可以按分類的創(chuàng)建時(shí)間進(jìn)行限定查詢。
在完成上節(jié)的設(shè)計(jì)之后,我們可以寫一個(gè)測試用例驗(yàn)證領(lǐng)域服務(wù)的設(shè)計(jì)。需要注意的是,因?yàn)樵谇懊娴腏PA配置中已經(jīng)有了更新表結(jié)構(gòu)的配置,所以如果表結(jié)構(gòu)不存在,則會(huì)自動(dòng)生成;如果表結(jié)構(gòu)更新,則啟動(dòng)程序也會(huì)自動(dòng)更新。下面的測試用例演示了如何插入分類和主類的數(shù)據(jù):
@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("計(jì)算機(jī)");
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");
}
...
}
其他查詢的測試用例可以參照這個(gè)方法設(shè)計(jì),如果斷言沒有錯(cuò)誤,則說明測試符合預(yù)期,即不會(huì)提示任何錯(cuò)誤信息。在調(diào)試環(huán)境中,還可以借助控制臺(tái)信息分析測試的過程。
類目接口微服務(wù)是一個(gè)獨(dú)立的微服務(wù)應(yīng)用,它將使用基于REST 協(xié)議的方式,對外提供一些有關(guān)類目查詢和類目數(shù)據(jù)管理的接口服務(wù)。這個(gè)接口服務(wù),既可以用于商家后臺(tái)進(jìn)行類目管理的設(shè)計(jì)之中,也可以用于移動(dòng)端、App或其他客戶端程序的設(shè)計(jì)之中。
當(dāng)上面的單元測試完成之后,我們就可以使用上面設(shè)計(jì)中提供的數(shù)據(jù)服務(wù)進(jìn)行類目接口微服務(wù)的開發(fā)了。
RESTful接口開發(fā)
我們將遵循REST協(xié)議的規(guī)范設(shè)計(jì)基于RESTful的接口開發(fā),例如,對于分類來說,我們可以設(shè)計(jì)如下請求:
下面的代碼展示了分類接口設(shè)計(jì)的部分實(shí)現(xiàn),完整的代碼可以查看項(xiàng)目工程的相關(guān)源代碼:
@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<>();//轉(zhuǎn)換每個(gè)分類,然后加入主類的分類列表中
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;
}
...
}
在上面微服務(wù)接口設(shè)計(jì)中,使用RestController 定義了對外提供服務(wù)的URL 接口,而接口之中有關(guān)數(shù)據(jù)的訪問則通過調(diào)用SortsService的各種方法來實(shí)現(xiàn)。其中,在接口調(diào)用中,都使用JSON方式的數(shù)據(jù)結(jié)構(gòu)來傳輸數(shù)據(jù),所以在上面代碼中,顯式或隱式地使用了JSON 的數(shù)據(jù)結(jié)構(gòu)。對于一個(gè)數(shù)據(jù)對象來說,為了保證其數(shù)據(jù)的完整性,我們一般使用GSON 工具對數(shù)據(jù)進(jìn)行顯式轉(zhuǎn)換。
需要注意的是,因?yàn)樵跀?shù)據(jù)傳輸中使用的是查詢對象,所以當(dāng)進(jìn)行數(shù)據(jù)保存和更新操作時(shí),需要將查詢對象轉(zhuǎn)換為實(shí)體對象。
微服務(wù)接口調(diào)試
當(dāng)微服務(wù)接口開發(fā)完成之后,即可啟動(dòng)項(xiàng)目的應(yīng)用程序進(jìn)行簡單調(diào)試。對于類目微服務(wù)接口,我們可以啟動(dòng)catalog-restapi模塊中的主程序SortsRestApiApplication進(jìn)行調(diào)試。
在啟動(dòng)成功之后,對于一些GET請求,可以直接通過瀏覽器進(jìn)行調(diào)試。
例如,通過下列鏈接地址,可以根據(jù)分類ID查看一個(gè)分類的信息:
http://localhost:9091/sorts/1
如果數(shù)據(jù)存在,則返回如圖6-3所示的JSON數(shù)據(jù)。
使用如下鏈接地址可以查詢分頁第一頁的數(shù)據(jù):
http://localhost:9091/sorts
如果查詢成功,則可以看到如圖6-4所示的信息。
因?yàn)镻OST 和 PUT等請求在調(diào)試時(shí)需要傳輸參數(shù),所以不能直接使用瀏覽器進(jìn)行測試,但是可以通過Postman等工具進(jìn)行調(diào)試。
我們可以使用多種方法調(diào)用基于RESTful接口的服務(wù)。例如,可以使用HTTP 訪問(例如HttpClient),或者使用RestTemplate的方式進(jìn)行調(diào)用,等等。但是,在微服務(wù)應(yīng)用中,最好的方法是使用聲明式的FeignClient。
因?yàn)?FeignClient是為其他微服務(wù)進(jìn)行調(diào)用的,所以這里將這些設(shè)計(jì)都放在模塊catalog-object中進(jìn)行開發(fā)。
聲明式FeignClient 設(shè)計(jì)
FeignClient是一個(gè)聲明式的客戶端,為了使用這個(gè)工具組件,我們需要在項(xiàng)目對象模型中引入 FeignClient的依賴,代碼如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
針對主類的接口調(diào)用,我們可以定義一個(gè)接口程序SortsClient,根據(jù)微服務(wù)catalogapi提供的接口服務(wù),使用如下所示的方法聲明一些調(diào)用方法:
@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);
}
在這個(gè)實(shí)現(xiàn)代碼中,首先通過注解@FeignClient引用微服務(wù)catalogapi,然后使用其暴露出來的URL直接聲明調(diào)用方法。需要注意的是,這里的數(shù)據(jù)傳輸,即數(shù)據(jù)的生產(chǎn)和消費(fèi),都是通過JSON格式進(jìn)行的,所以為了保證中文字符的正確性,我們使用UTF8編碼。
斷路器的使用
基于SortsClient的聲明方法,我們可以創(chuàng)建一個(gè)服務(wù)類SortsRestService進(jìn)行調(diào)用。然后,使用SortsRestService提供的功能,就可以像使用本地方法一樣使用微服務(wù)catalogapi 提供的接口方法。服務(wù)類SortsRestService的實(shí)現(xiàn)代碼如下所示:
@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);
}
...
}
在上面的代碼中,我們實(shí)現(xiàn)了對SortsClient的調(diào)用,同時(shí)增加了一個(gè)注解@HystrixCommand。通過這個(gè)注解,定義了一個(gè)回退方法。而這一回退方法的設(shè)計(jì),就是SpringCloud組件提供的斷路器功能的實(shí)現(xiàn)方法。斷路器的含義是,當(dāng)服務(wù)調(diào)用過載或不可用時(shí),通過降級調(diào)用或故障轉(zhuǎn)移的方法,減輕服務(wù)的負(fù)載。這里我們使用了回退方法設(shè)計(jì),以快速響應(yīng)來自客戶端的訪問,并保障客戶端對微服務(wù)的訪問不會(huì)因?yàn)槌霈F(xiàn)故障而崩潰。斷路器的設(shè)計(jì)就像電路的保護(hù)開關(guān)一樣,對系統(tǒng)服務(wù)起到一定的保護(hù)作用。與保護(hù)開關(guān)不同的是,當(dāng)系統(tǒng)恢復(fù)正常時(shí),斷路器會(huì)自動(dòng)失效,不用人為干預(yù)。
這里 類目管理是一個(gè)基于 PC 端的 Web 應(yīng)用,它也是一個(gè)獨(dú)立的微服務(wù)應(yīng)用。這個(gè)應(yīng)用在項(xiàng)目工程的模塊catalog-web 中實(shí)現(xiàn),可以把它看成一個(gè)獨(dú)立的項(xiàng)目。
在這個(gè)應(yīng)用中,我們將演示如何使用類目管理微服務(wù)接口提供的服務(wù),進(jìn)行相關(guān)應(yīng)用功能的開發(fā),從而實(shí)現(xiàn)在PC端提供一個(gè)對類目進(jìn)行操作管理的友好操作界面。
接口調(diào)用引用的相關(guān)配置
上面的接口調(diào)用服務(wù)是在模塊catalog-object 中進(jìn)行開發(fā)的,想要在模塊“catalog-web”中使用這些服務(wù),就必須先在項(xiàng)目對象模型中進(jìn)行引用配置,代碼如下所示:
<dependency>
<groupId>com.demo</groupId>
<artifactId>catalog-object</artifactId><version>${project.version}</version></dependency>
因?yàn)閮蓚€(gè)模塊處于同一個(gè)項(xiàng)目工程之中,所以上面引用配置的版本直接使用了項(xiàng)目的版本。這樣,當(dāng)接口服務(wù)啟動(dòng)之后,我們就可以在接下來的 Web應(yīng)用中進(jìn)行相關(guān)調(diào)用了。
需要注意的是,如果有多個(gè)FeignClient程序調(diào)用了同一個(gè)微服務(wù)接口服務(wù),則必須在項(xiàng)目的配置文件中使用如下所示的配置進(jìn)行設(shè)置,以支持這種調(diào)用方式。因?yàn)檫@個(gè)Spring Cloud版本的默認(rèn)配置是不開啟這種調(diào)用方式的:
#允許多個(gè)接口使用相同的服務(wù)
spring:
main:
allow-bean-definition-overriding: true
Spring MVC控制器設(shè)計(jì)
Spring MVC是 Web應(yīng)用開發(fā)的一個(gè)基礎(chǔ)組件,下面我們使用這一組設(shè)計(jì)一個(gè)控制器。在Web應(yīng)用的主類控制器設(shè)計(jì)中,我們直接使用上面設(shè)計(jì)的服務(wù)類:SortsRestService。我們可以像使用本地方法一樣使用SortsRestService類,直接調(diào)用微服務(wù)提供的接口服務(wù),代碼如下所示:
@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方法是一個(gè)使用頁面來顯示分類信息的設(shè)計(jì)。在這個(gè)設(shè)計(jì)中,一方面引用了上面設(shè)計(jì)的服務(wù)類SortsRestService,并調(diào)用了它的findByld 方法,進(jìn)行數(shù)據(jù)查詢;另一方面將查詢數(shù)據(jù)通過一個(gè) show頁面顯示出來。這個(gè)設(shè)計(jì)與一般的本地調(diào)用不同的是,查詢數(shù)據(jù)時(shí)得到的返回值是一種ISON結(jié)構(gòu),所以必須將它轉(zhuǎn)化為一個(gè)查詢對象,這樣才能方便使用。
接下來的頁面設(shè)計(jì)將會(huì)用到Thymeleaf模板的功能。
在 Web應(yīng)用的頁面設(shè)計(jì)中,我們將使用Thymeleaf 這個(gè)模板,因此,必須在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>
有關(guān) Thymeleaf 的配置,使用其默認(rèn)配置即可,即只要在程序的資源目錄中有static和templates這兩個(gè)目錄就可以了。這兩個(gè)目錄分別用來存放靜態(tài)文件和模板設(shè)計(jì)及其頁面設(shè)計(jì)文件,頁面文件的后綴默認(rèn)使用html。
HTML頁面設(shè)計(jì)
在6.10節(jié)控制器的設(shè)計(jì)中,類目信息輸出的是一個(gè)show頁面,它的設(shè)計(jì)在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特有的地方外,其他設(shè)計(jì)都與一般的HTML標(biāo)簽語言相同。設(shè)計(jì)之后,這個(gè)頁面的最終效果如圖6-5所示。
統(tǒng)一風(fēng)格模板設(shè)計(jì)
Thymeleaf更強(qiáng)大的功能是提供了一個(gè)統(tǒng)一風(fēng)格的模板設(shè)計(jì),即整個(gè)網(wǎng)站可以使用統(tǒng)一風(fēng)格的框架結(jié)構(gòu)。在類目管理這個(gè)項(xiàng)目中,使用了總體頁面框架設(shè)計(jì)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">
當(dāng)前位置:首頁 > <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>
頁面上方是狀態(tài)欄,頁面左側(cè)是導(dǎo)航欄,中間部分是內(nèi)容顯示區(qū)域,底端還有一個(gè)頁腳設(shè)計(jì)。在引用這個(gè)模板之后,只需對需要更改的區(qū)域進(jìn)行覆蓋就可以了,而不需要更改的地方使用模板的默認(rèn)設(shè)計(jì)即可。一般來說,在使用這個(gè)模板時(shí),只要更改狀態(tài)欄和內(nèi)容顯示區(qū)域就可以了,而導(dǎo)航欄和頁腳,則可以使用通用的頁面設(shè)計(jì)。
在這個(gè)例子中,分類的主頁是通過index.html這個(gè)頁面設(shè)計(jì)來引用這個(gè)模板的,代碼如下所示:
<! DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
layout:decorator="fragments/layout">
<body>
<!--狀態(tài)欄-->
<div class="locationLine" layout: fragment="prompt">
當(dāng)前位置:首頁> <em >類目管理</em>
</div>
<!--主要內(nèi)容區(qū)域-->
<div class="statisticBoX w-782"layout:fragment="content">
...
</div></body></html>
可以看出,在上面的代碼中,我們只更新了狀態(tài)欄和主要內(nèi)容顯示區(qū)域的設(shè)計(jì),其他部分都沿用了模板的設(shè)計(jì)。
在上面的一些設(shè)計(jì)講解和演示中,我們只說明了主類的設(shè)計(jì),二級分類的設(shè)計(jì)與主類的設(shè)計(jì)大同小異,不再贅述。
至此,類目管理的微服務(wù)應(yīng)用的開發(fā)工作就基本完成了。
現(xiàn)在我們可以體驗(yàn)微服務(wù)之間的調(diào)用了,因?yàn)槭褂昧薙pring Cloud工具組件來開發(fā),所以在各個(gè)方面的實(shí)現(xiàn)都是非常方便的。當(dāng)然,對于微服務(wù)的調(diào)用,不僅僅是Web應(yīng)用的調(diào)用,還有其他如App應(yīng)用、微信公眾號或小程序客戶端,或者其他語言的設(shè)計(jì)、異構(gòu)環(huán)境的調(diào)用,等。不管使用哪種工具來設(shè)計(jì),只要能用HTTP,就可以輕易實(shí)現(xiàn)對微服務(wù)的調(diào)用。
在類目管理的微服務(wù)接口及其Web微服務(wù)應(yīng)用都開發(fā)完成之后,我們就可以進(jìn)行一個(gè)總體測試了。首先確認(rèn)Consul已經(jīng)運(yùn)行就緒,然后先后啟動(dòng)catalog-restapi和 catalog-web兩個(gè)模塊。啟動(dòng)成功之后,通過瀏覽器訪問如下鏈接地址:
http://localhost:8091
如果一切正常,則可以進(jìn)入如圖6-6所示的類目管理的主頁。在這里,我們可以分別對主類和二級分類中的所有類目進(jìn)行增刪改查的所有操作。
在使用IDEA開發(fā)工具執(zhí)行打包時(shí),可以使用 Maven項(xiàng)目管理器執(zhí)行打包操作,如圖6-7所示。
如果是模塊化的項(xiàng)目,請務(wù)必在項(xiàng)目的根(root)目錄中執(zhí)行打包操作,這樣才能將其所依賴的模塊同時(shí)打包在一起。
當(dāng)打包完成之后,可以使用命令終端,分別切換到catalog-restapi和 catalog-web模塊的 target目錄中執(zhí)行下列命令,啟動(dòng)應(yīng)用進(jìn)行調(diào)試:
java -jar catalog*.jar
以這種方式啟動(dòng)應(yīng)用,與上面使用IDEA工具進(jìn)行調(diào)試時(shí)的效果是一樣的。如果啟動(dòng)正常,則可以進(jìn)行與上面一樣的測試。
這種啟動(dòng)方式也可以作為一種普通的方式來發(fā)布微服務(wù),在生產(chǎn)環(huán)境中,可以在上面指令的基礎(chǔ)上增加一些內(nèi)存和日志存儲(chǔ)方面的參數(shù)。
有關(guān)微服務(wù)應(yīng)用的部署,將在運(yùn)維部署部分進(jìn)行詳細(xì)介紹。
本章介紹了電商平臺(tái)的類目管理接口和Web類目管理后臺(tái)兩個(gè)微服務(wù)的開發(fā)實(shí)例,通過這個(gè)項(xiàng)目的開發(fā)和演示,我們清楚了微服務(wù)之間快速通信和相互調(diào)用的方法。在類目管理接口開發(fā)中,我們通過Spring Data JPA開發(fā)工具,了解了DDD開發(fā)方法在Spring 開發(fā)框架中的工作原理和實(shí)現(xiàn)方法。通過類目管理接口的實(shí)現(xiàn),我們將有狀態(tài)的數(shù)據(jù)訪問行為,轉(zhuǎn)變成沒有狀態(tài)的接口服務(wù)。
下一章,我們將介紹另一種數(shù)據(jù)庫開發(fā)工具 MyBatis,體驗(yàn)不同的數(shù)據(jù)庫開發(fā)工具在Spring項(xiàng)目工程中的應(yīng)用方法。
擊“了解更多”獲取工具
DevExpress WinForms Subscription擁有180+組件和UI庫,能為Windows Forms平臺(tái)創(chuàng)建具有影響力的業(yè)務(wù)解決方案。DevExpress WinForms能完美構(gòu)建流暢、美觀且易于使用的應(yīng)用程序,無論是Office風(fēng)格的界面,還是分析處理大批量的業(yè)務(wù)數(shù)據(jù),它都能輕松勝任!
DevExpress Winforms v20.2日前全新發(fā)布,此版本加強(qiáng)了Scheduler、Spreadsheet控件功能,歡迎下載最新版體驗(yàn)!
Year View
WinForms Scheduler控件附帶Year View顯示選項(xiàng),它旨在可視化跨越數(shù)天和數(shù)周的事件/約會(huì)。
Year View包含“ MonthCount”屬性,其他與視圖相關(guān)的設(shè)置與Month View設(shè)置相同。
下拉日歷和視圖選擇器
在日期導(dǎo)航欄中添加了兩個(gè)新的UI元素。
這兩個(gè)UI元素最初都是隱藏的,激活DateNavigationBar.CalendarButton和DateNavigationBar.ShowViewSelectorButton選項(xiàng)以使其可見。
在Timeline View中新的單元格自動(dòng)高度模式
新版本將CellsAutoHeightOptions.Enabled屬性重命名為AutoHeightMode,AutoHeightMode屬性接受枚舉值,而不是布爾值。'None'和 'Limited' 對應(yīng)于'false' 和 'true',第三個(gè)值 - “ Full”- 激活新的AutoHeight模式。
使用AutoHeight時(shí),時(shí)間單元將忽略ResourcesPerPage屬性值,并根據(jù)內(nèi)容調(diào)整大小,這還允許用戶對Timeline View進(jìn)行像素滾動(dòng)。
Excel 2016圖表(CTP)
WinForms Spreadsheet控件現(xiàn)在支持以下Excel 2016圖表類型:
全面的Spreadsheet API可讓您根據(jù)需要?jiǎng)?chuàng)建和編輯Excel 2016圖表,WinForms Spreadsheet控件可以使用Excel 2016圖表打開、打印和導(dǎo)出(導(dǎo)出為PDF)現(xiàn)有工作簿。
HTML格式
現(xiàn)在,您可以使用以下標(biāo)準(zhǔn)HTML標(biāo)記來格式化字符串:
大多數(shù)控件現(xiàn)在都支持<image>標(biāo)簽,要指定圖像集合,請使用控件的“ HtmlImages”屬性。
疊加層 - 支持DirectX
疊加層現(xiàn)在支持DirectX硬件加速,現(xiàn)在動(dòng)畫在高分辨率顯示器上的呈現(xiàn)更加流暢(并且內(nèi)存使用效率更高)。
MVVM - MessageBox表單樣式
我們向MessageBoxService類添加一個(gè)新的MessageBoxFormStyle屬性,此屬性使您可以指定MessageBox表單的外觀設(shè)置。
C#
var flyoutMsgService = MessageBoxService.CreateFlyoutMessageBoxService();
flyoutMsgService.MessageBoxFormStyle = (form) => {
FlyoutDialog msgFrm = form as FlyoutDialog;
msgFrm.Properties.AppearanceButtons.FontStyleDelta = FontStyle.Bold;
};
mvvmContext1.RegisterService(flyoutMsgService);
Docking - 浮動(dòng)面板始終位于頂部
浮動(dòng)面板和DocumentManager文檔(在Tabbed和Widget Views中)可以顯示在其父窗體的上方或下方。 以下新選項(xiàng)使您始終可以將浮動(dòng)窗口置于最上方:
如果單獨(dú)使用浮動(dòng)窗口(禁用了FloatPanelsAlwaysOnTop選項(xiàng)),它將顯示Minimize按鈕,該按鈕會(huì)將窗口折疊到Windows任務(wù)欄。 要隱藏Minimize按鈕,請禁用ShowMinimizeButton選項(xiàng)。
*請認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。