containerd 與安全沙箱的 Kubernetes 初體驗

1{icon} {views}

作者 | 易立  阿里雲資深技術專家

containerd 是一個開源的行業標準容器運行時,關注於簡單、穩定和可移植,同時支持 Linux 和 Windows。

  • 2016 年 12 月 14 日,Docker 公司宣布將 Docker Engine 的核心組件 containerd 捐贈到一個新的開源社區獨立發展和運營。阿里雲、AWS、 Google、IBM 和 Microsoft 作為初始成員,共同建設 containerd 社區;

  • 2017 年 3 月,Docker 將 containerd 捐獻給 CNCF(雲原生計算基金會)。containerd 得到了快速的發展和廣泛的支持;

  • Docker 引擎已經將 containerd 作為容器生命周期管理的基礎,Kubernetes 也在 2018 年 5 月,正式支持 containerd 作為容器運行時管理器;

  • 2019 年 2 月,CNCF 宣布 containerd 畢業,成為生產可用的項目。

containerd 從 1.1 版本開始就已經內置了 Container Runtime Interface (CRI) 支持,進一步簡化了對 Kubernetes 的支持。其架構圖如下:

在 Kubernetes 場景下,containerd 與完整 Docker Engine 相比,具有更少的資源佔用和更快的啟動速度。

圖片來源:

紅帽主導的 cri-o 是與 containerd 競爭的容器運行時管理項目。containerd 與 cri-o 項目相比,在性能上具備優勢,在社區支持上也更加廣泛。

圖片來源:

更重要的是 containerd 提供了靈活的擴展機制,支持各種符合 OCI(Open Container Initiative)的容器運行時實現,比如 runc 容器(也是熟知的 Docker 容器)、KataContainer、gVisor 和 Firecraker 等安全沙箱容器。

在 Kubernetes 環境中,可以用不同的 API 和命令行工具來管理容器 / Pod、鏡像等概念。為了便於大家理解,我們可以用下圖說明如何利用不同層次的 API 和 CLI 管理容器生命周期管理。

  • Kubectl:是集群層面的命令行工具,支持 Kubernetes 的基本概念
  • :是針對節點上 CRI 的命令行工具
  • :是針對 containerd 的命令行工具

體驗

Minikube 是體驗 containerd 作為 Kubernetes 容器運行時的最簡單方式,我們下面將其作為 Kubernetes 容器運行時,並支持 runc 和 gvisor 兩種不同的實現。

早期由於網絡訪問原因,很多朋友無法直接使用官方 Minikube 進行實驗。在最新的 Minikube 1.5 版本中,已經提供了完善的配置化方式,可以幫助大家利用阿里雲的鏡像地址來獲取所需 Docker 鏡像和配置,同時支持 Docker/Containerd 等不同容器運行時。我們一個 Minikube 虛擬機環境,注意需要指明 --container-runtime=containerd 參數設置 containerd 作為容器運行時。同時 registry-mirror 也要替換成自己的阿里雲鏡像加速地址。

$ minikube start --image-mirror-country cn \
    --iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.5.0.iso \
    --registry-mirror=https://XXX.mirror.aliyuncs.com \
    --container-runtime=containerd
  Darwin 10.14.6 上的 minikube v1.5.0
  Automatically selected the 'hyperkit' driver (alternates: [virtualbox])
️  您所在位置的已知存儲庫都無法訪問。正在將 registry.cn-hangzhou.aliyuncs.com/google_containers 用作後備存儲庫。
  正在創建 hyperkit 虛擬機(CPUs=2,Memory=2000MB, Disk=20000MB)...
️  VM is unable to connect to the selected image repository: command failed: curl -sS https://k8s.gcr.io/
stdout:
stderr: curl: (7) Failed to connect to k8s.gcr.io port 443: Connection timed out
: Process exited with status 7
  正在 containerd 1.2.8 中準備 Kubernetes v1.16.2…
  拉取鏡像 ...
  正在啟動 Kubernetes ...
⌛  Waiting for: apiserver etcd scheduler controller
  完成!kubectl 已經配置至 "minikube"
$ minikube dashboard
  Verifying dashboard health ...
  Launching proxy ...
  Verifying proxy health ...
  Opening http://127.0.0.1:54438/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...

部署測試應用

我們通過 Pod 部署一個 nginx 應用:

$ cat nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
$ kubectl apply -f nginx.yaml
pod/nginx created
$ kubectl exec nginx -- uname -a
Linux nginx 4.19.76 #1 SMP Fri Oct 25 16:07:41 PDT 2019 x86_64 GNU/Linux

然後,我們開啟 minikube 對 gvisor 支持:

$ minikube addons enable gvisor
  gvisor was successfully enabled
$ kubectl get pod,runtimeclass gvisor -n kube-system
NAME         READY   STATUS    RESTARTS   AGE
pod/gvisor   1/1     Running   0          60m
NAME                              CREATED AT
runtimeclass.node.k8s.io/gvisor   2019-10-27T01:40:45Z
$ kubectl get runtimeClass
NAME     CREATED AT
gvisor   2019-10-27T01:40:45Z

當 gvisor pod 進入 Running 狀態的時候,可以部署 gvisor 測試應用。

我們可以看到 K8s 集群中已經註冊了一個 gvisor 的“runtimeClassName”。之後,開發者可以通過在 Pod 聲明中的 “runtimeClassName” 來選擇不同類型的容器運行時實現。比如,如下我們創建一個運行在 gvisor 沙箱容器中的 nginx 應用。

$ cat nginx-untrusted.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-untrusted
spec:
  runtimeClassName: gvisor
  containers:
  - name: nginx
    image: nginx
$ kubectl apply -f nginx-untrusted.yaml
pod/nginx-untrusted created
$ kubectl exec nginx-untrusted -- uname -a
Linux nginx-untrusted 4.4 #1 SMP Sun Jan 10 15:06:54 PST 2016 x86_64 GNU/Linux

我們可以清楚地發現:由於基於 runc 的容器與宿主機共享操作系統內核,runc 容器中查看到的 OS 內核版本與 Minikube 宿主機 OS 內核版本相同;而 gvisor 的 runsc 容器採用了獨立內核,它和 Minikube 宿主機 OS 內核版本不同。

正是因為每個沙箱容器擁有獨立的內核,減小了安全攻擊面,具備更好的安全隔離特性。適合隔離不可信的應用,或者多租戶場景。注意:gvisor 在 minikube 中,通過 ptrace 對內核調用進行攔截,其性能損耗較大,此外 gvisor 的兼容性還有待增強。

使用 ctl 和 crictl 工具

我們現在可以進入進入 Minikube 虛擬機:

$ minikube ssh

containerd 支持通過名空間對容器資源進行隔離,查看現有 containerd 名空間:

$ sudo ctr namespaces ls
NAME   LABELS
k8s.io
# 列出所有容器鏡像
$ sudo ctr --namespace=k8s.io images ls
...
# 列出所有容器列表
$ sudo ctr --namespace=k8s.io containers ls

在 Kubernetes 環境更加簡單的方式是利用 crictl 對 pods 進行操作。

# 查看pod列表
$ sudo crictl pods
POD ID              CREATED             STATE               NAME                                         NAMESPACE              ATTEMPT
78bd560a70327       3 hours ago         Ready               nginx-untrusted                              default                0
94817393744fd       3 hours ago         Ready               nginx                                        default                0
...
# 查看名稱包含nginx的pod的詳細信息
$ sudo crictl pods --name nginx -v
ID: 78bd560a70327f14077c441aa40da7e7ad52835100795a0fa9e5668f41760288
Name: nginx-untrusted
UID: dda218b1-d72e-4028-909d-55674fd99ea0
Namespace: default
Status: Ready
Created: 2019-10-27 02:40:02.660884453 +0000 UTC
Labels:
    io.kubernetes.pod.name -> nginx-untrusted
    io.kubernetes.pod.namespace -> default
    io.kubernetes.pod.uid -> dda218b1-d72e-4028-909d-55674fd99ea0
