SpringSecurity系列之自定義登錄驗證成功與失敗的結果處理

一、需要自定義登錄結果的場景

在我之前的文章中,做過登錄驗證流程的源碼解析。其中比較重要的就是

  • 當我們登錄成功的時候,是由AuthenticationSuccessHandler進行登錄結果處理,默認跳轉到defaultSuccessUrl配置的路徑對應的資源頁面(一般是首頁index.html)。
  • 當我們登錄失敗的時候,是由AuthenticationfailureHandler進行登錄結果處理,默認跳轉到failureUrl配置的路徑對應的資源頁面(一般是登錄頁login.html)。

但是在web應用開發過程中需求是千變萬化的,有時需要我們針對登錄結果做個性化處理,比如:

  • 我們希望不同的人登陸之後,看到不同的首頁
  • 我們應用是前後端分離的,驗證響應結果是JSON格式數據,而不是頁面跳轉

以上的這些情況,使用Spring Security作為安全框架的時候,都需要我們使用本節學到的知識進行自定義的登錄驗證結果處理。

二、自定義登陸成功的結果處理

為了滿足上面的需求,我們該如何去做呢?下面一小節我們來說明一下。AuthenticationSuccessHandler接口是Security提供的認證成功處理器接口,我們只需要去實現它即可。但是通常來說,我們不會直接去實現AuthenticationSuccessHandler接口,而是繼承SavedRequestAwareAuthenticationSuccessHandler 類,這個類會記住用戶上一次請求的資源路徑,比如:用戶請求books.html,沒有登陸所以被攔截到了登錄頁,當你萬成登陸之後會自動跳轉到books.html,而不是主頁面。

@Component
public class MyAuthenticationSuccessHandler 
                        extends SavedRequestAwareAuthenticationSuccessHandler {

    //在application配置文件中配置登陸的類型是JSON數據響應還是做頁面響應
    @Value("${spring.security.logintype}")
    private String loginType;

    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        Authentication authentication) 
                                        throws ServletException, IOException {

        if (loginType.equalsIgnoreCase("JSON")) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success()));
        } else {
            // 會幫我們跳轉到上一次請求的頁面上
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}
  • 在上面的自定義登陸成功處理中,既適應JSON前後端分離的應用登錄結果處理,也適用於模板頁面跳轉應用的登錄結果處理
  • ObjectMapper 是Spring Boot默認集成的JSON數據處理類庫Jackson中的類。
  • AjaxResponse是一個自定義的通用的JSON數據接口響應類。

三、自定義登錄失敗的結果處理

這裏我們同樣沒有直接實現AuthenticationFailureHandler接口,而是繼承SimpleUrlAuthenticationFailureHandler 類。該類中默認實現了登錄驗證失敗的跳轉邏輯,即登陸失敗之後回到登錄頁面。我們可以利用這一點簡化我們的代碼。

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    //在application配置文件中配置登陸的類型是JSON數據響應還是做頁面響應
    @Value("${spring.security.logintype}")
    private String loginType;

    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response, 
                                        AuthenticationException exception) 
                                        throws IOException, ServletException {

        if (loginType.equalsIgnoreCase("JSON")) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(
                    objectMapper.writeValueAsString(
                            AjaxResponse.error(
                                    new CustomException(
                                        CustomExceptionType.USER_INPUT_ERROR,
                                        "用戶名或密碼存在錯誤,請檢查后再次登錄"))));
        } else {
            response.setContentType("text/html;charset=UTF-8");
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}
  • 在上面的自定義登陸失敗處理中,既適應JSON前後端分離的應用登錄失敗結果處理,也適用於模板頁面跳轉應用的登錄失敗結果處理
  • 登陸失敗之後,將默認跳轉到默認的failureUrl,即登錄界面。

四、配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.csrf().disable() //禁用跨站csrf攻擊防禦,後面的章節會專門講解
           .formLogin()
           .successHandler(myAuthenticationSuccessHandler)
           .failureHandler(myAuthenticationFailureHandler)
           .defaultSuccessUrl("/index")//登錄認證成功后默認轉跳的路徑
           .failureUrl("/login.html") //登錄認證是被跳轉頁面
}
  • 將自定義的AuthenticationSuccessHandler和AuthenticationFailureHandler注入到Spring Security配置類中
  • 使用fromlogin模式,配置successHandler和failureHandler。
  • 並且配置defaultSuccessUrl和failureUrl

    期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

憂瀕危山地大猩猩染疫 剛果關閉園區

摘錄自2020年3月24日公視報導

部份科學家認為許多病毒是以「野生動物」做為媒介,傳到人類身上,但其實人類也可能把疾病傳染給動物。非洲剛果民主共和國的「維龍加國家公園」,就為了保護園裡瀕臨絕種的山地大猩猩,決定關閉到6月1號,免得入山的遊客帶來毀滅性的災難。

園區擔心武漢肺炎會經由與人接觸傳染給大猩猩,已經下令這處非洲第一個成立的國家公園,即日起關閉到6月1號。保育人員之所以這麼緊張,是因為一個尋常的感冒,就可能使山地大猩猩死亡。疫情一旦在這個群體擴散,很可能讓全球僅剩1000隻左右的山地大猩猩瀕臨滅絕。

為了保護山地大猩猩,這處園區原本規定,遊客與猩猩必須保持七公尺以上的距離,但這樣的規範執行起來有實際困難,有時猩猩也會主動靠近人類。因此維龍加國家公園才會忍痛做出閉園的決定,不過也將因此失去可觀的觀光收入,鄰近的盧安達和烏干達國家公園則決定先以加強防疫因應。

生態保育
國際新聞
剛果
大猩猩保育
國際疫情

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

神奇!澳洲高速公路驚見三眼蛇 3顆眼球皆具正常視力

摘錄自2020年3月24日自由時報報導

澳洲野生動物聞名世界,經常發現出乎人意料之外的特別生物;日前,澳洲北部高速公路上驚現一條三隻眼睛的蛇,北領地公園(The Northern Territory Parks)野生動物局將這隻特別的蛇拍照分享在社群網路上,引起廣泛討論。

根據《BBC》報導,相關單位為這條地毯莫瑞蟒(Carpet python)取了暱稱,叫做「蟒蛇蒙蒂(Monty Python)」;該品種蟒蛇為澳洲常見蛇類,於3月初被發現。專家分析後認為,蒙蒂頭部正中間多出來的一隻眼應該是自然突變。

公園野生動物局對蒙蒂展開研究,經X光掃描後,確認蒙蒂並非雙頭蛇,這特別的小生物反而更像擁有著「長有3個眼窩的頭骨,嵌著3顆可正常運作的眼球」。為何蒙蒂會有第三隻眼?佛萊推測,那可能是被牠吸收的雙胞胎手足所剩下的部分。這隻特別的蟒蛇在3月初被發現後,似乎因為畸形而導致進食困難,幾週後便過世了。

生態保育
國際新聞
澳洲
蟒蛇

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

法國因疫情全國封城 巴黎空污程度改善近三成

摘錄自2020年3月25日自由時報報導

為打擊武漢肺炎疫情,法國實施全國封城。根據大巴黎地區空氣品質監測局報告,這讓巴黎空污程度下降20%至30%,氮氧化物排放量更降低超過六成。

自3月17日生效的全國封城令讓無數汽車及貨車停駛,並大幅降低巴黎兩座機場的航班量。大巴黎地區空氣品質監測局(Airparif)報告指出,全國封城政策開始兩天後,巴黎大都會區空氣品質改善「20%至30%」,氮氧化物排放量下降超過六成。

大巴黎空品局表示,空污減少後,溫室氣體二氧化碳也隨之下降,凸顯了這兩個問題的關聯,這也顯示改善空氣品質會為氣候帶來好處。然而,空品局也表示,封城沒讓細懸浮微粒PM2.5跟懸浮微粒PM10顯著減少。

