標籤: 網頁設計公司

  • 北汽規劃打造四大海外基地 南非新工廠將投產

    據媒體報導,從北汽集團官方獲悉,北汽將斥資50億元在南非建設總裝 廠,該工廠擬於下月開始建設,計畫於2017年11月建成投產。按照規劃,北汽將打造包括南非、伊朗在內的四大海外運營基地並輻射周邊市場,形成四大屬地化產業運營集團。

    未來北汽將海外市場拓展劃分了三條線,歸納為“一帶一路一洲 哥倫布航線”,其中“一帶一路”符合大環境下的海外發展趨勢,“一洲”則是“一帶一路”中涉及到的非洲,在此基礎上,北汽增加了“哥倫布航線”沿途的中南美地區。繼續細化,北汽將以“南非、伊朗、東南亞、墨西哥”四大重點專案為引領,建設輻射周邊市場的四大海外運營基地,逐步實現從“旅行者”到“定居者”的角色轉變。

    與跨國車企在中國的發展要尋求本地車企進行合作類似,北汽在南非新建的工廠將由北汽集團與南非工業開發公司的合資企業負責運營。新工廠總投資金額達到50億元(7.73億美元),計畫於下月開始建設,有望於2017年11月建成投產,計畫年產能為10萬輛。

    據介紹,南非工廠將作為試點,為後續北汽加速海外涉及12個國家的19個KD(散件組裝廠)專案建設積累經驗。北汽集團已於2013年在南非開設了一家小型SKD廠,位於斯普林斯鎮,該廠生產小型麵包計程車,此次在南非斥鉅資打造新工廠並作為試點也就不難理解。

    北京汽車國際發展公司擁有五大核心業務,包括自主品牌整車和零部件產品的出口,技術、設備、整車的進口,此外還有產品改裝,境外投資以及國際合作,初期投放海外市場的產品也將以北汽自主品牌為主。這意味著即將建成的南非汽車生產廠將有望投產包括北京紳寶、北京牌和北汽威旺三大產品系列,初步形成對當地市場佈局的同時還將進行他國出口,擴大南非及周邊國家和地區市場份額。

    在北汽擴大海外市場佈局後,未來市場銷量將成為企業諸多努力的體現。“十三五”期間北汽制定了“2030”戰略,戰略中指出企業要在2020年實現20萬輛整車出口,完成3個建設,包括國際化運營隊伍建設、合作夥伴隊伍建設、體系能力建設,從而實現品牌高端市場的突破。

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

  • 別翻了,這篇文章就是要讓你入門java多線程!

    別翻了,這篇文章就是要讓你入門java多線程!

    目錄

    就在前幾天,有位讀者朋友私信宜春,說期待出一篇多線程的文章,我當時內心是小鹿亂撞啊….於是這幾天茶不思飯不想,好幾天深夜皆是輾轉反側,兩目深凝,以至於這幾天走起路來格外飄飄然,左搖右晃的,魔鬼般的步伐,一般兩步,走在大馬路中央上差點被打~我承認太誇張了,感覺又要被打~。最終還是君意不可違,答應了這位讀者朋友,從這位讀者朋友的博客頭像可以看的出來,這位朋友絕bi歷經滄桑,對生活無盡的坦然浩對,看透俗世凡塵、世態炎涼、趨炎附勢,擁有着極高的安心恬盪情懷…啥?啥子?這個是系統默認頭像….嗯嗯嗯呃。。。那個那個宜春啥都沒說哈,別把什麼事都扯宜春身上,你們一天天的,我啥都沒說(理直氣壯)…

    @

    1. 理解線程與進程

    由於併發肯定涉及到多線程,因此在進入併發編程主題之前,我們先來了解一下進程和線程的由來,這對後面對併發編程的理解將會有很大的幫助。

    進程和線程的對比這一知識點由於過於基礎,正因為過於基礎,所以我們更應該透徹它!我們必須掌握什麼是線程和進程,掌握線程與進程的關係、區別及優缺點 !

    1.1、何為進程?

    首先我們來看一下進程的概念:

    進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。

    看完之後,是不是感覺很抽象?很懵bi?懵bi就對了,說明你和我智商一樣高….~開個玩笑~

    不妨先憋棄上面的概念,放鬆一下大腦,雙擊打開LOL,秒選德馬打野,輸了直接退出遊戲並且保持微笑,然後正襟危坐心平氣和的看宜春寫的博客….

    這個時候的你不僅僅是愉快的擼了一把遊戲,而且還親自體驗擼了一把進程…其實在你雙擊打開LOL的時候就已經創建了進程,此話怎講?眾所周知,我們的電腦安裝的軟件比如:LOL、微信、谷歌等等都是存儲在我們的硬盤上的,硬盤上的數據可以說是永久存儲(ORM),當我們雙擊LOL的時候,LOL程序執行就進入了內存中,所有的程序必須進入內存中才能執行,內存屬於臨時存儲(RAM),而進入內存的程序都可以叫做是進程,把LOL程序退出的時候,LOL程序就會退出內存,進程也就隨之銷毀了!因此說各位擼了一把進程也不為過吧。

    啥?字太多了,看的不夠明了,不如看圖得勁….額。。。

    上面主要是通過抽象的描述了進程,其實進程是可以很直觀的看的到的,我們可以再電腦底部任務欄,右鍵—–>打開任務管理器,可以查看當前任務的進程:

    其實,關於線程博主我完全可以一兩句話概括,但是這樣並不負責,畢竟這篇文章標題就是要讓你徹底入門java多線程。如果連進程都理解不好談何徹底理解多線程?

    1.2、何為線程?

    同樣的,我們先來看線程的概念

    線程是進程中的一個執行單位,負責當前進程中程序的執行。一個進程中至少有一個線程,也就是說一個進程可以有多個線程的,而多個線程的進程運用程序就叫做多線程程序

    線程的概念稍微好理解很多,但是想更深層次的去理解光靠上面一段文字的概述是完全不夠的!

    這不打LOL的過程中,屬實卡的一批,果然花高價998買的6手戴爾筆記本打LOL屬實像極了愛情。這個時候不得不雙擊打開電腦安全管家進行殺毒,果然2500天沒有進行過病毒查殺,我天。。。其實我相信很多人都用過電腦管家或者手機管家之類的安全軟件,我們都很清楚我們開啟病毒查殺之後一般要幾分鐘掃描查殺,這個時候我們是可以讓它後台進行的,我們不會等而是開啟另一個垃圾清理的功能,這個時候我們也不會等而是再去啟動電腦加速功能。等到 這些操作都完成之後果斷退出電腦管家,繼續LOL,果然高價998買的6手戴爾筆記本再怎麼殺毒打LOL還是照樣的卡….

    其實清楚線程必然涉及到CPU的相關概念了,將上面文字所描述的用圖片概括,大致為:

    1.3、何為多線程?

    從上一節中,我們也提到過多線程,所以理解起來應該不難。

    多線程就是多個線程同時運行交替運行

    單核CPU:交替運行。
    多核CPU:同時運行。

    其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

    1.4、何為線程調度優先級?

    說起線程調度優先級這個概念,就讓我想到現在我們大部分人投簡歷一樣。如果你的學歷或者工作經驗越高,那麼你的優先級就越高,面試官很大幾率就會讓你去面試但也不是一定只是幾率特別大,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性)!在我們每個人的電腦中線程是可以設置線程的優先級的,但是生活中沒有優先級(學歷、工作經驗)的孩子就只能靠自己的能力了~媽耶,太真實了…~

    線程優先級具有繼承特性比如A線程啟動B線程,則B線程的優先級和A是一樣的。

    線程優先級具有隨機性也就是說線程優先級高的不一定每一次都先執行完,只是被執行的可能性更大。

    在今後的多線程學習旅遊中我們會使用到getPriority()方法獲取線程的優先級。

    1.5、為什麼提倡使用多線程而不是多進程?

    線程與進程相似,但線程是一個比進程更小的執行單位,是程序執行的最小單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享同一塊內存空間和一組系統資源,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。同時線程是程序執行的最小單位。使用多線程而不是用多進程去進行併發程序的設計,是因為線程間的切換和調度的成本遠遠小於進程。

    而使用多線程,多線程會將程序運行方式從串行運行變為併發運行,效率會有很大提高。

    2、理解并行和併發

    在博主認為併發和并行是兩個非常容易被混淆的概念。為了防止繞暈大家,所以我選擇長話短說!

    1. 併發:一個時間段內同時發生(並不是同時發生)。
    2. 并行:同一時刻發生(真正的同時發生)。

    它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。

    於此同時,我們不妨回顧一下上面所提到過的CPU,並再次理解併發與并行的區別,從而溫故知新 ~我TM簡直是個天才!~

    單核CPU:交替運行【併發】
    多核CPU:同時運行【并行】

    併發給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的!

    3、特殊的一個單線程:主線程(Main線程)

    我們常說的主線程就是Main線程,它是一個特殊的單線程,話不多說,直接擼碼:

    定義一個用於測試的demo類Person

    package demo;
    
    public class Person {
       public String name;
    
       public Person(String name){
           this.name=name;
       }
    
       public void run(){
           int i=1;
           while (i<5){
               System.out.println(name+i);
               i++;
           }
       }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

    編寫Main方法

    package demo;
    
    public class MainThreadDemo {
        public static void main(String[] args) {
            Person per=new Person("常威");
            per.run();
    
            Person Per2=new Person("來福");
            Per2.run();
        }
    }

    運行結果就已經很顯而易見了,放心我不是靠你們運行結果而是單純的先分析主線程。

    運行結果:
        常威1
        常威2
        常威3
        常威4
        來福1
        來福2
        來福3
        來福4

    3.1、分析主線程原理

    3.2、 單線程的局限性

    單線程不僅效率低下,而且存在很大的局限性,惟一的優點就是安全。所以說女孩子長得安全其實也是一種優點,噗哈哈哈…

    如何體現出單線程效率低下以及它的局限性呢?其實只要一句代碼即可,還是以上面的單線程Main線程為例:

    package demo;
    
    public class MainThreadDemo {
        public static void main(String[] args) {
            Person per=new Person("常威");
            per.run();
            int a=6/0;  //=====================特別注意這行代碼
            Person Per2=new Person("來福");
            Per2.run();
        }
    }

    試想一下運行結果…

    如果對上面的運行結果有問題,或者疑問。那沒錯了,你簡直是個天(小)才(白)!真真的天(小)才(白),很有可能異常機制沒學好,好吧我給你貼出來:

    言歸正傳,效率低下何以見得?這是數據少,如果是一億條數據呢,單線程就是一個一個打印。那局限性又何以見得呢?從上面運行結果來看也能看出,只因為一行代碼而導致下面代碼不再執行。已經很明顯了。

    4、 創建多線程的四種方式

    說是說創建多線程有四種方式,但考慮到是入門文章還是主要寫入門的兩種方式,剩下的兩個暫時忽略。忽略的兩種方法有:實現Callable接口通過FutureTask包裝器來創建Thread線程、使用ExecutorServiceCallableFuture實現有返回結果的線程。現在可能對於入門的童鞋來說是接收不了的,以後再去了解也不晚!

    4.1、繼承Thread類

    Java使用java.lang.Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。

    Java中通過繼承Thread類來創建啟動多線程的步驟如下:

    1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
    2. 創建Thread子類的實例,即創建了線程對象
    3. 調用線程對象的start()方法來啟動該線程

    代碼如下:

    測試類:

    public class Demo01 {
        public static void main(String[] args) {
            //創建自定義線程對象
            MyThread mt = new MyThread("新的線程!");
            //開啟新線程
            mt.start();
            //在主方法中執行for循環
            for (int i = 0; i < 10; i++) {
                System.out.println("main線程!"+i);
            }
        }
    }

    自定義線程類:

    public class MyThread extends Thread {
        //定義指定線程名稱的構造方法
        public MyThread(String name) {
            //調用父類的String參數的構造方法,指定線程的名稱
            super(name);
        }
        /**
         * 重寫run方法,完成該線程執行的邏輯
         */
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(getName()+":正在執行!"+i);
            }
        }
    }

    Thread類本質上是實現了Runnable接口的一個實例,代表一個線程的實例。啟動線程的唯一方法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啟動一個新線程,並執行run()方法。這種方式實現多線程很簡單,通過自己的類直接extend Thread,並複寫run()方法,就可以啟動新線程並執行自己定義的run()方法。

    4.2、實現Runnable接口

    如果自己的類已經繼承另一個類,就無法直接繼承Thread,此時,可以實現一個Runnable接口來創建線程,顯然實現Runnable接口方式創建線程的優勢就很明顯了。

    直接擼碼:

    自定義一個類實現Runnable接口,並重寫接口中的run()方法,併為run方法添加要執行的代碼方法。

    public class RunableDemo implements Runnable{
    
        @Override
        public void run() {
            int a = 1;
            while (a<20){
                System.out.println(Thread.currentThread().getName()+ a);//Thread.currentThread().getName()為獲取當前線程的名字
                a++;
            }
        }
    }

    編寫Main方法

    為了啟動自定義類RunableDemo ,需要首先實例化一個Thread,並傳入RunableDemo 實例

    public class MainThreadDemo {
    
        public static void main(String[] args) {
            RunableDemo runn=new RunableDemo();
            
            //實例化一個Thread並傳入自己的RunableDemo 實例
            Thread thread=new Thread(runn);
            thread.start();
    
            int a = 1;
            while (a<20){
                //Thread.currentThread().getName()為獲取當前線程的名字
                System.out.println(Thread.currentThread().getName()+ a);
                a++;
            }
        }
    }

    運行結果:

    main1
    main2
    main3
    Thread-01
    Thread-02
    Thread-03
    Thread-04
    Thread-05
    Thread-06
    ....

    其實多運行幾遍,你會方法每次運行的結果順序都不一樣,這主要是由於多線程會去搶佔CPU的資源,誰搶到了誰就執行,而Main和Thread兩個線程一直在爭搶。

    實際上,當傳入一個Runnable target(目標)參數給Thread后,Threadrun()方法就會調用target.run(),參考JDK源代碼:

    public void run() {  
      if (target != null) {  
       target.run();  
      }  
    }  

    4.3、兩種入門級創建線程的區別

    採用繼承Thread類方式:

    (1)優點:編寫簡單,如果需要訪問當前線程,無需使用Thread.currentThread()方法,直接使用this,即可獲得當前線程。
    (2)缺點:因為線程類已經繼承了Thread類,所以不能再繼承其他的父類。

    採用實現Runnable接口方式:

    (1)優點:線程類只是實現了Runable接口,還可以繼承其他的類。在這種方式下,可以多個線程共享同一個目標對象,所以非常適合多個相
    同線程來處理同一份資源的情況,從而可以將CPU代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
    (2)缺點:編程稍微複雜,如果需要訪問當前線程,必須使用Thread.currentThread()方法。

    小結:
    如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

    實現Runnable接口比繼承Thread類的優勢:

    1.適合多個相同代碼的線程去處理同一個資源。

    2.可以避免java中單繼承的限制。

    3.增加代碼的健壯性,實現解耦。代碼可以被多個線程共享,代碼和數據獨立。

    4.線程池中只能放入實現Runnable或Callable類線程,不能放入繼承Thread的類【線程池概念之後會慢慢涉及】

    所以,如果選擇哪種方式,盡量選擇實現Runnable接口

    其實學到後面的線程池,你會發現上面兩種創建線程的方法實際上很少使用,一般都是用線程池的方式比較多一點。使用線程池的方式也是最推薦的一種方式,另外,《阿里巴巴Java開發手冊》在第一章第六節併發處理這一部分也強調到“線程資源必須通過線程池提供,不允許在應用中自行显示創建線程”。不過處於入門階段的童鞋博主還是強烈建議一步一個腳印比較好!

    5、使用匿名內部類方式創建線程

    談起匿名內部類,可能很多小白是比較陌生的,畢竟開發中使用的還是比較少,但是同樣是非常重要的一個知識!於此同時我就貼出關於匿名內部類的文章如果小白童鞋能看懂下面這個代碼,真的你不需要看那篇文章了,你T喵的簡直是個天才!

    package AnonymousInner;
    
    public class NiMingInnerClassThread {
        public static void main(String[] args) {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i<5;i++){
                        System.out.println("熊孩子:"+i);
                    }
                }
            };
            new Thread(r).start();
            for (int i = 0; i < 5 ; i++){
                System.out.println("傻狍子:"+i);
            }
        }
    }

    小白童鞋還愣着幹啥呀趕緊去補補…

    6、線程安全問題

    線程安全問題主要是共享資源競爭的問題,也就是在多個線程情況下,一個或多個線程同時搶佔同一資源導致出現的一些不必要的問題,最典型的例子就是火車四個窗口售票問題了,這裏就不再舉售票例子了,已經爛大街了,這裏就簡單實現一個線程安全問題代碼….

    實現Runnable接口方式為例,主要實現過程是:實例化三個Thread,並傳入同一個RunableDemo 實例作為參數,最後開啟三條相同參數的線程,代碼如下:

    public class RunableDemo implements Runnable{
        public int a = 100;//線程共享數據
        
        @Override
        public void run() {
            while (a>0){
                System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
                a--;
            }
        }
    }
    public class MainThreadDemo {
    
        public static void main(String[] args) {
            RunableDemo runn=new RunableDemo();
            
            Thread thread1=new Thread(runn);
            Thread thread2=new Thread(runn);
            Thread thread3=new Thread(runn);
            thread1.start();
            thread2.start();
            thread3.start();
            }
     }

    運行結果:

    Thread-0==100
    Thread-0==99
    Thread-1==100
    Thread-1==97
    Thread-1==96
    Thread-1==95
    Thread-2==98
    ...

    根據結果可以看出,確實是三條線程(Thread-0、1、2)在執行,安全問題就出在線程會出現相同的結果比如上面的100就出現了兩次,如果循環條件更改一下可能也會出現負數的情況。這種情況該怎麼解決呢?這個時候就需要線程同步了!

    7、解決線程安全問題:線程同步

    實際上,線程安全問題的解決方法有三種:

    1、同步代碼塊
    2、同步方法
    3、鎖機制

    7.1、 synchronized同步代碼塊

    第一種方法:同步代碼塊

    格式:

    synchronized(鎖對象) {
    可能會出現線程安全問題的代碼(訪問共享數據的代碼)
    }

    使用同步代碼塊特別注意:
    1、通過代碼塊的鎖對象,可以是任意對象
    2、必須保證多個線程使用的鎖對象必須是同一個
    3、鎖對象的作用是把同步代碼快鎖住,只允許一個線程在同步代碼塊執行

    還是以上麵線程安全問題為例子,使用同步代碼塊舉例:

    public class RunableDemo implements Runnable{
        public int a = 100;//線程共享數據
    
        Object object=new Object(); //事先準備好一個鎖對象
    
        @Override
        public void run() {
            synchronized (object){  //使用同步代碼塊
            while (a>0){
                System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
                a--;
            }
            }
        }
    }

    Main方法沒有任何改動,運行一下結果是絕對沒問題的,數據都是正確的沒有出現重複情況這一出,各位可以自己嘗試一下!

    同步代碼塊的原理:

    使用了一個鎖對象,叫同步鎖,對象鎖,也叫同步監視器,當開啟多個線程的時候,多個線程就開始搶奪CPU的執行權,比如現在t0線程首先的到執行,就會開始執行run方法,遇到同步代碼快,首先檢查是否有鎖對象,發現有,則獲取該鎖對象,執行同步代碼塊中的代碼。之後當CUP切換線程時,比如t1得到執行,也開始執行run方法,但是遇到同步代碼塊檢查是否有鎖對象時發現沒有鎖對象,t1便被阻塞,等待t0執行完畢同步代碼塊,釋放鎖對象,t1才可以獲取從而進入同步代碼塊執行。
    同步中的線程,沒有執行完畢是不會釋放鎖的,這樣便實現了線程對臨界區的互斥訪問,保證了共享數據安全。
    缺點:頻繁的獲取釋放鎖對象,降低程序效率

    7.2、同步方法

    使用步驟:

    1、把訪問了共享數據的代碼抽取出來,放到一個方法中
    2、在該方法上添加 synchronized 修飾符

    格式:

    修飾符 synchronized 返回值類型 方法名稱(參數列表) {
      方法體...
    }

    代碼示例:

    public class RunableDemo implements Runnable{
        public int a = 100;//線程共享數據
    
        @Override
        public void run() {
            while (true){
                sell(); //調用下面的sell方法
            }
        }
        
        //訪問了共享數據的代碼抽取出來,放到一個方法sell中 
        public synchronized void sell(){
            while (a>0){
                System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
                a--;
            }
        }
    }

    同步方法的也是一樣鎖住同步的代碼,但是鎖對象的是Runable實現類對象,也就是this,誰調用方法,就是誰。

    說到同步方法,就不得不說一下靜態同步方法,顧名思義,就是在同步方法上加上static,靜態的同步方法,添加一個靜態static修飾符,此時鎖對象就不是this了,靜態同步方法的鎖對象是本類的class屬性,class文件對象(反射)

    public class RunableDemo implements Runnable{
        public static int a = 100;//線程共享數據     =====此時共享數據也要加上static
    
        @Override
        public void run() {
            while (true){
                sell();
            }
        }
    
        public static synchronized void sell(){  //注意添加了static關鍵字
            while (a>0){
                System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
                a--;
            }
        }
    }

    使用靜態同步方法時,此時共享數據也要加上static,因為static成員才能訪問static成員,如果對static關鍵字不是他別理解的可以補補了,放心,博主有信心讓你有所收穫,會讓你重新認識到static的魅力:

    當然靜態同步方法了解即可!

    7.3、Lock鎖

    Lock接口位於java.util.concurrent.locks.Lock它是JDK1.5之後出現的,Lock接口中的方法:

    void lock(): 獲取鎖

     

    void unlock(): 釋放鎖

    Lock接口的一個實現類java.util.concurrent.locks.ReentrantLock implements Lock接口

    使用方法:
    1、在Runable實現類的成員變量創建一個ReentrantLock對象
    2、在可能產生線程安全問題的代碼該對象調用lock方法獲取鎖
    3、在可能產生線程安全問題的代碼該對象調用unlock方法釋放鎖

    代碼示例:

    import java.util.concurrent.locks.ReentrantLock;
    
    public class RunableDemo implements Runnable{
        public static int a = 100;//線程共享數據
    
        //1、在Runable實現類的成員變量創建一個ReentrantLock對象============
        ReentrantLock reentrantLock = new ReentrantLock();
    
        @Override
        public void run() {
            // 2、在可能產生線程安全問題的代碼前該對象調用lock方法獲取鎖=======
            reentrantLock.lock();
            while (a>0){
                System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
                a--;
            }
            // 3、在可能產生線程安全問題的代碼后該對象調用unlock方法獲取鎖======
            reentrantLock.unlock();
        }
    
    }

    當然更安全的寫法是,在線程安全問題代碼中try...catchy,最後在finally語句中添加reentrantLock.unlock();,這樣方為上上策!

    7.4、三種方法小結

    第一種
    synchronized 同步代碼塊:可以是任意的對象必須保證多個線程使用的鎖對象是同一個

     

    第二種
    synchronized 同步方法: 鎖對象是this,誰調用鎖對象就是誰

     

    synchronized 靜態同步方法: 鎖對象是其class對象,該對象可以用this.getClass()方法獲取,也可以使用當前類名.class 表示。【了解即可】

     

    第三種
    Look鎖方法:該方法提供的方法遠遠多於synchronized方式,主要在Runable實現類的成員變量創建一個ReentrantLock對象,並使用該對象調用lock方法獲取鎖以及unlock方法釋放鎖!

    8、線程常用方法

    8.1、Thread類

      Thread():用於構造一個新的Thread。

      Thread(Runnable target):用於構造一個新的Thread,該線程使用了指定target的run方法。

      Thread(ThreadGroup group,Runnable target):用於在指定的線程組中構造一個新的Thread,該

      線程使用了指定target的run方法。

      currentThread():獲得當前運行線程的對象引用。

      interrupt():將當前線程置為中斷狀態。

      sleep(long millis):使當前運行的線程進入睡眠狀態,睡眠時間至少為指定毫秒數。

      join():等待這個線程結束,即在一個線程中調用other.join(),將等待other線程結束后才繼續本線程。

      yield():當前執行的線程讓出CPU的使用權,從運行狀態進入就緒狀態,讓其他就緒線程執行。

    8.2、Object類

      wait():讓當前線程進入等待阻塞狀態,直到其他線程調用了此對象的notify()或notifyAll()方法后,當前線程才被喚醒進入就緒狀態。

      notify():喚醒在此對象監控器(鎖對象)上等待的單個線程。

      notifyAll():喚醒在此對象監控器(鎖對象)上等待的所有線程。

    注意:wait()、notify()、notifyAll()都依賴於同步鎖,而同步鎖是對象持有的,且每個對象只有一個,所以這些方法定義在Object類中,而不是Thread類中。

    8.3、yield()、sleep()、wait()比較

       wait():讓線程從運行狀態進入等待阻塞狀態,並且會釋放它所持有的同步鎖。

       yield():讓線程從運行狀態進入就緒狀態,不會釋放它鎖持有的同步鎖。

       sleep():讓線程從運行狀態進入阻塞狀態,不會釋放它鎖持有的同步鎖。

    9、線程的狀態

    以上只是簡單的一個線程狀態圖,其實線程狀態博大精深,要講清楚還是要一大篇文筆,作為入門文章先了解一下吧,之後的併發編程文章將再講述吧!

    如果想要去深入了解一下的話也是可以的:

    10、線程池

    在java中只要說到池,基本都是一個套路,啥數據庫連接池、jdbc連接池等,思想基本上就是:一個容納多個要使用資源的容器,其中的資源可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建資源而消耗過多資源。

    10.1、線程池概述

    線程池其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。

    合理利用線程池能夠帶來三個好處:

    1. 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
    2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
    3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

    10.2、 線程池的使用

    Java裏面線程池的最頂級接口是java.util.concurrent.Executor,但是嚴格意義上講Executor並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService

    要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。

    Executors類中有個創建線程池的方法如下:

    • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)

    獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:

    • public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行

    Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。

    使用線程池中線程對象的步驟:

    1. 創建線程池對象。
    2. 創建Runnable接口子類對象。(task)
    3. 提交Runnable接口子類對象。(take task)
    4. 關閉線程池(一般不操作這一步)。

    Runnable實現類代碼:

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("我要一個游泳教練");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("教練來了: " + Thread.currentThread().getName());
            System.out.println("教我游泳,教會後,教練又回到了游泳池");
        }
    }

    線程池測試類:

    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 創建線程池對象
            ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
            // 創建Runnable實例對象
            MyRunnable r = new MyRunnable();
    
            //自己創建線程對象的方式
            // Thread t = new Thread(r);
            // t.start(); ---> 調用MyRunnable中的run()
    
            // 從線程池中獲取線程對象,然後調用MyRunnable中的run()
            service.submit(r);
            // 再獲取個線程對象,調用MyRunnable中的run()
            service.submit(r);
            service.submit(r);
            // 注意:submit方法調用結束后,程序並不終止,是因為線程池控制了線程的關閉。
            // 將使用完的線程又歸還到了線程池中
            // 關閉線程池
            //service.shutdown();
        }
    }

    以上只是簡單的使用線程池,僅僅是入門階段!道阻且長,路還很長….

    到這裏,本文章入門暫時告一段落,以後有時間盡量抽空更新….

    如果本文章對你有幫助,哪怕是一點點,請點個讚唄,謝謝你~

    歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術…說好了來了就是盆友喔…

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

    【其他文章推薦】

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

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

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

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

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

    ※試算大陸海運運費!

  • 三、netcore跨平台之 Linux配置nginx負載均衡

    三、netcore跨平台之 Linux配置nginx負載均衡

    前面兩章講了netcore在linux上部署以及配置nginx,並讓nginx代理webapi。

    這一章主要講如何配置負載均衡,有些步驟在前兩章講的很詳細了,所以這一章我就不會一個個截圖了。

    因為本人只有一個服務器。所以我會在同一台服務器上部署兩套差不多的實例。

    同樣的代碼,我們在Program.cs進行了修改,如圖所示:

    這裏我把原來的端口6666改成了8888

     

     然後你可以改一改你的接口部分的代碼,便於讓你更好的看到效果。

    這裏把value1和value2改成value3和value4,這裡是為了看到測試效果,在實際的開發中這裏不用改。

     

     然後發布和上傳到服務器,如何發布和上傳,我在第一章有講到:https://www.cnblogs.com/dengbo/p/11878766.html

    注意的是你同樣的地方新建一個新的目錄保存你新上傳的程序,netcore是我第一章建立的,netcore1是新建的,

    你把你新的發布包放在netcore即可。如圖:

    上傳結束后,在這個目錄中運行你的程序,輸入下面的命令

    dotnet WebApiTest.dll   --server.urls "http://*:8888"

    如圖所示

     

     然後去看看你的接口是否正常

     

     

    好了,這裏的準備工作完成了,下面我們進入到nginx的配置的目錄中

    輸入下面的命令:

    cd /usr/local/nginx/conf

    然後對文件進行編輯

    vim nginx.conf

     

     我們需要在這裏修改一下配置。

    在如圖的server的平級添加如下的代碼

    upstream NgWebApi {
                    server localhost:6666;
                    server localhost:8888;
        }

    上面的 NgWebApi是隨意寫的名稱,不要糾結這裏。

    然後在修改 proxy_pass後面的內容:

    proxy_pass http://NgWebApi;

    最終的結果如下:

     

     這樣你就修改完成,輸入:wq退出並保存即可。

    最後檢查並重啟nginx

    /usr/local/nginx/sbin/nginx -t
    /usr/local/nginx/sbin/nginx -s reload

    最後不要忘記把你的8888端口的webapi啟動一下。

    這裏我務必要提醒你,請進入到你的程序的目錄中執行這段代碼,

    cd /root/netcore1
    dotnet WebApiTest.dll   --server.urls "http://*:8888"

    啟動如下:

     

     

     好了,配置結束了,下面我們來測試下

     

    還是昨天的那個網站進行測試   https://www.sojson.com/httpRequest/

     

     

     

    多次發送請求會出現下面的響應

     

     

    看到上面兩個請求,就說明你配置成功了,是不是很簡單。

    上面這種配置,系統會採用默認的輪詢訪問不同的端口,nginx作為強大的反向代理,強大的遠遠不止這裏

    下面簡單講講分發策略。

    1)、輪詢 ——輪流處理請求(這是系統默認的)

          每個請求按時間順序逐一分配到不同的應用服務器,如果應用服務器down掉,自動剔除它,剩下的繼續輪詢,如果您的服務器都差不多,建議這個。 

    2)、權重 ——誰的設置的大,誰就承擔大部分的請求

          通過配置權重,指定輪詢幾率,權重和訪問比率成正比,用於應用服務器性能不均的情況,有時候你買的服務器可能參差不齊,有的性能強大

        有的一般,你可以通過設置權重,把服務器性能強大權重設置大一點,這樣可以合理分配壓力。 

    3)ip_哈希算法

          每一次的請求按訪問iphash結果分配,這樣每個訪客固定訪問一個應用服務器,可以解決session共享的問題。

     

     

    關於權重的策略,如下圖示的 你只要加一個  weight=6 即可這裏不一定是6,是整數都行。

     

     

     然後保存即可

    這裏不要忘記重啟nginx,以及運行8888端口的程序了,如果你不會,可以看前面的部分

    最後我們看看效果

    結果和上面的測試結果差不多,唯一不同的是出現下面這個結果的次數要大於另外一個的。

     

     

    到這裏就結束了,感謝觀看。

     

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • Nebula 架構剖析系列(二)圖數據庫的查詢引擎設計

    Nebula 架構剖析系列(二)圖數據庫的查詢引擎設計

    摘要

    上文(存儲篇)說到數據庫重要的兩部分為存儲和計算,本篇內容為你解讀圖數據庫 Nebula 在查詢引擎 Query Engine 方面的設計實踐。

    在 Nebula 中,Query Engine 是用來處理 Nebula 查詢語言語句(nGQL)。本篇文章將帶你了解 Nebula Query Engine 的架構。

    上圖為查詢引擎的架構圖,如果你對 SQL 的執行引擎比較熟悉,那麼對上圖一定不會陌生。Nebula 的 Query Engine 架構圖和現代 SQL 的執行引擎類似,只是在查詢語言解析器和具體的執行計劃有所區別。

    Session Manager

    Nebula 權限管理採用基於角色的權限控制(Role Based Access Control)。客戶端第一次連接到 Query Engine 時需作認證,當認證成功之後 Query Engine 會創建一個新 session,並將該 session ID 返回給客戶端。所有的 session 統一由 Session Manger 管理。session 會記錄當前 graph space 信息及對該 space 的權限。此外,session 還會記錄一些會話相關的配置信息,並臨時保存同一 session 內的跨多個請求的一些信息。

    客戶端連接結束之後 session 會關閉,或者如果長時間沒通信會切為空閑狀態。這個空閑時長是可以配置的。
    客戶端的每個請求都必須帶上此 session ID,否則 Query Engine 會拒絕此請求。

    Storage Engine 不管理 session,Query Engine 在訪問存儲引擎時,會帶上 session 信息。

    Parser

    Query Engine 解析來自客戶端的 nGQL 語句,分析器(parser)主要基於著名的 flex / bison 工具集。字典文件(lexicon)和語法規則(grammar)在 Nebula 源代碼的 src/parser  目錄下。設計上,nGQL 的語法非常接近 SQL,目的是降低學習成本。 圖數據庫目前沒有統一的查詢語言國際標準,一旦 ISO/IEC 的圖查詢語言(GQL)委員會發布 GQL 國際標準,nGQL 會儘快去實現兼容。
    Parser 構建產出的抽象語法樹(Abstrac Syntax Tree,簡稱 AST)會交給下一模塊:Execution Planner。

    Execution Planner

    執行計劃器(Execution Planner)負責將抽象樹 AST 解析成一系列執行動作 action(可執行計劃)。action 為最小可執行單元。例如,典型的 action 可以是獲取某個節點的所有鄰節點,或者獲得某條邊的屬性,或基於特定過濾條件篩選節點或邊。當抽象樹 AST 被轉換成執行計劃時,所有 ID 信息會被抽取出來以便執行計劃的復用。這些 ID 信息會放置在當前請求 context 中,context 也會保存變量和中間結果。

    Optimization

    經由 Execution Planner 產生的執行計劃會交給執行優化框架 Optimization,優化框架中註冊有多個 Optimizer。Optimizer 會依次被調用對執行計劃進行優化,這樣每個 Optimizer都有機會修改(優化)執行計劃。最後,優化過的執行計劃可能和原始執行計劃完全不一樣,但是優化后的執行結果必須和原始執行計劃的結果一樣的。

    Execution

    Query Engine 最後一步是去執行優化后的執行計劃,這步是執行框架(Execution Framework)完成的。執行層的每個執行器一次只處理一個執行計劃,計劃中的 action 會挨個一一執行。執行器也會一些有針對性的局部優化,比如:決定是否併發執行。針對不同的 action所需數據和信息,執行器需要經由 meta service 與storage engine的客戶端與他們通信。

    最後,如果你想嘗試編譯一下 Nebula 源代碼可參考如下方式:

    有問題請在 GitHub(GitHub 地址:) 或者微信公眾號上留言,也可以添加 Nebula 小助手微信號:NebulaGraphbot 為好友反饋問題~

    推薦閱讀

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

  • 用雲開發快速製作客戶業務需求收集小程序丨實戰

    用雲開發快速製作客戶業務需求收集小程序丨實戰

    一、導語

    ​ 如何省去企業上門(現場)搜集客戶需求的環節,節約企業人力和時間成本,將客戶的業務定製需求直接上傳至雲數據庫?雲開發為我們提供了這個便利!

    二、需求背景

    ​ 作為一名XX公司IT萌萌新,這段時間對小程序開發一直有非常濃厚的興趣,並且感慨於“雲開發·不止於快”的境界。近期工作中,剛好碰見業務部門的一個需求,目的是節約上門跟客戶收集業務定製資料的時間,以往是每變更一次,就需要上門一次,碰見地域較遠的,費時費力,且往往要求幾天內完成上線,時間非常緊迫。因此,結合一直以來對雲開發的各種優勢的了解,我說服公司領導通過小程序·雲開發來實現。

    下面是其中一項業務定製界面的展示:
    (1)業務對業務流程有簡單說明;
    (2)相關業務介紹;
    (3)不同客戶輸入個性化需求;
    (4)雲存儲後台實現需求表單的收集。

    ​ 得力於雲開發提供的API和WeUI庫的便利,本項目在我在極短的時間內就實現了比較理想的效果 。接下來,我就從本項目入手,講講我是如何依靠小程序·雲開發將想法快速實現的,其實我也是剛入門沒多久,只是想分享一下自身在學習小程序開發項目中的一些知識點和體會,代碼可能略為粗糙,邏輯也有待優化,歡迎大家在評論區多多交流。

    三、開發過程

    1、組件

    主要使用了官方WeUI擴展能力的一些組件庫來實現主要功能。

    核心的WeUI庫主要有 Msg、Picker、圖片的Upload等(以快為目的,節省自己寫CSS樣式的時間,也方便0基礎的同學上手,這裏又體會到了小程序開發的便捷)。

    2、實現代碼

    本次雲開發包括雲數據庫雲存儲兩大功能:

    (1)雲數據庫

    雲數據庫的主要就是搜集客戶提交上來的表單信息,包括客戶的聯繫方式和選擇的業務類型等,並存儲在雲數據庫中,方便業務經理搜集需求。

    我們來看簡單的實現過程:

    首先是表單,用到了 form 表單組件以及它的 bindsubmit 方法,在 wxml 中放置 form 表單:

    <form bindsubmit="formSubmit">
        <view class="form">
          <view class="section">
            <picker bindchange="bindPickerGsd" mode="selector" value="{{indexGsd}}" range="{{arrayGsd}}">
              <view class="picker">歸屬縣市</view>
              <view class="picker-content" >{{arrayGsd[indexGsd]?arrayGsd[indexGsd]:"(必填項) 請下拉選擇歸屬地"}}</view> 
            </picker>
          </view>    
          <!---中間部分詳見代碼--->
        </view>
    
        <view class="footer">
          <button class="dz-btn" formType="submit" loading="{{formStatus.submitting}}" disabled="{{formStatus.submitting}}" bindtap="openSuccess">提交</button>
        </view>
      </form>

    表單中除了普通的文本輸入,增加有下拉列表的實現(畢竟客戶有時候是比較懶的)。

    來看一下具體代碼:

    bindPickerGsd: function (e) {    
      console.log('歸屬地已選擇,攜帶值為', e.detail.value)
      console.log('歸屬地選擇:', this.data.arrayGsd[e.detail.value])    
      this.setData({
         indexGsd: e.detail.value     
       })   
       this.data.formData.home_county = this.data.arrayGsd[e.detail.value]
    },

    最後表單上傳到雲數據庫:

      // 表單提交
      formSubmit: function (e) {
        var minlength = '';
        var maxlength = '';
        console.log("表單內容",e)
        var that = this;
        var formData = e.detail.value;
        var result = this.wxValidate.formCheckAll(formData);
        
        console.log("表單提交formData", formData);
        console.log("表單提交result", result)
        wx.showLoading({
          title: '發布中...',
        })
        const db = wx.cloud.database()
        db.collection('groupdata').add({
          data: {
            time: getApp().getNowFormatDate(),
            home_county: this.data.formData.home_county,
            group_name: formData.group_name,
            contact_name: formData.contact_name,
            msisdn: formData.msisdn,
            product_name: this.data.formData.product_name,
            word: formData.word,
          },
          success: res => {
            wx.hideLoading()
            console.log('發布成功', res)
    
          },
          fail: err => {
            wx.hideLoading()
            wx.showToast({
              icon: 'none',
              title: '網絡不給力....'
            })
            console.error('發布失敗', err)
          }
        })
      },
    (2)雲存儲

    ​ 因為業務的定製需要填單客戶所在單位的授權證明材料,因此需要提單人(使用人)上傳證明文件,因此增加了使用雲存儲的功能。

    核心代碼:

        promiseArr.push(new Promise((reslove,reject)=>{
            wx.cloud.uploadFile({
                cloudPath: "groupdata/" + group_name + "/" + app.getNowFormatDate() +suffix,
                filePath:filePath
            }).then(res=>{
                console.log("授權文件上傳成功")          
                })
                reslove()
                }).catch(err=>{
                console.log("授權文件上傳失敗",err)
        })
    
        因為涉及到不同頁面的數據傳遞,即將表單頁面的group_name作為雲存儲的文件夾用於存儲該客戶在表單中上傳的圖片,因此還需要用到getCurrentPages()來進行頁面間的數據傳遞 
    
        var pages = getCurrentPages();
        var prePage = pages[pages.length - 2];//pages.length就是該集合長度 -2就是上一個活動的頁面,也即是跳過來的頁面
        var group_name = prePage.data.formData.group_name.value//取上頁data里的group_name數據用於標識授權文件所存儲文件夾的名稱

    3、待進一步優化

    ​ 基於時間關係,本次版本僅對需求進行了簡單實現,作為公司一個可靠的項目,還需要關注”客戶隱私”、“數據安全”,以及更人性化的服務。比如:

    (1)提單人確認和認證過程

    可靠性:增加驗證碼驗證(防止他人冒名登記),以及公司受理業務有個客戶本人提交憑證。

    (2)訂閱消息

    受理成功后,可以給客戶進行處理結果的反饋,增強感知。

    (3)人工客服

    進行在線諮詢等。

    四、總結

    ​ 在本次項目開發中,我深刻體會到了雲開發的“快”,特別是雲數據庫的增刪查改功能非常方便。雲開發提供的種種便利,讓我在有新創意的時候,可以迅速採用小程序雲開發快速實現,省時省力,還能免費使用騰訊雲服務器,推薦大家嘗試!

    源碼地址

    如果你想要了解更多關於雲開發CloudBase相關的技術故事/技術實戰經驗,請掃碼關注【騰訊云云開發】公眾號~

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

    【其他文章推薦】

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

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

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

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

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

    ※試算大陸海運運費!

  • SpringBoot 源碼解析 (六)—– Spring Boot的核心能力 – 內置Servlet容器源碼分析(Tomcat)

    SpringBoot 源碼解析 (六)—– Spring Boot的核心能力 – 內置Servlet容器源碼分析(Tomcat)

    Spring Boot默認使用Tomcat作為嵌入式的Servlet容器,只要引入了spring-boot-start-web依賴,則默認是用Tomcat作為Servlet容器:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    Servlet容器的使用

    默認servlet容器

    我們看看spring-boot-starter-web這個starter中有什麼

    核心就是引入了tomcat和SpringMvc,我們先來看tomcat

    Spring Boot默認支持Tomcat,Jetty,和Undertow作為底層容器。如圖:

    而Spring Boot默認使用Tomcat,一旦引入spring-boot-starter-web模塊,就默認使用Tomcat容器。

    切換servlet容器

    那如果我么想切換其他Servlet容器呢,只需如下兩步:

    • 將tomcat依賴移除掉
    • 引入其他Servlet容器依賴

    引入jetty:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <!--移除spring-boot-starter-web中的tomcat-->
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <!--引入jetty-->
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>

    Servlet容器自動配置原理

    EmbeddedServletContainerAutoConfiguration

    其中
    EmbeddedServletContainerAutoConfiguration是嵌入式Servlet容器的自動配置類,該類在
    spring-boot-autoconfigure.jar中的web模塊可以找到。

    我們可以看到EmbeddedServletContainerAutoConfiguration被配置在spring.factories中,看過我前面文章的朋友應該知道SpringBoot自動配置的原理,這裏將EmbeddedServletContainerAutoConfiguration配置類加入到IOC容器中,接着我們來具體看看這個配置類:

    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @Configuration
    @ConditionalOnWebApplication// 在Web環境下才會起作用
    @Import(BeanPostProcessorsRegistrar.class)// 會Import一個內部類BeanPostProcessorsRegistrar
    public class EmbeddedServletContainerAutoConfiguration {
    
        @Configuration
        // Tomcat類和Servlet類必須在classloader中存在 // 文章開頭我們已經導入了web的starter,其中包含tomcat和SpringMvc // 那麼classPath下會存在Tomcat.class和Servlet.class
        @ConditionalOnClass({ Servlet.class, Tomcat.class }) // 當前Spring容器中不存在EmbeddedServletContainerFactory類型的實例
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat {
    
            @Bean
            public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
                // 上述條件註解成立的話就會構造TomcatEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
                return new TomcatEmbeddedServletContainerFactory();
            }
        }
        
        @Configuration
        @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
                WebAppContext.class })
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
        public static class EmbeddedJetty {
    
            @Bean
            public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
                return new JettyEmbeddedServletContainerFactory();
            }
    
        }
        
        @Configuration
        @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
        public static class EmbeddedUndertow {
    
            @Bean
            public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
                return new UndertowEmbeddedServletContainerFactory();
            }
    
        }
        
        //other code...
    }

    在這個自動配置類中配置了三個容器工廠的Bean,分別是:

    • TomcatEmbeddedServletContainerFactory

    • JettyEmbeddedServletContainerFactory

    • UndertowEmbeddedServletContainerFactory

    這裏以大家熟悉的Tomcat為例,首先Spring Boot會判斷當前環境中是否引入了Servlet和Tomcat依賴,並且當前容器中沒有自定義的
    EmbeddedServletContainerFactory的情況下,則創建Tomcat容器工廠。其他Servlet容器工廠也是同樣的道理。

    EmbeddedServletContainerFactory

    • 嵌入式Servlet容器工廠
    public interface EmbeddedServletContainerFactory {
    
        EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers);
    }

    內部只有一個方法,用於獲取嵌入式的Servlet容器。

    該工廠接口主要有三個實現類,分別對應三種嵌入式Servlet容器的工廠類,如圖所示:

    TomcatEmbeddedServletContainerFactory

    以Tomcat容器工廠TomcatEmbeddedServletContainerFactory類為例:

    public class TomcatEmbeddedServletContainerFactory extends AbstractEmbeddedServletContainerFactory implements ResourceLoaderAware {
        
        //other code...
        
        @Override
        public EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) {
            //創建一個Tomcat
            Tomcat tomcat = new Tomcat(); //配置Tomcat的基本環節
            File baseDir = (this.baseDirectory != null ? this.baseDirectory: createTempDir("tomcat"));
            tomcat.setBaseDir(baseDir.getAbsolutePath());
            Connector connector = new Connector(this.protocol);
           tomcat.getService().addConnector(connector);
            customizeConnector(connector);
          tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false);
            configureEngine(tomcat.getEngine());
            for (Connector additionalConnector : this.additionalTomcatConnectors) {
                tomcat.getService().addConnector(additionalConnector);
            }
            prepareContext(tomcat.getHost(), initializers);
            
            //包裝tomcat對象,返回一個嵌入式Tomcat容器,內部會啟動該tomcat容器
            return getTomcatEmbeddedServletContainer(tomcat);
        }
    }

    首先會創建一個Tomcat的對象,並設置一些屬性配置,最後調用getTomcatEmbeddedServletContainer(tomcat)方法,內部會啟動tomcat,我們來看看:

    protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
        Tomcat tomcat) {
        return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);
    }

    該函數很簡單,就是來創建Tomcat容器並返回。看看TomcatEmbeddedServletContainer類:

    public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer {
    
        public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
            Assert.notNull(tomcat, "Tomcat Server must not be null");
            this.tomcat = tomcat;
            this.autoStart = autoStart;
            
            //初始化嵌入式Tomcat容器,並啟動Tomcat
     initialize();
        }
        
        private void initialize() throws EmbeddedServletContainerException {
            TomcatEmbeddedServletContainer.logger
                    .info("Tomcat initialized with port(s): " + getPortsDescription(false));
            synchronized (this.monitor) {
                try {
                    addInstanceIdToEngineName();
                    try {
                        final Context context = findContext();
                        context.addLifecycleListener(new LifecycleListener() {
    
                            @Override
                            public void lifecycleEvent(LifecycleEvent event) {
                                if (context.equals(event.getSource())
                                        && Lifecycle.START_EVENT.equals(event.getType())) {
                                    // Remove service connectors so that protocol
                                    // binding doesn't happen when the service is
                                    // started.
                                    removeServiceConnectors();
                                }
                            }
    
                        });
    
                        // Start the server to trigger initialization listeners
                        //啟動tomcat
                        this.tomcat.start(); // We can re-throw failure exception directly in the main thread
                        rethrowDeferredStartupExceptions();
    
                        try {
                            ContextBindings.bindClassLoader(context, getNamingToken(context),
                                    getClass().getClassLoader());
                        }
                        catch (NamingException ex) {
                            // Naming is not enabled. Continue
                        }
    
                        // Unlike Jetty, all Tomcat threads are daemon threads. We create a
                        // blocking non-daemon to stop immediate shutdown
                        startDaemonAwaitThread();
                    }
                    catch (Exception ex) {
                        containerCounter.decrementAndGet();
                        throw ex;
                    }
                }
                catch (Exception ex) {
                    stopSilently();
                    throw new EmbeddedServletContainerException(
                            "Unable to start embedded Tomcat", ex);
                }
            }
        }
    }

    到這裏就啟動了嵌入式的Servlet容器,其他容器類似。

    Servlet容器啟動原理

    SpringBoot啟動過程

    我們回顧一下前面講解的SpringBoot啟動過程,也就是run方法:

    public ConfigurableApplicationContext run(String... args) {
        // 計時工具
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
    
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
        configureHeadlessProperty();
    
        // 第一步:獲取並啟動監聽器
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    
            // 第二步:根據SpringApplicationRunListeners以及參數來準備環境
            ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
            configureIgnoreBeanInfo(environment);
    
            // 準備Banner打印器 - 就是啟動Spring Boot的時候打印在console上的ASCII藝術字體
            Banner printedBanner = printBanner(environment);
    
            // 第三步:創建Spring容器
            context = createApplicationContext();
    
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
    
            // 第四步:Spring容器前置處理
            prepareContext(context, environment, listeners, applicationArguments,printedBanner);
    
            // 第五步:刷新容器
     refreshContext(context);
    
         // 第六步:Spring容器後置處理
            afterRefresh(context, applicationArguments);
    
          // 第七步:發出結束執行的事件
            listeners.started(context);
            // 第八步:執行Runners
            this.callRunners(context, applicationArguments);
            stopWatch.stop();
            // 返回容器
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, exceptionReporters, ex);
            throw new IllegalStateException(ex);
        }
    }

    我們回顧一下第三步:創建Spring容器

    public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
                + "annotation.AnnotationConfigApplicationContext";
    
    public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."
                + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";
    
    protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                //根據應用環境,創建不同的IOC容器
                contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
            }
        }
        return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
    }

    創建IOC容器,如果是web應用,則創建
    AnnotationConfigEmbeddedWebApplicationContext的IOC容器;如果不是,則創建AnnotationConfigApplicationContext的IOC容器;很明顯我們創建的容器是AnnotationConfigEmbeddedWebApplicationContext
    接着我們來看看
    第五步,刷新容器
    refreshContext(context);

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
    }
    
    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
        //調用容器的refresh()方法刷新容器
     ((AbstractApplicationContext) applicationContext).refresh();
    }

    容器刷新過程

    調用抽象父類AbstractApplicationContext的refresh()方法;

    AbstractApplicationContext

     1 public void refresh() throws BeansException, IllegalStateException {
     2     synchronized (this.startupShutdownMonitor) {
     3         /**
     4          * 刷新上下文環境
     5          */
     6         prepareRefresh();
     7 
     8         /**
     9          * 初始化BeanFactory,解析XML,相當於之前的XmlBeanFactory的操作,
    10          */
    11         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    12 
    13         /**
    14          * 為上下文準備BeanFactory,即對BeanFactory的各種功能進行填充,如常用的註解@Autowired @Qualifier等
    15          * 添加ApplicationContextAwareProcessor處理器
    16          * 在依賴注入忽略實現*Aware的接口,如EnvironmentAware、ApplicationEventPublisherAware等
    17          * 註冊依賴,如一個bean的屬性中含有ApplicationEventPublisher(beanFactory),則會將beanFactory的實例注入進去
    18          */
    19         prepareBeanFactory(beanFactory);
    20 
    21         try {
    22             /**
    23              * 提供子類覆蓋的額外處理,即子類處理自定義的BeanFactoryPostProcess
    24              */
    25             postProcessBeanFactory(beanFactory);
    26 
    27             /**
    28              * 激活各種BeanFactory處理器,包括BeanDefinitionRegistryBeanFactoryPostProcessor和普通的BeanFactoryPostProcessor
    29              * 執行對應的postProcessBeanDefinitionRegistry方法 和  postProcessBeanFactory方法
    30              */
    31             invokeBeanFactoryPostProcessors(beanFactory);
    32 
    33             /**
    34              * 註冊攔截Bean創建的Bean處理器,即註冊BeanPostProcessor,不是BeanFactoryPostProcessor,注意兩者的區別
    35              * 注意,這裏僅僅是註冊,並不會執行對應的方法,將在bean的實例化時執行對應的方法
    36              */
    37             registerBeanPostProcessors(beanFactory);
    38 
    39             /**
    40              * 初始化上下文中的資源文件,如國際化文件的處理等
    41              */
    42             initMessageSource();
    43 
    44             /**
    45              * 初始化上下文事件廣播器,並放入applicatioEventMulticaster,如ApplicationEventPublisher
    46              */
    47             initApplicationEventMulticaster();
    48 
    49             /**
    50  * 給子類擴展初始化其他Bean 51              */
    52  onRefresh(); 53 
    54             /**
    55              * 在所有bean中查找listener bean,然後註冊到廣播器中
    56              */
    57             registerListeners();
    58 
    59             /**
    60              * 設置轉換器
    61              * 註冊一個默認的屬性值解析器
    62              * 凍結所有的bean定義,說明註冊的bean定義將不能被修改或進一步的處理
    63              * 初始化剩餘的非惰性的bean,即初始化非延遲加載的bean
    64              */
    65             finishBeanFactoryInitialization(beanFactory);
    66 
    67             /**
    68              * 通過spring的事件發布機制發布ContextRefreshedEvent事件,以保證對應的監聽器做進一步的處理
    69              * 即對那種在spring啟動后需要處理的一些類,這些類實現了ApplicationListener<ContextRefreshedEvent>,
    70              * 這裏就是要觸發這些類的執行(執行onApplicationEvent方法)
    71              * spring的內置Event有ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、RequestHandleEvent
    72              * 完成初始化,通知生命周期處理器lifeCycleProcessor刷新過程,同時發出ContextRefreshEvent通知其他人
    73              */
    74             finishRefresh();
    75         }
    76 
    77         finally {
    78     
    79             resetCommonCaches();
    80         }
    81     }
    82 }

    我們看第52行的方法:

    protected void onRefresh() throws BeansException {
    
    }

    很明顯抽象父類AbstractApplicationContext中的onRefresh是一個空方法,並且使用protected修飾,也就是其子類可以重寫onRefresh方法,那我們看看其子類AnnotationConfigEmbeddedWebApplicationContext中的onRefresh方法是如何重寫的,AnnotationConfigEmbeddedWebApplicationContext又繼承EmbeddedWebApplicationContext,如下:

    public class AnnotationConfigEmbeddedWebApplicationContext extends EmbeddedWebApplicationContext {

    那我們看看其父類EmbeddedWebApplicationContext 是如何重寫onRefresh方法的:

    EmbeddedWebApplicationContext

    @Override
    protected void onRefresh() {
        super.onRefresh();
        try {
            //核心方法:會獲取嵌入式的Servlet容器工廠,並通過工廠來獲取Servlet容器
     createEmbeddedServletContainer();
        }
        catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start embedded container", ex);
        }
    }

    在createEmbeddedServletContainer方法中會獲取嵌入式的Servlet容器工廠,並通過工廠來獲取Servlet容器:

     1 private void createEmbeddedServletContainer() {
     2     EmbeddedServletContainer localContainer = this.embeddedServletContainer;
     3     ServletContext localServletContext = getServletContext();
     4     if (localContainer == null && localServletContext == null) {
     5         //先獲取嵌入式Servlet容器工廠
     6         EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();  7         //根據容器工廠來獲取對應的嵌入式Servlet容器
     8         this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());  9     }
    10     else if (localServletContext != null) {
    11         try {
    12             getSelfInitializer().onStartup(localServletContext);
    13         }
    14         catch (ServletException ex) {
    15             throw new ApplicationContextException("Cannot initialize servlet context",ex);
    16         }
    17     }
    18     initPropertySources();
    19 }

    關鍵代碼在第6和第8行,先獲取Servlet容器工廠,然後根據容器工廠來獲取對應的嵌入式Servlet容器

    獲取Servlet容器工廠

    protected EmbeddedServletContainerFactory getEmbeddedServletContainerFactory() {
        //從Spring的IOC容器中獲取EmbeddedServletContainerFactory.class類型的Bean
        String[] beanNames = getBeanFactory().getBeanNamesForType(EmbeddedServletContainerFactory.class); //調用getBean實例化EmbeddedServletContainerFactory.class
        return getBeanFactory().getBean(beanNames[0], EmbeddedServletContainerFactory.class);
    }

    我們看到先從Spring的IOC容器中獲取EmbeddedServletContainerFactory.class類型的Bean,然後調用getBean實例化EmbeddedServletContainerFactory.class,大家還記得我們第一節Servlet容器自動配置類EmbeddedServletContainerAutoConfiguration中注入Spring容器的對象是什麼嗎?當我們引入spring-boot-starter-web這個啟動器后,會注入TomcatEmbeddedServletContainerFactory這個對象到Spring容器中,所以這裏獲取到的Servlet容器工廠是TomcatEmbeddedServletContainerFactory,然後調用

    TomcatEmbeddedServletContainerFactory的getEmbeddedServletContainer方法獲取Servlet容器,並且啟動Tomcat,大家可以看看文章開頭的getEmbeddedServletContainer方法。

    大家看一下第8行代碼獲取Servlet容器方法的參數getSelfInitializer(),這是個啥?我們點進去看看

    private ServletContextInitializer getSelfInitializer() {
        //創建一個ServletContextInitializer對象,並重寫onStartup方法,很明顯是一個回調方法
        return new ServletContextInitializer() { public void onStartup(ServletContext servletContext) throws ServletException { EmbeddedWebApplicationContext.this.selfInitialize(servletContext); } };
    }

    創建一個ServletContextInitializer對象,並重寫onStartup方法,很明顯是一個回調方法,這裏給大家留一點疑問:

    • ServletContextInitializer對象創建過程是怎樣的?
    • onStartup是何時調用的?
    • onStartup方法的作用是什麼?

    ServletContextInitializer是 Servlet 容器初始化的時候,提供的初始化接口。這裏涉及到Servlet、Filter實例的註冊,我們留在下一篇具體講

     

     

     

     

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • QQ是怎樣創造出來的?——解密好友系統的設計

    QQ是怎樣創造出來的?——解密好友系統的設計

    本篇介紹筆者接觸的第一個後台系統,從自身見聞出發,因此涉及的內容相對比較基礎,後台大牛請自覺略過。

    什麼是好友系統?

    簡單的說,好友系統是維護用戶好友關係的系統。我們最熟悉的好友系統案例當屬QQ,實際上QQ是一款即時通訊工具,憑着好友系統沉澱了海量的好友關係鏈,從而鑄就了一個堅不可摧的商業帝國。好友系統的重要性可見一斑。

    熟悉互聯網產品的人都知道,當產品有了一定的用戶量,往往會開發一個好友系統。其主要目的是增加用戶粘性(有了好友就會常來)或者增加社區活躍度(有了好友就會多交流)。

    而我的後台開發生涯就是從這樣一個系統開始的。

    那時候,好友系統對於我們團隊大部分人來說,都是一個全新的事物,因為我們大部分人都是應屆生。整個系統的架構自然不是我們一群黃毛小孩所能創造。當年的架構圖已經找不到了,但是憑着一點記憶和多年來的經驗積累,還是可以把當年的架構勾勒出來。

     

    如圖,好友系統的架構是常見的3層結構,包括接入層、邏輯層和數據層。

    我們先從數據層講起。

    因為我們對QQ太熟悉了,我們可以很容易地列出好友系統的數據主要包括用戶資料、好友關係鏈、消息(聊天消息和系統消息)、在線狀態等。

    互聯網產品往往要面對海量的請求併發,傳統的關係型數據庫比較難滿足讀寫需求。在存儲中,一般是讀多寫少的數據才會使用MySQL等關係型數據庫,而且往往還需要增加緩存來保證性能;NoSQL(Not Only SQL)應該是目前的主流。

    對於好友系統,用戶資料和好友關係鏈都使用了kv存儲,而消息使用公司自研的tlist(可以用redis的list替代),在線狀態下面再介紹。

    接着是邏輯層

    在這個系統中複雜度最高的應該是消息服務(而這個服務我並沒有參与開發[捂臉])。

    消息服務中,消息按類型分為聊天消息和系統消息(系統消息包括加好友消息、全局tips推送等),按狀態分為在線消息和離線消息。在實現中,維護3種list:聊天消息、系統消息和離線消息。聊天消息是兩個用戶共享的,系統消息和離線消息每個用戶獨佔。當用戶在線時,聊天消息和系統消息是直接發送的;如果用戶離線,就把消息往離線消息list存入一份,等用戶再次登錄時拉取。

    這樣看來,消息服務並不複雜?其實不然,系統設計中常規的流程設計往往是比較簡單的,但是對於互聯網產品,異常情況才是常態,當把各種異常情況都考慮進來時,系統就會非常複雜。

    這個例子中,消息發送丟包是一種異常情況,怎麼保證在丟包情況下,還能正常運行就是一個不小的問題。

    常見的解決方法是收包方回復確認包,發送方如果沒收到確認包就重發。但是確認包又可能丟包,那又可以給確認包增加一個確認包,這是一個永無止境的確認。

    解決方法可以參考TCP的重傳機制。那問題來了,我們為什麼不用TCP呢?因為TCP還是比較慢的,聊天消息的可靠性沒有交易數據要求那麼高,丟幾條消息並不會造成嚴重後果,但是如果用戶每次發送消息后都要等很久才能被收到,那體驗是很差的。

    一個比較折中的方案是,收包方回復確認包,如果發送方在一定時間內沒有收到確認就重發;如果收包方收到兩個相同的包(自定義seq一樣),去重即可。

    一個面試題引發的討論:

    面試時我常常會問候選人一個問題:在分佈式系統中怎樣實現一個用戶同時只能有一個終端在線(用戶在兩個地方先後登錄賬號,后一次登錄可以把前一次登錄踢下線)?這是互聯網產品中非常基礎的一個功能,考察的是候選人基本的架構設計能力。

    設計要先從接入服務器(下稱接口機)說起。接口機是好友系統對外的窗口,主要功能是維護用戶連接、登錄鑒權、加解密數據和向後端服務透傳數據等。用戶連接好友系統,首先是連接到接口機,鑒權成功后,接口機會在內存中維護用戶session,後續的操作都是基於session進行。

    如圖所示,用戶如果嘗試登錄兩次,接口機通過session就可以將第一次的登錄踢下線,從而保證只有一個終端在線。

    問題解決了嗎?

    沒有。因為實際系統肯定不會只有一台接口機,在多台接口的情況下,上面的方法就不可行了。因為每個接口機只能維護部分用戶的session,所以如果用戶先後連接到不同的接口機,就會造成用戶多處登錄的問題。

     

    自然可以想到,解決的方法就是要維護一個用戶狀態的全局視圖。在我們的好友系統中,稱為在線狀態服務。

    在線狀態服務,顧名思義就是維護用戶的在線狀態(登錄時間、接口機IP等)的服務。用戶登錄和退出會通過接口機觸發這裏的狀態變更。因為登錄包和退出包都可能丟包,所以心跳包也用作在線狀態維護(收到一次心跳標記為在線,收不到n次心跳標記為離線)。

    一種常用的方法是,採用bitmap存儲在線狀態,具體是指在內存中分配一塊空間,32位機器上的自然數一共有4294967296個,如果用一個bit來表示一個用戶ID(例如QQ號),1代表在線,0代表離線,那麼把全部自然數存儲在內存只要4294967296 / (8 * 1024 * 1024) = 512MB(8bit = 1Byte)。當然,實現中也可以根據需要給每個用戶分配更多的bit。

    於是,踢下線功能如圖所示。

     

    用戶登錄的時候,接口機首先查找本機上是否有session,如果有則更新session,接着給在線狀態服務發送登錄包,在線狀態服務檢查用戶是否已經在線,如果在線則更新狀態信息,並向上次登錄的接口機IP發送踢下線包;接口機在收到踢下線包時會檢查包中的用戶ID是否存在session,如果存在則給客戶端發送踢下線包並刪除session。

    在實際中,踢下線功能還有很多細節問題需要注意。

    又回到用戶先後登錄同一台接口機的情況:

     

    圖中踢下線流程是正確的,但是如果步驟10和13調換了順序(在UDP傳輸中是常見的)會發生什麼?大家可以自己推演一下,後到的踢下線包會把第二次登錄的A’踢下線了。這不是我們期望的。怎麼辦呢?

    解決方法分幾個細節,①接口機在收到13號登錄成功包時,先將session A替換成session A’,然後給客戶端A發生踢下線包(避免多處存活導致互相踢下線);②踢下線包中必須包含除用戶ID外的其他標識信息,session的唯一標識應該是ID+XXX的形式(我最開始採用的是ID+LoginTime),XXX是為了區分某次的登錄;③接口機在收到踢下線包的時候只要判斷ID+XXX是否吻合來決定是否給客戶端發踢下線包。

    現實情況,問題總是千奇百怪的,好在辦法總比問題多。

    比如我在項目中遇到過接口機和在線狀態服務時間漂移(差幾秒)的情況。這樣踢下線的唯一標識就不能是用戶ID+LoginTime的形式了。可以為每次的登錄生成一個唯一的UUID解決。類似的問題還有很多,不再贅述。

    總結一下,本篇主要介紹了好友系統的整體架構和部分模塊的實現方式。分佈式系統中各個模塊的實現其實並不難,難點主要在於應對複雜網絡環境帶來的問題(如丟包、時延等)和服務器異常帶來的問題(如為了應對服務器宕機會增加服務器冗餘度,進而又會引發其它問題)。

    好友系統雖然簡單,但麻雀雖小五臟俱全,架構設計的各種技術基本都有涉及。例如分層結構、負載均衡、平行擴展、容災、服務發現、服務器開發框架等方面,後面我會在各個不同的項目中介紹這些技術,敬請期待。

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

  • Cesium坐標系及坐標轉換詳解

    Cesium坐標系及坐標轉換詳解

    前言

    Cesium項目中經常涉及到模型加載、瀏覽以及不同數據之間的坐標轉換,弄明白Cesium中採用的坐標系以及各個坐標系之間的轉換,是我們邁向三維GIS大門的前提,本文詳細的介紹了Cesium中採用的兩大坐標系以及之間轉換的各種方法。

    Cesium中的坐標系

    Cesium中常用的坐標有兩種WGS84地理坐標系和笛卡爾空間坐標系,我們平時常用的以經緯度來指明一個地點就是用的WGS84坐標,笛卡爾空間坐標系常用來做一些空間位置變換如平移旋轉縮放等等。二者的聯繫如下圖。

    其中,WGS84地理坐標系包括 WGS84經緯度坐標系(沒有實際的對象)和 WGS84弧度坐標系(Cartographic);

             笛卡爾空間坐標系包括 笛卡爾空間直角坐標系(Cartesian3)、平面坐標系(Cartesian2),4D笛卡爾坐標系(Cartesian4)。

    WGS84坐標系

    World Geodetic System 1984,是為GPS全球定位系統使用而建立的坐標系統,坐標原點為地球質心,其地心空間直角坐標系的Z軸指向BIH (國際時間服務機構)1984.O定義的協議地球極(CTP)方向,X軸指向BIH 1984.0的零子午面和CTP赤道的交點,Y軸與Z軸、X軸垂直構成右手坐標系。我們平常手機上的指南針显示的經緯度就是這個坐標系下當前的坐標,進度範圍[-180,180],緯度範圍[-90,90]。

    WGS84坐標系

    Cesium目前支持兩種坐標系WGS84和WebMercator,但是在Cesium中沒有實際的對象來描述WGS84坐標,都是以弧度的方式來進行運用的也就是Cartographic類:

    new Cesium.Cartographic(longitude, latitude, height),這裏的參數也叫longitude、latitude,就是經度和緯度,計算方法:弧度= π/180×經緯度角度。

     笛卡爾空間直角坐標系(Cartesian3)

    笛卡爾空間坐標的原點就是橢球的中心,我們在計算機上進行繪圖時,不方便使用經緯度直接進行繪圖,一般會將坐標系轉換為笛卡爾坐標系,使用計算機圖形學中的知識進行繪圖。這裏的Cartesian3,有點類似於三維繫統中的Point3D對象,new Cesium.Cartesian3(x, y, z),裏面三個分量x、y、z。

    笛卡爾空間直角坐標系

    平面坐標系(Cartesian2)

    平面坐標系也就是平面直角坐標系,是一個二維笛卡爾坐標系,與Cartesian3相比少了一個z的分量,new Cesium.Cartesian2(x, y)。Cartesian2經常用來描述屏幕坐標系,比如鼠標在電腦屏幕上的點擊位置,返回的就是Cartesian2,返回了鼠標點擊位置的xy像素點分量。

    平面坐標系

    坐標轉換

    經緯度和弧度的轉換

    var radians=Cesium.Math.toRadians(degrees);//經緯度轉弧度
    var degrees=Cesium.Math.toDegrees(radians);//弧度轉經緯度

    WGS84經緯度坐標和WGS84弧度坐標系(Cartographic)的轉換

    //方法一:
    var longitude = Cesium.Math.toRadians(longitude1); //其中 longitude1為角度
    
    var latitude= Cesium.Math.toRadians(latitude1); //其中 latitude1為角度
    
    var cartographic = new Cesium.Cartographic(longitude, latitude, height);
    
    //方法二:
    var cartographic= Cesium.Cartographic.fromDegrees(longitude, latitude, height);//其中,longitude和latitude為角度
    
    //方法三:
    var cartographic= Cesium.Cartographic.fromRadians(longitude, latitude, height);//其中,longitude和latitude為弧度

    WGS84坐標系和笛卡爾空間直角坐標系(Cartesian3)的轉換

    通過經緯度或弧度進行轉換
    var position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);//其中,高度默認值為0,可以不用填寫;longitude和latitude為角度
    
    var positions = Cesium.Cartesian3.fromDegreesArray(coordinates);//其中,coordinates格式為不帶高度的數組。例如:[-115.0, 37.0, -107.0, 33.0]
    
    var positions = Cesium.Cartesian3.fromDegreesArrayHeights(coordinates);//coordinates格式為帶有高度的數組。例如:[-115.0, 37.0, 100000.0, -107.0, 33.0, 150000.0]
    
    //同理,通過弧度轉換,用法相同,具體有Cesium.Cartesian3.fromRadians,Cesium.Cartesian3.fromRadiansArray,Cesium.Cartesian3.fromRadiansArrayHeights等方法

    注意:上述轉換函數中最後均有一個默認參數ellipsoid(默認值為Ellipsoid.WGS84)。

    通過過度進行轉換

    具體過度原理可以參考上邊的注意事項。

    var position = Cesium.Cartographic.fromDegrees(longitude, latitude, height);
    var positions = Cesium.Ellipsoid.WGS84.cartographicToCartesian(position);
    var positions = Cesium.Ellipsoid.WGS84.cartographicArrayToCartesianArray([position1,position2,position3]);

    笛卡爾空間直角坐標系轉換為WGS84

    直接轉換
    var cartographic= Cesium.Cartographic.fromCartesian(cartesian3);

    轉換得到WGS84弧度坐標系后再使用經緯度和弧度的轉換,進行轉換到目標值

    間接轉換
    var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(cartesian3);
    var cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray([cartesian1,cartesian2,cartesian3]);

    平面坐標系(Cartesian2)和笛卡爾空間直角坐標系(Cartesian3)的轉換

    平面坐標系轉笛卡爾空間直角坐標系

    這裏注意的是當前的點(Cartesian2)必須在三維球上,否則返回的是undefined;通過ScreenSpaceEventHandler回調會取到的坐標都是Cartesian2。

    屏幕坐標轉場景坐標-獲取傾斜攝影或模型點擊處的坐標

    這裏的場景坐標是包含了地形、傾斜攝影表面、模型的坐標。

    通過viewer.scene.pickPosition(movement.position)獲取,根據窗口坐標,從場景的深度緩衝區中拾取相應的位置,返回笛卡爾坐標。

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var position = viewer.scene.pickPosition(movement.position);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:若屏幕坐標處沒有傾斜攝影表面、模型時,獲取的笛卡爾坐標不準,此時要開啟地形深度檢測(viewer.scene.globe.depthTestAgainstTerrain = true; //默認為false)。

    屏幕坐標轉地表坐標-獲取加載地形后對應的經緯度和高程

    這裡是地球表面的世界坐標,包含地形,不包括模型、傾斜攝影表面。

    通過viewer.scene.globe.pick(ray, scene)獲取,其中ray=viewer.camera.getPickRay(movement.position)。

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var ray = viewer.camera.getPickRay(movement.position);
         var position = viewer.scene.globe.pick(ray, viewer.scene);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:通過測試,此處得到的坐標通過轉換成wgs84后,height的為該點的地形高程值。

    屏幕坐標轉橢球面坐標-獲取鼠標點的對應橢球面位置

    這裏的橢球面坐標是參考橢球的WGS84坐標(Ellipsoid.WGS84),不包含地形、模型、傾斜攝影表面。

    通過 viewer.scene.camera.pickEllipsoid(movement.position, ellipsoid)獲取,可以獲取當前點擊視線與橢球面相交處的坐標,其中ellipsoid是當前地球使用的橢球對象:viewer.scene.globe.ellipsoid,默認為Ellipsoid.WGS84

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var position = viewer.scene.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:通過測試,此處得到的坐標通過轉換成wgs84后,height的為0(此值應該為地表坐標減去地形的高程)。

    笛卡爾空間直角坐標系轉平面坐標系

    var cartesian2= Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene,cartesian3)

    空間位置變換

    經緯度轉換到笛卡爾坐標系后就能運用計算機圖形學中的仿射變換知識進行空間位置變換如平移旋轉縮放。

    Cesium為我們提供了很有用的變換工具類:Cesium.Cartesian3(相當於Point3D)Cesium.Matrix3(3×3矩陣,用於描述旋轉變換)Cesium.Matrix4(4×4矩陣,用於描述旋轉加平移變換),Cesium.Quaternion(四元數,用於描述圍繞某個向量旋轉一定角度的變換)。

    下面舉個例子:

          一個局部坐標為p1(x,y,z)的點,將它的局部坐標原點放置到loc(lng,lat,alt)上,局部坐標的z軸垂直於地表,局部坐標的y軸指向正北,並圍繞這個z軸旋轉d度,求此時p1(x,y,z)變換成全局坐標笛卡爾坐p2(x1,y1,z1)是多少?

    var rotate = Cesium.Math.toRadians(d);//轉成弧度
    var quat = Cesium.Quaternion.fromAxisAngle(Cesium.Cartesian3.UNIT_Z, rotate); //quat為圍繞這個z軸旋轉d度的四元數
    var rot_mat3 = Cesium.Matrix3.fromQuaternion(quat);//rot_mat3為根據四元數求得的旋轉矩陣
    var v = new Cesium.Cartesian3(x, y, z);//p1的局部坐標
    var m = Cesium.Matrix4.fromRotationTranslation(rot_mat3, Cesium.Cartesian3.ZERO);//m為旋轉加平移的4x4變換矩陣,這裏平移為(0,0,0),故填個Cesium.Cartesian3.ZERO
    m = Cesium.Matrix4.multiplyByTranslation(m, v);//m = m X v
    var cart3 = ellipsoid.cartographicToCartesian(Cesium.Cartographic.fromDegrees(lng, lat, alt)); //得到局部坐標原點的全局坐標
    var m1 = Cesium.Transforms.eastNorthUpToFixedFrame(cart3);//m1為局部坐標的z軸垂直於地表,局部坐標的y軸指向正北的4x4變換矩陣
    m = Cesium.Matrix4.multiplyTransformation(m, m1);//m = m X m1
    var p2 = Cesium.Matrix4.getTranslation(m);//根據最終變換矩陣m得到p2
    console.log('x=' + p2.x + ',y=' + p2.y + ',z=' + p2.z );

    總結

    通過本文,介紹了各個坐標系間的轉換問題,在具體項目中,可結合實際需求,靈活組合解決具體的實際問題。注意,博文是參照網上相關博客及結合自己的實踐總結得來,希望本文對你有所幫助,後續會更新更多內容,感興趣的朋友可以加關注,歡迎留言交流!

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

    【其他文章推薦】

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

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

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

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

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

    ※試算大陸海運運費!

  • 詳解JavaScript錯誤捕獲和上報流程

    詳解JavaScript錯誤捕獲和上報流程

     

     

     

     

    怎麼捕獲錯誤並且處理,是一門語言必備的知識。在JavaScript中也是如此。

    那怎麼捕獲錯誤呢?初看好像很簡單,try-catch就可以了嘛!但是有的時候我們發現情況卻繁多複雜。

    • Q1: 同步可以try-catch,但一個異步回調,比如setTimeOut里的函數還可以try-catch嗎?

    • Q2: Promise的錯誤捕獲怎麼做?

    • Q3: async/await怎麼捕獲錯誤?

    • Q4: 我能夠在全局環境下捕獲錯誤並且處理嗎?

    • Q5: React16有什麼新的錯誤捕獲方式嗎?

    • Q6: 捕獲之後怎麼上報和處理?

     

    問題有點多,我們一個一個來。

     

    Q1. 同步代碼里的錯誤捕獲方式

    在同步代碼里,我們是最簡單的,只要try-catch就完了 

    function test1 () {
      try {
        throw Error ('callback err');
      } catch (error) {
        console.log ('test1:catch err successfully');
      }
    }
    test1();

    輸出結果如下,顯然是正常的

    Q2. 普通的異步回調里的錯誤捕獲方式(Promise時代以前)

    上面的問題來了,我們還能通過直接的try-catch在異步回調外部捕獲錯誤嗎?我們試一試 

    // 嘗試在異步回調外部捕獲錯誤的結果
    function test2 () {
      try {
        setTimeout (function () {
          throw Error ('callback err');
        });
      } catch (error) {
        console.log ('test2:catch err successfully');
      }
    }
    test2(); 

    輸出

    注意這裏的Uncaught Error的文本,它告訴我們錯誤沒有被成功捕捉。

    為什麼呢? 因為try-catch的是屬於同步代碼,它執行的時候,setTimeOut內部的的匿名函數還沒有執行呢。而內部的那個匿名函數執行的時候,try-catch早就執行完了。( error的內心想法:哈哈,只要我跑的夠慢,try-catch還是追不上我!)

    但是我們簡單想一想,誒我們把try-catch寫到函數裏面不就完事了嘛!

     

     

    function test2_1 () {
      setTimeout (function () {
        try {
          throw Error ('callback err');
        } catch (error) {
          console.log ('test2_1:catch err successfully');
        }
      });
    }
    test2_1();

    輸出結果如下,告訴我們這方法可行

     

    總結下Promise時代以前,異步回調中捕獲和處理錯誤的方法

    • 在異步回調內部編寫try-catch去捕獲和處理,不要在外部哦

    • 很多異步操作會開放error事件,我們根據事件去操作就可以了

    Q3. Promise里的錯誤捕獲方式

    可通過Promise.catch方法捕獲

    function test3 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      }).catch (err => {
        console.log ('promise error');
      });
    }

    輸出結果

    >> reject方法調用和throw Error都可以通過Promise.catch方法捕獲

    function test4 () {
      new Promise ((resolve, reject) => {
        reject ('promise reject error');
      }).catch (err => {
        console.log (err);
      });
    } 

    輸出結果

     

    >> then方法中的失敗回調和Promise.catch的關係

    • 如果前面的then方法沒寫失敗回調,失敗時後面的catch是會被調用的

    • 如果前面的then方法寫了失敗回調,又沒拋出,那麼後面的catch就不會被調用了

    // then方法沒寫失敗回調
    function test5 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      })
        .then (success => {})
        .catch (err => {
          console.log ('the error has not been swallowed up');
        });
    }
    // then方法寫了失敗回調
    function test5 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      })
        .then (success => {},err => {})
        .catch (err => {
          console.log ('the error has not been swallowed up');
        });
    }

    輸出分別為

    1.the error has not been swallowed up
    2.無輸出

    Q4.async/await里的錯誤捕獲方式

    對於async/await這種類型的異步,我們可以通過try-catch去解決

    async function test6 () {
      try {
        await getErrorP ();
      } catch (error) {
        console.log ('async/await error with throw error');
      }
    }
     
    function getErrorP () {
      return new Promise ((resolve, reject) => {
        throw Error ('promise error');
      });
    }
    test6();
    
    

    輸出結果如下

     

    >> 如果被await修飾的Promise因為reject調用而變化,它也是能被try-catch的

    (我已經證明了這一點,但是這裏位置不夠,我寫不下了)

    Q5.在全局環境下如何監聽錯誤

    window.onerror可以監聽全局錯誤,但是很顯然錯誤還是會拋出

    window.onerror = function (err) {
      console.log ('global error');
    };
    throw Error ('global error');

     

    輸出如下

     

    Q6.在React16以上如何監聽錯誤

    >> componentDidCatch和getDerivedStateFromError鈎子函數

    class Bar extends React.Component {
      // 監聽組件錯誤
      componentDidCatch(error, info) {
        this.setState({ error, info });
      }
      // 更新 state 使下一次渲染能夠显示降級后的 UI
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
      render() {
      }
    }

     

     

    有錯誤,那肯定要上報啊!不上報就發現不了Bug這個樣子。Sentry這位老哥就是個人才,日誌記錄又好看,每次見面就像回家一樣

     

     

    Sentry簡單介紹

    Sentry provides open-source and hosted error monitoring that helps all software
    teams discover, triage, and prioritize errors in real-time.
    One million developers at over fifty thousand companies already ship
    better software faster with Sentry. Won’t you join them?
    —— Sentry官網

     

    Sentry是一個日誌上報系統,Sentry 是一個實時的日誌記錄和匯總處理的平台。專註於錯誤監控,發現和數據處理,可以讓我們不再依賴於用戶反饋才能發現和解決線上bug。讓我們簡單看一下Sentry支持哪些語言和平台吧

     

    在JavaScript領域,Sentry的支持也可以說是面面俱到

     

    參考鏈接
    https://docs.sentry.io/platforms/ 

    Sentry的功能簡單說就是,你在代碼中catch錯誤,然後調用Sentry的方法,然後Sentry就會自動幫你分析和整理錯誤日誌,例如下面這張圖截取自Sentry的網站中

     

    在JavaScript中使用Sentry 

    1.首先呢,你當然要註冊Sentry的賬號

    這個時候Sentry會自動給你分配一個唯一標示,這個標示在Sentry里叫做 dsn

    2. 安卓模塊並使用基礎功能

    安裝@sentry/browser 

    npm install @sentry/browser

     

    在項目中初始化並使用

    import * as Sentry from '@sentry/browser';
     
    Sentry.init ({
      dsn: 'xxxx',
    });
     
    try {
      throw Error ('我是一個error');
    } catch (err) {
        // 捕捉錯誤
      Sentry.captureException (err);
    }

    3.上傳sourceMap以方便在線上平台閱讀出錯的源碼

     

    // 安裝
    $ npm install --save-dev @sentry/webpack-plugin
    $ yarn add --dev @sentry/webpack-plugin
     
    // 配置webpack
    const SentryWebpackPlugin = require('@sentry/webpack-plugin');
    module.exports = {
      // other configuration
      plugins: [
        new SentryWebpackPlugin({
          include: '.',
          ignoreFile: '.sentrycliignore',
          ignore: ['node_modules', 'webpack.config.js'],
          configFile: 'sentry.properties'
        })
      ]
    }; 

    4. 為什麼不是raven.js?

     

    // 已經廢棄,雖然你還是可以用
    var Raven = require('raven-js');
    Raven
      .config('xxxxxxxxxxx_dsn')
      .install();

     

    Sentry的核心功能總結

    捕獲錯誤

    try { aFunctionThatMightFail(); } catch (err) { Sentry.captureException(err); }

     

    設置該錯誤發生的用戶信息

    下面每個選項都是可選的,但必須 存在一個選項 才能使Sentry SDK捕獲用戶: id 

    Sentry.setUser({
        id:"penghuwan12314"
      email: "penghuwan@example.com",
      username:"penghuwan",
      ip_addressZ:'xxx.xxx.xxx.xxx'
      });

     

    設置額外數據

    Sentry.setExtra("character_name", "Mighty Fighter");
    設置作用域 
    Sentry.withScope(function(scope) {
        // 下面的set的效果只存在於函數的作用域內
      scope.setFingerprint(['Database Connection Error']);
      scope.setUser(someUser);
      Sentry.captureException(err);
    });
    // 在這裏,上面的setUser的設置效果會消失

     

    設置錯誤的分組

    整理日誌信息,避免過度冗餘 

    Sentry.configureScope(function(scope) {
      scope.setFingerprint(['my-view-function']);
    });

     

    設置錯誤的級別

    在閱讀日誌時可以確定各個bug的緊急度,確定排查的優先書序

    Sentry.captureMessage('this is a debug message', 'debug');
    //fatal,error,warning,info,debug五個值
    // fatal最嚴重,debug最輕

     

    自動記錄某些事件

    例如下面的方法,會在每次屏幕調整時完成上報 

    window.addEventListener('resize', function(event){
      Sentry.addBreadcrumb({
        category: 'ui',
        message: 'New window size:' + window.innerWidth + 'x' + window.innerHeight,
        level: 'info'
      });
    })

    Sentry實踐的運用

    根據環境設置不同的dsn

    let dsn;
      if (env === 'test') {
        dsn = '測試環境的dsn';
      } else {
        dsn =
          '正式環境的dsn';
      }
     
    Sentry.init ({
      dsn
    });

     

     

     

     

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • Java編程思想——第14章 類型信息(二)反射

    六、反射:運行時的類信息

      我們已經知道了,在編譯時,編譯器必須知道所有要通過RTTI來處理的類。而反射提供了一種機制——用來檢查可用的方法,並返回方法名。區別就在於RTTI是處理已知類的,而反射用於處理未知類。Class類與java.lang.reflect類庫一起對反射概念進行支持,該類庫包含Field、Method以及Constructor(每個類都實現了Member接口)。這些類型是由JVM運行時創建的,用來表示未知類種對應的成員。使用Constructor(構造函數)創建新的對象,用get(),set()方法讀取和修改與Field對象(字段)關聯的字段,用invoke()方法調用與Method對象(方法)關聯的方法。這樣,匿名對象的類信息就能在運行時被完全確定下來,而在編譯時不需要知道任何事情。

      其實,當反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,在做其他事情之前必須先加載這個類的Class對象。因此,那個類的.class文件對於JVM來說必須時可獲取的(在本地或網絡上)所以反射與RTTI的區別只在於:對於RTTI來說,編譯器在編譯時打開和檢查.class文件,而對於反射來說,.class文件在編譯時是不可獲得的,所以是運行時打開和檢查.class文件。反射在需要創建更動態的代碼時很有用。

    七、動態代理

      代理是基本的設計模式:為其他對象提供一種代理,以便控制對象,而在對象前或后加上自己想加的東西。

    interface Interface {
        void doSomething();
    
        void doSomeOtherThing(String args);
    }
    
    class RealObject implements Interface {
    
        @Override
        public void doSomething() {
            System.out.println("doSomething");
        }
    
        @Override
        public void doSomeOtherThing(String args) {
            System.out.println("doSomeOtherThing" + args);
        }
    }
    
    class SimpleProxy implements Interface {
    
        private Interface proxyId;
    
        public SimpleProxy(Interface proxyId) {
            this.proxyId = proxyId;
        }
    
        @Override
        public void doSomething() {
            //將原有的doSomething 方法添加上了一個輸出 這就是代理之後新增的東西
            //就好比某公司代理遊戲后加的內購
            System.out.println("SimpleProxy doSomething");
            proxyId.doSomething();
        }
    
        @Override
        public void doSomeOtherThing(String args) {
            proxyId.doSomeOtherThing(args);
            //新增的東西可以在原有之前或之後都行
            System.out.println("SimpleProxy doSomeOtherThing" + args);
        }
    }
    
    public class SimpleProxyDemo {
        static void consumer(Interface i) {
            i.doSomething();
            i.doSomeOtherThing(" yi gi woli giao");
        }
    
        public static void main(String[] args) {
            consumer(new RealObject());
            System.out.println("-----  -----  -----");
            consumer(new SimpleProxy(new RealObject()));
        }
    }

    結果:

    doSomething
    doSomeOtherThing yi gi woli giao
    -----  -----  -----
    SimpleProxy doSomething
    doSomething
    doSomeOtherThing yi gi woli giao
    SimpleProxy doSomeOtherThing yi gi woli giao

      因為consumer()接受的Interface,所以無論是RealObject還是SimpleProxy,都可以作為參數,而SimpleProxy插了一腳 代理了RealObject加了不少自己的東西。

      java的動態代理更前進一步,因為它可以動態創建代理並動態地處理對所代理方法的調用。在動態代理上所做的所有調用都會被重定向到單一的調用處理器上,它的工作是揭示調用的類型並確定相應的對策。

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    interface Interface {
        void doSomething();
    
        void doSomeOtherThing(String args);
    }
    
    class RealObject implements Interface {
    
        @Override
        public void doSomething() {
            System.out.println("doSomething");
        }
    
        @Override
        public void doSomeOtherThing(String args) {
            System.out.println("doSomeOtherThing" + args);
        }
    }
    
    class DynamicProxyHandler implements InvocationHandler {
        private Object proxyId;
    
        public DynamicProxyHandler(Object proxyId) {
            this.proxyId = proxyId;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("**** proxy:" + proxy.getClass() + ", method" + method + ", args:" + args);
            if (args != null) {
                for (Object arg : args) {
                    System.out.println(" " + arg);
                }
            }
            return method.invoke(proxyId, args);
        }
    }
    
    public class SimpleProxyDemo {
        static void consumer(Interface i) {
            i.doSomething();
            i.doSomeOtherThing(" yi gi woli giao");
        }
    
        public static void main(String[] args) {
            RealObject realObject = new RealObject();
            consumer(realObject);
            System.out.println("-----  -----  -----");
         //動態代理 可以代理任何東西 Interface proxy
    = (Interface) Proxy.newProxyInstance(Interface.class.getClassLoader(), new Class[]{Interface.class}, new DynamicProxyHandler(realObject)); consumer(proxy); } }

    結果:

    doSomething
    doSomeOtherThing yi gi woli giao
    -----  -----  -----
    **** proxy:class $Proxy0, methodpublic abstract void Interface.doSomething(), args:null
    doSomething
    **** proxy:class $Proxy0, methodpublic abstract void Interface.doSomeOtherThing(java.lang.String), 
    args:[Ljava.lang.Object;@7ea987ac  yi gi woli giao
    doSomeOtherThing yi gi woli giao

    通過Proxy.newProxyInstance()可以創建動態代理,這個方法需要三個參數:

    1. 類加載器:可以從已經被加載的對象中獲取其類加載器;

    2. 你希望該代理實現的接口列表(不可以是類或抽象類,只能是接口);

    3. InvocationHandler接口的一個實現;

    在 invoke 實現中還可以根據方法名處對不同的方法進行處理,比如:

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("**** proxy:" + proxy.getClass() + ", method" + method + ", args:" + args);
            if (args != null) {
                for (Object arg : args) {
                    System.out.println(" " + arg);
                }
            }
            if (method.getName().equals("doSomething")) { System.out.println("this is the proxy for doSomething"); } return method.invoke(proxyId, args);
        }

    還可以對參數或方法進行更多的操作因為 你已經得到了他們 盡情的使用你的代理權吧 ~~ 先加它十個內購。

    九、接口與類型信息

      interface關鍵字的一種重要目標就是允許程序員隔離構件,進而降低耦合。反射,可以調用所有方法,甚至是private。唯獨final是無法被修改的,運行時系統會在不拋任何異常的情況接受任何修改嘗試,但是實際上不會發生任何修改。

        void callMethod(Object a, String methodName) throws Exception {
            Method method = a.getClass().getDeclaredMethod(methodName);
            method.setAccessible(true);
            method.invoke(a);
        }

     

     

      

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

    【其他文章推薦】

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?