Annotations:
    kubectl.kubernetes.io/last-applied-configuration -> {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx-untrusted","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx"}],"runtimeClassName":"gvisor"}}
    kubernetes.io/config.seen -> 2019-10-27T02:40:00.675588392Z
    kubernetes.io/config.source -> api
ID: 94817393744fd18b72212a00132a61c6cc08e031afe7b5295edafd3518032f9f
Name: nginx
UID: bfcf51de-c921-4a9a-a60a-09faab1906c4
Namespace: default
Status: Ready
Created: 2019-10-27 02:38:19.724289298 +0000 UTC
Labels:
    io.kubernetes.pod.name -> nginx
    io.kubernetes.pod.namespace -> default
    io.kubernetes.pod.uid -> bfcf51de-c921-4a9a-a60a-09faab1906c4
Annotations:
    kubectl.kubernetes.io/last-applied-configuration -> {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx"}]}}
    kubernetes.io/config.seen -> 2019-10-27T02:38:18.206096389Z
    kubernetes.io/config.source -> api

containerd 與 Docker 的關係

很多同學都關心 containerd 與 Docker 的關係,以及是否 containerd 可以取代 Docker?

containerd 已經成為容器運行時的主流實現,也得到了 Docker 社區和 Kubernetes 社區的大力支持。Docker Engine 底層的容器生命周期管理也是基於 containerd 實現。

但是 Docker Engine 包含了更多的開發者工具鏈,比如鏡像構建。也包含了 Docker 自己的日誌、存儲、網絡、Swarm 編排等能力。此外,絕大多數容器生態廠商,如安全、監控、開發等對 Docker Engine 的支持比較完善,對 containerd 的支持也在逐漸補齊。

所以在 Kubernetes 運行時環境,對安全和效率和定製化更加關注的用戶可以選擇 containerd 作為容器運行時環境;對於大多數開發者,繼續使用 Docker Engine 作為容器運行時也是一個不錯的選擇。

阿里雲容器服務對 containerd 的支持

在阿里雲 Kubernetes 服務 ACK,我們已經採用 containerd 作為容器運行時管理,來支撐安全沙箱容器和 runc 容器的混合部署。在現有產品中,我們和阿里雲操作系統團隊、螞蟻金服一起支持了基於輕量虛擬化的 runV 沙箱容器,4Q 也將和操作系統團隊、安全團隊合作發布基於 Intel SGX 的可信加密沙箱容器。

具體產品信息可以參考。

Serverless Kubernetes(ASK)中,我們也利用 containerd 靈活的插件機制定製和剪裁了面向 nodeless 環境的容器運行時實現。

“ 阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

特斯拉Model X中國售價96萬元起 Q2開始交付

3{icon} {views}

2月1日,特斯拉公司宣佈將在中國市場首次推出Model X頂級限量 Signature Red版P90D(售價人民幣147.95萬元), 同時公佈目前在中國市場推出的Model X 標準版90D的官方價格區間(人民幣96.1-117.18萬元),並開始電話邀請預定Model X的消費者進行車輛選配,新車預計將於二季度開始在中國交付。

特斯拉目前在中國推出的Model X 90D擁有90千瓦時(kWh)的電池容量,續航里程高達470公里,零到百公里加速5.0秒,最高時速達到250公里/小時。Model X在中國的官方價格一直備受關注。得益於業內獨創的“官網電商+線下體驗店”的直營模式,特斯拉得以做到全球公平、透明定價。本次公佈的Model X 90D在中國的定價(人民幣96.1-117.18萬元)繼續秉持了這一原則,即只在美國本土售價的基礎上增加了關稅和運輸成本。消費者可以透明、自主得通過特斯拉官網定制和下單。

頂級限量Signature版是特斯拉在第一代超級跑車Roadster、第二代豪華轎跑車Model S的首發階段慣例推出的限量產品,車身飾有“Signature”字樣,可謂“頂配中的頂配”。本次在中國推出的Model X頂級限量Signature Red版P90D,其所擁有的名為“Signature Red”的紅色區別於所有其他量產的特斯拉車輛。當它開啟Ludicrous狂暴模式時,百公里加速僅需3.4秒,續航里程高達450公里,並配備主動擾流板和特斯拉紅色制動卡鉗等。


特斯拉頂配限量版“Signature Red” Model X

駕駛過程中,Model X的主動安全防護功能通過雷達和聲納系統即時監視路面情況,並主動向駕駛者提供回饋,最大幅度避免事故發生。獨特的車身結構能夠大幅度降低重心,與同類SUV相比,降低了50%的側翻風險。在NHTSA(美國國家公路交通安全管理局)的所有安全測試項目中,Model X都獲得了5星級評價。作為智慧SUV的代表,Model X最新的空中升級(OTA)7.1系統,擁有業界領先的自動駕駛(Autopilot)、自動泊車、召喚(Summon)等功能,讓駕車不再是一件麻煩的事情,而且該系統還在不斷前行。

Model X獨有的鷹翼門採用碳纖維雙鉸鏈結構,在傳統鷗翼門的設計之上,進一步提高承壓性及整車密封性。超聲波感應器智慧探測車身周圍的空間,以判斷車門是否能夠安全開啟。車門全部開啟只需要6-7秒,僅佔用30釐米的側方空間。全景擋風玻璃將前擋風玻璃和天窗連在一起,是目前轎車上安裝的最大單塊玻璃,給予使用者更加開闊的視野,優化的防曬隔熱玻璃膜全面提高安全係數。

7座Model X採用2+3+2的佈局,並且每個座位都採用獨立座椅,滿足全家的出行需求。此外還擁有豐富、超大的儲物空間。行李可以輕鬆置於Model X的前備箱、後備箱以及座椅下方。第三排座椅放倒之後,Model X的後備箱可輕鬆擺放高爾夫球包、折疊式兒童手推車、自行車、滑雪手杖及滑雪板等大型裝備。如果加裝車尾懸掛支架,Model X還可以攜帶更多行李。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

FreeSql v0.11 幾個實用功能說明

FreeSql 開源發布快一年了,立志成為 .Net 平台方便好用的 ORM,倉庫地址:

隨着不斷的迭代更新,越來越穩定,也越來越強大。預計在一周年的時候(2020年1月1日)發布 1.0 正式版本。

金九銀十的日子過去了,在這個銅一般的月份里,鄙人做了幾個重大功能,希望對使用者開發提供更大的便利。

  • 一、Dto 映射查詢
  • 二、IncludeMany 聯級加載
  • 三、Where(a => true) 邏輯表達式解析優化
  • 四、SaveManyToMany 聯級保存多對多集合屬性
  • 五、遷移實體 – 到指定表名
  • 六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert
  • 七、ISelect.ToDelete 高級刪除
  • 八、全局過濾器

以下的代碼,先決定義代碼如下 :

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=|DataDirectory|\db1.db;Max Pool Size=10";)
    .UseAutoSyncStructure(true) //自動同步實體結構到數據庫
    .Build();

public class Blog
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

一、Dto 映射查詢

class Dto
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int xxx { get; set; }
}

fsql.Select<Blog>().ToList<Dto>();
//SELECT Id, Url FROM Blog

fsql.Select<Blog>().ToList(a => new Dto { xxx = a.Rating} );
//SELECT Id, Url, Rating as xxx FROM Blog
//這樣寫,附加所有映射,再額外映射 xxx

fsql.Select<Blog>().ToList(a => new Blog { Id = a.Id }) 
//這樣寫,只查詢 id

fsql.Select<Blog>().ToList(a => new { a.Id }) 
//這樣寫,只查詢 id,返回匿名對象

映射支持單表/多表,是在查詢數據之前映射(不是先查詢所有字段再到內存映射)

查找規則,查找屬性名,會循環內部對象 _tables(join 查詢後會增長),以 主表優先查,直到查到相同的字段。

如:

A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射。也可以指定 id = C.id 映射。

友情提醒:在 dto 可以直接映射一個導航屬性

二、IncludeMany 聯級加載

之前已經實現,有設置關係,和未設置關係 的導航集合屬性聯級加載。

有設置關係的(支持一對多、多對多):

fsql.Select<Tag>().IncludeMany(a => a.Goods).ToList();

未設置關係的,臨時指定關係(只支持一對多):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Where(b => b.TagId == a.Id));

只查詢每項子集合的前幾條數據,避免像EfCore加載所有數據導致IO性能低下(比如某商品下有2000條評論):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Take(10));

上面已有的 IncludeMany 功能還不夠自由靈活。

新功能1:在 Dto 上做映射 IncludeMany