空氣污染
公害污染
國際新聞
巴黎
武漢肺炎
封城

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

吉利總投資80億元新能源整車項目落戶杭州 首期年產10萬輛

4月20日,總投資 80億元吉利新能源整車項目成功簽約落戶杭州大江東。

吉利新能源整車專案計畫投資80億元,用地600畝,首期年產10萬輛AMA平臺新能源汽車整車,達產後年銷售收入不少於150億元。專案包括了衝壓、焊裝、塗裝、總裝四大工藝以及與之配套的試車跑道、停車場、動力站房、管理辦公中心、生活配套區等協助工具設施。據悉,AMA平臺是吉利集團歷時三年專門為為新能源汽車打造的全新平臺,確保了整車項目的工藝技術水準、設備裝備水準達到國內領先、國際一流的先進水準。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

中國自主研製出2.0版新能源汽車動力系統

中車株洲電機公司自主研製的新能源汽車第二代動力系統——輪邊永磁驅動系統22日成功下線。這實現了中國國產新能源汽車動力系統由第一代動力集中型向第二代動力分散型的革命性轉變,打破了對國外進口的依賴。

據中車株洲電機技術人員介紹,驅動系統改變了新能源汽車過去所採用的中央電機集中驅動方式,直接由2台安裝在電動汽車車橋兩側的永磁輪邊電機進行驅動。其中,永磁電機額定功率、轉速、扭矩等各項技術指標優異,最高效率可達96%,電機控制器輸出頻率高達600Hz,提升了電動汽車運行的平穩性與舒適性。

其扁平式的電機結構非常先進,裝車後與採用集中驅動的電動汽車相比,地板高度可以降低180-320mm,達到低地板水準,能夠取消上下車門處的臺階,車內後排坐椅也無需階梯狀佈置,可有效方便乘客特別是老人及小孩上下車,並縮短電動汽車停站時間,緩解公交壓力。

目前,中國新能源汽車第二代動力分散型驅動系統多依賴進口,且停留在三相非同步電機的水準。而中車株洲電機自主研製的第二代輪邊驅動系統成功將高鐵永磁技術運用於新能源汽車,通過以永磁高鐵核心動力技術為支撐,有效克服了永磁體失磁難題,保證了電機在各種極限條件下的磁體穩定。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

020.掌握Pod-Pod基礎使用

一 Pod定義詳解

1.1 完整Pod定義文件

  1 apiVersion: v1			#必選,版本號,例如v1,版本號必須可以用 kubectl api-versions 查詢到
  2 kind: Pod				#必選,Pod
  3 metadata:				#必選,元數據
  4   name: string			#必選,Pod名稱,需符合RFC 1035規範
  5   namespace: string			#必選,Pod所屬的命名空間,默認為"default"
  6   labels:				#自定義標籤
  7     - name: string			#自定義標籤名字
  8   annotations:			#自定義註釋列表
  9     - name: string
 10 spec:				#必選,Pod中容器的詳細定義
 11   containers:			#必選,Pod中容器列表
 12   - name: string			#必選,容器名稱,需符合RFC 1035規範
 13     image: string			#必選,容器的鏡像名稱
 14     imagePullPolicy: [ Always|Never|IfNotPresent ]	#獲取鏡像的策略,Alawys表示每次都嘗試下載鏡像,IfnotPresent表示優先使用本地鏡像,否則下載鏡像,Nerver表示僅使用本地鏡像
 15     command: [string]		#容器的啟動命令列表,如不指定,使用打包時使用的啟動命令
 16     args: [string]			#容器的啟動命令參數列表
 17     workingDir: string		#容器的工作目錄
 18     volumeMounts:			#掛載到容器內部的存儲卷配置
 19     - name: string			#引用pod定義的共享存儲卷的名稱,需用volumes[]部分定義的的卷名
 20       mountPath: string		#存儲卷在容器內mount的絕對路徑,應少於512字符
 21       readOnly: boolean		#是否為只讀模式,默認為讀寫模式
 22     ports:				#需要暴露的端口庫號列表
 23     - name: string			#端口的名稱
 24       containerPort: int		#容器需要監聽的端口號
 25       hostPort: int		        #容器所在主機需要監聽的端口號,默認與Container相同
 26       protocol: string		#端口協議,支持TCP和UDP,默認TCP
 27     env:				#容器運行前需設置的環境變量列表
 28     - name: string			#環境變量名稱
 29       value: string		        #環境變量的值
 30     resources:			#資源限制和請求的設置
 31       limits:			#資源限制的設置
 32         cpu: string		        #CPU的限制,單位為core數,將用於docker run --cpu-shares參數
 33         memory: string		#內存限制,單位可以為Mib/Gib,將用於docker run --memory參數
 34       requests:			#資源請求的設置
 35         cpu: string		        #CPU請求,容器啟動的初始可用數量
 36         memory: string		#內存請求,容器啟動的初始可用數量
 37     livenessProbe:			#對Pod內各容器健康檢查的設置,當探測無響應幾次后將自動重啟該容器,檢查方法有exec、httpGet和tcpSocket,對一個容器只需設置其中一種方法即可
 38       exec:			        #對Pod容器內檢查方式設置為exec方式
 39         command: [string]		#exec方式需要制定的命令或腳本
 40       httpGet:			#對Pod內個容器健康檢查方法設置為HttpGet,需要制定Path、port
 41         path: string
 42         port: number
 43         host: string
 44         scheme: string
 45         HttpHeaders:
 46         - name: string
 47           value: string
 48       tcpSocket:			#對Pod內個容器健康檢查方式設置為tcpSocket方式
 49          port: number
 50        initialDelaySeconds: 0	#容器啟動完成后首次探測的時間,單位為秒
 51        timeoutSeconds: 0		#對容器健康檢查探測等待響應的超時時間,單位秒,默認1秒
 52        periodSeconds: 0		#對容器監控檢查的定期探測時間設置,單位秒,默認10秒一次
 53        successThreshold: 0
 54        failureThreshold: 0
 55        securityContext:
 56          privileged: false
 57     restartPolicy: [Always | Never | OnFailure]	#Pod的重啟策略,Always表示一旦不管以何種方式終止運行,kubelet都將重啟,OnFailure表示只有Pod以非0退出碼退出才重啟,Nerver表示不再重啟該Pod
 58     nodeSelector: obeject		#設置NodeSelector表示將該Pod調度到包含這個label的node上,以key:value的格式指定
 59     imagePullSecrets:		#Pull鏡像時使用的secret名稱,以key:secretkey格式指定
 60     - name: string
 61     hostNetwork: false		#是否使用主機網絡模式,默認為false,如果設置為true,表示使用宿主機網絡
 62     volumes:			#在該pod上定義共享存儲卷列表
 63     - name: string			#共享存儲卷名稱 (volumes類型有很多種)
 64       emptyDir: {}			#類型為emtyDir的存儲卷,與Pod同生命周期的一個臨時目錄。為空值
 65       hostPath: string		#類型為hostPath的存儲卷,表示掛載Pod所在宿主機的目錄
 66         path: string		#Pod所在宿主機的目錄,將被用於同期中mount的目錄
 67       secret:			#類型為secret的存儲卷,掛載集群與定義的secre對象到容器內部
 68         scretname: string
 69         items:
 70         - key: string
 71           path: string
 72       configMap:			#類型為configMap的存儲卷,掛載預定義的configMap對象到容器內部
 73         name: string
 74         items:
 75         - key: string
 76           path: string

二 Pod的基本用法

2.1 創建Pod


Pod可以由1個或多個容器組合而成,通常對於緊耦合的兩個應用,應該組合成一個整體對外提供服務,則應該將這兩個打包為一個pod。

