台北萬豪酒店首設Tesla充電站,即日啟用

美商特斯拉(Tesla)於今年9月正式進軍台灣,在台引發電動車熱。台北萬豪酒店宣布與Tesla合作設置充電站,同時推出限時活動,鼓勵民眾響應環保行動。

繼在台灣科技大學校區內設置充電站後,Tesla正式宣布與台北萬豪酒店合作,於飯店內設置6組充電站,成為全台首家提供Tesla電動車充電服務的飯店。車主只要於飯店內消費,就可免費享有充電服務。

台北萬豪酒店主管表示,萬豪國際的核心理念是「擁抱改變」,不但要跟著時代進步,還要勇於創新,與Tesla的形象相輔相成。藉由與Tesla合作設置電動車充電站,希望能提供客戶更便捷的服務,並推動現代科技。

萬豪酒店同時也推出相關活動,凡是11月30日前佩戴紅色配件到萬豪酒店消費的顧客,就可享有9折優惠。

Tesla主管表示,Tesla進入台灣市場後,將提供銷售、服務、充電等全方位服務,未來將繼續加速布建「目的地充電站」的腳步,以為台灣的Tesla車主提供更綿密的充電網絡。

除了萬豪酒店之外,台北晶華酒店也與BMW合作設置了i公共充電站,可供i3、i8等車款使用。

(照片來源:)

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

聚甘新

BMW 2017年目標售出10萬輛電動車

德國媒體報導,汽車廠商BMW CEO Harald Krueger 在接受採訪時表示,電動車的時代即將到來,這一市場還有巨大的潛力,BMW 將在2017 年力爭把電動車的銷量提升到10 萬台,目前該公司2015 年電動車和混合動力車的銷量大約為6 萬台。

BMW CEO Harald Krueger 表示,該公司希望能夠在2017 年大幅提升電動車的銷量,至少提升六成,達到年銷量10 萬台。BMW 2016 年電動車和混合動力車的銷量合計大約為6 萬台,自2013 年發售電動車以來,BMW 電動車的雷軍銷量已經超過了10 萬台。

Harald Krueger 認為電動車的時代即將到來,這一市場還有很大的潛力,需求還沒有完全被發掘。

BMW 旗下唯一的純電動車車款i3 上市以來市場反響不如預期,2015 年i3 僅售出了2.5 萬台,為了提升這款產品的競爭力,BMW 將在2016 年的升級中把i3 系列電動車的續航里程提升50%。

在高階汽車市場中,BMW 的產品銷量已經落後於Benz,該公司希望在2025 年前將電動車和混合動力車的銷量提升到15%-25%。

(本文由《》授權提供。照片來源:)

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聚甘新

電動車有害道路安全?美國規定汽車不能太安靜!

根據外電報導,美國政府正式通過一則汽車行駛新規定,也就是未來一些「安靜的汽車」,如電動汽車或混合動力汽車等,在行駛速度超過時速18.6 英里 (約30 公里)的情況下,必須增加噪音警報裝置,以避免傷及行人、盲人和騎車的路人。

根據美國國家公路交通安全管理局(NHTSA)發布的這則新規定表示,該規定將在2019 年9 月1 日起生效。屆時,包括特斯拉 (TESLA)、日產汽車 (Nisson) 和豐田汽車 (TOYOTA) 等廠商都需要為所有汽車增加噪音裝置。美國政府希望通過此規定,能夠防止每年多達2,400 件的行人受傷事故,並為53 萬輛汽車增設噪音專制。

NHTSA 指出,這項新規每年約會給汽車廠商增加3,900 萬美元的花費。因為,他們要為汽車增設一個外設防水喇叭。但是,能夠透過降低交通事故而每年節省約2.5 億到3.2 億美元的醫療費用。

該規定未來將適用於全美各地區,並且涵蓋路上行駛的電動汽車和混合動力汽車等。另外,包括帶有4 個輪子、總重量低於10,000 磅的車輛,也都必須在向後或向前行駛時,在速度達到每小時30 公里之際發出聲響,以警告周遭的路人與騎車的民眾。不過,NHTSA 也認為,規定汽車喇叭可有可無,因為輪胎和風也可以製造出足夠的噪音聲響,只要該聲響能達到警告的標準即可。

該項新的規定雖然受到全美盲人主組織的歡迎,但是卻讓汽車廠商表示擔憂。因為噪音也許會太大,並且實施起來將可能相當複雜。因為,新規只規定了最低的聲音要求,卻沒有制定必須發出什麼聲音。

(本文由《》授權提供。照片來源:特斯拉)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

聚甘新

優秀源碼帶給我們的一些啟示

首先是個人的一些閱讀源碼的小技巧,不一定適用每個人,可以直接跳過。

 

閱讀源碼的一些個人技巧

 

博客+總結

個人覺得大多數情況下跟着一篇優秀的博客配合著看就足夠了,之後再自己寫博客總結一遍加深印象,畫一下流程圖基本都能理順。(圖為學AQS時本人畫的獲取獨佔鎖流程圖)

 

類關係

配合idea看類之間的關係(ctrl+alt+shift+u)的功能也能更好的理解整個項目的整體架構。因為很多源碼其實並不是真的複雜,只是為了擴展性優雅簡潔等原因建立了大量的接口和抽象類以及各種設計模式,使得項目看起來很龐大很複雜,藉助這個功能有利於你排除掉一些你暫時不想去關心的設計邏輯。知道那個部分才是最核心的邏輯,專註於去看核心代碼。 

 

多看註釋

但是如果你看的博客裏面剛好缺少了一部分你想看的內容,而你又找不到資料,需要自己去看,又或者你想看的源碼一點點資料都找不到的情況下想去看源碼。

這個時候比較有作用的就是註釋,源碼中的註釋看不懂也沒關係,放到百度翻譯里基本也能理解大概的意思。仔細看完方法或類的註釋之後你就理解了接下來這個類大致是在做什麼,之後讀它的源碼會通順很多,有一些方法或類甚至在你看完註釋之後就已經能知道你想看的內容了,已經沒有需要繼續往下讀了。

不僅僅是類或方法的註釋文檔,方法中代碼的註釋也很重要,基本上稍微複雜一點點的代碼,甚至有時候加個鎖,作者都會認認真真的寫一行註釋解釋自己這麼做的原因。

 

適當囫圇吞棗

還有一點是適當忽略一些不重要的細節,這個主要看你想看什麼,一般我們看第一遍大多數只是想知道大致的流程是什麼樣的,所以可以適當忽略併發邏輯和一些方法里的內容(看一眼註釋先知道這個方法會做什麼的就夠了)。第一遍大致知道流程,第二第三遍再去深究細節和併發邏輯等。

 

善用debug

多用debug,很多時候源碼走的路線會和你想象中的有很大不同,你以為會進入這個if,其實他偷偷進了else。

  

短路機制

經常看到利用短路機制的代碼,這裏以 AbstractQueuedSynchronizer 的 acquire 方法為例子,只有 tryAcquire 獲取鎖失敗, !tryAcquire 返回 true 時才會執行後面進入阻塞隊列並掛起的方法,如果獲取鎖成功了,根據短路機制自然不會執行入隊方法。

// AbstractQueuedSynchronizer.acquire(int arg)
if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
    selfInterrupt();
}

 

拆高低位

ReentrantReadWriteLock的這段代碼里將AQS的state一分為二給共享鎖和獨佔鎖使用,個人覺得這種設計非常巧妙:

// ReentrantReadWriteLock
abstract static class Sync extends AbstractQueuedSynchronizer {
    // 下面這塊說的就是將 state 一分為二,高 16 位用於共享模式,低16位用於獨佔模式
    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    // 取 c 的高 16 位值,代表讀鎖的獲取次數(包括重入)
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 取 c 的低 16 位值,代表寫鎖的重入次數,因為寫鎖是獨佔模式
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

 