老的 IncludeMany 限制只能在 ISelect 內使用,必須要先查上級數據,解決這個問題我們做了直接在 Dto 上做映射:

查詢 Goods 商品表,分類1、分類2、分類3 各10條數據

//定義臨時類,也可以是 Dto 類
class Dto {
    public int TypeId { get; set; }
    public List<Goods > GoodsList { get; set; }
}

var dto = new [] { 1,2,3 }.Select(a => new Dto { TypeId = a }).ToList();
dto.IncludeMany(d => d.GoodsList.Take(10).Where(gd => gd.TypeId == d.TypeId));

//執行后,dto 每個元素.Vods 將只有 10條記錄

現在 IncludeMany 不再是 ISelect 的專利,普通的 List<T> 也可以用它來貪婪加載數據,並準確填充到內部各元素中。

新功能2:查詢子集合表的指定字段

老的 IncludeMany 限制只能查子表的所有字段,子表過段多過的話比較浪費 IO 性能。

新功能可以設置子集合返回部分字段,避免子集合字段過多的問題。

fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods { Id = b.Id, Title = b.Title }));
//只查詢 goods 表 id, title 字段,再作填充

三、Where(a => true) 邏輯表達式解析優化

相信很多 ORM 解析表達式的時候處理不了這個問題,我們之前已經解決了 99%。

這個月發現還有一餘孽未清,發現問題后及時解決了,並增加單元測試代碼以絕後患。

四、SaveManyToMany 聯級保存多對多集合屬性

在此之前,FreeSql.DbContext 和 倉儲實現,已經實現了聯級保存功能,如下:

聯級保存功能可實現保存對象的時候,將其【OneToMany】、【ManyToMany】導航屬性集合也一併保存。

全局關閉:

fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);

局部關閉:

var repo = fsql.GetRepository<T>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;

新功能:

保存實體的指定【多對多】導航屬性,SaveManyToMany 方法實現在 BaseRepository、DbContext。

解決問題:當實體類導航數據過於複雜的時候,選擇關閉聯級保存的功能是明智之選,但是此時【多對多】數據保存功能寫起來非常繁瑣麻煩(因為要與現有數據對比后保存)。

var song = new Song { Id = 1 };
song.Tags = new List<Tag>();
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
repo.SaveManyToMany(song, "Tags");
//輕鬆保存 song 與 tag 表的關聯

機制規則與聯級保存的【多對多】一樣,如下:

我們對中間表的保存是完整對比操作,對外部實體的操作只作新增(注意不會更新)

  • 屬性集合為空時,刪除他們的所有關聯數據(中間表)
  • 屬性集合不為空時,與數據庫存在的關聯數據(中間表)完全對比,計算出應該刪除和添加的記錄

五、遷移實體 – 到指定表名

fsql.CodeFirst.SyncStructure(typeof(Log), "Log_1"); //遷移到 Log_1 表
fsql.CodeFirst.SyncStructure(typeof(Log), "Log_2"); //遷移到 Log_2 表

在此功能上,我們對分表功能做了點升級,以下動作都會做遷移動作:

fsql.Select<Log>().AsTable((_, oldname) => $"{oldname}_1");
fsql.GetRepository<Log>(null, oldname => $"{oldname}_1");

六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert

FreeSql 提供了多種插入或更新方法,v0.11 之前主要使用 FreeSql.Repository/FreeSql.DbContext 庫提供的方法實現。

FreeSql.Repository 之 InsertOrUpdate

此方法與 FreeSql.DbContext AddOrUpdate 方法功能一樣。

var repo = fsql.GetRepository<T>();
repo.InsertOrUpdate(實體);

如果內部的狀態管理存在數據,則更新。

如果內部的狀態管理不存在數據,同查詢數據庫,是否存在。

存在則更新,不存在則插入

缺點:不支持批量操作

新功能:MySql 特有功能 On Duplicate Key Update

FreeSql.Provider.MySql 和 FreeSql.Provider.MySqlConnector 在 v0.11.11 版本已支持 MySql 特有的功能,On Duplicate Key Update。

這個功能也可以實現插入或更新數據,並且支持批量操作。

class TestOnDuplicateKeyUpdateInfo
{
    [Column(IsIdentity = true)]
    public int id { get; set; }
    public string title { get; set; }
    public DateTime time { get; set; }
}

var item = new TestOnDuplicateKeyUpdateInfo { id = 100, title = "title-100", time = DateTime.Parse("2000-01-01") };
fsql.Insert(item)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`, `time`) VALUES(100, 'title-100', '2000-01-01 00:00:00.000')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = VALUES(`time`)

OnDuplicateKeyUpdate() 之後可以調用的方法:

方法名 描述
IgnoreColumns 忽略更新的列,機制和 IUpdate.IgnoreColumns 一樣
UpdateColumns 指定更新的列,機制和 IUpdate.UpdateColumns 一樣
Set 手工指定更新的列,與 IUpdate.Set 功能一樣
SetRaw 作為 Set 方法的補充,可傳入 SQL 字符串
ToSql 返回即將執行的 SQL 語句
ExecuteAffrows 執行,返回影響的行數

IInsert 與 OnDuplicateKeyUpdate 都有 IgnoreColumns、UpdateColumns 方法。

當插入實體/集合實體的時候,忽略了 time 列,代碼如下:

fsql.Insert(item)
    .IgnoreColumns(a => a.time)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`) VALUES(200, 'title-200')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = '2000-01-01 00:00:00.000'

我們發現,UPDATE time 部分變成了常量,而不是 VALUES(`time`),機制如下:

當 insert 部分中存在的列,在 update 中將以 VALUES(`字段`) 的形式設置;

當 insert 部分中不存在的列,在 update 中將為常量形式設置,當操作實體數組的時候,此常量為 case when … end 執行(與 IUpdate 一樣);

新功能2:PostgreSQL 特有功能 On Conflict Do Update

使用方法 MySql OnDuplicateKeyUpdate 大致相同。

七、ISelect.ToDelete 高級刪除

默認 IDelete 不支持導航對象,多表關聯等。ISelect.ToDelete 可將查詢轉為刪除對象,以便支持導航對象或其他查詢功能刪除數據,如下:

fsql.Select<T1>().Where(a => a.Options.xxx == 1).ToDelete().ExecuteAffrows();

注意:此方法不是將數據查詢到內存循環刪除,上面的代碼產生如下 SQL 執行:

DELETE FROM `T1` WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)

複雜刪除使用該方案的好處:

  • 刪除前可預覽測試數據,防止錯誤刪除操作;
  • 支持更加複雜的刪除操作(IDelete 默認只支持簡單的操作),甚至在 ISelect 上使用 Limit(10) 將只刪除附合條件的前 10條記錄;

還有 ISelect.ToUpdate 高級更新數據功能,使用方法類似

八、全局過濾器

FreeSql 基礎層實現了 Select/Update/Delete 可設置的全局過濾器功能。

public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();

fsql.GlobalFilter
    .Apply<TestAddEnum>("test1", a => a.Id == TenantId.Value)
    .Apply<AuthorTest>("test2", a => a.Id == 111)
    .Apply<AuthorTest>("test3", a => a.Name == "11");

Apply 泛型參數可以設置為任何類型,當使用 Select/Update/Delete 方法時會進行過濾器匹配嘗試(try catch):

  • 匹配成功的,將附加 where 條件;
  • 匹配失敗的,標記下次不再匹配,避免性能損耗;

如何禁用?

fsql.Select<TestAddEnum>().ToList(); //所有生效
fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); //禁用 test1
fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); //禁用所有

fsql.Update/Delete 方法效果同上。

注意:IFreeSql.GlobalFilter 與 倉儲過濾器 不是一個功能,可以同時生效

鳴謝

感謝反饋 bug 的朋友!

倉庫地址:

請移步更新日誌:https://github.com/2881099/FreeSql/wiki/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

go中的數據結構-接口interface

2{icon} {views}

1. 接口的基本使用

  golang中的interface本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那麼該類就實現了該接口。與其他語言不同,golang並不需要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。

