標籤: 如何寫文案

  • LG Chem 再下一城 與奧迪簽電池供應協議

    LG Chem 再下一城 與奧迪簽電池供應協議

        近來積極拓展車用電池事業的南韓化學巨擘 LG Chem Ltd. 宣佈,該公司已敲定價值數億美元的電動車電池供應合約,對象是德國汽車製造商福斯 (Volkswagen AG) 旗下的豪華車廠奧迪 (Audi AG)。   LG Chem 目前為通用汽車 (General Motors Co.)、雷諾 (Renault SA)、福特汽車 (Ford Motor Co.)、現代汽車 (Hyundai Motor Co.) 以及起亞汽車 (Kia Motors Corp.) 等 20 家業者的車用電池供應商。日前 LG Chem 才宣布將與通用汽車等汽車廠商攜手研發一款車用電池,其單次充電的里程數多達 200 英里,為市面上大多數電動車的兩倍以上。   20 日 LG Chem 又傳出好消息,該公司透過聲明表示,會為奧迪的次世代插電式混合動力電動車提供車用電池,總值多達數百億韓圜,而由於奧迪與母公司福斯一起共用了許多汽車平台,因此 LG Chem 認為未來應該能與福斯敲定更多類似的協議。     (圖片來源:)

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

  • Tesla Model S 變身電動跑車 性能更強

    Tesla Model S 變身電動跑車 性能更強

      Saleen Automotive 在改車界享負盛名,無論任何車款經其改裝後,都會變得更快更有勁。最近他們看上了 Tesla 的 Model S 電動車,將其改裝為 Saleen FourSixteen,比起原版本擁有更高性能。   Saleen FourSixteen 採用與 Model S 相同的 416 匹引擎,不過其齒輪比率卻提高至 11.39:1,能大幅提高跑車的加速性能,因此由 0 加速至 100 公里只需 4 秒,比起 Model S 足足快了 1 秒。此外,Saleen FourSixteen 同時亦改善了冷卻系統及防傾桿,與及採用賽車用的車底盤及碳纖維剎車盤。   車廂內部也經過重新設計,將 Model S 原有的五座位改成全真皮的四座位,變成了一部真正的四座位超級電動跑車。該跑車目前已開放預訂,但售價並不便宜,約 15 萬 2 千美元。     (圖片來源:)

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

  • 跟着whatwg看一遍事件循環

    跟着whatwg看一遍事件循環

    前言

    對於單線程來說,事件循環可以說是重中之重了,它為任務分配不同的優先級,井然有序的調度。讓js解析,用戶交互,頁面渲染等互不衝突,各司其職。

    我們書寫的代碼無時無刻都在和事件循環打交道,要想寫出更流暢,我們就必須深入了解事件循環,下面我們將從規範中翻譯和解讀整個流程。

    以下內容來自whatwg文檔,均為個人理解,若有不對,煩請指出,我會第一時間修改,避免誤導他人!

    正文

    為了協調用戶操作,js執行,頁面渲染,網絡請求等事件,每個宿主中,存在事件循環這樣的角色,並且該角色在當前宿主中是唯一的。

    簡單解釋一下宿主:宿主是一個ECMAScript執行上下文,一般包含執行上下文棧,運行時執行環境,宿主記錄和一個執行線程,除了這個執行線程外,其他的專屬於當前宿主。例如,某些瀏覽器在不同的tabs使用同一個執行線程。

    不僅如此,事件循環又存於在各個不同場景,有瀏覽器環境下的,worker環境下的和Worklet環境下的。

    Worklet是一個輕量級的web worker,可以讓開發者訪問更底層的渲染工作線,也就是說你可以通過Worklet去干預瀏覽器的渲染環境。

    提到了worklet,那就順便看一個例子(需開啟服務,不要以file協議運行),通過這個例子,可以看到事件循環不同階段觸發了什麼鈎子函數:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Document</title>
            <style>
                .fancy {
                    background-image: paint(headerHighlight);
                    display: layout(sample-layout);
                    background-color: green;
                }
            </style>
        </head>
        <body>
            <h1 class="fancy">My Cool Header</h1>
            <script>
                console.log('開始');
                CSS.paintWorklet.addModule('./paint.js');
                CSS.layoutWorklet.addModule('./layout.js');
    
                requestAnimationFrame(() => {
                    console.log('requestAnimationFrame');
                });
                Promise.resolve().then(() => {
                    console.log('微任務');
                });
                setTimeout(function () {
                    document.querySelector('.fancy').style.height = '150px';
                    ('translateZ(0)');
    
                    Promise.resolve().then(() => {
                        console.log('新一輪的微任務');
                    });
                    requestAnimationFrame(() => {
                        console.log('新一輪的requestAnimationFrame');
                    });
                }, 2000);
                console.log(2);
            </script>
        </body>
    </html>
    
    
    // paint.js
    registerPaint(
        'headerHighlight',
        class {
            static get contextOptions() {
                console.log('contextOptions');
                return {alpha: true};
            }
    
            paint(ctx) {
                console.log('paint函數');
            }
        }
    );
    
    // ==========================分割線
    
    // layout.js
    registerLayout(
        'sample-layout',
        class {
            async intrinsicSizes(children, edges, styleMap) {}
    
            async layout(children, edges, constraints, styleMap, breakToken) {
                console.log('layout階段');
            }
        }
    );
    

    事件循環有一個或多個Task隊列,每個Task隊列都是Task的一個集合。其中Task不是指我們的某個函數,而是一個上下文環境,結構如下:

    • step:一系列任務將要執行的步驟
    • source:任務來源,常用來對相關任務進行分組和系列化
    • document:與當前任務相關的document對象,如果是非window環境則為null
    • 環境配置對象:在任務期間追蹤記錄任務狀態

    這裏的Task隊列不是Task,是一個集合,因為取出一個Task隊列中的Task是選擇一個可執行的Task,而不是出隊操作。

    微任務隊列是一個入對出對的隊列。

    這裏說明一下,Task隊列為什麼有多個,因為不同的Task隊列有不同的優先級,進而進行次序排列和調用,有沒有感覺react的fiber和這個有點類似?

    舉個例子,Task隊列可以是專門負責鼠標和鍵盤事件的,並且賦予鼠標鍵盤隊列較高的優先級,以便及時響應用戶操作。另一個Task隊列負責其他任務源。不過也不要餓死任何一個task,這個後續處理模型中會介紹。

    Task封裝了負責以下任務的算法:

    • Events: 由專門的Task在特定的EventTarget(一個具有監聽訂閱模式列表的對象)上分發事件對象
    • Parsing: html解析器標記一個或多個字節,並處理所有生成的結果token
    • Callbacks: 由專門的Task觸發回調函數
    • Using a resource: 當該算法獲取資源的時候,如果該階段是以非阻塞方式發生,那麼一旦部分或者全部資源可用,則由Task進行後續處理
    • Reacting to DOM manipulation: 通過dom操作觸發的任務,例如插入一個節點到document

    事件循環有一個當前運行中的Task,可以為null,如果是null的話,代表着可以接受一個新的Task(新一輪的步驟)。

    事件循環有微任務隊列,默認為空,其中的任務由微任務排隊算法創建。

    事件循環有一個執行微任務檢查點,默認為false,用來防止微任務死循環。

    微任務排隊算法:

    1. 如果未提供event loop,設置一個隱式event loop。
    2. 如果未提供document,設置一個隱式document.
    3. 創建一個Task作為新的微任務
    4. 設置setp、source、document到新的Task上
    5. 設置Task的環境配置對象為空集
    6. 添加到event loop的微任務隊列中

    微任務檢查算法:

    1. 如果微任務檢查標誌為true,直接return
    2. 設置微任務檢查標誌為true
    3. 如果微任務隊里不為空(也就是說微任務添加的微任務也會在這個循環中出現,直到微任務隊列為空):
      1. 從微任務隊列中找出最老的任務(防餓死)
      2. 設置當前執行任務為這個最老的任務
      3. 執行
      4. 重置當前執行任務為null
    4. 通知環境配置對象的promise進行reject操作
    5. 清理indexdb事務(不太明白這一步,如果有讀者了解,煩請點撥一下)
    6. 設置微任務檢查標誌為false

    處理模型

    event loop會按照下面這些步驟進行調度:

    1. 找到一個可執行的Task隊列,如果沒有則跳轉到下面的微任務步驟
    2. 讓最老的Task作為Task隊列中第一個可執行的Task,並將其移除
    3. 將最老的Task作為event loop的可執行Task
    4. 記錄任務開始時間點
    5. 執行Task中的setp對應的步驟(上文中Task結構中的step)
    6. 設置event loop的可執行任務為null
    7. 執行微任務檢查算法
    8. 設置hasARenderingOpportunity(是否可以渲染的flag)為false
    9. 記住當前時間點
    10. 通過下面步驟記錄任務持續時間
      1. 設置頂層瀏覽器環境為空
      2. 對於每個最老Task的腳本執行環境配置對象,設置當前的頂級瀏覽器上下文到其上
      3. 報告消耗過長的任務,並附帶開始時間,結束時間,頂級瀏覽器上下文和當前Task
    11. 如果在window環境下,會根據硬件條件決定是否渲染,比如刷新率,頁面性能,頁面是否在後台,不過渲染會定期出現,避免頁面卡頓。值得注意的是,正常的刷新率為60hz,大概是每秒60幀,大約16.7ms每幀,如果當前瀏覽器環境不支持這個刷新率的話,會自動降為30hz,而不是丟幀。而李蘭其在後台的時候,聰明的瀏覽器會將這個渲染時機降為每秒4幀甚至更低,事件循環也會減少(這就是為什麼我們可以用setInterval來判斷時候能打開其他app的判斷依據的原因)。如果能渲染的話會設置hasARenderingOpportunity為true。

    除此之外,還會在觸發resize、scroll、建立媒體查詢、運行css動畫等,也就是說瀏覽器幾乎大部分用戶操作都發生在事件循環中,更具體點是事件循環中的ui render部分。之後會進行requestAnimationFrame和IntersectionObserver的觸發,再之後是ui渲染

    1. 如果下麵條件都成立,那麼執行空閑階段算法,對於開發者來說就是調用window.requestIdleCallback方法
      1. 在window環境下
      2. event loop中沒有活躍的Task
      3. 微任務隊列為空
      4. hasARenderingOpportunity為false

    借鑒網上的一張圖來粗略表示下整個流程

    小結

    上面就是整個事件循環的流程,瀏覽器就是按照這個規則一遍遍的執行,而我們要做的就是了解並適應這個規則,讓瀏覽器渲染出性能更高的頁面。

    比如:

    1. 非首屏相關性能打點可以放到idle callback中執行,減少對頁面性能的損耗
    2. 微任務中遞歸添加微任務會導致頁面卡死,而不是隨着事件循環一輪輪的執行
    3. 更新元素布局的最好時機是在requestAnimateFrame中
    4. 盡量避免頻繁獲取元素布局信息,因為這會觸發強制layout(哪些屬性會導致強制layout?),影響頁面性能
    5. 事件循環有多個任務隊列,他們互不衝突,但是用戶交互相關的優先級更高
    6. resize、scroll等會伴隨事件循環中ui渲染觸發,而不是根據我們的滾動觸發,換句話說,這些操作自帶節流
    7. 等等,歡迎補充

    最後感謝大家閱讀,歡迎一起探討!

    提前祝大家端午節nb

    參考

    composite

    深入探究 eventloop 與瀏覽器渲染的時序問題

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

    【其他文章推薦】

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

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

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

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

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

  • 只看到了別人28歲從字節跳動退休,背後的期權知識你知道嗎?

    只看到了別人28歲從字節跳動退休,背後的期權知識你知道嗎?

    前一陣字節跳動程序員,年僅 28 歲的郭宇財務自由,宣布退休的消息在網上火了一把,這個事情大家應該都知道了,細節就不多說了。

    我剛看了這個新聞之後,起初內心出現的幾個關鍵詞是:卧槽、牛、羡慕……在意識到瞎感慨、羡慕沒啥用之後,我心裏不禁有個疑問:他的錢是怎麼來的?

    我找在字節上班的朋友打聽了一下,據說郭同學近幾年投資賺了錢。當然還有網上大部分傳的,郭同學的發家致富是因為有字節跳動的期權,期權的價值隨着字節的估值翻上了天。

    說到期權,估計很多人對期權不太清楚,除了關注熱點,我們還得學點知識對不對。今天我就和大家聊聊期權,省的將來大家遇到類似機會了,你一臉懵逼,甚至掉坑裡。

    同時,因為我不是專業的,只是接觸過,所以講的不對、不嚴謹的,大家多擔待。

    講期權之前,我們先接着上面說完郭同學的期權。

    郭同學 2014 年加入字節,當時字節估值 5 億美元,現在字節估值 1000 億美元以上,估值妥妥的漲了 200 倍啊。如果當時有 10 萬期權,現在值 2000 萬;如果有 100 萬期權,現在值 2 億。就算考慮上後幾輪字節融資帶來的稀釋,打個折扣,那也是一筆不小的財富啊。

    那麼郭同學有多少期權呢?說實話我也不知道,引用幾年前他和同學的對話,大家自己猜測吧。

    郭同學的事說完了,我們繼續說期權。

    1. 什麼是期權?

    先舉個例子:

    比如公司承諾你說,4 年之後,可以按每股 10 塊錢的價格,購買公司 1 萬份股票。期權的“期”就是指 4 年時間;期權的“權”就是 10 塊錢購買 1 萬份股票的權利。

    等 4 年之後,如果公司的股票價格漲到了 100 塊,你依然可以用 10 塊錢的價格購買 1 萬份股票。花 10 萬買的股票就值 100 萬了,轉手賣出去之後,你能賺 90 萬。恭喜你!

    另外一種可能是,如果 4 年後公司股票價格掉到 8 塊錢,這時候你肯定就不會按 10 塊錢價格購買股票了,不行使這個權利你也不會虧錢。

    期權在國內互聯網公司不算少見,這當初是從硅谷那些互聯網公司學來的。

    公司為了留住你,同時也為了長期激勵你,期權通常不會一下都給你,按上面的例子來說,4 年時間,可能每滿一年給你 1/4 數量的期權,或者是按月給。這也是為什麼有人把期權稱為“金手銬”。

    2. 離職了期權怎麼辦?

    上面的例子繼續展開說,如果乾滿了兩年,還沒幹滿四年,離職了期權怎麼辦?

    這種情況,需要看之前你和公司怎麼約定的。通常有 3 種方式:

    1. 干滿 2 年,給了 50%,那麼就可以對這 50% 的期權行權,然後帶走,等着公司上市后變成股票。
    2. 公司回購你這 50% 的期權。
    3. 被公司收回,什麼都沒有。

    說一個我自己的例子,我在 2008 年打工的時候,公司給過我期權,數量不多,具體的價格、時間已經完全記不住了,後來離職的時候,期權都被公司收回了。當時對期權完全不懂,想着收回就收回唄。後來那家公司發展的不是很好,即使我堅守到現在,那些期權啥也兌現不了。否則,我現在腸子都要悔青了。

    3. 有期權是不是肯定拿低工資?

    這個不一定。比如小米早期,就有高工資低期權、低工資高期權兩種方案供員工選擇。

    如果你覺得老闆靠譜、公司有希望,同時少拿點工資也不影響自己的生活,當然可以選擇低工資高期權。博一把,一旦成了,那麼期權帶來的財富肯定遠超過工資。

    當然選擇高工資低期權,也能理解,畢竟生活不易,很多人都要養家糊口。而且創業公司的成功率非常低,期權最後能兌現都是小概率事件。萬一期權不能兌現,除了損失了工資,時間成本和機會成本也不小。

    但是大家要小心,有的公司會用期權給你畫個大大的餅,刻意把你的工資壓到非常低。如果老闆真有誠意,起碼會給你開一個可接受的工資。

    期權是為了留住和激勵優秀員工的,工資太低,優秀員工的忠誠度、穩定性怎麼保證?

    4. 白紙黑字

    如果你遇到的老闆是天天把分期權掛在嘴上,但是遲遲不和你簽協議,那你要小心了。無論怎樣,任何口頭承諾都沒有任何意義。

    一定要白紙黑字落在協議上,特別是行權時間、價格、數量、期限、公司回購價格等等要寫到合同協議里,省的將來產生糾紛。

    當然,最省心的還是找一個靠譜的公司,比如:

    1. 公司的創始人信用好,如果他之前兌現過期權,那就更好了。
    2. 是著名投資機構投資的公司。
    3. 過往的融資都很順利,而且每一輪融資公司估值都漲的很快。
    4. 能吸引很多優秀的人才加入,也能側面說明這家公司比較健康。大家都眼瞎的概率還是很小的。

    寫完以上幾點,我不禁想到了曾經的樂視,好像能滿足以上好幾點……

    5. 期權的其他知識

    進入公司差一年,為什麼期權相差很多?
    拿字節跳動來說,入職時間差一年,可能期權差一個數量級。發展越迅猛的公司,這個差距越大,早期公司估值低,越早加入越有優勢。

    期權怎麼變現?
    通常除了公司回購,只能等上市了。

    員工是否可以賣掉期權?
    一般來說,員工是不能出售期權的。如果員工可以出售期權,只能賣給你的公司。

    6. 乾股

    再說一下乾股。

    有些技術大牛在加入創業公司的時候,創始人可能會對技術大牛說:你出技術,不用出錢了,我白給你 20% 的乾股(或叫技術股)。

    乾股聽起來好像挺好,大家一起干,不用出錢,還可以分紅。

    但是通常乾股是虛股,只有分紅的權利,沒有參与經營、決策的權利,往往也不能轉讓。

    乾股是不規範的叫法,由於技術合伙人沒有實際出資,也沒經過工商登記,有可能忙活半天,最後你的名字都不會出現在股東名單里。如果之前也沒簽過協議,這麼不清不楚的,將來會不會被踢出局,都不好說。

    最穩妥的辦法還是去工商局登記變更,把乾股變成法律上認可的股權。

    最後

    以上就是這篇文章的主要內容,估計很多讀者會覺得,期權、股票這些離自己太遠,和自己關係不大。

    千萬別這麼想!

    第一、現在互聯網行業給股票、期權的公司不算少見,提前儲備點知識,萬一以後談 offer 的時候出現這些字眼,你也不至於啥也不懂。

    第二、覺得自己現在還很菜,公司的股票、期權都是給核心員工的,輪不到自己。現在菜又怎樣,誰不是由菜變強的,現在沒有股票、期權,不代表以後也拿不到。

    第三、在工資不降低或者可接受的範圍內,我建議大家拿股票、期權,萬一哪天自己就莫名其妙的發財了呢。

    第三點大家一定要謹慎,再次提醒一下你們,別為了股票、期權就接受一個特別低的工資,這塊我教訓深刻。

    就寫這麼多吧,希望這篇掃盲文章能讓你們有收穫。起碼我旁邊的碼農二毛又有收穫了:

    二毛說:四哥你這麼一說,我明白了,我現在就算能跳槽進字節,就算給我期權,現在太晚了,字節估值都這麼高了,別說暴富,估計連湯都喝不上了。我應該怎麼辦呢?

    我說:打開你的炒股軟件……

    二毛說:我明白了,四哥你是讓我買字節的股票,拿着股票也可以跟着字節市值上漲賺錢。

    我說:呸,字節還沒上市,你買毛線。我的意思是,如果幾年前你能拿出來選股票的勁頭來分析、選擇出一家潛力公司,早跳槽過去堅持到現在,估計你也發財了。

    感謝閱讀,如果覺得有用,希望你可以分享給你的朋友、同事,或者點個在看支持一下。

    —END—

    我準備了一些純手打的高質量PDF,有好友贊助的也有我自己的,大家可以免費領取:

    深入淺出Java多線程、HTTP超全匯總、Java基礎核心總結、程序員必知的硬核知識大全、簡歷面試談薪的超全乾貨。

    別看數量不多,但篇篇都是乾貨,看完的都說很肝。

    領取方式:掃碼關注后,在公眾號後台回復:666

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

    【其他文章推薦】

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

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

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

    ※幫你省時又省力,新北清潔一流服務好口碑

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

  • 微服務中如何設計一個權限授權服務

    微服務中如何設計一個權限授權服務

    基於角色的訪問控制  (RBAC) 

      是將系統訪問限製為授權用戶的一種方法,是圍繞角色和特權定義的與策略無關的訪問控制機制,RBAC的組件使執行用戶分配變得很簡單。

      在組織內部,將為各種職務創建角色執行某些操作的權限已分配給特定角色。成員或職員(或其他系統用戶)被分配了特定角色,並且通過這些角色分配獲得執行特定系統功能所需的權限。由於未直接為用戶分配權限,而是僅通過其角色(一個或多個角色)獲取權限,因此,對單個用戶權限的管理就變成了簡單地為用戶帳戶分配適當角色的問題。這簡化了常見操作,例如添加用戶或更改用戶部門。

    RBAC定義了三個主要規則

      1、角色分配:僅當對象已選擇或分配了角色時,對象才能行使權限。

      2、角色授權:必須為主體授權主體的活動角色。使用上面的規則1,此規則可確保用戶只能承擔獲得其授權的角色。

      3、權限授權:僅當對象的活動角色被授權時,對象才能行使權限。使用規則1和2,此規則可確保用戶只能行使其被授權的權限。

    創建RBAC的模型

    菜單 

      public class SysMenu
        {
            /// <summary>
            ///     父級
            /// </summary>
            public int ParentId { get; set; } = 0;
    
            /// <summary>
            ///     菜單名稱
            /// </summary>
            [StringLength(20)]
            public string Name { get; set; }
    
            /// <summary>
            ///     菜單地址
            /// </summary>
            [StringLength(20)]
            [Required]
            public string Url { get; set; }
    
            /// <summary>
            ///     層級
            /// </summary>
            [Column(TypeName = "tinyint(4)")]
            public int Level { get; set; } = 1;
    
            /// <summary>
            ///     菜單權限(list<int /> json)
            /// </summary>
            [StringLength(100)]
            public string Operates { get; set; }
    
            /// <summary>
            ///     排序
            /// </summary>
            public int Sort { get; set; }
    
            /// <summary>
            /// 菜單圖標
            /// </summary>
            public string Icon { get; set; }        
        }

     功能

      public class SysOperate
        {
            /// <summary>
            ///     按鈕名稱
            /// </summary>
            [StringLength(20)]
            [Required]
            public string Name { get; set; }
    
            /// <summary>
            ///     備註
            /// </summary>
            [StringLength(int.MaxValue)]
            public string Remark { get; set; }
    
            /// <summary>
            /// 唯一標識
            /// </summary>
            [Required]
            public int Unique { get; set; }
        }

    角色

      public class SysRole 
        {
            /// <summary>
            ///     角色名稱
            /// </summary>
            [StringLength(20)]
            [Required]
            public string Name { get; set; }
    
            /// <summary>
            ///     備註
            /// </summary>
            [StringLength(int.MaxValue)]
            public string Remark { get; set; }
        }

    用戶

        public class SysUser
        {
            /// <summary>
            ///     角色id
            /// </summary>
            public int RoleId { get; set; }
    
            /// <summary>
            ///     用戶名
            /// </summary>
            [StringLength(32)]
            [Required]
            public string UserName { get; set; }
    
            /// <summary>
            ///     密碼
            /// </summary>
            [StringLength(500)]
            [Required]
            public string Password { get; set; }
        }

     微服務中讓它成為一個授權權限服務

      在日常工作中,總會有很多系統要做,每個系統都要一套完整的權限功能,有現成的直接拿來粘貼複製,沒有現成的又要浪費很多時間去設計實現它。 如果有這樣一個服務,我們可以節省很多不必要的粘貼複製操作,節省很多時間。

      於是 ketchup.zero 這樣一個服務就誕生了。它是基於ketchu微服務框架來實現的一個權限授權服務,基本可以滿足我們日常工作的的權限需求。

      服務的前端是基於vue的模板d2admin 開發的。

    ketchup.zero的功能

    登陸

    面板

     

     用戶配置角色

     

     菜單設置擁有那些權限

     

     權限/功能/按鈕 管理

     

     角色設置權限

     

    最後安利

    如果它對你有幫助,請給一波start

    服務 ketchup.zero 源碼地址:https://github.com/simple-gr/ketchup.zero

    微服務框架 ketchup 源碼地址:https://github.com/simple-gr/ketchup 

    ketchup 交流群:592407137

     

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

    【其他文章推薦】

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

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

    ※超省錢租車方案

  • 前端業務代碼中的配置化

    前端業務代碼中的配置化

    業務代碼中的配置化

    工作中有許多邏輯冗雜、迭代頻繁的業務代碼,隨着迭代將越來越難以維護,一些場景適合通過配置化的方式來處理便於維護。

    一、什麼是業務代碼配置化?

    根據業務場景使用配置化的 Object|Array|Map 處理條件判斷邏輯,通常需要配置文件 CONFIG.js,若邏輯複雜需添加 getConfig 的處理函數 – tool.js

    • 本質上 if/else 邏輯是一種狀態匹配

    • 表驅動法,使用表數據,存儲對應的狀態處理

    • 可讀性好,減少了繁雜嵌套的 if-else,讀取配置,邏輯更清晰

    • 可維護性高,邏輯分支的增刪只是 CONFIG 的增刪

    二、如何在業務場景中進行代碼配置化?

    1. 簡單的狀態映射

    • 按需使用 Object|Map 配置
    單一條件
    • Object 形式:
    // CONFIG.JS
      export const STATUS = {
        STUDENT: 0,
        TEACHER: 1,
        MA_NONG: 2,
      };
      export const WORK_MAP = {
        STATUS.STUDENT: '學生',
        STATUS.TEACHER: '老師',
        STATUS.MA_NONG: '碼農',
      };
    
    // index.js
      this.setData({
        work: WORK_MAP[status],
      });
    
      axios.post(url, { status: STATUS.MA_NONG });
    
    • Map 形式:
    // CONFIG.JS
    export const WORK_MAP = new Map([
      [0, "學生"],
      [1, "老師"],
      [2, "碼農"],
    ]);
    // index.js
    this.setData({
      work: WORK_MAP.get(status),
    });
    
    多重條件
    const config = new Map([
      [
        (condition0, condition1, condition2) =>
          condition0 && condition1 && condition2,
        () => {
          console.log("map0");
        },
      ],
      [
        (condition0, condition1, condition2) =>
          condition0 || condition1 || condition2,
        () => {
          console.log("map1");
        },
      ],
    ]);
    config.forEach((action, _if) => _if(0, 1, 0) && action());
    

    2. 每個狀態有多種屬性

    • 多個屬性
    • 使用 Array 配置
    // CONFIG.JS
      export const CONFIG = [
        {
          status: STATUS.STUDENT,
          name: '學生',
          action: '談戀愛',
        },
        {
          status: STATUS.TEACHER,
          name: '老師',
          action: '教書',
        },
        {
          status: STATUS.MA_NONG,
          name: '碼農',
          action: '寫bug',
        },
      ];
    
    // index.js
      <!-- 根據狀態不同的行為 -->
      function action(status) {
        const { name, work } = CONFIG.find(i => i.status === status);
        console.log(`${name}在${action}`);
      }
    

    3. 每個狀態有多種屬性且參數定製化

    • 參數高度定製化,不同狀態需要適配接口不同的字段
    • 使用 Array 配置
    • 通過配置函數並傳參注入接口數據可滿足定製化需求
    // CONFIG.JS
      export const CONFIG = [
        {
          status: STATUS.STUDENT,
          name: '學生',
          action: () => {
            console.log('學生的工作是談戀愛');
          },
        },
        {
          status: STATUS.TEACHER,
          name: '老師',
          action: (info) => {
            alert(`老師${info.age}歲,每天${info.action}`);
          },
        },
        {
          status: STATUS.MA_NONG,
          name: '碼農',
          action: (info) => {
            toast(`碼農工作${info.workTime}年了,頭髮僅剩${info.hair}根了`);
          },
        },
      ];
    
    // index.js
      <!-- 根據接口狀態action -->
      function action(res) {
        const { action, info } = CONFIG.find(i => i.status === res.status);
        action && action(info); // 傳參定製化
      }
    

    三、實例

    大首頁瀑布流 item 樣式

    • 根據 list 接口下發的 item 的類型(type)&樣式(layout)字段取 item 中的封面、標題、標籤、頭像…,字段各不相同
    • 十幾種 item 類型,有的還有不同的 layout,item 數據下發方式不同
    • 公共組件,需要適配其他模塊的接口數據作展示

    index.xml

    • 數據驅動,減少模板中的判斷邏輯
    <view class="panel" bind:tap="goDetail">
      <!-- 封面 -->
      <image  wx:if="{{panel.cover}}" class="panel__cover" src="{{panel.cover.image}}">
          <view class="panel__tag {{panel.tagClass}}" wx:if="{{panel.tagClass}}">{{panel.tag}}</view>
      </image>
      <!-- 標題 -->
      <view class="panel__titl" wx:if="{{panel.title}}">{{panel.title}}</view>
      <!-- footer -->
      <view class="panel__footer" wx:if="{{panel.showFooter}}">
        <image class="panel__footer-icon" wx:if="{{panel.user.icon}}" src="{{panel.user.icon}}"></image>
        <text class="panel__footer-name" wx:if="{{panel.user.nickname}}">{{panel.user.nickname}}</text>
        <text class="panel__footer-comment" wx:if="{{panel.commentCount}}">{{panel.commentCount}}評論</text>
      </view>
    </view>
    

    CONFIG.js

    import { Layout, NewsType, Redirect } from 'src/constant';
    import { formatImage, formatUser } from './tool';
    
    /**
     * 配置項
     * @param {String} title 標題
     * @param {String} cover 封面
     * @param {String} tag 標籤
     * @param {Object} user 用戶信息
     * @param {Boolean} showFooter 是否显示footer
     * @param {Boolean} isAd 是否廣告
     * @param {Function} itemWrap 兼容接口數據函數,數據可能以ref_xxx下發,比如帖子:ref_post
     * ......
     */
    
    <!-- 默認配置項 -->
    export const DEFAULT = {
      title: ({ title = '' }) => title,
      cover: ({ image_list = [], article_images = [] }) =>
        formatImage(image_list[0]) || formatImage(article_images[0]),
      showFooter: true,
      user: ({ user, user_account, article_source_tx = '' }) =>
        user
          ? formatUser(user)
          : user_account
          ? formatUser(user_account)
          : {
              icon: '',
              nickname: article_source_tx,
            },
    };
    
    export const CONFIG = [
      {
        type: NewsType.NEWS,
        ...DEFAULT,
        tag: '資訊',
        tagClass: 'news',
      },
      {
        type: NewsType.VIDEO,
        ...DEFAULT,
        tag: '',
        tagClass: 'video',
        cover: ({ image_gif_list = [], image_list = [] }) => formatImage(image_gif_list[0] || image_list[0]),
        video: ({ video_url = '', tencent_vid = '', image_gif_list = [] }) => ({
          hasVideo: true,
          src: tencent_vid || video_url,
          video_url,
          tencent_vid,
          gifCover: formatImage(image_gif_list[0]),
        }),
      },
      {
        type: Redirect.EVAL_DETAIL,
        layouts: [
          {
            layout: Layout.EVALUATION,
            ...DEFAULT,
            tag: '口碑',
            tagClass: 'praise',
          },
          {
            layout: Layout.PRAISE_COMPONENT,
            ...DEFAULT,
            tag: '口碑',
            tagClass: 'praise',
            itemWrap: ({ ref_car_score = {} }) => ref_car_score,
            title: ({ chosen_topic = '' }) => chosen_topic,
            commentCount: ({ comment_count = null }) => comment_count,
            cover: ({ images = [], recommend_images = [] }) =>
              formatImage(images[0]) ||
              formatImage(getCoverFromRecommendImages(recommend_images)),
          },
          {
            layout: Layout.NORMAL,
            ...DEFAULT,
          },
        ],
      },
      ......
    ];
    

    tool.js

    import { CONFIG, DEFAULT, AD_CONFIG } from "./CONFIG";
    // 獲取瀑布流item數據
    export const getPanelData = (item) => {
      const getConfigByTypeAndLayout = () => {
        let config = CONFIG.find((i) => i.type == item.type);
        if (item.isAd) {
          config = AD_CONFIG;
        }
        if (config && config.layouts) {
          config = config.layouts.find(
            (i) => i.layout === item.layout_type || i.layout === item.display_style
          );
        }
        if (!config) {
          config = DEFAULT;
          console.log("no-config", item.type, item.layout_type, item);
        }
        return config;
      };
      const getPanelDataByConfig = (c) => {
        const panel = {};
        let _item = item;
        if (c.itemWrap) {
          _item = c.itemWrap(item);
        }
        Object.keys(c).forEach((key) => {
          if (typeof c[key] === "function") {
            panel[key] = c[key](_item);
          } else {
            panel[key] = c[key];
          }
        });
        return panel;
      };
      // 根據item的類型、樣式獲取配置
      const config = getConfigByTypeAndLayout(item);
      // 根據配置獲取瀑布流item信息
      return getPanelDataByConfig(config);
    };
    

    四、結語

    所以,業務代碼配置化很簡單,大家也都一直在用,只是如果在一些業務場景中都形成配置化的習慣或者共識,可能更好維護吧。

    鄙人拙見,大佬賜教~

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

    【其他文章推薦】

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

    ※超省錢租車方案

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

  • 在採用K8S之前您必須了解的5件事情

    在採用K8S之前您必須了解的5件事情

    作者簡介

    Christopher Tozzi,自2008年來以自由職業者的身份對Linux、虛擬化、容器、數據存儲及其相關主題進行報道。

    時至今日,Kubernetes已然成為風靡一時的容器編排調度工具,許多IT分析師均提出了企業應當在何時採用Kubernetes的深刻建議。然而,和所有其他的軟件平台一樣,Kubernetes並非是一個適用於所有人的靈丹妙藥。我更傾向於認為人們在有些時候過分誇大了Kubernetes的作用,以至於他們產生了一種錯覺:他們無法離開Kubernetes,而實際上,Kubernetes比他們真正的需求要複雜得多。

    為了分析人們真正的需求與Kubernetes的匹配程度,我分析了企業在採用Kubernetes編排之前必須考慮的5個事情。

    Kubernetes是什麼?

    如果您關注容器,您可能會知道Kubernetes是一個用於容器編排的開源工具,它可以自動執行諸如啟動容器、停止容器以及在同一個容器的不同實例之間的負載均衡等重要任務。

    簡而言之,Kubernetes的主要目的是最大限度地減少工程師必須手動執行的管理工作量,並通過簡化容器操作,幫助企業大規模運行複雜的容器化應用程序。

    決定是否採用Kubernetes的關鍵要素

    基於Kubernetes的設立初衷,如果您喜歡自動化,討厭手動執行重複性的任務,那麼Kubernetes無疑是您的極佳選擇。

    這是您決定是否採用Kubernetes的重要前提,但是,您不能僅根據這一“前提”就決定是否採用Kubernetes。在採用Kubernetes之前,您還需要考慮並權衡其他重要的因素。

    1、Kubernetes的基礎設施規模

    您的基礎設施規模是其中一個決定Kubernetes是否能夠很好地為您所用的關鍵要素。

    Kubernetes的設計初衷是協調分佈在真正龐大的環境中的容器,這往往意味着企業應當擁有數十台主機服務器。根據過往的實施經驗,如果基礎架構中的服務器少於50個,那麼您可能沒有足夠的資源來利用Kubernetes的全部優勢。

    這並不是指Kubernetes無法在較小規模的基礎設施上運行。實際上,如果您願意,您可以在單個主機上運行Kubernetes。然而,由於Kubernetes其中的一個研發目的是:通過在數量龐大的集群中分佈容器化應用程序提供高可用性,因此,如果您只有少量服務器,則無法享受到Kubernetes的某些價值。

    除此之外,考慮到設置和維護Kubernetes的複雜性,如果您的基礎設施規模較小,無法完全實現Kubernetes的高可用性承諾,那麼或許您不應投入過多時間和精力在Kubernetes上。

    對於較小的基礎架構,您可以使用較為簡單的容器編排工具,或者使用如AWS ECS等具有內置編排的基於雲的容器服務。

    2、Kubernetes操作系統環境

    Kubernetes主要是一種Linux技術。儘管Kubernetes可以用於管理託管Windows服務器上的容器化應用程序,這些應用程序作為Kubernetes服務器集群內的所謂工作節點運行。但託管Kubernetes核心服務的主要服務器或者說主節點必須是Linux。

    因此,如果您的商店以Windows為中心,那麼Kubernetes並非您的最佳選擇。但是您可以選擇Rancher輕鬆將Kubernetes的優勢引入Windows,並且極大程度降低使用的複雜性。

    3、安裝和設置Kubernetes

    在決定採用Kubernetes之前,您還需要評估您可以在此項目上投入的工作時間。

    普通的開放源代碼版本的Kubernetes缺少內置的應用程序,也並未提供一種可以適用於所有默認配置的安裝方式。在集群正常運行之前,您需要投入大量的時間從頭開始編寫及調整配置文件。因此,安裝和配置Kubernetes的過程或許是一個令人生畏的過程,您需要投入大量的時間和精力。

    部分Kubernetes發行版提供了交互式安裝程序腳本,可以幫助您自動執行大部分設置過程。如果您選擇Rancher等Kubernetes發行版,則有望在一两天內輕鬆完成配置及安裝。

    第三種選擇是使用諸如Google Kubernetes Engine等雲供應商解決方案,將Kubernetes作為託管服務在雲上運行。在這種情況下,您可以自行選擇安裝及設置。但值得注意的一點是,在確定如何配置Kubernetes環境時,您的選擇可能會受到限制。

    您必須意識到最為關鍵的一點:不要低估配置Kubernetes的難度。在您真的要全身心投入Kubernetes之前,請確保您所付出的努力是值得的。另一方面,如果您無法確定為企業在生產集群上安裝和部署Kubernetes的難度,您可以嘗試使用K3s等輕量級Kubernetes發行版來進行測試,預估後續需要付出多少努力來進行Kubernetes的配置和設置。

    4、Kubernetes和聲明式配置管理

    Kubernetes採用了所謂的聲明式配置管理方法,這就意味着,您需要自行編寫配置文件來設置Kubernetes應用程序應當如何運行,而Kubernetes將自動指出如何使應用程序符合規範。

    聲明式配置管理與命令式配置管理相反,在命令式配置管理中,您可以自行配置應用程序的每個組件,並讓其按照您所想要的方式運行。

    聲明式配置是Kubernetes在許多用戶實例中如此強大和可伸縮的其中一個原因。您可以設置一次配置,並且根據需要多次應用它。

    但是,如果您的配置需求不斷變化,或者在工作負載或環境中的不同部分之間變化,那麼您應當如何處理呢?在這種情況下,聲明式配置管理將成為一個障礙,您將發現自己需要不斷地調整先前認為是“一勞永逸”的配置文件。

    因此,在您選擇採用Kubernetes之前,您需要考慮應用程序的配置需求。只有當您所需要的配置相對通用且靜態時,Kubernetes才是一個不錯的選項。

    5、Kubernetes和多雲

    Rancher等部分Kubernetes發行版的主要功能之一,是單個Kubernetes部署可以編排多個集群,無論集群位於在不同的公有雲還是私有雲上。這一功能使Kubernetes成為協助控制多雲架構複雜性的優秀工具。

    在跨多雲部署容器化應用程序,並且Kubernetes的設置和配置工作很合理時,多雲上的Kubernetes是十分有意義的。

    在這一因素中,您需要留意的是,在考慮是否以及何時採用Kubernetes時,應考慮您當前的多雲戰略以及多雲擴展計劃。

    結 語

    Kubernetes是一個非常棒的工具,在正確設置的情況下,它可以產生巨大的價值。但是,它並沒有達到殺手級應用程序的狀態,因為它無法在所有用戶實例中交付價值。在您被巨大的宣傳攻勢攻陷,並確定您無法離開Kubernetes之前,請清醒地對自己的需求進行評估,明確Kubernetes是否能在真正意義上幫助您更加有效、更加可靠地運行應用程序。

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

  • Spring和Springboot相關知識點整理

    Spring和Springboot相關知識點整理

    簡介

    本文主要整理一些Spring & SpringBoot應用時和相關原理的知識點,對於源碼不做沒有深入的講解。

    1. 思維導圖

    右鍵新窗口打開可以放大。

    說明

    • 使用@Configuration在java代碼中聲明一個bean——而不是使用xml——實際上很早就有了(至少在《Spring實戰(第3版)》出版時,也就是Spring3.0時),我一直以為是SpringBoot的新特性。

    2. Spring

    2.1 AOP術語

    • 通知Advice —— 切面要做什麼,何時執行。何時,包括方法調用前、方法調用后、方法成功調用后、方法調用拋異常后、環繞(Around)。環繞允許提供一些需要跨越方法調用前後的功能,如計算調用耗時。
    • 連接點Joinpoint —— 應用執行時能插入切面的點。實際上是一個邏輯概念,不體現在配置中。
    • 切點Pointcut —— 執行通知的具體的連接點。
    • 切面Aspect —— 通知+切點
    • 引入Introduction —— 允許為類添加新的方法或屬性。(個人理解即應用使用切面中的方法和屬性,就好像這些方法和屬性是原生的一樣。可以參考<aop:declare-parents>元素)
    • 織入Weaving —— 將切面應用到目標對象創建新的代理對象的過程,Spring使用的是運行時。編譯期和類加載時是其他的方式。

    2.2 Bean的生命周期

    雖然被稱為生命周期,實際上指的是bean在初始化、回收期間調用了哪些方法。如果只看《Spring實戰》,可以看到類似下面的圖(圖源:參考文獻[3])

    具體哪一步做了什麼?其實這些方法都是可選的,自定義的bean可以實現,也可以不實現,實現里寫什麼似乎都沒問題,參考文獻[3]中的測試代碼展示了這些方法在bean生命周期中的執行順序。
    但是對於Spring框架的bean,就有必要的用途了。這裏沒有深入研究,有興趣可以自行讀源碼。

    2.3 Cglib和JdkProxy

    2.3.1 與Spring的關係

    這是Spring AOP的兩種實現方式。根據官方文檔:

    1. 默認使用JdkProxy
    2. 對於被代理對象沒有實現任何接口,使用Cglib
    3. 可以強制指定使用Cglib。
      這樣就可以解釋為什麼有的bean實現了接口,有的沒有,但是在同一個工程中可以並存了。

    2.3.2 示例代碼

    本節代碼改寫自參考文獻[5]。

    //用戶管理接口
    public interface UserManager {
        //新增用戶抽象方法
        void addUser(String userName,String password);
        //刪除用戶抽象方法
        void delUser(String userName);
    }
    
    //用戶管理實現類,實現用戶管理接口
    public class UserManagerImpl implements UserManager{
        @Override
        public void addUser(String userName) {
            System.out.println("調用了新增的方法!");
            System.out.println("傳入參數為 userName: "+userName+" password: "+password);
        }
        @Override
        public void delUser(String userName) {
            System.out.println("調用了刪除的方法!");
            System.out.println("傳入參數為 userName: "+userName);
        }   
    }
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    import com.lf.shejimoshi.proxy.entity.UserManager;
    import com.lf.shejimoshi.proxy.entity.UserManagerImpl;
    //JDK動態代理實現InvocationHandler接口
    public class JdkProxy implements InvocationHandler {
        private Object target ;//需要代理的目標對象
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("JDK動態代理,監聽開始!");
            Object result = method.invoke(target, args);
            System.out.println("JDK動態代理,監聽結束!");
            return result;
        }
        //定義獲取代理對象方法
        // 因為只是在main()里測試,聲明為private了 
        private Object getJDKProxy(Object targetObject){
            this.target = targetObject;
            return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
        }
        
        public static void main(String[] args) {
            JdkProxy jdkProxy = new JdkProxy();
            UserManager user = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());//獲取代理對象
            user.addUser("admin");
        }   
    }
    
    import java.lang.reflect.Method;
    
    import com.lf.shejimoshi.proxy.entity.UserManager;
    import com.lf.shejimoshi.proxy.entity.UserManagerImpl;
    
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    //Cglib動態代理,實現MethodInterceptor接口
    public class CglibProxy implements MethodInterceptor {
        private Object target;//需要代理的目標對象
        
        //重寫攔截方法
        @Override
        public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
            System.out.println("Cglib動態代理,監聽開始!");
            Object invoke = method.invoke(target, arr);//方法執行,參數:target 目標對象 arr參數數組
            System.out.println("Cglib動態代理,監聽結束!");
            return invoke;
        }
        //定義獲取代理對象方法
        public Object getCglibProxy(Object objectTarget){
            //為目標對象target賦值
            this.target = objectTarget;
            Enhancer enhancer = new Enhancer();
            //設置父類,因為Cglib是針對指定的類生成一個子類,所以需要指定父類
            enhancer.setSuperclass(objectTarget.getClass());
            enhancer.setCallback(this);// 設置回調 
            Object result = enhancer.create();//創建並返回代理對象
            return result;
        }
        
        public static void main(String[] args) {
            CglibProxy cglib = new CglibProxy();
            UserManager user =  (UserManager) cglib.getCglibProxy(new UserManagerImpl());
            user.delUser("admin");
        }
        
    }
    

    2.3.3 比較

    比較一下兩者的區別,這也是常見的面試問題。

    JdkProxy Cglib
    依賴 被代理對象實現了接口(所有接口的方法數總和必須>0[4]) 引入asm、cglib jar ;不能是final類和方法
    原理 反射,實現被代理對象接口的匿名內部類,通過InvocationHandler.invoke()包圍被代理對象的方法 引入asm、cglib jar,代理類實現MethodInterceptor,通過底層重寫字節碼來實現
    效率 創建快,運行慢(見下方說明2) 創建慢,運行快

    說明

    1. Cglib是如何修改字節碼,從代碼上是看不出來的。使用的是ASM技術,修改class文件,可以自行查閱。
    2. JDK1.8及以後,JdkProxy的運行速度已經比Cglib快了(之前則是慢於Cglib),測試代碼可見參考文獻[6]。

    2.3.4 關於org.apoalliance.intercept.MethodInterceptor

    我讀了一下之前所用的日誌攔截器源碼,發現其實現的是這節標題的接口:

    class CommonInterceptor implements MethodInterceptor {
          @Override
          public Object invoke(MethodInvocation invocation) throws Throwable {
                try {
                       // 攔截器內部邏輯
                      result = invoication.proceed();
                catch(Throwable e) {
                      // 異常處理
                }
                return result;
          }
    }
    

    聲明代理鏈

    @Configuration
    public class InterceptorConfig {
    
          @Bean
          public CommonInterceptor serviceInterceptor() {
                CommonInterceptor bean = new CommonInterceptor();
                return bean;
          }
    
          // 代理名稱後綴為servie的實現類
          @Bean
          public BeanNameAutoProxyCreator servieBeanNameAutoProxyCreator() {
                BeanNameAutoProxyCreator creator = new BeanNameAutoProxyCreator();
                creator.setName("*ServieImpl");
                creator.setInterceptorNames("serviceInterceptor");
                creator.setProxyTargetClass(true);
                return creator;
          }
    }
    

    查了一些資料,apoalliance包下只是aop的接口規範,不是具體的實現,不要把這裏的MethodInterceptor和cglib的MethodInterceptor搞混。

    2.4 構造方法注入和設值注入的區別

    注:設值注入指的是通過setter注入。
    之前看參考文獻[7],感覺很難懂,試着改換了一種說法:

    1. 如果只設置基本類型(int、long等)的值,建議設置默認值而不是通過任何一種注入完成
    2. 構造注入不支持大部分的依賴注入。構造注入僅在創建時執行,設值注入的值在後續也可以變化。
    3. 設值注入可以支持尚未完整的被依賴的對象,構造注入則不行。可以通過構造注入決定依賴關係,因此如果依賴關係不會發生變更也可以選擇依賴注入。

    2.5 ApplicationContext事件

    可以通過實現ApplicationEvent類和ApplicationListener接口,進行ApplicationContext的事件處理。這是標準的發送者-監聽者的模型,可以用來處理業務邏輯,將代碼解耦。
    但是,發送和接收實際上是同步的,如果有事務,會在同一個事務內,並不能作為異步處理機制[8]
    示例代碼見參考文獻[9]。

    3. SpringBoot

    注:工作中我對SpringBoot是偏應用的,研究並不是很深入。

    3.1 如何快速搭建一個SpringBoot項目

    見參考文獻[10]。實際的過程是藉助Spring Initializer這個網絡應用程序來生成SpringBoot項目。

    3.2 SpringBoot的關鍵註解

    所謂核心註解,這裏指的是相對Spring本身新增的一些註解,來看看它們有什麼作用。
    恰好這裏提到的註解,都可以打在SpringBoot的啟動類(不限於啟動類),用下面的代碼片段來進行說明。

    3.2.1 代碼示例

    package com.example.demo;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Import;
    import org.springframework.context.annotation.PropertySource;
    
    @PropertySource(value = "classpath:application.properties")
    @MapperScan("com.example.demo.dal")
    @SpringBootApplication(scanBasePackages = {"com.example.demo"})
    @Import({DemoConfig1.class, DemoConfig2.class,})
    public class DemoApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(DemoApplication.class, args);
    	}
    }
    

    3.2.2 @PropertySource

    從指定的文件讀取變量。示例代碼會從resource目錄下讀取application.properties變量的值。文件的格式如下,既可以用常量,也可以用變量替換,來對不同環境的值作區分。

    name=value
    env.name=$env.value
    

    如何使用這個值?在要使用的地方獲取即可。

    @PropertySource(value = "classpath:application.properties")
    class TestClass {
    	@Resource
    	private Environment environment;
    
          @Test
          public void test() {
                String value = environment.getProperty("name"));
          }
    }
    

    3.2.2.1 與@Value配合使用

    使用@Value可以把配置文件的值直接注入到成員變量中。

    @PropertySource("classpath:application.properties")
    public class PropertyConfig {
    
        @Value("${name}")
        private String value;
    
         ...
    }
    

    3.2.2.2 通過@Import引用

    3.2.1的示例代碼中,如果類上沒有@PropertySource,但DemoConfig1或DemoConfig2中有@PropertySource,通過@Import可以將它們加載的變量也讀出來。
    @Import的作用在下文會繼續介紹。

    3.2.2.3 .properties和.yml配置文件

    @PropertySource只能導入.properties配置文件里的內容,對於.yml是不支持的。看了一些文章,得出結論是yml文件是不需要註解就能導入,但是需要路徑。
    Springboot有兩種核心配置文件,application和bootstrap,都可以用properties或yml格式。區別在於bootstrap比application優先加載,並且不可覆蓋。

    3.2.3 @MapperScan

    這實際上是一個mybatis註解,作用是為指定路徑下的DAO接口,通過sqlmapping.xml文件,生成實現類。

    3.2.4 @SpringBootApplication

    @SpringBootApplication是由多個註解組合成的。源碼如下:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public @interface SpringBootApplication {
          // 略
    }
    

    簡單介紹一下這些註解。

    3.2.4.1 元註解

    最上面四行都是元註解。回憶一下它們的作用[12]

    • @Target 註解可以用在哪。TYPE表示類型,如類、接口、枚舉
    • @Retention 註解的保留時間期限。只有RUNTIME類型可以在運行時通過反射獲取其值
    • @Documented 該註解在生成javadoc文檔時是否保留
    • @Inherited 被註解的元素,是否具有繼承性,如子類可以繼承父類的註解而不必顯式的寫下來。

    3.2.4.2 @SpringBootConfiguration

    標註這是一個SpringBoot的配置類,和@Configuration功能是相通的,從源碼也可以看出它直接使用了@Configuration。

    3.2.4.3 @EnableAutoConfiguration

    這個註解是實現自動化配置的核心註解,定義如下

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
          // 略
    }
    

    藉助@Import引入的AutoConfigurationImportSelector,SpringBoot應用將所有符合條件的@Configuration配置都加載到當前SpringBoot創建並使用的IoC容器。具體的細節這裏不展開了。

    3.2.4.4 @ComponentScan

    掃描@Service、@Repository、@Component、@Controller等標註的類,創建bean。
    可以設置掃描範圍,決定類哪些生成bean,哪些類不生成。

    3.2.5 @Import

    將外部資源(bean、@Configuration配置類)導入到當前IOC容器中。
    使用@Import便可以實例化引用的jar中定義的bean了。

    3.3 Starter

    指的是在依賴中引用的各種starter包。starter可以看作是“依賴jar+配置”打包的結果,目的是降低開發者引入組件的成本,不用自己梳理依賴、編寫配置文件。
    starter遵循“約定大於配置”的原則,使用的組件的配置大部分都有默認值,不聲明也可以直接用。

    創建一個Spring boot的簡易步驟(完整的可以看參考文獻[14]):

    1. 創建maven項目
    2. 創建proterties類來保存配置信息
    3. 編寫業務功能類(包含會實例化為bean的類)
    4. 編寫Configuration類,定義bean
    5. 在resources 文件夾下新建目錄 META-INF,在目錄中新建 spring.factories 文件,並且在 spring.factories 中配置AutoConfiguration
    6. 打包

    3.4 war包

    3.4.1 和jar包的區別

    • jar 把類和相關的資源封裝
    • war 代表了一個可部署的Web應用程序

    3.4.2 SpringBoot項目打war包部署

    通用步驟如下,其中1可能需要移除內嵌tomcat,2有其他形式,因為我工作時都是拿線程腳本打包的,沒有實際操作過,下面步驟僅供參考。

    1. pom.xml修改為按war打包
    2. 修改main入口方法,繼承一個SpringBootServletInitializer類,並且覆蓋configure方法
    3. maven打包
    4. 複製到tomcat路徑下(tomcat需要預先配置),使用startup啟動

    3.5 Springboot面試題補充

    本節內容結合了參考文獻[16]進行補充,上面提到的知識不再重複。

    3.5.1 使用Springboot的兩種方式

    1. 繼承spring-boot-starter-parent項目
    <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>1.5.6.RELEASE</version>
    </parent>
    
    1. 導入spring-boot-dependencies項目依賴
    <dependencyManagement>
          <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>1.5.6.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
          </dependencies>
    </dependencyManagement>
    

    3.5.2 SpringBoot 需要獨立的容器運行嗎?

    可以不需要,內置了 Tomcat/Jetty等容器。
    如何使用Jetty?排除掉Tomcat依賴並引入Jetty,並更改一些application配置。兩種容器的比較和替換方式見參考文獻[17]。

    3.5.3 運行 SpringBoot 有哪幾種方式?

    1. 打包用命令或者放到容器中運行
    2. 用 Maven/ Gradle 插件運行
    3. 直接執行 main 方法運行

    3.5.4 SpringBoot啟動時運行特定代碼的方式

    Bean實現接口 ApplicationRunner或者CommandLineRunner即可。

    3.5.5 SpringBoot實現熱部署有哪幾種方式?

    主要有兩種

    • Spring Loaded —— 引用依賴(maven plugin)。對於註解和xml變動無法感知需要重啟
    • Spring-boot-devtools —— 引用依賴、更改配置(可選)、啟動idea的自動編譯。注意生產環境插件可能導致gc

    3.5.6 Spring Boot 可以兼容老 Spring 項目嗎,如何做?

    可以兼容,使用 @ImportResource 註解導入老 Spring 項目配置文件。

    參考文獻

    1.AOP -連接點和切點的區別
    2.Spring AOP術語:連接點和切點的區別。
    3.深究Spring中Bean的生命周期
    4.Spring AOP代理用的到底是CGLIB還是JDK動態代理
    5. Spring的兩種動態代理:Jdk和Cglib 的區別和實現
    6. Spring AOP中的JDK和CGLib動態代理哪個效率更高?
    7. 經典面試題-構造方法注入和設值注入有什麼區別?
    8. Spring的ApplicationEvent
    9. spring-第三篇之ApplicationContext的事件機制
    10. 使用IDEA搭建一個簡單的SpringBoot項目——詳細過程
    11. 淺析PropertySource 基本使用
    12. JAVA核心知識點–元註解詳解
    13. 簡單講講@SpringBootApplication
    14. Spring Boot Starters
    15. SpringBoot 打包成war
    16. 吐血整理 20 道 Spring Boot 面試題,我經常拿來面試別人!
    17. SpringBoot2使用Jetty容器(替換默認Tomcat)

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

  • 用Map+函數式接口來實現策略模式

    用Map+函數式接口來實現策略模式

    用Map+函數式接口來實現策略模式

    目前在魔都,貝殼找房是我的僱主,平時關注一些 java 領域相關的技術,希望你們能在這篇文章中找到些有用的東西。個人水平有限,如果文章有錯誤還請指出,在留言區一起交流。

    本文已投稿至公眾號 Hollis 原文鏈接:https://mp.weixin.qq.com/s/hkypvNBkRjPM6HM51_jW9g

    我想大家肯定都或多或少的看過各種“策略模式”的講解、佈道等等,這篇文章就是來好好“澄清”一下策略模式,並嘗試回答以下的問題:

    1. 策略模式是如何優化業務邏輯代碼結構的?
    2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!
    3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?

    策略模式是如何優化業務邏輯代碼結構的?

    要回答這個問題,我們還得先扒一扒策略模式的定義,從定義着手來理解它:

    策略模式的教科書定義

    它的定義很精簡:一個類的行為或其算法可以在運行時更改。我們把它降維到代碼層面,用人話翻譯一下就是,運行時我給你這個類的方法傳不同的“key”,你這個方法會執行不同的業務邏輯。細品一下,這不就是 if else 乾的事嗎?

    策略模式優化了什麼?

    其實策略模式的核心思想和 if else如出一轍,根據不同的key動態的找到不同的業務邏輯,那它就只是如此嗎?

    實際上,我們口中的策略模式其實就是在代碼結構上調整,用接口+實現類+分派邏輯來使代碼結構可維護性好點。

    一般教科書上講解到接口與實現類就結束了,其他博客上會帶上提及分派邏輯。這裏就不啰嗦了。

    小結一下,即使用了策略模式,你該寫的業務邏輯照常寫,到邏輯分派的時候,還是變相的if else。而它的優化點是抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。

    殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!

    我想小夥伴們經常有這樣的不滿,我的業務邏輯就3 4 行,你給我整一大堆類定義?有必要這麼麻煩嗎?我看具體的業務邏輯還需要去不同的類中,簡單點行不行。

    其實我們所不滿的就是策略模式帶來的缺點:1、策略類會增多 2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯視整個業務邏輯

    針對傳統策略模式的缺點,在這分享一個實現思路,這個思路已經幫我們團隊解決了多個複雜if else的業務場景,理解上比較容易,代碼上需要用到Java8的特性——利用Map與函數式接口來實現。

    直接show代碼結構:為了簡單演示一個思路,代碼用String 類型來模擬一個業務BO

    其中:

    1. getCheckResult() 為傳統的做法
    2. getCheckResultSuper()則事先在Map中定義好了“判斷條件”與“業務邏輯”的映射關係,具體講解請看代碼註釋
    /**
     * 某個業務服務類
     */
    @Service
    public class BizService {
    
        /**
         * 傳統的 if else 解決方法
         * 當每個業務邏輯有 3 4 行時,用傳統的策略模式不值得,直接的if else又顯得不易讀
         */
        public String getCheckResult(String order) {
            if ("校驗1".equals(order)) {
                return "執行業務邏輯1";
            } else if ("校驗2".equals(order)) {
                return "執行業務邏輯2";
            }else if ("校驗3".equals(order)) {
                return "執行業務邏輯3";
            }else if ("校驗4".equals(order)) {
                return "執行業務邏輯4";
            }else if ("校驗5".equals(order)) {
                return "執行業務邏輯5";
            }else if ("校驗6".equals(order)) {
                return "執行業務邏輯6";
            }else if ("校驗7".equals(order)) {
                return "執行業務邏輯7";
            }else if ("校驗8".equals(order)) {
                return "執行業務邏輯8";
            }else if ("校驗9".equals(order)) {
                return "執行業務邏輯9";
            }
            return "不在處理的邏輯中返回業務錯誤";
        }
    
        /**
         * 業務邏輯分派Map
         * Function為函數式接口,下面代碼中 Function<String, String> 的含義是接收一個Stirng類型的變量,返回一個String類型的結果
         */
        private Map<String, Function<String, String>> checkResultDispatcher = new HashMap<>();
    
        /**
         * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
         */
        @PostConstruct
        public void checkResultDispatcherInit() {
            checkResultDispatcher.put("校驗1", order -> String.format("對%s執行業務邏輯1", order));
            checkResultDispatcher.put("校驗2", order -> String.format("對%s執行業務邏輯2", order));
            checkResultDispatcher.put("校驗3", order -> String.format("對%s執行業務邏輯3", order));
            checkResultDispatcher.put("校驗4", order -> String.format("對%s執行業務邏輯4", order));
            checkResultDispatcher.put("校驗5", order -> String.format("對%s執行業務邏輯5", order));
            checkResultDispatcher.put("校驗6", order -> String.format("對%s執行業務邏輯6", order));
            checkResultDispatcher.put("校驗7", order -> String.format("對%s執行業務邏輯7", order));
            checkResultDispatcher.put("校驗8", order -> String.format("對%s執行業務邏輯8", order));
            checkResultDispatcher.put("校驗9", order -> String.format("對%s執行業務邏輯9", order));
        }
    
        public String getCheckResultSuper(String order) {
            //從邏輯分派Dispatcher中獲得業務邏輯代碼,result變量是一段lambda表達式
            Function<String, String> result = checkResultDispatcher.get(order);
            if (result != null) {
                //執行這段表達式獲得String類型的結果
                return result.apply(order);
            }
            return "不在處理的邏輯中返回業務錯誤";
        }
    }
    

    通過http調用一下看看效果:

    /**
     * 模擬一次http調用
     */
    @RestController
    public class BizController {
    
        @Autowired
        private BizService bizService;
    
        @PostMapping("/v1/biz/testSuper")
        public String test2(String order) {
            return bizService.getCheckResultSuper(order);
        }
    }
    

    這是個簡單的demo演示,之後會舉一些複雜的場景案例。

    魯迅曾說過,“每解決一個問題,就會引出更多的問題”。我們一起來看看這樣的實現有什麼好處,會帶來什麼問題。

    好處很直觀:

    1. 在一段代碼里直觀的看到”判斷條件”與業務邏輯的映射關係
    2. 不需要單獨定義接口與實現類,直接使用現有的函數式接口(什麼?不知道函數式接口?快去了解),而實現類直接就是業務代碼本身。

    不好的點:

    1. 需要團隊成員對lambda表達式有所了解(什麼?Java14都出來了還有沒用上Java8新特性的小夥伴?)

    接下來我舉幾個在業務中經常遇到的if else場景,並用Map+函數式接口的方式來解決它

    在真實業務場景問題的解決思路

    有的小夥伴會說,我的判斷條件有多個啊,而且很複雜,你之前舉個例子只有單個判斷邏輯,而我有多個判斷邏輯該怎麼辦呢?

    很好解決:寫一個判斷邏輯的方法,Map的key由方法計算出

    /**
     * 某個業務服務類
     */
    @Service
    public class BizService {
    
        private Map<String, Function<String, String>> checkResultDispatcherMuti = new HashMap<>();
    
        /**
         * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
         */
        @PostConstruct
        public void checkResultDispatcherMuitInit() {
            checkResultDispatcherMuti.put("key_訂單1", order -> String.format("對%s執行業務邏輯1", order));
            checkResultDispatcherMuti.put("key_訂單1_訂單2", order -> String.format("對%s執行業務邏輯2", order));
            checkResultDispatcherMuti.put("key_訂單1_訂單2_訂單3", order -> String.format("對%s執行業務邏輯3", order));
        }
    
        public String getCheckResultMuti(String order, int level) {
            //寫一段生成key的邏輯:
            String ley = getDispatcherKey(order, level);
    
            Function<String, String> result = checkResultDispatcherMuti.get(ley);
            if (result != null) {
                //執行這段表達式獲得String類型的結果
                return result.apply(order);
            }
            return "不在處理的邏輯中返回業務錯誤";
        }
    
        /**
         * 判斷條件方法
         */
        private String getDispatcherKey(String order, int level) {
            StringBuilder key = new StringBuilder("key");
            for (int i = 1; i <= level; i++) {
                key.append("_" + order + i);
            }
            return key.toString();
        }
    }
    

    用http模擬一下:

    /**
     * 模擬一次http調用
     */
    @RestController
    public class BizController {
    
        @Autowired
        private BizService bizService;
    
        @PostMapping("/v1/biz/testMuti")
        public String test1(String order, Integer level) {
            return bizService.getCheckResultMuti(order, level);
        }
    }
    

    只要設計好你的key的生成規則就好。

    還有小夥伴會問:我的業務邏輯有很多很多行,在checkResultDispatcherMuitInit()方法的Map中直接寫不會很長嗎?

    直接寫當然長了,我們可以抽象出一個service服務專門放業務邏輯,然後在定義中調用它就好了:

    提供一個業務邏輯單元:

    /**
     * 提供業務邏輯單元
     */
    @Service
    public class BizUnitService {
    
        public String bizOne(String order) {
            return order + "各種花式操作1";
        }
        public String bizTwo(String order) {
            return order + "各種花式操作2";
        }
        public String bizThree(String order) {
            return order + "各種花式操作3";
        }
    }
    
    /**
     * 某個業務服務類
     */
    @Service
    public class BizService {
        @Autowired
        private BizUnitService bizUnitService;
    
        private Map<String, Function<String, String>> checkResultDispatcherComX = new HashMap<>();
    
        /**
         * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
         */
        @PostConstruct
        public void checkResultDispatcherComXInit() {
            checkResultDispatcherComX.put("key_訂單1", order -> bizUnitService.bizOne(order));
            checkResultDispatcherComX.put("key_訂單1_訂單2", order -> bizUnitService.bizTwo(order));
            checkResultDispatcherComX.put("key_訂單1_訂單2_訂單3", order -> bizUnitService.bizThree(order));
        }
    
        public String getCheckResultComX(String order, int level) {
            //寫一段生成key的邏輯:
            String ley = getDispatcherComXKey(order, level);
    
            Function<String, String> result = checkResultDispatcherComX.get(ley);
            if (result != null) {
                //執行這段表達式獲得String類型的結果
                return result.apply(order);
            }
            return "不在處理的邏輯中返回業務錯誤";
        }
    
        /**
         * 判斷條件方法
         */
        private String getDispatcherComXKey(String order, int level) {
            StringBuilder key = new StringBuilder("key");
            for (int i = 1; i <= level; i++) {
                key.append("_" + order + i);
            }
            return key.toString();
        }
    }
    

    調用結果:

    總結

    最後,我們一起嘗試回答以下幾個問題:

    1. 策略模式是如何優化業務邏輯代碼結構的?
      —— 抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。
    2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!——我們所不滿的其實就是傳統接口實現的缺點:1、策略類會很多。 2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯覽整個業務邏輯
    3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?——針對傳統策略模式的缺點,分享了利用Map與函數式接口來實現的思路。

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

    【其他文章推薦】

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

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

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

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

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

  • 在樹莓派上讀取土壤濕度傳感器讀數-Python代碼實現及常見問題(全面簡單易懂)

    在樹莓派上讀取土壤濕度傳感器讀數-Python代碼實現及常見問題(全面簡單易懂)

    本篇文章簡單介紹了如何在樹莓派上配置土壤濕度傳感器以讀取土壤濕度及代碼實現。

    主要包含有以下4個模塊:

    一、土壤濕度傳感器常見類型及介紹

    二、實驗所需設備

    三、設備連線方式與Python代碼實現

    四、常見問題及注意事項

    需要哪個模塊的內容直接跳轉去看即可~

     

    一、土壤濕度傳感器常見類型及介紹

     土壤濕度傳感器,又名土壤水分傳感器、土壤墒情傳感器、土壤含水量傳感器等。顧名思義,主要用來測量土壤相對含水量,做土壤墒情監測。在智能農業,農業灌溉和林業防護等領域極廣。該傳感器價格低廉,如果想在家製作一個簡易的智能自動化作物灌溉系統,有了它,再加上溫濕度傳感器、樹莓派/Arduino就可以輕鬆完成。

    常見的土壤傳感器分為兩類,電阻型和電容型土壤濕度傳感器。但原理大同小異,都是測量土壤中水分的體積含量,並以電壓表示水分值。

    (一)電阻型土壤濕度傳感器

    常見的有傳感器型號有YL-69(下圖左)和FC-28(下圖右)。這是一種低技術含量的傳感器這類傳感器由兩部分組成,帶探針的傳感器,A to D(模擬信號轉数字信號)电子板(校準靈敏度主板),兩者用母對母杜邦線連接。                

                它根據土壤的介電常數(土壤的導電能力)來估計土壤體積水含量,工作時,使用兩個探針讓電流通過土壤,然後讀取電阻來獲得濕度水平。水分越多,土壤導電越容易(電阻越小),而土壤乾燥,導電越差(電阻越大)。土壤中的濕度是一個連續變化的一系列值,為模擬信號,使用A to D接線板之後可以將從環境中得來的模擬信號轉成数字信號,在該板上有兩個指示燈,PWR-LED和DO-LED,前者檢測是否插好電源,如果電源的正負極連接正確,則會亮起,如下圖(左)所示。在這裏我使用的是YL-69型號,燈為綠色,也有一些廠商生產的傳感器指示燈為紅色。該傳感器在輸出数字信號時,可以使用改錐調節板上的電位計(藍色中間有十字架的部位)來提前設定閾值大小,一旦土壤濕度達到或大於閾值,則DO-LED亮起,如下圖(中)所示。

     电子板從左到右的標記為AO、DO、GND、VCC,如下圖(右)所示。AO和DO為信號引腳,如果需要模擬信號,則連接AO,輸出的模擬值是介於所提供的電壓值到0V間的變化的電壓值,如果輸出0V,則代表土壤導電性不好,即水分含量低,可以用這個電壓值來估計土壤濕度。如果需要数字信號輸出則連接DO,簡單的輸出0和1,可直接通過信號燈判斷土壤中水分是否低於閾值,高於則“開”,低則“關”。GND表示接地,VCC連接電源,但在這個項目中,我們將單獨利用模擬輸出。

                   

     該傳感器的優點是價格低廉,且有指示燈,觀察方便,但由於土壤環境是酸性的(acidic),隨着時間的推移,土壤里的化學物質會使得探針氧化(oxidize)導致測量不準確,所以需要時不時的進行更換以保證測量的準確性。

    (二)電容型傳感器

    相比較前一類型的傳感器,這類傳感器就顯得“光禿禿”了,只有一個組件,沒有指示燈,且只能輸出模擬信號。它區別於電阻傳感器,利用電容感應原理來檢測土壤濕度,避免了電阻式傳感器極易被腐蝕的問題,生命周期較長,缺點是不能用指示燈判斷傳感器是否正常工作,它同時只提供模擬信號。如圖設計DF Robot的一款傳感器,內置穩壓芯片,支持3.3V-5.5V寬電壓工作。DFRobot-Gravity接口具有兼容性,可直接和Gravity IO擴展板相連接。輸出電壓為0-3VDC。

    在自動化澆灌系統中,濕度傳感器用於測量土壤中水分,可以提前預設一個閾值,一旦土壤中的水分低於此閾值,則啟動連接着蓄水池(家用拿礦泉水瓶裝滿水替代即可)的水泵噴水,等到水分值超過預設值,則水泵暫停工作。

     

    二、實驗所需設備

    樹莓派 3 b+/樹莓 4 b/樹莓派Zero W

    MCP3008

    麵包板

    跳線(公對母,母對母,最好多準備一些)

    土壤傳感器(電容式、電阻式均可)

    一杯水(可以將傳感器放入水中來觀察濕度讀數的變化,若沒有條件也可直接用手握住傳感器的探針)

    MCP3008

    由於樹莓派沒有模擬信號引腳,所以沒有辦法直接輸出模擬信號數值,此時我們需要使用MCP3008集成電路。

    MCP3008 IC(Integrated Circuit)是一個8通道,10位的具有SPI串行接口的A / D轉換器(模擬-数字轉換器),共有16個引腳,可以用來解決模擬引腳問題(MCP3004也是ADC,不過為4路,體型更小)MCP3008使用SPI總線協議從樹莓派接收模擬輸入值。它有8個模擬輸入(ch0-ch7),另外一列的8個引腳中有4個電源和地引腳和4個連接樹莓派的引腳,它產生範圍為0-1023的輸出值(注意:0表示0V, 1023表示3.3V)。

    三、設備連線方式與代碼實現

    在此實驗中,主要介紹和使用的FC-28型號土壤濕度傳感器,但其它型號的傳感器使用,連線均與此相同。

    (一)設備連線方式

    1.電路圖及說明

    MCP3008共有16個引腳,其中8個用於記錄模擬輸入值,位於CH0-CH7(引腳1-8),4個通信引腳通過SPI協議方法與樹莓派通信,還有2個電源引腳,2個接地引腳。

    (二)代碼實現

    1. 啟用樹莓派的SPI接口

    樹莓派的SPI接口與SSH、VNC服務相同,是默認關閉的,需要我們在配置中打開此服務才可以使用。

    按照以下步驟啟動終端並輸入以下命令:

    (1)打開樹莓派配置選項

    sudo raspi-config
    

    (2)導航到Interface選項,啟用SPI接口。

    (3)重啟樹莓派

    reboot
    

    2. 安裝spidev庫

    光啟用SPI接口,但是樹莓派還是無法讀取傳感器傳過來的值,spidev庫將幫助通過SPI接口讀取傳感器值。

     使用以下命令安裝spidev庫:

    sudo apt-get install git python-dev
    git clone git://github.com/doceme/py-spidev
    cd py-spidev/
    sudo python setup.py install
    

    3. 安裝numpy庫

    我們從傳感器獲得的值還是電壓值而非土壤濕度值,土壤濕度需要使用百分比的形式體現,為了將輸出值轉換為百分比,還需要使用numpy庫。我們從MCP3008 IC接收到的輸出值是在前面提到的0-0123範圍內的數值,仍需要將把這些值映射到0-100,以得到一個百分比。

    使用以下命令安裝numpy模塊:

    sudo apt-get install python-numpy
    

    4. Python代碼

    # Importing modules
    import spidev # To communicate with SPI devices
    from numpy import interp  # To scale values
    from time import sleep  # To add delay
    
    
    # Start SPI connection
    spi = spidev.SpiDev() # Created an object
    spi.open(0,0) 
    
    
    # Read MCP3008 data
    def analogInput(channel):
      spi.max_speed_hz = 1350000
      adc = spi.xfer2([1,(8+channel)<<4,0])
      data = ((adc[1]&3) << 8) + adc[2]
      return data
    
    
    while True:
      output = analogInput(0) # Reading from CH0
      output = interp(output, [0, 1023], [100, 0])
      output = int(output)
      print("Moisture:", output)
      sleep(0.1)

    當從土壤濕度傳感器讀取模擬輸出值時,它以百分比測量濕度,使用來自numpy庫的特定interp模塊進行映射得到從0-100的值。

    四、常見問題及注意事項

    (一)常見問題

    1. 持續輸出0或100,無論探針是否放入水中均沒有變化

    2. 沒有操作探針,但讀數呈有規律地變化

    針對以上出現有以下幾種解決方案

    1.檢查樹莓派的SPI服務有無正確打開

    2.先檢查線有沒有接穩,查看是不是線的連接順序(傳感器的正負極有沒有接反,與樹莓派的連線有沒有串行)有誤

    3. 線是否有生鏽或損壞(之前第一次做實驗時,各種調試都出現不了正確結果,後來才發現是有幾根線生鏽了所以不通)

    (二)注意事項 

    1. 盡量使用長線,便於看清連線位置

    2. 盡量不要使用拼接線(一根公對公,一根母對母拼接成公對母的),這樣需要照顧的線更多,也更容易出紕漏

    3. 一定一定要有耐心,出現問題后按照順序逐一排查。因為涉及到的連線較多,對硬件小白來說,很容易眼花繚亂想放棄,但是太簡單的東西誰都能做,能攻破學習或者生活中一個個難關的人才能有所成長啊~

    如果你在配置土壤濕度傳感器時或使用樹莓派時遇見了什麼問題,歡迎在評論區寫下,看到了會及時答覆。期待與大家一起學習。

    文字及圖片部分來源:https://maker.pro/raspberry-pi/tutorial/interfacing-soil-moisture-sensor-with-raspberry-pi

    轉發請標明來源。祝大家學派happy!

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

    【其他文章推薦】

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

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

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

    ※幫你省時又省力,新北清潔一流服務好口碑

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