屬於一個Pod的多個容器應用之間相互訪問只需要通過localhost即可通信,這一組容器被綁定在一個環境中。

  1 [root@k8smaster01 study]# vi frontend-localredis-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: redis-php
  6   label:
  7     name: redis-php
  8 spec:
  9   containers:
 10   - name: frontend
 11     image: kubeguide/guestbook-php-frontend:localredis
 12     ports:
 13     - containersPort: 80
 14   - name: redis-php
 15     image: kubeguide/redis-master
 16     ports:
 17     - containersPort: 6379
 18 
 19 [root@k8smaster01 study]# kubectl create -f frontend-localredis-pod.yaml
 20 


2.2 查看Pod

  1 [root@k8smaster01 study]# kubectl get pods	                #READY為2/2,表示此Pod中運行了yaml定義的兩個容器
  2 NAME        READY   STATUS    RESTARTS   AGE
  3 redis-php   2/2     Running   0          7m45s
  4 [root@k8smaster01 study]# kubectl describe pod redis-php	#查看詳細信息
  5 


三 靜態Pod

3.1 靜態Pod概述


靜態pod是由kubelet進行管理的僅存在於特定Node的Pod上,他們不能通過API Server進行管理,無法與ReplicationController、Deployment或者DaemonSet進行關聯,並且kubelet無法對他們進行健康檢查。靜態Pod總是由kubelet進行創建,並且總是在kubelet所在的Node上運行。

創建靜態Pod有兩種方式:配置文件或者HTTP方式。

3.2 配置文件方式創建

  1 [root@k8snode01 ~]# mkdir -p /etc/kubelet.d
  2 [root@k8snode01 ~]# vi /etc/kubelet.d/static-web.yaml
  3 apiVersion: v1
  4 kind: Pod
  5 metadata:
  6   name: static-web
  7   label:
  8     name: static-web
  9 spec:
 10   containers:
 11   - name: static-web
 12     image: nginx
 13     ports:
 14     - name: web
 15       containersPort: 80
 16 
 17 [root@k8snode01 ~]# vi /etc/systemd/system/kubelet.service
 18 ……
 19   --config=/etc/kubelet.d/ \·				#加入此參數
 20 ……
 21 [root@k8snode01 ~]# systemctl daemon-reload
 22 [root@k8snode01 ~]# systemctl restart kubelet.service	#重啟kubelet
 23 [root@k8snode01 ~]# docker ps				#查看創建的pod



提示:由於靜態pod不能通過API Server進行管理,因此在Master節點執行刪除操作後會變為Pending狀態,且無法刪除。刪除該pod只能在其運行的node上,將定義POD的yaml刪除。

3.3 HTTP方式


通過設置kubelet的啟動參數–mainfest-url,會定期從該URL下載Pod的定義文件,並以.yaml或.json文件的格式進行解析,從而創建Pod。

四 Pod容器共享Volume

4.1 共享Volume


在同一個Pod中的多個容器能夠共享Pod級別的存儲就Volume。Volume可以被定義為各種類型,多個容器各自進行掛載操作,將一個Volume掛載為容器內部需要的目錄。


示例1:

Pod級別設置Volume “app-logs”,同時Pod包含兩個容器,Tomcat向該Volume寫日誌,busybox讀取日誌文件。

  1 [root@k8smaster01 study]# vi pod-volume-applogs.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: volume-pod
  6 spec:
  7   containers:
  8   - name: tomcat
  9     image: tomcat
 10     ports:
 11     - containerPort: 8080
 12     volumeMounts:
 13     - name: app-logs
 14       mountPath: /usr/local/tomcat/logs
 15   - name: logreader
 16     image: busybox
 17     command: ["sh","-c","tail -f /logs/catalina*.log"]
 18     volumeMounts:
 19     - name: app-logs
 20       mountPath: /logs
 21   volumes:
 22   - name: app-logs
 23     emptyDir: {}

解釋:

Volume名:app-logs;

emptyDir:為Pod分配到Node的時候創建。無需指定宿主機的目錄文件,為Kubernetes自動分配的目錄。

  1 [root@k8smaster01 study]# kubectl create -f pod-volume-applogs.yaml	#創建
  2 [root@k8smaster01 study]# kubectl get pods				#查看
  3 [root@k8smaster01 study]# kubectl logs volume-pod -c busybox	#讀取log




  1 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- ls /usr/local/tomcat/logs
  2 catalina.2019-06-29.log      localhost_access_log.2019-06-29.txt
  3 host-manager.2019-06-29.log  manager.2019-06-29.log
  4 localhost.2019-06-29.log
  5 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- tail /usr/local/tomcat/logs/catalina.2019-06-29.log



提示:通過tomcat容器可查看日誌,對比busybox通過共享Volume查看的日誌是否一致。

五 Pod配置管理

5.1 Pod配置概述


應用部署的一個最佳實踐是將應用所需的配置信息與程序進行分離,使程序更加靈活。將相應的應用打包為鏡像,可以通過環境變量或者外掛volume的方式在創建容器的時候進行配置注入,從而實現更好的復用。

Kubernetes提供一種統一的應用配置管理方案:ConfigMap。

5.2 ConfigMap概述


ConfigMap供容器使用的主要場景:

  • 生成容器內部的環境變量;
  • 設置容器的啟動命令的參數(需設置為環境變量);
  • 以volume的形式掛載為容器內部的文件或者目錄。


ConfigMap以一個或多個key:value的形式定義。value可以是string也可以是一個文件內容,可以通過yaml配置文件或者通過kubectl create configmap 的方式創建configMap。

5.3 創建ConfigMap資源對象——yaml方式

  1 [root@k8smaster01 study]# vi cm-appvars.yaml
  2 apiVersion: v1
  3 kind: ConfigMap
  4 metadata:
  5   name: cm-appvars
  6 data:
  7   apploglevel: info
  8   appdatadir: /var/data
  9 
 10 [root@k8smaster01 study]# kubectl create -f cm-appvars.yaml
 11 configmap/cm-appvars created
 12 [root@k8smaster01 study]# kubectl get configmaps
 13 NAME         DATA   AGE
 14 cm-appvars   2      8s
 15 [root@k8smaster01 study]# kubectl describe configmaps cm-appvars



  1 [root@k8smaster01 study]# kubectl get configmaps cm-appvars -o yaml


5.4 創建ConfigMap資源對象——命令行方式


語法1

  1 # kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source



解釋:通過–from-file參數從文件中創建,可以指定key名稱,也可以制定多個key。

語法2

  1 # kubectl create configmap NAME --from-file=config-files-dir



解釋:通過–from-file參數從目錄中創建,該目錄下的每個配置文件名都被設置為key,文件的內容被設置為value。

語法3

  1 # kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2



解釋:通過–from-literal參數從文本中創建,直接將指定的key#=value#創建為ConfigMap的內容。

5.5 Pod使用ConfigMap


容器應用使用ConfigMap有兩種方式:

  • 通過環境變量獲取ConfigMap中的內容;
  • 通過Volume掛載的方式將ConfigMap中的內容掛載為容器內容的文件或目錄。

  1 [root@k8smaster01 study]# vi cm-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: cm-test-pod
  6 spec:
  7   containers:
  8   - name: cm-test
  9     image: busybox
 10     command: ["/bin/sh","-c","env|grep APP"]	#容器里執行查看環境變量的命令
 11     env:
 12     - name: APPLOGLEVEL				#定義容器環境變量名稱
 13       valueFrom:
 14         configMapKeyRef:			#環境變量的值來自ConfigMap
 15           name: cm-appvars			#指定來自cm-appvars的ConfigMap
 16           key: apploglevel			#key為apploglevel
 17     - name: APPDATADIR
 18       valueFrom:
 19         configMapKeyRef:
 20           name: cm-appvars
 21           key: appdatadir
 22 
 23 [root@k8smaster01 study]# kubectl create -f cm-test-pod.yaml
 24 [root@k8smaster01 study]# kubectl get pods
 25 NAME          READY   STATUS      RESTARTS   AGE
 26 cm-test-pod   0/1     Completed   2          24s