 While(n– > 0)和while (–n >= 0)

忘記在哪裡看到的了,翻了一下瀏覽記錄應該是在Java AIO部分的源碼里,這種寫法感覺很簡潔就記下來了,不過可讀性似乎不太高,特別是第一種乍一看還以為是lambda表達式

意思等同於 for (int i = 0; i < n; i++)  ,但是 while(n– > 0) 和 while (–n >= 0) 這種寫法會直接改變n的值

 

xx = null

在很多jdk的源碼中我們都可以看到 xx = null // help GC 這樣的代碼,用來置空引用,幫助jvm完成gc。具體可以了解可達性算法。

這裏我們以LinkList為例子:

// LinkList 的方法
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

 

位移運算符

在很多地方都會使用位移來進行運算,平時寫算法題也一樣很多人都這麼使用,下面以 ArrayList 的 grow 方法為例子,這裏通過右移1位使 oldCapacity 變為原來的0.5倍,之後加上它本身得到  newCapacity 

// ArrayList.grow(int minCapacity)
private void grow(int minCapacity) {
    // . . . . . .
    int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
    // . . . . . . 
}

 

以上是我目前的水平所能總結出來的,後續學到其他的會繼續更新,如果大家有什麼補充的請告訴我

 

最後慣例附一圖:(根本不存在想雇傭我的地方( ´_ゝ`).jpg

 

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

【其他文章推薦】

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

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

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

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

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

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

聚甘新

小師妹學JVM之:JIT中的LogCompilation

目錄

  • 簡介
  • LogCompilation簡介
  • LogCompilation的使用
  • 解析LogCompilation文件
  • 總結

簡介

我們知道在JVM中為了加快編譯速度,引入了JIT即時編譯的功能。那麼JIT什麼時候開始編譯的,又是怎麼編譯的,作為一個高傲的程序員,有沒有辦法去探究JIT編譯的秘密呢?答案是有的,今天和小師妹一起帶大家來看一看這個編譯背後的秘密。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

LogCompilation簡介

小師妹:F師兄,JIT這麼神器,但是好像就是一個黑盒子,有沒有辦法可以探尋到其內部的本質呢?

追求真理和探索精神是我們作為程序員的最大優點,想想如果沒有玻爾關於原子結構的新理論,怎麼會有原子體系的突破,如果沒有海森堡的矩陣力學,怎麼會有量子力學的建立?

JIT的編譯日誌輸出很簡單,使用 -XX:+LogCompilation就夠了。

如果要把日誌重定向到一個日誌文件中,則可以使用-XX:LogFile= 。

但是要開啟這些分析的功能,又需要使用-XX:+UnlockDiagnosticVMOptions。 所以總結一下,我們需要這樣使用:

-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=www.flydean.com.log

LogCompilation的使用

根據上面的介紹,我們現場來生成一個JIT的編譯日誌,為了體現出專業性,這裏我們需要使用到JMH來做性能測試。

JMH的全稱是Java Microbenchmark Harness,是一個open JDK中用來做性能測試的套件。該套件已經被包含在了JDK 12中。

如果你使用的不是JDK 12,那麼需要添加如下依賴:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.19</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.19</version>
</dependency>

更多詳情可以參考我之前寫的: 在java中使用JMH(Java Microbenchmark Harness)做性能測試一文。

之前有的朋友說,代碼也用圖片,看起來好看,從本文之後,我們會盡量把代碼也轉成圖片來展示:

看完我的JMH的介紹,上面的例子應該很清楚了,主要就是做一個累加操作,然後warmup 5輪,測試5輪。

在@Fork註解裏面,我們可以配置jvm的參數,為什麼我註釋掉了呢?因為我發現在jvmArgsPrepend中的-XX:LogFile是不生效的。

沒辦法,我只好在運行配置中添加:

運行之後,你就可以得到輸出的編譯日誌文件。

解析LogCompilation文件

小師妹:F師兄,我看了一下生成的文件好複雜啊,用肉眼能看得明白嗎?

別怕,只是內容的多一點,如果我們細細再細細的分析一下,你會發現其實它真的非常非常……複雜!

其實寫點簡單的小白文不好嗎?為什麼要來分析這麼複雜,又沒人看,看了也沒人懂的JVM底層…..

大概,這就是專業吧!

LogCompilation文件其實是xml格式的,我們現在來大概分析一下,它的結構,讓大家下次看到這個文件也能夠大概了解它的重點。

首先最基本的信息就是JVM的信息,包括JVM的版本,JVM運行的參數,還有一些properties屬性。

我們收集到的日誌其實是分兩類的,第一類是應用程序本身的的編譯日誌,第二類就是編譯線程自己內部產生的日誌。

第二類的日誌會以hs_c*.log的格式存儲,然後在JVM退出的時候,再將這些文件跟最終的日誌輸出文件合併,生成一個整體的日誌文件。

比如下面的兩個就是編譯線程內部的日誌:

<thread_logfile thread='22275' filename='/var/folders/n5/217y_bgn49z18zvjch907xb00000gp/T//hs_c22275_pid83940.log'/>
<thread_logfile thread='41731' filename='/var/folders/n5/217y_bgn49z18zvjch907xb00000gp/T//hs_c41731_pid83940.log'/>

上面列出了編譯線程的id=22275,如果我們順着22275找下去,則可以找到具體編譯線程的日誌:

<compilation_log thread='22275'>
...
</compilation_log>

上面由compilation_log圍起來的部分就是編譯日誌了。

接下來的部分表示,編譯線程開始執行了,其中stamp表示的是啟動時間,下圖列出了一個完整的編譯線程的日誌:

<start_compile_thread name='C2 CompilerThread0' thread='22275' process='83940' stamp='0.058'/>

接下來描述的是要編譯的方法信息:

<task compile_id='10' method='java.lang.Object <init> ()V' bytes='1' count='1409' iicount='1409' stamp='0.153'>

上面列出了要編譯的方法名,compile_id表示的是系統內部分配的編譯id,bytes是方法中的字節數,count表示的是該方法的調用次數,注意,這裏的次數並不是方法的真實調用次數,只能做一個估計。

iicount是解釋器被調用的次數。

task執行了,自然就會執行完成,執行完成的內容是以task_done標籤來表示的:

<task_done success='1' nmsize='120' count='1468' stamp='0.155'/>

其中success表示是否成功執行,nmsize表示編譯器編譯出來的指令大小,以byte為單位。如果有內聯的話,還有個inlined_bytes屬性,表示inlined的字節個數。

<type id='1025' name='void'/>

type表示的是方法的返回類型。

<klass id='1030' name='java.lang.Object' flags='1'/>

klass表示的是實例和數組類型。

<method id='1148' holder='1030' name='<init>' return='1025' flags='1' bytes='1' compile_id='1' compiler='c1' level='3' iicount='1419'/>

method表示執行的方法,holder是前面的klass的id,表示的是定義該方法的實例或者數組對象。method有名字,有
return,return對應的是上面的type。

flags表示的是方法的訪問權限。

接下來是parse,是分析階段的日誌:

<parse method='1148' uses='1419.000000' stamp='0.153'>

上面有parse的方法id。uses是使用次數。

<bc code='177' bci='0'/>

bc是byte Count的縮寫,code是byte的個數,bci是byte code的索引。

<dependency type='no_finalizable_subclasses' ctxk='1030'/>

dependency分析的是類的依賴關係,type表示的是什麼類型的依賴,ctkx是依賴的context class。

我們注意有的parse中,可能會有uncommon_trap:

<uncommon_trap bci='10' reason='unstable_if' action='reinterpret' debug_id='0' comment='taken never'/>

怎麼理解uncommon_trap呢?字面上意思就是捕獲非常用的代碼,就是說在解析代碼的過程中發現發現這些代碼是uncommon的,然後解析產生一個uncommon_trap,不再繼續進行了。

它裏面有兩個比較重要的字段,reason表示的是被標記為uncommon_trap的原因。action表示的出發uncommon_trap的事件。

有些地方還會有call:

<call method='1150' count='5154' prof_factor='1.000000' inline='1'/>

call的意思是,在該代碼中將會調用其他的方法。count是執行次數。

總結

複雜的編譯日誌終於講完了,可能講的並不是很全,還有一些其他情況這裏並沒有列出來,後面如果遇到了,我再添加進去。

本文的例子https://github.com/ddean2009/learn-java-base-9-to-20

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-jit-logcompilation/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

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

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

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

聚甘新

對 JsonConvert 的認識太膚淺了,終於還是遇到了問題

一:背景

1. 講故事

在開始本文之前,真的好想做個問卷調查,到底有多少人和我一樣,對 JsonConvert 的認識只局限在 SerializeObjectDeserializeObject 這兩個方法上(┬_┬), 這樣我也好結伴同行,不再孤單落魄,或許是這兩個方法基本上能夠解決工作中 80% 的場景,對於我來說確實是這樣,但隨着編碼的延續,終究還是會遇到那剩下的 20% ,所以呀。。。

我的場景是這樣的:前段時間寫業務代碼的時候,我有一個自定義的客戶算法類型的Model,這個Model中有這種算法類型下的客戶群以及Report統計信息,還用了 HashSet 記錄了該類型下的 CustomerID集合,為了方便講述,我把Model簡化如下:


    class CustomerAlgorithmModel
    {
        public string DisplayName { get; set; }

        public int CustomerType { get; set; }

        public ReprotModel Report { get; set; }

        public HashSet<int> CustomerIDHash { get; set; }
    }

    class ReprotModel
    {
        public int TotalCustomerCount { get; set; }

        public int TotalTradeCount { get; set; }
    }

那有意思的就來了,我個人是有記日誌的癖好,就想着以後不會出現死無對證的情況,然後就理所當然的使用 JsonConvert.SerializeObject, 這一下就出問題了,日誌送入到了 ElasticSearch ,然後通過 Kibana 查不出來,為啥呢? 看完上面的 Model 我想你也猜到了原因,json體太大了哈,好歹 CustomerIDHash 中也有幾十萬個撒,這一下全導出成json了,這 size 還能小嗎? 要不我寫段代碼看一看。


        static void Main(string[] args)
        {
            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000))
            };

            var json = JsonConvert.SerializeObject(algorithModel);

            File.WriteAllText("1.txt", json, Encoding.UTF8);

            Console.WriteLine("寫入完成!");
        }

可以看到,僅一個json就 3.3M,這樣的記錄多來幾打后,在 kibana 上一檢索,瀏覽器就卡的要死,其實 CustomerIDHash 這個字段對我來說是可有可無的,就算存下來了也沒啥大用,所以需求就來了,如何屏蔽掉 CustomerIDHash

二:尋求解決方案

1. 使用 JsonIgnore

有問題就網上搜啊,這一搜馬上就有人告訴你可以使用 JsonIgnoreAttribute 忽略特性,加好這個特性後繼續跑一下程序。


    [Newtonsoft.Json.JsonIgnore]
    public HashSet<int> CustomerIDHash { get; set; }

太好了,終於搞定了,但是靜下心來想一想,總感覺心裏有那麼一點不舒服,為什麼這麼說,一旦你給這個 CustomerIDHash 套上了 JsonIgnore ,這就意味着它在 JsonConvet 的世界中從此消失,也不管是誰在使用這個Model, 但這並不是我的初衷,我的初衷僅僅是為了在記錄日誌的時候踢掉 CustomerIDHash,可千萬不要影響在其他場景下的使用哈,現在這種做法就會給自己,給別人挖坑,埋下了不可預知的bug,我想你應該明白我的意思,還得繼續尋找下一個方案。

2. 使用自定義的 JsonConverter

真的,Newtonsoft 太強大了,我都想寫一個專題好好彌補彌補我的知識盲區,其實在這個場景中不就是想把 HashSet<int> 給屏蔽掉嘛,Newtonsoft 中專門提供了一個針對特定類型的自定義處理類,接下來我就寫一段:


   /// <summary>
   /// 自定義一個 針對 HashSet<int> 的轉換類
   /// </summary>
   public class HashSetConverter : Newtonsoft.Json.JsonConverter<HashSet<int>>
   {
       public override HashSet<int> ReadJson(JsonReader reader, Type objectType, HashSet<int> existingValue, bool hasExistingValue, JsonSerializer serializer)
       {
           return existingValue;
       }

       public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
       {
           writer.WriteNull();
       }
   } 

就是這麼簡單,然後就可以在 SerializeObject 的時候指定下自定義的 HashSetConverter 即可,然後再將程序跑起來看一下。


 var json = JsonConvert.SerializeObject(algorithModel, Formatting.Indented, new HashSetConverter());

從圖中看,貌似也是解決了,但我突然發現自己要鑽牛角尖了,如果我的實體中又來了一個頂級優質客戶群的 TopNCustomerIDHash,但因為這個CustomerID 比較少,我希望在 Json 中能保留下來,然後就是踢掉的那個 CustomerIDHash 我要保留 CustomerIDHash.Length ,哈哈,搞事情哈,那接下來怎麼解決呢?

  • 修改 Model 實體

    class CustomerAlgorithmModel
    {
        public HashSet<int> CustomerIDHash { get; set; }

        // topN 優質客戶群
        public HashSet<int> TopNCustomerIDHash { get; set; }
    }

  • HashSetConverter 增加邏輯鑒別是否為保留字段

        public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
        {
            if (writer.Path == "TopNCustomerIDHash")
            {
                writer.WriteStartArray();

                foreach (var item in value)
                {
                    writer.WriteValue(item);
                }

                writer.WriteEndArray();
            }
            else
            {
                writer.WriteValue(value.Count);
            }
        }

  • 最後給 TopNCustomerIDHash 賦值

            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000)),
                TopNCustomerIDHash = new HashSet<int>(Enumerable.Range(1, 10)),
            };

三塊都搞定后就可以把程序跑起來了,如下圖:

貌似鑽牛角尖的問題是解決了,既然鑽牛角尖肯定要各種鄙視,比如這裏的 ReportModel 我是不需要的,CustomerType 我也是不需要的,我僅僅需要看一下 DisplayNameTotalCustomerCount 這兩個字段就可以了, 那這個要怎麼解決呢?

3. 使用 匿名類型

確實很多時候記日誌,就是為了跟蹤 Model 中你特別關心的那幾個字段,所以摻雜了多餘的字段確實也是沒必要的,這裏可以用匿名來解決,我就來寫一段代碼:


    var json = JsonConvert.SerializeObject(new
    {
        algorithModel.DisplayName,
        algorithModel.Report.TotalCustomerCount
    }, Formatting.Indented);

三: 總結

雖然阻擊了幾個回合,但同時也發現了 Newtonsoft 中還有特別多的未挖掘功能,真的需要好好研究研究,源碼已下好,接下來準備做個系列來解剖一下,值得一提的是 .Net中已自帶了 System.Text.Json.JsonSerializer 類,目前來看功能還不算太豐富,簡單用用還是可以的,本篇就說到這裏,希望對您有幫助。

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新

.Net Core微服務入門全紀錄(五)——Ocelot-API網關(下)

前言

上一篇【.Net Core微服務入門全紀錄(四)——Ocelot-API網關(上)】已經完成了Ocelot網關的基本搭建,實現了服務入口的統一。當然,這隻是API網關的一個最基本功能,它的進階功能還有很多很多。

服務發現

首先需要解決的就是服務發現的問題,服務發現的優點之前講過,就不說了。
上一篇中我們的服務地址都是寫在ocelot.json配置文件里,現在我們需要結合之前的Consul來實現服務發現。

  • 改造代碼:

首先NuGet安裝Ocelot.Provider.Consul

修改Startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            //添加ocelot服務
            services.AddOcelot()
                .AddConsul();//添加consul支持
        }

修改ocelot.json配置:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}

這個配置應該很好理解,就是把我們上次的DownstreamHostAndPorts節點去掉了,然後增加了ServiceDiscoveryProvider服務發現相關配置。
注意,Ocelot除了支持Consul服務發現以外,還有Eureka也可以,Eureka也是一個類似的註冊中心。

好了,代碼修改就差不多了,下面運行程序測試一下:

客戶端正常運行。

至此我們就實現了服務註冊與發現和api網關的基本功能。接下來就要提到:服務治理

服務治理

其實服務治理也沒有一個非常明確的定義。它的作用簡單來說,就是幫助我們更好的管理服務,提升服務的可用性。——緩存,限流,熔斷,鏈路追蹤 等等。。。都屬於常用的服務治理手段。
之前講的負載均衡,服務發現也可以算是服務治理。

  • 緩存:

在Ocelot中啟用緩存,需要NuGet安裝一下Ocelot.Cache.CacheManager

修改Startup.cs中的ConfigureServices()方法:

//添加ocelot服務
services.AddOcelot()
    //添加consul支持
    .AddConsul()
    //添加緩存
    .AddCacheManager(x =>
    {
        x.WithDictionaryHandle();
    });

修改ocelot.json配置文件:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}

在Routes路由配置中增加了FileCacheOptions。TtlSeconds代表緩存的過期時間,Region代表緩衝區名稱,這個我們目前用不到。

好了,代碼修改完需要編譯重啟一下網關項目,然後打開客戶端網站測試一下:

可以看到,5秒之內的請求都是同樣的緩存數據。Ocelot也支持自定義緩存。

  • 限流:

限流就是限制客戶端一定時間內的請求次數。
繼續修改配置:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "5s",
        "PeriodTimespan": 2,
        "Limit": 1
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "5s",
        "PeriodTimespan": 2,
        "Limit": 2
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false,
      "QuotaExceededMessage": "too many requests...",
      "HttpStatusCode": 999,
      "ClientIdHeader": "Test"
    }
  }
}

在Routes路由配置中增加了RateLimitOptions。ClientWhitelist代表客戶端白名單,在白名單中的客戶端可以不受限流的影響;EnableRateLimiting代表是否限流;Period代表限流的單位時間,例如1s,5m,1h,1d等;PeriodTimespan代表客戶端達到請求上限多少秒后可以重試;Limit代表客戶端在定義的時間內可以發出的最大請求數。
在GlobalConfiguration配置中也增加了RateLimitOptions。DisableRateLimitHeaders代表是否禁用X-Rate-Limit和Retry-After標頭(請求達到上限時response header中的限制數和多少秒后能重試);QuotaExceededMessage:代表請求達到上限時返回給客戶端的消息;HttpStatusCode:代表請求達到上限時返回給客戶端的HTTP狀態代碼。ClientIdHeader可以允許自定義用於標識客戶端的標頭。默認情況下為“ ClientId”。
最重要的就是Period,PeriodTimespan,Limit這幾個配置。

重新編譯啟動看一下效果:

  • 超時/熔斷

超時很好理解,就是網關請求服務時可容忍的最長響應時間。熔斷的意思就是當請求某個服務的異常次數達到一定量時,那麼網關在一定時間內就不再對這個服務發起請求了,直接熔斷。
Ocelot中啟用 超時/熔斷 需要NuGet安裝一下Ocelot.Provider.Polly

修改Startup.cs中的ConfigureServices()方法:

//添加ocelot服務
services.AddOcelot()
    //添加consul支持
    .AddConsul()
    //添加緩存
    .AddCacheManager(x =>
    {
        x.WithDictionaryHandle();
    })
    //添加Polly
    .AddPolly();

同樣的在ocelot.json路由配置中增加QoSOptions:

"QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3,
        "DurationOfBreak": 10000,
        "TimeoutValue": 5000
      }

ExceptionsAllowedBeforeBreaking代表發生錯誤的次數,DurationOfBreak代表熔斷時間,TimeoutValue代表超時時間。
以上的配置意思就是當服務發生3次錯誤時,那麼就熔斷10秒,期間客戶端的請求直接返回錯誤,10秒之後恢復。
這個不太好模擬,就不演示了,應該也挺好理解的。

。。。。。。

關於服務治理的學問還有很多,不繼續了。。。就到此為止吧。
想要更深入了解Ocelot的,請看官網:https://ocelot.readthedocs.io/en/latest/
或者看源碼:https://github.com/ThreeMammals/Ocelot

下一篇準備說一下:事件總線。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

聚甘新

認證授權方案之JwtBearer認證

1.前言

回顧:認證方案之初步認識JWT

在現代Web應用程序中,即分為前端與後端兩大部分。當前前後端的趨勢日益劇增,前端設備(手機、平板、電腦、及其他設備)層出不窮。因此,為了方便滿足前端設備與後端進行通訊,就必須有一種統一的機制。所以導致API架構的流行。而RESTful API這個API設計思想理論也就成為目前互聯網應用程序比較歡迎的一套方式。

這種API架構思想的引入,因此,我們就需要考慮用一種標準的,通用的,無狀態的,與語言無關的身份認證方式來實現API接口的認證。

HTTP提供了一套標準的身份驗證框架:服務端可以用來針對客戶端的請求發送質詢(challenge),客戶端根據質詢提供應答身份驗證憑證。

質詢與應答的工作流程如下:服務端向客戶端返回401(Unauthorized,未授權)狀態碼,並在WWW-Authenticate頭中添加如何進行驗證的信息,其中至少包含有一種質詢方式。然後客戶端可以在請求中添加Authorization頭進行驗證,其Value為身份驗證的憑證信息。

在本文中,將要介紹的是以Jwt Bearer方式進行認證。

2.Bearer認證

本文要介紹的Bearer驗證也屬於HTTP協議標準驗證,它隨着OAuth協議而開始流行,詳細定義見: RFC 6570。

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

A security token with the property that any party in possession of the token (a “bearer”) can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

因此Bearer認證的核心是Token,Bearer驗證中的憑證稱為BEARER_TOKEN,或者是access_token,它的頒發和驗證完全由我們自己的應用程序來控制,而不依賴於系統和Web服務器,Bearer驗證的標準請求方式如下:

Authorization: Bearer [BEARER_TOKEN] 

那麼使用Bearer驗證有什麼好處呢?

  • CORS: cookies + CORS 並不能跨不同的域名。而Bearer驗證在任何域名下都可以使用HTTP header頭部來傳輸用戶信息。
  • 對移動端友好: 當你在一個原生平台(iOS, Android, WindowsPhone等)時,使用Cookie驗證並不是一個好主意,因為你得和Cookie容器打交道,而使用Bearer驗證則簡單的多。
  • CSRF: 因為Bearer驗證不再依賴於cookies, 也就避免了跨站請求攻擊。
  • 標準:在Cookie認證中,用戶未登錄時,返回一個302到登錄頁面,這在非瀏覽器情況下很難處理,而Bearer驗證則返回的是標準的401 challenge

3.JWT

上面介紹的Bearer認證,其核心便是BEARER_TOKEN,那麼,如何確保Token的安全是重中之重。一種是通過HTTPS的方式,另一種是通過對Token進行加密編碼簽名,而最流行的Token編碼簽名方式便是:JSON WEB TOKEN。

Json web token (Jwt), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計為緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

JWT是由.分割的如下三部分組成:

Header.Payload.Signature

還記得之前說個的一篇認證方案之初步認識JWT嗎?沒有的,可以看看,對JWT的特點和基本原理介紹,可以進一步的了解。

學習了之前的文章后,我們可以發現使用JWT的好處在於通用性、緊湊性和可拓展性。

  • 通用性:因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
  • 緊湊性:JWT的構成非常簡單,字節佔用很小,通過 GET、POST 等放在 HTTP 的 header 中,便於傳輸。
  • 可擴展性:JWT是自我包涵的,因為有了payload部分,包含了必要的一些其他業務邏輯所必要的非敏感信息,自身存儲,不需要在服務端保存會話信息, 非常易於應用的擴展。

4.開始

1. 註冊認證服務

在這裏,我們用微軟給我們提供的JwtBearer認證方式,實現認證服務註冊 。

引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

註冊服務,將服務添加到容器中,

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        var Issurer = "JWTBearer.Auth";  //發行人
        var Audience = "api.auth";       //受眾人
        var secretCredentials = "q2xiARx$4x3TKqBJ";   //密鑰

        //配置認證服務
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(o=>{
            o.TokenValidationParameters = new TokenValidationParameters
            {
                //是否驗證發行人
                ValidateIssuer = true,
                ValidIssuer = Issurer,//發行人
                //是否驗證受眾人
                ValidateAudience = true,
                ValidAudience = Audience,//受眾人
                //是否驗證密鑰
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)),
                
                ValidateLifetime = true, //驗證生命周期
                RequireExpirationTime = true, //過期時間
            };
        });
    }

注意說明:

一. TokenValidationParameters的參數默認值:
1. ValidateAudience = true,  ----- 如果設置為false,則不驗證Audience受眾人
2. ValidateIssuer = true ,   ----- 如果設置為false,則不驗證Issuer發布人,但建議不建議這樣設置
3. ValidateIssuerSigningKey = false,
4. ValidateLifetime = true,  ----- 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比
5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必須包含Expires
6. ClockSkew = TimeSpan.FromSeconds(300), ----- 允許服務器時間偏移量300秒,即我們配置的過期時間加上這個允許偏移的時間值,才是真正過期的時間(過期時間 +偏移值)你也可以設置為0,ClockSkew = TimeSpan.Zero

調用方法,配置Http請求管道:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        //1.先開啟認證
        app.UseAuthentication();
        //2.再開啟授權
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

JwtBearerOptions的配置中,通常IssuerSigningKey(簽名秘鑰), ValidIssuer(Token頒發機構), ValidAudience(頒發給誰) 三個參數是必須的,后兩者用於與TokenClaims中的IssuerAudience進行對比,不一致則驗證失敗。

2.接口資源保護

創建一個需要授權保護的資源控制器,這裏我們用建立API生成項目自帶的控制器,WeatherForecastController.cs, 在控制器上使用Authorize即可

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

3. 生成Token

因為微軟為我們內置了JwtBearer驗證,但是沒有提供Token的發放,所以這裏我們要實現生成Token的方法

引入Nugets包:System.IdentityModel.Tokens.Jwt

這裏我們根據IdentityModel.Tokens.Jwt文檔給我們提供的幫助類,提供了方法WriteToken創建Token,根據參數SecurityToken,可以實例化,JwtSecurityToken,指定可選參數的類。

        /// <summary>
        /// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters.
        /// </summary>
        /// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param>
        /// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param>
        /// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List&lt;object&gt; } will be created to contain the duplicate values.</param>
        /// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param>
        /// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param>
        /// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param>
        /// <exception cref="ArgumentException">If 'expires' &lt;= 'notbefore'.</exception>
        public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null)
        {
            if (expires.HasValue && notBefore.HasValue)
            {
                if (notBefore >= expires)
                    throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value)));
            }

            Payload = new JwtPayload(issuer, audience, claims, notBefore, expires);
            Header = new JwtHeader(signingCredentials);
            RawSignature = string.Empty;
        }

這樣,我們可以根據參數指定內容:

1. string iss = "JWTBearer.Auth";  // 定義發行人
2. string aud = "api.auth";       //定義受眾人audience
3. IEnumerable<Claim> claims = new Claim[]
{
new Claim(JwtClaimTypes.Id,"1"),
new Claim(JwtClaimTypes.Name,"i3yuan"),
};//定義許多種的聲明Claim,信息存儲部分,Claims的實體一般包含用戶和一些元數據
4. var nbf = DateTime.UtcNow;  //notBefore  生效時間
5. var Exp = DateTime.UtcNow.AddSeconds(1000);  //expires 過期時間
6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符
 var secret = Encoding.UTF8.GetBytes(sign);
 var key = new SymmetricSecurityKey(secret);
 var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

好了,通過以上填充參數內容,進行傳參賦值得到,完整代碼如下:

新增AuthController.cs控制器:

    [HttpGet]
    public IActionResult GetToken()
    {
        try
        {
            //定義發行人issuer
            string iss = "JWTBearer.Auth";
            //定義受眾人audience
            string aud = "api.auth";

            //定義許多種的聲明Claim,信息存儲部分,Claims的實體一般包含用戶和一些元數據
            IEnumerable<Claim> claims = new Claim[]
            {
                new Claim(JwtClaimTypes.Id,"1"),
                new Claim(JwtClaimTypes.Name,"i3yuan"),
            };
            //notBefore  生效時間
            // long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
            var nbf = DateTime.UtcNow;
            //expires   //過期時間
            // long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds();
            var Exp = DateTime.UtcNow.AddSeconds(1000);
            //signingCredentials  簽名憑證
            string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符
            var secret = Encoding.UTF8.GetBytes(sign);
            var key = new SymmetricSecurityKey(secret);
            var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds);
		    var JwtHander = new JwtSecurityTokenHandler();
            var token = JwtHander.WriteToken(jwt);
            return Ok(new
            {
                access_token = token,
                token_type = "Bearer",
            });
        }
        catch (Exception ex)
        {
            throw;
        }
    }
注意:
1.SecurityKey 的長度必須 大於等於 16個字符,否則生成會報錯。(可通過在線隨機生成密鑰)

5. 運行

訪問獲取Token方法,獲取得到access_token:

再訪問,授權資源接口,可以發現,再沒有添加請求頭token值的情況下,返回了401沒有權限。

這次,在請求頭通過Authorization加上之前獲取的token值后,再次進行訪問,發現已經可以獲取訪問資源控制器,並返回對應的數據。

6.擴展說明

在HTTP標準驗證方案中,我們比較熟悉的是”Basic”和”Digest”,前者將用戶名密碼使用BASE64編碼後作為驗證憑證,後者是Basic的升級版,更加安全,因為Basic是明文傳輸密碼信息,而Digest是加密後傳輸。

一、Basic基礎認證

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全。

客戶端請求需要帶Authorization請求頭,值為“Basic xxx”,xxx為“用戶名:密碼”進行Base64編碼後生成的值。 若客戶端是瀏覽器,則瀏覽器會提供一個輸入用戶名和密碼的對話框,用戶輸入用戶名和密碼后,瀏覽器會保存用戶名和密碼,用於構造Authorization值。當關閉瀏覽器后,用戶名和密碼將不再保存。

憑證為“YWxhzGRpbjpvcGVuc2VzYWl1”,是通過將“用戶名:密碼”格式的字符串經過的Base64編碼得到的。而Base64不屬於加密範疇,可以被逆向解碼,等同於明文,因此Basic傳輸認證信息是不安全的。

Basic基礎認證圖示:

缺陷匯總
1.用戶名和密碼明文(Base64)傳輸,需要配合HTTPS來保證信息傳輸的安全。
2.即使密碼被強加密,第三方仍可通過加密后的用戶名和密碼進行重放攻擊。
3.沒有提供任何針對代理和中間節點的防護措施。
4.假冒服務器很容易騙過認證,誘導用戶輸入用戶名和密碼。

二、Digest摘要認證

Digest認證是為了修復基本認證協議的嚴重缺陷而設計的,秉承“絕不通過明文在網絡發送密碼”的原則,通過“密碼摘要”進行認證,大大提高了安全性。

Digest認證步驟如下:
第一步:客戶端訪問Http資源服務器。由於需要Digest認證,服務器返回了兩個重要字段nonce(隨機數)和realm。
第二步:客戶端構造Authorization請求頭,值包含username、realm、nouce、uri和response的字段信息。其中,realm和nouce就是第一步返回的值。nouce只能被服務端使用一次。uri(digest-uri)即Request-URI的值,但考慮到經代理轉發后Request-URI的值可能被修改、因此實現會複製一份副本保存在uri內。response也可叫做Request-digest,存放經過MD5運算后的密碼字符串,形成響應碼。
第三步:服務器驗證包含Authorization值的請求,若驗證通過則可訪問資源。
Digest認證可以防止密碼泄露和請求重放,但沒辦法防假冒。所以安全級別較低。
Digest和Basic認證一樣,每次都會發送Authorization請求頭,也就相當於重新構造此值。所以兩者易用性都較差。

Digest認證圖示:

7.注意

  1. 在進行JwtBearer認證時,在生成token之後,還需要與刷新token配合使用,因為當用戶執行了退出,修改密碼等操作時,需要讓該token無效,無法再次使用,所以,會給access_token設置一個較短的有效期間,(JwtBearer認證默認會驗證有效期,通過notBeforeexpires來驗證),當access_token過期后,可以在用戶無感知的情況下,使用refresh_token重新獲取access_token,但這就不屬於Bearer認證的範疇了,但是我們可以通過另一種方式通過IdentityServer的方式來實現,在後續中會對IdentityServer進行詳細講解。
  2. 在生成token的時候,需要用的secret,主要是用來防止token被偽造與篡改。因為當token被劫取的時候,可以得到你的令牌中帶的一些個人不重要的信息明文,但不用擔心,只要你不在生成token里把私密的個人信息放出去的話,就算被動機不良的人得到,也做不了什麼事情。但是你可能會想,如果用戶自己隨便的生成一個 token ,帶上你的信息,那不就可以隨便訪問你的資源服務器了,因此這個時候就需要利用secret 來生成 token,來確保数字簽名的正確性。而且在認證授權資源,進行token解析的時候,通過微軟的源碼發現,已經幫我們封裝了方法,對secret進行了校驗了,確保了token的安全性,從而保證api資源的安全。

8.總結

  1. JwtToken在認證時,無需Security token service安全令牌服務器的參与,都是基於Claim的,默認會驗證有效期,通過notBeforeexpires來驗證,這在分佈式中提供給了極大便利。
  2. JwtToken與平台、無言無關,在前端也可以直接解析出Claims。
  3. 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
  4. 後面會對認證授權方案中的授權這一塊進行說明分享。
  5. 本示例源碼地址
    參考JwtBearer源碼

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

【其他文章推薦】

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

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

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

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

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

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

聚甘新

深入理解 EF Core:EF Core 寫入數據時發生了什麼?

閱讀本文大概需要 14 分鐘。

原文:https://bit.ly/2C67m1C
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我自己的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。

這是深入理解 EF Core 系列的第二篇文章。第一篇是關於 EF Core 如何從數據庫讀取數據的;而這一篇是關於 EF Core 如何向數據庫寫入數據的。這是四種數據庫操作 CRUD(新增、讀取、更新和刪除)中的 CUD 部分。

我假設你對 EF Core 已經有了一定的認識,但在深入學習之前,我們先來了解一下如何使用 EF Core,以確保我們已經掌握了一些基本知識。這是一個“深入研究”的課題,所以我準備大量的技術細節,希望我的描述方式你能理解。

本文是“深入理解 EF Core”系列中的第二篇。以下是本系列文章列表:

  • 深入理解 EF Core:當 EF Core 從數據庫讀取數據時發生了什麼?
  • 深入理解 EF Core:當 EF Core 寫入數據到數據庫時發生了什麼?(本文)
  • 深入理解 EF Core:使用查詢過濾器軟刪除數據(敬請期待)

概要

∮. EF Core 可以通過新的或已存在的關聯關係創建一個新的實體。為此,它必須以正確的順序來組織實體類,以便能夠建立各類之間的關聯。這使得開發人員很容易寫出具有複雜關聯關係的類。

∮. 當你調用 EF Core 的 Add 命令來添加一個新條目時,會發生很多事情:

  • EF Core 查找添加的類和其他類的所有關聯。對於每個關聯的類,它也會判斷是否需要在數據庫中創建一個新行,或者僅僅鏈接到數據庫中現有的行。
  • 它使用現有行的主鍵或偽主鍵為新添加的條目填充外鍵信息。

∮. EF Core 可以監測你從數據庫讀取的類的屬性的變化。它通過已讀入的類的隱藏副本來實現這一點。當你調用 SaveChanges 時,它會將每個讀入的屬性值與其原始值進行比較,並且會創建相應的數據更新命令。

∮. EF Core 的 Remove 方法將刪除參數提供的實體類的主鍵所指向的數據行。如果被刪除的類有外鍵關聯,那麼數據庫會自動進行相關的操作(比如級聯刪除),但你可以更改刪除的規則。

數據寫入基礎

提示:如果你已經對 EF Core 有一定的了解,那麼你可以跳過這一部分,這隻是一個簡單的 EF Core 寫入數據的例子。

在這一節的介紹中,我將描述一下本文用到的數據庫結構,然後給出一個簡單的數據庫寫入示例。下面是類/表的基本關係圖:

這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由於篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。

和讀取數據一樣,EF Core 將數據寫入數據庫也是五部分:

  1. 數據庫服務器,如 SQL server, Sqlite, PostgreSQL…
  2. 映射到數據庫的一個類或多個類—我將它們稱為實體類
  3. 一個繼承 EF Core 的 DbContext 的類,該類包含 EF Core 的配置
  4. 一個創建數據庫的方法
  5. 最後,向數據庫寫入數據的命令

下面的單元測試代碼來自我的 GitHub 創庫[2],展示了一個簡單的示例,它從現有數據庫中讀取 4 個 Book 實體及其關聯的 BookAuthor 和 Authors 實體。

[Fact]
public void TestWriteTestDataSqliteInMemoryOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<EfCoreContext>();
    using (var context = new EfCoreContext(options))
    {
        context.Database.EnsureCreated();

        //ATTEMPT
        var book = new Book
        {
            Title = "Test",
            Reviews = new List<Review>()
        };
        book.Reviews.Add(new Review { NumStars = 5 });
        context.Add(book);
        context.SaveChanges();

        //VERIFY
        var bookWithReview = context.Books
            .Include(x => x.Reviews).Single()
        bookWithReview.Reviews.Count.ShouldEqual(1);
    }
}

現在,如果我們將單元測試代碼對應到上面的 5 部分,結果是這樣的:

  1. 數據庫服務器——第 5 行:我選擇了一個 Sqlite 數據庫服務器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用我的一個 NuGet 包 EfCore.TestSupport 創建了一個內存數據庫(內存中的數據庫對於單元測試非常有用,因為你可以為這個測試建立一個新的空數據庫)。
  2. 實體類——和上一篇結構差不多,但是多了一個與 Book 關聯的 Review 實體類。
  3. 一個繼承 DbContext 的類——第 6 行:EfCoreContext 類繼承了 DbContext 類並配置了從類到數據庫的映射關係(你可以在我的 GitHub 倉庫[3] 中查看該類)。
  4. 一個創建數據庫的方法——第 8 行:第一次執行時,這句代碼會創建一個新的數據庫,包括創建正確的表、鍵、索引等。EnsureCreated 方法用於單元測試,但對於真實的應用程序,你最好手動執行 EF Core 的 Migration 命令。
  5. 向數據庫寫入數據的命令——第 17 到 18 行:
    • 第 17 行:Add 方法告訴 EF Core 需要將一個 Book 實體及其關係(在本例中,只是一個 Review 實體)寫入數據庫。
    • 第 18 行:SaveChange 方法將在數據庫中的 Books 和 Reviews 表中創建新行。

在 //VERIFY 註釋之後的最後幾行用來檢查數據是否已經被寫入數據庫。

在本例中,你向數據庫添加了新的記錄(SQL 的 INSERT INTO 命令)。EF Core 也可以處理更新和刪除數據庫的數據,下一節介紹這個新增示例,然後介紹其他新增、更新和刪除的示例。

寫入數據時數據庫端發生了什麼

我將從創建一個新的 Book 實體類和新的 Review 實體類開始。這兩個類的關係比較簡單。使用上面單元測試的例子,主要代碼如下:

var book = new Book
{
    Title = "Test",
    Reviews = new List<Review>()
};
book.Reviews.Add(new Review { NumStars = 1 });
context.Add(book);
context.SaveChanges();

為了將這兩個實體添加到數據庫,EF Core 需要這樣做:

  1. 確定它應該以什麼順序創建這些新行——在本例中,它必須在 Books 表中創建一行,這樣它就擁有 Books 的主鍵。
  2. 將主鍵複製到與其關聯的外鍵——在本例中,它將 Books 中的主鍵 BookId 複製到 Review 的外鍵。
  3. 複製數據庫中新創建的數據,以便實體類正確表示數據庫的數據——在這種情況下,它必須複製 BookId 並更新 BookId 屬性,包括 Book 和 Review 實體類以及 Review 實體類的 ReviewId。

下面我們看看上面代碼生成的 SQL 語句:

-- 第一次訪問數據庫
SET NOCOUNT ON;
-- 向數據庫的 Books 表生成一條新數據.
-- 數據庫生成 Books 的主鍵值
INSERT INTO [Books] ([Description], [Title], ...)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);

-- 返回主鍵值,檢查並確認數據行是否已添加
SELECT [BookId] FROM [Books]
WHERE @@ROWCOUNT = 1 AND [BookId] = scope_identity();

-- 第二次訪問數據庫
SET NOCOUNT ON;
-- 向數據庫的 Review 表生成一條新數據.
-- 數據庫生成 Review 的主鍵值
INSERT INTO [Review] ([BookId], [Comment], ...)
VALUES (@p7, @p8, @p9, @p10);

-- 返回主鍵值,檢查並確認數據行是否已添加
SELECT [ReviewId] FROM [Review]
WHERE @@ROWCOUNT = 1 AND [ReviewId] = scope_identity();

重要的一點是,EF Core 是按正確的順序處理實體類的,這樣它就可以填充外鍵。這是簡單的例子,但我遇到一個客戶項目的例子是,我不得不建立一個非常複雜的數據組成的 15 個不同的實體類,一些實體類是新增,一些是更新和刪除,EF Core 通過一個 SaveChanges 將把所有工作有序地完成了庫。因此,EF Core 使開發者可以很容易地將複雜的數據寫入數據庫。

我之所以提到這一點,是因為我看到過在 EF Core 代碼中,開發人員多次調用 SaveChanges 方法來從第一個新增命令中獲得主鍵,並把它設置為相關實體的外鍵。例如:

var book = new Book
{
    Title = "Test"
};
context.Add(book);
context.SaveChanges();
var review = new Review { BookId = book.BookId, NumStars = 1 }
context.Add(review);
context.SaveChanges();

雖然這代碼效果是一樣的,但它有一個缺陷——如果第二 SaveChanges 失敗,那麼就會發生部分數據更新到數據庫的情況。在某種情況下,這可能不是個問題,但對於像我客戶那種需要保證數據一致的情況,就非常糟糕了。

因此,從中得到的收穫是,您不需要將主鍵複製到外鍵中,因為你可以設置導航屬性,EF Core 將為您挑選出外鍵。因此,如果你認為需要調用兩次 SaveChanges,那麼通常意味着你沒有設置正確的導航屬性來處理這種情況。

寫數據時 DbContext 做了什麼

在上一節中,你看到了 EF Core 在數據庫端做了什麼,現在你要看看在 EF Core 中發生了什麼。大多數情況,你不需要知道,但有時候知道這些是非常重要的。例如,你只能在 SaveChanges 之前捕獲數據的狀態。而對於自增主鍵,你只有在 SaveChanges 被調用之後才能拿到主鍵的值。

與上一個示例相比,這個示例稍微複雜一些。在這個示例中,我想向你展示 EF Core 通過從數據庫中讀取的已有實體類的實例來處理另一個實體類的新實例。下面的代碼創建了一個新的 Book,但 Author 已經在數據庫中了。代碼註明了階段 1、階段 2 和階段 3,然後我用圖表描述每個階段發生的事情。

// 階段 1
var author = context.Authors.First();
var bookAuthor = new BookAuthor { Author = author };
var book = new Book
{
    Title = "Test Book",
    AuthorsLink = new List<BookAuthor> { bookAuthor }
};

// 階段 2
context.Add(book);

// 階段 3
context.SaveChanges();

接下來的三個圖向你展示了實體類及其跟蹤數據在每個階段內發生的事情。每個圖显示了其階段結束時的以下數據:

  • 流程的每個階段中每個實例的狀態。
  • Book 和 BookAuthor 類是棕色的,表示它們是類的新實例,需要添加到數據庫中,而 Author 實體類是藍色的,表示從數據庫中讀取的實例。
  • 主鍵和外鍵旁邊的括號是其當前的值。如果一個鍵是 (0),那麼它還沒有被設值。
  • 箭頭連線連接的是從導航屬性到其相應實體類。
  • 每個階段之間的變化通過粗體文本或箭頭連線的粗線显示。

下圖显示了階段 1 完成后的情況。用於設置一個新的 Book 實體類(左)和一個新的 BookAuthor 實體類(中),後者將 Book 連接接到一個現有的 Author 實體類(右)。

階段 1 這是調用任何 EF Core 方法之前的起點。

下一個圖显示了執行 context.Add(book) 之後的情況。更改部分以粗體显示。

你可能會驚訝於執行 Add 方法時所發生的事情。它將作為參數提供的實體的狀態設置為 Added(在本例中為 Book 實體)。然後通過導航屬性或外鍵值查看與該實體連接的所有實體。對於每個被連接的實體,它會執行以下操作(注意:我不知道它們執行的確切順序)。

  • 如果實體未被跟蹤(即其當前狀態為 Detached),則將其狀態設置為 Added——在本例中,它是 BookAuthor 實體。
  • 它用主鍵的值填充正確的外鍵的值。如果連接的主鍵還不可用,它將為跟蹤的主鍵和外鍵數據的 CurrentValue 屬性設置一個惟一的負數。你可以在上圖中看到這一點。
  • 它填充當前未設值的導航屬性——如上圖中所示。

最後一個階段,即階段 3,是調用 SaveChanges 方法時發生的情況,如圖所示。

在“寫數據時數據庫端發生了什麼”一節中,數據庫更改的任何列都被複制回實體類中,以便實體與數據庫匹配。在本例中,數據更新到數據庫時會把主鍵值更新到 Book 的 BookId 和 BookAuthor 的 BookId。
而且,此次數據庫寫入完成后,涉及的所有實體的狀態都會被更新為 Unchanged。

對於上面這樣一個很長的解釋,很多時候你不需要知道這些細節,你只管它“工作了”就行。但是,當某些東西不能正常工作或者想做一些複雜的事情時,比如記錄實體類的更改,那麼了解這個就非常有用。

更新數據到數據庫時發生了什麼

上面的示例是關於向數據庫添加新記錄的,但是沒有進行更新。在這一節中,我將展示當你更新數據庫中已有的記錄時會發生什麼。這裏使用我上一篇文章“EF Core 讀取數據時發生了什麼?”中講到的查詢例子。

這個更新很簡單,只有三行,但是它在代碼中有三個階段:讀取、更新和保存。

var books = context.Books.ToList();
books.First().PublishedOn = new DateTime(2020, 1, 1);
context.SaveChanges();

下圖展示了這三個階段:

如你所見,你使用的查詢類型很重要——普通查詢加載數據並把返回的實體保存一份“跟蹤快照”,返回的實體類被稱為“被跟蹤的”。如果實體沒有沒跟蹤,則無法更新它。

注意:上一節中的 Author 實體類也是被“跟蹤”的。在這個例子中,Author 的跟蹤狀態告訴 EF Core Author 已經在數據庫中,因此不會再次創建。

因此,如果你更改了加載的跟蹤實體類中的任何屬性,那麼當你調用 SaveChanges 時,它會將所有跟蹤的實體類與它們的跟蹤快照進行比較。對於每個類,它遍歷映射到數據庫字段的所有屬性。這個過程稱為更改跟蹤,它將檢測被跟蹤實體中的每一個更改,包括 Title、PubishedOn 等非關係屬性。

在這個簡單的示例中,只有 4 個 Book 實體,但在實際應用程序中,您可能已經加載了許多相互連接的實體類。在這一點上,比較階段可能需要一段時間。因此,你應該嘗試只加載需要更改的實體類。

注意:EF Core 有一個名為 Update 的命令,它用於更新每個屬性/列的特定情況。EF Core 會自動跟蹤更改,默認只更新已更改的屬性/列。

每次更新都將創建一個 SQL UPDATE 命令,所有這些更新都將在一個 SQL 事務中執行。使用 SQL 事務意味着所有更新都作為一個整體,如果其中任何一部分失敗,那麼事務中的任何數據庫更改都會失效。

從數據庫刪除數據時發生了什麼

CRUD 的最後一部分是 DELETE,這在某些情況很簡單,你只需要調用 context.Remove()。在另一些情況它很複雜,例如,當你刪除另一個實體類依賴的實體類時會發生什麼?

刪除映射到數據庫的實體類的方法是 Remove。舉個例子,我加載一個特定的 Book,然後刪除它。

var book = context.Books
    .Single(p => p.Title == "Quantum Networking");
context.Remove(book);
context.SaveChanges();

它的階段如下:

  1. 加載要刪除的 Book 實體類。這會獲取它的所有屬性數據,但對於刪除,您實際上只需要實體類的主鍵。
  2. 調用 Remove 方法其實是將 Book 的狀態標記為 Deleted。這些信息會有序地存儲在跟蹤快照中。
  3. 最後,SaveChanges 創建一個 SQL DELETE 命令,該命令與任何其他數據庫更改一起發送到數據庫,並且在一個 SQL 事務中。

這看起來很簡單,但這裏發生了一些重要的事情,從代碼看並不明顯。原來書名為“Quantum Networking”的書有其他一些實體類關聯到到它——在某個特定的測試用例中,書名為“Quantum Networking”的書關聯到以下實體類:

  • 兩個 Review
  • 一個 PriceOffer
  • 一個 BookAuthor

現在,Review、PriceOffer 和 BookAuthor 實體類只與這本書相關——我們使用術語叫依賴於 Book 實體類。因此,如果這本書被刪除了,那麼這些 Review、PriceOffer 和所關聯的 BookAuthor 數據行也應該被刪除。如果不刪除,那麼數據庫的關聯關係就是不正確的,SQL 數據庫將拋出異常。那麼,為什麼做這個刪除工作?

這裏所發生的都是因為設置了級聯刪除,級聯刪除規則設置了 Books 表和三個依賴表之間的數據庫關係。
下面是 EF Core 為創建 Review 表而生成的 SQL 命令的一個示例:

CREATE TABLE [Review] (
    [ReviewId] int NOT NULL IDENTITY,
    [VoterName] nvarchar(max) NULL,
    [NumStars] int NOT NULL,
    [Comment] nvarchar(max) NULL,
    [BookId] int NOT NULL,
    CONSTRAINT [PK_Review] PRIMARY KEY ([ReviewId]),
    CONSTRAINT [FK_Review_Books_BookId] FOREIGN KEY ([BookId])
         REFERENCES [Books] ([BookId]) ON DELETE CASCADE
);

CONSTRAINT 語句部分定義了約束規則,該約束表示 Review 通過 BookId 列鏈接到 Books 表中的一行。在該約束的最後,你將看到關於 DELETE 級聯的規則。它告訴數據庫,如果它鏈接的書被刪除了,那麼這個 Review 也應該被刪除。這意味着書的刪除是允許的,因為所有相關的行也被刪除了。

這是非常有用的,但有時候想要更改刪除規則怎麼辦?比如我決定不允許刪除客戶訂單中存在的書。為了做到這一點,我在 DbContext 中添加了一些 EF Core 配置來改變刪除規則,如下:

public class EfCoreContext : DbContext
{
    private readonly Guid _userId;

    public EfCoreContext(DbContextOptions<EfCoreContext> options)
        : base(options)

    public DbSet<Book> Books { get; set; }
    //… 其它 DbSet<T>

    protected override void OnModelCreating(ModelBuilder modelBuilder
    {
        //… 其它代碼

        modelBuilder.Entity<LineItem>()
            .HasOne(p => p.ChosenBook)
            .WithMany()
            .OnDelete(DeleteBehavior.Restrict);
    }
}

一旦該配置應用到數據庫,就不會生成 SQL 語句的 DELETE CASCADE。這意味着,如果你試圖刪除客戶訂單中的一本書,那麼數據庫將返回一個錯誤,EF Core 將把這個錯誤變成一個異常。

這使你對正在發生的事情有一個更深的了解,但是還有相當多的內容我沒有介紹(但我在我的書中介紹了)。這裡有一些關於刪除我還沒有提到的事情:

  • 實體類之間可以有 required 關係(依賴關係)和 optional 關係,EF Core 為每種類型使用不同的規則。
  • EF Core 可以通過設置 DeleteBehavior 來設置級聯刪除規則,當實體類存在循環關聯關係時,可以用它避免一些錯誤——一些數據庫在發現循環刪除時會拋出錯誤。
  • 你可以在調用 Remove 方法時提供一個新的只有主鍵有值的類來刪除實體類。這在處理只返回主鍵的場景非常有用。

總結

本文我介紹了 CRUD 中的新增、更新和刪除部分,前一篇文章介紹了讀取部分。

正如您所看到的,使用 EF Core 在數據庫中創建記錄很容易,但內部很複雜。你通常不需要知道 EF Core 或數據庫中發生了什麼,但了解一些細節可以讓你更好地利用 EF Core 的優勢。

更新也很簡單——只需在你讀入的實體類中更改一個或多個屬性,當你調用 SaveChanges 時,EF Core 會找到已更改的數據,並構建 SQL 命令更新數據庫。這適用於非關係屬性(如圖 Book 的 Title 屬性)和導航屬性(你可以在他們的關係)。

最後,我們看了一個刪除案例。同樣很容易使用,但很多處理也是在背後執行的。​ 另外,敬請關注我的下一篇文章,我將討論所謂的“軟刪除”。如果你設置了一個標誌,EF Core 就不會再看到這個實體類了,它仍然在數據庫中,但它是隱藏的。

希望本文對你有用,也希望你關注本系列的更多文章。

祝你編程愉快!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO

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

【其他文章推薦】

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

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

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

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

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

聚甘新

第 10 篇 評論接口

作者:HelloGitHub-追夢人物

此前我們一直在操作博客文章(Post)資源,並藉此介紹了序列化器(Serializer)、視圖集(Viewset)、路由器(Router)等 django-rest-framework 提供的便利工具,藉助這些工具,就可以非常快速地完成 RESTful API 的開發。

評論(Comment)是另一種資源,我們同樣藉助以上工具來完成對評論資源的接口開發。

首先是設計評論 API 的 URL,根據 RESTful API 的設計規範,評論資源的 URL 設計為:/comments/

對評論資源的操作有獲取某篇文章下的評論列表和創建評論兩種操作,因此相應的 HTTP 請求和動作(action)對應如下:

HTTP請求 Action URL
GET list_comments /posts/:id/comments/
POST create /comments/

文章評論列表 API 使用自定義的 action,放在 /post/ 接口的視圖集下;發表評論接口使用標準的 create action,需要定義單獨的視圖集。

然後需要一個序列化器,用於評論資源的序列化(獲取評論時),反序列化(創建評論時)。有了編寫文章序列化器的基礎,評論序列化器就是依葫蘆畫瓢的事。

comments/serializers.py

from rest_framework import serializers
from .models import Comment


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = [
            "name",
            "email",
            "url",
            "text",
            "created_time",
            "post",
        ]
        read_only_fields = [
            "created_time",
        ]
        extra_kwargs = {"post": {"write_only": True}}

注意這裏我們在 Meta 中增加了 read_only_fieldsextra_kwargs 的聲明。

read_only_fields 用於指定只讀字段的列表,由於 created_time 是自動生成的,用於記錄評論發布時間,因此聲明為只讀的,不允許通過接口進行修改。

extra_kwargs 指定傳入每個序列化字段的額外參數,這裏給 post 序列化字段傳入了 write_only 關鍵字參數,這樣就將 post 聲明為只寫的字段,這樣 post 字段的值僅在創建評論時需要。而在返回的資源中,post 字段就不會出現。

首先來實現創建評論的接口,先為評論創建一個視圖集:

comments/views.py

from rest_framework import mixins, viewsets
from .models import Comment
from .serializers import CommentSerializer

class CommentViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = CommentSerializer

    def get_queryset(self):
        return Comment.objects.all()

視圖集非常的簡單,混入 CreateModelMixin 后,視圖集就實現了標準的 create action。其實 create action 方法的實現也非常簡單,我們來學習一下 CreateModelMixin 的源碼實現。

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

核心邏輯在 create 方法:首先取到綁定了用戶提交數據的序列化器,用於反序列化。接着調用 is_valid 方法校驗數據合法性,如果不合法,會直接拋出異常(raise_exception=True)。否則就執行序列化的 save 邏輯將評論數據存入數據庫,最後返迴響應。

接着在 router 里註冊 CommentViewSet 視圖集:

router.register(r"comments", comments.views.CommentViewSet, basename="comment")

進入 API 交互後台,可以看到首頁列出了 comments 接口的 URL,點擊進入 /comments/ 后可以看到一個評論表單,在這裏可以提交評論數據與創建評論的接口進行交互。

接下來實現獲取評論列表的接口。通常情況下,我們都是只獲取某篇博客文章下的評論列表,因此我們的 API 設計成了 /posts/:id/comments/。這個接口具有很強的語義,非常符合 RESTful API 的設計規範。

由於接口位於 /posts/ 空間下,因此我們在 PostViewSet 添加自定義 action 來實現,先來看代碼:

blog/views.py

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    # ...
    
    @action(
            methods=["GET"],
            detail=True,
            url_path="comments",
            url_name="comment",
            pagination_class=LimitOffsetPagination,
            serializer_class=CommentSerializer,
    )
    def list_comments(self, request, *args, **kwargs):
        # 根據 URL 傳入的參數值(文章 id)獲取到博客文章記錄
        post = self.get_object()
        # 獲取文章下關聯的全部評論
        queryset = post.comment_set.all().order_by("-created_time")
        # 對評論列表進行分頁,根據 URL 傳入的參數獲取指定頁的評論
        page = self.paginate_queryset(queryset)
        # 序列化評論
        serializer = self.get_serializer(page, many=True)
        # 返回分頁后的評論列表
        return self.get_paginated_response(serializer.data)

action 裝飾器我們在上一篇教程中進行了詳細說明,這裏我們再一次接觸到 action 裝飾器更為深入的用法,可以看到我們除了設置 methodsdetailurl_path 這些參數外,還通過設置 pagination_classserializer_class 來覆蓋原本在 PostViewSet 中設置的這些類屬性的值(例如對於分頁,PostViewSet 默認為我們之前設置的 PageNumberPagination,而這裏我們替換為 LimitOffsetPagination)。

list_comments 方法邏輯非常清晰,註釋中給出了詳細的說明。另外還可以看到我們調用了一些輔助方法,例如 paginate_queryset 對查詢集進行分頁;get_paginated_response 返回分頁后的 HTTP 響應,這些方法其實都是 GenericViewSet 提供的通用輔助方法,源碼也並不複雜,如果不用這些方法,我們自己也可以輕鬆實現,但既然 django-rest-framework 已經為我們寫好了,直接復用就行,具體的實現請大家通過閱讀源碼進行學習。

現在進入 API 交互後台,進入某篇文章的詳細接口,例如訪問 /api/posts/5/,Extra Actions 下拉框中可以看到 List comments 的選項:

點擊 List comments 即可進入這篇文章下的評論列表接口,獲取這篇文章的評論列表資源了:

關注公眾號加入交流群

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

【其他文章推薦】

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

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

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

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

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

聚甘新