PL真有意思(四):控制流

前言

對大多數計算模型而言,順序都是基本的東西,它確定了為完成所期望的某種工作,什麼事情應該最先做,什麼事應該隨後做,我們可以將語言規定順序的機制分為幾個類別:

  • 順序執行
  • 選擇
  • 迭代
  • 過程抽象
  • 遞歸
  • 併發
  • 異常處理和推斷
  • 非確定性

對於不同類別的語言對不同類別的控制流的重要性也不盡相同,比如順序執行相比於函數式對於命令式則更加重要。而命令式中更傾向用迭代,函數則更強調遞歸

表達式求值

在討論控制流之前先討論下錶達式的問題,先明確兩個概念:運算符通常是指那些採用特殊語法形式的內部函數(比如+-*/等),運算對象指的是運算符的參數(如2+3,2和3就是運算對象),那麼運算符和運算對象的組合就是表達式。一般根據運算符出現的位置(相對於運算對象而言),可以分為3類表示形式:前綴、中綴和後綴。比如Lisp就運用前綴語法:

(+ 1 3 4 6)      
(* (+ 1 7) 8)    

大多數命令式語言對二元運算符都使用中綴記法,而對一元運算符和其它函數使用前綴激發。但是像Lisp就全部統一使用中綴記法

優先級和結合性

大多數程序設計語言都提供豐富的內部算術。在用中綴方式(沒有括號)寫出就可能出現歧義。所以就需要優先級和結合性來解決歧義性,但是我覺得

媽的你寫括號就完事兒了

而且不同語言的優先級和結合性也不盡相同

賦值

在純函數式語言中,程序的基本組成部分是表達式,計算也僅是對表達式求值。任何一個表達式對於整個計算的影響也僅限於這個表達式所處的上下文環境。

而命令式語言的情況與此截然不同,計算通常是通過對內存中變量值的一系列修改操作來完成,賦值就是這種修改的最基本手段。每一次賦值都表示一個值被放入一個對應的變量中。

一般來說,如果語言中的一個結構除了返回一個值供其外圍環境所使用,還能以其他方式影響後續計算(並最終影響程序輸出),那麼我們就說這種結構有副作用。而副作用也是命令式語言里最核心的部分

而在純函數語言中沒有任何的副作用,表達式的值只依賴於輸入

但是現在許多語言都是混合的,像Python和Ruby主要是命令式的,但是也提供了很多的函數式的特徵,現在連Java都提供了對函數式的支持

引用和值

考慮一下下面的C語言的賦值:

d = a;
a = b + c;

第一個語句中,賦值語句右部引用了a的值,並希望把這個值放入d。第二個語句左部引用了a的位置,希望把b+c的結果放進去。這兩種解釋(值和位置)都是可行的,因為c語言中變量就是能保存值的命名容器,所以我們會說類似的語言的變量是值模型。由於指示位置的表達式被放在賦值語句的左部,所以這種指示位置的表達式成為左值表達式。表示一個值的表達式稱為右值。在變量的值模型下,同一表達式也可能是左值或者右值,比如(a=a+1),左部的a是左值,用於表示存放結果的位置;右部的a是右值,用於代表a具體所指的值。

在採用了變量的引用模型的語言中,這種左值和右值的差異就更加明顯了。

b = 2;
c = b;
a = b + c;

在值模型語言中程序員會說:“把2放入b,然後複製到c,然後用它們兩個的值相加,把結果4放入a。”。;

在引用模型語言中的程序員會說:“讓b引用2,讓c也引用2,然後把這兩個引用送給+運算,並讓a引用算出的結果,也是4“。

而在Java中,對於內部類型使用值模型,而類使用引用模型

裝箱

對於內部類型使用值模型,就無法以統一的方式將它們傳給要求類類型的參數的方法,所以這裏就需要一個裝箱過程

比如Java提供的Integer類

Integer i = new Integer(12);

多路賦值

我們知道賦值操作有右結合性,這使得我們可以寫出a=b=c的簡練代碼,在一些語言中(Ruby,Go,Python)我們可以進一步這樣寫:

a, b = 1, 2;
//上面的語句結果就是a等於1,b等於2。

a, b = b, a;
//交換兩個值,如果沒有這種語言特性,那麼就需要引入臨時變量了。

a, b , c = funx(d, e, f);

這種記法也消除了大多數程序設計語言中函數的非對稱性,這些語言可以允許任意多個參數,但只能返回一個返回值。但是其實在Python中的返回多個值,就是將多個值封裝為元組,在賦值的時候又拆箱而已

初始化

並不是所有語言都提供聲明變量時指定初始值的方式,但是至少有這幾點可以證明提供初始值的機制是有益的

  • 局部靜態變量需要一個初始值才能使用
  • 使用靜態分配的變量,可以由編譯器放入全局內存,避免了在運行時賦予吃數值所造成的開銷
  • 可以避免意外的使用未初始的變量

如果聲明時沒有明確的給定變量的初始值,語言也可以給定一個默認值。像C、Java和C#也都提供了類似的機制

動態檢查

除了可以指定默認值之外,還可以採用另外一種方式,將對為初始化的變量的使用作為動態語義錯誤,在運行時捕獲這種錯誤。但是在運行時捕獲所有使用到未初始化的情況的代價非常高

定義性賦值

在Java和C#中提供了一種定義性賦值的表示形式,意思就是由編譯器來檢查在達到一個表達式的所有可能控制路徑上,都必須為這個表達式中的每個變量賦過值

構造函數

許多面向對象語言都提供了為用戶定義的類型的自動初始化方法,也就是構造函數

在C++中,還區分了初始化和賦值,它將初始化賦值解釋為調用變量所屬類型的構造函數,以初始值作為調用參數。在沒有強制的情況下,賦值被解釋為調用相關類型的賦值運算符,如果沒有定義賦值運算符,就默認將賦值右部的值簡單的按位複製過來

區分初始化和賦值的好處是,可以區分在賦值前是不是需要先釋放空間

表達式的順序問題

雖然優先級和結合性規則定義了表達式里二元中綴運算符的應用順序,但卻沒有明確說明特定運算符的各運算對象的求值順序。舉例來說,如下錶達式:

 a - f(b) - c * d

根據結合性可知a-f(b)將在第二個減法前執行,根據優先級可知第二個減法的右運算對象是cd這個整體而不是c。但是如果沒有進一步的規則描述,我們無法得知a-f(b)是否在cd之前運行。諸如此類:對於f(a,g(b),c)這個子程序調用,我們也不知這三個參數的求值順序。

求值順序之所以重要:

  • 副作用:如果f(b)這個子程序可能會修改c的值,那麼a-f(b)-cd的求值結果將依賴f(b)和cd哪一個先執行;類似的,如果g(b)修改了a或者c的值,那麼f(a,g(b),c)的結果也是依賴於參數的求值順序。

  • 代碼改進:子表達式的求值順序對於寄存器分配和指令調度都有重要的影響。比如(ab+f(c)),我們可能會希望在執行ab之前調用f(c)。因為如果先計算乘法,則在調用f(c)之前就要先保存起來乘積,因為f(c)可能會用光所有的寄存器。

短路求值

對於布爾表達式,如果編譯器可以對其執行短路求值,那麼它生成的代碼可以在表達式前一半的結果可以確定整個表達式的值的情況下跳過後一半的計算。

比如(a<b) and(b<c),如果a>b,那麼完全沒必要去檢查b是否小於c就可以確定這個表達式一定為假。在一些特殊情況下,短路求值可節省大量時間,比如if(func&&func())。實際上這種情況下短路求值已經改變了布爾表達式的語義,如果非短路求值,那麼在func不存在的情況下去執行func(),程序是會拋出錯誤的。

我們常見的語法表現形式是&&和||這種布爾運算符身兼多職,既是布爾運算符又會觸發短路求值,但是有一些語言針對短路求值是有單獨的語法形式的,比如Clu語言中布爾運算符是and和or,短路運算符是cand和cor。這是為何呢,因為有些代碼邏輯是不需要這種短路求值的優化的。

結構化和非結構化的流程

彙編語言中的控制流通過有條件的或無條件的跳轉(分支)指令來完成,早期的高級語言模仿這種方式(如Fortran),主要依賴goto來描述大部分非過程化控制流,比如下面代碼:

if A < B goto label1;

label1;

但是如今goto像在Java、Clu和Eiffel里已經完全被禁止了,在其它語言也是受限了或者只是為了向前兼容而已

goto的結構化替代品

對於goto被廢棄,各種使用goto的地方也被結構的方案給代替了

  • 循環中的退出和繼續

break和contiune這兩個關鍵字大家應該很熟悉了

  • 從子程序提前返回

return

  • 多層返回

上面的兩個問題都可以有很好的替代品,但是對於多層返回就會比較麻煩一點。return或”局部的goto“只能在子程序中返回,如果遇到多層嵌套的子程序,想從內層的子程序返回來結束外圍子程序的執行,那return和局部的goto就無能為力了。這種情況下,語言實現要保證能恰當的恢復棧上的子程序調用信息,這種修復工作稱為”回卷”,為完成此事,不僅必須釋放需要跳出的所有子程序的棧幀,還要執行一些信息管理工作,比如恢復寄存器內容。

Common Lisp提供了return-from語句來明確指定需要退出的詞法外圍函數或嵌套塊,還可以提供一個返回值:

Common Lisp和另外一個語言Ruby中還內置一個throw/catch語法來支持這種多層返回,注意這種結構並不是所謂的異常處理,而是一種多層返回的語法結構,直白點說是一種功能強大的變相”goto“,看下面代碼:

//定義一個方法
def search_file(filename,pattern)
   file=File.Open(filename)
   //遍歷文件每一行
   file.each{|line|
        //根據parrern匹配模式查找,如果匹配就返回到定義found標籤的位置
        throw :found,line if line=~/#{pattern}/
   }
end

//用catch定義一個found標籤
math=catch:found do
   serach_file("f1",key) 
   serach_file("f2",key)    //如果f2文件找到了則就會返回line至math
   serach_file("f3",key)
   ”not fount“              //找不到就執行到此處了
end

print match
  • 錯誤和異常

多層返回的概念假定了被調用方知道調用方期的是什麼,並且能返回一個適當的值。還存在一種情況,其中深層嵌套的子程序中發生了一些情況,導致無法繼續執行下去,而且因為沒有足夠的環境信息,甚至無法合適的結束自己的工作,這種情況下,唯一能做的就是”退回去“,一直回退到能夠恢復執行的地方,這種要求程序退回去的條件通常稱為叫做”異常“。常見的結構化的異常處理和多層返回有很大的相似性,兩者都需要從某一個內層上下文回退到外層的上下文。具體的差異則是多層返回是內層的上下文正常的完成計算然後根據需要返回正確的值,然後轉移到外層上下文,並不需要後續處理。而異常中的內層上下文已經是無法進行正常的計算,必須以一種非正常的退出一直回卷,然後觸發某個特殊的處理流程直到catch到它。

  • 繼續

如果進一步推廣上一小節中造成棧回卷的非局部goto概念,則可以定義一種稱為繼續(Continuations)的概念。從底層來看,一個繼續是由一個代碼地址與其關聯的一個引用環境組成的,如果跳轉到這個地址,就該恢復這個引用環境。從抽象層面看,它描述一個可能由此繼續下去的執行上下文。在Scheme和Ruby中,繼續是基本的一等公民,我們可以利用這種機制有效的擴充流程控制結構集合。

Scheme中支持繼續由一個通常稱為call-with-current-continuation的函數實現,有時簡稱”call/cc”。該函數有一個參數f,f也是一個函數;”call/cc”調用函數f,把一個記錄著當前程序計數器和引用環境的“繼續(暫時稱為是c)c”傳遞給f,這種”繼續c”由一個閉包來表示(通過參數傳遞的子程序的表示的閉包並無不同)。在將來任何時候,f都可以調用c,然後可以用c來重新建立其保存的上下文。一般的應用情況是我們把這個c賦值給一個變量,則可重複的調用它,甚至我們可以在f中返回它,即使f已經執行完畢,仍然可以調用c。

順序執行

選擇

現在大部分命令式語言中採用的選擇語句,都是從Algol 60引進過的 if…then…else 的某種演進變形:

if condition then statement
else if condition then statement
else if condition then statement
...
else statement

短路條件

雖然 if…then…else 語句的條件是一個布爾表達式,但是通常沒有必要求出這個表達式的值放入寄存器。大部分機器都提供了條件分支指令(如上面提到的IL指令brtrue.s),因為這個表達式求值的目的並不是為了值,而是為了跳轉到合適的位置。這種看法使得可以對短路求值的表達式生成高效的代碼(稱為跳轉碼)。跳轉碼不但可以用於選擇語句,也可用在“邏輯控制的循環”中。如下面代碼:

if((A>B)&&(C<D)||(E!=F)){
    then_clause
}
else{
    else_clause
}

在不使用短路求值的Pascal中,生成的代碼大致如下(它會計算每個表達式的結果並放入寄存器r1…,然後再決定跳轉):

     r1=A
     r2=B
     r1=r1>r2
     r2=C
     r3=D
     r2=r2>r3
     r1=r1&r2
     r2=E
     r3=F
     r2=r2!=r3
     r1=r1|r2
     if r1=0 goto L2