【掛載形式-待補充】

5.6 ConfigMap限制


  • Configmap必須在pod創建之間創建;
  • ConfigMap受到namespace的限制,只有同一個命名空間下才能引用;
  • ConfigMap暫時無法配置配額;
  • 靜態的pod無法使用ConfigMap;
  • 在使用volumeMount掛載的時候,configMap基於items創建的文件會整體的將掛載數據卷的容器的目錄下的文件全部覆蓋。

六 Pod獲取自身信息

6.1 Downward API


pod擁有唯一的名字、IP地址,並且處於某個Namespace中。pod的容器內獲取pod的信息科通過Downward API實現。具體有以下兩種方式:

  • 環境變量:用於單個變量,可以將pod信息和container信息注入容器內部;
  • volume掛載:將數組類信息生成為文件,掛載至容器內部。


舉例1:通過Downward API將Pod的IP、名稱和所在的Namespace注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       command: [ "/bin/sh", "-c", "env" ]
 11       env:
 12         - name: MY_POD_NAME
 13           valueFrom:
 14             fieldRef:
 15               fieldPath: metadata.name
 16         - name: MY_POD_NAMESPACE
 17           valueFrom:
 18             fieldRef:
 19               fieldPath: metadata.namespace
 20         - name: MY_POD_IP
 21           valueFrom:
 22             fieldRef:
 23               fieldPath: status.podIP
 24   restartPolicy: Never



提示:Downward API提供如下變量:

metadata.name:Pod的名稱,當Pod通過RC生成時,其名稱是RC隨機產生的唯一名稱;

status.podIP:Pod的IP地址,POd的IP屬於狀態數據,而非元數據;

metadata.namespace:Pod所在的namespace。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod | grep MY_POD
  3 MY_POD_NAMESPACE=default
  4 MY_POD_IP=172.30.240.4
  5 MY_POD_NAME=dapi-test-pod
  6 



舉例2:通過Downward API將Container的自願請求和限制信息注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod-container-vars.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-container-vars
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       imagePullPolicy: Never
 11       command: [ "/bin/sh", "-c" ]
 12       args:
 13       - while true; do
 14           echo -en '\n';
 15           printenv MY_CPU_REQUEST MY_CPU_LIMIT;
 16           printenv MY_MEM_REQUEST MY_MEM_LIMIT;
 17           sleep 3600;
 18         done;
 19       resources:
 20         requests:
 21           memory: "32Mi"
 22           cpu: "125m"
 23         limits:
 24           memory: "64Mi"
 25           cpu: "250m"
 26       env:
 27         - name: MY_CPU_REQUEST
 28           valueFrom:
 29             resourceFieldRef:
 30               containerName: test-container
 31               resource: requests.cpu
 32         - name: MY_CPU_LIMIT
 33           valueFrom:
 34             resourceFieldRef:
 35               containerName: test-container
 36               resource: limits.cpu
 37         - name: MY_MEM_REQUEST
 38           valueFrom:
 39             resourceFieldRef:
 40               containerName: test-container
 41               resource: requests.memory
 42         - name: MY_MEM_LIMIT
 43           valueFrom:
 44             resourceFieldRef:
 45               containerName: test-container
 46               resource: limits.memory
 47   restartPolicy: Never



提示:Downward API提供如下變量:

requests.cpu:容器的CPU請求值;

limits.cpu:容器的CPU限制值;

requests.memory:容器的內存請求值;

limits.memory:容器的內存限制值。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-container-vars.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-container-vars
  3 1
  4 1
  5 33554432
  6 67108864



舉例3:通過Downward API將Pod的Label、Annotation列表通過Volume掛載為容器內的一個文件。

  1 [root@k8smaster01 study]# vi dapi-test-pod-volume.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-volume
  6   labels:
  7     zone: us-est-coast
  8     cluster: test-cluster1
  9     rack: rack-22
 10   annotations:
 11     build: two
 12     builder: john-doe
 13 spec:
 14   containers:
 15     - name: test-container
 16       image: busybox
 17       imagePullPolicy: Never
 18       command: [ "/bin/sh", "-c" ]
 19       args:
 20       - while true; do
 21           if [[ -e /etc/labels ]]; then
 22             echo -en '\n\n'; cat /etc/labels; fi;
 23           if [[ -e /etc/annotations ]]; then
 24             echo -en '\n\n'; cat /etc/annotations; fi;
 25           sleep 3600;
 26         done;
 27       volumeMounts:
 28         - name: podinfo
 29           mountPath: /etc
 30           readOnly: false
 31   volumes:
 32     - name: podinfo
 33       downwardAPI:
 34         items:
 35           - path: "labels"
 36             fieldRef:
 37               fieldPath: metadata.labels
 38           - path: "annotations"
 39             fieldRef:
 40               fieldPath: metadata.annotations



注意:Volume中的ddownwardAPI的items語法,將會以path的名稱生成文件。如上所示,會在容器內生產/etc/labels和/etc/annotations兩個文件,分別包含metadata.labels和metadata.annotations的全部Label。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-volume.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-volume
  3 



提示:DownwardAPI意義:

在某些集群中,集群中的每個節點需要將自身的標識(ID)及進程綁定的IP地址等信息事先寫入配置文件中,進程啟動時讀取此類信息,然後發布到某個類似註冊服務中心。此時可通過DowanwardAPI,將一個預啟動腳本或Init Container,通過環境變量或文件方式獲取Pod自身的信息,然後寫入主程序配置文件中,最後啟動主程序。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Toyota將在中國成立PHEV產線

日本豐田(Toyota)日前於北京發表會上表示,計畫在2018年時開始在中國市場推出PHEV(插電式油電混和車)產品,同時計畫轉移部份產能到中國。

在Toyota表示將在中國推出PHEV之際,因應在地生產的方針,Toyota也首度規劃在日本以外的國家生產PHEV電動車。去年秋天,Toyota已於中國發售Corolla、Levin兩款中國生產的混和車,未來將會推出這兩款車款的PHEV版本。

Toyota表示,2018年左右,會有更多品牌陸續在中國推出PHEV或純電動車,因此需搶先布局中國的相關產線。此外,在中國生產的車款將以中國市場為主,且以組裝為中國產線的主要業務。核心技術與配件則仍留在日本國內。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

通過javascript 執行環境理解她

古往今來最難的學的武功(javascript)算其一。

欲練此功必先自宮,願少俠習的此功,笑傲江湖。

你將了解

  • 執行棧(Execution stack)
  • 執行上下文(Execution Context)
  • 作用域鏈(scope chains)
  • 變量提升(hoisting)
  • 閉包(closures)
  • this 綁定

執行棧

又叫調用棧,具有 LIFO(last in first out 後進先出)結構,用於存儲在代碼執行期間創建的所有執行上下文。

當 JavaScript 引擎首次讀取你的腳本時,它會創建一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會為該函數創建一個新的執行上下文並將其推到當前執行棧的頂端。
引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成后,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。

我們通過下面的示例來說明一下

function one() {
  console.log('one')
  two()
}
function two() {
  console.log('two')
}
one()

當程序(代碼)開始執行時 javscript 引擎創建 GobalExecutionContext (全局執行上下文)推入當前的執行棧,此時 GobalExecutionContext 處於棧頂會立刻執行全局執行上下文 然後遇到 one() 引擎都會為該函數創建一個新的執行上下文 oneFunctionExecutionContext 並將其推到當前執行棧的頂端並執行,然後遇到two() twoFunctionExecutionContext 入棧並執行至出棧,回到 oneFunctionExecutionContext 繼續執行至出棧 ,最後剩餘一個 GobalExecutionContext 它會在程序關閉的時候出棧。