聲明

 1 type 接口名 interface{
 2     方法1
 3     方法2
 4     ...
 5    方法n 
 6 }
 7 type 接口名 interface {
 8     已聲明接口名1
 9     ...
10     已聲明接口名n
11 }
12 type iface interface{
13     tab *itab
14     data unsafe.Pointer
15 }

  接口自身也是一種結構類型,只是編譯器對其做了很多限制:

  • 不能有字段
  • 不能定義自己的方法
  • 只能聲明方法,不能實現
  • 可嵌入其他接口類型
 1 package main
 2 
 3     import (
 4         "fmt"
 5     )
 6 
 7     // 定義一個接口
 8     type People interface {
 9         ReturnName() string
10     }
11 
12     // 定義一個結構體
13     type Student struct {
14         Name string
15     }
16 
17     // 定義結構體的一個方法。
18     // 突然發現這個方法同接口People的所有方法(就一個),此時可直接認為結構體Student實現了接口People
19     func (s Student) ReturnName() string {
20         return s.Name
21     }
22 
23     func main() {
24         cbs := Student{Name:"小明"}
25 
26         var a People
27         // 因為Students實現了接口所以直接賦值沒問題
28         // 如果沒實現會報錯:cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method)
29         a = cbs       
30         name := a.ReturnName() 
31         fmt.Println(name) // 輸出"小明"
32     }

  如果一個接口不包含任何方法,那麼它就是一個空接口(empty interface),所有類型都符合empty interface的定義,因此任何類型都能轉換成empty interface。

  接口的值簡單來說,是由兩部分組成的,就是類型和數據,判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都為nil才代表接口是nil。

1 var a interface{} 
2 var b interface{} = (*int)(nil)
3 fmt.Println(a == nil, b == nil) //true false

2. 接口嵌套

  像匿名字段那樣嵌入其他接口。目標類型方法集中必須擁有包含嵌入接口方法在內的全部方法才算實現了該接口。嵌入其他接口類型相當於將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。

 1 type stringer interfaceP{
 2      string() string
 3 }
 4 
 5 type tester interface {
 6     stringer
 7     test()
 8 }    
 9 
10 type data struct{}
11 
12 func (*data) test() {}
13 
14 func (data) string () string {
15     return ""
16 }
17 
18 func main() {
19     var d data 
20     var t tester = &d 
21     t.test()
22     println(t.string())
23 }

  超集接口變量可隱式轉換為子集,反過來不行。

3. 接口的實現

golang的接口檢測既有靜態部分,也有動態部分。

  • 靜態部分
    對於具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,後續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址即可。具體實現可以看golang的提交日誌CL 20901、CL 20902。
    對於interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,並生成值

  • 動態部分
    在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,如果有就返回成功;如果沒有,就檢查這兩種類型能否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這裏的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決衝突。主要是interface <-> interface(接口賦值給接口、接口轉換成另一接口)使用到動態生產itab

interface的結構如下:

接口類型的結構interfacetype

 1 type interfacetype struct {
 2     typ     _type   
 3     pkgpath name   //記錄定義接口的包名
 4     mhdr    []imethod  //一個imethod切片,記錄接口中定義的那些函數。
 5 }
 6 
 7 // imethod表示接口類型上的方法
 8 type imethod struct {
 9     name nameOff // name of method
10     typ  typeOff // .(*FuncType) underneath
11 }

  nameOff 和 typeOff 類型是 int32 ,這兩個值是鏈接器負責嵌入的,相對於可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。

4. 接口值的結構iface和eface

 為了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口。

 1 type iface struct { 
 2     tab  *itab
 3     data unsafe.Pointer
 4 }
 5 
 6 type eface struct {
 7     _type *_type
 8     data  unsafe.Pointer
 9 }
10 
11 type itab struct {
12     inter *interfacetype   //inter接口類型
13     _type *_type   //_type數據類型
14     hash  uint32  //_type.hash的副本。用於類型開關。 hash哈希的方法
15     _     [4]byte
16     fun   [1]uintptr  // 大小可變。 fun [0] == 0表示_type未實現inter。 fun函數地址佔位符
17 }

  iface結構體中的data是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,然後data指向這塊新的內存。

itab中的hash方法拷貝自_type.hash;fun是一個大小為1的uintptr數組,當fun[0]為0時,說明_type並沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了

4.1 全局的itab table

iface.go:

1 const itabInitSize = 512
2 
3 // 注意:如果更改這些字段,請在itabAdd的mallocgc調用中更改公式。
4 type itabTableType struct {
5     size    uintptr             // 條目數組的長度。始終為2的冪。
6     count   uintptr             // 當前已填寫的條目數。
7     entries [itabInitSize]*itab // really [size] large
8 }

  可以看出這個全局的itabTable是用數組在存儲的,size記錄數組的大小,總是2的次冪。count記錄數組中已使用了多少。entries是一個*itab數組,初始大小是512。

5. 接口類型轉換

  把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。

 1 func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
 2     if raceenabled {
 3         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
 4     }
 5     if msanenabled {
 6         msanread(elem, t.size)
 7     }
 8     x := mallocgc(t.size, t, true)
 9     // TODO: 我們分配一個清零的對象只是為了用實際數據覆蓋它。
10     //確定如何避免歸零。同樣在下面的convT2Eslice,convT2I,convT2Islice中。
11     typedmemmove(t, x, elem)
12     e._type = t
13     e.data = x
14     return
15 }
16 
17 func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
18     t := tab._type
19     if raceenabled {
20         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
21     }
22     if msanenabled {
23         msanread(elem, t.size)
24     }
25     x := mallocgc(t.size, t, true)
26     typedmemmove(t, x, elem)
27     i.tab = tab
28     i.data = x
29     return
30 }
31 
32 func convT2I16(tab *itab, val uint16) (i iface) {
33     t := tab._type
34     var x unsafe.Pointer
35     if val == 0 {
36         x = unsafe.Pointer(&zeroVal[0])
37     } else {
38         x = mallocgc(2, t, false)
39         *(*uint16)(x) = val
40     }
41     i.tab = tab
42     i.data = x
43     return
44 }
45 
46 func convI2I(inter *interfacetype, i iface) (r iface) {
47     tab := i.tab
48     if tab == nil {
49         return
50     }
51     if tab.inter == inter {
52         r.tab = tab
53         r.data = i.data
54         return
55     }
56     r.tab = getitab(inter, tab._type, false)
57     r.data = i.data
58     return
59 }

  可以看出:

  • 具體類型轉空接口,_type字段直接複製源的type;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 對於接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。

對於那些特定類型的值,如果是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]

5.1 接口轉接口

 1 func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
 2     tab := i.tab
 3     if tab == nil {
 4         return
 5     }
 6     if tab.inter != inter {
 7         tab = getitab(inter, tab._type, true)
 8         if tab == nil {
 9             return
10         }
11     }
12     r.tab = tab
13     r.data = i.data
14     b = true
15     return
16 }
17 
18 func assertE2I(inter *interfacetype, e eface) (r iface) {
19     t := e._type
20     if t == nil {
21         // 顯式轉換需要非nil接口值。
22         panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
23     }
24     r.tab = getitab(inter, t, false)
25     r.data = e.data
26     return
27 }
28 
29 func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
30     t := e._type
31     if t == nil {
32         return
33     }
34     tab := getitab(inter, t, true)
35     if tab == nil {
36         return
37     }
38     r.tab = tab
39     r.data = e.data
40     b = true
41     return
42 }

  我們看到有兩種用法:

  • 返回值是一個時,不能轉換就panic。
  • 返回值是兩個時,第二個返回值標記能否轉換成功

  此外,data複製的是指針,不會完整拷貝值。每次都malloc一塊內存,那麼性能會很差,因此,對於一些類型,golang的編譯器做了優化。

5.2 接口轉具體類型

  接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個empty interface轉換成具體類型的例子:

 1 var EFace interface{}
 2 var j int
 3 
 4 func F4(i int) int{
 5     EFace = I
 6     j = EFace.(int)
 7     return j
 8 }
 9 
10 func main() {
11     F4(10)
12 }

反彙編:

  go build -gcflags ‘-N -l’ -o tmp build.go

  go tool objdump -s “main.F4” tmp

  可以看彙編代碼:

1 MOVQ main.EFace(SB), CX       //CX = EFace.typ
2 LEAQ type.*+60128(SB), DX    //DX = &type.int
3 CMPQ DX, CX.                         //if DX == AX

  可以看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是不是同一個type,而不是調用某個函數在運行時動態對比。

