從零開始入門 | Kubernetes 中的服務發現與負載均衡

作者 | 阿里巴巴技術專家  溪恆

一、需求來源

為什麼需要服務發現

在 K8s 集群裏面會通過 pod 去部署應用,與傳統的應用部署不同,傳統應用部署在給定的機器上面去部署,我們知道怎麼去調用別的機器的 IP 地址。但是在 K8s 集群裏面應用是通過 pod 去部署的, 而 pod 生命周期是短暫的。在 pod 的生命周期過程中,比如它創建或銷毀,它的 IP 地址都會發生變化,這樣就不能使用傳統的部署方式,不能指定 IP 去訪問指定的應用。

另外在 K8s 的應用部署里,之前雖然學習了 deployment 的應用部署模式,但還是需要創建一個 pod 組,然後這些 pod 組需要提供一個統一的訪問入口,以及怎麼去控制流量負載均衡到這個組裡面。比如說測試環境、預發環境和線上環境,其實在部署的過程中需要保持同樣的一個部署模板以及訪問方式。因為這樣就可以用同一套應用的模板在不同的環境中直接發布。

Service:Kubernetes 中的服務發現與負載均衡

最後應用服務需要暴露到外部去訪問,需要提供給外部的用戶去調用的。我們上節了解到 pod 的網絡跟機器不是同一個段的網絡,那怎麼讓 pod 網絡暴露到去給外部訪問呢?這時就需要服務發現。

在 K8s 裏面,服務發現與負載均衡就是 K8s Service。上圖就是在 K8s 里 Service 的架構,K8s Service 向上提供了外部網絡以及 pod 網絡的訪問,即外部網絡可以通過 service 去訪問,pod 網絡也可以通過 K8s Service 去訪問。

向下,K8s 對接了另外一組 pod,即可以通過 K8s Service 的方式去負載均衡到一組 pod 上面去,這樣相當於解決了前面所說的複發性問題,或者提供了統一的訪問入口去做服務發現,然後又可以給外部網絡訪問,解決不同的 pod 之間的訪問,提供統一的訪問地址。

二、用例解讀

下面進行實際的一個用例解讀,看 pod K8s 的 service 要怎麼去聲明、怎麼去使用?

Service 語法

首先來看 K8s Service 的一個語法,上圖實際就是 K8s 的一個聲明結構。這個結構里有很多語法,跟之前所介紹的 K8s 的一些標準對象有很多相似之處。比如說標籤 label 去做一些選擇、selector 去做一些選擇、label 去聲明它的一些 label 標籤等。

這裡有一個新的知識點,就是定義了用於 K8s Service 服務發現的一個協議以及端口。繼續來看這個模板,聲明了一個名叫 my-service 的一個 K8s Service,它有一個 app:my-service 的 label,它選擇了 app:MyApp 這樣一個 label 的 pod 作為它的後端。

最後是定義的服務發現的協議以及端口,這個示例中我們定義的是 TCP 協議,端口是 80,目的端口是 9376,效果是訪問到這個 service 80 端口會被路由到後端的 targetPort,就是只要訪問到這個 service 80 端口的都會負載均衡到後端 app:MyApp 這種 label 的 pod 的 9376 端口。

創建和查看 Service

如何去創建剛才聲明的這個 service 對象,以及它創建之後是什麼樣的效果呢?通過簡單的命令:

  • kubectl apply -f service.yaml

或者是

  • kubectl created -f service.yaml

上面的命令可以簡單地去創建這樣一個 service。創建好之後,可以通過:

  • kubectl discribe service

去查看 service 創建之後的一個結果。

service 創建好之後,你可以看到它的名字是 my-service。Namespace、Label、Selector 這些都跟我們之前聲明的一樣,這裏聲明完之後會生成一個 IP 地址,這個 IP 地址就是 service 的 IP 地址,這個 IP 地址在集群裏面可以被其它 pod 所訪問,相當於通過這個 IP 地址提供了統一的一個 pod 的訪問入口,以及服務發現。

這裏還有一個 Endpoints 的屬性,就是我們通過 Endpoints 可以看到:通過前面所聲明的 selector 去選擇了哪些 pod?以及這些 pod 都是什麼樣一個狀態?比如說通過 selector,我們看到它選擇了這些 pod 的一個 IP,以及這些 pod 所聲明的 targetPort 的一個端口。

實際的架構如上圖所示。在 service 創建之後,它會在集群裏面創建一個虛擬的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通過這樣一個 IP 地址和端口去訪問到這個 service。這個 service 會把它選擇的 pod 及其 IP 地址都掛載到後端。這樣通過 service 的 IP 地址訪問時,就可以負載均衡到後端這些 pod 上面去。

當 pod 的生命周期有變化時,比如說其中一個 pod 銷毀,service 就會自動從後端摘除這個 pod。這樣實現了:就算 pod 的生命周期有變化,它訪問的端點是不會發生變化的。

集群內訪問 Service

在集群裏面,其他 pod 要怎麼訪問到我們所創建的這個 service 呢?有三種方式:

  • 首先我們可以通過 service 的虛擬 IP 去訪問,比如說剛創建的 my-service 這個服務,通過 kubectl get svc 或者 kubectl discribe service 都可以看到它的虛擬 IP 地址是 172.29.3.27,端口是 80,然後就可以通過這個虛擬 IP 及端口在 pod 裏面直接訪問到這個 service 的地址。

  • 第二種方式直接訪問服務名,依靠 DNS 解析,就是同一個 namespace 里 pod 可以直接通過 service 的名字去訪問到剛才所聲明的這個 service。不同的 namespace 裏面,我們可以通過 service 名字加“.”,然後加 service 所在的哪個 namespace 去訪問這個 service,例如我們直接用 curl 去訪問,就是 my- 就可以訪問到這個 service。

  • 第三種是通過環境變量訪問,在同一個 namespace 里的 pod 啟動時,K8s 會把 service 的一些 IP 地址、端口,以及一些簡單的配置,通過環境變量的方式放到 K8s 的 pod 裏面。在 K8s pod 的容器啟動之後,通過讀取系統的環境變量比讀取到 namespace 裏面其他 service 配置的一個地址,或者是它的端口號等等。比如在集群的某一個 pod 裏面,可以直接通過 curl $ 取到一個環境變量的值,比如取到 MY_SERVICE_SERVICE_HOST 就是它的一個 IP 地址,MY_SERVICE 就是剛才我們聲明的 MY_SERVICE,SERVICE_PORT 就是它的端口號,這樣也可以請求到集群裏面的 MY_SERVICE 這個 service。

Headless Service

service 有一個特別的形態就是 Headless Service。service 創建的時候可以指定 clusterIP:None,告訴 K8s 說我不需要 clusterIP(就是剛才所說的集群裏面的一個虛擬 IP),然後 K8s 就不會分配給這個 service 一個虛擬 IP 地址,它沒有虛擬 IP 地址怎麼做到負載均衡以及統一的訪問入口呢?

它是這樣來操作的:pod 可以直接通過 service_name 用 DNS 的方式解析到所有後端 pod 的 IP 地址,通過 DNS 的 A 記錄的方式會解析到所有後端的 Pod 的地址,由客戶端選擇一個後端的 IP 地址,這個 A 記錄會隨着 pod 的生命周期變化,返回的 A 記錄列表也發生變化,這樣就要求客戶端應用要從 A 記錄把所有 DNS 返回到 A 記錄的列表裡面 IP 地址中,客戶端自己去選擇一個合適的地址去訪問 pod。