然後調用棧如下圖:

如果是這樣的代碼

function foo() {
  foo()
}
foo()

如下

當一個遞歸沒有結束點的時候就會出現棧溢出

什麼是執行上下文

了解 JavaScript 的執行上下文,有助於你理解更高級的內容比如變量提升、作用域鏈和閉包。既然如此,那到底什麼是“執行上下文”呢?

執行上下文是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念。

Javascript 中代碼的執行上下文分為以下三種:

  1. 全局執行上下文(Global Execution Context)- 這個是默認的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。
  2. 函數執行上下文(Function Execution Context) – 當執行一個函數時,運行函數體中的代碼。
  3. Eval – 在 Eval 函數內運行的代碼。

javascript 是一個單線程語言,這意味着在瀏覽器中同時只能做一件事情。當 javascript 解釋器初始執行代碼,它首先默認進入全局上下文。每次調用一個函數將會創建一個新的執行上下文。

javascript執行棧中不同執行上下文之間的詞法環境有一種關聯關係,從棧頂到棧底(從局部直到全局),這種關係被叫做作用域鏈

簡單的說,每次你試圖訪問函數執行上下文中的變量時,進程總是從自己上下文環境中開始查找。如果在自己的上下文中沒發現要查找的變量,繼續搜索下一層上下文。它將檢查執行棧中每一個執行上下文環境,尋找和變量名稱匹配的值,直到找到為止,如果到全局都沒有則拋出錯誤。

執行上下文的創建過程

我們現在已經知道,每當調用一個函數時,一個新的執行上下文就會被創建出來。然而,在 javascript 引擎內部,這個上下文的創建過程具體分為兩個階段:

創建階段 > 執行階段

創建階段

執行上下文在創建階段創建。在創建階段發生以下事情:

  1. LexicalEnvironment 組件已創建。
  2. VariableEnvironment 組件已創建。

因此,執行上下文可以在概念上表示如下:

ExecutionContext = {
  LexicalEnvironment = <詞法環境>,
  VariableEnvironment = <變量環境>,
}

詞法環境(Lexical Environment)

文檔將詞法環境定義為:

詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能為空引用(null)的外部詞法環境組成。

簡而言之,詞法環境是一個包含標識符變量映射的結構。(這裏的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用)

詞法環境有兩種類型

  • 全局環境(在全局執行上下文中)是一個沒有外部環境的詞法環境。全局環境的外部環境引用為 null。它擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this 的值指向這個全局對象。
  • 函數環境,用戶在函數中定義的變量被存儲在環境記錄中。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。

每個詞彙環境都有三個組成部分:

1)環境記錄(environment record)

2)對外部環境的引用(outer)

3) 綁定 this

環境記錄 同樣有兩種類型(如下所示):

  • 聲明性環境記錄 存儲變量、函數和參數。一個函數環境包含聲明性環境記錄。
  • 對象環境記錄 用於定義在全局執行上下文中出現的變量和函數的關聯。全局環境包含對象環境記錄

抽象地說,詞法環境在偽代碼中看起來像這樣:

詞法環境和環境記錄值是純粹的規範機制,ECMAScript 程序不能直接訪問或操縱這些值。

GlobalExectionContext = {
  // 詞法環境
  LexicalEnvironment:{
    // 功能環境記錄
    EnvironmentRecord:{
      Type:"Object",
      // Identifier bindings go here
     }
    outer:<null>,
    this:<global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment:{
    EnvironmentRecord:{
      Type:"Declarative",
      // Identifier bindings go here
     }
    outer:<Global or outer function environment reference>,
    this:<取決於函數的調用方式>
  }
}

變量環境:

它也是一個詞法環境,其 EnvironmentRecord 包含了由 VariableStatements 在此執行上下文創建的綁定。
如上所述,變量環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性。
在 ES6 中,LexicalEnvironment 組件和 VariableEnvironment 組件的區別在於前者用於存儲函數聲明和變量( let 和 const )綁定,而後者僅用於存儲變量( var )綁定。
讓我們結合一些代碼示例來理解上述概念:

let a = 20
const b = 30
var c

function multiply(e, f) {
  var g = 20
  return e * f * g
}

c = multiply(20, 30)

執行上下文如下所示:

GlobalExectionContext = {
  LexicalEnvironment:{
    EnvironmentRecord:{
      Type:"Object",
      // Identifier bindings go here
      a:<uninitialized>,
      b:<uninitialized>,
      multiply:<func>
    },
    outer:<null>,
    ThisBinding:<Global Object>
  },
  VariableEnvironment:{
    EnvironmentRecord:{
      Type:"Object",
      // Identifier bindings go here
      c:undefined,
    }
    outer:<null>,
    ThisBinding:<Global Object>
  }
}

在執行階段,完成變量賦值。因此,在執行階段,全局執行上下文將看起來像這樣。

// 執行
GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

當 multiply(20, 30)遇到函數調用時,會創建一個新的函數執行上下文來執行函數代碼。因此,在創建階段,函數執行上下文將如下所示:

// multiply 創建
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

在此之後,執行上下文將執行執行階段,這意味着完成了對函數內部變量的賦值。因此,在執行階段,函數執行上下文將如下所示:

// multiply 執行
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函數完成后,返回的值賦值給c。因此,全局詞法環境得到了更新。之後,全局代碼完成,程序結束。

注: 在執行階段,如果 Javascript 引擎在源代碼中聲明的實際位置找不到 let 變量的值,那麼將為其分配 undefined 值。

變量提升

在網上一直看到這樣的總結: 在函數中聲明的變量以及函數,其作用域提升到函數頂部,換句話說,就是一進入函數體,就可以訪問到其中聲明的變量以及函數。這是對的,但是知道其中的緣由嗎?相信你通過上述的解釋應該也有所明白了。不過在這邊再分析一下。

你可能已經注意到了在創建階段 letconst 定義的變量沒有任何與之關聯的值,但 var 定義的變量設置為 undefined
這是因為在創建階段,代碼會被掃描並解析變量和函數聲明,其中函數聲明存儲在環境中,而變量會被設置為 undefined(在 var 聲明變量的情況下)或保持未初始化(在 letconst 聲明變量的情況下)。
這就是為什麼你可以在聲明之前訪問var 定義的變量(儘管是 undefined ),但如果在聲明之前訪問letconst 定義的變量就會提示引用錯誤的原因。
這就是我們所謂的變量提升

思考題:

console.log('step1:',a)
var a = 'artiely'
console.log('step2:',a)
function bar (a){
  console.log('step3:',a)
  a = 'TJ'
  console.log('step4:',a)
  function a(){
  }
}
bar(a)
console.log('step5:',a)

對外部環境的引用

上面代碼如果我們改用調用方式如下:

let a = 20
const b = 30
var c

function multiply() {
  var g = 20
  return a * b * g
}

c = multiply()

其實你會發現結果是一樣的
但是 multiply 的執行上下文卻發生一些變化

// 創建
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: { length: 0},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
// 執行
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: { length: 0},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

multiply() 執行的時候會直接在 outer: <GlobalLexicalEnvironment>,中查找a,b

對外部環境的引用意味着它可以訪問其外部詞法環境。這意味着如果在當前詞法環境中找不到它們,JavaScript 引擎可以在外部環境中查找變量。這就是之前說的 作用域鏈

閉包

MDN 解釋 閉包是由函數以及創建該函數的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變量

這是我認為對閉包最合理的解釋了,就看你怎麼理解閉包的機制了。
其實閉包與作用域鏈有着密切的關係。

首先我們來看看什麼樣的代碼會產生閉包。

function foo() {
  var name = 'artiely'
  function bar() {
    console.log(`hello `)
  }
  bar()
}
foo()

以上代碼是有閉包嗎?沒有~

function foo() {
  var name = 'artiely'
  function bar() {
    console.log(`hello ${name}`)
  }
  bar()
}
foo()

我們只做了微小的調整,現在就有閉包了,我們只是在bar中加入了name得引用
上面的代碼還可以寫成這樣

// 或者
function foo() {
  var name = 'artiely'
  return function bar() {
    console.log(`hello ${name}`)
  }
}
foo()()

對於閉包的形成我進行了如下的幾點歸納

  1. A 函數內必須有 B 函數的聲明;
  2. B 函數必須引用 A 函數的變量;
  3. B 函數被調用(當然前提是 A 函數被調用)

以上 3 點缺一不可

我們來分析一下上面代碼的執行上下文

// 創建
fooFunctionExectionContext = {
LexicalEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    Arguments: { length: 0},
    bar: < func >,
  },
  outer: <GlobalLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    name: undefined
  },
  outer: <GlobalLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>
  }
}
// 執行 略
// 創建
barFunctionExectionContext = {
LexicalEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    Arguments: { length: 0},
  },
  outer: <fooLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
  },
  outer: <fooLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>
  }
}
// 執行 略