5.3 非空接口類型轉換

 1 var tf Tester
 2 var t testStruct
 3 
 4 func F4() int{
 5     t := tf.(testStruct)
 6     return t.i
 7 }
 8 
 9 func main() {
10     F4()
11 }
12 //反彙編
13 MOVQ main.tf(SB), CX   // CX = tf.tab(.inter.typ)
14 LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ)
15 CMPQ DX, CX //

  可以看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是不是同一個itab,而不是調用某個函數在運行時動態對比。

6. 獲取itab的流程

  golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。看下代碼:

 1 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
 2     if len(inter.mhdr) == 0 {
 3         throw("internal error - misuse of itab")
 4     }
 5 
 6     // 簡單的情況
 7     if typ.tflag&tflagUncommon == 0 {
 8         if canfail {
 9             return nil
10         }
11         name := inter.typ.nameOff(inter.mhdr[0].name)
12         panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
13     }
14 
15     var m *itab
16 
17     //首先,查看現有表以查看是否可以找到所需的itab。
18     //這是迄今為止最常見的情況,因此請不要使用鎖。
19     //使用atomic確保我們看到該線程完成的所有先前寫入更新itabTable字段(在itabAdd中使用atomic.Storep)。
20     t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
21     if m = t.find(inter, typ); m != nil {
22         goto finish
23     }
24 
25     // 未找到。抓住鎖,然後重試。
26     lock(&itabLock)
27     if m = itabTable.find(inter, typ); m != nil {
28         unlock(&itabLock)
29         goto finish
30     }
31 
32     // 條目尚不存在。進行新輸入並添加。
33     m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
34     m.inter = inter
35     m._type = typ
36     m.init()
37     itabAdd(m)
38     unlock(&itabLock)
39 finish:
40     if m.fun[0] != 0 {
41         return m
42     }
43     if canfail {
44         return nil
45     }
46     //僅當轉換時才會發生,使用ok形式已經完成一次,我們得到了一個緩存的否定結果。
47     //緩存的結果不會記錄,缺少接口函數,因此初始化再次獲取itab,以獲取缺少的函數名稱。
48     panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
49 }

  流程如下:

  • 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是為了防止查找過程中,itabTable被替換導致查找錯誤。
  • 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因為在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
  • 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。

  這裏我們可以看到申請新的itab空間時,內存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因為字段fun是一個大小為1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。

6.1 在itabTable中查找itab find

 1 func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
 2     // 編譯器為我們提供了一些很好的哈希碼。
 3     return uintptr(inter.typ.hash ^ typ.hash)
 4 }
 5 
 6    // find在t中找到給定的接口/類型對。
 7    // 如果不存在給定的接口/類型對,則返回nil。
 8 func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
 9     // 使用二次探測實現。
10      //探測順序為h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k。
11      //我們保證使用此探測序列擊中所有表條目。
12     mask := t.size - 1
13     h := itabHashFunc(inter, typ) & mask
14     for i := uintptr(1); ; i++ {
15         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
16         // 在這裏使用atomic read,所以如果我們看到m!= nil,我們也會看到m字段的初始化。
17         // m := *p
18         m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
19         if m == nil {
20             return nil
21         }
22         if m.inter == inter && m._type == typ {
23             return m
24         }
25         h += I
26         h &= mask
27     }
28 }

  從註釋可以看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。以前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中我們看到有了改進。

6.2 檢查並生成itab init

 1 // init用所有代碼指針填充m.fun數組m.inter / m._type對。 如果該類型未實現該接口,將m.fun [0]設置為0,並返回缺少的接口函數的名稱。
 2 //可以在同一m上多次調用此函數,即使同時調用也可以。
 3 func (m *itab) init() string {
 4     inter := m.inter
 5     typ := m._type
 6     x := typ.uncommon()
 7 
 8     // inter和typ都有按名稱排序的方法,
 9      //並且接口名稱是唯一的,
10      //因此可以在鎖定步驟中對兩者進行迭代;
11      //循環是O(ni + nt)而不是O(ni * nt)。
12     ni := len(inter.mhdr)
13     nt := int(x.mcount)
14     xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
15     j := 0
16 imethods:
17     for k := 0; k < ni; k++ {
18         i := &inter.mhdr[k]
19         itype := inter.typ.typeOff(i.ityp)
20         name := inter.typ.nameOff(i.name)
21         iname := name.name()
22         ipkg := name.pkgPath()
23         if ipkg == "" {
24             ipkg = inter.pkgpath.name()
25         }
26         for ; j < nt; j++ {
27             t := &xmhdr[j]
28             tname := typ.nameOff(t.name)
29             if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
30                 pkgPath := tname.pkgPath()
31                 if pkgPath == "" {
32                     pkgPath = typ.nameOff(x.pkgpath).name()
33                 }
34                 if tname.isExported() || pkgPath == ipkg {
35                     if m != nil {
36                         ifn := typ.textOff(t.ifn)
37                         *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
38                     }
39                     continue imethods
40                 }
41             }
42         }
43         // didn't find method
44         m.fun[0] = 0
45         return iname
46     }
47     m.hash = typ.hash
48     return ""
49 }

  這個方法會檢查interface和type的方法是否匹配,即type有沒有實現interface。假如interface有n中方法,type有m中方法,那麼匹配的時間複雜度是O(n x m),由於interface、type的方法都按字典序排,所以O(n+m)的時間複雜度可以匹配完。在檢測的過程中,匹配上了,依次往fun字段寫入type中對應方法的地址。如果有一個方法沒有匹配上,那麼就設置fun[0]為0,在外層調用會檢查fun[0]==0,即type並沒有實現interface

  這裏我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環中,那麼就在那一層的循環上加個標籤,然後continue 標籤

6.3 把itab插入到itabTable中 itabAdd

 1 // itabAdd將給定的itab添加到itab哈希表中。
 2 //必須保持itabLock。
 3 func itabAdd(m *itab) {
 4     // 設置了mallocing時,錯誤可能導致調用此方法,通常是因為這是在恐慌時調用的。
 5     //可靠地崩潰,而不是僅在需要增長時崩潰哈希表。
 6     if getg().m.mallocing != 0 {
 7         throw("malloc deadlock")
 8     }
 9 
10     t := itabTable
11     if t.count >= 3*(t.size/4) { // 75% 負載係數
12         // 增長哈希表。
13         // t2 = new(itabTableType)+一些其他條目我們撒謊並告訴malloc我們想要無指針的內存,因為所有指向的值都不在堆中。
14         t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
15         t2.size = t.size * 2
16 
17         // 複製條目。
18         //注意:在複製時,其他線程可能會尋找itab和找不到它。沒關係,他們將嘗試獲取Itab鎖,因此請等到複製完成。
19         if t2.count != t.count {
20             throw("mismatched count during itab table copy")
21         }
22         // 發布新的哈希表。使用原子寫入:請參閱getitab中的註釋。
23         atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
24         // 採用新表作為我們自己的表。
25         t = itabTable
26         // 注意:舊錶可以在此處進行GC處理。
27     }
28     t.add(m)
29 }
30 // add將給定的itab添加到itab表t中。
31 //必須保持itabLock。
32 func (t *itabTableType) add(m *itab) {
33     //請參閱註釋中的有關探查序列的註釋。
34     //將新的itab插入探針序列的第一個空位。
35     mask := t.size - 1
36     h := itabHashFunc(m.inter, m._type) & mask
37     for i := uintptr(1); ; i++ {
38         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
39         m2 := *p
40         if m2 == m {
41             //給定的itab可以在多個模塊中使用並且由於全局符號解析的工作方式,
42             //指向itab的代碼可能已經插入了全局“哈希”。
43             return
44         }
45         if m2 == nil {
46             // 在這裏使用原子寫,所以如果讀者看到m,它也會看到正確初始化的m字段。
47             // NoWB正常,因為m不在堆內存中。
48             // *p = m
49             atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
50             t.count++
51             return
52         }
53         h += I
54         h &= mask
55     }
56 }

  可以看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。然後使itabTable指向新的表,最後把新的itab插入到新表中。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

ThreadLocal 源碼解析

1{icon} {views}

在activeJDBC框架內部的實現中看到了 ThreadLocal 這個類,記錄下了每個線程獨有的連接

private static final ThreadLocal<HashMap<String, Connection>> connectionsTL = new ThreadLocal<>();