可以從上圖看一下跟剛才我們聲明的模板的區別,就是在中間加了一個 clusterIP:None,即表明不需要虛擬 IP。實際效果就是集群的 pod 訪問 my-service 時,會直接解析到所有的 service 對應 pod 的 IP 地址,返回給 pod,然後 pod 裏面自己去選擇一個 IP 地址去直接訪問。

向集群外暴露 Service

前面介紹的都是在集群裏面 node 或者 pod 去訪問 service,service 怎麼去向外暴露呢?怎麼把應用實際暴露給公網去訪問呢?這裏 service 也有兩種類型去解決這個問題,一個是 NodePort,一個是 LoadBalancer。

  • NodePort 的方式就是在集群的 node 上面(即集群的節點的宿主機上面)去暴露節點上的一個端口,這樣相當於在節點的一個端口上面訪問到之後就會再去做一層轉發,轉發到虛擬的 IP 地址上面,就是剛剛宿主機上面 service 虛擬 IP 地址。

  • LoadBalancer 類型就是在 NodePort 上面又做了一層轉換,剛才所說的 NodePort 其實是集群裏面每個節點上面一個端口,LoadBalancer 是在所有的節點前又掛一個負載均衡。比如在阿里雲上掛一個 SLB,這個負載均衡會提供一個統一的入口,並把所有它接觸到的流量負載均衡到每一個集群節點的 node pod 上面去。然後 node pod 再轉化成 ClusterIP,去訪問到實際的 pod 上面。

三、操作演示

下面進行實際操作演示,在阿里雲的容器服務上面進去體驗一下如何使用 K8s Service。

創建 Service

我們已經創建好了一個阿里雲的容器集群,然後並且配置好本地終端到阿里雲容器集群的一個連接。

首先可以通過 kubectl get cs ,可以看到我們已經正常連接到了阿里雲容器服務的集群上面去。

今天將通過這些模板實際去體驗阿里雲服務上面去使用 K8s Service。有三個模板,首先是 client,就是用來模擬通過 service 去訪問 K8s 的 service,然後負載均衡到我們的 service 裏面去聲明的一組 pod 上。

K8s Service 的上面,跟剛才介紹一樣,我們創建了一個 K8s Service 模板,裏面 pod,K8s Service 會通過前端指定的 80 端口負載均衡到後端 pod 的 80 端口上面,然後 selector 選擇到 run:nginx 這樣標籤的一些 pod 去作為它的後端。

然後去創建帶有這樣標籤的一組 pod,通過什麼去創建 pod 呢?就是之前所介紹的 K8s deployment,通過 deployment 我們可以輕鬆創建出一組 pod,然後上面聲明 run:nginx 這樣一個label,並且它有兩個副本,會同時跑出來兩個 pod。

先創建一組 pod,就是創建這個 K8s deployment,通過 kubectl create -f service.yaml。這個 deployment 也創建好了,再看一下 pod 有沒有創建出來。如下圖看到這個 deployment 所創建的兩個 pod 都已經在 running 了。通過 kubectl get pod -o wide 可以看到 IP 地址。通過 -l,即 label 去做篩選,run=nginx。如下圖所示可以看到,這兩個 pod 分別是 10.0.0.135 和 10.0.0.12 這樣一個 IP 地址,並且都是帶 run=nginx 這個 label 的。

下面我們去創建 K8s service,就是剛才介紹的通過 service 去選擇這兩個 pod。這個 service 已經創建好了。

根據剛才介紹,通過 kubectl describe svc 可以看到這個 service 實際的一個狀態。如下圖所示,剛才創建的 nginx service,它的選擇器是 run=nginx,通過 run=nginx 這個選擇器選擇到後端的 pod 地址,就是剛才所看到那兩個 pod 的地址:10.0.0.12 和 10.0.0.135。這裏可以看到 K8s 為它生成了集群裏面一個虛擬 IP 地址,通過這個虛擬 IP 地址,它就可以負載均衡到後面的兩個 pod 上面去。

現在去創建一個客戶端的 pod 實際去感受一下如何去訪問這個 K8s Service,我們通過 client.yaml 去創建客戶端的 pod,kubectl get pod 可以看到客戶端 pod 已經創建好並且已經在運行中了。

通過 kubectl exec 到這個 pod 裏面,進入這個 pod 去感受一下剛才所說的三種訪問方式,首先可以直接去訪問這個 K8s 為它生成的這個 ClusterIP,就是虛擬 IP 地址,通過 curl 訪問這個 IP 地址,這個 pod 裏面沒有裝 curl。通過 wget 這個 IP 地址,輸入進去測試一下。可以看到通過這個去訪問到實際的 IP 地址是可以訪問到後端的 nginx 上面的,這個虛擬是一個統一的入口。

第二種方式,可以通過直接 service 名字的方式去訪問到這個 service。同樣通過 wget,訪問我們剛才所創建的 service 名 nginx,可以發現跟剛才看到的結果是一樣的。

在不同的 namespace 時,也可以通過加上 namespace 的一個名字去訪問到 service,比如這裏的 namespace 為 default。

最後我們介紹的訪問方式裏面還可以通過環境變量去訪問,在這個 pod 裏面直接通過執行 env 命令看一下它實際注入的環境變量的情況。看一下 nginx 的 service 的各種配置已經註冊進來了。

可以通過 wget 同樣去訪問這樣一個環境變量,然後可以訪問到我們的一個 service。

介紹完這三種訪問方式,再看一下如何通過 service 外部的網絡去訪問。我們 vim 直接修改一些剛才所創建的 service。

最後我們添加一個 type,就是 LoadBalancer,就是我們前面所介紹的外部訪問的方式。

然後通過 kubectl apply,這樣就把剛剛修改的內容直接生效在所創建的 service 裏面。

現在看一下 service 會有哪些變化呢?通過 kubectl get svc -o wide,我們發現剛剛創建的 nginx service 多了一個 EXTERNAL-IP,就是外部訪問的一個 IP 地址,剛才我們所訪問的都是 CLUSTER-IP,就是在集群裏面的一個虛擬 IP 地址。

然後現在實際去訪問一下這個外部 IP 地址 39.98.21.187,感受一下如何通過 service 去暴露我們的應用服務,直接在終端裏面點一下,這裏可以看到我們直接通過這個應用的外部訪問端點,可以訪問到這個 service,是不是很簡單?

我們最後再看一下用 service 去實現了 K8s 的服務發現,就是 service 的訪問地址跟 pod 的生命周期沒有關係。我們先看一下現在的 service 後面選擇的是這兩個 pod IP 地址。

我們現在把其中的一個 pod 刪掉,通過 kubectl delete 的方式把前面一個 pod 刪掉。

我們知道 deployment 會讓它自動生成一個新的 pod,現在看 IP 地址已經變成 137。

現在再去 describe 一下剛才的 service,如下圖,看到前面訪問端點就是集群的 IP 地址沒有發生變化,對外的 LoadBalancer 的 IP 地址也沒有發生變化。在所有不影響客戶端的訪問情況下,後端的一個 pod IP 已經自動放到了 service 後端裏面。