這裏因為bar的創建存在着對fooLexicalEnvironment里變量的引用,雖然foo可能執行已結束但變量不會被回收。這種機制被叫做閉包

閉包是由函數以及創建該函數的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變量

我們結合上面例子重新分解一下這句話

閉包是由函數bar以及創建該函數foo的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變量name

但是從chrome的理解,閉包並沒有包含所能訪問的所有局部變量,僅僅包含所被引用的變量。

this 綁定

在全局執行上下文中,值是 this 指全局對象。(在瀏覽器中,this 指的是 Window 對象)。

在函數執行上下文中,值 this 取決於函數的調用方式。如果它由對象引用調用,則將值 this 設置為該對象,否則,將值 this 設置為全局對象或 undefined(在嚴格模式下)。例如:

let person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear)
  }
}

person.calcAge()
// 'this' 指向 'person', 因為 'calcAge' 是被 'person' 對象引用調用的。

let calculateAge = person.calcAge
calculateAge()
// 'this' 指向全局 window 對象,因為沒有給出任何對象引用

注意所有的()()自調用的函數 this 都是指向Global Object的既瀏覽器中的window

最後

如果本文對你有幫助或覺得不錯請幫忙點贊,如有疑問請留言。

其他參考:

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

netty源碼解析(4.0)-29 Future模式的實現

  Future模式是一個重要的異步併發模式,在JDK有實現。但JDK實現的Future模式功能比較簡單,使用起來比較複雜。Netty在JDK Future基礎上,加強了Future的能力,具體體現在:

  1. 更加簡單的結果返回方式。在JDK中,需要用戶自己實現Future對象的執行及返回結果。而在Netty中可以使用Promise簡單地調用方法返回結果。
  2. 更加靈活的結果處理方式。JDK中只提供了主動得到結果的get方法,要麼阻塞,要麼輪詢。Netty除了支持主動get方法外,還可以使用Listener被動監聽結果。
  3. 實現了進度監控。Netty提供了ProgressiveFuture、ProgressivePromise和GenericProgressiveFutureListener接口及其實現,支持對執行進程的監控。

  吹了那麼多牛,有一個關鍵問題還沒弄清楚:Future到底是幹嘛的?io.netty.util.concurrent.Future代碼的第一行註釋簡潔第回答了這個問題:Future就是異步操作的結果。這裏面有三個關鍵字:異步,操作,結果。首先,Future首先是一個“結果”;其次這個結果產生於一個“操作”,操作具體是什麼可以隨便定義;最後這個操作是”異步”執行的,這就意味着“操作”可能在另一個線程中併發執行,也可能隨後在同一個線程中執行,什麼時候產生結果是一件不確定的事。

  異步調用過程的一般過程是:調用方喚起一個異步操作,在接下來的某個恰當的時間點得到的異步操作操作的結果。要正確地完成上述步驟,需要解決以下幾個問題:

  • 怎樣維護這個調用狀態?
  • 如何獲取異步操作的結果?
  • 何時處理結果?

  io.netty.util.concurrent.DefaultPromise是Future的默認實現,以上三個問題的答案都能在這個類的代碼中找到。

 

DefaultPromise的派生體系

  下面是DefaultPromis及其父類,接口的聲明:

  public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> 

  public abstract class AbstractFuture<V> implements Future<V>

  public interface Promise<V> extends Future<V> 

  public interface Future<V> extends java.util.concurrent.Future<V> 

   可以看出,DefaultPromise派生自AbstractFuture類,並實現了Promise接口。抽象類型AbstractFuture派生自Future, 接口Promise派生自Future。Future派生自JDK的Future接口。

  和JDK的Future相比,Netty的Future接口增加一些自己的方法:

   /**
     當操作成功時返回true*/
    boolean isSuccess();

    /**
   只有當操作可以被取消時返回true
*/ boolean isCancellable(); /** 返回操作的異常*/ Throwable cause(); /** 添加一個監聽器到future。當操作完成(成功或失敗都算完成,此事isDone()返回true)時, 會通知這個監聽器。如果添加時操作已經完成,
   這個監聽器會立即被通知。
*/ Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener); /** 和上個方法一樣,可以同時添加多個監聽器*/ Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners); /** 刪除指定的監聽器, 如果這個監聽器還沒被通知的話。*/ Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener); /** 功能和上個方法一樣,可以同時刪除多個監聽器。*/ Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners); /** 同步等待直到操作完成。會被打斷。 */ Future<V> sync() throws InterruptedException; /**    同步等着知道操作完成。不會被打斷。 */ Future<V> syncUninterruptibly(); /** 同sync*/ Future<V> await() throws InterruptedException; /** 同synUniterruptibliy*/ Future<V> awaitUninterruptibly(); /** 等待,直到操作完成或超過指定的時間。會被打斷。*/ boolean await(long timeout, TimeUnit unit) throws InterruptedException; /** 同上*/ boolean await(long timeoutMillis) throws InterruptedException; /** 同上,不會被打斷。*/ boolean awaitUninterruptibly(long timeout, TimeUnit unit); /** 同上。*/ boolean awaitUninterruptibly(long timeoutMillis); /** 立即得到結果,不會阻塞。如果操作沒有完成或沒有成功,返回null*/ V getNow();

  Netty的Future最大的特點是增加了Listener被動接收任務完成通知,下面是兩個Listener接口的定義:

public interface GenericFutureListener<F extends Future<?>> extends EventListener {
    void operationComplete(F future) throws Exception;
}

public interface GenericProgressiveFutureListener<F extends ProgressiveFuture<?>> extends GenericFutureListener<F> {
    void operationProgressed(F future, long progress, long total) throws Exception;
}

  把一個listener添加到future之後。當異步操作完成之後,listener會被通知一次,同時會回調operationComplete方法。參數future是當前通知的future,這意味這,一個listener可以被添加到多個future中。

  當異步操作進度發送變化時,listener會被通知,同時會回調operationProgressed方法。progress是當前進度,total是總進度。progress==total表示操作完成。如果不知道何時完成操作progress=-1。

  Promise定義的方法:

    /**
    設置結果。把這個future設置為success,通知所有的listener,
  如果這個future已經是success或failed(操作已經完成),會拋出IllegalStateException
*/ Promise<V> setSuccess(V result); /**
同上。只有在操作沒有完成的時候才會生效,且會返回true */ boolean trySuccess(V result); /** 設置異常。把這個future設置為failed狀態,通知所有的listener.
如果這個future已經完成,會拋出IllegalStateException
*/ Promise<V> setFailure(Throwable cause); /** 同上。只有在操作沒有完成時才會生效,且返回ture */ boolean tryFailure(Throwable cause); /** 設置當前前future的操作不能被取消。這個future沒有完成且可以設置成功或這個future已經完成,返回true。否則返回false */ boolean setUncancellable();

 