感覺是個知識點,就打開源碼看看了。先看一下源碼里的解釋

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

這個鳥文,瞎翻譯一下,就是:

這個類提供了供線程專享的變量。這些變量不同與其它普通的變量,它是每個線程都有一個自己的獨立初始化的變量(通過get和set方法實現)。這個類的實例常用於類的私有靜態字段,以實現每個線程都有自己的狀態(例如userId,事務ID等)。

先跑一下用法吧,

package com.test.threadlocal;

public class TestController {
    
    private static int index = 0;
    private static String str = "這個字符串是每個線程共享的";
    // 這個變量,看似是一個類的靜態屬性,實則是每個線程有自己獨有的區域
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "main線程專享";
        }
    };      
    
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 3; i++) {
            Thread t = new MyThread();
            t.start();
            t.join();
        }
        
        System.out.println(str);
        System.out.println(threadStr.get());
    }
    
    static class MyThread extends Thread{

        @Override
        public void run() {
            index++;
            str = "第" + index + "個str";
            threadStr.set("第" + index + "個threadStr");
        }
        
        
    }

}

這個例子中,從str和threadStr變量的打印結果可以看出來。str被所有的線程讀和寫,threadStr在每個線程內部開闢了一塊線程專享的區域。接下來,我們看一下具體實現。
先看一下構造函數

     /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();
     /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

構造函數是空的,但是,該類有一個私有整型常量threadLocalHashCodenextHashCode()方法我們就不看了,省的一如源碼深似海。看鳥文的話,大概就是每new一個ThreadLocal變量的時候,就會生成一個散列碼,該碼非極端情況下與某個整數取模后不容易衝突(這句話有點迷吧,其實我也不懂)
然後看一下set方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

容易看出,這個方法設置每個線程自己的value,相當於當前線程是key,然後得出一個ThreadLocalMap。顯然,這個map用來保存線程內部的值,既然是map當然每個線程可以保存多個數值了,該map的value我們猜一下就是我要保存的具體的值,估計是用Object類聲明的。那key是什麼呢?我們看下ThreadLocalMap類的構造方法。

/**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
                
/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

我的天!這個類沒有繼承我們想象中的HashMap,或者是ConcurrentMap,但是看過,Map的內部實現的同學應該可以發現,這個Map的實現和HashMap的實現簡直就是小巫見大巫,有沒有。它在構造函數中做了如下幾步:

  1. 初始化一個大小為16的Entry數組
  2. 通過上面說過的很迷的HashCode散列值與15取模得到將要存儲在數組中的索引值
  3. 構造Entry,然後保存進去
  4. 長度設置為1
  5. 設置要擴容的限制大小為16的2/3

我們看到這個不就是用數組實現的Map嘛,看過HashMap實現的我們,覺得洒洒水啦。
Map的set和get方法就不分析了。ThreadLocal的get方法我們還是要貼出來的,畢竟是我們主要分析的東西

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可見,是獲取到當前線程,用作key獲取到Map,然後用當前this獲取到Entry實體。最後當然獲取到了存儲的value。

我編碼,我快樂~

本文由博客一文多發平台 發布!

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=19l4zaaz7g6ct

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

本田計畫到2030年新能源汽車比例將達到三分之二

據《財富》網路版報導,本田週三表示,計畫把新能源汽車比例由當前的5%提高到2030年的三分之二。

本田在發佈的最新戰略中稱,在未來不到15年,油田混合動力、插電式混合動力、電池動力和燃料電池動力汽車產量將超過燃油動力汽車。

本田首席執行官八鄉隆弘(Takahiro Hachigo)說,插電式混合動力汽車將是公司“未來電氣化的核心”。

八鄉隆弘說,本田將於2018年在北美推出一款插電式混合動力汽車,然後會推出主要車型的插電式版本,並增加車型數量。
 

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

Tim Cook:汽車是蘋果給用戶準備的聖誕禮物

在 2016 年蘋果公司股東大會上,該公司 CEO Tim Cook 在回答股東關於蘋果汽車的詢問時,他表示用戶等待這一款新品就是等待聖誕禮物,現在距離耶誕節還有一段時間。蘋果公司已經從 Tesla、Ford、GM、Samsung 等公司招募了數百名工程師,組成了電動車研發團隊。

蘋果正在研發電動車已經不是什麼新聞了,但一直沒有官方消息證實,該公司 CEO Tim Cook 在股東大會上回應了股東有關蘋果汽車的問題,他表示現在就像兒童時代,等待平安夜的時間,期待將收到什麼禮物,未來一段時間,就像是等待禮物的平安夜。這意味蘋果公司正在研發電動汽車,但上市還需要等一段時間。   除了電動汽車外,此次股東大會還討論其他議題,蘋果在 2016 的研發支出為 85 億美元,並收購了 19 家公司,目前該公司的員工總數大約為 11.6 萬人。關於近日廣受關注的 FBI 要求解鎖 iPhone 的事件,Tim Cook 再次重申了蘋果公司立場,堅決擁護用戶的個人隱私和資訊安全。蘋果公司的新總部即將完工,大約在 2017 年 1 月部分員工就將搬入新的員工,這是 Steve Jobs 生前最關注的專案之一。

(首圖來源: CC BY 2.0)    (本文授權轉載自《》─〈〉)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

[springboot 開發單體web shop] 6. 商品分類和輪播廣告展示

1{icon} {views}

目錄

商品分類&輪播廣告

因最近又被困在了OSGI技術POC,更新進度有點慢,希望大家不要怪罪哦。

我們實現了登錄之後前端的展示,如:

接着,我們來實現左側分類欄目的功能。

商品分類|ProductCategory

從上圖我們可以看出,商品的分類其實是有層級關係的,而且這種關係一般都是無限層級。在我們的實現中,為了效果的展示,我們僅僅是展示3級分類,在大多數的中小型電商系統中,三級分類完全足夠應對SKU的分類。

需求分析

先來分析分類都包含哪些元素,以jd為例:

  • logo(logo) 有的分類文字前面會有小標
  • 分類展示主圖(img_url)
  • 主標題(title)
  • 副標題/Slogan
  • 圖片跳轉地址(img_link_url)– 大多數時候我們點擊分類都會分類Id跳轉到固定的分類商品列表展示頁面,但是在一些特殊的場景,比如我們要做一個活動,希望可以點擊某一個分類的主圖直接定位到活動頁面,這個url就可以使用了。
  • 上級分類(parent_id)
  • 背景色(bg_color)
  • 順序(sort)
  • 當前分類級別(type)

開發梳理

在上一小節,我們簡單分析了一下要實現商品分類的一些points,那麼我們最好在每次拿到需求【開發之前】,對需求進行拆解,然後分解開發流程,這樣可以保證我們更好的理解需求,以及在開發之前發現一部分不合理的需求,並且如果需求設計不合理的話,開發人員完全有權,也有責任告知PM。大家的終極目的都是為了我們做的產品更加合理,好用,受歡迎!

  • 首次展示,僅僅讀取一級分類(Root)
  • 根據一級分類查詢二三級子分類

編碼實現

查詢一級分類

Service實現

1.在com.liferunner.service中創建service 接口ICategoryService.java, 編寫查詢所有一級分類的方法getAllRootCategorys,如下:

package com.liferunner.service;
import com.liferunner.dto.CategoryResponseDTO;
import com.liferunner.dto.SecondSubCategoryResponseDTO;
import java.util.List;
/**
 * ICategoryService for : 分類service
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/13
 */
public interface ICategoryService {
    /**
     * 獲取所有有效的一級分類(根節點)
     *
     * @return
     */
    List<CategoryResponseDTO> getAllRootCategorys();
}

2.編寫實現類com.liferunner.service.ICategoryService.java

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;
    
    @Override
    public List<CategoryResponseDTO> getAllRootCategorys() {
        Example example = new Example(Category.class);
        val conditions = example.createCriteria();
        conditions.andEqualTo("type", CategoryTypeEnum.ROOT.type);
        val categoryList = this.categoryMapper.selectByExample(example);
        //聲明返回對象
        List<CategoryResponseDTO> categoryResponseDTOS = new ArrayList<>();
        if (!CollectionUtils.isEmpty(categoryList)) {
            //賦值
            CategoryResponseDTO dto;
            for (Category category : categoryList) {
                dto = new CategoryResponseDTO();
                BeanUtils.copyProperties(category, dto);
                categoryResponseDTOS.add(dto);
            }
        }
        return categoryResponseDTOS;
    }
}