這樣就相當於在應用的組件調用的時候可以不用關心 pod 在生命周期的一個變化。

以上就是所有演示。

四、架構設計

最後是對 K8s 設計的一個簡單的分析以及實現的一些原理。

Kubernetes 服務發現架構

如上圖所示,K8s 服務發現以及 K8s Service 是這樣整體的一個架構。

K8s 分為 master 節點和 worker 節點:

  • master 裏面主要是 K8s 管控的內容;
  • worker 節點裏面是實際跑用戶應用的一個地方。

在 K8s master 節點裏面有 APIServer,就是統一管理 K8s 所有對象的地方,所有的組件都會註冊到 APIServer 上面去監聽這個對象的變化,比如說我們剛才的組件 pod 生命周期發生變化,這些事件。

這裏面最關鍵的有三個組件:

  • 一個是 Cloud Controller Manager,負責去配置 LoadBalancer 的一個負載均衡器給外部去訪問;
  • 另外一個就是 Coredns,就是通過 Coredns 去觀測 APIServer 裏面的 service 後端 pod 的一個變化,去配置 service 的 DNS 解析,實現可以通過 service 的名字直接訪問到 service 的虛擬 IP,或者是 Headless 類型的 Service 中的 IP 列表的解析;
  • 然後在每個 node 裏面會有 kube-proxy 這個組件,它通過監聽 service 以及 pod 變化,然後實際去配置集群裏面的 node pod 或者是虛擬 IP 地址的一個訪問。

實際訪問鏈路是什麼樣的呢?比如說從集群內部的一個 Client Pod3 去訪問 Service,就類似於剛才所演示的一個效果。Client Pod3 首先通過 Coredns 這裏去解析出 ServiceIP,Coredns 會返回給它 ServiceName 所對應的 service IP 是什麼,這個 Client Pod3 就會拿這個 Service IP 去做請求,它的請求到宿主機的網絡之後,就會被 kube-proxy 所配置的 iptables 或者 IPVS 去做一層攔截處理,之後去負載均衡到每一個實際的後端 pod 上面去,這樣就實現了一個負載均衡以及服務發現。

對於外部的流量,比如說剛才通過公網訪問的一個請求。它是通過外部的一個負載均衡器 Cloud Controller Manager 去監聽 service 的變化之後,去配置的一個負載均衡器,然後轉發到節點上的一個 NodePort 上面去,NodePort 也會經過 kube-proxy 的一個配置的一個 iptables,把 NodePort 的流量轉換成 ClusterIP,緊接着轉換成後端的一個 pod 的 IP 地址,去做負載均衡以及服務發現。這就是整個 K8s 服務發現以及 K8s Service 整體的結構。

後續進階

後續再進階部分我們還會更加深入地去講解 K8s Service 的實現原理,以及在 service 網絡出問題之後,如何去診斷以及去修復的技巧。

本文總結

本文的主要內容就到此為止了,這裏為大家簡單總結一下:

  1. 為什麼雲原生的場景需要服務發現和負載均衡,
  2. 在 Kubernetes 中如何使用 Kubernetes 的 Service 做服務發現和負載均衡
  3. Kubernetes 集群中 Service 涉及到的組件和大概實現原理

相信經過本文的學習與把握,大家能夠通過 Kubernetes Service 將複雜的企業級應用快速並標準地編排起來。

“阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

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

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

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

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

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

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

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

【Spring】簡述@Configuration配置類註冊BeanDefinition到Spring容器的過程

概述

本文以SpringBoot應用為基礎,嘗試分析基於註解@Configuration的配置類是如何向Spring容器註冊BeanDefinition的過程

其中主要分析了 ConfigurationClassPostProcessor 這個BeanDefinitionRegistryPostProcessor 即Bean定義註冊後置處理器,在Spring啟動過程中對@Configuration配置類的處理,主要體現在 解析並發現所有配置類,處理配置類的相關邏輯(如配置類上的@ComponentScan、@Import、@Bean註解等),註冊其中的BeanDefinition

SpringBoot版本:2.0.9.RELEASE

Spring版本:5.0.13.RELEASE

ConfigurationClassPostProcessor如何被引入

首先看一下ConfigurationClassPostProcessor的類繼承關係

從紅框中可以看出ConfigurationClassPostProcessorBeanDefinitionRegistryPostProcessor接口的實現類,即是一個Bean定義註冊的後置處理器,會在Spring容器啟動時被調用,具體時機為

// 調用鏈
AbstractApplicationContext.refresh()
    => invokeBeanFactoryPostProcessors()
    => PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()

invokeBeanFactoryPostProcessors()會先調用所有的BeanDefinitionRegistryPostProcessor之後,再調用所有的BeanFactoryPostProcessor

ConfigurationClassPostProcessor又是如何被引入Spring的呢??

SpringBoot應用會在ApplicationContext應用上下文被創建的構造函數中new AnnotatedBeanDefinitionReader這個用於註冊基於註解的BeanDefinition的Reader,在其構造中又會調用AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)使用工具類向Spring容器中註冊一些所謂的註解配置處理器,其中就包含ConfigurationClassPostProcessor

// ConfigurationClassPostProcessor被註冊
AnnotationConfigServletWebServerApplicationContext構造
    => new AnnotatedBeanDefinitionReader(registry)
        => AnnotationConfigUtils.registerAnnotationConfigProcessors(registry)
            => 註冊ConfigurationClassPostProcessor到Spring容器

ConfigurationClassPostProcessor處理過程簡述

首先,ConfigurationClassPostProcessor後置處理器的處理入口為postProcessBeanDefinitionRegistry()方法。其主要使用了ConfigurationClassParser配置類解析器解析@Configuration配置類上的諸如@ComponentScan@Import@Bean等註解,並嘗試發現所有的配置類;還使用了ConfigurationClassBeanDefinitionReader註冊所發現的所有配置類中的所有Bean定義;結束執行的條件是所有配置類都被發現和處理,相應的bean定義註冊到容器

大致流程如下:

1、通過BeanDefinitionRegistry查找當前Spring容器中所有BeanDefinition

2、通過ConfigurationClassUtils.checkConfigurationClassCandidate() 檢查BeanDefinition是否為 “完全配置類”“簡化配置類”,並對配置類做標記,放入集合待後續處理

Spring配置類的分類可以

3、通過 ConfigurationClassParser解析器 parse解析配置類集合,嘗試通過它們找到其它配置類

4、使用 ConfigurationClassBeanDefinitionReader 註冊通過所發現的配置類中找到的所有beanDefinition

5、處理完一輪配置類后,查看BeanDefinitionRegistry中是否存在新加載的且還未被處理過的 “完全配置類”“簡化配置類”,有的話繼續上面步驟

其中第3、4步後面重點分析

ConfigurationClassParser#parse():解析構建配置類

對於SpringBoot應用來說,參与解析的種子配置文件即為SpringBoot的Application啟動類

解析構建配置類流程