DefaultPromise的設計

關鍵屬性

  volatile Object result;

  異步操作的結果。可以通過它的值知道當前future的狀態。

  final EventExecutor executor;

  通知listener的線程。

  Object listeners;

  維護添加到當前future的listener對象。

  short waiters;

  記錄當前真正等待結果的線程數量。

  boolean notifyingListeners;

  是否正在通知listener,防止多線程併發執行通知操作。

 

狀態管理

  future有4種狀態: 未完成, 未完成-不能取消,完成-成功,完成-失敗。使用isDone()判斷是否完成,它代碼如下:

1     @Override
2     public boolean isDone() {
3         return isDone0(result);
4     }
5 
6     private static boolean isDone0(Object result) {
7         return result != null && result != UNCANCELLABLE;
8     }

  第7行是判斷當前完成狀態的。result != null 且 result != UNCANCELLABLE,表示處於完成狀態。

  result默認是null, 此時future處於未完成狀態。可以使用setUncancellable方法把它設置成為完成-不能取消狀態。

1     @Override
2     public boolean setUncancellable() {
3         if (RESULT_UPDATER.compareAndSet(this, null, UNCANCELLABLE)) {
4             return true;
5         }
6         Object result = this.result;
7         return !isDone0(result) || !isCancelled0(result);
8     }

  第3行,使用原子操作設置result的值,只有result==null時才能把result設置成UNCANCELLABLE。當result==UNCANCELLABLE時,不允許取消異步操作。

  使用isSuccess方法判斷future是否處於完成-成功狀態。

1     @Override
2     public boolean isSuccess() {
3         Object result = this.result;
4         return result != null && result != UNCANCELLABLE && !(result instanceof CauseHolder);
5     }

  第4行是完成-成功狀態result的取值:除null, UNCANCELLABLE和CauseHolder對象的任何值。

  只有滿足isDone() && !isSuccess()時,future處於完成失敗狀態,可以使用cause方法獲取異常。

  調用setSuccess和trySuccess方法,能夠把狀態轉換成完成-成功。

 1     @Override
 2     public Promise<V> setSuccess(V result) {
 3         if (setSuccess0(result)) {
 4             notifyListeners();
 5             return this;
 6         }
 7         throw new IllegalStateException("complete already: " + this);
 8     }
 9     
10     private boolean setSuccess0(V result) {
11         return setValue0(result == null ? SUCCESS : result);
12     }

  第3行嘗試把狀態設置成完成-成功狀態。如果可以,在第4行通知所有的listener。否則第7行拋出錯誤。第11行給出了成功的默認值SUCCESS。trySuccess少了第7行,不會拋出異常。

  調用setFailure和tryFailure方法,能夠包狀態轉換成完成-失敗狀態。

 1     @Override
 2     public Promise<V> setFailure(Throwable cause) {
 3         if (setFailure0(cause)) {
 4             notifyListeners();
 5             return this;
 6         }
 7         throw new IllegalStateException("complete already: " + this, cause);
 8     }
 9 
10     private boolean setFailure0(Throwable cause) {
11         return setValue0(new CauseHolder(checkNotNull(cause, "cause")));
12     }

  第3行嘗試把專題設置成完成-失敗狀態。如果可以,在第4行通知所有listener。否則在第7行拋出異常。第11行把異常包裝成CauseHolder對象。tryFailure少了第7行,不會拋出異常。

 

獲取異步操作的結果

  當異步操作完成時,調用Promise提供的setSuccess和trySuccess設置成功的結果,調用setFailure和tryFailure設置異常結果。不論什麼結果,都會使用setValue0方法保存到result屬性上。

1     private boolean setValue0(Object objResult) {
2         if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
3             RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
4             checkNotifyWaiters();
5             return true;
6         }
7         return false;
8     }

  第2,3行,使用原子操作設置result的值,只有result==null或result==UNCANCELLABLE時,才能設置成功。如果設置成功,在第4行喚醒所有等待中的線程。可以使用get方法得到result值。如果isSucess()==true, result的值是SUCCESS或異步操作的結果。否則result的值是CauseHolder對象,此時可以調用cause方法得到異常對象。

  使用get或cause,只有在異步操作完成后才能順利得到結果。可以使用listener,被動等待操作完成通知。

 

使用listener異步通知處理結果

  Future的listener是必須實現GenericFutureListener接口,調用方法可以在operationComplete方法中處理異步操作的結果。

  listeners屬性用來保存使用addListener,addListeners方法添加到future的listener。listeners可能使用一個GenericFutureListener對象,也可能是一個GenericFutureListener數組。所有添加listener方法都會調用addListener0方法添加listener。

1     private void addListener0(GenericFutureListener<? extends Future<? super V>> listener) {
2         if (listeners == null) {
3             listeners = listener;
4         } else if (listeners instanceof DefaultFutureListeners) {
5             ((DefaultFutureListeners) listeners).add(listener);
6         } else {
7             listeners = new DefaultFutureListeners((GenericFutureListener<? extends Future<V>>) listeners, listener);
8         }
9     }

  這段代碼中使用了一個DefaultFutureListeners類,它內部維護了一個GenericFutureListener數組。

  當一次操作完成時,會調用notifyListeners方法通知listeners中所有的listener,並調用listener的operationComplete方法。只有當isDone()==true時才會調用notifyListeners方法。觸發點在下面的一些方法中:

  addListener, addListeners。

  setSuccess, trySuccess。

  setFailure, tryFailure。

  notifyListeners的代碼如下:

 1     private void notifyListeners() {
 2         EventExecutor executor = executor();
 3         if (executor.inEventLoop()) {
 4             final InternalThreadLocalMap threadLocals = InternalThreadLocalMap.get();
 5             final int stackDepth = threadLocals.futureListenerStackDepth();
 6             if (stackDepth < MAX_LISTENER_STACK_DEPTH) {
 7                 threadLocals.setFutureListenerStackDepth(stackDepth + 1);
 8                 try {
 9                     notifyListenersNow();
10                 } finally {
11                     threadLocals.setFutureListenerStackDepth(stackDepth);
12                 }
13                 return;
14             }
15         }
16 
17         safeExecute(executor, new Runnable() {
18             @Override
19             public void run() {
20                 notifyListenersNow();
21             }
22         });
23     }

  這段代碼的作用是調用notifyListenersNow。如果當前線程就是executor的線程,在第9行直接調用notifyListenerNow,否則在第20行,把notifyListnerNow放在executor中執行。第4-7行和11行的作用是防止遞歸調用導致線程棧溢出,MAX_LISTENER_STACK_DEPTH就是listener遞歸調用的最大深度。

  notifyListenerNow的作用是,確保沒有併發執行notifyListener0或notifyListners0方法,且所有的listener只能被通知一次。

 1     private void notifyListenersNow() {
 2         Object listeners;
 3         synchronized (this) {
 4             // Only proceed if there are listeners to notify and we are not already notifying listeners.
 5             if (notifyingListeners || this.listeners == null) {
 6                 return;
 7             }
 8             notifyingListeners = true;
 9             listeners = this.listeners;
10             this.listeners = null;
11         }
12         for (;;) {
13             if (listeners instanceof DefaultFutureListeners) {
14                 notifyListeners0((DefaultFutureListeners) listeners);
15             } else {
16                 notifyListener0(this, (GenericFutureListener<? extends Future<V>>) listeners);
17             }
18             synchronized (this) {
19                 if (this.listeners == null) {
20                     // Nothing can throw from within this method, so setting notifyingListeners back to false does not
21                     // need to be in a finally block.
22                     notifyingListeners = false;
23                     return;
24                 }
25                 listeners = this.listeners;
26                 this.listeners = null;
27             }
28         }
29     }

  第3-11行的作用是防止多個線程併發執行11行之後的代碼。

  結合第5,9,10行可知, listeners中的所有listener只能被通知一次。

  13-17行,通知所有listeners。notifyListener0通知一個listener,notifyListeners0通知所有的listener。

  最後,18-27行,檢查在通知listeners的過程中,是否有新的listener被添加進來。如果有,25,26行得到所有新添加的listener並清空listeners屬性,13-17行繼續通知新添加的listener。否則,運行22,23行結束通知過程。

 1     private void notifyListeners0(DefaultFutureListeners listeners) {
 2         GenericFutureListener<?>[] a = listeners.listeners();
 3         int size = listeners.size();
 4         for (int i = 0; i < size; i ++) {
 5             notifyListener0(this, a[i]);
 6         }
 7     }
 8 
 9     @SuppressWarnings({ "unchecked", "rawtypes" })
