環境篇:嘔心瀝血@CDH線上調優

環境篇:嘔心瀝血@CDH線上調優

  • 為什麼出這篇文章?

近期有很多公司開始引入大數據,由於各方資源有限,並不能合理分配服務器資源,和服務器選型,小恭弘=叶 恭弘這裏將工作中的總結出來,給新入行的小夥伴帶個方向,不敢說一定對,但是本人親自測試,發現集群使用率穩定提高了3分之1,最高可達到2分之1,有不對的地方歡迎留言指出。

注:可能有些服務沒有設計,使用到的小夥伴可以參照這種方式去規劃。

0 資源:集群服務安排

服務名稱 子服務 CM-64G ZK-Kafka(3台)-12G DataNode(3台)-64G NameNode1-64G NameNode2-64G Resourcemanager1-32G Resourcemanager2-32G hive-hbase-16G hive-hbase-16G
MySQL MySQL
CM Activity Monitor
Alert Publisher
Event Server
Host Monitor
Service Monitor




HDFS NameNode
DataNode
Failover Controller
JournalNode
X

X
X

X


X

X
X
X
Yarn NodeManager
Resourcemanager
JobHisoryServer

X
X
X

X

Zookeeper Zookeeper Server
Kafka Kafka Broker
Hive Hive Metastore Server
HiveServer2
Gateway(安裝對應應用服務器)
X


X

X
Hbase HMaster
HRegionServer
Thrift Server
X


X

X
Oozie Oozie Server
Hue Hue Server
Load Balancer
X

X
Spark History Server
Gateway(安裝對應應用服務器)

X
Flume Flume Agent (安裝對應應用服務器)
Sqoop Sqoop(安裝對應應用服務器)

1 優化:Cloudera Management

1.1 Cloudera Management Service

這些服務主要是提供監控功能,目前的調整主要集中在內存放,以便有足夠的資源 完成集群管理。

服務 選項 配置值
Activity Monitor Java Heap Size 2G
Alert Publisher Java Heap Size 2G
Event Server Java Heap Size 2G
Host Monitor Java Heap Size 4G
Service Monitor Java Heap Size 4G
Reports Manager Java Heap Size 2G
Navigator Metadata Server Java Heap Size 8G

2 優化:Zookeeper

服務 選項 配置值
Zookeeper Java Heap Size (堆棧大小) 4G
Zookeeper maxClientCnxns (最大客戶端連接數) 1024
Zookeeper dataDir (數據文件目錄+數據持久化路徑) /hadoop/zookeeper (建議獨立目錄)
Zookeeper dataLogDir (事務日誌目錄) /hadoop/zookeeper_log (建議獨立目錄)

3 優化:HDFS

3.1 磁盤測試

3.1.1 讀測試

hdparm 用於查看硬盤的相關信息或對硬盤進行測速、優化、修改硬盤相關參數設定

#安裝hdparm
yum install hdparm
#獲取硬盤符
fdisk -l
#讀測試(讀取上一步獲取硬盤符)
hdparm -t /dev/vda

三次測試結果:

Timing buffered disk reads: 500 MB in 0.84 seconds = 593.64 MB/sec

Timing buffered disk reads: 500 MB in 0.93 seconds = 538.80 MB/sec

Timing buffered disk reads: 500 MB in 0.74 seconds = 672.95 MB/sec

說明:接近1s秒讀取了500MB磁盤,讀速度約 500 MB/秒

3.1.2 寫測試

dd 這裏使用 time + dd 簡單測試寫速度,不要求很精確

查看內存緩存情況
free -m
 
清除緩存
sync; echo 3 > /proc/sys/vm/drop_caches
 
查block size
blockdev --getbsz /dev/vda
 
寫測試
echo 3 > /proc/sys/vm/drop_caches; time dd if=/dev/zero of=/testdd bs=4k count=100000

三次測試結果:

記錄了100000+0 的讀入
記錄了100000+0 的寫出

409600000 bytes (410 MB) copied, 0.574066 s, 714 MB/s –410MB複製,用時0.57秒,評估714M/s

409600000 bytes (410 MB) copied, 1.84421 s, 222 MB/s –410MB複製,用時1.84秒,評估222 M/s

409600000 bytes (410 MB) copied, 1.06969 s, 383 MB/s –410MB複製,用時1.06秒,評估383M/s

3.1.3 網絡帶寬

iperf3測量一個網絡最大帶寬

#安裝iperf3
yum -y install iperf3
 
#服務端
iperf3 -s
 
#客戶端
iperf3 -c 上調命令執行的服務機器IP

測試結果:

[ ID]–>線程id Interva–>傳輸時間 Transfer–>接收數據大小 Bandwidth–>帶寬每秒大小 Retr 角色
[ 4] 0.00-10.00 sec 17.0 GBytes 14.6 Gbits/sec 0 sender–>發送
[ 4] 0.00-10.00 sec 17.0 GBytes 14.6 Gbits/sec receiver–>接收

3.2 官方壓測

3.2.1 用戶準備

由於只能使用yarn配置了允許用戶,故這裏選擇hive用戶,如果su hive不能進入,則需要配置該步驟

usermod -s /bin/bash  hive
su hive

3.2.2 HDFS 寫性能測試

  • 測試內容:HDFS集群寫入10個128M文件(-D指定文件存儲目錄)
hadoop  jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar  TestDFSIO  -D test.build.data=/test/benchmark -write -nrFiles 10 -fileSize 128

INFO fs.TestDFSIO: —– TestDFSIO —– : write
INFO fs.TestDFSIO: Date & time: Thu Jun 11 10:30:36 CST 2020
INFO fs.TestDFSIO: Number of files: 10 –十個文件
INFO fs.TestDFSIO: Total MBytes processed: 1280 –總大小1280M
INFO fs.TestDFSIO: Throughput mb/sec: 16.96 –吞吐量 每秒16.96M
INFO fs.TestDFSIO: Average IO rate mb/sec: 17.89 –平均IO情況17.89M
INFO fs.TestDFSIO: IO rate std deviation: 4.74 –IO速率標準偏差
INFO fs.TestDFSIO: Test exec time sec: 46.33 –總運行時間

3.2.3 HDFS 讀性能測試

  • 測試內容:HDFS集群讀取10個128M文件
hadoop jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar TestDFSIO  -D test.build.data=/test/benchmark -read -nrFiles 10 -fileSize 128

INFO fs.TestDFSIO: —– TestDFSIO —– : read
INFO fs.TestDFSIO: Date & time: Thu Jun 11 10:41:19 CST 2020
INFO fs.TestDFSIO: Number of files: 10 –文件數
INFO fs.TestDFSIO: Total MBytes processed: 1280 –總大小
INFO fs.TestDFSIO: Throughput mb/sec: 321.53 –吞吐量 每秒321.53M
INFO fs.TestDFSIO: Average IO rate mb/sec: 385.43 –平均IO情況385.43M
INFO fs.TestDFSIO: IO rate std deviation: 107.67 –IO速率標準偏差
INFO fs.TestDFSIO: Test exec time sec: 20.81 –總運行時間