通過ConfigurationClassParser解析器parse解析配置類集合,嘗試通過它們找到其它配置類

  • 循環解析所有配置類 ConfigurationClassParser#processConfigurationClass()

    • 根據@Conditional的ConfigurationPhase.PARSE_CONFIGURATION階段條件判斷是否跳過配置類

      注意:有些@Conditional是在當前這個PARSE_CONFIGURATION解析配置階段使用的,有些是在REGISTER_BEAN註冊beanDefinition階段使用的

    • 【重點】調用ConfigurationClassParser#doProcessConfigurationClass()循環解析配置類,直到不存在未處理過的父類

      • 1、處理配置類的成員內部類: 檢查其是否為“完全/簡化配置類”,是則對其繼續分析處理並將其放入分析器的屬性configurationClasses
      • 2、處理@PropertySource: 將找到的PropertySource添加到environment的PropertySource集合
      • 3、處理@ComponentScan: 掃描到的@Component類BeanDefinition就直接註冊到Spring容器;如果組件為配置類,繼續分析處理並將其放入分析器的屬性configurationClasses
      • 4、處理@Import:
        • (1)處理ImportSelector: 如果是DeferredImportSelector,如SpringBoot的自動配置導入,添加到deferredImportSelectors,延遲進行processImports();其它通過ImportSelector找到的類,繼續調用processImports(),要麼是@Configuration配置類繼續解析,要麼是普通組件導入Spring容器
        • (2)處理ImportBeanDefinitionRegistrar: 調用當前配置類的addImportBeanDefinitionRegistrar(),後面委託它註冊其它bean定義
        • (3)其它Import:調用processConfigurationClass()繼續解析,最終要麼是配置類放入configurationClasses,要麼是普通組件導入Spring容器
      • 5、處理@ImportResource: 添加到配置類的importedResources集合,後續ConfigurationClassBeanDefinitionReader#loadBeanDefinitions()時再使用這些導入的BeanDefinitionReader讀取Resource中的bean定義並註冊
      • 6、處理@Bean: 獲取所有@Bean方法,並添加到配置類的beanMethods集合
      • 7、處理配置類接口上的default methods
      • 8、檢查是否有未處理的父類: 如果配置類有父類,且其不在解析器維護的knownSuperclasses中,對其調用doProcessConfigurationClass()重複如上檢查,直到不再有父類或父類在knownSuperclasses中已存在
  • processDeferredImportSelectors():處理推遲的ImportSelector集合,其實就是延遲調用了processImports()

    SpringBoot的自動配置類就是被DeferredImportSelector推遲導入的

解析構建配置類源碼分析

ConfigurationClassParser#processConfigurationClass()

包含了處理單個配置類的大體流程,先根據ConfigurationPhase.PARSE_CONFIGURATION解析配置階段的@Conditional條件判斷當前配置類是否應該解析,之後調用ConfigurationClassParser#doProcessConfigurationClass()循環解析配置類,直到不存在未處理過的父類

/**
 * 解析單個配置類
 * 解析的最後會將當前配置類放到configurationClasses
 */
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    /**
     * 根據@Conditional條件判斷是否跳過配置類
     * 注意:當前這個PARSE_CONFIGURATION解析配置階段只會使用這個階段的@Conditional條件,有些REGISTER_BEAN註冊beanDefinition階段的條件不會在此時使用
     */
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
        return;
    }

    ConfigurationClass existingClass = this.configurationClasses.get(configClass);
    // 如果configClass在已經分析處理的配置類記錄中已存在
    if (existingClass != null) {
        //如果配置類是被@Import註冊的,return
        if (configClass.isImported()) {
            if (existingClass.isImported()) {
                existingClass.mergeImportedBy(configClass);
            }
            // Otherwise ignore new imported config class; existing non-imported class overrides it.
            return;
        }
        // 否則,清除老的記錄,在來一遍
        else {
            // Explicit bean definition found, probably replacing an import.
            // Let's remove the old one and go with the new one.
            this.configurationClasses.remove(configClass);
            this.knownSuperclasses.values().removeIf(configClass::equals);
        }
    }

    // Recursively process the configuration class and its superclass hierarchy.
    /**
     * 遞歸處理配置類及其超類層次結構
     * 從當前配置類configClass開始向上沿着類繼承結構逐層執行doProcessConfigurationClass,直到遇到的父類是由Java提供的類結束循環
     */
    SourceClass sourceClass = asSourceClass(configClass);
    /**
     * 循環處理配置類configClass直到sourceClass變為null,即父類為null
     * doProcessConfigurationClass的返回值是其參數configClass的父類
     * 如果該父類是由Java提供的類或者已經處理過,返回null
     */
    do {
        sourceClass = doProcessConfigurationClass(configClass, sourceClass);
    }
    while (sourceClass != null);

    this.configurationClasses.put(configClass, configClass);
}

ConfigurationClassParser#doProcessConfigurationClass():真正解析配置類

通過解析配置類上的註解、內部成員類和方法構建一個完整的ConfigurationClass配置類,過程中如果發現了新的配置類可以重複調用此方法

真正解析過程中會處理成員內部類@PropertySource@ComponentScan@Import@ImportSource@Bean方法等,流程如下:

  • 1、處理配置類的成員內部類: 檢查其是否為“完全/簡化配置類”,是則對其繼續分析處理並將其放入分析器的屬性configurationClasses
  • 2、處理@PropertySource: 將找到的PropertySource添加到environment的PropertySource集合
  • 3、處理@ComponentScan: 掃描到的@Component類BeanDefinition就直接註冊到Spring容器;如果組件為配置類,繼續分析處理並將其放入分析器的屬性configurationClasses
  • 4、處理@Import:
    • (1)處理ImportSelector: 如果是DeferredImportSelector,如SpringBoot的自動配置導入,添加到deferredImportSelectors,延遲進行processImports();其它通過ImportSelector找到的類,繼續調用processImports(),要麼是@Configuration配置類繼續解析,要麼是普通組件導入Spring容器
    • (2)處理ImportBeanDefinitionRegistrar: 調用當前配置類的addImportBeanDefinitionRegistrar(),後面委託它註冊其它bean定義
    • (3)其它Import: 調用processConfigurationClass()繼續解析,最終要麼是配置類放入configurationClasses,要麼是普通組件導入Spring容器
  • 5、處理@ImportResource: 添加到配置類的importedResources集合,後續ConfigurationClassBeanDefinitionReader#loadBeanDefinitions()時再使用這些導入的BeanDefinitionReader讀取Resource中的bean定義並註冊
  • 6、處理@Bean: 獲取所有@Bean方法,並添加到配置類的beanMethods集合
  • 7、處理配置類接口上的default methods
  • 8、檢查是否有未處理的父類: 如果配置類有父類,且其不在解析器維護的knownSuperclasses中,對其調用doProcessConfigurationClass()重複如上檢查,直到不再有父類或父類在knownSuperclasses中已存在
