分類: 3C資訊

  • 新來的”大神”用策略模式把if else給”優化”了,技術總監說:能不能想好了再改?

    新來的”大神”用策略模式把if else給”優化”了,技術總監說:能不能想好了再改?

    本文來自作者投稿,原作者:上帝愛吃蘋果

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

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

    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與函數式接口來實現的思路。

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 網絡虛擬化之linux虛擬網絡基礎

    網絡虛擬化之linux虛擬網絡基礎

    linux虛擬網絡基礎

    1 Device

    在linux裏面devic(設備)與傳統網絡概念里的物理設備(如交換機、路由器)不同,Linux所說的設備,其背後指的是一個類似於數據結構、內核模塊或設備驅動這樣的含義。就是說device可能只是軟件系統里的一個驅動,一個函數接口。

    2 Tap

    Tap位於二層數據鏈路層,tun位於三層網絡層,兩者在linux里的函數結構幾乎一致,除了一個flag值區分tap/tun。在linux中二層特指以太網(Ethernet)(傳統網絡里二層分Ethernet,P2P,HDLC,FR,ATM),因此有時tap也叫“虛擬以太設備”。有意思的是linux創建tap需要用到tun模塊。Linux創建tap/tun都使用tun模塊。

     

    3 Namespace

    Namespace類似傳統網絡里的VRF,與VRF不同的是:VRF做的是網絡層三層隔離。而namespace隔離的更徹底,它做的是整個協議棧的隔離,隔離的資源包括:UTS(UnixTimesharing  System的簡稱,包含內存名稱、版本、 底層體繫結構等信息)、IPS(所有與進程間通信(IPC)有關的信息)、mnt(當前裝載的文件系統)、PID(有關進程ID的信息)、user(資源配額的信息)、net(網絡信息)。

    從網絡角度看一個namespace提供了一份獨立的網絡協議棧(網絡設備接口、IPv4/v6、IP路由、防火牆規則、sockets等),而一個設備(Linux Device)只能位於一個namespace中,不同namespace中的設備可以利用vethpair進行橋接。

     

    4 veth pair

    veth pair不是一個設備,而是一對設備,以連接兩個虛擬以太端口。操作vethpair,需要跟namespace一起配合,不然就沒有意義。如圖

     

    5 Bridge

    在Linux的語境里,Bridge(網橋)與Switch(交換機)是一個概念。因為一對veth pair只能連接兩台device,因此如果需要多台設備互聯則需要bridge。

    如圖:4個namespace,每個namespace都有一個tap,每個tap與網橋vb1的tap組成一對veth pair,這樣,這4個namespace就可以二層互通了。

     

    6 Router

    Linux創建Router並沒有像創建虛擬Bridge那樣,有一個直接的命令brctl,而且它間接的命令也沒有,不能創建虛擬路由器……因為它就是路由器(Router) !

    如圖:我們需要在router(也就是我們的操作系統linux上增加去往各NS的路由)。

     

    7 tun

    tun是一個網絡層(IP)的點對點設備,它啟用了IP層隧道功能。Linux原生支持的三層隧道。支持隧道情況:ipip(ipv4 in ipv4)、gre(ipv4/ipv6 over ipv4)、sit(ipv6 over ipv4)、isatap(ipv6/ipv4隧道)、vti(ipsec接口)。

    學過傳統網絡GRE隧道的人更容易理解,如圖:

    NS1的tun1的ip 10.10.10.1與NS2的tun2的ip 10.10.20.2建立tun

    NS1的tun的ip是10.10.10.1,隧道的外層源ip是192.168.1.1,目的ip是192.168.2.1,是不是跟GRE很像。

     

    8 iptable

    我們通常把iptable說成是linux的防火牆,實際上這種說法並不準確。實際上iptable只是一個運行在用戶空間的命令行工具,真正實現防火牆功能的是內核空間的netfilter模塊。

    這裏我們先知道防火牆執行模塊netfilter位於內核空間,命令行iptable位於用戶空間。我們在通過iptable配置的防火牆策略(包括NAT)會在netfilter執行。

    iptables有5個鏈:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING

    l  PREROUTING:報文進入網絡接口尚未進入路由之前的時刻;

    l  INPUT:路由判斷是本機接收的報文,準備從內核空間進入到用戶空間的時刻;

    l  FORWARD:路由判斷不是本機接收的報文,需要路由轉發,路由轉發的那個時刻;

    l  OUTPUT:本機報文需要發出去 經過路由判斷選擇好端口以後,準備發送的那一刻;

    l  POSTROUTING:FORWARD/OUTPUT已經完成,報文即將出網絡接口的那一刻。

     DNAT用的是PREROUTING,修改的是目的地址,SNAT用的是POSTROUTING,修改的是源地址。

    Iptable有5個表:filter,nat,mangle,raw, security,raw表和security表不常用。主流文檔都是說5鏈4表,沒有包括security表。

    l  Raw表——決定數據包是否被狀態跟蹤機制處理

    l  Mangle表——修改數據包的服務類型、TTL、並且可以配置路由實現QOS

    l  Nat表——用於網絡地址轉換(IP、端口)

    l  filter表——過濾數據包

    l  security 表(用於強制訪問控制網絡規則,例如:SELinux)

    4個表的優先級由高到低的順序為:raw–>mangle–>nat–>filter。RAW表,在某個鏈上,RAW表處理完后,將跳過NAT表和 ip_conntrack處理,即不再做地址轉換和數據包的鏈接跟蹤處理了。RAW表可以應用在那些不需要做nat的情況下,以提高性能。如大量訪問的web服務器,可以讓80端口不再讓iptables做數據包的鏈接跟蹤處理,以提高用戶的訪問速度。

    下面講下數據包流向與處理:

    1. 如果是外部訪問的目的是本機,比如用戶空間部署了WEB服務,外部來訪問。數據包從外部進入網卡—–>PREROUTING處理—–>INPUT處理—–>到達用戶空間程序接口,程序處理完成后發出—–>OUTPUT處理—–>POSTROUTING處理。每個處理點都有對應的表,表的處理順序按照raw–>mangle–>nat–>filter處理。
    2. 如果用戶訪問的目的不是本機,linux只是一个中轉(轉發)設備,此時需要開啟ip forward功能,數據流就是進入網卡—–> PREROUTING處理—–> FORWARD處理—–> POSTROUTING處理。

     

    8.2 NAT

    Netfilter中的NAT有三個點做處理,

    (1)   NAT-PREROUTING (DNAT)

    數據報文進入PREROUTING,NAT模塊就會處理,比如用戶空間的WEB服務私網地址192.168.0.1,對外提供公網ip是220.1.1.1。

    當外部ip訪問220.1.1.1時,PREROUTING接受數據包,NAT模塊處理將目的ip 220.1.1.1轉換為私網ip192.168.0.1,這就是DNAT。

    (2)   NAT-POSTROUTING (SNAT)

    用戶空間應用程序訪問外部網絡,比如用戶空間應用程序訪問114.114.114.144,私網ip 192.168.0.1,此時數據包流經POSTROUTING,NAT模塊會處理,將192.168.0.1轉換為220.2.2.2,對於目的ip114.114.114.114來說,就是220.2.2.2訪問它,這就是SNAT。

    (3)   NAT-OUTPUT (DNAT)

    我們把內核空間想象成一台防火牆,防火牆自身對外發送報文訪問外部時,就在OUTPUT做DNAT,此時不需要再POSTROUTING點再做NAT。因為此時從OUTPUT出來的源IP已經是公網地址了

    8.3  Firewall

    防火牆根據規則執行accept/reject動作,防火牆規則的元素如下:

    入接口、出接口、協議、源地址/子網、目的地址/子網、源端口、目的端口。

    Netfilter中的Firewall會在這三個點進行處理:INPUT/FORWARD/OUTPUT

    8.4 Mangle

    mangle表主要用於修改數據包的ToS(  Type of Service,服務類型)、 TTL(Time to Live,生存周期)以及為數據包設置Mark標記,以實現QoS(Qualityof Service,服務質量)調整以及策略路由等應用。Netfilter每個點都可以做mangle。

    9 總結

    tap、tun、vethpair在Linux中都被稱為設備,但是在與日常概念的類比中,常常被稱作接口。而bridge和router這些日常稱為設備的再linux中反而不稱為設備。linux利用namespace做隔離,Bridge提供二層轉發功能,Router提供三層轉發功能。Router還常常藉助iptable提供SNAT/DNAT功能。Bridge也常常藉助iptable提供Firewall功能。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 跟着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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

  • Spark學習筆記(三)-Spark Streaming

    Spark學習筆記(三)-Spark Streaming

    Spark Streaming支持實時數據流的可擴展(scalable)、高吞吐(high-throughput)、容錯(fault-tolerant)的流處理(stream processing)。

     

                                                        架構圖

     

    特性如下:

     

    • 可線性伸縮至超過數百個節點;

    • 實現亞秒級延遲處理;

    • 可與Spark批處理和交互式處理無縫集成;

    • 提供簡單的API實現複雜算法;

    • 更多的流方式支持,包括Kafka、Flume、Kinesis、Twitter、ZeroMQ等。

     

    原理

     

    Spark在接收到實時輸入數據流后,將數據劃分成批次(divides the data into batches),然後轉給Spark Engine處理,按批次生成最後的結果流(generate the final stream of results in batches)。 

     

     

    API

     

    DStream

     

    DStream(Discretized Stream,離散流)是Spark Stream提供的高級抽象連續數據流。

     

    • 組成:一個DStream可看作一個RDDs序列。

    • 核心思想:將計算作為一系列較小時間間隔的、狀態無關的、確定批次的任務,每個時間間隔內接收的輸入數據被可靠存儲在集群中,作為一個輸入數據集。

     

     

    • 特性:一個高層次的函數式編程API、強一致性以及高校的故障恢復。

    • 應用程序模板:

    • 模板1

    • 模板2

     

    WordCount示例

     

     

    Input DStream

     

    Input DStream是一種從流式數據源獲取原始數據流的DStream,分為基本輸入源(文件系統、Socket、Akka Actor、自定義數據源)和高級輸入源(Kafka、Flume等)。

     

    • Receiver:
    • 每個Input DStream(文件流除外)都會對應一個單一的Receiver對象,負責從數據源接收數據並存入Spark內存進行處理。應用程序中可創建多個Input DStream并行接收多個數據流。

    • 每個Receiver是一個長期運行在Worker或者Executor上的Task,所以會佔用該應用程序的一個核(core)。如果分配給Spark Streaming應用程序的核數小於或等於Input DStream個數(即Receiver個數),則只能接收數據,卻沒有能力全部處理(文件流除外,因為無需Receiver)。

    • Spark Streaming已封裝各種數據源,需要時參考官方文檔。

     

    Transformation Operation

     

    • 常用Transformation

     

    * map(func) :對源DStream的每個元素,採用func函數進行轉換,得到一個新的DStream;

    * flatMap(func):與map相似,但是每個輸入項可用被映射為0個或者多個輸出項;

    * filter(func):返回一個新的DStream,僅包含源DStream中滿足函數func的項;

    * repartition(numPartitions):通過創建更多或者更少的分區改變DStream的并行程度;

    * union(otherStream):返回一個新的DStream,包含源DStream和其他DStream的元素;

    * count():統計源DStream中每個RDD的元素數量;

    * reduce(func):利用函數func聚集源DStream中每個RDD的元素,返回一個包含單元素RDDs的新DStream;

    * countByValue():應用於元素類型為K的DStream上,返回一個(K,V)鍵值對類型的新DStream,每個鍵的值是在原DStream的每個RDD中的出現次數;

    * reduceByKey(func, [numTasks]):當在一個由(K,V)鍵值對組成的DStream上執行該操作時,返回一個新的由(K,V)鍵值對組成的DStream,每一個key的值均由給定的recuce函數(func)聚集起來;

    * join(otherStream, [numTasks]):當應用於兩個DStream(一個包含(K,V)鍵值對,一個包含(K,W)鍵值對),返回一個包含(K, (V, W))鍵值對的新DStream;

    * cogroup(otherStream, [numTasks]):當應用於兩個DStream(一個包含(K,V)鍵值對,一個包含(K,W)鍵值對),返回一個包含(K, Seq[V], Seq[W])的元組;

    * transform(func):通過對源DStream的每個RDD應用RDD-to-RDD函數,創建一個新的DStream。支持在新的DStream中做任何RDD操作。

     

    • updateStateByKey(func)

    • updateStateByKey可對DStream中的數據按key做reduce,然後對各批次數據累加

    • WordCount的updateStateByKey版本

     

    • transform(func)

    • 通過對原DStream的每個RDD應用轉換函數,創建一個新的DStream。

    • 官方文檔代碼舉例

     

    • Window operations

    • 窗口操作:基於window對數據transformation(個人認為與Storm的tick相似,但功能更強大)。

    • 參數:窗口長度(window length)和滑動時間間隔(slide interval)必須是源DStream批次間隔的倍數。

    • 舉例說明:窗口長度為3,滑動時間間隔為2;上一行是原始DStream,下一行是窗口化的DStream。

    • 常見window operation

    有狀態轉換包括基於滑動窗口的轉換和追蹤狀態變化(updateStateByKey)的轉換。

    基於滑動窗口的轉換

    * window(windowLength, slideInterval) 基於源DStream產生的窗口化的批數據,計算得到一個新的DStream;

    * countByWindow(windowLength, slideInterval) 返迴流中元素的一個滑動窗口數;

    * reduceByWindow(func, windowLength, slideInterval) 返回一個單元素流。利用函數func聚集滑動時間間隔的流的元素創建這個單元素流。函數func必須滿足結合律,從而可以支持并行計算;

    * reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) 應用到一個(K,V)鍵值對組成的DStream上時,會返回一個由(K,V)鍵值對組成的新的DStream。每一個key的值均由給定的reduce函數(func函數)進行聚合計算。注意:在默認情況下,這個算子利用了Spark默認的併發任務數去分組。可以通過numTasks參數的設置來指定不同的任務數;

    * reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) 更加高效的reduceByKeyAndWindow,每個窗口的reduce值,是基於先前窗口的reduce值進行增量計算得到的;它會對進入滑動窗口的新數據進行reduce操作,並對離開窗口的老數據進行“逆向reduce”操作。但是,只能用於“可逆reduce函數”,即那些reduce函數都有一個對應的“逆向reduce函數”(以InvFunc參數傳入);

    * countByValueAndWindow(windowLength, slideInterval, [numTasks]) 當應用到一個(K,V)鍵值對組成的DStream上,返回一個由(K,V)鍵值對組成的新的DStream。每個key的值都是它們在滑動窗口中出現的頻率。

    • 官方文檔代碼舉例 

     

    • join(otherStream, [numTasks])

    • 連接數據流

    • 官方文檔代碼舉例1

    • 官方文檔代碼舉例2

     

    Output Operation

     

     

    緩存與持久化

     

    • 通過persist()將DStream中每個RDD存儲在內存。

    • Window operations會自動持久化在內存,無需显示調用persist()。

    • 通過網絡接收的數據流(如Kafka、Flume、Socket、ZeroMQ、RocketMQ等)執行persist()時,默認在兩個節點上持久化序列化后的數據,實現容錯。

     

    Checkpoint

     

    • 用途:Spark基於容錯存儲系統(如HDFS、S3)進行故障恢復。

    • 分類:

    • 元數據檢查點:保存流式計算信息用於Driver運行節點的故障恢復,包括創建應用程序的配置、應用程序定義的DStream operations、已入隊但未完成的批次。

    • 數據檢查點:保存生成的RDD。由於stateful transformation需要合併多個批次的數據,即生成的RDD依賴於前幾個批次RDD的數據(dependency chain),為縮短dependency chain從而減少故障恢復時間,需將中間RDD定期保存至可靠存儲(如HDFS)。

    • 使用時機:

    • Stateful transformation:updateStateByKey()以及window operations。

    • 需要Driver故障恢復的應用程序。

    • 使用方法

    • Stateful transformation

    streamingContext.checkpoint(checkpointDirectory)

     

    • 需要Driver故障恢復的應用程序(以WordCount舉例):如果checkpoint目錄存在,則根據checkpoint數據創建新StreamingContext;否則(如首次運行)新建StreamingContext。

     

    • checkpoint時間間隔

    • 方法:

    dstream.checkpoint(checkpointInterval)

     

    • 原則:一般設置為滑動時間間隔的5-10倍。

    • 分析:checkpoint會增加存儲開銷、增加批次處理時間。當批次間隔較小(如1秒)時,checkpoint可能會減小operation吞吐量;反之,checkpoint時間間隔較大會導致lineage和task數量增長。

     

    性能調優

     

    降低批次處理時間

     

    • 數據接收并行度

    • 增加DStream:接收網絡數據(如Kafka、Flume、Socket等)時會對數據反序列化再存儲在Spark,由於一個DStream只有Receiver對象,如果成為瓶頸可考慮增加DStream。

    • 設置“spark.streaming.blockInterval”參數:接收的數據被存儲在Spark內存前,會被合併成block,而block數量決定了Task數量;舉例,當批次時間間隔為2秒且block時間間隔為200毫秒時,Task數量約為10;如果Task數量過低,則浪費了CPU資源;推薦的最小block時間間隔為50毫秒。

    • 顯式對Input DStream重新分區:在進行更深層次處理前,先對輸入數據重新分區。

    inputStream.repartition(<number of partitions>)

     

    • 數據處理并行度:reduceByKey、reduceByKeyAndWindow等operation可通過設置“spark.default.parallelism”參數或顯式設置并行度方法參數控制。

    • 數據序列化:可配置更高效的Kryo序列化。

     

    設置合理批次時間間隔

     

    • 原則:處理數據的速度應大於或等於數據輸入的速度,即批次處理時間大於或等於批次時間間隔。

    • 方法:

    • 先設置批次時間間隔為5-10秒以降低數據輸入速度;

    • 再通過查看log4j日誌中的“Total delay”,逐步調整批次時間間隔,保證“Total delay”小於批次時間間隔。

     

    內存調優

     

    • 持久化級別:開啟壓縮,設置參數“spark.rdd.compress”。

    • GC策略:在Driver和Executor上開啟CMS。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 只看到了別人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)

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

  • 抗議氣候變遷 奧斯卡影后珍芳達被捕

    摘錄自2019年10月12日中央通訊社美國報導

    奧斯卡影后珍芳達(Jane Fonda)12日在美國國會山莊外被逮捕,當時她參與氣候變遷抗議活動,並在現場發表談話要求當局採取行動保護環境,之後就被警方銬上手銬帶走。

    法新社報導,長期投身社會運動的珍芳達在臉書(Facebook)專頁張貼的影片顯示,她在國會大廈的階梯上抗議長達10分鐘後,便與其他數名人士一同遭到拘押。

    警方發言人發布聲明說:「美國國會警察局(US Capitol Police)今天逮捕16人,因為他們違法在國會山莊東側抗議。」但聲明並未指明是哪些人被捕。

    81歲的珍芳達穿著亮紅色大衣,反覆呼喊氣候變遷相關口號,而後她被警方銬上手銬,並在其他示威者的歡呼聲下被帶走。數個小時後珍芳達就被釋放。

    珍芳達近期告訴「洛杉磯時報」(Los Angeles Times),她將搬到華府4個月,並效法瑞典環保少女桑柏格(Greta Thunberg)的熱情,全心全力對抗全球暖化。

    珍芳達被捕前曾向現場一小群人發表談話,她痛斥氣候變遷這項「人為危機」,還說自己跟其他環保人士「每週五中午11時,無論晴天、雨天、下雪或暴風雪」,他們都會回到國會山莊展開一連串示威活動。

    「紐約時報」(New York Times)報導,珍芳達若因參與違法示威活動被定罪,將面臨最多250美元(約新台幣7660元)罰款,以及最高90天的刑期。

    珍芳達曾分別以1971年「柳巷芳草」(Klute)與1978年「歸返家園」(Coming Home)兩部作品,摘下奧斯卡影后殊榮。

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

    【其他文章推薦】

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

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

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

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

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

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