3.2.4 刪除測試數據

hadoop jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar TestDFSIO  -D test.build.data=/test/benchmark -clean

3.3 參數調優

服務 選項 配置值
NameNode Java Heap Size (堆棧大小) 56G
NameNode dfs.namenode.handler.count (詳見3.3.2) 80
NameNode dfs.namenode.service.handler.count (詳見3.3.2) 80
NameNode fs.permissions.umask-mode (使用默認值022) 027(使用默認值022)
DataNode Java Heap Size (堆棧大小) 8G
DataNode dfs.datanode.failed.volumes.tolerated (詳見3.3.3) 1
DataNode dfs.datanode.balance.bandwidthPerSec (DataNode 平衡帶寬) 100M
DataNode dfs.datanode.handler.count (服務器線程數) 64
DataNode dfs.datanode.max.transfer.threads (最大傳輸線程數) 20480
JournalNode Java Heap Size (堆棧大小) 1G

3.3.1 數據塊優化

dfs.blocksize = 128M

  • 文件以塊為單位進行切分存儲,塊通常設置的比較大(最小6M,默認128M),根據網絡帶寬計算最佳值。
  • 塊越大,尋址越快,讀取效率越高,但同時由於MapReduce任務也是以塊為最小單位來處理,所以太大的塊不利於於對數據的并行處理。
  • 一個文件至少佔用一個塊(如果一個1KB文件,佔用一個塊,但是佔用空間還是1KB)
  • 我們在讀取HDFS上文件的時候,NameNode會去尋找block地址,尋址時間為傳輸時間的1%時,則為最佳狀態。
    • 目前磁盤的傳輸速度普遍為100MB/S
    • 如果尋址時間約為10ms,則傳輸時間=10ms/0.01=1000ms=1s
    • 如果傳輸時間為1S,傳輸速度為100MB/S,那麼一秒鐘我們就可以向HDFS傳送100MB文件,設置塊大小128M比較合適。
    • 如果帶寬為200MB/S,那麼可以將block塊大小設置為256M比較合適。

3.3.2 NameNode 的服務器線程的數量

  • dfs.namenode.handler.count=20*log2(Cluster Size),比如集群規模為16 ,8以2為底的對數是4,故此參數設置為80
  • dfs.namenode.service.handler.count=20*log2(Cluster Size),比如集群規模為16 ,8以2為底的對數是4,故此參數設置為80

NameNode有一個工作線程池,用來處理不同DataNode的併發心跳以及客戶端併發的元數據操作。該值需要設置為集群大小的自然對數乘以20,。

3.3.3 DataNode 停止提供服務前允許失敗的卷的數量

DN多少塊盤損壞后停止服務,默認為0,即一旦任何磁盤故障DN即關閉。 對盤較多的集群(例如DN有超過2塊盤),磁盤故障是常態,通常可以將該值設置為1或2,避免頻繁有DN下線。

4 優化:YARN + MapReduce

服務 選項 配置值 參數說明
ResourceManager Java Heap Size (堆棧大小) 4G
ResourceManager yarn.scheduler.minimum-allocation-mb (最小容器內存) 2G 給應用程序 Container 分配的最小內存
ResourceManager yarn.scheduler.increment-allocation-mb (容器內存增量) 512M 如果使用 Fair Scheduler,容器內存允許增量
ResourceManager yarn.scheduler.maximum-allocation-mb (最大容器內存) 32G 給應用程序 Container 分配的最大內存
ResourceManager yarn.scheduler.minimum-allocation-vcores (最小容器虛擬 CPU 內核數量) 1 每個 Container 申請的最小 CPU 核數
ResourceManager yarn.scheduler.increment-allocation-vcores (容器虛擬 CPU 內核增量) 1 如果使用 Fair Scheduler,虛擬 CPU 內核允許增量
ResourceManager yarn.scheduler.maximum-allocation-vcores (最大容器虛擬 CPU 內核數量) 16 每個 Container 申請的最大 CPU 核數
ResourceManager yarn.resourcemanager.recovery.enabled true 啟用后,ResourceManager 中止時在群集上運行的任何應用程序將在 ResourceManager 下次啟動時恢復,備註:如果啟用 RM-HA,則始終啟用該配置。
NodeManager Java Heap Size (堆棧大小) 4G
NodeManager yarn.nodemanager.resource.memory-mb 40G 可分配給容器的物理內存數量,參照資源池內存90%左右
NodeManager yarn.nodemanager.resource.cpu-vcores 32 可以為容器分配的虛擬 CPU 內核的數量,參照資源池內存90%左右
ApplicationMaster yarn.app.mapreduce.am.command-opts 右紅 傳遞到 MapReduce ApplicationMaster 的 Java 命令行參數 “-Djava.net.preferIPv4Stack=true
ApplicationMaster yarn.app.mapreduce.am.resource.mb (ApplicationMaster 內存) 4G
JobHistory Java Heap Size (堆棧大小) 2G
MapReduce mapreduce.map.memory.mb (Map 任務內存) 4G 一個MapTask可使用的資源上限。如果MapTask實際使用的資源量超過該值,則會被強制殺死。
MapReduce mapreduce.reduce.memory.mb (Reduce 任務內存) 8G 一個 ReduceTask 可使用的資源上限。如果 ReduceTask 實際使用的資源量超過該值,則會被強制殺死
MapReduce mapreduce.map.cpu.vcores 2 每個 MapTask 可使用的最多 cpu core 數目
MapReduce mapreduce.reduce.cpu.vcores 4 每個 ReduceTask 可使用的最多 cpu core 數目
MapReduce mapreduce.reduce.shuffle.parallelcopies 20 每個 Reduce 去 Map 中取數據的并行數。
MapReduce mapreduce.task.io.sort.mb(Shuffle 的環形緩衝區大小) 512M 當排序文件時要使用的內存緩衝總量。注意:此內存由 JVM 堆棧大小產生(也就是:總用戶 JVM 堆棧 – 這些內存 = 總用戶可用堆棧空間)
MapReduce mapreduce.map.sort.spill.percent 80% 環形緩衝區溢出的閾值
MapReduce mapreduce.task.timeout 10分鐘 Task 超時時間,經常需要設置的一個參數,該參數表 達的意思為:如果一個 Task 在一定時間內沒有任何進 入,即不會讀取新的數據,也沒有輸出數據,則認為 該 Task 處於 Block 狀態,可能是卡住了,也許永遠會 卡住,為了防止因為用戶程序永遠 Block 住不退出, 則強制設置了一個該超時時間。如果你的程序對每條輸入數據的處理時間過長(比如會訪問數據庫,通過網絡拉取數據等),建議將該參數調大,該參數過小常出現的錯誤提示是 :AttemptID:attempt_12267239451721_123456_m_00 0335_0 Timed out after 600 secsContainer killed by the ApplicationMaster。

5 優化:Impala