/**
 * Apply processing and build a complete {@link ConfigurationClass} by reading the
 * annotations, members and methods from the source class. This method can be called
 * multiple times as relevant sources are discovered.
 * @param configClass the configuration class being build
 * @param sourceClass a source class
 * @return the superclass, or {@code null} if none found or previously processed
 */
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
        throws IOException {

    // Recursively process any member (nested) classes first
    /**
     * 1、處理配置類的成員類(配置類內嵌套定義的類)
     * 內部嵌套類也可能是配置類,遍歷這些成員類,檢查是否為"完全/簡化配置類"
     * 有的話,調用processConfigurationClass()處理它們,最終將配置類放入configurationClasses集合
     */
    processMemberClasses(configClass, sourceClass);

    // Process any @PropertySource annotations
    /**
     * 2、處理 @PropertySource
     * 將找到的PropertySource添加到environment的PropertySource集合
     */
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
        if (this.environment instanceof ConfigurableEnvironment) {
            processPropertySource(propertySource);
        }
        else {
            logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }

    // Process any @ComponentScan annotations
    /**
     * 3、處理 @ComponentScan
     * 處理用戶手工添加的@ComponentScan,SpringBoot創建ApplicationContext時的ClassPathBeanDefinitionScanner是為了掃描啟動類下的包
     * 為的是找到滿足條件的@ComponentScan,即@Component相關的組件,先掃描一下,掃描到的就註冊為BeanDefinition
     * 看其中是否還有配置類,有的話parse()繼續分析處理,配置類添加到configurationClasses集合
     */
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    // 如果當前配置類上有@ComponentScan,且使用REGISTER_BEAN註冊beanDefinition的條件判斷也不跳過的話
    if (!componentScans.isEmpty() &&
            !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
        for (AnnotationAttributes componentScan : componentScans) {
            // The config class is annotated with @ComponentScan -> perform the scan immediately
            // 立即掃描,掃描到的就註冊為BeanDefinition,並獲得掃描到的所有beanDefinition
            // 在處理SpringBoot啟動類上的@ComponentScan時,雖然指指定了excludeFilters,但會根據啟動類所在包推測basePackage,就會掃描到SpringBoot啟動類包以下的Bean並註冊
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

            // Check the set of scanned definitions for any further config classes and parse recursively if needed
            // 檢查掃描到的beanDefinition中是否有配置類,有的話parse()繼續分析處理,,配置類添加到configurationClasses集合
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) {
                    bdCand = holder.getBeanDefinition();
                }
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }

    // Process any @Import annotations
    /**
     * 4、處理 @Import
     * (1)處理ImportSelector
     * 如果是DeferredImportSelector,如SpringBoot的自動配置導入,添加到deferredImportSelectors,延遲進行processImports()
     * 其它通過ImportSelector找到的類,繼續調用processImports(),要麼是@Configuration配置類繼續解析,要麼是普通組件導入Spring容器
     * (2)處理ImportBeanDefinitionRegistrar
     * 調用當前配置類的addImportBeanDefinitionRegistrar(),後面委託它註冊其它bean定義
     * (3)其它
     * 調用processConfigurationClass()繼續解析,最終要麼是配置類放入configurationClasses,要麼是普通組件導入Spring容器
     */
    processImports(configClass, sourceClass, getImports(sourceClass), true);

    // Process any @ImportResource annotations
    /**
     * 5、處理 @ImportResource
     * 添加到配置類的importedResources集合,後續loadBeanDefinitions()加載bean定義時再讓這些導入BeanDefinitionReader自行讀取bean定義
     */
    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    if (importResource != null) {
        String[] resources = importResource.getStringArray("locations");
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        for (String resource : resources) {
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }

    // Process individual @Bean methods
    /**
     * 6、處理個別@Bean方法
     * 獲取所有@Bean方法,並添加到配置類的beanMethods集合
     */
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // Process default methods on interfaces
    /**
     * 7、處理配置類接口上的default methods
     */
    processInterfaces(configClass, sourceClass);

    // Process superclass, if any
    /**
     * 8、檢查父類是否需要處理,如果父類需要處理返回父類,否則返回null
     * 如果存在父類,且不在knownSuperclasses已經分析過的父類列表裡,返回並繼續分析
     */
    if (sourceClass.getMetadata().hasSuperClass()) {
        String superclass = sourceClass.getMetadata().getSuperClassName();
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            this.knownSuperclasses.put(superclass, configClass);
            // Superclass found, return its annotation metadata and recurse
            return sourceClass.getSuperClass();
        }
    }

    // No superclass -> processing is complete
    return null;
}

ConfigurationClassBeanDefinitionReader#loadBeanDefinitions():讀取配置類,基於配置信息註冊BeanDefinition

讀取配置類,基於配置信息註冊BeanDefinition流程

在上面解析配置類的過程中,除了構建了一個完整的ConfigurationClass配置類,其實已經向BeanDefinitionRegistry中添加了一些beanDefinition了,比如在處理@ComponentScan時,掃描到的@Component相關組件就已經註冊了

ConfigurationClassBeanDefinitionReader會繼續讀取已經構建好的ConfigurationClass配置類中的成員變量,從而註冊beanDefinition

構建好的ConfigurationClass配置類中在本階段可用的成員變量包括:

  1. Set<BeanMethod> beanMethods: @Bean的方法
  2. Map<String, Class<? extends BeanDefinitionReader>> importedResources:配置類上@ImportResource註解的類存入此集合,會使用BeanDefinitionReader讀取Resource中的BeanDefinition並註冊
  3. Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars:ImportBeanDefinitionRegistrar集合

通過構建好的配置類的配置信息,使用ConfigurationClassBeanDefinitionReader註冊所有能夠讀取到的beanDefinition:

  • 根據ConfigurationPhase.REGISTER_BEAN階段條件判斷配置類是否需要跳過

    循環判斷配置類以及導入配置類的類,使用ConfigurationPhase.REGISTER_BEAN階段條件判斷是否需要跳過只要配置類或導入配置類的類需要跳過即返回跳過​

  • 如果configClass.isImported(),將配置類自身註冊為beanDefinition

  • 註冊配置類所有@Bean方法為beanDefinition

  • 註冊由@ImportedResources來的beanDefinition,即通過其它類型Resource的BeanDefinitionReader讀取BeanDefinition並註冊,如xml格式的配置源 XmlBeanDefinitionReader

  • 註冊由ImportBeanDefinitionRegistrars來的beanDefinition

讀取配置類,基於配置信息註冊BeanDefinition源碼分析

/**
 * Read a particular {@link ConfigurationClass}, registering bean definitions
 * for the class itself and all of its {@link Bean} methods.
 * 讀取特定配置類,根據配置信息註冊bean definitions
 */
private void loadBeanDefinitionsForConfigurationClass(
        ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

    /**
     * 根據ConfigurationPhase.REGISTER_BEAN階段條件判斷配置類是否需要跳過
     * 循環判斷配置類以及導入配置類的類,使用ConfigurationPhase.REGISTER_BEAN階段條件判斷是否需要跳過
     * 只要配置類或導入配置類的類需要跳過即返回跳過
     */
    if (trackedConditionEvaluator.shouldSkip(configClass)) {
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
    }

    // 1、如果當前配置類是通過內部類導入 或 @Import導入,將配置類自身註冊為beanDefinition
    if (configClass.isImported()) {
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }

    // 2、註冊配置類所有@Bean方法為beanDefinition
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }

    // 3、註冊由@ImportedResources來的beanDefinition
    // 即通過其它類型Resource的BeanDefinitionReader讀取BeanDefinition並註冊
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());

    // 4、註冊由ImportBeanDefinitionRegistrars來的beanDefinition
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

思維導圖

請放大觀看

參考

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

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

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

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

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

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

[ch02-02] 非線性反向傳播