上述代碼很好理解,創建tk.mybatis.mapper.entity.Example,將條件傳入,然後使用通用Mapper查詢到type=1的一級分類,接着將查到的對象列錶轉換為DTO對象列表。

Controller實現

一般情況下,此類查詢都會出現在網站的首頁,因此我們來創建一個com.liferunner.api.controller.IndexController,並對外暴露一個查詢一級分類的接口:

package com.liferunner.api.controller;

import com.liferunner.service.ICategoryService;
import com.liferunner.service.IProductService;
import com.liferunner.service.ISlideAdService;
import com.liferunner.utils.JsonResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import java.util.Collections;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
 * IndexController for : 首頁controller
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/12
 */
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;

    @GetMapping("/rootCategorys")
    @ApiOperation(value = "查詢一級分類", notes = "查詢一級分類")
    public JsonResponse findAllRootCategorys() {
        log.info("============查詢一級分類==============");
        val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============一級分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
}
Test API

編寫完成之後,我們需要對我們的代碼進行測試驗證,還是通過使用RestService插件來實現,當然,大家也可以通過Postman來測試。

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 1,
      "name": "煙酒",
      "type": 1,
      "parentId": 0,
      "logo": "img/cake.png",
      "slogan": "吸煙受害健康",
      "catImage": "http://www.life-runner.com/shop/category/cake.png",
      "bgColor": "#fe7a65"
    },
    {
      "id": 2,
      "name": "服裝",
      "type": 1,
      "parentId": 0,
      "logo": "img/cookies.png",
      "slogan": "我選擇我喜歡",
      "catImage": "http://www.life-runner.com/shop/category/cookies.png",
      "bgColor": "#f59cec"
    },
    {
      "id": 3,
      "name": "鞋帽",
      "type": 1,
      "parentId": 0,
      "logo": "img/meat.png",
      "slogan": "飛一般的感覺",
      "catImage": "http://www.life-runner.com/shop/category/meat.png",
      "bgColor": "#b474fe"
    }
  ],
  "ok": true
}

根據一級分類查詢子分類

因為根據一級id查詢子分類的時候,我們是在同一張表中做自連接查詢,因此,通用mapper已經不適合我們的使用,因此我們需要自定義mapper來實現我們的需求。

自定義Mybatis Mapper實現

在之前的編碼中,我們都是使用的插件幫我們實現的通用Mapper,但是這種查詢只能處理簡單的單表CRUD,一旦我們需要SQL 包含一部分邏輯處理的時候,那就必須得自己來編寫了,let’s code.
1.在項目mscx-shop-mapper中,創建一個新的custom package,在該目錄下創建自定義mappercom.liferunner.custom.CategoryCustomMapper

public interface CategoryCustomMapper {
    List<SecondSubCategoryResponseDTO> getSubCategorys(Integer parentId);
}

2.resources目錄下創建目錄mapper.custom,以及創建和上面的接口相同名稱的XML文件mapper/custom/CategoryCustomMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liferunner.custom.CategoryCustomMapper">
    <resultMap id="subCategoryDTO" type="com.liferunner.dto.SecondSubCategoryResponseDTO">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="type" jdbcType="INTEGER" property="type"/>
        <result column="parentId" jdbcType="INTEGER" property="parentId"/>
        <collection property="thirdSubCategoryResponseDTOList" ofType="com.liferunner.dto.ThirdSubCategoryResponseDTO">
            <id column="subId" jdbcType="INTEGER" property="subId"/>
            <result column="subName" jdbcType="VARCHAR" property="subName"/>
            <result column="subType" jdbcType="INTEGER" property="subType"/>
            <result column="subParentId" jdbcType="INTEGER" property="subParentId"/>
        </collection>
    </resultMap>
    <select id="getSubCategorys" resultMap="subCategoryDTO" parameterType="INTEGER">
        SELECT p.id as id,p.`name` as `name`,p.`type` as `type`,p.father_id as parentId,
        c.id as subId,c.`name` as subName,c.`type` as subType,c.parent_id as subParentId
        FROM category p
        LEFT JOIN category c
        ON p.id = c.parent_id
        WHERE p.parent_id = ${parentId};
    </select>
</mapper>

TIPS
上述創建的package,一定要在項目的啟動類com.liferunner.api.ApiApplication中修改@MapperScan(basePackages = { "com.liferunner.mapper", "com.liferunner.custom"}),如果不把我們的custom package加上,會造成掃描不到而報錯。

在上面的xml中,我們定義了兩個DTO對象,分別用來處理二級和三級分類的DTO,實現如下:

@Data
@ToString
public class SecondSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer id;

    /**
     * 分類名稱
     */
    private String name;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer type;

    /**
     * 父id
     */
    private Integer parentId;

    List<ThirdSubCategoryResponseDTO> thirdSubCategoryResponseDTOList;
}
---
    
@Data
@ToString
public class ThirdSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer subId;

    /**
     * 分類名稱
     */
    private String subName;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer subType;

    /**
     * 父id
     */
    private Integer subParentId;
}
Service實現

編寫完自定義mapper之後,我們就可以繼續編寫service了,在com.liferunner.service.ICategoryService中新增一個方法:getAllSubCategorys(parentId).如下:

public interface ICategoryService {
    ...
    /**
     * 根據一級分類獲取子分類
     *
     * @param parentId 一級分類id
     * @return 子分類list
     */
    List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId);
}

com.liferunner.service.impl.CategorySericeImpl實現上述方法:

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private CategoryCustomMapper categoryCustomMapper;
    ...
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId) {
        return this.categoryCustomMapper.getSubCategorys(parentId);
    }
}
Controller實現
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;
    ...
    @GetMapping("/subCategorys/{parentId}")
    @ApiOperation(value = "查詢子分類", notes = "根據一級分類id查詢子分類")
    public JsonResponse findAllSubCategorys(
            @ApiParam(name = "parentId", value = "一級分類id", required = true)
            @PathVariable Integer parentId) {
        log.info("============查詢id = {}的子分類==============", parentId);
        val categoryResponseDTOS = this.categoryService.getAllSubCategorys(parentId);
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============子分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
}
Test API
{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 11,
      "name": "國產",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 37,
          "subName": "中華",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 38,
          "subName": "冬蟲夏草",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 39,
          "subName": "南京",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 40,
          "subName": "雲煙",
          "subType": 3,
          "subParentId": 11
        }
      ]
    },
    {
      "id": 12,
      "name": "外煙",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 44,
          "subName": "XXXXX",
          "subType": 3,
          "subParentId": 12
        },
        {
          "subId": 45,
          "subName": "RRRRR",
          "subType": 3,
          "subParentId": 12
        }
      ]
    }
  ],
  "ok": true
}

以上我們就已經實現了和jd類似的商品分類的功能實現。

輪播廣告|SlideAD

需求分析

這個就是jd或者tb首先的最頂部的廣告圖片是一樣的,每隔1秒自動切換圖片。接下來我們分析一下輪播圖中都包含哪些信息:

  • 圖片(img_url)是最基本的
  • 圖片跳轉連接(img_link_url),這個是在我們點擊這個圖片的時候需要跳轉到的頁面
  • 有的可以直接跳轉到商品詳情頁面
  • 有的可以直接跳轉到某一分類商品列表頁面
  • 輪播圖的播放順序(sort)

開發梳理

直接查詢出所有的有效的輪播圖片,並且進行排序

編碼實現

Service 實現

和商品分類實現一樣,在mscx-shop-service中創建com.liferunner.service.ISlideAdService並實現,代碼如下:

public interface ISlideAdService {
    /**
     * 查詢所有可用廣告並排序
     * @param isShow
     * @return
     */
    List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking);
}
@Service
@Slf4j
public class SlideAdServiceImpl implements ISlideAdService {

    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }

    @Override
    public List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking) {
        Example example = new Example(SlideAds.class);
        //設置排序
        if (StringUtils.isBlank(sortRanking)) {
            example.orderBy("sort").asc();
        } else {
            example.orderBy("sort").desc();
        }
        val conditions = example.createCriteria();
        conditions.andEqualTo("isShow", isShow);
        val slideAdsList = this.slideAdsMapper.selectByExample(example);
        //聲明返回對象
        List<SlideAdResponseDTO> slideAdResponseDTOList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(slideAdsList)) {
            //賦值
            SlideAdResponseDTO dto;
            for (SlideAds slideAds : slideAdsList) {
                dto = new SlideAdResponseDTO();
                BeanUtils.copyProperties(slideAds, dto);
                slideAdResponseDTOList.add(dto);
            }
        }
        return slideAdResponseDTOList;
    }
}