服務 選項 配置值 參數說明
Impala Daemon mem_limit (內存限制) 50G 由守護程序本身強制執行的 Impala Daemon 的內存限制。
如果達到該限制,Impalad Daemon 上運行的查詢可能會被停止
Impala Daemon Impala Daemon JVM Heap 512M 守護進程堆棧大小
Impala Daemon scratch_dirs 節點上多塊獨立磁盤(目錄) Impala Daemon 將溢出信息等數據寫入磁盤以釋放內存所在的目錄。這可能是大量數據
Impala Catalog Server Java Heap Size 8G 堆棧大小

6 優化:Kafka

6.1 官方壓測

6.1.1 Kafka Producer 壓力測試

  • record-size 是一條信息有多大,單位是字節。
  • num-records 是總共發送多少條信息。
  • throughput 是每秒多少條信息,設成-1,表示不限流,可測出生產者最大吞吐量。
bash /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/lib/kafka//bin/kafka-producer-perf-test.sh --topic test --record-size 100 --num-records 100000 --throughput -1 --producer-props bootstrap.servers=cdh01.cm:9092,cdh02.cm:9092,cdh03.cm:9092

100000 records sent, 225733.634312 records/sec (21.53 MB/sec),

8.20 ms avg latency, 66.00 ms max latency,

3 ms 50th, 28 ms 95th, 30 ms 99th, 30 ms 99.9th.

參數解析:一共寫入 10w 條消息,吞吐量為 21.53 MB/sec,每次寫入的平均延遲

為 8.20 毫秒,最大的延遲為 66.00 毫秒。

6.1.2 Kafka Consumer 壓力測試

  • zookeeper 指定 zookeeper 的鏈接信息
  • topic 指定 topic 的名稱
  • fetch-size 指定每次 fetch 的數據的大小
  • messages 總共要消費的消息個數
bash /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/lib/kafka//bin/kafka-consumer-perf-test.sh --broker-list cdh01.cm:9092,cdh02.cm:9092,cdh03.cm:9092 --topic test --fetch-size 10000 --messages 10000000 --threads 1

start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec

2020-06-11 17:53:48:179, 2020-06-11 17:54:04:525, 57.2205, 3.5006, 600000, 36706.2278, 3051, 13295, 4.3039, 45129.7480

start.time:2020-06-11 17:53:48:179 開始時間

end.time:2020-06-11 17:54:04:525 結束時間(用時16秒)

data.consumed.in.MB:57.2205 消費57M數據

MB.sec:3.5006 3.5M/S

data.consumed.in.nMsg:600000 消費60萬消息

nMsg.sec:36706.2278 36706條消息/S

rebalance.time.ms:3051 平衡時間3S

fetch.time.ms:13295 抓取時間13S

fetch.MB.sec:4.3039 一秒抓取4.3M

fetch.nMsg.sec:45129.7480 一秒抓取45129條消息

開始測試時間,測試結束數據,共消費數據57.2205MB,吞吐量 3.5M/S,共消費600000條,平均每秒消費36706.2278條。

6.1.3 Kafka 機器數量計算

Kafka 機器數量(經驗公式)= 2 X(峰值生產速度 X 副本數 /100)+ 1

先拿到峰值生產速度,再根據設定的副本數,就能預估出需要部署 Kafka 的數量。

比如我們的峰值生產速度是 50M/s。副本數為 2。

Kafka 機器數量 = 2 X( 50 X 2 / 100 )+ 1 = 3 台

6.2 參數調優

服務 選項 配置值 參數說明
Kafka Broker Java Heap Size of Broker 2G Broker堆棧大小
Kafka Broker Data Directories 多塊獨立磁盤
Kafka 服務 Maximum Message Size 10M 服務器可以接收的消息的最大大小。此屬性必須與使用者使用的最大提取大小同步。否則,不守規矩的生產者可能會發布太大而無法消費的消息
Kafka 服務 Replica Maximum Fetch Size 20M 副本發送給leader的獲取請求中每個分區要獲取的最大字節數。此值應大於message.max.bytes。
Kafka 服務 Number of Replica Fetchers 6 用於複製來自領導者的消息的線程數。增大此值將增加跟隨者代理中I / O并行度。

7 優化:HBase

服務 選項 配置值 參數說明
HBase Java Heap Size 18G 客戶端 Java 堆大小(字節)主要作用來緩存Table數據,但是flush時會GC,不要太大,根據集群資源,一般分配整個Hbase集群內存的70%,16->48G就可以了
HBase hbase.client.write.buffer 512M 寫入緩衝區大小,調高該值,可以減少RPC調用次數,單數會消耗更多內存,較大緩衝區需要客戶端和服務器中有較大內存,因為服務器將實例化已通過的寫入緩衝區並進行處理,這會降低遠程過程調用 (RPC) 的數量。
HBase Master Java Heap Size 8G HBase Master 的 Java 堆棧大小
HBase Master hbase.master.handler.count 300 HBase Master 中啟動的 RPC 服務器實例數量。
HBase RegionServer Java Heap Size 31G HBase RegionServer 的 Java 堆棧大小
HBase RegionServer hbase.regionserver.handler.count 100 RegionServer 中啟動的 RPC 服務器實例數量,根據集群情況,可以適當增加該值,主要決定是客戶端的請求數
HBase RegionServer hbase.regionserver.metahandler.count 60 用於處理 RegionServer 中的優先級請求的處理程序的數量
HBase RegionServer zookeeper.session.timeout 180000ms ZooKeeper 會話延遲(以毫秒為單位)。HBase 將此作為建議的最長會話時間傳遞給 ZooKeeper 仲裁
HBase RegionServer hbase.hregion.memstore.flush.size 1G 如 memstore 大小超過此值,Memstore 將刷新到磁盤。通過運行由 hbase.server.thread.wakefrequency 指定的頻率的線程檢查此值。
HBase RegionServer hbase.hregion.majorcompaction 0 合併周期,在合格節點下,Region下所有的HFile會進行合併,非常消耗資源,在空閑時手動觸發
HBase RegionServer hbase.hregion.majorcompaction.jitter 0 抖動比率,根據上面的合併周期,有一個抖動比率,也不靠譜,還是手動好
HBase RegionServer hbase.hstore.compactionThreshold 6 如在任意一個 HStore 中有超過此數量的 HStoreFiles,則將運行壓縮以將所有 HStoreFiles 文件作為一個 HStoreFile 重新寫入。(每次 memstore 刷新寫入一個 HStoreFile)您可通過指定更大數量延長壓縮,但壓縮將運行更長時間。在壓縮期間,更新無法刷新到磁盤。長時間壓縮需要足夠的內存,以在壓縮的持續時間內記錄所有更新。如太大,壓縮期間客戶端會超時。
HBase RegionServer hbase.client.scanner.caching 1000 內存未提供數據的情況下掃描儀下次調用時所提取的行數。較高緩存值需啟用較快速度的掃描儀,但這需要更多的內存且當緩存為空時某些下一次調用會運行較長時間
HBase RegionServer hbase.hregion.max.filesize 50G HStoreFile 最大大小。如果列組的任意一個 HStoreFile 超過此值,則託管 HRegion 將分割成兩個
HBase Master hbase.master.logcleaner.plugins 日誌清除器插件 org.apache.hadoop.hbase.master.cleaner.TimeToLiveLogCleaner
HBase hbase.replication false 禁用複製
HBase hbase.master.logcleaner.ttl 10min 保留 HLogs 的最長時間,加上如上兩條解決oldWALs增長問題