系列博客,原文在筆者所維護的github上:,
點擊star加星不要吝嗇,星越多筆者越努力。

2.2 非線性反向傳播

2.2.1 提出問題

在上面的線性例子中,我們可以發現,誤差一次性地傳遞給了初始值w和b,即,只經過一步,直接修改w和b的值,就能做到誤差校正。因為從它的計算圖看,無論中間計算過程有多麼複雜,它都是線性的,所以可以一次傳到底。缺點是這種線性的組合最多只能解決線性問題,不能解決更複雜的問題。這個我們在神經網絡基本原理中已經闡述過了,需要有激活函數連接兩個線性單元。

下面我們看一個非線性的例子,如圖2-8所示。

圖2-8 非線性的反向傳播

其中\(1<x<=10,0<y<2.15\)。假設有5個人分別代表x、a、b、c、y:

正向過程

  1. 第1個人,輸入層,隨機輸入第一個x值,x取值範圍(1,10],假設第一個數是2
  2. 第2個人,第一層網絡計算,接收第1個人傳入x的值,計算:\(a=x^2\)
  3. 第3個人,第二層網絡計算,接收第2個人傳入a的值,計算b:\(b=\ln (a)\)
  4. 第4個人,第三層網絡計算,接收第3個人傳入b的值,計算c:\(c=\sqrt{b}\)
  5. 第5個人,輸出層,接收第4個人傳入c的值

反向過程

  1. 第5個人,計算y與c的差值:\(\Delta c = c – y\),傳回給第4個人
  2. 第4個人,接收第5個人傳回\(\Delta c,計算\Delta b:\Delta b = \Delta c \cdot 2\sqrt{b}\)
  3. 第3個人,接收第4個人傳回\(\Delta b,計算\Delta a:\Delta a = \Delta b \cdot a\)
  4. 第2個人,接收第3個人傳回\(\Delta a,計算\Delta x:\Delta x = \Delta a / 2x\)
  5. 第1個人,接收第2個人傳回\(\Delta x,更新x:x = x – \Delta x\),回到第1步

提出問題:假設我們想最後得到c=2.13的值,x應該是多少?(誤差小於0.001即可)

2.2.2 數學解析解

\[c=\sqrt{b}=\sqrt{\ln(a)}=\sqrt{\ln(x^2)}=2.13\]
\[x = 9.6653\]

2.2.3 梯度迭代解

\[ \frac{da}{dx}=\frac{d(x^2)}{dx}=2x=\frac{\Delta a}{\Delta x} \tag{1} \]
\[ \frac{db}{da} =\frac{d(\ln{a})}{da} =\frac{1}{a} = \frac{\Delta b}{\Delta a} \tag{2} \]
\[ \frac{dc}{db}=\frac{d(\sqrt{b})}{db}=\frac{1}{2\sqrt{b}}=\frac{\Delta c}{\Delta b} \tag{3} \]
因此得到如下一組公式,可以把最後一層\(\Delta c\)的誤差一直反向傳播給最前面的\(\Delta x\),從而更新x值:
\[ \Delta c = c – y \tag{4} \]
\[ \Delta b = \Delta c \cdot 2\sqrt{b} \tag{根據式3} \]
\[ \Delta a = \Delta b \cdot a \tag{根據式2} \]
\[ \Delta x = \Delta a / 2x \tag{根據式1} \]

我們給定初始值\(x=2,\Delta x=0\),依次計算結果如表2-2。

表2-2 正向與反向的迭代計算

方向 公式 迭代1 迭代2 迭代3 迭代4 迭代5
正向 \(x=x-\Delta x\) 2 4.243 7.344 9.295 9.665
正向 \(a=x^2\) 4 18.005 53.934 86.404 93.233
正向 \(b=\ln(a)\) 1.386 2.891 3.988 4.459 4.535
正向 \(c=\sqrt{b}\) 1.177 1.700 1.997 2.112 2.129
標籤值y 2.13 2.13 2.13 2.13 2.13
反向 \(\Delta c = c – y\) -0.953 -0.430 -0.133 -0.018
反向 \(\Delta b = \Delta c \cdot 2\sqrt{b}\) -2.243 -1.462 -0.531 -0.078
反向 \(\Delta a = \Delta b \cdot a\) -8.973 -26.317 -28.662 -6.698
反向 \(\Delta x = \Delta a / 2x\) -2.243 -3.101 -1.951 -0.360

表2-2,先看“迭代-1”列,從上到下是一個完整的正向+反向的過程,最後一行是-2.243,回到“迭代-2”列的第一行,2-(-2.243)=4.243,然後繼續向下。到第5輪時,正向計算得到的c=2.129,非常接近2.13了,迭代結束。

運行示例代碼的話,可以得到如下結果:

how to play: 1) input x, 2) calculate c, 3) input target number but not faraway from c
input x as initial number(1.2,10), you can try 1.3:
2
c=1.177410
input y as target number(0.5,2), you can try 1.8:
2.13
forward...
x=2.000000,a=4.000000,b=1.386294,c=1.177410
backward...
delta_c=-0.952590, delta_b=-2.243178, delta_a=-8.972712, delta_x=-2.243178
......
forward...
x=9.655706,a=93.232666,b=4.535098,c=2.129577
backward...
done!

為節省篇幅只列出了第一步和最後一步(第5步)的結果,第一步時c=1.177410,最後一步時c=2.129577,停止迭代。

代碼位置

ch02, Level2

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

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

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

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

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

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

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

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產品,價格不怕你比較

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

結合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產品,價格不怕你比較

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

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

一、前言

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收購,鏡頭 收購有可能以全新價回收嗎?

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

夏季電動機車市場正熱,Gogoro 掛牌數突破 20 萬

2019 年台灣電動機車市場百家爭鳴,也帶動市場熱度,越來越多消費者願意選擇電動機車。電動機車大廠 Gogoro 宣布旗下總掛牌數正式達 20 萬輛,寫下新的里程碑。

根據工研院產科國際所推估,台灣電動機車年銷量可以突破 15 萬輛,成長速度超過全球平均。2019 年夏季包括 Gogoro、光陽(Kymco)、山葉(YAMAHA)、中華汽車 eMOVING 和宏佳騰等品牌都推出新的電動機車,代表車廠也清楚了解市場對電動機車的需求。

受惠於暑假旺季提前發酵,加上受到新車效應與即將到來的開學季影響,Gogoro 從 2019 年 5 月以來已連續 4 個月單月掛牌數破萬,這也讓 Gogoro 總掛牌數正式達到 20 萬輛。在全新發表的 Gogoro S2 ABS 車款助攻之下,讓 Gogoro 在 8 月單月掛牌市占率達 16.35%,即使 8 月整體機車市場下滑 20%,仍然保持穩定成長。

Gogoro 資深行銷總監陳彥揚表示:「今年夏天對 Gogoro 來說深具意義,連續 4 個月掛牌破萬,帶動 Gogoro 總車主數超越 20 萬大關,除了非常感謝車主的熱情支持,業界的投入及政府政策的推動,也是電動機車產業發展向前邁進的關鍵要素,期盼能快速跟上全球化電動車趨勢的腳步。」

每年最具代表性的車主活動「快閃台北橋」將於 9 月 29 日舉辦,2018 年活動以 1,303 台機車創下「世界最大規模電動機車遊行」的金氏世界紀錄,Gogoro 也將擴大邀請所有電動機車車主,今年活動時集結台北橋。