10     private static void notifyListener0(Future future, GenericFutureListener l) {
11         try {
12             l.operationComplete(future);
13         } catch (Throwable t) {
14             logger.warn("An exception was thrown by " + l.getClass().getName() + ".operationComplete()", t);
15         }
16     }

  1-7行,notifyListeners0對每個listener調用一次notifyListener0,參數是當前的future。

  10-16,調用listener的operationComplete方法,捕獲了所有的異常,確保接下來可以繼續通知下一個listener。

 

使用await機制同步等待結果

  可以使用一系列的await,awaitXXX方法同步等待結果。這些方法可以分為: 能被打斷的,不能被打斷的。一直等待的,有超時時間的。await0方法是最複雜的等待實現,所有帶超時時間的await方法都會調用它。

 1     private boolean await0(long timeoutNanos, boolean interruptable) throws InterruptedException {
 2         if (isDone()) {
 3             return true;
 4         }
 5 
 6         if (timeoutNanos <= 0) {
 7             return isDone();
 8         }
 9 
10         if (interruptable && Thread.interrupted()) {
11             throw new InterruptedException(toString());
12         }
13 
14         checkDeadLock();
15 
16         long startTime = System.nanoTime();
17         long waitTime = timeoutNanos;
18         boolean interrupted = false;
19         try {
20             for (;;) {
21                 synchronized (this) {
22                     if (isDone()) {
23                         return true;
24                     }
25                     incWaiters();
26                     try {
27                         wait(waitTime / 1000000, (int) (waitTime % 1000000));
28                     } catch (InterruptedException e) {
29                         if (interruptable) {
30                             throw e;
31                         } else {
32                             interrupted = true;
33                         }
34                     } finally {
35                         decWaiters();
36                     }
37                 }
38                 if (isDone()) {
39                     return true;
40                 } else {
41                     waitTime = timeoutNanos - (System.nanoTime() - startTime);
42                     if (waitTime <= 0) {
43                         return isDone();
44                     }
45                 }
46             }
47         } finally {
48             if (interrupted) {
49                 Thread.currentThread().interrupt();
50             }
51         }
52     }

  這個方法返回的條件有: (1)isDone()==true;(2)允許被打斷(interrupted==true)的情況下被打斷;(3)已經超時。2-12行分別檢查了這3種情況。

  25,35行管理waiters屬性,這個屬性用來記錄當前正在等待的線程數。inWaiters方法正常情況下會把waiters加1,當檢查到waiters==Short.MAX_VALUE時會拋出異常,防止過多的線程等待。

  27行,調用wait等待,經歷waitTime后超時返回。在等待過程中,會被setValue0方法調用notifyAll喚醒。

  29-33行,處理被打斷的異常,如果運行被打斷,在30行拋出這個異常返回。

  38-45行,不論什麼原因線程被喚醒,檢查是否滿足返回條件,如果不滿足,繼續循環等待。

  沒有超時的wait方法實現要簡單一些,只需判讀返回條件(1)(2)。

 

跟蹤異步操作的執行進度

  如果想要跟蹤異步操作的執行進度,future需要換成DefaultProgressivePromise對象,listener需要換成GenericProgressiveFutureListener類型。DefaultProgressivePromise派生自DefaultPromise同時實現了ProgressivePromise接口。GenericProgressiveFutureListener接口派生自GenericFutureListener接口。

  ProgressivePromise定義了setProgress和tryProgress方法用來更新進度,是不是很眼熟,和Promise接口定義返回結果的方法很類似。

ProgressivePromise<V> setProgress(long progress, long total);
boolean tryProgress(long progress, long total);

  GenericProgressiveFutureListener定義了operationProgressed方法用來處理進度更新通知。

     void operationProgressed(F future, long progress, long total) throws Exception;

  

  DefaultProgressivePromise自己只實現了setProgress和tryProgress方法,其它都是復用了DefaultPromise的實現。

 1     @Override
 2     public ProgressivePromise<V> setProgress(long progress, long total) {
 3         if (total < 0) {
 4             // total unknown
 5             total = -1; // normalize
 6             if (progress < 0) {
 7                 throw new IllegalArgumentException("progress: " + progress + " (expected: >= 0)");
 8             }
 9         } else if (progress < 0 || progress > total) {
10             throw new IllegalArgumentException(
11                     "progress: " + progress + " (expected: 0 <= progress <= total (" + total + "))");
12         }
13 
14         if (isDone()) {
15             throw new IllegalStateException("complete already");
16         }
17 
18         notifyProgressiveListeners(progress, total);
19         return this;
20     }

  3-12行,檢查progress和total的合法性。

  14行,如isDone()==true,拋出異常。只有在操作還沒完成的是否更新進度才有意義。

  18行,調用notifyProgressiveListeners觸發進度更新通知,這個方法在DefaultPromise中實現。

  notifyProgressiveListeners實現了觸發進度更新通知的主要流程:

 1     void notifyProgressiveListeners(final long progress, final long total) {
 2         final Object listeners = progressiveListeners();
 3         if (listeners == null) {
 4             return;
 5         }
 6 
 7         final ProgressiveFuture<V> self = (ProgressiveFuture<V>) this;
 8 
 9         EventExecutor executor = executor();
10         if (executor.inEventLoop()) {
11             if (listeners instanceof GenericProgressiveFutureListener[]) {
12                 notifyProgressiveListeners0(
13                         self, (GenericProgressiveFutureListener<?>[]) listeners, progress, total);
14             } else {
15                 notifyProgressiveListener0(
16                         self, (GenericProgressiveFutureListener<ProgressiveFuture<V>>) listeners, progress, total);
17             }
18         } else {
19             if (listeners instanceof GenericProgressiveFutureListener[]) {
20                 final GenericProgressiveFutureListener<?>[] array =
21                         (GenericProgressiveFutureListener<?>[]) listeners;
22                 safeExecute(executor, new Runnable() {
23                     @Override
24                     public void run() {
25                         notifyProgressiveListeners0(self, array, progress, total);
26                     }
27                 });
28             } else {
29                 final GenericProgressiveFutureListener<ProgressiveFuture<V>> l =
30                         (GenericProgressiveFutureListener<ProgressiveFuture<V>>) listeners;
31                 safeExecute(executor, new Runnable() {
32                     @Override
33                     public void run() {
34                         notifyProgressiveListener0(self, l, progress, total);
35                     }
36                 });
37             }
38         }
39     }

  第3行,從listeners中選出GenericProgressiveFutureListener類型的listener。

  10-38行。調用notifyProgressiveListeners0, notifyProgressiveListener0通知進度跟新。11-17行,在當前線程中調用。

  19-37行,在executor中調用。notifyProgressiveListener0隻是簡單地調用listener的operationProgressed方法。notifyProgressiveListeners0是對每個listener調用一次notifyProgressiveListener0。

  和完成通知相比,進度更新通知要更加簡單。進度更新通知沒有處理併發問題,沒有處理棧溢出問題。

  

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!