8 優化:Hive

服務 選項 配置值 參數說明
HiveServer2 Java Heap Size 4G
Hive MetaStore Java Heap Size 8G
Hive Gateway Java Heap Size 2G
Hive hive.execution.engine Spark 執行引擎切換
Hive hive.fetch.task.conversion more Fetch抓取修改為more,可以使全局查找,字段查找,limit查找等都不走計算引擎,而是直接讀取表對應儲存目錄下的文件,大大普通查詢速度
Hive hive.exec.mode.local.auto(hive-site.xml 服務高級配置,客戶端高級配置) true 開啟本地模式,在單台機器上處理所有的任務,對於小的數據集,執行時間可以明顯被縮短
Hive hive.exec.mode.local.auto.inputbytes.max(hive-site.xml 服務高級配置,客戶端高級配置) 50000000 文件不超過50M
Hive hive.exec.mode.local.auto.input.files.max(hive-site.xml 服務高級配置,客戶端高級配置) 10 個數不超過10個
Hive hive.auto.convert.join 開啟 在join問題上,讓小表放在左邊 去左鏈接(left join)大表,這樣可以有效的減少內存溢出錯誤發生的幾率
Hive hive.mapjoin.smalltable.filesize(hive-site.xml 服務高級配置,客戶端高級配置) 50000000 50M以下認為是小表
Hive hive.map.aggr 開啟 默認情況下map階段同一個key發送給一個reduce,當一個key數據過大時就發生數據傾斜。
Hive hive.groupby.mapaggr.checkinterval(hive-site.xml 服務高級配置,客戶端高級配置) 200000 在map端進行聚合操作的條目數目
Hive hive.groupby.skewindata(hive-site.xml 服務高級配置,客戶端高級配置) true 有數據傾斜時進行負載均衡,生成的查詢計劃會有兩個MR Job,第一個MR Job會將key加隨機數均勻的分佈到Reduce中,做部分聚合操作(預處理),第二個MR Job在根據預處理結果還原原始key,按照Group By Key分佈到Reduce中進行聚合運算,完成最終操作
Hive hive.exec.parallel(hive-site.xml 服務高級配置,客戶端高級配置) true 開啟并行計算
Hive hive.exec.parallel.thread.number(hive-site.xml 服務高級配置,客戶端高級配置) 16G 同一個sql允許的最大并行度,針對集群資源適當增加

9 優化:Oozie、Hue

服務 選項 配置值 參數說明
Oozie Java Heap Size 1G 堆棧大小
Hue Java Heap Size 4G 堆棧大小

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

Android官方新推的DI庫 Hilt

Android官方新推的DI庫 Hilt

Hilt是Google Android官方新推薦的依賴注入工具.
已加入到官方文檔: Dependency injection with Hilt. 目前是alpha release階段.

Hilt是在Dagger之上, Hilt單詞的意思是: 刀把, 柄.
代碼庫還是這個google/dagger.

Hilt的出現, 讓我想起了曾經曇花一現的dagger.android, 不知道hilt能不能經得住時間的考驗.

本文介紹Hilt的基本使用. 熟悉dagger的朋友可能會發現很容易.

Hilt是花里胡哨的小打小鬧? 還是下一個主流工具? 讓我們拭目以待.

Codelab練習例子

  • Codelab: Using Hilt in your Android app.

我的Fork: https://github.com/mengdd/android-hilt.

這個項目最開始是一個用ServiceLocator手動注入的例子, 功能是記錄用戶點擊button操作, 显示log. 有兩個Fragment和Activity.

通過Codelab中一系列步驟的改造, 最終改成用Hilt做依賴注入.

本文舉例就用其中的代碼片段了.

原repo還貼心地附上了改造前後的對比diff: Full codelab comparison.

Hilt依賴添加

project root: build.gradle:

buildscript {
    ext.hilt_version = '2.28-alpha'
    ...
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

需要Android 4.0以上.

app/build.gradle:

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    // Hilt dependencies
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

    // Hilt testing dependency
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    // Make Hilt generate code in the androidTest folder
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
}

後面兩個是測試的依賴.

概念, 用詞解釋

  • Container, 對應Component.
  • Bindings, 對應依賴.
The information Hilt has about how to provide instances of different types are also called bindings.
  • Component hierarchy是指依賴在當前component中可以用, 也可以在它包含的child component中用. 舉例: application容器中的依賴activity可以用, 但是反過來不行.

Hilt基本使用

  • @HiltAndroidApp: 標記在Application類上, 觸發代碼生成, application container是整個app的parent container.
@HiltAndroidApp
class LogApplication : Application()
  • @AndroidEntryPoint: 標記在Activity和Fragment上. 創建了一個和當前Activity/fragment生命周期相關的container. 目前支持的類型是: Activity, Fragment, View, Service, BroadcastReceiver.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

每次Fragment(或Activity等)創建都會有它對應的container實例.

  • 字段注入: 用@Inject標記字段, 注意字段不能是private的.
@AndroidEntryPoint
class LogsFragment : Fragment() {

    @Inject
    lateinit var dateFormatter: DateFormatter
    ...
}


Component lifetimes可以看到每種類型的component創建和注入的時間.

Activity是onCreate(), Fragment是onAttach().

對Fragment來說, 在onAttach()的時候完成了對象的注入, 之後訪問對象都沒有問題.

  • 提供依賴: 用@Inject標記構造器.
class DateFormatter @Inject constructor() {
    ...
}

它的依賴可以在構造參數中標明.

Hilt的Scope支持

默認所有的依賴都是沒有scope的, 每次注入依賴都創建新的實例.
Hilt自動在對應的生命周期創建/銷毀對象: Component lifetimes.

也可以把依賴scope到某個component, 這樣在這個component內, 依賴就是單例.

  • scope: @Singleton: application container的scope, 說明是application範圍內的單例. @ActivityScoped對應activity component.

所有可用的scope: Component scopes.

module

module的使用基本和dagger一樣, 用來提供一些無法用構造@Inject的依賴, 比如接口, 第三方庫類型, Builder模式構造的對象等.

  • @Module: 標記這是一個module. 在Kotlin代碼中, module可以是一個object.
  • @Provides: 標記方法, 提供返回值類型的依賴.
  • @Binds: 標記抽象方法, 返回接口類型, 實現是方法的唯一參數.

Hilt多了一個註解:

  • @InstallIn: 指明module對應哪個container, 也即Component.
    Generated components for Android classes
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(
            appContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }

    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
        return database.logDao()
    }
}

module的拆分

module的名字最好能傳達它提供了什麼類型的依賴, 所以多種依賴拆分多個modules寫比較好.

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}