8 月推出的「暢快騎 0 元起」購車方案廣受歡迎,Gogoro 為了慶祝車主數量突破 20 萬大關,宣布活動時間將延長至 9 月 30 日,購買 Gogoro 全車系即贈送 6 個月電池服務資費 299 元,無論選擇什麼方案每個月都可抵扣電池資費 299 元。即日起至 9 月 30 日購買 Gogoro 全車系並符合學生資格的車主,即可享有 12 期 0 利率分期優惠,同時再送「1 年期車碰車險」。

(合作媒體:。首圖來源:Gogoro)

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

【其他文章推薦】

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

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

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

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

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

SpringBoot源碼學習系列之SpringMVC自動配置

目錄

源碼學習系列之WebMvc自動配置原理筆記

@

web的自動配置在SpringBoot項目中是一個很重要的方面,實現代碼在spring-boot-autoconfigure工程里:

按照官方文檔的說法,SpringBoot官方的說法,Springboot的SpringMVC自動配置,主要提供了如下自動配置:

WebMvcAutoConfiguration.java這個類很關鍵,這個就是SpringBoot Springmvc自動配置的一個很關鍵的配置類

@Configuration(proxyBeanMethods = false)//指定WebMvcAutoConfiguration不代理方法
@ConditionalOnWebApplication(type = Type.SERVLET)//在web環境(selvlet)才會起效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })//系統有有Servlet,DispatcherServlet(Spring核心的分發器),WebMvcConfigurer的情況,這個自動配置類才起效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)//系統沒有WebMvcConfigurationSupport這個類的情況,自動配置起效
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
....
}

翻下源碼,可以看到WebMvcAutoConfiguration自動配置類里還有一個WebMvcConfigurer類型的配置類,2.2.1版本是implements WebMvcConfigurer接口,1.+版本是extends WebMvcConfigurerAdapter

@Configuration(proxyBeanMethods = false)//定義為配置類
    @Import(EnableWebMvcConfiguration.class)//spring底層註解,將EnableWebMvcConfiguration加到容器
    @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })//使WebMvcProperties、ResourceProperties配置類生效
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
    ....
}

1、ContentNegotiatingViewResolver

如圖,是視圖解析器的自動配置,這個類起效的情況是系統沒有ContentNegotiatingViewResolver類的情況,就調用改方法自動創建ContentNegotiatingViewResolver類

關鍵的是ContentNegotiatingViewResolver類,翻下ContentNegotiatingViewResolver類,找到如下重要的初始化方法

@Override
    protected void initServletContext(ServletContext servletContext) {
    //調用Spring的BeanFactoryUtils掃描容器里的所有視圖解析器ViewResolver類
        Collection<ViewResolver> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList<>(matchingBeans.size());
            //遍歷候選的viewResolvers,封裝到this.viewResolvers列表
            for (ViewResolver viewResolver : matchingBeans) {
                if (this != viewResolver) {
                    this.viewResolvers.add(viewResolver);
                }
            }
        }
        else {
            for (int i = 0; i < this.viewResolvers.size(); i++) {
                ViewResolver vr = this.viewResolvers.get(i);
                if (matchingBeans.contains(vr)) {
                    continue;
                }
                String name = vr.getClass().getName() + i;
                obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
            }

        }
        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }

所以ContentNegotiatingViewResolver類的作用就是組合所有的視圖解析器,自動配置了ViewResolver(視圖解析器作用,根據方法返回值得到視圖對象view)

往下翻代碼,可以看到resolveViewName方法,裏面代碼是從this.viewResolvers獲取候選的視圖解析器,遍歷容器里所有視圖,然後通過如圖所標記的獲取候選視圖的方法,獲取候選的視圖列表,再通過getBestView獲取最合適的視圖

遍歷所有的視圖解析器對象,從視圖解析器里獲取候選的視圖,封裝成list保存

ok,跟了源碼就是只要將視圖解析器丟到Spring容器里,就可以加載到

寫個簡單的視圖解析類

DispatcherServlet是Spring核心分發器,找到doDispatch方法,debug,可以看到加的視圖解析器加載到了

2、靜態資源

也就是官方說的,如下圖所示:

翻譯過來就是支持靜態資源包括webjars的自動配置,webjars,就是以maven等等方式打成jar包的靜態資源,可以去看看文檔:

使用的話,直接去webjars官網負責對應的配置,加到項目里就可以

