分類: 3C資訊

  • 正則表達式 第五篇:C# 正則表達式

    正則表達式 第五篇:C# 正則表達式

    本文整理C#正則表達式的元字符,正則表達式是由字符構成的表達式,每個字符代表一個規則,表達式中的字符分為兩種類型:普通字符和元字符。普通字符是指字面含義不變的字符,按照完全匹配的方式匹配文本,而元字符具有特殊的含義,代表一類字符。

    把文本看作是字符流,每個字符放在一個位置上,例如,正則表達式 “Room\d\d\d”,前面四個字符Room是普通字符,後面的字符\是轉義字符,和後面的字符d組成一個元字符\d,表示該位置上有任意一個数字。

    用正則表達式的語言來描述是:正則表達式 “Room\d\d\d”共捕獲7個字符,表示“以Room開頭、以三個数字結尾”的一類字符串,我們把這一類字符串稱作一個模式(Pattern),也稱作是一個正則。

    一,轉義字符

    轉義字符是\,把普通字符轉義為具有特殊含義的元字符,常用的轉義字符有:

    • \t:水平製表符
    • \v:垂直製表符
    • \r:回車
    • \n:換行
    • \\:表示字符 \,也就說,把轉義字符 \ 轉義為普通的字符 \
    • \”:表示字符 “,在C#中,雙引號用於定義字符串,字符串包含的雙引號用 \” 來表示

    二,字符類

    在進行正則匹配時,把輸入文本看作是有順序的字符流,字符類元字符匹配的對象是字符,並會捕獲字符。所謂捕獲字符是指,一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    常用的字符類元字符:

    • [ char_group]:匹配字符組中的任意一個字符
    • [^char_group]:匹配除字符組之外的任意一個字符
    • [first-last]:匹配從first到last的字符範圍中的任意一個字符,字符範圍包括first和last。
    • .   :通配符,匹配除\n之外的任意一個字符
    • \w:匹配任意一個單詞(word)字符,單詞字符通常是指A-Z、a-z和0-9
    • \W:匹配任意一個非單詞字符,是指除A-Z、a-z和0-9之外的字符
    • \s:匹配任意一個空白字符
    • \S:匹配任意一個非空白字符
    • \d:匹配任意一個数字字符
    • \D:匹配任意一個非数字字符

    注意,轉義字符也屬於字符類元字符,在進行正則匹配時,也會捕獲字符。

    三,定位符

    定位符匹配(或捕獲)的對象是位置,它根據字符的位置來判斷模式匹配是否成功,定位符不會捕獲字符,是零寬的(寬度為0),常用的定位符有:

    • ^:默認情況下,匹配字符串的開始位置;在多行模式下,匹配每行的開始位置;
    • $:默認情況下,匹配字符串的結束位置,或 字符串結尾的\n之前的位置;在多行模式下,匹配每行結束之前的位置,或者每行結尾的\n之前的位置。
    • \A:匹配字符串的開始位置;
    • \Z:匹配字符串的結束位置,或 字符串結尾的\n之前的位置;
    • \z:匹配字符串的結束位置;
    • \G:匹配上一個匹配結束的位置;
    • \b:匹配一個單詞的開始或結束的位置;
    • \B:匹配一個單詞的中間位置;

     

    四,量詞、貪婪和懶惰

    量詞是指限定前面的一個正則出現的次數,量詞分為兩種模式:貪婪模式和懶惰模式,貪婪模式是指匹配盡可能多的字符,而懶惰模式是指匹配盡可能少的字符。默認情況下,量詞處於貪婪模式,在量詞的後面加上?來啟用懶惰模式。

    • *:出現0次或多次
    • +:出現1次或多次
    • ?:出現0次或1次
    • {n}:出現n次
    • {n,}:出現至少n次
    • {n,m}:出現n到m次

    注意,出現多次是指前面的元字符出現多次,例如,\d{2} 等價於 \d\d,只是出現兩個数字,並不要求兩個数字是相同的。要表示相同的兩個数字,必須使用分組來實現。

    五,分組和捕獲字符

    ()  括號不僅確定表達式的範圍,還創建分組,()內的表達式就是一個分組,引用分組表示兩個分組匹配的文本是完全相同的。定義一個分組的基本語法:

    (pattern)

    該類型的分組會捕獲字符,所謂捕獲字符是指:一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    1,分組編號和命名

    默認情況下,每個分組自動分配一個組號,規則是:從左向右,按分組左括號的出現順序進行編號,第一個分組的組號為1,第二個為2,以此類推。也可以為分組指定名稱,該分組稱作命名分組,命名分組也會被自動編號,編號從1開始,逐個加1,為分組指定名稱的語法是:

    (?< name > pattern)

    通常來說,分組分為命名分組和編號分組,引用分組的方式有:

    • 通過分組名稱來引用分組:\k<name>
    • 通過分組編號來引用分組:\number

    注意,分組只能後向引用,也就是說,從正則表達式文本的左邊開始,分組必須先定義,然後才能在定義之後面引用。

    在正則表達式里引用分組的語法為“\number”,比如“\1”代表與分組1 匹配的子串,“\2”代表與分組2 匹配的字串,以此類推。

    例如,對於 “<(.*?)>.*?</\1>” 可以匹配 <h2>valid</h2>,在引用分組時,分組對應的文本是完全相同的。

    2,分組構造器

    分組構造方法如下:

    • (pattern):捕獲匹配的子表達式,併為分組分配一個組號
    • (?< name > pattern):把匹配的子表達式捕獲到命名的分組中
    • (?:pattern):非捕獲的分組,並未分組分配一個組號
    • (?> pattern):貪婪分組

    3,貪婪分組

    貪婪分組也稱作非回溯分組,該分組禁用了回溯,正則表達式引擎將盡可能多地匹配輸入文本中的字符。如果無法進行進一步的匹配,則不會回溯嘗試進行其他模式匹配。

    (?> pattern )

    4,二選一

    | 的意思是或,匹配兩者中的任意一個,注意,|把左右兩邊的表達式分為兩部分。

    pattern1 | pattern2

    六,零寬斷言

    零寬是指寬度為0,匹配的是位置,所以匹配的子串不會出現在匹配結果中,而斷言是指判斷的結果,只有斷言為真,才算匹配成功。

    對於定位符,可以匹配一句話的開始、結束(^ $)或者匹配一個單詞的開始、結束(\b),這些元字符只匹配一個位置,指定這個位置滿足一定的條件,而不是匹配某些字符,因此,它們被成為 零寬斷言。所謂零寬,指的是它們不與任何字符相匹配,而匹配一個位置;所謂斷言,指的是一個判斷,正則表達式中只有當斷言為真時才會繼續進行匹配。零寬斷言可以精確的匹配一個位置,而不僅僅是簡單的指定句子或者單詞。

    正則表達式把文本看作從左向右的字符流,向右叫做後向(Look behind),向左叫做前向(Look ahead)。對於正則表達式,只有當匹配到指定的模式(Pattern)時,斷言為True,叫做肯定式,把不匹配模式為True,叫做否定式。

    按照匹配的方向和匹配的定性,把零寬斷言分為四種類型:

    • (?= pattern):前向、肯定斷言
    • (?! pattern):前向、否定斷言
    • (?<= pattern):後向、肯定斷言
    • (?<! pattern):後向、否定斷言

     1,前向肯定斷言

    前向肯定斷言定義一個模式必須存在於文本的末尾(或右側),但是該模式匹配的子串不會出現在匹配的結果中,前向斷言通常出現在正則表達式的右側,表示文本的右側必須滿足特定的模式:

     (?= subexpression )

    使用前向肯定斷言可以定一個模糊匹配,後綴必須包含特定的字符:

    \b\w+(?=\sis\b)

    對正則表達式進行分析:

    • \b:表示單詞的邊界
    •  \w+:表示單詞至少出現一次
    • (?=\sis\b):前向肯定斷言,\s 表示一個空白字符, is 是普通字符,完全匹配,\b 是單詞的邊界。

    從分析中,可以得出,匹配該正則表達式的文本中必須包含 is 單詞,is是一個單獨的單詞,不是某一個單詞的一個部分。舉個例子

    Sunday is a weekend day 匹配該正則,匹配的值是Sunday,而The island has beautiful birds 不匹配該正則。

    2,後向肯定斷言

    後向肯定斷言定義一個模式必須存在於文本的開始(或左側),但是該模式匹配的子串不會出現在匹配的結果中,後向斷言通常出現在正則表達式的左側,表示文本的左側必須滿足特定的模式:

    (?<= subexpression )

    使用後向肯定斷言可以定一個模糊匹配,前綴必須包含特定的字符:

    (?<=\b20)\d{2}\b

    對正則表達式進行分析:

    • (?<=\b20):後向斷言,\b表示單詞的開始,20是普通字符
    • \d{2}:表示兩個数字,数字不要求相同
    • \b:單詞的邊界

    該正則表達式匹配的文本具備的模式是:文本以20開頭、以兩個数字結尾。

    七,用正則從格式化的文本中扣值

    有如下的JSON格式的文本,從文本中扣出字段(CustomerId、CustomerName、CustomerIdSource和CustomerType)的值:

    {"CustomerDetails":"[{\"CustomerId\":\"57512f19\",\"CustomerName\":\"cust xyz\",\"CustomerIdSource\":\"AadTenantId\",\"CustomerType\":\"Enterprise\"}]"}

    注意,該文本轉換為C#中的字符時,需要對雙引號和轉義字符進行轉義。由於這四個字段提取規則相同,可以寫一個通用的模式來提取:

    public static string GetNestedItem(string txt, string key)
    {
        string pat = string.Format("(?<=\\\\\"{0}\\\\\":\\\\\").*?(?=\\\\\")", key);
        return Regex.Match(txt, pat, RegexOptions.IgnoreCase).Value;
    }

    正則表達式得解析:

    • (?<=\\\\\”{0}\\\\\”:\\\\\”):後向斷言,用於匹配字段名、雙引號和冒號
    • .*?:懶惰模式,匹配盡可能少的文本
    • (?=\\\\\”):前向斷言,用於匹配字段值得雙引號

     

     

    參考文檔:

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

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

  • 【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    本文為 SnailClimb 的原創,目前已經收錄自我開源的 中(61.5 k Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。歡迎 Star!)。

    另外推薦一篇原創:

    Java 併發基礎常見面試題總結

    1. 什麼是線程和進程?

    1.1. 何為進程?

    進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。

    在 Java 中,當我們啟動 main 函數時其實就是啟動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。

    如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。

    1.2. 何為線程?

    線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器虛擬機棧本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。

    Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下。

    public class MultiThread {
        public static void main(String[] args) {
            // 獲取 Java 線程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            // 不需要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
            ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
            // 遍歷線程信息,僅打印線程 ID 和線程名稱信息
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
            }
        }
    }

    上述程序輸出如下(輸出內容可能不同,不用太糾結下面每個線程的作用,只用知道 main 線程執行 main 方法即可):

    [5] Attach Listener //添加事件
    [4] Signal Dispatcher // 分發處理給 JVM 信號的線程
    [3] Finalizer //調用對象 finalize 方法的線程
    [2] Reference Handler //清除 reference 線程
    [1] main //main 線程,程序入口

    從上面的輸出內容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行

    2. 請簡要描述線程與進程的關係,區別及優缺點?

    從 JVM 角度說進程和線程之間的關係

    2.1. 圖解進程和線程的關係

    下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關係。如果你對 Java 內存區域 (運行時數據區) 這部分知識不太了解的話可以閱讀一下這篇文章:

    從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器虛擬機棧本地方法棧

    總結: 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反

    下面是該知識點的擴展內容!

    下面來思考這樣一個問題:為什麼程序計數器虛擬機棧本地方法棧是線程私有的呢?為什麼堆和方法區是線程共享的呢?

    2.2. 程序計數器為什麼是私有的?

    程序計數器主要有下面兩個作用:

    1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

    需要注意的是,如果執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的才是下一條指令的地址。

    所以,程序計數器私有主要是為了線程切換后能恢復到正確的執行位置

    2.3. 虛擬機棧和本地方法棧為什麼是私有的?

    • 虛擬機棧: 每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
    • 本地方法棧: 和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。

    所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

    2.4. 一句話簡單了解堆和方法區

    堆和方法區是所有線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新創建的對象 (所有對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。

    3. 說說併發與并行的區別?

    • 併發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);
    • 并行: 單位時間內,多個任務同時執行。

    4. 為什麼要使用多線程呢?

    先從總體上來說:

    • 從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
    • 從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。

    再深入到計算機底層來探討:

    • 單核時代: 在單核時代多線程主要是為了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閑;進行 IO 操作時,CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
    • 多核時代: 多核時代多線程主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

    5. 使用多線程可能帶來什麼問題?

    併發編程的目的就是為了能提高程序的執行效率提高程序運行速度,但是併發編程並不總是能提高程序運行速度的,而且併發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閑置問題。

    6. 說說線程的生命周期和狀態?

    Java 線程在運行的生命周期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。

    線程在生命周期中並不是固定處於某一個狀態而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 併發編程藝術》4.1.4 節):

    由上圖可以看出:線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法后開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)后就處於 RUNNING(運行) 狀態。

    操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源::),所以 Java 系統一般將這兩個狀態統稱為 RUNNABLE(運行中) 狀態 。

    當線程執行 wait()方法之後,線程進入 WAITING(等待) 狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。

    7. 什麼是上下文切換?

    多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 採取的策略是為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。

    概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

    上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

    Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

    8. 什麼是線程死鎖?如何避免死鎖?

    8.1. 認識線程死鎖

    多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

    如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

    下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源於《併發編程之美》):

    public class DeadLockDemo {
        private static Object resource1 = new Object();//資源 1
        private static Object resource2 = new Object();//資源 2
    
        public static void main(String[] args) {
            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 1").start();
    
            new Thread(() -> {
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource1");
                    synchronized (resource1) {
                        System.out.println(Thread.currentThread() + "get resource1");
                    }
                }
            }, "線程 2").start();
        }
    }

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 2,5,main]get resource2
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 2,5,main]waiting get resource1

    線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

    學過操作系統的朋友都知道產生死鎖必須具備以下四個條件:

    1. 互斥條件:該資源任意一個時刻只由一個線程佔用。
    2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
    3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
    4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

    8.2. 如何避免線程死鎖?

    我們只要破壞產生死鎖的四個條件中的其中一個就可以了。

    破壞互斥條件

    這個條件我們沒有辦法破壞,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

    破壞請求與保持條件

    一次性申請所有的資源。

    破壞不剝奪條件

    佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。

    破壞循環等待條件

    靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

    我們對線程 2 的代碼修改成下面這樣就不會產生死鎖了。

            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 2").start();

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 1,5,main]get resource2
    Thread[線程 2,5,main]get resource1
    Thread[線程 2,5,main]waiting get resource2
    Thread[線程 2,5,main]get resource2
    
    Process finished with exit code 0

    我們分析一下上面的代碼為什麼避免了死鎖的發生?

    線程 1 首先獲得到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。然後線程 1 再去獲取 resource2 的監視器鎖,可以獲取到。然後線程 1 釋放了對 resource1、resource2 的監視器鎖的佔用,線程 2 獲取到就可以執行了。這樣就破壞了破壞循環等待條件,因此避免了死鎖。

    9. 說說 sleep() 方法和 wait() 方法區別和共同點?

    • 兩者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖
    • 兩者都可以暫停線程的執行。
    • Wait 通常被用於線程間交互/通信,sleep 通常被用於暫停執行。
    • wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成后,線程會自動蘇醒。或者可以使用wait(long timeout)超時后線程會自動蘇醒。

    10. 為什麼我們調用 start() 方法時會執行 run() 方法,為什麼我們不能直接調用 run() 方法?

    這是另一個非常經典的 java 多線程面試問題,而且在面試中會經常被問到。很簡單,但是很多人都會答不上來!

    new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啟動一個線程並使線程進入了就緒狀態,當分配到時間片后就可以開始運行了。 start() 會執行線程的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多線程工作。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,所以這並不是多線程工作。

    總結: 調用 start 方法方可啟動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,還是在主線程里執行。

    開源項目推薦

    作者的其他開源項目推薦:

    1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
    2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
    3. : 我覺得技術人員應該有的一些好習慣!
    4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

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

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • [NLP] Adaptive Softmax

    [NLP] Adaptive Softmax

    1. Overview

    Adaptive softmax算法在鏈接1中的論文中提出,該算法目的是為了提高softmax函數的運算效率,適用於一些具有非常大詞彙量的神經網絡。

    在NLP的大部分任務中,都會用到softmax,但是對於詞彙量非常大的任務,每次進行完全的softmax會有非常大的計算量,很耗時(每次預測一個token都需要O(|V|)的時間複雜度)。

    所以paper中提出adaptive softmax來提升softmax的運算效率。

    1) 該算法的提出利用到了單詞分佈不均衡的特點(unbalanced word distribution)來形成clusters, 這樣在計算softmax時可以避免對詞彙量大小的線性依賴關係,降低計算時間;

    2) 另外通過結合現代架構和矩陣乘積操作的特點,通過使其更適合GPU單元的方式進一步加速計算。

    2. Introduction

    2.1 一般降低softmax計算複雜度的兩種方式

    1) 考慮原始分佈:近似原始概率分佈或近似原始概率分佈的子集

    2) 構造近似模型,但是產生準確的概率分佈。比如:hierarchical softmax.

    (上述兩個方法可以大致區分為:一是準確的模型產生近似概率分佈,二是用近似模型產生準確的概率分佈)

    2.2 本文貢獻點

    paper中主要採用的上述(2)的方式,主要借鑒的是hierarchical softmax和一些變型。與以往工作的不同在於,本paper結合了GPU的特點進行了計算加速。主要

    1. 定義了一種可以生成 近似層次模型 的策略,該策略同時考慮了矩陣乘積運算的計算時間。這個計算時間並非與矩陣的維數是簡單線性關係。

    2. 在近來的GPU上對該模型進行了實證分析。在所提出的優化算法中也包含了對實際計算時間模型的定義。

    3. 與一般的softmax相比,有2× 到 10× 的加速。這等價於在同等算力限制下提高了準確度。另外一點非常重要,該paper所提出的這種高效計算的方法與一些通過并行提高效率的方法相比,在給定一定量的training data下,對準確度並沒有損失。

    3. Adaptive Softmax 

    3.1 矩陣乘積運算的計算時間模型

    hidden states: (B × d); Word representation: (d × k); 這兩個矩陣的乘積運算時間: g(k, B)。其中B是batch size, d: hidden layer 的size, k vectors的數量。

    (1) 固定B,d, 探究k值大小與g(k)的關係:

    在K40、 M40兩種GPU model中進行實驗,發現k在大約為$k_0 \approx 50$的範圍之內,g(k)是常數級別的,在$k > k_0$后,呈線性依賴關係。其計算模型為:

    同樣的,關於B的計算時間的函數也呈現這樣的關係。這可以表示,矩陣相乘時,當其中一個維度很小時,矩陣乘法是低效的。

    如何理解呢?比如對於 $k_1 = 2^2$, $k_2 = 2^4$,它們均在同樣的常量時間內完成運算,那麼顯然對於$k1$量級有算力被浪費了。

    那麼這也說明,一些words的層次結構,其每個節點只具有少量的子節點(比如huffman 編碼的tree),是次優的。

    (2) 探究batch size B值大小與g(B)的關係:

    同樣的,當探究計算用時與batch size $B$,的關係時,發現在矩陣乘法運算中,其中一個維度很小時計算並不高效。因此會導致:

    (i) 在層次結構中,因為每個節點只有少數一些孩子節點 (比如Huffman 樹),其計算效率是次優的;

    (ii) 對於按照詞頻劃分clusters后,那些只包含稀有詞的cluster,被選擇的概率為p, 並且其batch size也縮減為 $p B$,也會出現以上所述的低效的矩陣乘積運算問題。

    (3) 本文的計算模型

    所以paper中綜合考慮了k與B,提出了下面的計算模型:

    對於Adaptive Softmax, 其核心思想是根據詞頻大小,將不同詞頻區間的詞分為不同的clusters,按照詞頻高的優先訪問的原則,來對每個cluster整體,進而對cluster中的每個詞進行訪問,進行softmax操作。

    那麼paper中首先以2個clusters為例,對該模型進行講解,之後推廣到多個clusters的一般情況。

    3.2 Two-Clusters Case

    根據Zipf定律,在Penn TreeBank中 20%的詞,可以覆蓋一般文檔中87%的出現過的詞。

    直觀地,可以將字典$V$中的詞分為$V_h$、$V_t$兩個部分。其中$V_h$表示高頻詞集合(head),$V_t$表示低頻詞集合(tail)。一般地,$|V_h| << |V_t|$,$P(V_h) >> P(V_t)$。

    (1) Clusters 的組織

    直觀地可以想到對於這兩個clusters有兩種組織方式:(i) 兩個clusters都是2層的樹結構,(ii) 將head部分用一個短列表保存,對tail部分用二層樹結構。何種方式更優可以就其計算效率和準確率進行比較:

    在準確率方面,(i) 較之 (ii) 一般有 5 ~ 10 %的下降。原因如下:

    對於屬於cluster $c$的每個詞$w$的概率計算為: 

    若採用(i):  $P(c | h) * P(w | c, h)$

    若採用(ii): 對於高頻詞head部分可以直接計算獲得 $P(w | h)$,其更為簡單直接。

    因此,除非(i)(ii)的計算時間有非常大的差別,否則選擇(ii)的組織方式。

    (2) 對計算時間的縮短

     

    圖2. Two clusters示意圖

    如圖2所示,$k_h = |V_h|, k_t = k – k_h, p_t = 1 – p_h$

    (i) 對於第一層:對於batch size 為B的輸入,在head中對$k_h$個高頻詞以及1個單位的二層cluster的向量(加陰影部分),共$k_h + 1$個向量,做softmax,

    這樣占文檔中$p_h$的詞可以基本確定下來,在head 的 short list 部分找到對應的單詞;

    而如果與陰影部分進行softmax值更大,表明待預測的詞,在第二層的低頻詞部分;

    第一層的計算開銷為:$g(k_h + 1, B)$

    (ii) 在short list 中確定了 $p_h × B$ 的內容后,還剩下 $p_t B$的輸入需要在tail中繼續進行softmax來確定所預測的詞。

    第二層中需要對$k_t$個向量做softmax;

    第二層的計算開銷為:$g(k_t, pB)$

    綜上,總的計算開銷為:$$C = g(k_h + 1, B) + g(k_t, p_t B)$$

    相比於最初的softmax,分母中需要對詞典中的每個詞的向量逐一進行計算;採用adaptive softmax可以使得該過程分為兩部分進行,每部分只需對該部分所包含的詞的向量進行計算即可,降低了計算用時。

    論文中的Figure 2显示了,對於$k_h$的合理劃分,最高可以實現相較於完全softmax的5x加速。

    (3) 不同cluster的分類器能力不同

    由於每個cluster基本是獨立計算的,它們並不要求有相同的能力(capacity)。

    一般可以為高頻詞的cluster予以更高的capacity,而可以適當降低詞頻詞的cluster的capacity。因為低頻詞在文檔中很少出現,因此對其capacity的適當降低並不會非常影響總體的性能。

    在本文中,採取了為不同的clusters共享隱層,通過加映射矩陣的方式降低分類器的輸入大小。一般的,tail部分的映射矩陣將其大小從$d$維縮小到$d_t = d / 4$維。

    3.3 一般性情況

    在3.2中以分為2個cluster為例,介紹了adaptive softmax如何組織並計算的,這裏將其拓展為一般情況,即可以分為多個clusters時的處理方式。

    假設將整個詞典分為一個head部分和J個詞頻詞cluster部分,那麼有:

    $$V = V_h \cup V_1 … V_j, V_i \cap V_j = \phi$$

    其中$V_h$在第一層,其餘均在第二層,如圖Figure 3所示。

    每個cluster中的詞向量的數目為$k_i = |V_i|$,單詞$w$屬於某個cluster的概率為:$p_i = \sum_{w \in V_i} p(w)$.

    那麼,

    計算head部分的時間開銷為:$C_h = g(J + k_h, B)$

    計算每個二層cluster的時間開銷為:$\forall_i, C_i = g(k_i, p_i B)$

    那麼總的時間開銷為:$C = g(J + k_h, B) + \sum_i g(k_i, p_i B) \tag8$

     

    回顧公式(7):

    $$g(k,B) = max(c + \lambda k_0 B_0, c + \lambda k B) \tag7$$

    這裡有兩部分,一個是常數部分$c + \lambda k_0 B_0$,一個是線性變換部分$c + \lambda k B$。

    通過3.1可知,在常數部分GPU的算力並未得到充分利用,因此應盡量避免落入該部分。那麼需要滿足:

    $kB \geq k_0B_0$,這樣在求max時,可以利用以上的第二部分。

    將(8)式代入(7)的第二部分得:

     

    那麼接下來的目標就是根據(10) 以最小化時間開銷C. 

    在(10)中,$J ,B$是固定的,那麼可以重點關注$\sum_i p_ik_i$與$k_h$。

    (1) $\sum_i p_ik_i$

    假設$p_{i + j} = p_i + p_j$, 那麼 $p_jk_j = (p_{i + j} – p_i) k_j$

    $p_ik_i + p_jk_j = p_i(k_i – k_j) + p_{i + j}k_j \tag{11}$  

    假設$k_i > k_j, p_{i + j}$, $k_j$均固定,那麼(11)中只有$p_i$可變,可以通過減小$p_i$的方式使(11)式減小。=> 也即$k_i > k_j$ 且 $p_i$盡量小,即包含越多詞的cluster,其概率越小。

    (2) $k_h$

    減小$k_h$可以使(10)式減小。 => 即為可以使高頻詞所在的cluster包含更少的詞。

    綜上,給定了cluster的個數J,與batch size B,可以通過給大的cluster分配以更小的概率降低時間開銷C.

    另外paper中講到可以用動態規劃的方法,在給定了J后,對$k_i$的大小進行劃分。

    cluster的劃分個數$J$。paper中實驗了採用不同的J所導致的不同計算時間。如圖Figure 4所示。雖然在$10 ~ 15$區間內效果最好,但$ > 5$之後效果沒有非常明顯的提升。所以文中建議採用2~5個clusters。

    實驗主要在text8, europarl, one billion word三個數據集上做的,通過比較ppl發現,adaptive softmax仍能保持低的ppl,並且相比於原模型有2x到10x的提速。

      

    參考鏈接:

    1. Efficient softmax approximation for GPUs: 

    Some of us get dipped in flat, some in satin, some in gloss. But every once in a while you find someone who’s iridescent, and when you do, nothing will ever compare. ~ Flipped

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

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

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

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

  • 微服務框架 – Jimu(積木) 升級 1.0.0 支持 .Net Core 3.0

    微服務框架 – Jimu(積木) 升級 1.0.0 支持 .Net Core 3.0

    如果不知道 Jimu(積木) 是啥,請移步
    這次升級除了支持 .Net Core 3.0 還新增部分功能,如 REST, 鏈路跟蹤等,以下為詳細;

    一、功能列表

    功能 說明 Jimu 1.0.0 Jimu 0.6.0
    平台 .Net Core 2.1
    .Net Core 3.0
    服務註冊與發現 consul
    網關 Asp.Net Core Web
    RPC DotNetty
    鑒權 JWT
    負載均衡 輪訓
    容錯策略 重試
    容器 docker
    路由配置 Attribute註解
    日誌記錄 log4net
    nlog
    文檔 swagger
    鏈路跟蹤 skywalking
    REST Attribute註解
    健康監測 心跳
    文件上存下載 多文件上存,單文件下載
    跳轉 在服務端跳轉到指定url
    ORM Dapper
    DDD MiniDDD

    二、建議用積木結合 docker 搭建分佈式架構

    三、swagger

    四、skywalking

    拓撲圖: user -> jimu_apigateway -> jimu_order -> jimu_user

    Trace 跟蹤

    五、網關

    服務器

    微服務

    微服務詳細

    六、源碼

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

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

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

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

  • 類加載器 – ClassLoader詳解

    獲得ClassLoader的途徑

    • 獲得當前類的ClassLoader
      • clazz.getClassLoader()
    • 獲得當前線程上下文的ClassLoader
      • Thread.currentThread().getContextClassLoader();
    • 獲得系統的ClassLoader
      • ClassLoader.getSystemClassLoader()
    • 獲得調用者的ClassLoader
      • DriverManager.getCallerClassLoader

    ClassLoader源碼解析

    概述

    類加載器是用於加載類的對象,ClassLoader是一個抽象類。如果我們給定了一個類的二進制名稱,類加載器應嘗試去定位或生成構成定義類的數據。一種典型的策略是將給定的二進制名稱轉換為文件名,然後去文件系統中讀取這個文件名所對應的class文件。

    每個Class對象都會包含一個定義它的ClassLoader的一個引用。

    數組類的Class對象,不是由類加載器去創建的,而是在Java運行期JVM根據需要自動創建的。對於數組類的類加載器來說,是通過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是一樣的;如果數組當中的元素類型是一個原生類型,數組類是沒有類加載器的【代碼一】。

    應用實現了ClassLoader的子類是為了擴展JVM動態加載類的方式。

    類加載器典型情況下時可以被安全管理器所使用去標識安全域問題。

    ClassLoader類使用了委託模型來尋找類和資源,ClassLoader的每一個實例都會有一個與之關聯的父ClassLoader,當ClassLoader被要求尋找一個類或者資源的時候,ClassLoader實例在自身嘗試尋找類或者資源之前會委託它的父類加載器去完成。虛擬機內建的類加載器,稱之為啟動類加載器,是沒有父加載器的,但是可以作為一個類加載器的父類加載器【雙親委託機制】。

    支持併發類加載的類加載器叫做并行類加載器,要求在初始化期間通過ClassLoader.registerAsParallelCapable 方法註冊自身,ClassLoader類默認被註冊為可以并行,但是如果它的子類也是并行加載的話需要單獨去註冊子類。

    在委託模型不是嚴格的層次化的環境下,類加載器需要并行,否則類加載會導致死鎖,因為加載器的鎖在類加載過程中是一直被持有的。

    通常情況下,Java虛擬機以平台相關的形式從本地的文件系統中加載類,比如在UNIX系統,虛擬機從CLASSPATH環境所定義的目錄加載類。
    然而,有些類並不是來自於文件;它們是從其它來源得到的,比如網絡,或者是由應用本身構建【動態代理】。定義類(defineClass )方法會將字節數組轉換為Class的實例,這個新定義類的實例可以由Class.newInstance創建。

    由類加載器創建的對象的方法和構造方法可能引用其它的類,為了確定被引用的類,Java虛擬機會調用最初創建類的類加載器的loadClass方法。

    二進制名稱:以字符串參數的形式向CalssLoader提供的任意一個類名,必須是一個二進制的名稱,包含以下四種情況

    • “java.lang.String” 正常類
    • “javax.swing.JSpinner$DefaultEditor” 內部類
    • “java.security.KeyStore\(Builder\)FileBuilder$1″ KeyStore的內部類Builder的內部類FileBuilder的第一個匿名內部類
    • “java.net.URLClassLoader$3$1” URLClassLoader類的第三個匿名內部類的第一個匿名內部類

    代碼一:

    public class Test12 {
        public static void main(String[] args) {
            String[] strings = new String[6];
            System.out.println(strings.getClass().getClassLoader());
            // 運行結果:null
    
            Test12[] test12s = new Test12[1];
            System.out.println(test12s.getClass().getClassLoader());
            // 運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2
    
            int[] ints = new int[2];
            System.out.println(ints.getClass().getClassLoader());
            // 運行結果:null
        }
    }

    loadClass方法

    loadClass的源碼如下, loadClass方法加載擁有指定的二進制名稱的Class,默認按照如下順序尋找類:

    • 調用findLoadedClass(String)檢查這個類是否被加載
    • 調用父類加載器的loadClass方法,如果父類加載器為null,就會調用啟動類加載器
    • 調用findClass(String)方法尋找

    使用上述步驟如果類被找到且resolve為true,就會去調用resolveClass(Class)方法

    protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
    {
      synchronized (getClassLoadingLock(name)) {
          // First, check if the class has already been loaded
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      c = parent.loadClass(name, false);
                  } else {
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }
    
              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // to find the class.
                  long t1 = System.nanoTime();
                  c = findClass(name);
    
                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {
              resolveClass(c);
          }
          return c;
      }
    }

    findClass方法

    findClass的源碼如下,findClass尋找擁有指定二進制名稱的類,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委託機制,該方法會在檢查完父類加載器之後被loadClass方法調用,默認返回ClassNotFoundException異常。

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    defineClass方法

    defineClass的源碼如下,defineClass方法將一個字節數組轉換為Class的實例。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

    自定義類加載器

    /**
     * 繼承了ClassLoader,這是一個自定義的類加載器
     * @author 夜的那種黑丶
     */
    public class ClassLoaderTest extends ClassLoader {
        public static void main(String[] args) throws Exception {
            ClassLoaderTest loader = new ClassLoaderTest("loader");
           Class<?> clazz = loader.loadClass("classloader.Test01");
            Object object = clazz.newInstance();
            System.out.println(object);
            System.out.println(object.getClass().getClassLoader());
        }
        //------------------------------以上為測試代碼---------------------------------
    
        /**
         * 類加載器名稱,標識作用
         */
        private String classLoaderName;
    
        /**
         * 從磁盤讀物字節碼文件的擴展名
         */
        private String fileExtension = ".class";
    
        /**
         * 創建一個類加載器對象,將系統類加載器當做該類加載器的父加載器
         * @param classLoaderName 類加載器名稱
         */
        private ClassLoaderTest(String classLoaderName) {
            // 將系統類加載器當做該類加載器的父加載器
            super();
            this.classLoaderName = classLoaderName;
        }
    
        /**
         * 創建一個類加載器對象,显示指定該類加載器的父加載器
         * 前提是需要有一個類加載器作為父加載器
         * @param parent 父加載器
         * @param classLoaderName 類加載器名稱
         */
        private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
            // 显示指定該類加載器的父加載器
            super(parent);
            this.classLoaderName = classLoaderName;
        }
    
        /**
         * 尋找擁有指定二進制名稱的類,重寫ClassLoader類的同名方法,需要自定義加載器遵循雙親委託機制
         * 該方法會在檢查完父類加載器之後被loadClass方法調用
         * 默認返回ClassNotFoundException異常
         * @param className 類名
         * @return Class的實例
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            byte[] data = this.loadClassData(className);
            /*
             * 通過defineClass方法將字節數組轉換為Class
             * defineClass:將一個字節數組轉換為Class的實例,在使用這個Class之前必須要被解析
             */
            return this.defineClass(className, data, 0 , data.length);
        }
    
        /**
         * io操作,根據類名找到對應文件,返回class文件的二進制信息
         * @param className 類名
         * @return class文件的二進制信息
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        private byte[] loadClassData(String className) throws ClassNotFoundException {
            InputStream inputStream = null;
            byte[] data;
            ByteArrayOutputStream byteArrayOutputStream = null;
    
            try {
                this.classLoaderName = this.classLoaderName.replace(".", "/");
                inputStream = new FileInputStream(new File(className + this.fileExtension));
                byteArrayOutputStream = new ByteArrayOutputStream();
    
                int ch;
                while (-1 != (ch = inputStream.read())) {
                    byteArrayOutputStream.write(ch);
                }
    
                data = byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                throw new ClassNotFoundException();
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (byteArrayOutputStream != null) {
                        byteArrayOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    }

    以上是一段自定義類加載器的代碼,我們執行這段代碼

    classloader.Test01@7f31245a
    sun.misc.Launcher$AppClassLoader@18b4aac2

    可以看見,這段代碼中進行類加載的類加載器還是系統類加載器(AppClassLoader)。這是因為jvm的雙親委託機製造成的,private ClassLoaderTest(String classLoaderName)將系統類加載器當做我們自定義類加載器的父加載器,jvm的雙親委託機制使自定義類加載器委託系統類加載器完成加載。

    改造以下代碼,添加一個path屬性用來指定類加載位置:

    public class ClassLoaderTest extends ClassLoader {
        public static void main(String[] args) throws Exception {
            ClassLoaderTest loader = new ClassLoaderTest("loader");
            loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
            Class<?> clazz = loader.loadClass("classloader.Test01");
            System.out.println("class:" + clazz);
    
            Object object = clazz.newInstance();
            System.out.println(object);
            System.out.println(object.getClass().getClassLoader());
        }
        //------------------------------以上為測試代碼---------------------------------
    
        /**
         * 從指定路徑加載
         */
        private String path;
    
        ......
        
        /**
         * io操作,根據類名找到對應文件,返回class文件的二進制信息
         * @param className 類名
         * @return class文件的二進制信息
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        private byte[] loadClassData(String className) throws ClassNotFoundException {
            InputStream inputStream = null;
            byte[] data;
            ByteArrayOutputStream byteArrayOutputStream = null;
    
            className = className.replace(".", "/");
    
            try {
                this.classLoaderName = this.classLoaderName.replace(".", "/");
                inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
                byteArrayOutputStream = new ByteArrayOutputStream();
    
                int ch;
                while (-1 != (ch = inputStream.read())) {
                    byteArrayOutputStream.write(ch);
                }
    
                data = byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                throw new ClassNotFoundException();
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (byteArrayOutputStream != null) {
                        byteArrayOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    }

    運行一下

    class:class classloader.Test01
    classloader.Test01@7f31245a
    sun.misc.Launcher$AppClassLoader@18b4aac2

    修改一下測試代碼,並刪除工程下的Test01.class文件

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
       loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz);
    
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }

    運行一下

    class:class classloader.Test01
    classloader.Test01@135fbaa4
    classloader.ClassLoaderTest@7f31245a

    分析

    改造后的兩塊代碼,第一塊代碼中加載類的是系統類加載器AppClassLoader,第二塊代碼中加載類的是自定義類加載器ClassLoaderTest。是因為ClassLoaderTest會委託他的父加載器AppClassLoader加載class,第一塊代碼的path直接是工程下,AppClassLoader可以加載到,而第二塊代碼的path在桌面目錄下,所以AppClassLoader無法加載到,然後ClassLoaderTest自身嘗試加載並成功加載到。如果第二塊代碼工程目錄下的Test01.class文件沒有被刪除,那麼依然是AppClassLoader加載。

    再來測試一塊代碼

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest("loader");
        loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    結果顯而易見,類由系統類加載器加載,並且clazz和clazz2是相同的。

    class:2133927002
    sun.misc.Launcher$AppClassLoader@18b4aac2
    class:2133927002
    sun.misc.Launcher$AppClassLoader@18b4aac2

    在改造一下

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
        loader2.setPath("/home/fanxuan/桌面/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    運行結果

    class:325040804
    classloader.ClassLoaderTest@7f31245a
    class:621009875
    classloader.ClassLoaderTest@45ee12a7

    ClassLoaderTest是顯而易見,但是clazz和clazz2是不同的,這是因為類加載器的命名空間的原因。

    我們可以通過設置父類加載器來讓loader和loader2處於同一命名空間

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
        loader2.setPath("/home/fanxuan/桌面/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    運行結果

    class:325040804
    classloader.ClassLoaderTest@7f31245a
    class:325040804
    classloader.ClassLoaderTest@7f31245a

    擴展:命名空間

    • 每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成
    • 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
    • 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類

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

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

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

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

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

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

  • 多線程編程(3)——synchronized原理以及使用

    多線程編程(3)——synchronized原理以及使用

    一、對象頭

      通常在java中一個對象主要包含三部分:

    • 對象頭 主要包含GC的狀態、、類型、類的模板信息(地址)、synchronization狀態等,在後面介紹。

    • 實例數據:程序代碼中定義的各種類型的字段內容。

    • 對齊數據:對象的大小必須是 8 字節的整數倍,此項根據情況而定,若對象頭和實例數據大小正好是8的倍數,則不需要對齊數據,否則大小就是8的差數。

    先看下面的實例、程序的輸出以及解釋。

    /*需提前引入jar包
    <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 解析java對象布局 -->
            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>
    ​
    */
    //Java對象以8個字節對其,不夠則使用對其數據
    public class Student {
        private int id;       // 4字節
        private boolean sex;  // 1字節
        public Student(int id, boolean sex){
            this.id = id;
            this.sex = sex;
        }
    }
    public class Test01 {
        public static void main(String[] args) {
            Student stu = new Student(6, true);
            //計算對象hash,底層是C++實現,不需要java去獲取,如果此處不調用,則後面的hash值不會去計算
            System.out.println("hashcode: " + stu.hashCode());  
            System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
    }
    /* output
    hashcode: 523429237
    com.thread.synchronizeDemo.Student object internals:
    OFFSET SIZE TYPE DESCRIPTION        VALUE
     0     4    (object header)    01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
    4      4     (object header)    1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
    8      4     (object header)    43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
    12     4       int Student.id                                6
    16     1   boolean Student.sex                               true
    17     7           (loss due to the next object alignment)
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
    ​
    備註:上述代碼在64位的機器上運行,此時
    對象頭占  (4+4+4)*8 = 96 位(bit)
    實例數據  (4+1)*8 = 40 位(bit)
    對齊數據  7*8 = 56 位(bit) 因為Java對象以8個字節對其的方式,需補7byte去對齊
    */

       下面主要陳述對對象頭的解釋,內容從hotspot官網摘抄下來的信息:

    object header

      Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

    mark word

      The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

    klass pointer

      The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original objec

      由此可知,對象頭主要包含GC的狀態(用4位表示——表示範圍0-15,用來記錄GC年齡,這也就是為什麼對象在survivor中從from區到to區來迴轉換15次後轉入到老年代tenured區)、類型、類的模板信息(地址)、synchronization 狀態等,由兩個字組成mark word和klass pointer(類元素據信息地址,具體數據通常在堆的方法區中,即8字節,但有時候會有一些優化設置,會開啟指針壓縮,將代表klass pointer的8字節變成4字節大小,這也是為什麼在上述代碼中對象頭大小是(8+4)byte,而不是16byte。)。本節最主要介紹對象頭的mark word這部分。關於對象頭中每部分bit所代表的意義可以查看hotspot源碼中代碼的注,這段註釋是從openjdk中拷貝的。

    JVM和hotspot、openjdk的區別

    JVM是一種產品的規範定義,hotspot(Oracle公司)是對該規範實現的產品,還有遵循這些規範的其他產品,比如J9(IBM開發的一個高度模塊化的JVM)、Zing VM等。

    openjdk是一個hotspot項目的大部分源代碼(可以通過編譯后變成.exe文件),hotspot小部分代碼Oracle並未公布

    // openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\oops\markOop.hpp
    /*
    Bit-format of an object header (most significant first, big endian layout below):
    ​
    32 bits:
    --------
            hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
            JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
            size:32 ------------------------------------------>| (CMS free block)
            PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    ​
    64 bits:
    --------
    unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    size:64 ----------------------------------------------------->| (CMS free block)
    ​
    */

    可以看到在32位機器和64位機器中,對象的布局的差異還是很大的,本文主要 敘述64位機器下的布局,其實兩者無非是位數不同而已,大同小異。在64位機器用64位(8byte)表示Mark Word,首先前25位(0-25)是未被使用,接下來31位表示hash值,然後是對象分代年齡大小,最後Synchronized的鎖信息,分為兩部分,共3bit,如下錶,鎖的嚴格性依次是鎖、偏向鎖、輕量級鎖、重量級鎖。

     

    關於鎖的一些解釋

    無鎖

      無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。

    偏向鎖

      引入偏向鎖是為了在無多線程競爭的情況下,一段同步代碼一直被一個線程所訪問因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,當由另外的線程所訪問,偏向鎖就會升級為輕量級鎖。

    輕量級鎖 

      當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。

    重量級鎖

      依賴於操作系統Mutex Lock所實現的鎖,JDK中對Synchronized做的種種優化,其核心都是為了減少這種重量級鎖的使用。JDK1.6以後,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了“輕量級鎖”和“偏向鎖”。  

    GC

      這並不是鎖的狀態,而是GC標誌,等待GC回收。

    現在開始從程序層面分析前面程序的對象頭的布局信息,在此之前需要知道的是,在windows中對於數據的存儲採用的是小端存儲,所以要反過來讀

    大端模式——是指數據的高字節保存在內存的低地址中,而數據的低字節保存在內存的高地址中,這樣的存儲模式有點兒類似於把數據當作字符串順序處理:地址由小向大增加,而數據從高位往低位放;這和我們的閱讀習慣一致。

    小端模式——是指數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。 一般在網絡中用的大端;本地用的小端;

    運行程序如下,可以看到對應的hashcode值被打印出來:

    public static void main(String[] args) {
         Student stu = new Student(6, true);
        //Integer.toHexString()此方法返回的字符串表示的無符號整數參數所表示的值以十六進制
         System.out.println("hashcode: " + Integer.toHexString(stu.hashCode()));
         System.out.println(ClassLayout.parseInstance(stu).toPrintable());
    }
    /*
    hashcode: 1f32e575
    com.thread.synchronizeDemo.Student object internals:
     OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
          0     4           (object header)                           01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
          4     4           (object header)                           1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
          8     4           (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
         12     4       int Student.id                                6
         16     1   boolean Student.sex                               true
         17     7           (loss due to the next object alignment)
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

    //前8個字節反過來看,可以看出對象頭的hash是1f32e575,同時是無鎖的狀態00000001
    */

     二、Monitor

           可以把它理解為一個同步工具(數據結構),也可以描述為一種同步機制,通常被描述為一個對象。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處於鎖定狀態(每一個線程都有一個可用 monitor record 列表)[具體可以看參考資料5]。需要注意的是這種監視器鎖是發生在對象的內部鎖已經變成重量級鎖的時候。

    /*  openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
    // initialize the monitor, exception the semaphore, all other fields // are simple integers or pointers ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; } */

      Monitor的實現主要藉助三個結構去完成多線程的併發操作——_owner、_WaitSet 、_EntryList。當多個線程同時訪問由synchronized修飾的對象、類或一段同步代碼時,首先會進入_EntryList 集合,如果某個線程取得了_owner的所有權,該線程就可以去執行,如果該線程調用了wait()方法,就會放棄_owner的所有權,進入等待狀態,等下一次喚醒。如下圖(圖片摘自參考資料5)。

     三、synchronized的用法

         synchronized修飾方法和修飾一個代碼塊類似,只是作用範圍不一樣,修飾代碼塊是大括號括起來的範圍,而修飾方法範圍是整個函數。其中synchronized(this) 與synchronized(class) 之間的區別有以下五點要注意:

        1、對於靜態方法,由於此時對象還未生成,所以只能採用類鎖;

        2、只要採用類鎖,就會攔截所有線程,只能讓一個線程訪問。

        3、對於對象鎖(this),如果是同一個實例,就會按順序訪問,但是如果是不同實例,就可以同時訪問。

       4、如果對象鎖跟訪問的對象沒有關係,那麼就會都同時訪問。

       5、當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

    當然,Synchronized也可修飾一個靜態方法,而靜態方法是屬於類的而不屬於對象的,所以synchronized修飾的靜態方法鎖定的是這個類的所有對象。關於如下synchronized的用法,我們經常會碰到的案例:

    public class Thread5 implements Runnable {
        private static int count = 0;
        public synchronized static void add() {
            count++;
        }
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (Thread5.class){
                    count++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            ExecutorService es = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 20; i++) {
                es.execute(new Thread5());
            }
            es.shutdown();
            es.awaitTermination(6, TimeUnit.SECONDS);
            System.out.println(count);
        }
    }
    /* 類鎖
      20000000
      */

    而一旦換成對象鎖,不同實例,就可以同時訪問。則會出錯:

    public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (this){
                    count++;
                }
            }
    }
    /* 對象鎖
     10746948
    */
    

    這是因為靜態變量並不屬於某個實例對象,而是屬於類所有,所以對某個實例加鎖,並不會改變count變量臟讀和臟寫的情況,還是造成結果不正確。

     

    參考資料

    1. 對象布局的各部分介紹——

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

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

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

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

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

  • 小程序雲開發:菜鳥也能全棧做產品

    小程序雲開發:菜鳥也能全棧做產品

    我想獨立實現一個全棧產品為什麼這麼難

    日常生活中,我們會使用很多軟件產品。在使用這些產品的時候,我們看得見的東西稱為“前端界面”如一個輸入框、一個按鈕,點擊按鈕之後發生的一切看不見的東西稱為“後端服務”。與之對應的創造者分別稱為“前端程序員”、“後端程序員”,然而,一個完整產品的開發不僅僅是只有前端和後端,還有設計師,架構師,運維等。有沒有可能這些所有的事情都一個人干呢?有可能,事實上如今就有很多的“全棧工程師”,他們身兼數職,是多面手。能獨立完成一個產品的方方面面。這種人固然十分了得,他們通常具有多年的經驗,涉獵廣泛,是老手,也是高手,當有一個產品想法的時候,他們可以用自己的全面專業技能,盡情的發揮去實現自己的想法。所以,從某種意義上講“全棧也是一種自由”,你可以自由的實現你的想法,這簡直太美妙了!

    然而,很多時候當我們有一個產品想法的時候,我們往往發現,前端寫完了,後端怎麼搞?數據庫怎麼搞?域名怎麼搞?域名還要備案?應用部署怎麼搞?我的買什麼樣的服務器啊?靜態資源 CDN 怎麼搞?文件上傳服務器怎麼搞?萬一訪問用戶多了能撐住嗎?等等……問題很多,導致你的一個個想法,都只是在腦海中曇花一現,從來都無法將她們實現,或者說你激情飽滿的實現了其中自己最擅長的一部分,當碰到其他難題的時候就止步了。於是仰天長嘯:我就想獨立做一個完整的產品為什麼這麼難?年輕人,這一切都不怪你……

    破局:小程序雲開發

    為什麼使用小程序雲開發來破局?

    為啥是用“小程序雲開發”來破局?首先,我們的目的是全棧實現一個產品。全棧可以有多種技術方案,你可用任何你能會的技能來達到全棧的目的。你可以開發安卓,IOS,或者 PC 站,然而小程序是最實際的!為啥?手機上能做的事情為啥要用 PC 版?OK,既然手機版比較好,那能不能再簡單一點?能,就是小程序,不需要開發IOS,安卓兩個版本。可以快速產出,快速試錯。

    其次,前面說到了,全棧實現一個產品並不容易,對很多人來說甚至是巨難!選擇了小程序已經是比較划算的方案。而再集成雲開發,全棧立馬就有了。這就是為什麼選擇“小程序雲開發”來破局。

    小程序雲開發是什麼?

    小程序雲開發是什麼?官方文檔是這麼說的:開發者可以使用雲開發開發微信小程序、小遊戲,無需搭建服務器,即可使用雲端能力。雲開發為開發者提供完整的原生雲端支持和微信服務支持,弱化後端和運維概念,無需搭建服務器,使用平台提供的 API 進行核心業務開發,即可實現快速上線和迭代,同時這一能力,同開發者已經使用的雲服務相互兼容,並不互斥。

    看完上面的描述,也許你仍然無法非常清楚的知道什麼是“小程序雲開發”,沒關係,你只需要注意加粗的部分,大概知道它“無需搭建服務器”,從傳統觀念將,這個似乎“毀三觀”咋可能沒服務器啊?是的,可以沒有傳統意義上的服務器,這種模式是 serveless 的。

    那麼,小程序雲開發提供了哪些東西來破局呢?且看下面的表格:

    能 力 作 用 說 明
    雲函數 無需自建服務器 在雲端運行的代碼,微信私有協議天然鑒權,開發者只需編寫自身業務邏輯代碼
    數據庫 無需自建數據庫 一個既可在小程序前端操作,也能在雲函數中讀寫的 JSON 數據庫
    存儲 無需自建存儲和 CDN 在小程序前端直接上傳/下載雲端文件,在雲開發控制台可視化管理
    雲調用 原生微信服務集成 基於雲函數免鑒權使用小程序開放接口的能力,包括服務端調用、獲取開放數據等能力

    上面的表格中提到了“雲開發”中的一些能力:“雲函數”,“數據庫”,“存儲”,“雲調用”,我們可以將這些詞帶入你曾經開發過的應用,看看它們分別代表了哪些部分。對於程序員來說,如果有疑問的話,沒有什麼是一個 helloword 解決不了的。

    實戰:獨立開發一個簡易的零售小程序

    哆嗦再多,不如實戰。下面我們就來使用小程序雲開發實現一個簡單的零售小程序。

    項目構思

    既然是一個零售小程序,那麼我們可以思考一下零售小程序的大致業務流程,以及粗略的梳理一下,其功能點。現根據自己的想法,大致畫一下草圖,如果沒有靈感可以參考一下別的 APP 是如何設計的。

    我根據自己的想法設計之後是這樣的:

    功能模塊:首頁,商品列表頁,購物車,確認訂單,個人中心,個人訂單,管你模塊(商品添加,分類添加)其中商品需要上傳圖片。

    梳理完功能之後,我們對於要實現的東西已經有個初步的概念了。接下來,我們需要大概畫一下頁面設計、及功能流轉。初次設計可能沒有太多經驗,沒關係,開始做就行了,做着做着就會想法越來越多,然後優化的越來越好。。我也是經過了多番修改調整,最終找到了一些思路。我的(拙劣)設計如下,圖片如果看不清楚可複製圖片鏈接在新窗口打開查看:

    說明,以上圖片是根據成品(我真的開發了一個雲小程序並上線使用了)截圖的,而實際我再設計的時候也是經過幾番修改才最終定成這樣。

    同時,補充說明一下,這裏前端頁面使用的是 vant-weapp控件,非常好用。推薦!如果你和我一樣是一個純後端程序員,建議使用 vant-weapp 來作為 ui,非常方便。否則自己寫頁面樣式的話可能就做不出來了。全棧不是那麼好乾的啊。選擇自己能駕馭的,能實現最終功能,就是一個合格的全棧。

    創建小程序雲開發項目

    我們先下載微信小程序開發工具,下載地址,安裝好了之後,新建項目,界面如下,APPID 需要你自己去註冊一個。然後注意,選擇“小程序雲開發”,如下圖所示:

    創建好了之後,項目目錄如下,先看 1 標註的地方:

    如果你曾經有過小程序的開發經驗,那麼miniprogram文件夾下面的結構你肯定熟悉了,miniprogram下面的子目錄分別是小程序對應的組件、圖片、頁面、樣式以及app.js,app.json,sitemap.json,其中components下面的vant-weapp就是上面提到的 ui 組件。

    最後一個比較重要的文件夾就是cloudfunctions,這個目錄是用來存放“雲函數的”,雲函數就是我們的後端。每一個雲函數提供一個服務。一個個的雲函數組成了我們整體的後端服務。雲函數可以看做是 FaaS(function as a service)。途中,2 標記的位置的“雲開發”按鈕,我們點進去,就可以看到“雲開發的控制台”,如下圖所示:

    如果上圖看不清楚,可以複製鏈接到新的瀏覽器窗口查看,如圖,小程序雲開發默認的免費套餐有一定的額度可供使用。首頁便是使用統計。然後我們能看到,有“數據庫”,“存儲”,“雲函數”。

    這裏的“數據庫”其實就是類似於一個 MongoDB,你可以點進去創建一個個的 collection(即:關係型數據庫中的table);這裏的“存儲”其實就是“文件夾”,我們可以通過微信提供的 api把圖片上傳到“存儲”中;這裏的“雲函數”就是我們需要實現的後端業務邏輯,他就是一個個的函數(函數由我們自己寫好後上傳)。一般開發過程中我們在開發者工具中的cloudfunctions目錄下創建雲函數(比方說是:user-add)開發完成之後在雲函數目錄點擊右鍵——上傳即可。然後就可以在小程序的代碼中調用這個user-add雲函數。

    雲開發之——3 分鐘實現文件上傳

    注意:在開始雲開發之前,我們現在 小程序代碼的 app.js 中加入wx.cloud.init,如下:

    App({
      onLaunch: function () {
        if (!wx.cloud) {
          console.error('請使用 2.2.3 或以上的基礎庫以使用雲能力')
        } else {
          wx.cloud.init({
            // env 參數說明:
            //   env 參數決定接下來小程序發起的雲開發調用(wx.cloud.xxx)會默認請求到哪個雲環境的資源
            //   此處請填入環境 ID, 環境 ID 可打開雲控制台查看
            //   如不填則使用默認環境(第一個創建的環境)
            env: 'your-env-id',
            traceUser: true,
          })
        }
        this.globalData = {}
      }
    })

    上面的圖中,我們已經看到了“商品添加”頁面的效果,它需要我們輸入商品名稱、價格、並上傳圖片,然後保存。傳統架構中,上傳圖片需要前端頁面擺一個控件,然後後端提供一個 api用來接收前端傳來的文件,通常來說這個後端 api 接收到圖片之後,會將圖片文件保存到自己的文件服務器或者是阿里雲存儲、或者是七牛雲存儲之類的。然後返回給你一個文件鏈接地址。非常麻煩,然而,小程序雲開發上傳文件超級簡單,上代碼:

    頁面代碼:
    <van-notice-bar
      scrollable="false"
      text="發布商品"
    />
      <van-field
        value="{{ productName }}"
        required
        clearable
        label="商品名稱"
        placeholder="請輸入商品名稱"
        bind:change="inputName"
      />
        <van-field
        value="{{ productPrice }}"
        required
        clearable
        label="價格"
        icon="question-o"
         bind:click-icon="onClickPhoneIcon"
        placeholder="請輸入價格"
        error-message="{{phoneerr}}"
        border="{{ false }}"
        bind:change="inputPrice"
      />
    
    <van-action-sheet
      required
      show="{{ showSelect }}"
      actions="{{ actions }}"
      close-on-click-overlay="true"
      bind:close="toggleSelect"
      bind:select="onSelect" cancel-text="取消"
    />
      <van-field
        value="{{ productCategory }}"
        center
        readonly
        label="商品分類"
        border="{{ false }}"
        use-button-slot
      >
        <van-button slot="button" size="small" plain type="primary"  
         bind:click="toggleSelect">選擇分類</van-button>
      </van-field>
      
      <van-button class="rightside" type="default" bind:click="uploadImage" >上傳商品圖片</van-button>
      <view class="imagePreview">
        <image src="{{productImg}}" />
      </view>
     <van-submit-bar
      price="{{ totalShow }}"
      button-text="提交"
      bind:submit="onSubmit"
      tip="{{ false }}"
     >
     </van-submit-bar> 
    <van-toast id="van-toast" />
    <van-dialog id="van-dialog" />

    這裡有個控件,綁定了uploadImage方法,其代碼為:

      uploadImage:function(){
        let that = this;
        wx.chooseImage({
          count: 1,
          sizeType: ['compressed'],
          sourceType: ['album', 'camera'],
          success(res) {
            wx.showLoading({
              title: '上傳中...',
            })
            const tempFilePath = res.tempFilePaths[0]
            const name = Math.random() * 1000000;
            const cloudPath = name + tempFilePath.match(/\.[^.]+?$/)[0]
            wx.cloud.uploadFile({
              cloudPath:cloudPath,//雲存儲圖片名字
              filePath: tempFilePath,//臨時路徑
              success: res => {
                let fileID = res.fileID;
                that.setData({
                  productImg: res.fileID,
                });
                wx.showToast({
                  title: '圖片上傳成功',
                })
              },
              fail: e =>{
                wx.showToast({
                  title: '上傳失敗',
                })
              },
              complete:()=>{
                wx.hideLoading();
              }
            });
          }
        })
      }

    這裏,wx.chooseImage用於調起手機選擇圖片(相冊/相機拍照),然後wx.cloud.uploadFile用於上傳圖片到上面說到的雲開發能力之一的“存儲”中。上傳圖片成功之後返回一個文件 ID,類似:

    cloud://release-0kj63.7265-release-0kj63-1300431985/100477.13363146288.jpg  

    這個鏈接可以直接在小程序頁面展示:

    <image src="cloud://release-0kj63.7265-release-0kj63-1300431985/100477.13363146288.jpg  " />

    也可以通過微信 api,裝換成 http 形式的圖片鏈接。

    雲開發之——操作數據庫,1 分鐘寫完保存商品到數據庫的代碼

    上面我們實現了商品圖片上傳,但是,商品圖片並沒有保存到數據庫。正常錄入商品的時候,我們會填好商品名稱,價格等,然後上傳圖片,最終點擊“保存”按鈕,將商品保存到數據庫。傳統模式下,前端仍然是需要調用一個後端接口,通過 post 提交數據,最終由後端服務(比如 java 服務)將數據保存到數據庫。小程序雲開發使得操作數據庫十分簡單,首先我們在雲開發控制台創建“商品表”,即一個 collection,取名為:products。然後我們就可以保存數據到數據庫了,代碼如下:

    onSubmit:function(){
        // 校驗代碼,略
        let product = {};
        product.imgId = this.data.productImg;
        product.name= this.data.productName;
        product.categoryId = this.data.productCategoryId;
        product.price = this.data.productPrice;
        // 其他賦值,略
        const db = wx.cloud.database();
        db.collection('products').add({
         data: product,
         success(res) {
           wx.showToast({
             title: '保存成功',
           })
         }
       });
      }

    以上就實現了數據入庫,就這點代碼,超簡單,1 分鐘寫完,誠不欺我。其中這裏的products就是我們的“商品表”,之前說過,類似 MongoDB 數據庫,這裏操作的是db.collection,這和 MongoDB 的語法差不多。

    雲開發之——使用雲函數完成後端業務邏輯,訂單創建

    小程序雲開發提供了幾大能力:“數據庫”,“存儲”,“雲函數”,前兩項我們已經有所體會了。下面我們能創建一個雲函數來實現訂單創建。這裏說明,雲函數其實就是 一段JavaScript 代碼,上傳至雲服務器之後,最終也是運行在 nodejs 環境的,只是這一切,我們不需要關心。我們只需要關心我們這個雲函數提供的功能是什麼就可以了。

    創建雲函數很簡單,直接在開發工具中右鍵“新建Node.js 雲函數”。然後以創建訂單為例,假設我們創建一個雲函數名為c-order-add,創建好了之後,目錄是這樣:

    雲函數的主要代碼在 index.js 中,其完整代碼是這樣:

    // 雲函數入口文件
    const cloud = require('wx-server-sdk')
    cloud.init({
      env: 'release-xxx'// your-env-id
    })
    const db = cloud.database()
    
    // 雲函數入口函數
    exports.main = async (event, context) => {
      const wxContext = cloud.getWXContext();
      console.log("雲函數 c-order-add : ")  
      // 這裡是一些邏輯處理...
      
      return await db.collection('uorder').add({
        data: {
          openid: event.userInfo.openId,
          address: event.address,
          userName: event.userName,
          phone: event.phone,
          shoppingInfo: event.shoppingInfo,
          totlePrice: event.totlePrice,
          shoppingStr: event.shoppingStr,
          remark:event.remark,
          createTime: now,
          // ...
        }
      });
    }

    這個雲函數寫好之後,需要上傳到服務器,直接在雲函數目錄點擊右鍵,然後點擊“上傳並部署”即可,這就相當於部署好了後端服務。前端小程序頁面調用的寫法是這樣的:

    let orderData={};
    orderData.userName = this.data.userName;
    orderData.phone = this.data.phone;
    orderData.address = this.data.address;
    // ....
    wx.cloud.callFunction({
          // 雲函數名稱
          name: 'c-order-add',
          // 傳給雲函數的參數
          data: orderData,
          complete: res => {
            Dialog.alert({
              title: '提交成功',
              message: '您的訂單成功,即將配送,請保持手機通暢。'
            }).then(() => {
              // ....
              wx.redirectTo({
                url: '../uorder/uorder'
              });
            });
          }
    })

    這裏,向程序前端,通過wx.cloud.callFunction完成了對雲函數的調用,也可以理解為對後端服務的調用。至此我們我們介紹完了,小程序雲開發的功能。雖然,我只貼出了少量的代碼,即保存商品,和提交訂單。由於時間和篇幅有限,我不可能把整個完整的程序代碼貼出來。但是你可以參照這個用法示例,將剩下的業務邏輯補充完整,最終完成“項目構思”一節中展示的成品截圖效果。

    小程序審核的一點經驗

    我開發的小程序審核在提交審核的時候遭遇了兩次退回,第一次是因為:“小程序具備電商性質,個人小程序號不支持”。所以,我只好申請了一個企業小程序號,使用的是超市的營業執照。服務類目的選擇也被打回了一次,最後選擇了食品還提交了食品經營許可證。第二次打回是因為:“用戶體驗問題”。其實就是“授權索取”的問題,微信不讓打開首頁就“要求授權”,同時不能強制用戶接受授權,得提供拒絕授權也能使用部分功能。

    上面兩條解決之後,更新新了好幾版,都沒有出現過被拒的情況。並且,有次我是夜晚 10 左右提價的審核,結果10 點多就提示審核通過,當時沒看具體時間,就是接盆水泡了個腳的時間審核通過了。所以,我推斷小程序審核初次審核會比較嚴,之後如果改動不大應該直接機審就過了。

    總結及對比

    這裏我們可以對小程序雲開發和傳統模式做一個對比:

    對比條目 傳統模式 雲開發
    是否需要後端服務 需要 (如一個java應用部署在 Tomcat 中) 不需要 只需要“雲函數”
    是否需要域名 需要 (還得在微信後台的把域名加入安全域名) 不需要
    是否需要購買服務器 需要 (你得部署後端 Java 應用,還得安裝數據庫) 不需要
    開通雲開發之後免費套餐夠用
    不夠的話購買套餐按調用量計費
    是否需要懂運維 需要
    (你得會折騰服務器,數據庫之類的
    還得配置好相關的用戶,端口,啟動服務)
    不需要
    圖片上傳及 CDN 麻煩 簡單
    獲取微信 openID 麻煩 超級簡單,雲函數中直接獲取
    ···

    就對比這麼多吧,總之,我非常喜歡小程序雲開發,小程序真的可以讓你輕鬆干全棧。或者咱們別動不動就提“全棧”,姑且說,小程序雲開發可以讓你更簡單、更快速、更便宜的實現你的產品落地。我自己開發的雲小程序上線之後,使用了一兩個月,沒出現任何問題。我也不用操心服務器什麼的。所以,我已經給身邊很多人安利了小程序雲開發了。這裏我就不貼出我的小程序碼了,因為已經正式給我同學的超市使用了,所以不方便讓別人去產生測試數據。如果你感興趣想看的話,可以聯繫我。

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

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

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • 【面經系列】一線互聯網大廠前端面試技巧深入淺出總結

    【面經系列】一線互聯網大廠前端面試技巧深入淺出總結

    一二面(基礎面)

    1. 一面基礎面

    1.1 面試準備

    1.1.1 個人簡歷

    • 基本信息:姓名-年齡-手機-郵箱-籍貫
    • 工作經歷:時間-公司-崗位-職責-技術棧-業績(哪些成就)
    • 學歷: 博士 > 碩士 > 本科 > 大專
    • 工作經歷:時間-公司-崗位-職責-技術棧-業績
    • 開源項目:GitHub和說明

    1.2.2 自我陳述

    1.2.2.1 把我面試的溝通方向(別把自己帶到坑裡面)

    答:我平時喜歡研究一些網站,並對一些技術的原理和好玩的點感興趣,我自己也喜歡思考,也喜歡嘗試探索有沒有更好的方式和實現。(有所收留,不要全部說出來,稍微留一點懸念留作面試官來提問)

    1.2.2.2 豁達、自信的適度發揮

    答:適當自信,向自己擅長的方向上面來引路;要讓面試官來欣賞我,而不是來鄙視他。

    1.2.2.3 自如談興趣

    (豁達自信,適當收住),巧妙演示實例,適時討論疑問(不知道的問題請求指導一下,如何去解決,不要說不知道,或者不了解)

    1.2.2.4 節奏要適宜

    切忌小聰明(盡量把問題的所有實現方法都寫出來,表現出來的是熟練)

    1.2 面試實戰

    [!NOTE]
    > 1. 方向要對,過程要細(性能優化,過程詳細)
    > 2. 膽子要大、心態要和(算法題認真思考,認真使勁想;敢於承擔責任,不要輕易放棄)

    2. CSS相關

    2.1 頁面布局

    2.1.1 如何實現垂直居中布局呢?

    1.已知寬高

    /*v1*/
    .container {
        position: absolute;
        left: 50%;
        top: 50%;
        marigin-left: -width / 2;
        marigin-top: -width / 2;
    }
    
    /*v2*/
    .container {
        position: absolute;
        top: calc(50% - 5em);
        left: calc(50% - 9em);
    }
    

    2.未知寬高

    /*v1*/
    .container {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
    
    /*v2:flex+ auto*/
    .wrapper {
        dislay: flex;
    }
    .content {
        margin: auto;
    }
    
    /*v3. 父元素居中*/
    .wrapper {
        display: flex;
        /* 盒子橫軸的對齊方式 */
        justify-content: center;
        /* 盒子縱軸的對齊方式 */
        align-items: center;
    }
    
    /*v4.body內部居中*/
    .content {
         /* 1vh = 1% * 視口高度 */
          margin: 50vh auto;
          transform: translateY(-50%);
    }

    2.1.2 如何實現水平居中布局呢?

    1. 如果需要居中的元素為常規流中 inline / inline-block 元素,為父元素設置 text-align: center;
    2. 父元素上設置 text-align: center; 居中元素上margin 為 auto。
    3. 如果元素positon: absolute; 那麼
      • 0)設置父元素postion: relative
      • 1)為元素設置寬度,
      • 2)偏移量設置為 50%,
      • 3)偏移方向外邊距設置為元素寬度一半乘以-1

    2.1.3 如何實現三欄布局呢?

    1. left和right寫在center前面,並且分別左右浮動;
    2. 左右區域分別postion:absolute,固定到左右兩邊;中間的這個div因為是塊級元素,所以在水平方向上按照他的包容塊自動撐開。
    3. 父元素display: table;並且寬度為100%; 每一個子元素display: table-cell; 左右兩側添加寬度,中間不加寬度
    4. 包裹這個3個塊的父元素display: flex; 中間的元素flex: 1;
    5. 網格布局
    /* 網格布局 */
    .wrapper {
        display: grid;
        width: 100%;
        grid-template-columns: 300px 1fr 300px;
    }

    2.2 知道CSS動畫的實現嗎?

    [!NOTE]
    知道transition 過渡動畫和animation 關鍵幀動畫區別和具體實現。

    • 1.CSS動畫實現輪播圖
    • 2.CSS動畫實現旋轉的硬幣
    • 3.CSS動畫實現鐘擺效果

    2.3 CSS盒子模型

    2.3.1 說一下CSS的盒子模型?標準模型和IE模型的區別?CSS如何設置這兩種模型?

    • 標準盒子模型:width = content
    • IE盒子模型:width = content + pading + border

    • box-sizing : content-box
    • box-sizing : border-box

    2.4 CSS樣式獲取

    2.4.1 JS如何設置獲取盒子模型對應的寬度和高度?(面試重點)

    • dom.style.width/height : 只能取到內聯樣式的的屬性信息(拿不到外部引入的CSS樣式信息的)
    • dom.currentStyle.width/height : 會拿到瀏覽器渲染之後的屬性信息(IE瀏覽器)
    • window.getComputedStyle(dom).width/height : Chrome/Firefox 兼容, Firefox可以通過document.defaultView.getComputedStyle(dom)的方式來獲取
    • dom.getBoundingClientRect().width/height : 可以獲取距離viewport位置的寬度和高度

    2.5 BFC

    2.5.1 根據盒子模型解釋邊距額重疊問題?邊距重疊問題的解決方案?

    • 父子元素
    • 兄弟元素
    • 其他 ————————–計算方式:以參數的最大值來進行計算

    解決方案:對父級元素創建BFC

    2.5.2 BFC原理

    [!NOTE]
    BFC: 塊級格式化上下文,IFC(內聯格式化上下文)

    1. 在BFC的垂直邊距上面會發生重疊
    2. BFC的區域不會與浮動元素的BOX重疊
    3. BFC在頁面上是一個獨立的渲染區域,外部的元素不會影響到我,同時也不會影響到外部的元素
    4. 計算BFC的高度的時候,浮動元素也會參与運算

    2.5.3 如何創建BFC?

    1. float值不是none
    2. position值不是static或者relative
    3. display值為table, table-cell, inline-box1.
    4. overflow : auto/hidden

    2.5.4 BFC的使用場景?(重點理解)

    1. 解決邊距的重疊問題
    <section id="margin">
            <style>
                #margin {
                    background-color: #4eff35;
                    overflow: hidden;
                }
                #margin>p {
                    /*上 左右 下*/
                    margin: 5px auto 25px;
                    background-color: #ff255f;
                }
            </style>
            <p>1</p>
            <!--把一個元素放在一個容器裏面,為這個容器創建BFC即可解決邊距重疊問題-->
            <div style="overflow: hidden">
                <p>2</p>
            </div>
    
            <p>3</p>
    </section>
    1. BFC 不與float部分重疊的解決
    <section id="layout">
          <style>
              #layout {
                  background-color: #48adff;
              }
              #layout .left {
                  float: left;
                  height: 300px;
                  width: 200px;
                  background-color: #ff4344;
              }
              #layout .right {
                  height: 400px;
                  background-color: #ff255f;
                  /*給右邊的這個盒子容器創建一個BFC, 這個容器裏面的內容就會沿着垂直方向延伸*/
                  overflow: auto;
                  /*overflow: auto;*/
                  /*display: table;*/
                  /*float: left;*/
                  /*position: fixed;*/
              }
          </style>
          <div class="left">
              LEFT
          </div>
          <div class="right">
              RIGHT
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
          </div>
      </section>
    1. BFC子元素即使是float元素也要參与運算
    <section id="float">
          <style>
              /*一個盒子內部的內容如果是浮動的話,那麼這個盒子的內容實際上是不參与父容器高度計算的*/
              #float {
                  background-color: red;
                  /*overflow: hidden;*/
                  float: left;
              }
              #float .float {
                  float: left;
                  font-size: 30px;
              }
          </style>
          <div class="float">
              我是浮動的元素
          </div>
    </section>

    3. 事件相關

    3.1 DOM事件

    3.1.1 DOM事件的級別有哪些?

    [!NOTE]
    DOM級別一共可以分為四個級別:DOM0級、DOM1級、DOM2級和DOM3級。而DOM事件分為3個級別:DOM0級事件處理,DOM2級事件處理和DOM3級事件處理。

    1. DOM0 : element.onclick = function(e) {}
      DOM1 :該標準中未涉及到事件綁定的相關東西
    2. DOM2 : element.addEventListener(‘click’, function(e){}, false), 一個DOM元素可以添加多個事件
    3. DOM3 : element.addEventListener(‘keyup’, function(e){}, false),在DOM2標準基礎上面增加了新的事件類型:鼠標事件,鍵盤事件,焦點事件

    3.1.2 DOM事件模型有哪些?

    1. 事件捕獲:從外向內, window -> document -> body -> button
    2. 事件冒泡:從內向外,button -> body -> document -> window

    3.1.3 DOM事件流?

    瀏覽器為當前的頁面與用戶進行交互的過程中,點擊鼠標後事件如何傳入和響應的呢?

      1. 捕獲階段:從外部容器開始向內
      1. 目標階段:事件通過捕獲到達目標階段
      1. 冒泡階段:從目標元素再上傳到window對象

    3.1.4 什麼事件可以代理?什麼事件不可以代理呢?

    什麼樣的事件可以用事件委託,什麼樣的事件不可以用呢?

    [!NOTE]

    1. 通常支持事件冒泡(Event Bubbling)的事件類型為鼠標事件和鍵盤事件,例如:mouseover, mouseout, click, keydown, keypress。
    2. 接口事件(指的是那些不一定與用戶操作有關的事件)則通常不支持事件冒泡(Event Bubbling),例如:load, change, submit, focus, blur。

    很明顯:focus 和 blur 都屬於不支持冒泡的接口事件。既然都不支持冒泡,那又如何實現事件代理呢?

    3.1.5 IE和DOM事件流的區別?

    IE採用冒泡型事件 Netscape使用捕獲型事件 DOM使用先捕獲后冒泡型事件

    1. 冒泡型事件模型: button -> div -> body (IE瀏覽器本身只支持Bubbling不支持Capturing)
    2. 捕獲型事件模型: body -> div-> button (Netscape事件流,網景瀏覽器公司)
    3. DOM事件模型: body -> div -> button -> button -> div -> body (先捕獲后冒泡,除了IE以外的其他瀏覽器都支持標準的DOM事件處理模型)

    [!NOTE]

    • 規範和瀏覽器實現的差別?
      • DOM2級事件規範的捕獲階段,事件從文檔節點document開始傳播,現代瀏覽器大多數都是從window對象開始傳播事件的;
      • DOM2級事件規範捕獲階段不涉及事件目標,現代瀏覽器大多數都在這個階段包含事件目標。

    3.1.6 事件對象event的屬性方法的差別?

            IE                    DOM
    cancelBubble = true    stopPropagation()    // 停止冒泡
    returnValue = false    preventDefault()     // 阻止元素默認事件
    srcEelement            target               // 事件目標

    3.1.7 描述DOM事件捕獲的具體流程?

    window -> document -> HTML標籤 -> body -> … -> 目標元素

    [!NOTE]
    關鍵點: 注意根節點是window這個對象的

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
    </head>
    <body>
      <div id="container">
          <style>
              #container {
                  width: 200px;
                  height: 200px;
                  background-color: #ff255f;
              }
          </style>
      </div>
      <script>
          // 事件捕獲機制
          window.addEventListener('click', function(){
              console.log('window capture');
          }, true)
          document.addEventListener('click', function () {
              console.log('document capture');
          }, true)
          document.documentElement.addEventListener('click', function () {
              console.log('HTML capture');
          }, true)
          document.body.addEventListener('click', function () {
              console.log('body capture');
          }, true)
          document.getElementById('container').addEventListener('click', function () {
              console.log('container capture');
          }, true)
    
          // 事件冒泡機制
          window.addEventListener('click', function(){
              console.log('window capture');
          })
          document.addEventListener('click', function () {
              console.log('document capture');
          })
          document.documentElement.addEventListener('click', function () {
              console.log('HTML capture');
          })
          document.body.addEventListener('click', function () {
              console.log('body capture');
          })
          document.getElementById('container').addEventListener('click', function () {
              console.log('container capture');
          })
    
          // 輸出結果
          window capture  --> document capture --> HTML capture --> body capture --> container capture --> container capture -->  body capture --> HTML capture --> document capture --> window capture
      </script>
    </body>
    </html>

    3.1.8 如何拿到HTML這個標籤節點元素呢?(加分項)

      var html = document.documentElement;

    3.1.9 描述Event對象的常見應用?

    1. e.preventDefault() : 阻止默認事件(如阻止a標籤的默認跳轉行為)
    2. e.stopPropagation() : 阻止事件冒泡的行為
    3. *** e.stopImmediatePropagation() : 事件響應的優先級的應用場景,如果一個元素綁定了多個事件,但是又不想讓其他的事件執行的時候使用該方法【也會阻止冒泡】
    4. e.currentTarget : 當前所綁定的事件對象
      document.documentElement.onclick = function(e) {
        console.log(e.currentTarget, e.target);       // <html><body>...</body></html>()給綁定事件的那個元素, 當前被點擊的那個元素
      }

    [!NOTE]
    e.target : 當前被點擊的元素,父元素使用事件代理的方式來實現,可以直接使用該屬性獲取被點擊的那個元素

    3.2 如何自定義事件?(重點))

    3.2.1 如何給一個按鈕綁定一個自己定義的事件呢?

      // v1. 使用Event對象來自定義事件
      // 開始創建一個自己定義的事件對象
      var eve = new Event('customEvent');
      // 使用dom2事件處理的方式來給這個元素綁定一個事件
      var dom = document.documentElement;
      dom.addEventListener('customEvent', function(e) {
        console.log('customEvent called!');
      });
      // 下面的這句話可以在適合的場景中來觸發一個自己定義的事件對象
      setTimeout(function(){
        // 在1s之後觸發這個事件
        dom.dispatchEvent(eve);
      }, 1000)
    
    
      // v2. 使用CustomEvent來實現自定義事件
      var dom = document.documentElement;
      // 使用CustomEvent的方式可以在事件觸發的時候傳遞一個參數,然後通過e.detail 的方式來獲取這個參數信息
      var myClick = new CustomEvent('myClick', {detail : {name : 'zhangsan', age : 24}});
      dom.addEventListener('myClick', function(e){
        console.log(e.detail, e.target)
      })
      dom.dispatchEvent(myClick);
    

    4. HTTP協議

    4.1 HTTP協議的主要特點?

    • 簡單快速
    • 靈活
    • 無連接
    • 無狀態

    4.2 HTTP報文的組成部分?

    • 請求報文
      請求行:請求方法 資源地址 HTTP版本
      請求頭: key : value
      空行 :
      請求體 : name=zhangsan&age=18
    • 響應報文 : HTTP版本 狀態碼
      狀態行
      響應頭
      空行
      響應體

    4.3 HTTP方法?

    • GET : 獲取資源
    • POST : 傳輸資源
    • PUT :更新資源
    • DELETE : 刪除資源
    • HEAD :獲取報文首部
    • OPTIONS : 允許客戶端查看服務器的性能。

    4.4 POST和GET的區別?

    1. GET請求在瀏覽器回退的時候是無害的,而POST會再次提交請求
    2. GET請求產生的URL地址可以被收藏,而POST不可以
    3. GET請求會被瀏覽器主動緩存,而POST不會,除非主動設置
    4. GET請求只能進行URL編碼,而POST支持多種編碼方式
    5. GET請求參數會被完整第保留在瀏覽器的歷史記錄裏面,而POST參數不會被保留
    6. GET請求愛URL中傳送的參數的長度是有限的(2KB),而POST沒有限制
    7. 對參數的數據類型,GET值接受ASCII字符,而POST沒有限制
    8. POST比GET更安全,GET參數直接暴露在URL上,所以不能用來傳遞敏感信息
      9. GET參數通過URL傳遞,POST參數直接放在了Request body中

    4.5 HTTP狀態碼?

    4.5.1 狀態碼的第一位

    • 1xx :指示信息-表示請求已接收,繼續處理(重點)
    • 2xx :成功-表示請求已被成功接收
    • 3xx :重定向-要完成請求必須進行更進一步的操作
    • 4xx :客戶端錯誤-請求有語法錯誤或請求無法實現
    • 5xx :服務器錯誤-服務器未能實現合法的請求

    4.5.2 狀態碼詳解

    • 200 OK : 客戶端請求成功
    • 206 Partial Content : 客戶端發送了一個帶有Range頭的GET請求(Video標籤或者audio標籤在請求數據的時候)
    • 301 Moved Permanently : 請求的頁面已經轉移到了新的URL
    • 302 Found : 所請求的頁面已經臨時轉移到了新的URL
    • 304 Not Modified :客戶端有緩衝的文檔併發出了一個條件下的請求,原來緩衝的文檔還可以繼續使用
    • 400 Bad Request : 客戶端請求有語法錯誤,不被服務器所理解
    • 401 Unauthorized : 請求未經授權,這個狀態碼必須和WWW-Authenticate報頭域一起使用
    • 403 Forbidden:對被請求頁面的訪問被禁止
    • 404 Not Found : 請求資源不存在
    • 500 Internal Server Error :服務器發生不可預期的錯誤,原來緩衝的文檔還可以繼續使用
    • 503 Service Unavailable : 請求未完成,服務器臨時過載或宕機,一段時間后可能恢復正常

    4.6 什麼是持久連接?

    [!NOTE]
    HTTP協議採用‘請求-應答’模式, HTTP1.1版本才支持的,使用Keep-alive字段可以建立一個長連接,從而不需要每次請求都去建立一個新的連接。

    4.7 什麼是管線化?

    4.7.1 基本概念

    • 在使用持久連接(Keep-alive)的情況下,某個連接上的消息的傳遞類似於:請求1 –> 響應1 –> 請求2 –> 響應2 –> 請求3 –> 響應3
    • 管線化的過程: 請求1 –> 請求2 –> 請求3 –> 響應1 –> 響應2 –> 響應3

    4.7.2 管線化的特點(特點)

    1. 管線化機制通過持久連接完成,僅在HTTP1.1版本之後支持
    2. 只有GET和HEAD請求可以進行管線化,POST有所限制的
    3. 初次創建連接的時候不應該啟動管線機制,因為對方(服務器)不一定支持HTTP1.1版本的協議
    4. 管線化不會影響到響應到來的順序,HTTP響應返回的順序並未改變
    5. HTTP1.1 要求服務器支持管線化,但並不要求服務器也對響應進行管線化處理,只是要求對於管線化的請求不失敗即可
    6. 由於上面提到的服務器端問題,開啟管線化很可能並不會帶來大幅度的性能提升,而且很多服務器和代理程序對管線化的支持並不好,因此現代的瀏覽器如Chrome和Firefox默認並沒有開啟管線化支持

    5. 原型鏈

    5.1 創建對象的幾種方法?

    // 1. 使用字面量的方式來創建
    var o1 = {name : 'zhangsan'};
    var o11 = new Object({name : 'zhangsan'});
    
    // 2. 使用普通構造函數的方式來創建
    var M = function(){
        this.name = 'zhangsan';
    }
    var o2 = new M();
    
    // 3. Object.create方法
    var p = {name : 'zhangsan'};
    var o3 = Object.create(p);

    5.2 原型、構造函數、實例、原型鏈?

    構造函數:使用new運算符來聲明一個實例(任何函數都是可以通過構造函數來使用的)

    原型鏈:通過原型鏈可以找到上一級別的原型對象

    原型對象:多個實例公用的數據和屬性或者方法

    5.3 instanceof的原理?

    [!NOTE]
    instanceof 檢測一個對象A是不是另一個對象B的實例的原理是:查看對象B的prototype指向的對象是否在對象A的[[prototype]]鏈上。如果在,則返回true,如果不在則返回false。不過有一個特殊的情況,當對象B的prototype為null將會報錯(類似於空指針異常)。

    // 2. 使用普通構造函數的方式來創建
    var M = function(){
      this.name = 'zhangsan';
    }
    var o2 = new M();
    undefined
    o2.__proto__ == M.prototype
    true
    o2.__proto__ == M.prototype
    true
    o2.__proto__.constructor === Object
    false
    o2.__proto__.constructor === M
    true

    5.4 new運算符的原理?

    1. 一個新對象被創建。它繼承於foo.prototype
    2. 構造函數foo被執行。執行的時候,相應的傳參會被傳入,同時上下文(this)會被指定為這個新實例,new foo等同於 new foo(),只能用在不傳遞任何參數的情況
    3. 如果構造函數返回了一個“對象”,那麼這個對象會取代整個new出來的結果。如果構造函數沒有返回對象,那麼new 出來的結果為步驟1創建的對象
    // new 一個對象的過程
    var _new = function (fn) {
      // 1. 創建一個對象,這個對象要繼承fn這個構造函數的原型對象
      var o = Object.create(fn.prototype);
      // 2. 執行構造函數
      var k = fn.call(o, arguments);
      // 3. 看下執行的這個函數的運行效果是不是函數
      if (typeof k === 'object'){
          return k;
      }
      else
      {
          return o;
      }
    }

    6. 面向對象

    6.1 類與繼承:如何實現繼承,繼承的幾種實現方式

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
    </head>
    <body>
    <script>
      // 類的聲明
      function Animal1() {
          this.name = 'name';
      }
      // ES6 中的class的聲明
      class Animal2 {
          constructor(){
              this.name = 'name';
          }
      }
    
      console.log(new Animal1(), new Animal2());
      ///////////////////////////////////////////////////////////////////////////////////////////
    
    
      // 如何實現類的繼承呢???-----------本質:原型鏈
      // v1. 藉助構造函數實現繼承
      function Parent1() {
          this.name = 'parent1'
      }
      Parent1.prototype.sayHello = function () {
          console.log('hello');
      }
      function Child1() {
          // 執行父親的構造函數:
          // 1. 實現原理:將父級函數的this指向了這個子類的實例上面去了
          // 2. 缺點:父親的原型鏈上面的方法或者屬性不能被繼承;只能實現部分繼承
          Parent1.call(this);
          this.type = 'child1';
      }
      // 沒有參數的時候,可以直接new + 函數名稱
      console.log(res = new Child1);
    
    
    
    
      // v2. 藉助原型鏈實現繼承
      function Parent2() {
          this.name = 'parent2';
          this.data = [1, 2, 3];
      }
      Parent2.prototype.sayHello = function () {
          console.log('hello');
      }
      function Child2() {
          this.type = 'child2';
      }
      // prototype 就是為了讓這個對象的實例可以訪問到原型鏈上的內容
      Child2.prototype = new Parent2();
      // new Child2().__proto__ === Child2.prototype  // true
      // new Child2().__proto__.name                  // parent2
      // 原型鏈繼承的缺點:
      // 1. 原理:通過修改原型鏈來實現對象的繼承關係
      // 2. 缺點:修改第一個對象上面的屬性,會直接修改第二個對象屬性數據(引用類型)
      var c1 = new Child2();
      var c2 = new Child2();
      c1.data.push(100, 200, 300);
    
      // v3. 組合繼承
      function Parent3() {
          this.name = 'parent3';
          this.data = [1, 2, 3];
      }
      function Child3() {
          // 1. 借用構造函數繼承
          Parent3.call(this);
          this.type = 'child3';
      }
      // 2. 原型鏈繼承
      // child3的原型對象是Parent3的一個實例對象,但是這個實例對象中是沒有constructor這個屬性的,因此尋找屬性的時候回沿着這個實例對象的原型鏈繼續向上尋找new Parent3().prototype 這個原型對象的,
      // 最終在Parent3.prototype這個原型對象中找到了這個屬性,new一個對象找的實際上是{Parent3.prototype.constructor : Parent3}
      Child3.prototype = new Parent3();
      var c1 = new Child3();
      var c2 = new Child3();
      c1.data.push(100, 200, 300);
      // 組合繼承的特點:
      // 1. 原理:結合借用構造函數繼承和原型鏈繼承的優點,摒棄二者的缺點
      // 2. 缺點:父類構造函數在創建實例的時候總共執行了兩次(new Parent3(), new Child3())
    
    
      // v4. 組合繼承的優化1
      function Parent4() {
          this.name = 'parent4';
          this.data = [1, 2, 3];
      }
      function Child4() {
          // 1. 借用構造函數繼承
          Parent4.call(this);
          this.type = 'child4';
      }
      // 讓子類的構造函數的原型對象和父類構造函數的原型對象執向同一個對象(都是同一個對象)
      Child4.prototype = Parent4.prototype;
      // 測試
      var c1 = new Child4();
      var c2 = new Child4();
      console.log(c1 instanceof Child4, c1 instanceof Parent4);
      console.log(c1.constructor)         // Parent4? 如何實現:c1.constructor(c1.__proto__.constructor) === Child4 呢?
      // 缺點:
      // 1. 無法通過原型對象的constructor屬性來獲取對象的屬性對應的構造函數了(子類和父類公用的是一個contructor)
      // 2. obj instanceof Child4 === true; obj instanceof Parent4 === true
      // 3. obj.__proto__.constructor === Child4; obj.__proto__.constructor === Parent4  ???
    
      // v5. 組合繼承的優化2【完美寫法】
      function Parent5() {
          this.name = 'parent5';
          this.data = [1, 2, 3, 4, 5];
      }
      function Child5(){
          Parent5.call(this);
          this.type = 'child5';
      }
    
      // 通過創建中間對象的方式來把兩個對象區分開
      // var obj = new Object(); obj.__proto__ = Constructor.prototype;
      // 1. Object.create創建的對象obj, 這個obj的原型對象就是參數
      // 2. Child5的原型對象是Child5.prototype
      // 3. Child5.prototype = obj,obj這個對象相當於就是一个中間的橋樑關係
      Child5.prototype = Object.create(Parent5.prototype);
      // 當前的方式還是會按照原型鏈一級一級向上尋找的, 給Child5的原型對象上面綁定一個自己定義的constructor屬性
      Child5.prototype.constructor = Child5;
    
      // var s1 = new Child5()
    
      // 上面的代碼等價於
      var obj = Object.create(Parent5.prototype);     // obj.prototype = Parent5.prototype
      Child5.prototype = obj;
      Child5.prototype.constructor = Child5;
      // 1. 對象之間就是通過__proto__ 屬性向上尋找的
      // 2. 尋找規則: child5 ---> Child5.prototype ---> obj(Object.create(Parent5.prototype)) ---> Parent5.prototype
    
    
      // 技巧:不要讓面試官問太多題目:拖拉時間【擠牙膏】,把一個問題盡量吃透
      // 消化這一塊內容
    </script>
    </body>
    </html>

    [!WARNING]
    面試技巧

    1. 不要讓面試官問太多題目:拖拉時間【擠牙膏】,把一個問題盡量吃透
    2. 知識深度

    7. 通信

    7.1 什麼是同源策略個限制?

    [!NOTE]
    同源策略限制是從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。(一個源的文檔或腳本是沒有權利直接操作另外一個源的文檔或腳本的)

    1. Cookie, LocalStorage和IndexDB無法讀取
    2. DOM無法獲得;(document.body是無法獲取的)
    3. Ajax請求不能發送

    7.2 前後端如何進行通信呢?

    1. Ajax(有同源策略限制);Fetch API則是XMLHttpRequest的最新替代技術, 它是W3C的正式標準
    2. WebSocket:支持跨域請求數據,沒有同源策略的限制
    3. CORS:新的協議通信標準;CORS則將導致跨域訪問的請求分為三種:Simple Request,Preflighted Request以及Requests with Credential;cors相對於jsonp而言的好處就是支持所有的請求方式,不止是get請求,還支持post,put請求等等,而它的缺點就很明顯,無法兼容所有的瀏覽器,對於要兼容到老式瀏覽器而言,還是使用jsonp好點

    7.3 如何創建Ajax呢?

    1. XMLHttpRequest對象的工作流程
    2. 瀏覽器的兼容性處理【重點】
    3. 事件的觸發條件
    4. 事件的觸發順序
      function ajax(params){
        // 1. 創建對象,考慮兼容性【重點】
        var xhr = XMLHTTPRequest ? new XMLHTTPRequest() : new window.ActiveXObject('Microsoft.XMLHTTP');      // *** 兼容性問題必須考慮
        // 2. 打開連接
        var type = params.type || 'GET',
            url = params.url || '',
            data = params.data || {},
            success = params.success,
            error = params.error,
            dataArr = [];
        for (var k in data) {
          dataArr.push(k + '=' + data[k]);
        }
        //帶上Cookie
        xhr.withCredentials = true;
        if (type.toUpperCase() === 'GET') {
          // get
          url += '?' + dataArr.join('&');
          // 問號結尾的話,直接替換為空字符串
          xhr.open(type, url.replace(/\?$/g, ''), true);
          // GET 請求的話,是不需要再send方法中帶上參數的
          xhr.send();
        }
        else {
          // POST
          xhr.open(type, url, true);
          xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
          // POST 請求需要把數據放在send方法裏面, data = name=zhangsna&age=18&sex=male
          xhr.send(dataArr.join('&'));
        }
        // 開始監聽變化
        xhr.onreadystatechange = function(){
          // 這裏需要考慮強緩存和協商緩存的話直接處理,206是媒體資源的創建方式
          if (xhr.readyState === 4 && xhr.status === 200 || xhr.status === 304) {
              var res;
              if (success instanceof Function) {
                res = xhr.responseText;
                if (typeof res === 'string') {
                  res = JSON.parse(res);
                  // 開始執行成功的回調函數
                  success.call(xhr, res);
                }
              } else {
                if (error instanceof Function) {
                  // 失敗的話直接返回這個responseText中的內容信息
                  error.call(xhr, res);
                }
              }
          }
        }
      }

    7.4 跨域通信的幾種方式?

    7.4.1 JSONP

      function jsonp(url, onsuccess, onerror, charset){
        // 1. 全局註冊一個callback
        var callbackName = 'callback' + Math.random() * 100;
        window[callbackName] = function(){
          if (onsuccess && typeof onsuccess === 'Function') {
            onsuccess(arguments[0]);
          }
        }
        // 2. 動態創建一個script標籤
        var script = document.createElement('script');
        script.setAttribute('type', 'text/javascript');
        charset && script.setAttribute('charset', charset);
        script.setAttribute('src', url);
        script.async = true;
        // 3. 開始監聽處理的過程
        script.onload = script.onreadystatechange = function(){
          if (!script.readyState || /loaded|complete/.test(script.readyState)) {
            // 4. 成功之後移除這個事件
            script.onload = script.onreadystatechange = null;
            // 刪除這個script的DOM對象(head.removeChild(script), 這個DOM節點的父節點相當於是head標籤這個父節點)
            script.parentNode && script.parentNode.removeChild(script);
            // 刪除函數或變量
            window[callbackName] = null;
          }
        }
        script.onerror = function(){
          if (onerror && typeof onerror === 'Function') {
            onerror();
          }
        }
        // 5. 開始發送這個請求(把這個標籤放在頁面中的head標籤中即可)
        document.getElementsByTagName('head')[0].appendChild(script);
      }

    7.4.2 Hash

    hash 改變后頁面不會刷新的

    [!NOTE]
    使用場景:當前的頁面A通過iframe或者frame嵌入了跨域的頁面

      // 1. A頁面中的代碼如下
      var B = document.getElementsByTagName('iframe');
      B.src = B.src + '#' + JSON.stringfy(data);
      // 2. B中的偽代碼如下
      window.onhashchange = function(){
        var data = window.location.hash;    // 接受數據
        data = JSON.parse(data);
      }

    7.4.3 postMessage(HTML5中新增)

    [!NOTE]
    使用場景: 可以實現窗口A(A.com)向窗口B(B.com)發送信息

      // 1. 窗口B中的代碼如下
      var BWindow = window;
      BWindow.postMessage(JSON.stringfy(data), 'http://www.A.com');   
      // 2. 窗口A中代碼
      var AWindow = window;
      AWindow.addEventListener('message', function(e){
          console.log(e.origin);                  // http://www.B.com
          console.log(e.source);                  // BWindow
    
          e.source.postMessage('已成功收到消息');
    
          console.log(JSON.parse(e.data));        // data
      }, false)
      // 父窗口給子窗口發信息,需要用iframe的contentWindow屬性作為調用主體
      // 子窗口給父窗口發的信息需要使用window.top,多層iframe使用window.frameElement

    7.4.4 . WebSocket

    [!NOTE]
    不受同源策略影響,可以直接使用

      var ws = new window.WebSocket('ws://echo.websocket.org');
    
      // 打開連接
      ws.onopen = function(e){
        console.log('Connection open ……');
        ws.send('Hello WebSocket!');
      }
    
      // 接受消息
      ws.onmessage = function(e){
        console.log('Received Message : ', e.data);
      }
    
      // 關閉連接
      ws.onclose = function(e){
        console.log('Connection closed');
      }

    7.4.5 CORS

    支持跨域通信版本的Ajax,是一種新的標準(Origin頭)【ajax的一個變種,適用於任何】

      fetch('/get/name', {
        method : 'get'
      }).then(function(response){
        console.log(response);
      }).catch(function(err){
        // 出錯了;等價於then的第二個參數
      });
      // 原因:瀏覽器默認會攔截ajax請求,會根據頭中的origin消息進行判斷處理消息;Origin字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。JSONP只支持GET請求,CORS支持所有類型的HTTP請求。JSONP的優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。
    7.4.5.1 CORS請求的基本流程
    1. 對於簡單請求,瀏覽器直接發出CORS請求。具體來說,就是在頭信息之中,增加一個Origin字段。
    2. Origin字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
    3. 如果Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。
    4. 如果Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。
      Access-Control-Allow-Origin: http://api.bob.com   // 必需的字段
      Access-Control-Allow-Credentials: true            // 可選字段: 是否允許發送cookie
      Access-Control-Expose-Headers: FooBar
      Content-Type: text/html; charset=utf-8
    1. 簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為”預檢”請求(preflight)。OPTIONS表示當前的這個請求是用來詢問的;服務器收到”預檢”請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以後,確認允許跨源請求,就可以做出回應。
    7.4.5.2 JSONP和CORS的區別?
    1. JSONP只支持GET請求,CORS支持所有類型的HTTP請求
    2. JSONP的優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。

    8. 安全

    8.1 CSRF

    8.1.1 基本概念和縮寫

    CSRF: 跨站請求偽造,Cross site request forgery

    8.1.2 CSRF 攻擊原理

    8.1.3 可以成功攻擊的條件?

    1. 目標網站存在CSRF漏洞的請求接口(一般為get請求)
    2. 目標用戶之前已經成功登錄過這個網站(留下了Cookie)

    8.1.4 如何防禦呢?

    1. Token驗證:訪問服務器接口的時候,會自動帶上這個token
    2. Referer驗證:驗證網站的頁面來源(只有我當前網站下的頁面才可以請求,對於來自其他網站的請求一律攔截)
    3. 隱藏令牌: 隱藏信息會放在header中(類似於Token)

    8.2 XSS

    8.2.1 基本概念和縮寫

    XSS: cross-site scripting, 跨站腳本攻擊

    8.2.2 XSS防禦

    攻擊原理: 注入JS腳本

    防禦措施: 讓JS代碼無法解析執行

    8.3 CSRF和XSS的區別呢?

    1. CSRF:網站本身存在漏洞的接口,依賴這些登錄過目標網站的用戶來實現信息的竊取;
    2. XSS:向頁面中注入JS執行,JS函數體內執行目標任務;

    [!NOTE]

    1. 一定要說出中文名稱,實現原理,防範措施都說出來
    2. 不要拖泥帶水,言簡意賅

    9. 算法

    [!NOTE]
    算法攻略:多刷題才是硬道理!!!

    二三面(知識深度面)

    10. 渲染機制

    10.1 什麼是DOCTYPE及作用?

    1. DTD(Document Type Definition):文檔類型定義,是一系列的語法規則,用來定義XML或者(X)HTML的文件類型。瀏覽器會使用它來判斷文檔的類型,決定使用哪一種協議來解析,以及切換瀏覽器模式;
    2. DOCTYPE: 是用來聲明文檔類型和DTD規範的,一個主要的用途是文件的合法性驗證;如果文件代碼不合法,那麼瀏覽器解析的時候就會出現一些出錯
    3. 總結:Doctype就是通知瀏覽器當前的文檔是屬於那種類型的,包含哪些DTD。
      <!--HTML5的寫法-->
      <DOCTYPE html>
      <!-- HTML 4.01  Strict
        1. 這個DTD 包含所有的HTML元素和屬性
        2. 但是不包含展示性的和棄用的元素(比如font)
      -->
      <DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" >
      <!-- HTML 4.0.1 Transitional
        1. 這個DTD 包含所有的HTML元素和屬性
        2. 也包含展示性的和棄用性的元素(比如font)
      -->
      <DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd" >

    [!NOTE]
    在W3C標準出來之前,不同的瀏覽器對頁面渲染有不同的標準,產生了一定的差異。這種渲染方式叫做混雜模式。在W3C標準出來之後,瀏覽器對頁面的渲染有了統一的標準,這種渲染方式叫做標準模式。<!DOCTYPE>不存在或者形式不正確會導致HTML或XHTML文檔以混雜模式呈現,就是把如何渲染html頁面的權利交給了瀏覽器,有多少種瀏覽器就有多少種展示方式。因此要提高瀏覽器兼容性就必須重視<!DOCTYPE>

    10.2 嚴格模式和混雜模式

    [!NOTE]
    嚴格模式和混雜模式都是瀏覽器的呈現模式,瀏覽器究竟使用混雜模式還是嚴格模式呈現頁面與網頁中的DTD(文件類型定義)有關,DTD裡面包含了文檔的規則。比如:loose.dtd

    • 嚴格模式:又稱標準模式,是指瀏覽器按照W3C標準來解析代碼,呈現頁面
    • 混雜模式:又稱為怪異模式或者兼容模式,是指瀏覽器按照自己的方式來解析代碼,使用一種比較寬鬆的向後兼容的方式來显示頁面。

    10.3 瀏覽器的渲染過程?

    10.3.1 開始進行DOM解析,渲染DOM Tree

    10.3.2 開始進行CSS解析,渲染CSSOM Tree

    10.3.3 DOM樹和CSS樹的結合,最後會轉換為Render Tree

    10.3.4 Layout的過程,計算每一個DOM元素的位置、寬度、高度等信息,最終渲染並显示頁面到瀏覽器

    10.4 何時會觸發Reflow?

    [!NOTE]
    定義:DOM結構中每個元素都有自己的盒子模型,這些都是需要根據各種樣式來計算並根據計算結果將元素放在它該出現的位置,這個過程就是reflow;

    1. 當你增加、刪除、修改DOM節點的時候,會導致Reflow或Repaint
    2. 當你移動DOM的位置,或者設置動畫的時候
    3. 當你修改CSS樣式的時候
    4. 當你Resize窗口的時候(移動端沒有這個問題,與瀏覽器有關),或者在滾動窗口的時候
    5. 當你修改網頁的默認的字體的時候

    10.5 何時回觸發Repaint?

    [!NOTE]
    定義:當各種盒子的位置、大小以及其他屬性,例如顏色、字體大小都確定下來以後,瀏覽器於是便按照元素各自的特性繪製了一遍,於是頁面的內容出現了,這個過程就是repaint

    1. DOM改動
    2. CSS改動

    10.6 如何最大程度上的減少瀏覽器的重繪Repaint過程(頻率)呢?

    10.6.1 避免在document上直接進行頻繁的DOM操作,如果確實需要可以採用off-document的方式進行

      1. 先將元素從document中刪除,完成修改之後然後再把元素放回原來的位置
      1. 將元素的display設置為none, 然後完成修改之後再把元素的display屬性修改為原來的值
      1. 如果需要創建多個DOM節點,可以使用DocumentFragment創建完畢之後一次性地加入document中去
      var frag = document.createDocumentFragment();
      frag.appendChild(dom);    /*每次創建的節點先放入DocumentFragment中*/

    10.6.2 集中修改樣式

    1. 盡可能少的修改元素style上的屬性
    2. 盡量通過修改className來修改樣式(一次性修改)
    3. 通過cssText屬性來設置樣式值
      document.getElementById("d1").style.cssText = "color:red; font-size:13px;";

    10.6.3 緩存Layout的屬性值

    [!NOTE]
    對於Layout屬性中非引用類型的值(数字型),如果需要多次訪問則可以在一次訪問時先存儲到局部變量中,之後都使用局部變量,這樣可以避免每次讀取屬性時造成瀏覽器的渲染。

      var width = el.offsetWidth;
      var scrollLeft = el.scrollLeft;

    10.6.4 設置元素的position為absolute或fixed

    [!NOTE]
    在元素的position為static和relative時,元素處於DOM樹結構當中,當對元素的某個操作需要重新渲染時,瀏覽器會渲染整個頁面。將元素的position設置為absolute和fixed可以使元素從DOM樹結構中脫離出來獨立的存在,而瀏覽器在需要渲染時只需要渲染該元素以及位於該元素下方的元素,從而在某種程度上縮短瀏覽器渲染時間。

    11. 布局Layout?

    Layout屬性包括

    1. offsetLeft、offsetTop、offsetHeight、offsetWidth: 相對於父對象的邊距信息,且返回值為数字;left獲取或設置相對於具有定位屬性(position定義為relative)的父對象的邊距信息,返回值為字符串10px
    2. scrollTop/Left/Width/Height:滾動條在各個方向上拉動的距離,返回值為数字
    3. clientTop/Left/Width/Height:瀏覽器的可視區域的大小
    4. getComputedStyle()、currentStyle(in IE):瀏覽器渲染DOM元素之後的寬度和高度等樣式信息

    12. JS運行機制

    12.1 如何理解JS的單線程?

    看代碼,寫結果?

      // 同步任務
      console.log(1);
      // 異步任務要掛起
      setTimeout(function(){
        console.log(2)
      }, 0);
      console.log(3)
      // out : 1 3 2
      console.log('A');
      setTimeout(function(){
        console.log('B')
      }, 0);
      while (true) {
    
      }
    
      // out : A

    12.2 什麼是任務隊列?

      for (var i = 0; i < 4; i++) {
        // setTimeout , setInterval 只有在時間到了的時候,才會把這個事件放在異步隊列中去
        setTimeout(function(){
          console.log(i);
        }, 1000);
      }
      // out : 4 4 4 4

    12.3 什麼是Event Loop?

    [!NOTE]
    JS是單線程的,瀏覽器引擎會先來執行同步任務,遇到異步任務之後,會把當前的這個異步任務放在time模塊中,等到主線程中的所有的同步任務全部執行完畢之後;然後當前的這個異步任務只有時間到了之後,才會把這個任務(回調函數)放在一個異步隊列中;噹噹前的任務棧中的任務全部執行完畢了之後,會先去執行微任務隊列中的任務(Promise),然後等到微任務隊列中的所有任務全部執行完畢之後,再去執行process.nextTick()這個函數,等到這個函數執行完畢之後,本次的事件輪訓結束;
    開啟新的執行棧,從宏任務隊列中依次取出異步任務,開始執行;每個宏任務執行都會重新開啟一個新的任務執行棧

    12.3.1 3個關鍵點

    1. 執行棧執行的是同步任務;
    2. 什麼時候去異步隊列中取這個任務;
    3. 什麼時候向這個任務隊列中放入新的異步任務

      12.3.2 異步任務的分類

    • setTimeout, setInterval;
    • DOM事件(點擊按鈕的時候也會先去執行同步任務);
    • Promise

    13. 知識點總結

    1. 理解JS的單線程的概念
    2. 理解任務隊列
    3. 理解Event Loop
    4. 理解哪些語句會翻入到異步任務隊列
    5. 理解與放入到異步任務隊列的時機

      13.1 頁面性能

      13.1.1 提升頁面性能的方法有哪些?

    6. 資源壓縮合併,減少HTTP請求;
    7. 非核心代碼的異步加載 —> 異步加載的方式有哪些? —> 異步加載的區別?
    8. 利用瀏覽器的緩存 —> 緩存的分類 —> 緩存的原理
    9. 使用CDN加速
    10. 預解析DNS:DNS Prefetch 是一種DNS 預解析技術,當你瀏覽網頁時,瀏覽器會在加載網頁時對網頁中的域名進行解析緩存,這樣在你單擊當前網頁中的連接時就無需進行DNS的解析,減少用戶等待時間,提高用戶體驗。(提前解析域名,而不是點擊鏈接的時候才去進行DNS域名解析,可以節省DNS解析需要耗費的20-120毫秒時間)

      <!-- https協議的網站,默認是關閉了DNS的預解析的,可以使用下面的語句開啟  -->
      <meta http-equiv="x-dns-prefetch-control" content="on">
      <!-- 開始配置需要進行DNS預解析的域名 -->
      <link rel="dns-prefetch" href="//www.zhix.net">                               <!--支持http和HTTPS-->
      <link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />               <!--支持http的協議-->
      <link rel="dns-prefetch" href="http://nsclick.baidu.com" />
      <link rel="dns-prefetch" href="http://hm.baidu.com" />
      <link rel="dns-prefetch" href="http://eiv.baidu.com" />

    14. 異步加載的方式

    14.1 動態腳本的加載

      var script = document.createElement('script');
      document.getElementsByTagName('head')[0].appendChild(script);
    
      // 沒有 defer 或 async,瀏覽器會立即加載並執行指定的腳本,“立即”指的是在渲染該 script 標籤之下的文檔元素之前,也就是說不等待後續載入的文檔元素,讀到就加載並執行。
      <script src="script.js"></script>

    14.2 defer

    <!-- 有 defer,加載後續文檔元素的過程將和 script.js 的加載并行進行(異步),但是 script.js 的執行要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。 -->
    <script defer src="myscript.js"></script>

    14.3 async

      <!-- 有 async,加載和渲染後續文檔元素的過程將和 script.js 的加載與執行并行進行(異步)。 -->
      <script async src="script.js"></script>

    14.4 異步加載的區別?

    [!NOTE]

    1. defer是在HTML解析完成之後(DOMContentLoaded事件執行之後)才會執行,如果是多個,會按照加載的順序依次執行(按照順序執行)
    2. async是在加載完之後立即執行,如果是多個,執行順序和加載順序無關(與順序無關)

    15. 說一下瀏覽器的緩存機制吧?

    15.1 緩存的分類

    [!NOTE]
    緩存目的就是為了提升頁面的性能

    15.1.1 強緩存

    直接從本地讀取,不發送請求

        Response Headers
        cache-control: max-age=315360000(相對時間,優先級比expires高)
        expires: Sat, 10 Mar 2029 04:01:39 GMT(絕對時間)

    15.1.2 協商緩存

    問一下服務器,這個文件有沒有過期,然後再使用這個文件

        Response Headers
        last-modified: Tue, 12 Mar 2019 06:22:34 GMT(絕對時間)
        etag: "52-583dfb6f4de80"

    向服務器請求資源的時候,帶上if-Modified-Since或者if-None-Match這個請求頭,去詢問服務器:

        Request Headers
        if-Modified-Since: Tue, 12 Mar 2019 06:22:34 GMT
        if-None-Match: "52-583dfb6f4de80"

    16. 錯誤監控/如何保證前端產品的上線質量?

    16.1 前端錯誤的分類?

    1. 即時運行錯誤:代碼錯誤
    2. 資源加載錯誤:圖片/css/js文件加載失敗

    16.2 錯誤的捕獲方式?

    16.2.1 即時運行錯誤的捕獲方式

      // 方法一:使用try catch捕獲
      try {
        // ...
      } catch (e) {
        // error
      } finally {
        // handle error
      }
    
      // 方法二:使用window.onerror 捕獲錯誤
      // 無法捕獲到資源加載錯誤
      window.onerror = function(msg, url, line, col, error){
        // ...
      }  
      window.addEventListener('error', function(msg, url, line, col, error){
        // ...
      })

    16.2.2 資源加載錯誤(不會向上冒泡)

      // 方法一: 直接在script, img這些DOM標籤上面直接加上onerror事件
      Object.onerror = function(e){
          // ...
      }
    
      // 方法二:window.performace.getEntries(間接獲取資源加載錯誤的數量)
      var loadedResources = window.performance.getEntries();           // 1. 獲取瀏覽器中已經加載的所有資源(包括各個階段的詳細加載時間)
      var loaderImgs = loadedResources.filter(item => {
          return /\.jpg|png|gif|svg/.test(item.name)
      });
      var imgs = document.getElementsByTagName('img');                // 2. 獲取頁面中所有的img集合
      var len = imgs.length - loaderImgs.length;                      // 3. 加載失敗的圖片數量
      console.log('圖片加載失敗數量:', len, '條');
    
    
      // 方法三: 使用事件捕獲的方式來實現Error事件捕獲
      // 使用事件捕獲的方式來實現資源加載錯誤的事件的捕獲:window ---> document --> html --- > body ---> div ---...
      window.addEventListener('error', function (msg) {
          console.log(msg);
      }, true);

    16.2.3 補充的方法

          // 使用事件捕獲的方式來實現
         window.addEventListener('error', function (msg) {
             console.log('資源加載異常成功捕獲:', msg);
         }, true);
         // 使用事件冒泡的方式是只能捕獲到運行的時候的一些異常
         window.addEventListener('error', function (e) {
             console.log('運行異常成功捕獲1:', e.message, e.filename, e.lineno, e.colno, e.error);
         }, false);
    
         // 這種方式是可以按照參數的方式來接受相關的參數信息
         window.onerror = function (msg, url, line, col, error) {
             console.log('運行異常成功捕獲2:', msg, url, line, col, error);
         }

    16.2.4 問題的延伸:跨域的js運行錯誤可以捕獲嗎,錯誤提示是什麼?應該怎麼處理呢?

    16.2.4.1 錯誤信息

    errorinfo :
    Script0 error
    0 row
    0 col
    16.2.4.2 處理方法
    1. 第一步:在script標籤上增加crossorigin屬性
      <!-- script 表情添加crossorigin屬性 -->
      <!-- 除了 script,所有能引入跨域資源的標籤包括 link 和 img 之類,都有一樣的屬性 -->
      <script crossorigin  src="http://www.lmj.com/demo/crossoriginAttribute/error.js"></script>
    1. 第二步:設置js資源響應頭’Access-Control-Allow-Origin: * ‘,服務器端需要開啟
      // 服務器可以直接設置一個響應頭信息
      res.setResponseHeader('Access-Control-Allow-Origin', 'www.lmj.com');

    16.3 上報錯誤的基本原理?

    1. 採用Ajax通信的方式來上報
    2. 利用Image對象進行上報(cnzz)[重點理解掌握]
      // 下面的兩種方式都是可以實現錯誤信息的上報功能的
      (new Image).src = 'http://www.baidu.com?name=zhangsna&age=18&sex=male'
      (new Image()).src = 'https://www.baidu.com?name=zhangsan'

    17. 如何使用JS獲取客戶端的硬件信息呢?

      // IE 瀏覽器提供的獲取電腦硬件的API
      var locator = new ActiveXObject ("WbemScripting.SWbemLocator");
      var service = locator.ConnectServer(".");
      var properties = service.ExecQuery("SELECT * FROM Win32_Processor");

    18. 使用window.performace 來實現用戶體驗的數據記錄呢?

    [!NOTE]
    可以參考性能優化章節-performance性能監控一文內容。

    三四面(業務項目面)

    [!NOTE]

    • 知識面要廣
    • 理解要深刻
    • 內心要誠實:沒了解過,問面試官有哪些資料可以學習
    • 態度要謙虛
    • 回答要靈活:把握一個度,不要和面試官爭執對錯
      • 要學會讚美:被問住了可以回答,適當讚美(沒面試官理解的那麼深,虛心請教)

    19.介紹一下你做過的項目?

    19.1 項目介紹模板(業務能力體現)

    1. 我做過什麼業務?
    2. 負責的業務有什麼業績?
    3. 使用了什麼技術方案?
    4. 突破了什麼技術難點?
    5. 遇到了什麼問題?
    6. 最大的收穫是什麼?

    19.2 團隊協作能力

    19.3 事務推動能力

    19.4 帶人能力

    終面(HR面)

    20. 技術終面或HR面試要點

    [!NOTE]
    主要考察點:樂觀积極、主動溝通、邏輯順暢、上進有責任心、有主張,做事果斷、職業競爭力、職業規劃

    20.1 職業競爭力

    1. 業務能力:可以做到行業第一

    2. 思考能力:對同一件事可以從不同角度去思考,找到最優解

    3. 學習能力:不斷學習新的業務,沉澱、總結

    4. 無上限的付出:對於無法解決的問題可以熬夜、加班

    20.2 職業規劃

    1. 目標是什麼:在業務上成為專家,在技術上成為行業大牛

    2. 近階段的目標:不斷的學習積累各方面地經驗,以學習為主

    3. 長期目標:做幾件有價值的事情,如開源作品、技術框架等

    4. 方式方法:先完成業務上的主要問題,做到極致,然後逐步向目標靠攏

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

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

  • JAVA繼承中子父類的構造方法

    JAVA繼承中子父類的構造方法

     

    首先,構造方法本身會有一個隱式的無參構造(默認):

      ①不寫構造方法,類中的第一行代碼事實上有一個默認的無參構造(系統會隱式為你寫好)

        

    public class Student {
            private String name;
    //        public Student() {}      隱式地“寫”在這裏,你看不見
    
    //  Strudent類中雖然沒有寫構造方法,但相當於有上面的無參構造
    //   只不過是隱式的,你看不見
    
    }

     

      ②只寫帶參構造方法,相當於只有該帶參構造方法(隱式的無參構造會被屏蔽無視掉,視為無效)

    public class Student {
            private String name;
            public Student(String name) {
                this.name=name;
            }
    //  此時原來Strudent類中的隱式的無參構造方法被屏蔽了,無效了
    //  類中只有帶參構造
    
    }

     

      ③若想同時擁有無參和帶參構造,必須顯式地寫出無參和帶參構造方法

    public class Student {
            private String name;
            public Student() {}
    // 顯式地將無參構造寫出來        
            public Student(String name) {
                this.name=name;
            }
    //  若想Strudent類中擁有無參構造方法,必須顯式地寫出來
    
    
    }

     

    進一步結合繼承,就需要考慮到子父類:

      ④在子類的構造方法(無論是無參和有參)中,方法中的第一行代碼事實上都隱式地包含了父類的無參構造方法
        即: super()

    public class Stu extends Student {
        private String name;
        public Stu() {
        // super();
        // 在子類的無參構造中,super()是隱式的“寫”在這裏的
        }
        
        public Stu(String name) {
        // super();
        this.name=name;
        // 在子類的帶參構造,上面的super()同樣也是隱式的“寫”在這裏的
        }
    
    }

    這就是為什麼,調用子類的構造方法時,都會先調用父類的無參構造方法了,因為默認的super()存在。

     ⑤同理,類似與上面的②,此時若寫一個有參構造,super(xx)會把隱式的super()屏蔽掉 

    public class Stu extends Student {
        private String name;
        
        public Stu(String name) {
        // super();  原來隱式寫在這裏的super()被屏蔽了,無效了
        super(name);
        
        // 在子類的帶參構造, 由於的super(name)的存在,super()無效了 //此時子類的帶參構造中,只有super(name)
        }
    
    }

     

    這就是為什麼當父類沒有無參構造(即只有帶參構造——對應情況②)時,子類的構造方法編譯無法通過。這是因為子類的構造函數(帶參或無參)將調用父類的無參構造函數。 由於編譯器試圖向子類中的2個構造函數中插入super() ,但父類的默認構造函數未定義,因此編譯器會報告錯誤消息

    要解決這個問題,只需要

          1)添加一個無參構造函數給父類——顯式地在父類中添加無參構造

          2)刪除父類中自定義的有參構造函數——等價於恢復了默認的無參構造
          3)將 Super(XXX) 添加到子類構造函數——通過⑤的原來來屏蔽默認的super()  

     

    一些關於子父類有參無參的組合情況(其實就是排列組合)練習,有興趣的可以自己驗證一下,如下(圖片來源):

     

     

     

     

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

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • Spring框架學習總結(上)

    Spring框架學習總結(上)

    目錄

    @

    1、Spring的概述

    在學習SSM框架中,我建議初學者最好先學Spring框架,其次mybatis接着springMVC,先學mybatis當然也是可以的,今天我們就以絕對優雅的姿態闖進Spring世界,系好安全帶,準備好了嗎,出發了哦!!!咳咳….平時開發接觸最多的估計就是IOC容器,它可以裝載bean(所謂的bean也就是我們java中的類,當然也包括servicedao裏面),有了這個機制,我們就不用在每次使用這個類的時候為它初始化,很少看到鍵字new。另外spring的aop,事務管理等等都是我們經常用到的,可見Spring的尤為重要的作用Spring的核心是控制反轉(IoC)和面向切面(AOP)

    1.1什麼是Spring

    肯定有熊dei會問SE/EE開發的一站式框架所謂的一站式是什麼意思,(哼,人類,我早就猜到你會問了)
    所謂一站式框架指的是有EE開發的每一層解決方案。

    WEB層 :SpringMVC

    Service層 :Spring的Bean管理,Spring聲明式事務

    DAO層 :Spring的Jdbc模板,Spring的ORM模塊

    1.2為什麼學習Spring

    俗話說,人狠話不多(兄嘚看圖)

    1.3Spring的版本

    Spring3.x、Spring4.x和Spring5.x

    1.4Spring的體繫結構

    正所謂,人狠話不多(兄嘚看圖)

    2、Spring的入門(IOC)

    2.1什麼IOC

    一說起IOC我就想起了武哥對IOC的理解的幾個例子,可謂通俗易懂,非常適合剛入門Spring的兄嘚!有興趣的可以去了解了解武哥,武哥博客:https://blog.csdn.net/eson_15

    IOC(Inverse of Control):控制反轉,也可以稱為依賴倒置。
    控制反轉:將對象的創建權反轉給(交給)Spring。

    所謂依賴,從程序的角度看,就是比如A要調用B的方法,那麼A就依賴於B,反正A要用到B,則A依

    賴於B。所謂倒置,你必須理解如果不倒置,會怎麼著,因為A必須要有B,才可以調用B,如果不倒

    置,意思就是A主動獲取B的實例:B b = new B(),這就是最簡單的獲取B實例的方法(當然還有各種

    設計模式可以幫助你去獲得B的實例,比如工廠、Locator等等),然後你就可以調用b對象了。所

    以,不倒置,意味着A要主動獲取B,才能使用B;到了這裏,就應該明白了倒置的意思了。倒置就是

    A要調用B的話,A並不需要主動獲取B,而是由其它人自動將B送上門來。

    2.2通俗理解IOC

    形象的舉例就是:
    通常情況下,假如你有一天在家裡口渴了,要喝水,那麼你可以到你小區的小賣部去,告訴他們,你需要一瓶水,然後小賣部給你一瓶水!這本來沒有太大問題,關鍵是如果小賣部很遠,那麼你必須知道:從你家如何到小賣部;小賣部里是否有你需要的水;你還要考慮是否開着車去;等等等等,也許有太多的問題要考慮了。也就是說,為了一瓶水,你還可能需要依賴於車等等這些交通工具或別的工具,問題是不是變得複雜了?那麼如何解決這個問題呢?
    解決這個問題的方法很簡單:小賣部提供送貨上門服務,凡是小賣部的會員,你只要告知小賣部你需要什麼,小賣部將主動把貨物給你送上門來!這樣一來,你只需要做兩件事情,你就可以活得更加輕鬆自在:
    第一:向小賣部註冊為會員。
    第二:告訴小賣部你需要什麼。

    這和Spring的做法很類似!Spring就是小賣部,你就是A對象,水就是B對象
    第一:在Spring中聲明一個類:A
    第二:告訴Spring,A需要B

    假設A是UserAction類,而B是UserService類

    <bean id="userService" class="org.leadfar.service.UserService"/>
    <bean id="documentService" class="org.leadfar.service.DocumentService"/>
    <bean id="orgService" class="org.leadfar.service.OrgService"/>
     
    <bean id="userAction" class="org.leadfar.web.UserAction">
         <property name="userService" ref="userService"/>
    </bean>

    在Spring這個商店(工廠)中,有很多對象/服務:userService,documentService,orgService,也有很多會員:userAction等等,聲明userAction需要userService即可,Spring將通過你給它提供的通道主動把userService送上門來,因此UserAction的代碼示例類似如下所示:

    package org.leadfar.web;
    public class UserAction{
         private UserService userService;
         public String login(){
              userService.valifyUser(xxx);
         }
         public void setUserService(UserService userService){
              this.userService = userService;
         }
    }
    

    在這段代碼裏面,你無需自己創建UserService對象(Spring作為背後無形的手,把UserService對象通過你定義的setUserService()方法把它主動送給了你,這就叫依賴注入!),當然咯,我們也可以使用註解來注入。Spring依賴注入的實現技術是:動態代理

    2.3下載Spring的開發包以及解壓說明

    官網下載:http://spring.io/
    什麼?不會下載?what???
    好吧,已打包好了QAQ:https://pan.baidu.com/s/18wyE-5SRWcCu12iPOX56pg
    什麼?沒有網盤?what???
    有事請燒香謝謝…

    解壓之後,文件說明:
    docs :Spring的開發規範和API
    libs :Spring的開發的jar和源碼
    schema :Spring的配置文件的約束

    2.4創建web項目,引入jar包

    2.5創建普通接口和實現類

    創建普通接口,定義一個eat方法

    package com.gx.sping;
    
    public interface IUserDao {
    
        public void eat();
    }
    

    創建普通實現類

    package com.gx.sping;
    
    public class UserDaoimpl implements IUserDao {
      @Override
        public void eat() {
            // TODO Auto-generated method stub
            System.out.println(用戶eat了");
        }
    }
    

    2.6Spring的IOC底層實現原理

    創建普通接口和類出現的問題:
    如果底層的實現切換了,需要修改源代碼,能不能不修改程序源代碼對程序進行擴展?
    重點來了,要想不改變源碼,Spring的IOC就能實現!如下圖:Spring的IOC底層實現

    2.7將實現類交給Spring管理

    1、在classpath下(也就是src)創建一個XML文件

    2、文件名最好統一叫applicationContext.xml

    3、其xml文件的內容頭為schema約束

    4、約束文件位置在spring的解壓路徑下lspring-framework-4.2.4.RELEASE\docs\spring-framework-reference\html\xsd-configuration.htm

    5、不要求xml文件的內容頭能夠背出來,但要了解的是你要知道它是怎麼來的

    6、xml文件的內容頭添加后,將實現類交給Spring管理

    applicationContext.xml配置文件如下

    <?xml version="1.0" encoding="UTF-8"?>
    <beans 
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
     <!-- 實現類UserDaoimpl交給Spring管理 -->
         <bean id="IuserDao" class="com.gx.Ioc.UserDaoimpl" ></bean>
    </beans>

    2.8編寫測試類

    package com.gx.Ioc;
    
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class SpingDemo1 {
        @Test
        public void demo11() {
            // 面向接口傳統方式
            UserDaoimpl userdao = new UserDaoimpl();
            userdao.eat();
        }
           //Spring的bean管理方式
        @Test
        public void demo22() {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            IUserDao userdao = (IUserDao) applicationContext.getBean("IuserDao");
            userdao.eat();
    
        }
    
    }

    兄嘚,如果測試不成功最好看看二者是否對應!!!

    兄dei,到這裏,Spring的入門(IOC)算是入門了,是不是覺得很有成就感啊?

    拉倒吧! 我都不好意思說了.(兄dei,我錯了,是我飄了,呀呀呀,兄dei別打臉鴨QAQ)

    但是我依舊是阻止不了你驕傲的心.

    那就頂我,讓我感受感受你的驕傲!哈哈哈QAQ

    2.9 IOC和DI

    IOC不是什麼技術,而是一種設計思想,IOC能指導我們如何設計出松耦合、更優良的程序。傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難於測試;有了IoC容器后,把創建和查找依賴對象的控制權交給了Spring容器,由容器進行注入組合對象,所以對象與對象之間是鬆散耦合,這樣利於功能復用,更重要的是使得程序的整個體繫結構變得非常靈活。
    IOC:控制反轉,將對象的創建權反轉給了Spring。
    DI:依賴注入,前提必須有IOC的環境,Spring管理這個類的時候將類的依賴的屬性注入(設置)進來。比如說下面講到的Spring的屬性注入其實就是典型的DI

    所謂繼承:is a

    Class A{
    
    }
    Class B extends A{
    
    }

    所謂依賴:

    Class A{
    
    }
    Class B{
        public void xxx(A a){
    
    }
    }
    

    所謂聚合:has a

    3、Spring的工廠類

    3.1Spring工廠類的結構

    3.2老版本的工廠類:BeanFactory

    BeanFactory:調用getBean的時候,才會生成類的實例。

    3.3新版本的工廠類:ApplicationContext

    ApplicationContext:加載配置文件的時候,就會將Spring管理的類都實例化
    ApplicationContext有兩個實現類
    1、ClassPathXmlApplicationContext :加載類路徑下的配置文件
    2、FileSystemXmlApplicationContext :加載文件系統下的配置文件

    4、Spring的配置

    4.1XML的提示配置(Schema的配置)

    在XML文件中要使用各種標籤來給spring進行配置,博主我這佩奇腦袋怎麼可能記住spring中所有的標籤呢,不怕不怕,博主我會配置XML的提示配置QAQ,會了這一招就算兄dei你是喬治腦袋也不用擔心(再說了我看兄dei各各英俊瀟洒,玉樹臨風,聰明絕頂…咳咳,暴露了暴露了)

    4.2Bean的相關的配置(< bean >標籤的id和name的配置)

    id :使用了約束中的唯一約束。裏面不能出現特殊字符的。上面提及到了要與getbean參數值對應

    name :沒有使用約束中的唯一約束(理論上可以出現重複的,但是實際開發不能出現的)。裏面可以出現特殊字符。

    4.3Bean的生命周期的配置(了解)

    init-method :Bean被初始化的時候執行的方法
    destroy-method :Bean被銷毀的時候執行的方法(Bean是單例創建,工廠關閉)

    4.4Bean的作用範圍的配置(重點)

    scope屬性Bean的作用範圍

    scope屬性值如下(主要用的是前二者)
    singletonscope屬性的默認值,Spring會採用單例模式創建這個對象。
    prototype多例模式。(Struts2和Spring整合一定會用到)
    request :應用在web項目中,Spring創建這個類以後,將這個類存入到request範圍中。
    session :應用在web項目中,Spring創建這個類以後,將這個類存入到session範圍中。
    globalsession :應用在web項目中,必須在porlet環境下使用。但是如果沒有這種環境,相對於session。

    5、Spring的屬性注入

    首先,創建幾個普通類

    com.gx.spring.demo.Car
    public class Car {
        private String name;
        private Double price;
        
        public Car(String name, Double price) {
            super();
            this.name = name;
            this.price = price;
        }
        @Override
        public String toString() {
            return "Car [name=" + name + ", price=" + price + "]";
        }
    }
    com.gx.spring.demo.Car2
    /**
     * 用作set方法的屬性注入類
     */
    public class Car2 {
        private String name;
        private Double price;
        public void setName(String name) {
            this.name = name;
        }
        public void setPrice(Double price) {
            this.price = price;
        }
        @Override
        public String toString() {
            return "Car2 [name=" + name + ", price=" + price + "]";
        }
        
    }
    
    com.gx.spring.demo.Person 
    /**
     * 用作set方法的對象屬性注入類
     */
    public class Person {
        private String name;
        private Car2 car2;
        public void setName(String name) {
            this.name = name;
        }
        public void setCar2(Car2 car2) {
            this.car2 = car2;
        }
        @Override
        public String toString() {
            return "Employee [name=" + name + ", car2=" + car2 + "]";
        }
    }
    

    5.1構造方法的方式的屬性注入

    構造方法的屬性注入
    constructor-arg 標籤用於配置構造方法的屬性注入
    name :參數的名稱
    value:設置普通數據
    ref:引用數據,一般是另一個bean id值

    當然了,構造方法的方式的屬性注入也支持對象屬性的注入,標籤中對應屬性也是ref
    如果只有一個有參數的構造方法並且參數類型與注入的bean類型匹配,那就會注入到該構造方法中

    applicationContext.xml中配置:

    <!-- 構造方法的方式 -->
        <bean id="car" class="com.gx.spring.demo.Car">
            <constructor-arg name="name" value="瑪莎拉蒂"/>
            <constructor-arg name="price" value="800000"/>
        </bean>

    測試方法:

        /**
         * 構造方法方式的普通屬性注入方法
         */
        public void demo1(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            Car car = (Car) applicationContext.getBean("car");
            System.out.println(car);
        }

    5.2Set方法的方式的屬性注入【開發常用】

    Set方法的普通屬性注入
    property 標籤用於配置Set方法的屬性注入
    name :參數的名稱
    value:設置普通數據
    ref:引用數據,一般是另一個bean id值

    applicationContext.xml中配置:

    <!-- set方法的方式 -->
    <bean id="car2" class="com.gx.spring.demo.Car2">
            <property name="name" value="法拉利黃金跑車"/>
            <property name="price" value="10000000"/>
    </bean> 

    測試方法:

    @Test
        /**
         * set方法方式的屬性注入
         */
        public void demo2(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            Car2 car2 = (Car2) applicationContext.getBean("car2");
            System.out.println(car2);
        }

    Set方法設置對象類型的屬性
    applicationContext.xml中配置:

    <!-- set方法注入對象類型的屬性 -->
    <bean id="Person" class="com.gx.spring.demo.Person">
                <!-- value:設置普通類型的值,ref:設置其他的類的id或name-->
            <property name="name" value="濤哥"/>
            <property name="car2" ref="car2"/>
        </bean> 

    測試方法:

    @Test
        /**
         * set方法注入對象類型
         */
        public void demo3(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            Person person= (Person) applicationContext.getBean("Person");
            System.out.println(person);
        }

    5.3註解的方式屬性注入【開發常用】

    @Component (作用在類上通用:組件)
    @Component(“userService”)相當於< bean id=”userService” class=”…”>

    衍生:
    @Controller Web層
    @Service 業務層
    @Repository 持久層
    這三個註解是為了讓標註類本身的用途清晰

    屬性注入的註解 ( 可以沒有set方法
    普通類型屬性:@Value

    對象類型屬性:@Resource對應bean中的id)= @Autowired(類型)+ @Qualifier(名稱)

    5.3.1註解的理解

    額,初學框架,註解二字可能對於大部分熊dei來說,太過於陌生,註解其實就是在一個類、方法、屬性上,使用@註解名稱,就比如是我們最熟悉的接實現口中的方法默認會有一個 @Override (熊dei,這樣理解能接受?)

    5.3.2註解的jar包導入

    Spring3.x註解的jar包
    在Spring3.x的版本中,使用註解開發,只需要 spring核心基礎四包外 + log4j包 + 1個依賴包 即可

    Spring4.x註解的jar包
    然而在Spring4.x版本之後則需在 再添加一個要引入 spring-aop 的 jar 包,因為,spring4.x版本中一些註解的依賴方法封裝在spring-aop 的 jar 包中

    5.3.3引入註解的context約束

    所謂約束就是就是就是約束啦(搽汗),其中bean約束是最基本的約束!(下圖也可以看出)

    引入約束:(引入 context 的約束):

    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context.xsd">
    </beans>
    5.3.4編寫相關的類
    public interface UserDao {
    public void sayHello();
    }
    public class UserDaoImpl implements UserDao {
    @Override
    public void sayHello() {
    System.out.println("Hello Spring...");
    } }
    5.3.5配置註解掃描

    Spring的註解開發:組件掃描(不使用類上註解的時候可以不用組件掃描
    使用註解方式,需要開啟組件掃描< context:component-scan base-package=直接包名或者包名.類名/>,當然開發中一般都是base-package=包名,畢竟這樣可以掃描整個包,方便開發
    Spring 的註解開發:組件掃描(類上註解: 可以直接使用屬性注入的註解)

    <!-- Spring 的註解開發:組件掃描(類上註解: 可以直接使用屬性注入的註解) -->
    <context:component-scan base-package="com.gx.spring.demo1"/>
    5.3.6 在相關的類上添加註解

    1、使用類上註解方式@Component(value=“userDao”),相當於< bean id=”userDao class=”com.gx.類名”>< /bean>
    當然value屬性名可以省去直接@Component(”userDao”),當然@Component(“value值任意寫建議取的要有意義”)
    2、註解方式可以沒有set方法

    @Component(value="userDao")  //相當於配置了<bean id="userDao" class="com.gx.UserDaoImpl "></bean>
    public class UserDaoImpl implements UserDao {
    @Override
    public void sayHello() {
    System.out.println("Hello Spring Annotation...");
    } }
    5.3.7 編寫測試類
    @Test
    public void demo3() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = (UserDao) applicationContext.getBean("userDao");
    userDao.sayHello();
    }

    5.4P名稱空間的屬性注入(Spring2.5以後)

    通過引入p名稱空間完成屬性的注入:
    寫法:
    普通屬性 p:屬性名=”值”
    對象屬性 p:屬性名-ref=”值”

    P名稱空間的約束引入

    使用p名稱空間

    5.5 SpEL的屬性注入(Spring3.0以後)

    SpEL:Spring Expression Language,Spring的表達式語言。
    語法: #{SpEL}

    5.6集合類型屬性注入(了解)

    集合類型屬性配置:
    集合的注入都是在< property>標籤中添加子標籤
    數組:< array >
    List:< list >
    Set:< set >
    Map:< map > ,map存放k/v 鍵值對,使用 描述
    Properties:< props> < prop key=””>< /prop>
    普通數據:< value >
    引用數據:< ref >

        <!-- Spring的集合屬性的注入============================ -->
        <!-- 注入數組類型 -->
        <bean id="collectionBean" class="com.gx.spring.demo.CollectionBean">
            <!-- 數組類型 -->
            <property name="arrs">
                <list>
                    <value>天才</value>
                    <value>王二</value>
                    <value>冠希</value>
                </list>
            </property>
            
            <!-- 注入list集合 -->
            <property name="list">
                <list>
                    <value>李兵</value>
                    <value>趙如何</value>
                    <value>鄧鳳</value>
                </list>
            </property>
            
            <!-- 注入set集合 -->
            <property name="set">
                <set>
                    <value>aaa</value>
                    <value>bbb</value>
                    <value>ccc</value>
                </set>
            </property>
            
            <!-- 注入Map集合 -->
            <property name="map">
                <map>
                    <entry key="aaa" value="111"/>
                    <entry key="bbb" value="222"/>
                    <entry key="ccc" value="333"/>
                </map>
            </property>
        </bean>

    6、Spring的分模塊開發的配置

    分模塊配置:
    在加載配置文件的時候,加載多個,沒錯,這就是傳說中的騷操作,堪稱開掛級別的操作(當然,這是可以的不是開掛)

    在一個配置文件中引入多個配置文件,簡直優秀!!!

    到這裏,恭喜恭喜,各位熊dei以優雅的儀式感闖進Spring世界,對Spring的IOC以及DI有了一定了解了,是不是也很期待Spring的Aop吶,畢竟Spring的核心是控制反轉(IOC)和面向切面(AOP)。

    (哎哎,別打..別打..別打臉….)

    如果本文對你有一點點幫助,就請點個讚唄,手留余香,謝謝!

    最後,歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術…

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

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

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

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