@Binds@Provides不能放在同一個module里.
因為@Binds需要module是一個abstract class, 而@Provides需要module是一個object.
放一起會報錯: error: A @Module may not contain both non-static and abstract binding methods.

默認依賴

有一些默認依賴是已經有的, 比如:

  • @ApplicationContext.
  • @ActivityContext.

可以直接作為@Provides方法或@Inject構造的參數.

默認依賴是和component對應的.

Component default bindings

Qualifier

要提供同一個接口的不同實現, 可以用不同的註解來標記. (dagger之前用的是@Named).

A qualifier is an annotation used to identify a binding.

舉例: LoggerDataSource接口提供了內存和數據庫兩種實現.

定義兩個註解:

@Qualifier
annotation class InMemoryLogger

@Qualifier
annotation class DatabaseLogger

module中提供的時候用來標記相應的依賴:

@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {

    @DatabaseLogger
    @Singleton
    @Binds
    abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}

@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {

    @InMemoryLogger
    @ActivityScoped
    @Binds
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}

這裏用了兩個module因為它們對應兩個不同的component, 一個是application一個是activity, 依賴也是相應的scope.

注入的時候也對應加上:

@InMemoryLogger
@Inject
lateinit var logger: LoggerDataSource

@EntryPoint

Hilt支持最常用的Android組件, 對於默認不支持的類型, 如果還想做字段注入, 需要用@EntryPoint.

注意這裏只是限制了字段注入的情況, 自定義類型一般用構造注入比較方便(如果能用的話).

@EntryPoint的意思就是一個邊界點, 這裏可以通往Hilt的世界, 得到容器提供的依賴對象們.

Codelab中的例子是一個ContentProvider.

關鍵的部分是這段代碼:

@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
    fun logDao(): LogDao
}

private fun getLogDao(appContext: Context): LogDao {
    val hiltEntryPoint = EntryPointAccessors.fromApplication(
        appContext,
        LogsContentProviderEntryPoint::class.java
    )
    return hiltEntryPoint.logDao()
}

Hilt and Dagger

當初的dagger.android已被放棄, 它是為了簡化dagger在Android上的使用而單獨推出的. (根據Activity和Fragment的生命周期, 開發者不用手動調用inject方法, 但是確實最開始的setup code比較多.)

Hilt相對於Dagger來說, 簡化了幾個點:

  • 不用自己創建Component.
  • 不用手動調用inject()方法來完成字段注入.
  • 不用自己在Application中保存component.
  • 提供了一些Scope, 不用自己定義和寫.
  • 提供了一些默認依賴, 比如Context.

總體來說Hilt就是針對Android做的定製, 讓依賴注入框架用起來更方便. 畢竟dagger是一個java注入庫, 它的應用範圍不限於Android.

因為Hilt和Dagger可以共存, 可以逐步遷移. 既然官方推薦了, 可以在項目內小範圍地先試試.

最後推薦這個cheat sheet

Reference

  • Dependency Injection on Android with Hilt
  • Dependency injection with Hilt
  • Dagger Hilt

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

.net core3.1 abp動態菜單和動態權限(思路) (二)

ps:本文需要先把abp的源碼下載一份來下,跟着一起找實現,更容易懂

在abp中,對於權限和菜單使用靜態來管理,菜單的加載是在登陸頁面的地方(具體是怎麼知道的,瀏覽器按F12,然後去sources中去找)

這個/AbpScripts/GetScripts是獲取需要初始化的script,源自AbpScriptsController,GetScripts方法包括

頁面加載時的鏈接是:http://localhost:62114/AbpScripts/GetScripts?v=637274153555501055

_multiTenancyScriptManager //當前租戶初始化 對應報文的 abp.multiTenancy

_sessionScriptManager //當前session初始化 對應報文的 abp.session
_localizationScriptManager  //本地化的初始化 對應報文的 abp.localization
_featuresScriptManager  //對應報文的 abp.features
_authorizationScriptManager  //權限初始化  對應報文的 abp.auth
_navigationScriptManager  //導航菜單初始化  對應報文的 abp.nav
_settingScriptManager  //設置初始化  對應報文的 abp.setting
_timingScriptManager  //對應報文的 abp.clock
_customConfigScriptManager  //對應報文的 abp.custom

 

 

 

 

 好了,現在基本算是找到菜單和權限js獲取的地方了,一般系統裏面,權限是依賴於菜單和菜單按鈕的,所以我們先不管權限,先把菜單做成動態加載的

從await _navigationScriptManager.GetScriptAsync()開始,一路F12,大概流程是

(接口)INavigationScriptManager=>(接口實現)NavigationScriptManager=>(方法)GetScriptAsync=>(調用)await _userNavigationManager.GetMenusAsync=>
(接口)IUserNavigationManager=>(接口實現)UserNavigationManager=>(方法)GetMenuAsync=>(調用)navigationManager.Menus=>
(接口)INavigationManager=>(接口實現)NavigationManager=>(非靜態構造函數為Menus屬性賦值)NavigationManager

 到這裏之後基本就到底了,我們看看NavigationManager的內容

    internal class NavigationManager : INavigationManager, ISingletonDependency
    {
        public IDictionary<string, MenuDefinition> Menus { get; private set; }  //屬性

        public MenuDefinition MainMenu //屬性
        {
            get { return Menus["MainMenu"]; }
        }

        private readonly IIocResolver _iocResolver;  
        private readonly INavigationConfiguration _configuration;

        public NavigationManager(IIocResolver iocResolver, INavigationConfiguration configuration) //非靜態構造函數
        {
            _iocResolver = iocResolver;
            _configuration = configuration;

            Menus = new Dictionary<string, MenuDefinition>
                    {
                        {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))}
                    };
        }

        public void Initialize()  //初始化方法
        {
            var context = new NavigationProviderContext(this);

            foreach (var providerType in _configuration.Providers)
            {
                using (var provider = _iocResolver.ResolveAsDisposable<NavigationProvider>(providerType))
                {
                    provider.Object.SetNavigation(context);  //中式英語翻譯一下,應該是設置導航
                }
            }
        }
    }

這個類裏面就只有屬性、需要注入的接口聲明、非靜態構造函數、初始化方法,我們到這裏需要關注的是Menus這個屬性,這個屬性似乎將會包含我們需要生成的菜單內容

Menus = new Dictionary<string, MenuDefinition>
                    {
                        {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))}
                    };

這裡是對Menus的賦值,實例化了一個Dictionary,前面的不用看,主要是看標紅的這句話,從new LocalizableString(“MainMenu”, AbpConsts.LocalizationSourceName)裏面獲取到值

好了現在基本找到地方了,我們不知道LocalizableString是什麼意思,但是我們可以百度一波

ILocalizableString/LocalizableString:封裝需要被本地化的string的信息,並提供Localize方法(調用ILocalizationManager的GetString方法)返回本地化的string. SourceName指定其從那個本地化資源讀取本地化文本。

  LocalizableString(“Questions”, “”) 如果本地找不到資源,會報300