路徑都是在META-INF/webjars/**

WebMvcAutoConfiguration.addResourceHandlers,這個是比較重要的資源配置方法

@Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
            //CacheControl是Spring框架提供的http緩存
            CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
            //讀取到webjars資源,將classpath:/META-INF/resources/webjars/的webjars資源都掃描出來
            if (!registry.hasMappingForPattern("/webjars/**")) {
                customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                        .addResourceLocations("classpath:/META-INF/resources/webjars/")
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
            String staticPathPattern = this.mvcProperties.getStaticPathPattern();
            if (!registry.hasMappingForPattern(staticPathPattern)) {
                customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
        }

ok,通過源碼可以知道,Springboot支持webjars和其它等等靜態資源,其它的靜態資源要放在如下目錄里,Springboot就能自動加載到

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/
  • classpath:/

3、自動註冊 Converter, GenericConverter, and Formatter beans.

翻譯過來就是自動註冊了 Converter, GenericConverter, and Formatter beans.

  • Converter:轉換器 ,作用就是能自動進行類型轉換
    eg: public String hello(User user),這是一個方法,然後前端視圖傳來的參數通過轉換器能夠根據屬性進行映射,然後進行屬性類型轉換
  • Formatter :格式化器,eg:比如對前端傳來的日期2019/11/25,進行格式化處理

源碼在這裏,WebMvcAutoConfiguration.addFormatters方法是添加格式化器的方法

同理,也是從Spring容器里將這幾種類拿過來

當然,還有其它的,比如WebMvcAutoConfiguration.localeResolver方法是實現i18n國際化語言支持的自動配置

@Bean
        @ConditionalOnMissingBean//沒有自定義localeResolver的情況
        @ConditionalOnProperty(prefix = "spring.mvc", name = "locale")//application.properties有配置了spring.mvc.locale
        public LocaleResolver localeResolver() {
            if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
                return new FixedLocaleResolver(this.mvcProperties.getLocale());
            }
            //默認使用AcceptHeaderLocaleResolver 
            AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
            localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
            return localeResolver;
        }

具體的源碼參考我之前博客:,博客裏面有涉及源碼的

4、支持HttpMessageConverters

HttpMessageConverters :消息轉換器,Springmvc中用來轉換http請求和響應的

源碼里是通過configureMessageConverters方法實現,很顯然也是從容器里獲取的

官方文檔里也進行了比較詳細描述,Springboot已經為我們自動配置了json的、xml的自動轉換器,當然你也可以自己添加

5、支持MessageCodesResolver

MessageCodesResolver:是消息解析器,WebMvcAutoConfiguration.getMessageCodesResolver是實現Exception異常信息格式的

WebMvcProperties配置文件定義的一個異常枚舉值

格式為如圖所示,定了了錯誤代碼是生成規則:

6、首頁支持

Springboot默認的首頁是index.html,也就是你在classpath路徑丟個index.html文件,就被Springboot默認為首頁,或者說歡迎頁

如圖示代碼,就是遍歷靜態資源文件,然後獲取index.html作為歡迎頁面

7、網站logo設置

Springboot1.+版本,是有默認的logo圖標的,2.2.1版本,經過全局搜索,沒有發現給自定義的圖標,使用的話,是直接丟在classpath路徑,文件命名為favicon.ico,不過在2.2.1代碼並沒有找到相應的配置代碼,1.+版本是有的,不過文檔還是有描述了

8、ConfigurableWebBindingInitializer 初始綁定器

跟下源碼,也是從Spring容器里獲取的,然後注意到,如果沒有這個ConfigurableWebBindingInitializer ,代碼就會調用基類的getConfigurableWebBindingInitializer

源碼,這裏也是創建一個getConfigurableWebBindingInitializer

ConfigurableWebBindingInitializer 是Springboot為系統自動配置的,當然我們也可以自己定義一個ConfigurableWebBindingInitializer ,然後加載到容器里即可

初始化綁定的方法,ok,本博客簡單跟一下源碼

注意:
ok,Springboot官方文檔里還有這樣的描述,如圖所示

意思是,在使用webmvcConfigurer配置的時候,不要使用@EnableWebMvc註解,為什麼不要使用呢?因為使用了@EnableWebMvc,就是實現全面接管SpringMVC自動配置,也就是說其它的自動配置都會失效,全部自己配置

原理是為什麼?可以簡單跟一下源碼,如圖,SpringMVC自動配置類,有這個很關鍵的註解,這個註解的意思是@WebMvcConfigurationSupport註解不在系統時候自動配置才起效

然後為什麼加了@EnableWebMvc自動配置就可以被全面接管?點一下@EnableWebMvc源碼

很顯然,DelegatingWebMvcConfiguration類extends WebMvcConfigurationSupport類,所以這也就是為什麼@EnableWebMvc註解能實現全面接管自動配置的原理

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

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

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

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

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

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

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

js 關於apply和call的理解使用

  關於call和apply,以前也思考良久,很多時候都以為記住了,但是,我太難了。今天我特地寫下筆記,希望可以完全掌握這個東西,也希望可以幫助到任何想對學習這個東西的同學。

一.apply函數定義與理解,先從apply函數出發

  在MDN上,apply的定義是:

    “apply()方法調用一個具有給定this值的函,以及作為一個數組(或)提供的參數。”

  我的理解是:apply的前面有個含有this的對象,設為A,apply()的參數里,也含有一個含有this的對象設為B。則A.apply(B),表示A代碼執行調用了B,B代碼照常執行,執行后的結果作為apply的參數,然後apply把這個結果所指代表示的this替換掉A本身的this,接着執行A代碼。

  比如:

 1     var aa = {
 2         _name:111,
 3         _age:222,
 4         _f:function(){
 5             console.log(this)
 6             console.log(this._name)
 7         }
 8     }
 9     var cc = {
10         _name:0,
11         _age:0,
12         _f:function(){
13             console.log(this)
14             console.log(this._name)
15         }
16     }
17     cc._f.apply(aa)//此時aa表示的this就是aa本身
18     cc._f.apply(aa._f)//此時aa._f表示的this就是aa._f本身
19     
20     /**
21      * 此時aa._f()表示的this,就是執行后的結果本身。aa._f()執行后,沒有返回值,所以應該是undefined,而undefined作為call和apply的參數時,call和apply前面的方法 cc._f 的this會替換成全局對象window。
22      * 參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 的參數說明
23      */
24     cc._f.apply(aa._f())

執行結果:

  1.參數為aa

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa的時候算是初始化,裏面的 aa._f 方法沒有執行。

  2.參數為aa.f

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa.f 的時候應該也算是初始化,或者是整個函數當參數傳但是不執行這個參數,即 aa._f 方法沒有執行。

  3.參數為aa.f()

   

  這四行的打印,前面兩行來自 aa._f() 方法執行打印的;後面兩行是來自cc._f()方法打印的。

  后兩行解析:aa._f()執行后,沒有返回值,所以是undefined,在apply執行解析后,cc._f()的this變成的window,所以打印了window。window裏面沒有_name這個屬性,所以undefined。

 二.apply與call的區分

  1.apply()

    A.apply(B, [1,2,3]) 後面的參數是arguments對象或類似數組的對象,它會被自動解析為A的參數;

    對於A.apply(B) / A.call(B) , 簡單講,B先執行,執行后根據結果去執行A。這個時候,用A去執行B的內容代碼,然後再執行自己的代碼。

  比如:

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.apply(f1,[1,2])//1 2 undefined

   先執行f1,f1執行后(f1是f1,不是f1()執行方法,所以console.log(a+b)這行代碼並沒有執行,相當於,初始化了代碼f1),由於沒有返回值,所以結果是undefined,f2執行的時候this指向window;參數中的 ” [1,2] “,解析后變成 f2 的參數 “ 1,2,undefined ”,執行f2方法后,打印出1,2,undefined三個值

  2.call()

    A.call(B, 1,2,3) 後面的參數都是獨立的參數對象,它們會被自動解析為A的參數;

  比如: 

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.call(f1,[1,2])//[1,2] undefined undefined
    f2.call(f1,1,2)//1 2 undefined

   參數中的 ” [1,2] “,因為傳入了一個數組,相當於只傳入了第一個參數,b和c參數沒有傳。解析后變成 f2 的參數 “ [1,2],undefined ,undefined ”,執行f2方法后,打印出 [1,2],undefined ,undefined 三個值。

三.apply與call帶來的便利

  1. push();

  push參數是類似(a,b,c,d,e)如此傳輸的,如果在一個數組的基礎上進行傳輸另一個數組的內容,可以如下:

    //apply用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.apply(arr,arr1)
    console.log(arr)//[1, 2, 3, 11, 21, 31]
    
    //call用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.call(arr,arr1[0],arr1[1],arr1[2])
    console.log(arr)//[1, 2, 3, 11, 21, 31]

   2. 數組利用Math求最大和最小值

  apply和call的第一個參數,如果是null或者undefined,則apply或call前面的函數會把this指向window

    //apply的用法
    var _maxNum = Math.max.apply(null,[1,3,2,4,5])
    console.log(_maxNum)//5
    var _minNum = Math.min.apply(null,[1,3,2,4,5])
    console.log(_minNum)//1
    
    //call的用法
    var _maxNum = Math.max.call(null,1,3,2,4,5)
    console.log(_maxNum)//5
    var _minNum = Math.min.call(null,1,3,2,4,5)
    console.log(_minNum)//1

 四.總結

  簡而言之,apply和call函數的第一個參數都是用來替換this指向的對象;apply的第二個參數使用arguments或者類似數組的參數進行傳參,call的第二個或以上的參數,使用獨立單位,一個一個進行傳參;執行順序是apply或call的第一個參數先執行得到結果,然後執行apply或call前面的函數,執行的時候用已經執行的結果所指代的this去執行。apply和call的使用除了參數上的使用方式不同外,功能是一模一樣的。

  以上內容純屬個人理解,有誤勿噴請指出!謝謝!

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

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

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

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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