L1: then_clause
    goto L3
L2: else_clause
L3:

跳轉碼的情況於此不同,它不會把表達式的值存入寄存器,而是直接跳轉(只用到了r1和r2兩個寄存器,明顯也不會針對整個表達式進行求值,比上面的要高效一些):

     r1=A
     r2=B
     if r1<=r2 goto L4
     r1=C
     r2=D
     if r1>r2 goto L1
L4: r1=E
     r2=F
     if r1=r2 goto L2
L1: then_clause
    goto L3
L2: else_clause
L3:

case/switch語句

對於if else結構來說,如果嵌套的層數過多、或者是用於判斷的條件表達式是基於一些有限的簡單值(或編譯時常量),那麼出現了一種更為優雅的語法結構“case語句”,有很多ifelse都可以很輕鬆的改寫成case/switch語句

對於case/switch的優勢還不只是語法上的優雅,有時還可以生成更高效的代碼

T: &L1
   &L2
   &L3
   &L4
   &L5
   &L6
L1: clause_A
    goto L7
L2: clause_B
    goto L7
L3: clause_C
    goto L7
L4: clause_D
    goto L7
L5: clause_E
    goto L7
L6: clause_F
    goto L7
L7:

這樣其實T就是一個地址跳轉表

迭代

迭代和遞歸是計算機能夠重複執行一些操作的兩種機制;命令式語言傾向於使用迭代、函數式語言則更看重遞歸。大多數語言中的迭代都是以循環的形式出現的,和複合語句中的語句一樣,迭代的執行通常也是為了副作用,也就是修改一些變量的值。根據用何種方式控制迭代的次數來看,循環有兩個主要變種”枚舉控制的循環”和“邏輯控制的循環”。前者是在給定的某個有限的集合中執行,後者則是不確定要執行多少次(直到它所依賴的表達式結果被改變)。對於這兩種結構,大多數的語言都提供了不同的語法結構來表示。

枚舉控制的循環

枚舉控制的循環最初來自Fortran的do循環,

do i = 1, 10, 2
  ...
enddo

等號後面的表達式分別是i的初始值,邊界值和步長

像這種枚舉循環可以說的不多,但是如果前幾次迭代的執行會導致迭代的次數或下標值的發生變化,那麼我們就需要一個更通用的實現

思考幾個問題:

  • 控制是否可以通過枚舉之外的任何方式進入和離開循環呢?
  • 如果循環體修改了用於計算循環結束邊界的變量,會發生什麼?
  • 如果循環體修改了下標變量,會發生?
  • 程序是否可以在循環結束後讀取下標變量,如果可以,它的值將是什麼?
  1. 現在的大多數語言都提供了,break類似的機制來離開循環。Fortran IV允許通過goto跳入到一個循環中,但是這個通常被認為是一個語言缺陷
  2. 同樣的,在大多數語言中,邊界值只在第一次計算,並且保存在一個臨時寄存器中,所以對於之後的修改並不會起作用
  3. 早期的大部分語言都禁止在枚舉控制的循環中修改下邊變量。但是剛剛試驗了一下,許多的語言好像都放開了這個禁止,也就是按照修改后的正常邏輯繼續執行
  4. 首先這是一個語言實現的問題,現在的大多數語言應該都是將循環下標的作用域限定在循環體內了,所以出了循環體是訪問不到的

當然在之後出現了C的for循環

for (int i = first; i < last; i += step) {
  ...
}

這樣有關結束條件、溢出和循環方向的問題全都交由程序員來掌控

迭代器

上面描述的循環都是在算術值的序列上迭代。不過一般而言,我們還希望可以在任何定義的良好的集合的元素上迭代。在C++和Java里叫做迭代器

真迭代器

Clu,Ruby等語言允許任何容器對象提供一個枚舉自己元素的迭代器,這種迭代器就像是允許包含yield語句的子程序,每次yield生成一個循環下標

在Python里就可以這樣寫

for i in range(first, last, step):
    ...

在被調用時,這個迭代器算出循環的第一個下標值,然後通過yield語句返回給調用者,yield語句很像return,但是不同的是再每次循環結束后控制權會再次的交給迭代器,重新開始下一次yield,直到迭代器沒有元素可yield為止才結束for循環。從效果上看迭代器就像是另外一個控制線程,有它自己的程序計數器,它的執行與為它提供下標值的for循環交替執行,這一類通常稱為真迭代器。

迭代器

在許多面向對象語言里,用了更加面向對象的方法來實現迭代器。它們的迭代器就是一個常規對象,它提供了一套方法,用來初始化、生成下一個下標值和檢測結束條件

BinTree<Integer> myTree;

for (Integer i : myTreee) {

}

上面的這段代碼其實是下面這段的一個語法糖

for(Iterator<Integer> it = myTree.iterator();it.hasNext();) {

}

用一級函數來實現迭代器

實現是將循環的體寫成一個函數,用循環的下標作為函數的參數,然後將這函數作為參數傳遞給一個迭代器