從上述可以看到,這裏我使用的是構造函數注入SlideAdsMapper,其餘代碼單表查詢沒什麼特別的,根據條件查詢輪播圖,並返回結果,返回的對象是com.liferunner.dto.SlideAdResponseDTO列表,代碼如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "輪播廣告返回DTO", description = "輪播廣告返回DTO")
public class SlideAdResponseDTO{
    /**
     * 主鍵
     */
    private String id;

    /**
     * 圖片地址
     */
    private String imageUrl;

    /**
     *  背景顏色
     */
    private String backgroundColor;

    /**
     * 商品id
     */
    private String productId;

    /**
     * 商品分類id
     */
    private String catId;

    /**
     * 圖片跳轉URL
     */
    private String imageLinkUrl;

    /**
     * 輪播圖類型 用於判斷,可以根據商品id或者分類進行頁面跳轉,1:商品 2:分類 3:鏈接url
     */
    private Integer type;

    /**
     * 輪播圖展示順序 輪播圖展示順序,從小到大
     */
    private Integer sort;

    /**
     * 是否展示 是否展示,1:展示    0:不展示
     */
    private Integer isShow;

    /**
     * 創建時間 創建時間
     */
    private Date createTime;

    /**
     * 更新時間 更新
     */
    private Date updateTime;
}

Controller實現

com.liferunner.api.controller.IndexController中,新添加一個查詢輪播圖API,代碼如下:

    @Autowired
    private ISlideAdService slideAdService;

    @GetMapping("/slideAds")
    @ApiOperation(value = "查詢輪播廣告", notes = "查詢輪播廣告接口")
    public JsonResponse findAllSlideList() {
        log.info("============查詢所有輪播廣告,isShow={},sortRanking={}=============="
                , 1, "desc");
        val slideAdsList = this.slideAdService.findAll(1, "desc");
        if (CollectionUtils.isEmpty(slideAdsList)) {
            log.info("============未查詢到任何輪播廣告==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============輪播廣告查詢result:{}=============="
                , slideAdsList);
        return JsonResponse.ok(slideAdsList);
    }

Test API

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": "slide-100002",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmH6AeuRrAAEZviPhyQ0768.png",
      "backgroundColor": "#55be59",
      "productId": "",
      "catId": "133",
      "type": 2,
      "sort": 2,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    },
    {
      "id": "slide-100003",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmHuAPlXvAAFe-H5_-Nw961.png",
      "backgroundColor": "#ff9801",
      "productId": "y200008",
      "catId": "",
      "type": 1,
      "sort": 1,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    }
  ],
  "ok": true
}

福利講解

在我們的實現代碼中,有心的同學可以看到,我使用了3種不同的Bean注入方式:

  • 屬性注入
    @Autowired
    private ISlideAdService slideAdService;
  • 構造函數注入
    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }
  • Lombok插件注入(本質也是構造器注入,代碼會動態生成。)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ProductServiceImpl implements IProductService {
    // RequiredArgsConstructor 構造器注入
    private final ProductCustomMapper productCustomMapper;
    private final ProductsMapper productsMapper;
    ...
}

那麼,這幾種注入都有什麼區別呢?首先我們下了解一下Spring的注入是干什麼的?

Spring提出了依賴注入的思想,即依賴類不由程序員實例化,而是通過Spring容器幫我們new指定實例並且將實例注入到需要該對象的類中。
依賴注入的另一種說法是”控制反轉”。通俗的理解是:平常我們new一個實例,這個實例的控制權是我們程序員, 而控制反轉是指new實例工作不由我們程序員來做而是交給Spring容器來做。

在傳統的SpringMVC中,大家使用的都是XML注入,比如:

<!--配置bean,配置后該類由spring管理--> 
<bean name="CategorySericeImpl" class="com.liferunner.service.impl.CategorySericeImpl"> 
<!--注入配置當前類中相應的屬性--> 
<property name="categoryMapper" ref="categoryMapper"></property> 
</bean> 
<bean name="categoryMapper" class="com.liferunner.mapper.CategoryMapper"></bean>

注入之後,使用@Autowired,我們可以很方便的自動從IOC容器中查找屬性,並返回。

@Autowired的原理
在啟動spring IoC時,容器自動裝載了一個AutowiredAnnotationBeanPostProcessor後置處理器,當容器掃描到@Autowied、@Resource或@Inject時,就會在IoC容器自動查找需要的bean,並裝配給該對象的屬性。
注意事項:
在使用@Autowired時,首先在容器中查詢對應類型的bean
如果查詢結果剛好為一個,就將該bean裝配給@Autowired指定的數據
如果查詢的結果不止一個,那麼@Autowired會根據名稱來查找。
如果查詢的結果為空,那麼會拋出異常。解決方法時,使用required=false

上述三種註解方式,其實本質上還是注入的2種:set屬性注入 & 構造器注入,使用方式都可以,根據個人喜好來用,本人喜歡使用lombok插件注入是因為,它將代碼整合在一起,更加符合我們Spring自動注入的規範。

源碼下載

下節預告

下一節我們將繼續開發我們電商的核心部分-商品列表和詳情展示,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

亞馬遜粉紅豚 外表夢幻屢遭漁夫獵捕

2{icon} {views}

摘錄自2020年3月5日公視報導

腹部泛著粉紅漂亮膚色的亞馬遜淡水豚,動保專家率領的團隊將牠撈捕上岸,進行檢測、採樣、烙印標記以利追蹤的作業後,再把牠放回大自然。亞馬遜淡水豚是世界上體型最大的淡水豚,成年後體色從深灰轉淡變成粉紅,當牠改變行為或經陽光照射,膚色也會跟著改變,就像人會臉紅一樣。

亞馬遜粉紅豚的孕育期長達13個月,河豚寶寶出生後,有兩年時間需要母親照顧,由於哺育期比較長,母河豚每隔三到五年才會生育,加上每年約有2500隻粉紅豚遭到漁夫獵殺,做為獵捕另一種大型鯰魚的誘餌,這種淡水豚已經列入易危名單。當地檢方在2015年提出禁捕鯰魚的禁令,希望保護粉紅豚,不過禁漁令上個月到期,動保專家疾呼應該展延。

動保專家擔心,如果不趕快採取有效行動,亞馬遜粉紅豚恐怕會重演長江白鱀豚於2006年滅絕的悲劇。

物種保育
海洋
生態保育
國際新聞
亞馬遜

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

德國SMA推出新型儲能2.5逆變器「陽光男孩」

1{icon} {views}

德國太陽能逆變器巨頭SMA推出其新型「陽光男孩」儲能2.5(Sunny Boy Storage 2.5)逆變器,可與特斯拉的Powerwall系統結合,將「陽光男孩」蓄電成本減半。

SMA表示,這款新型逆變器 Sunny Boy Storage 2.5將於5月份投入德國市場,而歐洲和澳大利亞推出時間更晚一些。

此高壓蓄電裝置是一個AC耦合定制串式逆變器,可讓電池與標準的太陽能串式逆變器相連,也可以和家庭和電網相連。「陽光男孩」儲能2.5容量為7kWh,通過德國批發商進行銷售。SMA表示,到今年中期,其他廠商的高壓電池將與SMA的電力解決方案相結合。

該SMA設備單相運行,最大放電功率為2.5kW。可以並聯,可以與Sunny Home Manager的能量管理共同協調,並且可以在戶外安裝。

相比於SMA現有的交流耦合儲能產品「陽光冰島」(Sunny Island)的價格,「陽光男孩」儲能2.5的售價低了56%。因為「陽光冰島」有些必備的部件對於儲能2.5而言根本不需要。根據「陽光冰島」目前網上商店的價格,「陽光男孩」儲能2.5售價不會超過1,000歐元,這對於儲能系統而言並不算貴。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!