大概的意思是通過new LocalizableString,我們可以在本地化來源為AbpConsts.LocalizationSourceName的string裏面尋找到Key為MainMenu的value(理解不對請噴)

 

現在需要去找到那個地方對MainMenu進行了本地化操作,一般來說這個事情都是在程序加載的時候進行的,先對MainMenu進行讀取,保存到本地,然後在_navigationScriptManager讀取,傳輸給前台

似乎不好找了,但是我們發現有一個類型MenuDefinition,F12一下,可以發現寶藏

namespace Abp.Application.Navigation
{
    /// <summary>
    /// Represents a navigation menu for an application.  //表示應用程序的導航菜單
/// </summary>
    public class MenuDefinition : IHasMenuItemDefinitions
    {
        /// <summary>
        /// Unique name of the menu in the application. Required.  //應用程序中菜單的唯一名稱。 必須
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Display name of the menu. Required.  //菜單显示名稱 必須
/// </summary>
        public ILocalizableString DisplayName { get; set; }

        /// <summary>
        /// Can be used to store a custom object related to this menu. Optional.  //可用於存儲與此菜單相關的自定義對象
/// </summary>
        public object CustomData { get; set; }

        /// <summary>
        /// Menu items (first level).   //菜單項(第一級)
/// </summary>
        public List<MenuItemDefinition> Items { get; set; }

        /// <summary>
        /// Creates a new <see cref="MenuDefinition"/> object.
        /// </summary>
        /// <param name="name">Unique name of the menu</param>
        /// <param name="displayName">Display name of the menu</param>
        /// <param name="customData">Can be used to store a custom object related to this menu.</param>
        public MenuDefinition(string name, ILocalizableString displayName, object customData = null)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name", "Menu name can not be empty or null.");
            }

            if (displayName == null)
            {
                throw new ArgumentNullException("displayName", "Display name of the menu can not be null.");
            }

            Name = name;
            DisplayName = displayName;
            CustomData = customData;

            Items = new List<MenuItemDefinition>();
        }

        /// <summary>
        /// Adds a <see cref="MenuItemDefinition"/> to <see cref="Items"/>.
        /// </summary>
        /// <param name="menuItem"><see cref="MenuItemDefinition"/> to be added</param>
        /// <returns>This <see cref="MenuDefinition"/> object</returns>
        public MenuDefinition AddItem(MenuItemDefinition menuItem)
        {
            Items.Add(menuItem);
            return this;
        }

        /// <summary>
        /// Remove menu item with given name
        /// </summary>
        /// <param name="name"></param>
        public void RemoveItem(string name)
        {
            Items.RemoveAll(m => m.Name == name);
        }
    }
}

找到了菜單的類型了,那麼我們去找保存的地方就好找了,我們其實可以根據AddItem這個方法去找,去查看哪個地方引用了

AddItem方法添加的是MenuItemDefinition類型的變量,那我們現在退出abp源碼,去我們的AbpLearn項目中去全局搜索一下

 

 

看來是同一個AbpLearnNavigationProvider類裏面,雙擊過去看一下

 

    /// <summary>
    /// This class defines menus for the application.
    /// </summary>
    public class AbpLearnNavigationProvider : NavigationProvider
    {
        public override void SetNavigation(INavigationProviderContext context)
        {
            context.Manager.MainMenu
                .AddItem(
                    new MenuItemDefinition(
                        PageNames.Home,
                        L("HomePage"),
                        url: "",
                        icon: "fas fa-home",
                        requiresAuthentication: true
                    )
                ).AddItem(
                    new MenuItemDefinition(
                        PageNames.Tenants,
                        L("Tenants"),
                        url: "Tenants",
                        icon: "fas fa-building",
                        permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Tenants)
                    )
                ).AddItem(
                    new MenuItemDefinition(
                        PageNames.Users,
                        L("Users"),
                        url: "Users",
                        icon: "fas fa-users",
                        permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Users)
                    )
                ).AddItem(
                    new MenuItemDefinition(
                        PageNames.Roles,
                        L("Roles"),
                        url: "Roles",
                        icon: "fas fa-theater-masks",
                        permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Roles)
                            )
                )
                .AddItem(
                    new MenuItemDefinition(
                        PageNames.About,
                        L("About"),
                        url: "About",
                        icon: "fas fa-info-circle"
                    )
                ).AddItem( // Menu items below is just for demonstration!
                    new MenuItemDefinition(
                        "MultiLevelMenu",
                        L("MultiLevelMenu"),
                        icon: "fas fa-circle"
                    ).AddItem(
                        new MenuItemDefinition(
                            "AspNetBoilerplate",
                            new FixedLocalizableString("ASP.NET Boilerplate"),
                            icon: "far fa-circle"
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetBoilerplateHome",
                                new FixedLocalizableString("Home"),
                                url: "https://aspnetboilerplate.com?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetBoilerplateTemplates",
                                new FixedLocalizableString("Templates"),
                                url: "https://aspnetboilerplate.com/Templates?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetBoilerplateSamples",
                                new FixedLocalizableString("Samples"),
                                url: "https://aspnetboilerplate.com/Samples?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetBoilerplateDocuments",
                                new FixedLocalizableString("Documents"),
                                url: "https://aspnetboilerplate.com/Pages/Documents?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        )
                    ).AddItem(
                        new MenuItemDefinition(
                            "AspNetZero",
                            new FixedLocalizableString("ASP.NET Zero"),
                            icon: "far fa-circle"
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZeroHome",
                                new FixedLocalizableString("Home"),
                                url: "https://aspnetzero.com?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZeroFeatures",
                                new FixedLocalizableString("Features"),
                                url: "https://aspnetzero.com/Features?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZeroPricing",
                                new FixedLocalizableString("Pricing"),
                                url: "https://aspnetzero.com/Pricing?ref=abptmpl#pricing",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZeroFaq",
                                new FixedLocalizableString("Faq"),
                                url: "https://aspnetzero.com/Faq?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZeroDocuments",
                                new FixedLocalizableString("Documents"),
                                url: "https://aspnetzero.com/Documents?ref=abptmpl",
                                icon: "far fa-dot-circle"
                            )
                        )
                    )
                );
        }

        private static ILocalizableString L(string name)
        {
            return new LocalizableString(name, AbpLearnConsts.LocalizationSourceName);
        }
    }

好了,現在我們找到菜單定義的地方了,那麼我們如何去做動態菜單哪?

 

首先我們想一下需要什麼樣的動態菜單?

1.從數據庫加載,不從數據庫加載怎麼叫動態

2.可以根據不同Host(管理者)和Tenant(租戶)加載不同的菜單,不可能管理者和租戶看到的菜單全是一個樣子的吧!

3.可以根據不同的角色或者用戶加載不同的菜單(這個就牽扯到權限了,比如誰可以看到什麼,不可以看到什麼)

4.權限、按鈕最好和菜單相綁定,這樣便於控制

……

 

根據以上幾點,我們可以確定

1.必須要在用戶登錄之後加載出來的菜單才能符合條件

2.菜單需要建一個表(因為abp默認沒有單獨的菜單表),來進行存放