(define uptoby
    (lambda (low high step f)
        (if (<= low higt)
            (begin
                (f low)
                (uptoby (+ low step) high step f))
            '())))

不用迭代器的迭代

在那些沒有真迭代器或者迭代器對象的語言中,還是可以通過編程方式實現集合枚舉和使用元素之間的解耦的,用C語言做例子:

tree_node *my_tree;    
tree_iter ti:                 
...
for(ti_create(my_tree,&ti);
              !ti_done(ti);
              ti_next(&ti)){
     tree_node *n=ti_val(ti);
     ...
}
ti_delete(&ti);

邏輯控制的循環

和枚舉循環相比,邏輯控制的循環關注點只在結束條件上

前置檢測

由Algol W引進,後來被Pascal保留

while cond do stat

後置檢測

這種的循環體不管是否滿足循環條件,都至少會執行一次循環體。如C語言的do while語句

do{
   line=read_line();
   //...代碼
} while line[0]!='$'; 

中置檢測

中置檢測一般依賴if

for(;;){
   line=read_line();
   if line[0]!='$' break;
}

遞歸

遞歸和上述討論的其他控制流都不同,它不依賴特殊的語法形式,只要語言允許函數直接或間接的調用自身,那麼就是支持遞歸的。大部分情況下遞歸和迭代都可以互相用對方重寫的。

迭代和遞歸

早期的一些語言不支持遞歸(比如Fortan77以前的版本),也有一些函數式語言不允許迭代,然而大部分現代語言都是同時支持兩者的。在命令式語言中,迭代在某種意義上顯得更自然一些,因為它們的核心就是反覆修改一些變量;對於函數式語言,遞歸更自然一些,因為它們並不修改變量。如果是要計算gcd(更相減損法),遞歸更自然一些:

int gcd(int a,int b){
  if(a==b) return a;
  else if (a>b) return gcd(a-b,b);
  else return gcd(a,b-a);
}

用迭代則是這樣:

int gcd(int a,int b){
   while(a!=b){
      if(a>b) a=a-b;
      else  b=b-a;
   }
   return a;
}

尾遞歸

經常有人說迭代比遞歸效率更高,其實更準確的說法應該是,迭代的樸素實現的(無優化)效率通常比遞歸的樸素實現的效率要高。如上面gcd的例子,如果遞歸的實現確實是實實在在的子程序調用,那麼這種子程序調用所帶來的棧的分配等的開銷確實要比迭代要大。然而一個“優化”的編譯器(通常是專門為函數式語言設計的編譯器),常常能對遞歸函數生成優異的代碼,如上面的gcd尾遞歸(尾遞歸函數是指在遞歸調用之後再無其他計算的函數,其返回值就是遞歸調用的返回值)。對這種函數完全不必要進行動態的棧分配,編譯器在做遞歸調用時可以重複使用當前的棧空間,從效果上看,好的編譯器可以把上面遞歸的gcd函數改造為:

int gcd(int a,int b){
start:
   if (a==b) return a;
   else if (a>b){
     a=a-b;
     goto start;  
   }
   else{
     b=b-a;
     goto start;
  }
}

即使是那些非尾遞歸函數,通過簡單的轉換也可能產生出尾遞歸代碼。

應用序和正則序求值

在上述的討論中,我們都假定所有參數在傳入子程序之前已經完成了求值,但是實際中這並不是必須的。完全可以採用另外一種方式,把為求值的之際參數傳遞給子程序,僅在需要某個值得時候再去求它。前一種在調用前求值的方案稱為應用序求值;后一種到用時方求值的方式稱為正則序求值。正則序求值在宏這個概念中是自然而然的方式,前面討論的短路求值、以及後面要討論的按名調用參數也是應用的正則序求值,一些函數式語言中偶爾也會出現這種方式。

但是我們來看一個例子:

#define MAX(a,b) ((a)>(b)?(a):(b))

如果我這麼調用MAX(i++,j++),導致i和j都執行兩次++,產生了兩次副作用,這是我們不願意看到的結果。總結來說,只有在表達式求值不會產生副作用的情況下正則序才是安全的。

惰性求值

從清晰性和高效的角度看,應用序求值通常會比正則序合適一些,一次大部分語言都採用如此的方式。然而也確實有一些特殊情況下正則序更高效一些,而應用序會造成一些錯誤出現,這種情況的出現時因為一些參數的值實際上並不會被需要,但是還是被求值了,應用序求值有時也成為非惰性求值,比如下面的JavaScript代碼就會是一個死循環:

function while1() {
    while (true) { console.log('死循環')}
}
function NullFunction() { }
console.log(NullFunction(1,2,3,while1()));

Scheme通過內部函數delay和force提供可選的正則序求值功能,這兩個函數提供的實際上是惰性求值的一種實現

惰性求值最常見的一種用途就是用來創建無窮數據結構

(define naturals
    (letrec ((next (lambda (n) (cons n (delay (next (+ n 1)))))))
    (next 1)))

這樣就可以用Scheme表述所有的自然數

小結

本篇首先從表達式開始,介紹了表達式(語句)中的一些基本概念;然後從討論了從彙編時代到結構化程序設計時代語言中的控制流程的演進以及發展;有了前面兩個基礎,後面就詳細的介紹了程序中的三大基本流程控制結構順序、選擇、循環(遞歸和迭代)。

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※高價收購3C產品,價格不怕你比較

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

【決戰西二旗】|你真的懂快速排序?

本文首發於:

微信公眾號:後端技術指南針

持續乾貨 歡迎關注!

看似青銅實則王者

很多人提起快排和二分都覺得很容易的樣子,但是讓現場Code很多就翻車了,就算可以寫出個遞歸版本的代碼,但是對其中的複雜度分析、邊界條件的考慮、非遞歸改造、代碼優化等就無從下手,填鴨背誦基本上分分鐘就被面試官擺平了。

 

那年初識快速排序

快速排序Quicksort又稱劃分交換排序partition-exchange sort,簡稱快排,一種排序算法。最早由東尼·霍爾(C. A. R. Hoare)教授在1960年左右提出,在平均狀況下,排序n個項目要O(nlogn)次比較。

在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他算法更快,因為它的內部循環可以在大部分的架構上很有效率地達成。

快速排序的核心思想

在計算機科學中,分治法(Divide&Conquer)是建基於多項分支遞歸的一種很重要的算法範式,快速排序是分治思想在排序問題上的典型應用。

所謂分治思想D&C就是把一個較大規模的問題拆分為若干小規模且相似的問題。再對小規模問題進行求解,最終合併所有小問題的解,從而形成原來大規模問題的解。

字面上的解釋是”分而治之”,這個技巧是很多高效算法的基礎,如排序算法(歸併排序、快速排序)、傅立恭弘=叶 恭弘變換(快速傅立恭弘=叶 恭弘變換)。

分治法中最重要的部分是循環遞歸的過程,每一層遞歸有三個具體步驟:

  • 分解:將原問題分解為若干個規模較小,相對獨立,與原問題形式相同的子問題。
  • 解決:若子問題規模較小且易於解決時,則直接解。否則,遞歸地解決各子問題。
  • 合併:將各子問題的解合併為原問題的解。

快速排序的發明者

查爾斯·安東尼·理查德·霍爾爵士(Sir Charles Antony Richard Hoare縮寫為C. A. R. Hoare,1934年1月11日-),昵稱為東尼·霍爾(Tony Hoare),生於大英帝國錫蘭可倫坡(今斯里蘭卡),英國計算機科學家,圖靈獎得主。

他設計了快速排序算法、霍爾邏輯、交談循序程式。在操作系統中,他提出哲學家就餐問題,併發明用來作為同步程序的監視器(Monitors)以解決這個問題。他同時證明了監視器與信號標(Semaphore)在邏輯上是等價的。

1980年獲頒圖靈獎、1982年成為英國皇家學會院士、2000年因為他在計算機科學與教育方面的傑出貢獻,獲得英國王室頒贈爵士頭銜、2011年獲頒約翰·馮諾依曼獎,現為牛津大學榮譽教授,並在劍橋微軟研究院擔任研究員。

快速排序的基本過程

快速排序使用分治法來把一個序列分為小於基準值和大於基準值的兩個子序列。

遞歸地排序兩個子序列,直至最小的子序列長度為0或者1,整個遞歸過程結束,詳細步驟為:

  • 挑選基準值: 從數列中挑出一個元素稱為基準pivot,選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
  • 基準值分割: 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面,與基準值相等的數可以到任何一邊,在這個分割結束之後,對基準值的排序就已經完成。
  • 遞歸子序列: 遞歸地將小於基準值元素的子序列和大於基準值元素的子序列排序,步驟同上兩步驟,遞歸終止條件是序列大小是0或1,因為此時該數列顯然已經有序。

快速排序的遞歸實現

    • 版本一 C實現
#include<stdio.h>

int a[9]={5,1,9,6,7,11,3,8,4};

void exchange(int *p,int *q){
    int temp=*p;
    *p=*q;
    *q=temp;
}

int quicksort(int left,int right){
    if(left>=right){
        return 0;
    }

    int i,j,temp;
    temp=a[left];
    i=left;
    j=right;

    while(i!=j){
        while(i<j&&a[j]>=temp){
            j--;
        }
        exchange(&a[i],&a[j]); 
        while(i<j&&a[i]<=temp){
            i++; 
        }
        exchange(&a[i],&a[j]);
    }
    quicksort(i+1,right);
    quicksort(left,i-1); 
}

int main(){
    quicksort(0,8);
    for(int i=0;i<=8;i++){
        printf("%d ",a[i]);
    }
}
    • 版本二 C++實現
 1 #include<iostream>
 2 using namespace std;
 3 
 4 template <typename T>
 5 void quick_sort_recursive(T arr[], int start, int end) {
 6     if (start >= end)
 7         return;
 8     T mid = arr[end];
 9     int left = start, right = end - 1;
10     //整個範圍內搜尋比樞紐值小或大的元素,然後左側元素與右側元素交換
11     while (left < right) {
12             //試圖在左側找到一個比樞紐元更大的元素
13         while (arr[left] < mid && left < right)
14             left++;
15                 //試圖在右側找到一個比樞紐元更小的元素
16         while (arr[right] >= mid && left < right)
17             right--;
18                 //交換元素
19         std::swap(arr[left], arr[right]);
20     }
21         //這一步很關鍵
22     if (arr[left] >= arr[end])
23         std::swap(arr[left], arr[end]);
24     else
25         left++;
26     quick_sort_recursive(arr, start, left - 1);
27     quick_sort_recursive(arr, left + 1, end);
28 }
29 
30 //模板化
31 template <typename T> 
32 void quick_sort(T arr[], int len) {
33     quick_sort_recursive(arr, 0, len - 1);
34 }
35 
36 int main()
37 {
38     int a[9]={5,1,9,6,7,11,3,8,4};
39     int len = sizeof(a)/sizeof(int);
40     quick_sort(a,len-1);
41     for(int i=0;i<len-1;i++)
42         cout<<a[i]<<endl;
43 }

兩個版本均可正確運行,但代碼有一點差異:

  • 版本一 使用雙指針交替從左(右)兩邊分別開始尋找大於基準值(小於基準值),然後與基準值交換,直到最後左右指針相遇。
  • 版本二 使用雙指針向中間集合,左指針遇到大於基準值時則停止,等待右指針,右指針遇到小於基準值時則停止,與左指針指向的元素交換,最後基準值放到合適位置。

過程說起來比較抽象,穩住別慌!靈魂畫手大白會畫圖來演示這兩個過程。

快速排序的遞歸演示

  • 版本一遞歸代碼的排序過程示意圖:

第一次遞歸循環為例:

步驟1: 選擇第一個元素為基準值pivot=a[left]=5,right指針指向尾部元素,此時先由right自右向左掃描直至遇到<5的元素,恰好right起步元素4<5,因此需要將4與5互換位置;

步驟2: 4與5互換位置之後,輪到left指針從左向右掃描,注意一下left的起步指針指向了由步驟1交換而來的4,新元素4不滿足停止條件,因此left由綠色虛箭頭4位置遊走到元素9的位置,此時left找到9>5,因此將此時left和right指向的元素互換,也就是元素5和元素9互換位置;

步驟3: 互換之後right指針繼續向左掃描,從藍色虛箭頭9位置遊走到3的位置,此時right發現3<5,因此將此時left和right指向的元素互換,也就是元素3和元素5互換位置;

步驟4: 互換之後left指針繼續向右掃描,從綠色虛箭頭3位置遊走到6的位置,此時left發現6>5,因此將此時left和right指向的元素互換,也就是元素6和元素5互換位置;

步驟5: 互換之後right指針繼續向左掃描,從藍色虛箭頭6位置一直遊走到與left指針相遇,此時二者均停留在了pivot=5的新位置上,且左右兩邊分成了兩個相對於pivot值的子序列;

循環結束:至此出現了以5為基準值的左右子序列,接下來就是對兩個子序列實施同樣的遞歸步驟。

第二次和第三次左子序列遞歸循環為例:

步驟1-1:選擇第一個元素為基準值pivot=a[left]=4,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素3<4,因此將3和4互換;

步驟1-2:互換之後left指針從元素3開始向右掃描,一直遊走到與right指針相遇,此時本次循環停止,特別注意這種情況下可以看到基準值4隻有左子序列,無右子序列,這種情況是一種退化,就像冒泡排序每次循環都將基準值放置到最後,因此效率將退化為冒泡的O(n^2);

步驟1-3:選擇第一個元素為基準值pivot=a[left]=3,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素1<3,因此將1和3互換;

步驟1-4:互換之後left指針從1開始向右掃描直到與right指針相遇,此時注意到pivot=3無右子序列且左子序列len=1,達到了遞歸循環的終止條件,此時可以認為由第一次循環產生的左子序列已經全部有序。

循環結束:至此左子序列已經排序完成,接下來對右子序列實施同樣的遞歸步驟,就不再演示了,聰明的你一定get到了。

特別注意:

以上過程中left和right指針在某個元素相遇,這種情況在代碼中是不會出現的,因為外層限制了i!=j,圖中之所以放到一起是為了直觀表達終止條件。

  • 版本二C++版本動畫演示:

 

分析一下:

個人覺得這個版本雖然同樣使用D&C思想但是更加簡潔,從動畫可以看到選擇pivot=a[end],然後左右指針分別從index=0和index=end-1向中間靠攏。

過程中掃描目標值並左右交換,再繼續向中間靠攏,直到相遇,此時再根據a[left]和a[right]以及pivot的值來進行合理置換,最終實現基於pivot的左右子序列形式。

腦補場景:

上述過程讓我覺得很像統帥命令左右兩路軍隊從兩翼會和,並且在會和過程中消滅敵人有生力量(認為是交換元素),直到兩路大軍會師。

此時再將統帥王座擺到正確的位置,此過程中沒有統帥王座的反覆變換,只有最終會師的位置,以王座位中心形成了左翼子序列和右翼子序列。

再重複相同的過程,直至完成大一統。

腦補不過癮 於是湊圖一張:

快速排序的多種版本

吃瓜時間:

印象中2017年初換工作的時候去CBD一家公司面試手寫快排,我就使用C++模板化的版本二實現的,但是面試官質疑說這不是快排,爭辯之下讓我們彼此都覺得對方很Low,於是很快就把我送出門SayGoodBye了^_^。

我想表達的意思是,雖然快排的遞歸版本是基於D&C實現的,但是由於pivot值的選擇不同、交換方式不同等諸多因素,造成了多種版本的遞歸代碼。

並且內層while循環裏面判斷>=還是>(即是否等於的問題),外層循環判斷本序列循環終止條件等寫法都會不同,因此在寫快排時切忌死記硬背,要不然邊界條件判斷不清楚很容易就死循環了。

看下上述我貼的兩個版本的代碼核心部分:

//版本一寫法
while(i!=j){
    while(i<j&&a[j]>=temp){
        j--;
    }
    exchange(&a[i],&a[j]); 
    while(i<j&&a[i]<=temp){
        i++; 
    }
    exchange(&a[i],&a[j]);
}

//版本二寫法
while (left < right) {
    while (arr[left] < mid && left < right)
        left++;
    while (arr[right] >= mid && left < right)
        right--;
    std::swap(arr[left], arr[right]);
}

覆蓋or交換:

代碼中首先將pivot的值引入局部變量保存下來,這樣就認為A[L]這個位置是個坑,可以被其他元素覆蓋,最終再將pivot的值填到最後的坑裡。

這種做法也沒有問題,因為你只要畫圖就可以看到,每次坑的位置是有相同元素的位置,也就是被備份了的元素。

個人感覺 與其叫坑不如叫備份,但是如果你代碼使用的是基於指針或者引用的swap,那麼就沒有坑的概念了。

這就是覆蓋和交換的區別,本文的例子都是swap實現的,因此沒有坑位被最後覆蓋一次的過程。

快速排序的迭代實現

所謂迭代實現就是非遞歸實現一般使用循環來實現,我們都知道遞歸的實現主要是藉助系統內的棧來實現的。

如果調用層級過深需要保存的臨時結果和關係會非常多,進而造成StackOverflow棧溢出。

Stack一般是系統分配空間有限內存連續速度很快,每個系統架構默認的棧大小不一樣,筆者在x86-CentOS7.x版本使用ulimit -s查看是8192Byte。

避免棧溢出的一種辦法是使用循環,以下為筆者驗證的使用STL的stack來實現的循環版本,代碼如下:

#include <stack>
#include <iostream>
using namespace std;

template<typename T>
void qsort(T lst[], int length) {
    std::stack<std::pair<int, int> > mystack;
    //將數組的首尾下標存儲 相當於第一輪循環
    mystack.push(make_pair(0, length - 1));

    while (!mystack.empty()) {
        //使用棧頂元素而後彈出
        std::pair<int,int> top = mystack.top();
        mystack.pop();

        //獲取當前需要處理的子序列的左右下標
        int i = top.first;
        int j = top.second;

        //選取基準值
        T pivot = lst[i];

        //使用覆蓋填坑法 而不是交換哦
        while (i < j) {
            while (i < j and lst[j] >= pivot) j--;
            lst[i] = lst[j];
            while (i < j and lst[i] <= pivot) i++;
            lst[j] = lst[i];
        }
        //注意這個基準值回填過程
        lst[i] = pivot;

        //向下一個子序列進發
        if (i > top.first) mystack.push(make_pair(top.first, i - 1));
        if (j < top.second) mystack.push(make_pair(j + 1, top.second));
    }
}

int main()
{
    int a[9]={5,1,9,6,7,11,3,8,4};
    int len = sizeof(a)/sizeof(int);
    qsort(a,len);
    for(int i=0;i<len-1;i++)
        cout<<a[i]<<endl;
}

下期精彩

由於篇幅原因,目前文章已經近6000字,因此筆者決定將快排算法的優化放到另外一篇文章中,不過可以提前預告一下會有哪些內容:

  • 基準值選擇對性能的影響
  • 基準值選擇的多種策略
  • 尾遞歸的概念原理和優勢
  • 基於尾遞歸實現快速排序
  • STL中sort函數的底層實現
  • glibc中快速排序的實現

參考資料

 

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

結合RBAC模型講解權限管理系統需求及表結構創建

在本號之前的文章中,已經為大家介紹了很多關於Spring Security的使用方法,也介紹了RBAC的基於角色權限控制模型。但是很多朋友雖然已經理解了RBAC控制模型,但是仍有很多的問題阻礙他們進一步開發。比如:

  • RBAC模型的表結構該如何創建?
  • 具體到某個頁面,某個按鈕權限是如何控制的?
  • 為了配合登錄驗證表,用戶表中應該包含哪些核心字段?
  • 這些字段與登錄驗證或權限分配的需求有什麼關係?

那麼本文就希望將這些問題,與大家進行一下分享。

一、回顧RBAC權限模型

  • 用戶與角色之間是多對多的關係,一個用戶有多個角色,一個角色包含多個用戶
  • 角色與權限之間是多對多關係,一個角色有多種權限,一個權限可以屬於多個角色

上圖中:

  • User是用戶表,存儲用戶基本信息
  • Role是角色表,存儲角色相關信息
  • Menu(菜單)是權限表,存儲系統包含哪些菜單及其屬性
  • UserRole是用戶和角色的關係表
  • RoleMenu是角色和權限的關係表

本文講解只將權限控制到菜單的訪問級別,即控制頁面的訪問權限。如果想控制到頁面中按鈕級別的訪問,可以參考Menu與RoleMenu的模式同樣的實現方式。或者乾脆在menu表裡面加上一個字段區別該條記錄是菜單項還是按鈕。

為了有理有據,我們參考一個比較優秀的開源項目:若依後台管理系統。

二、組織部門管理

2.1.需求分析

之所以先將部門管理提出來講一下,是因為部門管理沒有在我們上面的RBAC權限模型中進行提現。但是部門這樣一個實體仍然是,後端管理系統的一個重要組成部分。通常有如下的需求:

  • 部門要能體現出上下級的結構(如上圖中的紅框)。在關係型數據庫中。這就需要使用到部門id及上級部門id,來組合成一個樹形結構。這個知識是SQL學習中必備的知識,如果您還不知道,請自行學習。
  • 如果組織與用戶之間是一對多的關係,就在用戶表中加上一個org_id標識用戶所屬的組織。原則是:實體關係在多的那一邊維護。比如:是讓老師記住自己的學生容易,還是讓學生記住自己的老師更容易?
  • 如果組織與用戶是多對多關係,這種情況現實需求也有可能存在。比如:某人在某單位既是生產部長,又是技術部長。所以他及歸屬於技術部。也歸屬於生產部。對於這種情況有兩種解決方案,把該人員放到公司級別,而不是放到部門級別。另外一種就是從數據庫結構上創建User與Org組織之間的多對多關係。
  • 組織信息包含一些基本信息,如組織名稱、組織狀態、展現排序、創建時間
  • 另外,要有基本的組織的增刪改查功能

2.2 組織部門表的CreateSQL

以下SQL以MySQL為例:

CREATE TABLE `sys_org` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `org_pid` INT(11) NOT NULL COMMENT '上級組織編碼',
    `org_pids` VARCHAR(64) NOT NULL COMMENT '所有的父節點id',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `org_name` VARCHAR(32) NOT NULL COMMENT '組織名',
    `address` VARCHAR(64) NULL DEFAULT NULL COMMENT '地址',
    `phone` VARCHAR(13) NULL DEFAULT NULL COMMENT '電話',
    `email` VARCHAR(32) NULL DEFAULT NULL COMMENT '郵件',
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '組織層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統組織結構表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

注意:mysql沒有oracle中的start with connect by的樹形數據匯總SQL。所以通常需要為了方便管理組織之間的上下級樹形關係,需要加上一些特殊字段,如:org_pids:該組織所有上級組織id逗號分隔,即包括上級的上級;is_leaf是否是恭弘=叶 恭弘子結點;level組織所屬的層級(1,2,3)。

三、菜單權限管理

3.1 需求分析

  • 由上圖可以看出,菜單仍然是樹形結構,所以數據庫表必須有id與menu_pid字段
  • 必要字段:菜單跳轉的url、是否啟用、菜單排序、菜單的icon矢量圖標等
  • 最重要的是菜單要有一個權限標誌,具有唯一性。通常可以使用菜單跳轉的url路徑作為權限標誌。此標誌作為權限管理框架識別用戶是否具有某個頁面查看權限的重要標誌
  • 需要具備菜單的增刪改查基本功能
  • 如果希望將菜單權限和按鈕超鏈接相關權限放到同一個表裡面,可以新增一個字段。用戶標誌該權限記錄是菜單訪問權限還是按鈕訪問權限。

3.2 菜單權限表的CreateSQL

CREATE TABLE `sys_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `menu_pid` INT(11) NOT NULL COMMENT '父菜單ID',
    `menu_pids` VARCHAR(64) NOT NULL COMMENT '當前菜單所有父菜單',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `name` VARCHAR(16) NOT NULL COMMENT '菜單名稱',
    `url` VARCHAR(64) NOT NULL COMMENT '跳轉URL',
    `icon` VARCHAR(45) NULL DEFAULT NULL,
    `icon_color` VARCHAR(16) NULL DEFAULT NULL,
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '菜單層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統菜單表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

四、角色管理

上圖為角色修改及分配權限的頁面

4.1.需求分析

  • 角色本身的管理需要注意的點非常少,就是簡單的增刪改查。重點在於角色分配該如何做。
  • 角色表包含角色id,角色名稱,備註、排序順序這些基本信息就足夠了
  • 為角色分配權限:以角色為基礎勾選菜單權限或者操作權限,然後先刪除sys_role_menu表內該角色的所有記錄,在將新勾選的權限數據逐條插入sys_role_menu表。
  • sys_role_menu的結構很簡單,記錄role_id與menu_id,一個角色擁有某一個權限就是一條記錄。
  • 角色要有一個全局唯一的標識,因為角色本身也是一種權限。可以通過判斷角色來判斷某用戶的操作是否合法。
  • 通常的需求:不會在角色管理界面為角色添加用戶,而是在用戶管理界面為用戶分配角色。

4.2.角色表與角色菜單權限關聯表的的CreateSQL

CREATE TABLE `sys_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `role_name` VARCHAR(16) NOT NULL COMMENT '角色名',
    `role_flag` VARCHAR(64) NULL DEFAULT NULL COMMENT '角色標識',
    `sort` INT(11) NULL DEFAULT NULL COMMENT '排序',
    PRIMARY KEY (`id`)
)
COMMENT='系統角色表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
CREATE TABLE `sys_role_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `menu_id` INT(11) NOT NULL COMMENT '菜單ID',
    PRIMARY KEY (`id`)
)
COMMENT='角色菜單多對多關聯表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

五、用戶管理

5.1.需求分析

  • 上圖中點擊左側的組織菜單樹結點,要能显示出該組織下的所有人員(系統用戶)。在組織與用戶是一對多的關係中,需要在用戶表加上org_id字段,用於查詢某個組織下的所有用戶。
  • 用戶表中要保存用戶的用戶名、加密后的密碼。頁面提供密碼修改或重置的功能。
  • 角色分配:實際上為用戶分配角色,與為角色分配權限的設計原則是一樣的。所以可以參考。
  • 實現用戶基本信息的增刪改查功能

5.2.sys_user 用戶信息表及用戶角色關係表的CreateSQL

CREATE TABLE `sys_user` (
        `id` INT(11) NOT NULL AUTO_INCREMENT,
        `org_id` INT(11) NOT NULL,
        `username` VARCHAR(64) NULL DEFAULT NULL COMMENT '用戶名',
        `password` VARCHAR(64) NULL DEFAULT NULL COMMENT '密碼',
        `enabled` INT(11) NULL DEFAULT '1' COMMENT '用戶賬戶是否可用',
        `locked` INT(11) NULL DEFAULT '0' COMMENT '用戶賬戶是否被鎖定',
        `lockrelease_time` TIMESTAMP NULL  '用戶賬戶鎖定到期時間',
        `expired_time` TIMESTAMP NULL  '用戶賬戶過期時間',
        `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '用戶賬戶創建時間',
    PRIMARY KEY (`id`)
)
COMMENT='用戶信息表'
ENGINE=InnoDB
;
CREATE TABLE `sys_user_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NULL DEFAULT NULL,
    `user_id` VARCHAR(18) NULL DEFAULT NULL,
    PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

在用戶的信息表中,體現了一些隱藏的需求。如:多次登錄鎖定與鎖定到期時間的關係。賬號有效期的設定規則等。

當然用戶表中,根據業務的不同還可能加更多的信息,比如:用戶頭像等等。但是通常在比較大型的業務系統開發中,業務模塊中使用的用戶表和在權限管理模塊使用的用戶表通常不是一個,而是根據某些唯一字段弱關聯,分開存放。這樣做的好處在於:經常發生變化的業務需求,不會去影響不經常變化的權限模型。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

前言

在企業項目開發中,對系統的安全和權限控制往往是必需的,常見的安全框架有 Spring Security、Apache Shiro 等。本文主要簡單介紹一下 Spring Security,再通過 Spring Boot 集成開一個簡單的示例。

Spring Security

什麼是 Spring Security?

Spring Security 是一種基於 Spring AOP 和 Servlet 過濾器 Filter 的安全框架,它提供了全面的安全解決方案,提供在 Web 請求和方法調用級別的用戶鑒權和權限控制。

Web 應用的安全性通常包括兩方面:用戶認證(Authentication)和用戶授權(Authorization)。

用戶認證指的是驗證某個用戶是否為系統合法用戶,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼來完成認證。

用戶授權指的是驗證某個用戶是否有權限執行某個操作。

2.原理

Spring Security 功能的實現主要是靠一系列的過濾器鏈相互配合來完成的。以下是項目啟動時打印的默認安全過濾器鏈(集成5.2.0):

[
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546,
    org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6,
    org.springframework.security.web.header.HeaderWriterFilter@4fefa770,
    org.springframework.security.web.csrf.CsrfFilter@6346aba8,
    org.springframework.security.web.authentication.logout.LogoutFilter@677ac054,
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781,
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678,
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6,
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2,
    org.springframework.security.web.session.SessionManagementFilter@471f8a70,
    org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569,
    org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62
]
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

詳細解讀可以參考:https://blog.csdn.net/dushiwodecuo/article/details/78913113

3.核心組件

SecurityContextHolder

用於存儲應用程序安全上下文(Spring Context)的詳細信息,如當前操作的用戶對象信息、認證狀態、角色權限信息等。默認情況下,SecurityContextHolder 會使用 ThreadLocal 來存儲這些信息,意味着安全上下文始終可用於同一執行線程中的方法。

獲取有關當前用戶的信息

因為身份信息與線程是綁定的,所以可以在程序的任何地方使用靜態方法獲取用戶信息。例如獲取當前經過身份驗證的用戶的名稱,代碼如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

其中,getAuthentication() 返回認證信息,getPrincipal() 返回身份信息,UserDetails 是對用戶信息的封裝類。

Authentication

認證信息接口,集成了 Principal 類。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串
getCredentials() 獲取用戶提交的密碼憑證,用戶輸入的密碼字符竄,在認證過後通常會被移除,用於保障安全
getDetails() 獲取用戶詳細信息,用於記錄 ip、sessionid、證書序列號等值
getPrincipal() 獲取用戶身份信息,大部分情況下返回的是 UserDetails 接口的實現類,是框架中最常用的接口之一

AuthenticationManager

認證管理器,負責驗證。認證成功后,AuthenticationManager 返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼通常會被移除)的 Authentication 實例。然後再將 Authentication 設置到 SecurityContextHolder 容器中。

AuthenticationManager 接口是認證相關的核心接口,也是發起認證的入口。但它一般不直接認證,其常用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,存放里多種認證方式,默認情況下,只需要通過一個 AuthenticationProvider 的認證,就可被認為是登錄成功。

UserDetailsService

負責從特定的地方加載用戶信息,通常是通過JdbcDaoImpl從數據庫加載實現,也可以通過內存映射InMemoryDaoImpl實現。

UserDetails

該接口代表了最詳細的用戶信息。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取授予用戶的權限
getPassword() 獲取用戶正確的密碼,這個密碼在驗證時會和 Authentication 中的 getCredentials() 做比對
getUsername() 獲取用於驗證的用戶名
isAccountNonExpired() 指示用戶的帳戶是否已過期,無法驗證過期的用戶
isAccountNonLocked() 指示用戶的賬號是否被鎖定,無法驗證被鎖定的用戶
isCredentialsNonExpired() 指示用戶的憑據(密碼)是否已過期,無法驗證憑證過期的用戶
isEnabled() 指示用戶是否被啟用,無法驗證被禁用的用戶

Spring Security 實戰

1.系統設計

本文主要使用 Spring Security 來實現系統頁面的權限控制和安全認證,本示例不做詳細的數據增刪改查,sql 可以在完整代碼里下載,主要是基於數據庫對頁面 和 ajax 請求做權限控制。

1.1 技術棧

  • 編程語言:Java
  • 編程框架:Spring、Spring MVC、Spring Boot
  • ORM 框架:MyBatis
  • 視圖模板引擎:Thymeleaf
  • 安全框架:Spring Security(5.2.0)
  • 數據庫:MySQL
  • 前端:Layui、JQuery

1.2 功能設計

  1. 實現登錄、退出
  2. 實現菜單 url 跳轉的權限控制
  3. 實現按鈕 ajax 請求的權限控制
  4. 防止跨站請求偽造(CSRF)攻擊

1.3 數據庫層設計

t_user 用戶表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
username varchar 20 用戶名
password varchar 255 密碼

t_role 角色表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_name varchar 20 角色名稱

t_menu 菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
menu_name varchar 20 菜單名稱
menu_url varchar 50 菜單url(Controller 請求路徑)

t_user_roles 用戶權限表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
user_id int 8 用戶表id
role_id int 8 角色表id

t_role_menus 權限菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_id int 8 角色表id
menu_id int 8 菜單表id

實體類這裏不詳細列了。

2.代碼實現

2.0 相關依賴

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- 熱部署模塊 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 -->
        </dependency>
        
            <!-- mysql 數據庫驅動. -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- mybaits -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- alibaba fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

2.1 繼承 WebSecurityConfigurerAdapter 自定義 Spring Security 配置

/**
prePostEnabled :決定Spring Security的前註解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 決定是否Spring Security的保障註解 [@Secured] 是否可用
jsr250Enabled :決定 JSR-250 annotations 註解[@RolesAllowed..] 是否可用.
 */
@Configurable
@EnableWebSecurity
//開啟 Spring Security 方法級安全註解 @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 靜態資源設置
     */
    @Override
    public void configure(WebSecurity webSecurity) {
        //不攔截靜態資源,所有用戶均可訪問的資源
        webSecurity.ignoring().antMatchers(
                "/",
                "/css/**",
                "/js/**",
                "/images/**",
                "/layui/**"
                );
    }
    /**
     * http請求設置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //http.csrf().disable(); //註釋就是使用 csrf 功能       
        http.headers().frameOptions().disable();//解決 in a frame because it set 'X-Frame-Options' to 'DENY' 問題           
        //http.anonymous().disable();
        http.authorizeRequests()
            .antMatchers("/login/**","/initUserData","/main")//不攔截登錄相關方法        
            .permitAll()        
            //.antMatchers("/user").hasRole("ADMIN")  // user接口只有ADMIN角色的可以訪問
//          .anyRequest()
//          .authenticated()// 任何尚未匹配的URL只需要驗證用戶即可訪問
            .anyRequest()
            .access("@rbacPermission.hasPermission(request, authentication)")//根據賬號權限訪問         
            .and()
            .formLogin()
            .loginPage("/")
            .loginPage("/login")   //登錄請求頁
            .loginProcessingUrl("/login")  //登錄POST請求路徑
            .usernameParameter("username") //登錄用戶名參數
            .passwordParameter("password") //登錄密碼參數
            .defaultSuccessUrl("/main")   //默認登錄成功頁面
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler) //無權限處理器
            .and()
            .logout()
            .logoutSuccessUrl("/login?logout");  //退出登錄成功URL
            
    }
    /**
     * 自定義獲取用戶信息接口
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    /**
     * 密碼加密算法
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
 
    }
}

2.2 自定義實現 UserDetails 接口,擴展屬性

public class UserEntity implements UserDetails {

    /**
     * 
     */
    private static final long serialVersionUID = -9005214545793249372L;

    private Long id;// 用戶id
    private String username;// 用戶名
    private String password;// 密碼
    private List<Role> userRoles;// 用戶權限集合
    private List<Menu> roleMenus;// 角色菜單集合

    private Collection<? extends GrantedAuthority> authorities;
    public UserEntity() {
        
    }
    
    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities,
            List<Menu> roleMenus) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.roleMenus = roleMenus;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Role> getUserRoles() {
        return userRoles;
    }

    public void setUserRoles(List<Role> userRoles) {
        this.userRoles = userRoles;
    }

    public List<Menu> getRoleMenus() {
        return roleMenus;
    }

    public void setRoleMenus(List<Menu> roleMenus) {
        this.roleMenus = roleMenus;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

2.3 自定義實現 UserDetailsService 接口

/**
 * 獲取用戶相關信息
 * @author charlie
 *
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;
    @Autowired
    private MenuDao menuDao;

    @Override
    public UserEntity loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名查找用戶
        UserEntity user = userDao.getUserByUsername(username);
        System.out.println(user);
        if (user != null) {
            System.out.println("UserDetailsService");
            //根據用戶id獲取用戶角色
            List<Role> roles = roleDao.getUserRoleByUserId(user.getId());
            // 填充權限
            Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //填充權限菜單
            List<Menu> menus=menuDao.getRoleMenuByRoles(roles);
            return new UserEntity(username,user.getPassword(),authorities,menus);
        } else {
            System.out.println(username +" not found");
            throw new UsernameNotFoundException(username +" not found");
        }       
    }

}

2.4 自定義實現 URL 權限控制

/**
 * RBAC數據模型控制權限
 * @author charlie
 *
 */
@Component("rbacPermission")
public class RbacPermission{

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean hasPermission = false;
        // 讀取用戶所擁有的權限菜單
        List<Menu> menus = ((UserEntity) principal).getRoleMenus();
        System.out.println(menus.size());
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {
                hasPermission = true;
                break;
            }
        }
        return hasPermission;
    }
}

2.5 實現 AccessDeniedHandler

自定義處理無權請求

/**
 * 處理無權請求
 * @author charlie
 *
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        boolean isAjax = ControllerTools.isAjaxRequest(request);
        System.out.println("CustomAccessDeniedHandler handle");
        if (!response.isCommitted()) {
            if (isAjax) {
                String msg = accessDeniedException.getMessage();
                log.info("accessDeniedException.message:" + msg);
                String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"沒有權限\"}";
                ControllerTools.print(response, accessDenyMsg);
            } else {
                request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
                response.setStatus(HttpStatus.FORBIDDEN.value());
                RequestDispatcher dispatcher = request.getRequestDispatcher("/403");
                dispatcher.forward(request, response);
            }
        }

    }

    public static class ControllerTools {
        public static boolean isAjaxRequest(HttpServletRequest request) {
            return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        }

        public static void print(HttpServletResponse response, String msg) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(msg);
            writer.flush();
            writer.close();
        }
    }

}

2.6 相關 Controller

登錄/退出跳轉

/**
 * 登錄/退出跳轉
 * @author charlie
 *
 */
@Controller
public class LoginController {
    @GetMapping("/login")
    public ModelAndView login(@RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout) {
        ModelAndView mav = new ModelAndView();
        if (error != null) {
            mav.addObject("error", "用戶名或者密碼不正確");
        }
        if (logout != null) {
            mav.addObject("msg", "退出成功");
        }
        mav.setViewName("login");
        return mav;
    }
}

登錄成功跳轉

@Controller
public class MainController {

    @GetMapping("/main")
    public ModelAndView toMainPage() {
        //獲取登錄的用戶名
        Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username=null;
        if(principal instanceof UserDetails) {
            username=((UserDetails)principal).getUsername();
        }else {
            username=principal.toString();
        }
        ModelAndView mav = new ModelAndView();
        mav.setViewName("main");
        mav.addObject("username", username);
        return mav;
    }
    
}

用於不同權限頁面訪問測試

/**
 * 用於不同權限頁面訪問測試
 * @author charlie
 *
 */
@Controller
public class ResourceController {

    @GetMapping("/publicResource")
    public String toPublicResource() {
        return "resource/public";
    }
    
    @GetMapping("/vipResource")
    public String toVipResource() {
        return "resource/vip";
    }
}

用於不同權限ajax請求測試

/**
 * 用於不同權限ajax請求測試
 * @author charlie
 *
 */
@RestController
@RequestMapping("/test")
public class HttptestController {

    @PostMapping("/public")
    public JSONObject doPublicHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }

    @PostMapping("/vip")
    public JSONObject doVipHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }
}

2.7 相關 html 頁面

登錄頁面

<form class="layui-form" action="/login" method="post">
            <div class="layui-input-inline">
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
                <input type="text" name="username" required
                    placeholder="用戶名" autocomplete="off" class="layui-input">
            </div>
            <div class="layui-input-inline">
                <input type="password" name="password" required  placeholder="密碼" autocomplete="off"
                    class="layui-input">
            </div>
            <div class="layui-input-inline login-btn">
                <button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登錄</button>
            </div>
            <div class="form-message">
                <label th:text="${error}"></label>
                <label th:text="${msg}"></label>
            </div>
        </form>

防止跨站請求偽造(CSRF)攻擊

退出系統

<form id="logoutForm" action="/logout" method="post"
                                style="display: none;">
                                <input type="hidden" th:name="${_csrf.parameterName}"
                                    th:value="${_csrf.token}">
                            </form>
                            <a
                                href="javascript:document.getElementById('logoutForm').submit();">退出系統</a>

ajax 請求頁面

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF">
<button class="layui-btn" id="btnPublic">公共權限請求按鈕</button>
<br>
<br>
<button class="layui-btn" id="btnVip">VIP權限請求按鈕</button>
<script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script>
<script type="text/javascript" th:src="@{/layui/layui.js}"></script>
<script type="text/javascript">
        layui.use('form', function() {
            var form = layui.form;
            $("#btnPublic").click(function(){
                $.ajax({
                    url:"/test/public",
                    type:"POST",
                    data:{id:1},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                
                    }   
                });
            });
            $("#btnVip").click(function(){
                $.ajax({
                    url:"/test/vip",
                    type:"POST",
                    data:{id:2},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                        
                    }
                });
            });
        });
    </script>

2.8 測試

測試提供兩個賬號:user 和 admin (密碼與賬號一樣)

由於 admin 作為管理員權限,設置了全部的訪問權限,這裏只展示 user 的測試結果。

完整代碼

非特殊說明,本文版權歸 所有,轉載請註明出處.

原文標題:Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

原文地址:

如果文章有不足的地方,歡迎提點,後續會完善。

如果文章對您有幫助,請給我點個贊,請掃碼關注下我的公眾號,文章持續更新中…

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※高價收購3C產品,價格不怕你比較

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

碼農當自強

碼農當自強

導航

  • 初出茅廬
  • 跳槽才能漲薪
  • 力拔山兮氣蓋世
  • 止步中層
  • 契約精神?聯盟
  • 技工?匠人
  • 碼農當自強

  有人的地方就有江湖。有江湖必有俠客。IT人的江湖水生草闊,從來都盛產俠客和隱士。很多人離開這片江湖,沒有留下自己的故事,而那些有故事的終究成了傳說。

初出茅廬

  本文的主人公木木君,2011年畢業於一個普通二本大學的計算機專業。那年六月,他懷揣夢想,來到西部的一座准一線城市。

  “天高任鳥飛,海闊憑魚游”。同大多數應屆畢業生一樣,木木君懷揣夢想,滿腔熱心,對未來充滿希冀,希望能夠在這座大城市打拚出自己的一片天地。“求突破,求提高,求發展”,這是他給自己設定的未來五年計劃,分三個步驟執行。

  IT行業的門檻向來很高,大多數民企很少招生應屆生,特別是非985,211大學的童鞋,容易碰壁。木木君面試了20家大大小小的公司,花了一個多月,總算找了個正規公司。這是一家生產機頂盒的工廠,幾千人的公司,僅有不到50人的研發團隊。木木君在這裏開啟了自己的IT職業生涯。團隊不比正規的軟件公司,但同事和睦相處,也能夠接觸到實際的開發項目,總算有所突破。

  “笨鳥先飛”。木木君憑藉自己的努力,半年之內就把部門內部的項目都摸了一遍,並在原有基礎上增加了很多功能。

跳槽才能漲薪

  IT江湖潛規則之跳槽才能漲薪。

  有一天,木木君和往常一樣正在配合運維同事改進新的OA系統。“滴滴滴~”,一波QQ消息來襲。木木君點開消息,是師兄。木木君和這個師兄其實不是一個專業,師兄比他高一個年級,因為大學被同一個老師帶着一起做過項目,經常串寢室,比較熟悉。師兄告訴他,他換工作了,這是他兩年來第三次換工作,在軟件園,月薪8K。聊天完畢,木木君沉思良久,開始對高大上的軟件園產生嚮往。

  在第一家公司待了11個月,木木君選擇了離開,換到了一家軟件園的公司,並有機會和華為的工程師一起合作開發項目。值得一提的是,這次跳槽工資幾乎翻了一倍。

  新公司是行業里排名靠前的,公司制度規範,開發流程標準。團隊里研發同事嚴謹,當然壓力也很大。

  剛進公司的前兩個月,壓力挺大,見識了以前沒有接觸過的框架和組件,以及很多聽不懂的術語。每晚和同事加班到8點半,有時甚至11點才從公司離開,儘管很累,但是能夠感受的技術和經驗上的進步。

  在這家公司待了兩年。見識 了一些牛人,甚至有些一個人頂一個小團隊的人。華為的狼性文化在木木君心中打下烙印,深入血液,變成做事風格的一部分。在以後的工作中,也是秉持了這種文化。

力拔山兮氣蓋世

  大多數碼農,在工作三年左右能夠迎來自己的一個技術上的小高峰。

  木木君在第二家公司待了兩年感覺就像到山上跟了一個武林高手學了一身本領,瞬間有了自信。離職的時候,我和我同事還開玩笑說,出去之後至少都是8K…

  “移動互聯網時代已經來臨,站在風口上豬也能飛”。那一年,手機APP大行其道,獨領風騷,是個公司都想做APP。y也是在那一年小米火了,華為剛開始邁入手機領域。進入第三家公司,是一個不到一百人的軟件公司,做旅遊APP的,期望在這裡能夠接觸到移動端。

  “圈子不同,不硬融”。木木君在這家公司真正見識了什麼是公司內耗。小幫派林立,你再努力也無法融入。一年不到,領導換了幾茬,還玩的是家天下。

止步中層

  對大多數人而言,職場的中層就是天花板。

  “天下之大,竟無用武之地”。就像一個武林俠客,讓他困惑不是練武的孤獨和寂寞,而是竟然沒有用武之地。

  “此處不留爺,自有留爺處”。很快,木木君便進入一件互聯網產品公司,主營電商相關Saas軟件。這裏部門分工明確,需求,產品研發,安全,運維,DBA,測試一應俱全。公司產品成熟,客戶穩定,可以學習真正的大數據和自動化運維。

  在這裏團隊從零開始搭建自動化平台,並逐步迭代,一路摸爬滾打,基本能夠滿足公司100多台服務器的自動化運維。

  木木君一腔熱情,終於受到領導器重,升入中層。團隊不大,但是業務不少,支撐多個部門的系統研發和運維,幾乎人人都掛了2+項目。身為leader的木木君,更是不在話下。特殊時期,幾乎一人承擔一個團隊開發任務。甚至非常時期通宵支持…

  四年過去,木木君也進入了而立之年。2018年,經濟不景氣的一年…公司漲薪已經渺茫…陸續身邊逐漸有人跳槽。不到半年,身邊已經有三個人跳槽,其中一個老領導走了留下一句“待了六年,已經沒有上升空間”。

契約精神?聯盟

  我們是一個團隊,不是一個家庭。

  企業跟員工應該是一種什麼樣的關係?

  領英的執行總裁在《聯盟》這本書中開頭就寫道:“我們是一個團隊,不是一個家庭”。

  近期屢屢爆出的HR被辭退事件和員工因患病被辭退事件,引起了輿論共鳴。但是希望大家認識到員工和公司的關係是合作和聯盟關係。未來更是如此…

技工?匠人

  碼農,一群靠技術謀生的人。只是在互聯網的光環加持下,變得“高精尖”。但是,放在歷史的長河中來看,也不過是特定時代的勞動者。他們和上一個時代的磚瓦工,木匠其實沒有太大差別。是社會生產力發展到一定階段,一種工種對另一種工種的替代。

  互聯網給技術人帶來紅利,容易讓技術人感到天生的優越感,再加上身處大廠就容易自我感覺良好。

  吳軍博士對的碼農層級的分類,可以看出,技術人的發展方向不只是在技術本身,還要具備綜合能力。

  • 第五級:能獨立解決問題,完成工程工作。

  • 第四級:能指導和帶領其他人一同完成更有影響力的工作。

  • 第三級:能獨立設計和實現產品,並且在市場上獲得成功。

  • 第二級:能設計和實現別人不能做出的產品,也就是說他的作用很難取代。

  • 第一級:開創一個產業。

  試問諸君在第幾層?

  說到底,技術人應該秉持匠人精神

碼農當自強

  生活不止眼前的苟且,還有詩和遠方。

  2019年各大公司的財報,都反應很多公司效益差強人意。很多互聯網公司也在尋找新的風口和增長點。而立之年的木木君,雖然躊躇滿志,但再次陷入迷茫。深處IT行業多年,幾乎五年一個風口,技術行業更新快,玩的是創新和顛覆。移動互聯網時代是如何革傳統行業的命,木木君歷歷在目…

  同時,各大媒體充斥着中年危機的推文,一時間人人自危,催生了各種打着知識旗號販賣焦慮的二道販子。不禁讓木木君想起了之前有個大神的文章《屌絲的出路》。那個大神也是一段傳奇。

  很多碼農都在焦慮,但是又不知道如何做?技術人有一個通病,純粹。這不是缺點,但是生活是多元的,要多主動接觸技術之外的世界。

  • 副業

  同時,可以多一些副業嘗試。比如,和朋友一起接一些項目。創建自己的博客。口才好的,可以錄一些技術教學視頻放到網上。

  • 健身

  身體是革命的本錢,這裏的健身不是一定要去健身房,而是要學會鍛煉身體和合理作息。

  • 投資

  年輕的時候,做一點投資和理財。投資房產也是不錯的選擇。投資可以為未來增加一筆資產。

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

Redis是什麼?看這一篇就夠了

本文由葡萄城技術團隊編撰並首發

轉載請註明出處:,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

引言

在Web應用發展的初期,那時關係型數據庫受到了較為廣泛的關注和應用,原因是因為那時候Web站點基本上訪問和併發不高、交互也較少。而在後來,隨着訪問量的提升,使用關係型數據庫的Web站點多多少少都開始在性能上出現了一些瓶頸,而瓶頸的源頭一般是在磁盤的I/O上。而隨着互聯網技術的進一步發展,各種類型的應用層出不窮,這導致在當今雲計算、大數據盛行的時代,對性能有了更多的需求,主要體現在以下四個方面:

  1. 低延遲的讀寫速度:應用快速地反應能極大地提升用戶的滿意度
  2. 支撐海量的數據和流量:對於搜索這樣大型應用而言,需要利用PB級別的數據和能應對百萬級的流量
  3. 大規模集群的管理:系統管理員希望分佈式應用能更簡單的部署和管理
  4. 龐大運營成本的考量:IT部門希望在硬件成本、軟件成本和人力成本能夠有大幅度地降低

為了克服這一問題,NoSQL應運而生,它同時具備了高性能、可擴展性強、高可用等優點,受到廣泛開發人員和倉庫管理人員的青睞。

Redis是什麼

Redis是現在最受歡迎的NoSQL數據庫之一,Redis是一個使用ANSI C編寫的開源、包含多種數據結構、支持網絡、基於內存、可選持久性的鍵值對存儲數據庫,其具備如下特性:

  • 基於內存運行,性能高效
  • 支持分佈式,理論上可以無限擴展
  • key-value存儲系統
  • 開源的使用ANSI C語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API

相比於其他數據庫類型,Redis具備的特點是:

  • C/S通訊模型
  • 單進程單線程模型
  • 豐富的數據類型
  • 操作具有原子性
  • 持久化
  • 高併發讀寫
  • 支持lua腳本

哪些大廠在使用Redis?

  • github
  • twitter
  • 微博
  • Stack Overflow
  • 阿里巴巴
  • 百度
  • 美團
  • 搜狐

Redis的應用場景有哪些?

Redis 的應用場景包括:緩存系統(“熱點”數據:高頻讀、低頻寫)、計數器、消息隊列系統、排行榜、社交網絡和實時系統。

 

Redis的數據類型及主要特性

Redis提供的數據類型主要分為5種自有類型和一種自定義類型,這5種自有類型包括:String類型、哈希類型、列表類型、集合類型和順序集合類型。

String類型:

它是一個二進制安全的字符串,意味着它不僅能夠存儲字符串、還能存儲圖片、視頻等多種類型, 最大長度支持512M。

對每種數據類型,Redis都提供了豐富的操作命令,如:

  • GET/MGET
  • SET/SETEX/MSET/MSETNX
  • INCR/DECR
  • GETSET
  • DEL

哈希類型:

該類型是由field和關聯的value組成的map。其中,field和value都是字符串類型的。

Hash的操作命令如下:

  • HGET/HMGET/HGETALL
  • HSET/HMSET/HSETNX
  • HEXISTS/HLEN
  • HKEYS/HDEL
  • HVALS

列表類型:

該類型是一個插入順序排序的字符串元素集合, 基於雙鏈表實現。

List的操作命令如下:

  • LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
  • LINDEX/LRANGE
  • LLEN/LTRIM

集合類型:

Set類型是一種無順序集合, 它和List類型最大的區別是:集合中的元素沒有順序, 且元素是唯一的。

Set類型的底層是通過哈希表實現的,其操作命令為:

  • SADD/SPOP/SMOVE/SCARD
  • SINTER/SDIFF/SDIFFSTORE/SUNION

Set類型主要應用於:在某些場景,如社交場景中,通過交集、並集和差集運算,通過Set類型可以非常方便地查找共同好友、共同關注和共同偏好等社交關係。

順序集合類型:

ZSet是一種有序集合類型,每個元素都會關聯一個double類型的分數權值,通過這個權值來為集合中的成員進行從小到大的排序。與Set類型一樣,其底層也是通過哈希表實現的。

ZSet命令:

  • ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT
  • ZINTER/ZDIFF/ZDIFFSTORE/ZUNION

Redis的數據結構

Redis的數據結構如下圖所示:

關於上表中的部分釋義:

  1. 壓縮列表是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數,要麼就是長度比較短的字符串,Redis就會使用壓縮列表來做列表鍵的底層實現
  2. 整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現

如下是定義一個Struct數據結構的例子:

 

簡單動態字符串SDS (Simple Dynamic String)

基於C語言中傳統字符串的缺陷,Redis自己構建了一種名為簡單動態字符串的抽象類型,簡稱SDS,其結構如下:

SDS幾乎貫穿了Redis的所有數據結構,應用十分廣泛。

SDS的特點

和C字符串相比,SDS的特點如下:

  1. 常數複雜度獲取字符串長度

    Redis中利用SDS字符串的len屬性可以直接獲取到所保存的字符串的長
    度,直接將獲取字符串長度所需的複雜度從C字符串的O(N)降低到了O(1)。

  2. 減少修改字符串時導致的內存重新分配次數

    通過C字符串的特性,我們知道對於一個包含了N個字符的C字符串來說,其底層實現總是N+1個字符長的數組(額外一個空字符結尾)

    那麼如果這個時候需要對字符串進行修改,程序就需要提前對這個C字符串數組進行一次內存重分配(可能是擴展或者釋放) 

    而內存重分配就意味着是一個耗時的操作。

Redis巧妙的使用了SDS避免了C字符串的缺陷。在SDS中,buf數組的長度不一定就是字符串的字符數量加一,buf數組裡面可以包含未使用的字節,而這些未使用的字節由free屬性記錄。

與此同時,SDS採用了空間預分配的策略,避免C字符串每一次修改時都需要進行內存重分配的耗時操作,將內存重分配從原來的每修改N次就分配N次——>降低到了修改N次最多分配N次。

如下是Redis對SDS的簡單定義:

  

Redis特性1:事務

  • 命令序列化,按順序執行
  • 原子性
  • 三階段: 開始事務 – 命令入隊 – 執行事務
  • 命令:MULTI/EXEC/DISCARD

Redis特性2:發布訂閱(Pub/Sub)

  • Pub/sub是一種消息通訊模式
  • Pub發送消息, Sub接受消息
  • Redis客戶端可以訂閱任意數量的頻道
  • “fire and forgot”, 發送即遺忘
  • 命令:Publish/Subscribe/Psubscribe/UnSub

  

Redis特性3:Stream

  • Redis 5.0新增
  • 等待消費
  • 消費組(組內競爭)
  • 消費歷史數據
  • FIFO

 

 

 

以上就是Redis的基本概念,下面我們將介紹在開發過程中可能會踩到的“坑”。

Redis常見問題解析:擊穿

概念:在Redis獲取某一key時, 由於key不存在, 而必須向DB發起一次請求的行為, 稱為“Redis擊穿”。

引發擊穿的原因:

  • 第一次訪問
  • 惡意訪問不存在的key
  • Key過期

合理的規避方案:

  • 服務器啟動時, 提前寫入
  • 規範key的命名, 通過中間件攔截
  • 對某些高頻訪問的Key,設置合理的TTL或永不過期

Redis常見問題解析:雪崩

概念:Redis緩存層由於某種原因宕機后,所有的請求會湧向存儲層,短時間內的高併發請求可能會導致存儲層掛機,稱之為“Redis雪崩”。

合理的規避方案:

  • 使用Redis集群
  • 限流

Redis在產品開發中的應用實踐

為此,我很高興的為大家介紹,葡萄城架構師Jim將在2019-11-27 14:00 為大家帶來一場公開課,其中 Jim除了為大家講解Redis的基礎,同時也會實際演示他所在的項目組使用Redis時碰到的問題以及解決方案,對於剛接觸Redis的同學來說,更具參考意義和學習價值,歡迎大家屆時參加,公開課地址:。

  • 後端採用nodeJS
  • 使用Azure的Redis服務
  • Redis的使用場景

    -  token緩存, 用於令牌驗證

    -  IP白名單

碰到的問題

  • “網絡抖動”或者Redis服務異常導致Redis訪問超時
  • Redis客戶端驅動穩定性問題

    -  連接池 “Broken connection” 問題

    -  JS的Promise引出的Redis重置問題

下面我們來簡單了解一下Redis的進階知識。

進階之Redis協議簡介

Redis客戶端通訊協議:RESP(Redis Serialization Protocol),其特點是:

  • 簡單
  • 解析速度快
  • 可讀性好

Redis集群內部通訊協議:RECP(Redis Cluster Protocol ) ,其特點是:

  • 每一個node兩個tcp 連接
  • 一個負責client-server通訊(P: 6379)
  • 一個負責node之間通訊(P: 10000 + 6379)

 

Redis協議支持的數據類型:

  • 簡單字符(首字節: “+”)

        “+OK\r\n”

  • 錯誤(首字節: “-”)

        “-error msg\r\n”

  • 数字(首字節: “:”)

        “:123\r\n”

  • 批量字符(首字節: “$”)

        “&hello\r\nWhoa re you\r\n”

  • 數組(首字節: “*”)

        “*0\r\n”

        “*-1\r\n”

除了Redis,還有什麼NoSQL型數據庫

市面上類似於Redis,同樣是NoSQL型的數據庫有很多,如下圖所示,除了Redis,還有MemCache、Cassadra和Mongo。下面,我們就分別對這幾個數據庫做一下簡要的介紹:

 

 

Memcache這是一個和Redis非常相似的數據庫,但是它的數據類型沒有Redis豐富。Memcache由LiveJournal的Brad Fitzpatrick開發,作為一套分佈式的高速緩存系統,被許多網站使用以提升網站的訪問速度,對於一些大型的、需要頻繁訪問數據庫的網站訪問速度的提升效果十分顯著。

Apache Cassandra(社區內一般簡稱為C*)這是一套開源分佈式NoSQL數據庫系統。它最初由Facebook開發,用於儲存收件箱等簡單格式數據,集Google BigTable的數據模型與Amazon Dynamo的完全分佈式架構於一身。Facebook於2008將 Cassandra 開源,由於其良好的可擴展性和性能,被 Apple、Comcast、Instagram、Spotify、eBay、Rackspace、Netflix等知名網站所採用,成為了一種流行的分佈式結構化數據存儲方案。

MongoDB:是一個基於分佈式文件存儲、面向文檔的NoSQL數據庫,由C++編寫,旨在為WEB應用提供可擴展的高性能數據存儲解決方案。MongoDB是一個介於關係數據庫和非關係數據庫之間的產品,是非關係數據庫當中功能最豐富,最像關係型數據庫的,它支持的數據結構非常鬆散,是一種類似json的BSON格式。

總結

以上就是Redis入門介紹教程,如果各位還想了解更多,歡迎通過評論和私信的方式告訴我。

 

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

原創|我是如何從零學習開發一款跨平台桌面軟件的(Markdown編輯器)

原始衝動

最近一直在學習 Electron 開發桌面應用程序,目的是想做一個桌面編輯器,雖然一直在使用Typora這款神器,但無奈Typora太過國際化,在國內水土不服,無法滿足我的一些需求。

比如實現本地圖片上傳到雲端(mac版可以藉助iPic),無法幫我把本地圖片和文章一起發布到博客園、CSDN、SegmentFault、掘金等國內知名博客平台,要麼使用一些免費或付費的圖床,藉助類似iPic的工具,把圖片一鍵上傳到雲端。

我個人也嘗試過七牛雲的免費10G存儲空間,但是說實話,這些免費的空間到最後一定是為了讓你成為付費用戶,各種限制各種吐槽在網上很容易可以搜索到。

免費的圖床如新浪微博等,還算是比較好的圖床工具,相比一些網絡上的壓根不知道啥公司甚至是歸屬個人的免費圖床,新浪應該是比較靠譜的,相對來說可以保證圖片的存活時間,我個人用過一些免費的圖床網站,記得印象深刻的就是服務器出問題,網站掛個公告,曾經的圖片再去訪問就是默認的404。

雖然新浪家大業大不是說倒閉就倒閉的,圖片相對穩定可靠,不過新浪的圖片服務器會檢測訪問來源Referer來防止外部網站引用,造成訪問403。

總結起來就是一句話,圖片還是隨着文章一鍵發布到博客平台比較好。要丟一起丟~

心理掙扎

緣起這個動機,但是下定決心依舊是困難重重。

我個人是一個Java工程師,雖說搞過Andorid、HTML前端,但對前端深感不適的我果斷放棄了。對於桌面程序開發,我連Swing都不會,造一個Markdown編輯器有點難,何況還要加上這些定製功能。

猶猶豫豫,還是決定去嘗試一下。於是調研寫跨平台的一些途徑。

先嘗試Swing,不過Swing不好實現我期望的一些功能,改成JavaFX倒是可以,不過說實話,寫起來很累,太過繁瑣,就放棄了。最後把目光瞄向electron,就它了,HTML+Js+Css,聽起來就很簡單,事實證明,無論是測試還是打包都很方便。

決定之後,便開始進行 Electron 的系統學習。

邁出第一步

第一步就是安裝 Electron 的本地開發環境,這也是大多數應用開發的第一步。

你需要安裝 Node.js 在你的本地電腦,Electron 也是依賴於 Node.js 的環境,嚴格來說, Electron 通過將 Chromium 和 Node.js 合併到同一個運行時環境中,並將其打包為Mac,Windows和Linux系統下的應用來實現這一目的。

關於 Electron 的具體開發流程,這裏不再贅述,你完全可以在開發中使用Web前端開發的思維,除了在處理多個窗口之間交互的時候,就不得不了解Eelctron的進程機制。

主進程和渲染進程

Electron 運行 package.json 的 main 腳本的進程被稱為主進程。 在主進程中運行的腳本通過創建web頁面來展示用戶界面。 一個 Electron 應用總是有且只有一個主進程。

由於 Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進程架構也被使用到。 每個 Electron 中的 web 頁面運行在它自己的渲染進程中。

在普通的瀏覽器中,web頁面通常在沙盒環境中運行,並且無法訪問操作系統的原生資源。 然而 Electron 的用戶在 Node.js 的 API 支持下可以在頁面中和操作系統進行一些底層交互。

主進程與渲染進程的區別

主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。

主進程管理所有的web頁面和它們對應的渲染進程。 每個渲染進程都是獨立的,它只關心它所運行的 web 頁面。

在頁面中調用與 GUI 相關的原生 API 是不被允許的,因為在 web 頁面里操作原生的 GUI 資源是非常危險的,而且容易造成資源泄露。 如果你想在 web 頁面里使用 GUI 操作,其對應的渲染進程必須與主進程進行通訊,請求主進程進行相關的 GUI 操作。

主進程與渲染進程通信

那麼進程間如何通訊?

Electron為主進程( main process)和渲染器進程(renderer processes)通信提供了多種實現方式,如可以使用ipcRenderer 和 ipcMain模塊發送消息,使用 remote模塊進行RPC方式通信。

你還可以用 Electron 內的 IPC 機制實現。將數據存在主進程的某個全局變量中,然後在多個渲染進程中使用 remote 模塊來訪問它。

示例代碼:

// 在主進程中
global.sharedObject = {
  someProperty: 'default value'
}
// 在第一個頁面中
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
// 在第二個頁面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

使用Electron的API

Electron在主進程和渲染進程中提供了大量API去幫助開發桌面應用程序, 在主進程和渲染進程中,你可以通過require的方式將其包含在模塊中以此,獲取Electron的API

const electron = require('electron')

所有Electron的API都被指派給一種進程類型。 許多API只能被用於主進程或渲染進程中,但其中一些API可以同時在上述兩種進程中使用。 每一個API的文檔都將聲明你可以在哪種進程中使用該API。

Electron中的窗口是使用BrowserWindow類型創建的一個實例, 它只能在主進程中使用。

// 這樣寫在主進程會有用,但是在渲染進程中會提示'未定義'
const { BrowserWindow } = require('electron')

const win = new BrowserWindow()

因為進程之間的通信是被允許的, 所以渲染進程可以調用主進程來執行任務。 Electron通過remote模塊暴露一些通常只能在主進程中獲取到的API。 為了在渲染進程中創建一個BrowserWindow的實例,通常使用remote模塊為中間件:

// 這樣寫在渲染進程中時行得通的,但是在主進程中是'未定義'
const { remote } = require('electron')
const { BrowserWindow } = remote

const win = new BrowserWindow()

使用Node.js的API

Electron同時在主進程和渲染進程中對Node.js 暴露了所有的接口。 這裡有兩個重要的定義:

1)所有在Node.js可以使用的API,在Electron中同樣可以使用。 在Electron中調用如下代碼是有用的:

const fs = require('fs')

const root = fs.readdirSync('/')

// 這會打印出磁盤根級別的所有文件
// 同時包含'/'和'C:\'。
console.log(root)

2)你可以在你的應用程序中使用Node.js的模塊。 選擇您最喜歡的 npm 模塊。 npm 提供了目前世界上最大的開源代碼庫,那裡包含良好的維護、經過測試的代碼,提供給服務器應用程序的特色功能也提供給Electron。

例如,在你的應用程序中要使用官方的AWS SDK,你需要首先安裝它的依賴:

npm install --save aws-sdk

然後在你的Electron應用中,通過require引入並使用該模塊,就像構建Node.js應用程序那樣:

// 準備好被使用的S3 client模塊
const S3 = require('aws-sdk/clients/s3')

有一個非常重要的提示: 原生Node.js模塊 (即指,需要編譯源碼過後才能被使用的模塊) 需要在編譯后才能和Electron一起使用。

最終產品殺青落地

終於搞明白了 Electron 的應用架構,那麼接着就要進入產品的開發階段。比較慶幸的是,ELectron 的UI完全由CSS+HTML組成,這部分可用的框架太多了,我選擇了又老又知名的 BootStarp 框架搭建界面UI,還引用了JS框架JQuery。選擇了 electron-store 作為本地存儲文件,至於最關鍵的Markdown語法解析,對比了一番主流解析框架,最終選擇了 markdown-it。貼一下效果圖:

這款軟件我給他起名為 JustWrite,意思就是現在就寫,也是在督促自己吧,畢竟猶豫徘徊,等於白來。

現在軟件的功能除了包含一鍵發布本地文章加本地圖片到博客園、CSDN、SegmentFault、掘金、開源中國等平台,我還打算將他打造為一個體驗不錯的Markdown寫作軟件。現在你閱讀的這篇文章,就是我使用 JustWrite 書寫的,使用的字體是我個人喜歡的幼圓體,除此之外,還有六款風格迥異的字體可以切換使用。字號也是可以動態放大或者縮小,還可以關閉右側預覽,專註於寫作,如下圖所示:

這些截圖是我截屏后使用快捷鍵Ctrl+V一鍵粘貼的,圖片會自動放到當前md文件所在目錄下的picture文件夾內。

關於 JustWrite 從構思到實踐的心路歷程大致就以上這些了,這次開發 JustWrite 也讓我過了一把產品經理的癮,基本已經滿足了我的日常需求。如果你有更好的想法和創意也可以告訴我,說不定第二天就會實現了。

Github:

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

※公開收購3c價格,不怕被賤賣!

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

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

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

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

abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之二(二十八)




 










   

       在上一篇 文章中我們學習了TreeGrid的一些基礎知識,接下我們來創建我們開發組織管理功能用到的一些類。關於如何創建類我們之前的文章中已經寫了很多了,這裡會有些簡略。

 

四、定義應用服務接口需要用到的DTO類

      為了在進行查詢時使用, PagedOrgResultRequestDto被用來將模塊數據傳遞到基礎設施層.

       1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“ABP.TPLMS.Application”項目,在彈出菜單中選擇“添加” > “新建文件夾”,並重命名為“Orgs”

      2. 使用鼠標右鍵單擊我們剛才創建的“Orgs”文件夾,在彈出菜單中選擇“添加” > “新建文件夾”,並重命名為“Dto”。

      3.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 Paged OrgResultRequestDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.Application.Services.Dto;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs.Dto
{

public class PagedOrgResultRequestDto : PagedResultRequestDto
    {
        public string Keyword { get; set; }
    }
}

      4.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 OrgDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using ABP.TPLMS.Entitys;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs.Dto
{

    [AutoMapFrom(typeof(Org))]
    public class OrgDto : EntityDto<int>
    {

        int m_parentId = 0;
        public string Name { get; set; }       

        public string HotKey { get; set; }
        public int ParentId { get { return m_parentId; } set { m_parentId = value; } }       

        public string ParentName { get; set; }
        public bool IsLeaf { get; set; }
        public bool IsAutoExpand { get; set; }       

        public string IconName { get; set; }
        public int Status { get; set; }
        public int Type { get; set; }      
        public string BizCode { get; set; }       

        public string CustomCode { get; set; }
        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }

        public int CreateId { get; set; }
        public int SortNo { get; set; }
        public int _parentId {
            get { return m_parentId; }          

        }
    }
}

 

      5.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 CreateUpdateOrgDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using ABP.TPLMS.Entitys;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;

 
namespace ABP.TPLMS.Orgs.Dto
{

    [AutoMapTo(typeof(Org))]
    public class CreateUpdateOrgDto : EntityDto<int>
    {

        public string Name { get; set; }
        [StringLength(255)]
        public string HotKey { get; set; }

        public int ParentId { get; set; }

        [Required]
        [StringLength(255)]
        public string ParentName { get; set; }

        public bool IsLeaf { get; set; }
        public bool IsAutoExpand { get; set; }

        [StringLength(255)]
        public string IconName { get; set; }

        public int Status { get; set; }
        public int Type { get; set; }

        [StringLength(255)]
        public string BizCode { get; set; }

        [StringLength(100)]
        public string CustomCode { get; set; }
        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }

        public int CreateId { get; set; }
        public int SortNo { get; set; } 

    }
}

 

 

 

 

五、定義IOrgAppService接口

        6. 在Visual Studio 2017的“解決方案資源管理器”中,鼠標右鍵單擊“Org”文件夾,然後選擇“添加” > “新建項”,在彈出對話框中選擇“接口”。為應用服務定義一個名為 IOrgAppService 的接口。代碼如下。

using System;
using System.Collections.Generic;
using System.Text;
using Abp.Application.Services;
using ABP.TPLMS.Orgs.Dto; 

 

namespace ABP.TPLMS.Orgs
{
  public  interface IOrgAppService : IAsyncCrudAppService<//定義了CRUD方法

             OrgDto, //用來展示組織信息
             int, //Org實體的主鍵
             PagedOrgResultRequestDto, //獲取組織信息的時候用於分頁
             CreateUpdateOrgDto, //用於創建組織信息
             CreateUpdateOrgDto> //用於更新組織信息

    {
    }
}

 

      六、實現IOrgAppService

       7.在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“Org”文件夾,然後選擇“添加” > “新建項”,在彈出對話框中選擇“類”。為應用服務定義一個名為 OrgAppService 的服務類。代碼如下。

using Abp.Application.Services;
using Abp.Domain.Repositories;
using ABP.TPLMS.Entitys;
using ABP.TPLMS.Orgs.Dto;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs
{

    public class OrgAppService : AsyncCrudAppService<Org, OrgDto, int, PagedOrgResultRequestDto,
                            CreateUpdateOrgDto, CreateUpdateOrgDto>, IOrgAppService 

    {
        public OrgAppService(IRepository<Org, int> repository)

            : base(repository)

        { 

        }
    }
}

七 創建OrgController繼承自TPLMSControllerBase

      1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊在領域層“ABP.TPLMS.Web.Mvc”項目中的Controller目錄。 選擇“添加” > “新建項…”。如下圖。

 

     2. 在彈出對話框“添加新項-ABP.TPLMS.Web.Mvc”中選擇“控制器類”,然後在名稱輸入框中輸入“OrgsController”,然後點擊“添加”按鈕。

     3.在OrgsController.cs文件中輸入如下代碼,通過構造函數注入對應用服務的依賴。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.AspNetCore.Mvc.Authorization;
using Abp.Web.Models;
using ABP.TPLMS.Controllers;
using ABP.TPLMS.Orgs;
using ABP.TPLMS.Orgs.Dto;
using ABP.TPLMS.Web.Models.Orgs;
using Microsoft.AspNetCore.Mvc;


namespace ABP.TPLMS.Web.Controllers
{
    [AbpMvcAuthorize]
    public class OrgsController : TPLMSControllerBase
    {
        private readonly IOrgAppService _orgAppService;
        private const int MAX_COUNT= 1000;
        
        public OrgsController(IOrgAppService orgAppService)
        {
            _orgAppService = orgAppService;
        }
        [HttpGet]
        // GET: /<controller>/
        public IActionResult Index()
        {
            return View();
        }
        [DontWrapResult]
        [HttpPost]
        public string List()
        {         
            PagedOrgResultRequestDto paged = new PagedOrgResultRequestDto();
            paged.MaxResultCount = MAX_COUNT;
           var userList = _orgAppService.GetAll(paged).GetAwaiter().GetResult().Items;
            int total = userList.Count;
            var json = JsonEasyUI(userList, total);
            return json;
        }       
    }
}

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

靚仔靚女如何用瀏覽器自拍和保存

一、前言

1.核心技術

  • Web Real-Time Communication:網頁即時通信,可以在瀏覽器進行實時語音或者視頻對話的API
  • Canvas:HTML5中的新元素,可以用來來繪製圖形、圖標、以及其它任何視覺性圖像

2.音頻採集的基本概念

  • 攝像頭:用於採集圖像和視頻
  • 麥克風:採集音頻數據
  • 幀率:一秒鐘採集圖像的次數。幀率越高,越平滑流暢
  • 軌:借鑒了多媒體的概念,每條軌數據都是獨立的,如MP4中的音頻軌、視頻軌,是分別被存儲的
  • 流:可以理解為容器。在WebRTC中,流可以分為媒體流(MediaStream)和數據流(DataStream)。
  • 分辨率:2K、1080P、720P等,越清晰,佔用帶寬越多

3.音視頻設備的基本原理

  • 音頻設備
    音頻輸入設備主要是採集音數據,而採集音頻數據的本質是模擬信號轉成数字信號,
    採集到的數據經過量化、編碼,最終開成数字信號,這就是音頻設備要完成的工作。
    人的聽覺範圍的頻率是20Hz~20kHz之間,日常語音交流8kHz就哆了。
    為了追求高品質、高保真,需要將音頻輸入設備採樣率設置在40kHz上才能完整保留原始信號

  • 視頻設備
    當實物光通過鏡頭進行攝像機后,它會通過視頻設備的模數轉換(A/D)模塊,即光學傳感器,將光轉換成数字信號,即RGB數據,獲得RGB數據后,再通過DSP進行優化處理,如自動增強、白平衡、色彩飽和等,等到24位的真彩色圖片

模數轉換使用的採集定理稱為奈奎斯特定理:

在進行模擬 / 数字信號的轉換過程中,當採樣率大於信號中最高頻率的 2 倍時,採樣之後的数字信號就完整地保留原始信號中的信息。

talk is cheap, 上代碼,以下示例運行的時候會請求攝像頭權限,同意即可,接下來就是見證奇迹的時刻!

二、示例

1.示例1-打開攝像頭

這就是照像的核心功能,以後可以用來化妝,擠痘痘,整理髮型

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>打開攝像頭</title>
</head>
<body>
<h1>打開攝像頭</h1>
<video autoplay playsinline></video>
</body>
</html>

<script>
    const mediaStreamContrains = {
        video: {
            frameRate: {min: 20},
            width: {min: 640, ideal: 1280},
            height: {min: 360, ideal: 720},
            aspectRatio: 16 / 9
        },
        audio: {
            echoCancellation: true,
            noiseSuppression: true,
            autoGainControl: true
        }
    };

    const localVideo = document.querySelector('video');

    function gotLocalMediaStream(mediaStream) {
        localVideo.srcObject = mediaStream;
    }

    function handleLocalMediaStreamError(error) {
        console.log('navigator.getUserMedia error: ', error);
    }

    navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
        gotLocalMediaStream
    ).catch(
        handleLocalMediaStreamError
    );
</script>

運行結果如下

示例2-拍照保存

這裏展示了兩個按鈕,拍照和保存,yes,就是自拍的核心功能!

<html>
<head>
    <meta charset="UTF-8">
    <title>拍照一分鐘,P圖兩小時</title>
</head>

<body>
<section>
    <div>
        <video autoplay playsinline id="player"></video>
    </div>

</section>
<section>
    <div>
        <button id="snapshot">拍照</button>
        <button id="download">下載</button>
    </div>
    <div>
        <canvas id="picture"></canvas>
    </div>
</section>
</body>
</html>


<script>
    'use strict';

    var videoplay = document.querySelector('video#player');

    function gotMediaStream(stream) {
        window.stream = stream;
        videoplay.srcObject = stream;
    }

    function handleError(err) {
        console.log('getUserMedia error:', err);
    }

    function start() {
        var constraints = {
            video: {
                width: 1280,
                height: 720,
                frameRate: 15,
                facingMode: 'enviroment'
            },
            audio: false
        }

        navigator.mediaDevices.getUserMedia(constraints)
            .then(gotMediaStream)
            .catch(handleError);
    }


    //拍照
    var snapshot = document.querySelector('button#snapshot');
    snapshot.onclick = function () {
        var picture = document.querySelector('canvas#picture');
        picture.width = 1280;
        picture.height = 720;
        picture.getContext('2d').drawImage(videoplay, 0, 0, picture.width, picture.height);
    };


    //下載
    function downLoad(url) {
        var oA = document.createElement("a");
        oA.download = 'photo';// 設置下載的文件名,默認是'下載'
        oA.href = url;
        document.body.appendChild(oA);
        oA.click();
        oA.remove(); // 下載之後把創建的元素刪除
    }

    document.querySelector("button#download").onclick = function () {
        downLoad(picture.toDataURL("image/jpeg"));
    };

    start();

</script>

運行結果如下

就是這麼簡單!

重點方法和參數解釋

  • 1.方法:avigator.mediaDevices.getUserMedia(constraints);
    返回一個promise對象,調用成功,可以通過promise對象獲取MediaStream對象,

  • 2.參數:mediaStreamContrains
    傳入的constraints參數類型為 MediaStreamConstraints,可以指定 MediaStream 中包含哪些類型的媒體軌(音頻軌、視頻軌),並且可為這些媒體軌設置一些限制。
    視頻的幀率最小 20 幀每秒;
    寬度最小是 640,理想的寬度是 1280;
    高度最小是 360,最理想高度是 720;
    寬高比是 16:9;
    對於音頻則是開啟迴音消除、降噪以及自動增益功能。

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

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

不只是換殼上市!EC-05 與 Gogoro S2 實測比較

EC-05 由台灣山葉(YAMAHA)和 Gogoro 聯手打造,動力系統採用與 Gogoro S2 相同的 G2 鋁合金水冷永磁同步馬達(S-Version),許多人不禁疑問 EC-05 和 Gogoro S2 到底有什麼差別?難道只是換了一個車殼?更別說 EC-05 的價格還比 Gogoro S2 貴了 2,820 元。為了比較其中的差別,《科技新報》試駕了 EC-05 和 2020 年式的 Gogoro S2,帶來兩台車的第一手觀察。

EC-05 配備嵌合式環型頭燈,能在夜間騎乘時避免光線發散,確保前方視野的明亮程度。Gogoro S2 同樣配備環型頭燈,不過形狀略有不同,和一般機車一樣完全外露。EC-05 的後照鏡為菱形設計,視野較為寬廣,Gogoro S2 的後照鏡是圓形,視野較狹窄,而且騎乘者容易被自己的身體擋住部分視線。

Gogoro S2 前方配備了多功能置物箱,可以擺放手機和手套等小型物品,也能做為飲料置杯架使用,EC-05 就沒有這項裝置。Gogoro S2 的前方置物箱內附 USB 充電插槽,EC-05 的 USB 插孔則位於後座的置物箱。EC-05 使用無線智慧鑰匙,靠近車輛後按壓鑰匙的灰色部分進行解鎖。Gogoro S2 則使用 iQ system 智慧鑰匙卡,只要輕觸 S 標誌就能解鎖。

EC-05(左)配備嵌合式環型頭燈,Gogoro S2 的頭燈則外露。

EC-05 後照鏡呈菱形,視野較寬廣。

Gogoro S2 的後照鏡為圓形。

Gogoro S2 配備多功能置物箱,並附有 USB 充電插槽。

EC-05 的 USB 充電插槽位於後置物箱內側。

EC-05 以無線智慧鑰匙解鎖。

試乘時騎行陽明山的山路,EC-05 的穩定度和避震能力優於 Gogoro S2,在路面凹凸不平時較不容易顛簸,雙載騎乘時的差距更加明顯。EC-05 的座墊將後座的後方略為墊高,讓座墊呈現「山」字型的結構,側邊則採切削設計,讓騎乘者的腿部更為舒適。Gogoro S2 的後座較為平滑,中間使用皮質材質並標有「S」字樣。雖然乍看之下差異不大,但實際騎乘時後座乘客的感受明顯不同。

EC-05 的乘客在機車減速和加速時都更容易維持穩定,比較不會有往後飛出去或往前貼到前方騎乘者身上的狀況,即使長時間乘坐也不會滑動。Gogoro S2 的後座把手較為平直,EC-05 的把手彎曲向上,對於手不夠長的乘客更容易抓握。相較於 Gogoro S2,EC-05 能給予後座乘客更安穩的乘坐體驗。

EC-05 的座墊呈現「山」字型的凸起,讓後座乘客乘坐時更穩定。

Gogoro S2 的座墊較為平滑。

EC-05 與 Gogoro S2 座墊比較。

EC-05 的後座把手彎曲向上,較容易抓握。

Gogoro S2 的後座把手平直,手沒那麼長的後座乘客較難抓握。

EC-05(右)與 Gogoro S2 的後座把手差異從車尾可以明顯看出。

由於 EC-05 與 Gogoro S2 採用相同的動力系統,因此性能上的數據相差無幾。EC-05 最大馬力為 10hp,安全極速達到時速 90 公里,靜止加速到時速 50 公里需要 3.9 秒。Gogoro S2 最大馬力為 10.18hp,安全極速達到時速 92 公里,靜止加速到時速 50 公里僅需 3.8 秒,各方面都是 Gogoro S2 稍勝。

相較於 Gogoro S2,EC-05 的儀表板略為調整傾斜角度並向前方移動,用以減少騎乘者所需的視線移動。EC-05 的腳踏板具有菱形的凸起幫助止滑,讓騎乘時腳的位置可以更穩定。Gogoro S2 的腳踏板則是完全平滑,並有著「gogoro」的字樣。

EC-05 在車及座墊高度上也有更動,車高為 1,180 mm 略高於 Gogoro S2 的 1,080 mm,座高則以 768 mm 略低於 Gogoro S2 的 780 mm。調整過後 EC-05 在龍頭和座墊間的空間增加,騎乘時較為舒適也不容易意外按壓到喇叭等按鍵。不過 EC-05 的龍頭相較 Gogoro S2 略為沉重,力氣較小的使用者操控時可能會稍微費力。

EC-05 的腳踏板具有止滑的菱形格紋。

Gogoro S2 的腳踏板較為平滑。

身高 176 公分的騎乘者與 EC-05 的比例。

身高 156 公分的騎乘者與 EC-05 的比例。

YAMAHA 的設計理念標榜「人機官能」,也就是在設計時結合機械結構和人體工學。這樣的概念讓 EC-05 看似與 Gogoro S2 僅有微幅的差異,但這些細節卻在騎乘時能發揮不成比例的效果。至於這樣的差別值不值得多花 2,820 元購買,那就要看個人是否在意這些細節帶來的感受。

EC-05 提供藍灰色、深黑色、深藍灰色和白銀色 4 種顏色讓消費者選擇,定價為台幣 99,800 元。Gogoro S2 僅有灰黑色一個款式,定價為台幣 96,980 元。

(合作媒體:。圖片來源:)

延伸閱讀:

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

【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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