3.字段需要包含:菜單名,菜單與權限對應的名稱(用於動態權限),菜單對應的Url,Icon,級聯父Id,是否啟用,排序,租戶Id

4.需要對菜單進行編輯時,因為牽扯到多租戶,我們需要對多租戶定義一個標準的菜單,在添加租戶時,自動將標準菜單複製保存一份到新租戶中,所以我們需要對於菜單的進行區分,一般來說Host對應的數據行TenantId(int)都為null,我們可以將標準菜單的TenantId標為-1,已經分配保存的菜單TenantId為當前租戶Id,這樣便於區分和查詢

 

好了,讓我們開始寫動態菜單吧

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

手動造輪子——基於.NetCore的RPC框架DotNetCoreRpc

前言

    一直以來對內部服務間使用RPC的方式調用都比較贊同,因為內部間沒有這麼多限制,最簡單明了的方式就是最合適的方式。個人比較喜歡類似Dubbo的那種使用方式,採用和本地方法相同的方式,把接口層獨立出來作為服務契約,為服務端提供服務,客戶端也通過此契約調用服務。.Net平台上類似Dubbo這種相對比較完善的RPC框架還是比較少的,GRPC確實是一款非常優秀的RPC框架,能跨語言調用,但是每次還得編寫proto文件,個人感覺還是比較麻煩的。如今服務拆分,微服務架構比較盛行的潮流下,一個簡單實用的RPC框架確實可以提升很多開發效率。

簡介

    隨着.Net Core逐漸成熟穩定,為我一直以來想實現的這個目標提供了便利的方式。於是利用閑暇時間本人手寫了一套基於Asp.Net Core的RPC框架,算是實現了一個自己的小目標。大致的實現方式,Server端依賴Asp.Net Core,採用的是中間件的方式攔截處理請求比較方便。Client端可以是任何可承載.Net Core的宿主程序。通信方式是HTTP協議,使用的是HttpClientFactory。至於為什麼使用HttpClientFactory,因為HttpClientFactory可以更輕鬆的實現服務發現,而且可以很好的集成Polly,很方便的實現,超時重試,熔斷降級這些,給開發過程中提供了很多便利。由於本人能力有限,基於這些便利,站在巨人的肩膀上,簡單的實現了一個RPC框架,項目託管在GitHub上https://github.com/softlgl/DotNetCoreRpc有興趣的可以自行查閱。

開發環境

  • Visual Studio 2019
  • .Net Standard 2.1
  • Asp.Net Core 3.1.x

使用方式

    打開Visual Studio先新建一個RPC契約接口層,這裏我起的名字叫IRpcService。然後新建一個Client層(可以是任何可承載.Net Core的宿主程序)叫ClientDemo,然後建立一個Server層(必須是Asp.Net Core項目)叫WebDemo,文末附本文Demo連接,建完這些之後項目結構如下:

Client端配置

Client端引入DotNetCoreRpc.Client包,並引入自定義的契約接口層

<PackageReference Include="DotNetCoreRpc.Client" Version="1.0.2" />

然後可以愉快的編碼了,大致編碼如下

class Program
{
    static void Main(string[] args)
    {
        IServiceCollection services = new ServiceCollection();
        //*註冊DotNetCoreRpcClient核心服務
        services.AddDotNetCoreRpcClient()
        //*通信是基於HTTP的,內部使用的HttpClientFactory,自行註冊即可
        .AddHttpClient("WebDemo", client => { client.BaseAddress = new Uri("http://localhost:13285/"); });

        IServiceProvider serviceProvider = services.BuildServiceProvider();
        //*獲取RpcClient使用這個類創建具體服務代理對象
        RpcClient rpcClient = serviceProvider.GetRequiredService<RpcClient>();

        //IPersonService是我引入的服務包interface,需要提供ServiceName,即AddHttpClient的名稱
        IPersonService personService = rpcClient.CreateClient<IPersonService>("WebDemo");

        PersonDto personDto = new PersonDto
        {
            Id = 1,
            Name = "yi念之間",
            Address = "中國",
            BirthDay = new DateTime(2000,12,12),
            IsMarried = true,
            Tel = 88888888888
        };

        bool addFlag = personService.Add(personDto);
        Console.WriteLine($"添加結果=[{addFlag}]");

        var person = personService.Get(personDto.Id);
        Console.WriteLine($"獲取person結果=[{person.ToJson()}]");

        var persons = personService.GetAll();
        Console.WriteLine($"獲取persons結果=[{persons.ToList().ToJson()}]");

        personService.Delete(person.Id);
        Console.WriteLine($"刪除完成");

        Console.ReadLine();
    }
}

到這裏Client端的代碼就編寫完成了

Server端配置

Client端引入DotNetCoreRpc.Client包,並引入自定義的契約接口層

<PackageReference Include="DotNetCoreRpc.Server" Version="1.0.2" />

然後編寫契約接口實現類,比如我的叫PersonService

//實現契約接口IPersonService
public class PersonService:IPersonService
{
    private readonly ConcurrentDictionary<int, PersonDto> persons = new ConcurrentDictionary<int, PersonDto>();
    public bool Add(PersonDto person)
    {
        return persons.TryAdd(person.Id, person);
    }

    public void Delete(int id)
    {
        persons.Remove(id,out PersonDto person);
    }

    //自定義Filter
    [CacheFilter(CacheTime = 500)]
    public PersonDto Get(int id)
    {
        return persons.GetValueOrDefault(id);
    }

    //自定義Filter
    [CacheFilter(CacheTime = 300)]
    public IEnumerable<PersonDto> GetAll()
    {
        foreach (var item in persons)
        {
            yield return item.Value;
        }
    }
}

通過上面的代碼可以看出,我自定義了Filter,這裏的Filter並非Asp.Net Core框架定義的Filter,而是DotNetCoreRpc框架定義的Filter,自定義Filter的方式如下

//*要繼承自抽象類RpcFilterAttribute
public class CacheFilterAttribute: RpcFilterAttribute
{
    public int CacheTime { get; set; }

    //*支持屬性注入,可以是public或者private
    //*這裏的FromServices並非Asp.Net Core命名空間下的,而是來自DotNetCoreRpc.Core命名空間
    [FromServices]
    private RedisConfigOptions RedisConfig { get; set; }

    [FromServices]
    public ILogger<CacheFilterAttribute> Logger { get; set; }

    public override async Task InvokeAsync(RpcContext context, RpcRequestDelegate next)
    {
        Logger.LogInformation($"CacheFilterAttribute Begin,CacheTime=[{CacheTime}],Class=[{context.TargetType.FullName}],Method=[{context.Method.Name}],Params=[{JsonConvert.SerializeObject(context.Parameters)}]");
        await next(context);
        Logger.LogInformation($"CacheFilterAttribute End,ReturnValue=[{JsonConvert.SerializeObject(context.ReturnValue)}]");
    }
}

以上代碼基本上完成了對服務端業務代碼的操作,接下來我們來看如何在Asp.Net Core中配置使用DotNetCoreRpc。打開Startup,配置如下代碼既可

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IPersonService, PersonService>()
        .AddSingleton(new RedisConfigOptions { Address = "127.0.0.1:6379", Db = 10 })
        //*註冊DotNetCoreRpcServer
        .AddDotNetCoreRpcServer(options => {
            //*確保添加的契約服務接口事先已經被註冊到DI容器中

            //添加契約接口
            //options.AddService<IPersonService>();

            //或添加契約接口名稱以xxx為結尾的
            //options.AddService("*Service");

            //或添加具體名稱為xxx的契約接口
            //options.AddService("IPersonService");

            //或掃描具體命名空間下的契約接口
            options.AddNameSpace("IRpcService");

            //可以添加全局過濾器,實現方式和CacheFilterAttribute一致
            options.AddFilter<LoggerFilterAttribute>();
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        //這一堆可以不要+1
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //添加DotNetCoreRpc中間件既可
        app.UseDotNetCoreRpc();

        //這一堆可以不要+2
        app.UseRouting();

        //這一堆可以不要+3
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Server Start!");
            });
        });
    }
}

以上就是Server端簡單的使用和配置,是不是感覺非常的Easy。附上可運行的Demo地址,具體編碼可查看Demo.

總結

    能自己實現一套RPC框架是我近期以來的一個願望,現在可以說實現了。雖然看起來沒這麼高大上,但是整體還是符合RPC的思想。主要還是想自身實地的實踐一下,順便也希望能給大家提供一些簡單的思路。不是說我說得一定是對的,我講得可能很多是不對的,但是我說的東西都是我自身的體驗和思考,也許能給你帶來一秒鐘、半秒鐘的思考,亦或是哪怕你覺得我哪一句話說的有點道理,能引發你內心的感觸,這就是我做這件事的意義。最後,歡迎大家評論區或本項目GitHub下批評指導。

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

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

希臘野火竄燒 波及著名邁錫尼古城遺址

摘錄自2020年8月31日中央社報導

希臘青銅時代的著名遺址邁錫尼古城附近野火延燒,當局緊急疏散遊客,消防官員表示,火勢已獲得控制。

希臘消防部門官員表示,野火下午從阿卡曼儂(Agamemnon)古墓附近竄起,「部分已獲得控制」。

伯羅奔尼梭(Peloponnese)南部的消防局局長科利維拉斯(Thanassis Koliviras)告訴「雅典通訊社」(Athens News Agency),大火波及「考古遺址的一個區塊,並燒毀些許乾草,但沒有損及博物館」。

希臘文化部發表聲明說,根據初步調查,「大火沒有破壞古蹟」,並說之後「一組專家將評估受損情況」。

乾燥的夏季氣候期間,希臘每年都得應付燒不盡的野火,並且高溫時常超過攝氏30度。

氣候變遷
國際新聞
希臘
野火

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

南非克魯格公園防疫封鎖 動物佔領度假屋

摘錄自2020年8月31日中央社報導

南非為防止武漢肺炎(COVID-19)擴大進行各階段封鎖,知名的克魯格國家公園住宿小屋因長時間無人入住,部分屋舍遭動物佔領與破壞,園方已展開維修,準備重新逐步開放。

武漢肺炎爆發後,南非自3月26日進入最嚴格的第5級全國封鎖,幾個月來因疫情變化逐漸開放,8月18日起改善第2級,國家公園開放旅遊觀光。

新聞網站「時報即時消息」(TimesLIVE)今(31日)引述南非國家公園管理處執行長姆克特尼(Fundisile Mketeni)表示,克魯格國家公園(Kruger National Park)準備重新開放面臨許多挑戰,特別是清潔與維修工作。疫情封鎖期間,部分住宿小屋被靈長類動物、松鼠與蝙蝠佔領和破壞。

姆克特尼指出,已迅速維修,預計9月份會有旅遊高峰。

國際新聞
南非
防疫
國家公園

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

國際礦業鉅子力拓集團 誓言保留澳洲古老岩棚

摘錄自2020年8月31日中央社報導

國際礦業鉅子力拓集團(Rio Tinto)保證,將保護位於西澳州(Western Australia)銀草山(Silvergrass)鐵礦場邊緣具有4萬3000年歷史的岩棚。

路透社報導,力拓集團獲得西澳州政府批准,可以摧毀這處遺址,但於今年5月在同一地區炸毀古代原住民洞穴而引起眾怒後,力拓集團開始對遺址進行評估。

澳洲最大鐵礦公司力拓集團、必和必拓(BHP)及FMG集團(Fortescue Metals Group)持續評估可能受到礦場擴張影響的遺址。

力拓集團以電子郵件回應路透社對於岩棚遺址的提問,力拓集團指出:「我們將保護這個遺址,並將設立適當的採礦緩衝區,進一步確保遺址不受威脅。」

國際新聞
澳洲
礦業公司

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

全球最大濕地「內外燃燒」中 潘塔納爾火勢15年來最嚴重

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

印尼醫療廢棄物流入河中 恐成病毒溫床

摘錄自2020年9月1日自由時報報導

綜合外媒報導,印尼大量的醫療廢棄物被堆積在坦格朗(Tangerang),當地的垃圾掩埋場5月時崩塌,大量的醫療垃圾流入西薩達河(Cisadane River)中。總長138公里的西薩達河是當地居民重要的水資源,許多民眾會在此處洗澡和洗衣服,河流恐成病毒溫床。

印尼衛生部指出,3月到6月期間國內共產生1480噸醫療廢棄物。衛生部官員努拉里(Imran Agus Nurali)表示,已通過新的法規,其中包括醫療機構處理相關廢棄物的準則。

印尼法律健康協會的流行病專家邁爾克(Mahesa Paranadipa Maikel)指出,如果這些醫療廢棄物散佈在河流附近的社區,則會污染當地水源且有傳播武漢肺炎的可能。

污染治理
國際新聞
印尼
醫療廢棄物
水污染
垃圾掩埋場
動物與大環境變遷

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

全球冰川湖增53% 冰川融化形成湖泊水量增 恐釀洪災

摘錄自2020年9月1日聯合報報導

由於氣候暖化導致冰川融化,自1990年起,全球冰川湖的數量已增加53%。影像顯示,冰川湖數量在1990年至2018年間增長53%,使湖泊覆蓋地球的面積擴大約51%。根據調查,有1萬4394個冰湖散布在近9000平方公里的地球表面上。

科學家分析來自NASA和谷歌地球引擎的衛星影像數據,評估冰川融化後所形成的湖泊如何受到全球暖化和其他過程影響。根據這些數據,研究人員估計,全球冰川湖水量在1990年至2018年間增加48%,目前總計約達156.5立方公里。

對於世界上許多最貧窮的人來說,冰川湖是重要的淡水資源,尤其是在亞洲山區和南美部分地區。但大量冰融水注入湖中會導致下游地區面臨洪水威脅,能摧毀村莊、沖刷道路、破壞管線和其他基礎設施。

氣候變遷
國際新聞
全球
冰川融化
全球暖化
洪水

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

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!