標籤: 網頁設計公司

  • LeetCode 75,90%的人想不出最佳解的簡單題

    LeetCode 75,90%的人想不出最佳解的簡單題

    本文始發於個人公眾號:TechFlow,原創不易,求個關注

    今天是LeetCode專題的44篇文章,我們一起來看下LeetCode的75題,顏色排序 Sort Colors。

    這題的官方難度是Medium,通過率是45%,點贊2955,反對209(國際版數據),從這份數據上我們大概能看得出來,這題的難度不大,並且點贊遠遠高於反對,說明題目的質量很不錯。事實上也的確如此,這題足夠簡單也足夠有趣,值得一做。

    題意

    給定一個n個元素的數組,數組當中的每一個元素表示一個顏色。一共有紅白藍三種顏色,分別用0,1和2來表示。要求將這些顏色按照大小進行排序,返回排序之後的結果。

    要求不能調用排序庫sort來解決問題。

    桶排序

    看完題目應該感受到了,如果沒有不能使用sort的限制,這題毫無難度。即使加上了限制難度也不大,我們既然不能調用sort,難道還不能自己寫個sort嗎?Python寫個快排也才幾行而已。

    自己寫sort當然是可以的,顯然這是下下策。因為元素只有3個值,互相之間的大小關係也就只有那麼幾種,排序完全沒有必要。比較容易想到,我們可以統計一下這三個數值出現的次數,幾個0幾個1幾個2,我們再把這些數拼在一起,還原之前的數據不就可以了嗎?

    這樣的確可行,但實際上這也是一種排序方案,叫做基數排序,也稱為桶排序,還有些地方稱為小學生排序(大概是小學生都能懂的意思吧)。基數排序的思想非常簡單,我們創建一個數組,用它的每一位來表示某個元素是否在原數組當中出現過。出現過則+1,沒出現過則一直是0。我們標記完原數組之後,再遍歷一遍標記的數組,由於下標天然有序,所以我們就可以得到排序之後的結果了。

    如果你還有些迷糊也沒有關係,我們把代碼寫出來就明白了,由於這題讓我們提供一個inplace的方法,所以我們在最後的時候需要對nums當中的元素重新賦值。

    class Solution:
        def sortColors(self, nums: List[int]) -> None:
            """  Do not return anything, modify nums in-place instead.  """
            bucket = [0 for _ in range(3)]
            for i in nums:
                bucket[i] += 1
    
            ret = []
            for i in range(3):
                ret += [i] * bucket[i]
    
            nums[:] = ret[:]
    

    和排序相比,我們只是遍歷了兩次數據,第一次是遍歷了原數組獲得了其中0,1和2的數量,第二次是將獲得的數據重新填充回原數組當中。相比於快排或者是其他一些排序算法的耗時,桶排序只遍歷了兩次數組,明顯要快得多。但遺憾的是這並不是最佳的方法,題目當中明確說了,還有隻需要遍歷一次原數組的方法。

    two pointers

    在我們介紹具體的算法之前,我們先來分析一下問題。既然顏色只有三種,那麼當我們排完序之後,整個數組會被分成三個部分,頭部是0,中間是1,尾部是2。

    我們可以用一個區間來收縮1的範圍,假設我們當前區間的首尾元素分別是l和r。當我們讀到0的時候,我們就將它和l交換,然後將l向後移動一位。當我們讀到2的時候,則將它和r進行交換,將r向左移動一位。也就是說我們保證l和r之間的元素只有1。

    我們之前曾經介紹過這種維護一個區間的做法,雖然都是維護了一個區間,但是操作上是有一些區別的。之前介紹的two pointers算法,也叫做尺取法,本質上是通過移動區間的右側邊界來容納新的元素,通過移動左邊界彈出數據的方式來維護區間內所有元素的合法性。而當前的做法中,一開始獲得的就是一個非法的區間,我們通過元素的遍歷以及區間的移動,最後讓它變得合法。兩者的思路上有一些細微的差別,但形式是一樣的,就是通過移動左右兩側的邊界來維護或者是達到合法。

    class Solution:
        def sortColors(self, nums: List[int]) -> None:
            """  Do not return anything, modify nums in-place instead.  """
            l, r = 0, len(nums)-1
            i = 0
            while i < len(nums):
                if i > r:
                    break
       # 如果遇到0,則和左邊交換
                if nums[i] == 0:
                    nums[l], nums[i] = nums[i], nums[l]
                    l += 1
                # 如果遇到2,則和右邊交換
                # 交換之後i需要-1,因為可能換來一個0
                elif nums[i] == 2:
                    nums[r], nums[i] = nums[i], nums[r]
                    r -= 1
                    continue
                i += 1
    

    這種方法我們雖然只遍歷了數組一次,但是由於交換的次數過多,整體運行的速度比上面的方法還要慢。所以遍歷兩次數組並不一定就比只遍歷一次要差,畢竟兩者都是的算法,相差的只是一個常數。遍歷的次數只是構成常數的部分之一。

    除了這個方法之外,我們還有其他維護區間的方法。

    維護區間

    接下來要說的方法非常巧妙,我個人覺得甚至要比上面的方法還有巧妙。

    我們來假想一下這麼一個場景,假設我們不是在原數組上操作數據,而是從其中讀出數據放到新的數組當中。我們先不去想應該怎麼擺放這個問題,我們就來假設我們原數組當中的數據已經放好了若干個,那麼這個時候的新數組會是什麼樣?顯然,應該是排好序的,前面若干個0,中間若干個1,最後若干個2。

    那麼問題來了,假設這個時候我們讀到一個0,那麼應該怎麼放呢?為了簡化敘述我們把它畫成圖:

    我們假設藍色部分是0,綠色部分是1,粉色部分是2。a是0最右側的下標,b是1部分最右側的下標,c是2部分最右側的下標。那麼這個時候,當我們需要放入一個0的時候,應該怎麼辦?

    我們結合圖很容易想明白,我們需要把0放在a+1的位置,那麼我們需要把後面1和2的部分都往右側移動一格,讓出一格位置出來放0。我們移動數組顯然帶來的開銷會過於大,實際上沒有必要移動整個部分,只需要移動頭尾元素即可。比如1的部分左側被0佔掉了一格,那麼為了保持長度不變,右側也需要延伸一格。同理,2的部分右側也需要延伸一格。那麼整個操作用代碼來表示就是:nums[a+1] = 0,nums[b+1] = 1, nums[c+1] = 2。

    假設我們讀入的數是1,那麼我們需要把b延長一個單位,但是這樣帶來的後果是2的部分被侵佔,所以需要將2也延長,補上被1侵佔的一個單位。如果讀到的是2,那麼直接延長2即可,因為2後面沒有其他顏色了。

    假設我們有一個空白的數組,我們可以這麼操作,但其實我們沒有必要專門創建一個數組,我們完全可以用原數組自己填充自己。因為我們從原數組上讀取的數和擺放的數是一樣的,我們直接把数字擺放在原數組的頭部,佔用之前讀取的數即可。

    光說可能還有些迷糊,看下代碼馬上就清楚了:

    class Solution:
        def sortColors(self, nums: List[int]) -> None:
            """  Do not return anything, modify nums in-place instead.  """
            # 記錄0,1和2的末尾位置
            zero, one, two = -1, -1, -1
            n = len(nums)
            for i in range(n):
                # 如果擺放0
                # 那麼1和2都往後平移一位,讓一個位置出來擺放0
                if nums[i] == 0:
                    nums[two+1] = 2
                    nums[one+1] = 1
                    nums[zero+1] = 0
                    zero += 1
                    one += 1
                    two += 1
                elif nums[i] == 1:
                    nums[two+1] = 2
                    nums[one+1] = 1
                    one += 1
                    two += 1
                else:
                    nums[two+1] = 2
                    two += 1
    

    總結

    到這裏,這道題的解法基本上都講完了。

    相信大家也都看出來了,從難度上來說這題真的不難,相信大家都能想出解法來,但是要想到最優解還是有些困難的。一方面需要我們對題目有非常深入的理解,一方面也需要大量的思考。這類題目沒有固定的解法,需要我們根據題目的要求以及實際情況自行設計解法,這也是最考驗思維能力以及算法設計能力的問題,比考察某個算法會不會的問題要有意思得多。

    希望大家都能從這題當中獲得樂趣,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

    本文使用 mdnice 排版

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • 詳解 Seata Golang 客戶端 AT 模式及其使用

    詳解 Seata Golang 客戶端 AT 模式及其使用

    源碼seata-golang

    概述

      我們知道 Seata Java Client 的 AT 模式,通過代理數據源,實現了對業務代碼無侵入的分佈式事務協調機制,將與 Transaction Coordinator (TC) 交互的邏輯、Commit 的邏輯、Rollback 的邏輯,隱藏在切面和代理數據源相應的代碼中,使開發者無感知。那如果這個方法,要用 Golang 來實現一遍,應該如何操作呢?關於這個問題,我想了很久,最初的設想是,對 database/sql 的 mysql driver 進行增強,在對包 github.com/go-sql-driver/mysql 研究了一段時間后,還是沒有頭緒,不知如何下手,最後轉而增強 database/sql 包。由於 AT 模式必須保證本地事務的正確處理,在具體業務開發時,首先要通過 db.Begin() 獲得一個 Tx 對象,然後再 tx.Exec() 執行數據庫操作,最後 tx.Commit() 提交或 tx.Rollback() 回滾。這種處理方式算是一個 Golang 數據庫事務處理的基本操作。 所以對 database/sql 的增強,我們重點關注這幾個方法 db.Begin()tx.Exec()tx.Commit()tx.Rollback

    事務提交、回滾

      通過 Seata Java Client 的相關代碼,我們知道,在本地事務提交的時候,主要是將分支事務註冊到 TC 上,並將數據庫操作產生的 undoLog 一起寫入到 undoLog 表;本地事務回滾的時候,需要將分支事務(即本地事務)的執行狀態報告給 TC,使 TC 好知道是否通知參与全局事務的其他分支回滾。

    func (tx *Tx) Commit() error {
            //註冊分支事務
    	branchId,err := tx.register()
    	if err != nil {
    		return errors.WithStack(err)
    	}
    	tx.tx.Context.BranchId = branchId
    
    	if tx.tx.Context.HasUndoLog() {
                    //將 undoLog 寫入 undoLog 表
    		err = manager.GetUndoLogManager().FlushUndoLogs(tx.tx)
    		if err != nil {
    			err1 := tx.report(false)
    			if err1 != nil {
    				return errors.WithStack(err1)
    			}
    			return errors.WithStack(err)
    		}
    		err = tx.tx.Commit()
    		if err != nil {
    			err1 := tx.report(false)
    			if err1 != nil {
    				return errors.WithStack(err1)
    			}
    			return errors.WithStack(err)
    		}
    	} else {
    		return tx.tx.Commit()
    	}
    	if tx.reportSuccessEnable {
    		tx.report(true)
    	}
    	tx.tx.Context.Reset()
    	return nil
    }
    

      db.Begin() 會產生一個 Tx 對象,tx.Exec() 會產生 undoLog,tx.Commit() 將 undoLog 刷到數據庫中。那麼 undoLog 保存到哪裡呢?答案是 TxContext 中。

    type TxContext struct {
    	*context.RootContext
    	Xid string
    	BranchId int64
    	IsGlobalLockRequire bool
    
    	LockKeysBuffer *model.Set
    	SqlUndoItemsBuffer []*undo.SqlUndoLog
    }
    

      Commit() 方法中的 tx.tx.Context,第一個 tx 是封裝的 Tx 對象,第二個 tx 是 database/sql 的 Tx,tx.tx.Context 則是 TxContext。UndoLogManager 則是操作 undoLog 的核心對象,處理 undoLog 的插入、刪除,並查詢出 undoLog 用於回滾。

    func (tx *Tx) Rollback() error {
    	err := tx.tx.Rollback()
    	if tx.tx.Context.InGlobalTransaction() && tx.tx.Context.IsBranchRegistered() {
                    // 報告 TC 分支事務執行失敗
    		tx.report(false)
    	}
    	tx.tx.Context.Reset()
    	return err
    }
    

      通過上面的代碼呢,我們知道增強型 Tx 對象需要向 TC 註冊分支事務,並報告分支事務的執行狀態,相應代碼如下:

    func (tx *Tx) register() (int64,error) {
    	return dataSourceManager.BranchRegister(meta.BranchTypeAT,tx.tx.ResourceId,"",tx.tx.Context.Xid,
    		nil,tx.tx.Context.BuildLockKeys())
    }
    
    func (tx *Tx) report(commitDone bool) error {
    	retry := tx.reportRetryCount
    	for retry > 0 {
    		var err error
    		if commitDone {
    			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
    				meta.BranchStatusPhaseoneDone,nil)
    		} else {
    			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
    				meta.BranchStatusPhaseoneFailed,nil)
    		}
    		if err != nil {
    			logging.Logger.Errorf("Failed to report [%d/%s] commit done [%t] Retry Countdown: %d",
    				tx.tx.Context.BranchId,tx.tx.Context.Xid,commitDone,retry)
    			retry = retry -1
    			if retry == 0 {
    				return errors.WithMessagef(err,"Failed to report branch status %t",commitDone)
    			}
    		}
    	}
    	return nil
    }
    

      和 TC 進行通信的主要邏輯還是在 DataSourceManager 裏面。AT 模式涉及的兩個關鍵對象 DataSourceManager、UndoLogManager 就浮出水面。一個用於遠程 TC 交互,一個用於本地數據庫處理。

    事務執行

    func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
    	var parser = p.New()
            // 解析業務 sql
    	act,_ := parser.ParseOneStmt(query,"","")
    	deleteStmt,isDelete := act.(*ast.DeleteStmt)
    	if isDelete {
    		executor := &DeleteExecutor{
    			tx:            tx.tx,
    			sqlRecognizer: mysql.NewMysqlDeleteRecognizer(query,deleteStmt),
    			values:        args,
    		}
    		return executor.Execute()
    	}
    
    	insertStmt,isInsert := act.(*ast.InsertStmt)
    	if isInsert {
    		executor := &InsertExecutor{
    			tx:            tx.tx,
    			sqlRecognizer: mysql.NewMysqlInsertRecognizer(query,insertStmt),
    			values:        args,
    		}
    		return executor.Execute()
    	}
    
    	updateStmt,isUpdate := act.(*ast.UpdateStmt)
    	if isUpdate {
    		executor := &UpdateExecutor{
    			tx:            tx.tx,
    			sqlRecognizer: mysql.NewMysqlUpdateRecognizer(query,updateStmt),
    			values:        args,
    		}
    		return executor.Execute()
    	}
    
    	return tx.tx.Tx.Exec(query,args)
    }
    

      執行業務 sql,並生成 undoLog 的關鍵,在於識別業務 sql 執行了什麼操作:插入?刪除?修改?這裏使用 tidb 的 sql parser 去解析業務 sql,再使用相應的執行器去執行業務 sql,生成 undoLog 保存在 Tx_Context 中。

    事務開啟

      db.Begin() 返回增強型的 Tx 對象。

    func (db *DB) Begin(ctx *context.RootContext) (*Tx,error) {
    	tx,err := db.DB.Begin()
    	if err != nil {
    		return nil,err
    	}
    	proxyTx := &tx2.ProxyTx{
    		Tx:         tx,
    		DSN:        db.conf.DSN,
    		ResourceId: db.GetResourceId(),
    		Context:    tx2.NewTxContext(ctx),
    	}
    	return &Tx{
    		tx: proxyTx,
    		reportRetryCount: db.conf.ReportRetryCount,
    		reportSuccessEnable: db.conf.ReportSuccessEnable,
    	},nil
    }
    

    seata-golang at 模式的使用

    sample 代碼

    • 首先執行 scripts 腳本,初始化數據庫
      如果之前沒有初始化過 seata 數據庫,先執行 seata-golang/scripts/server/db/mysql.sql 腳本
    • 修改 dsn 數據庫配置,修改下列文件:
    seata-golang/tc/app/profiles/dev/config.yml
    seata-golang/samples/at/product_svc/conf/client.yml
    seata-golang/samples/at/product_svc/conf/client.yml
    
    • 將下列文件中的 configPath 修改為 client.yml 配置文件的路徑
    seata-golang/samples/at/product_svc/main.go
    seata-golang/samples/at/order_svc/main.go
    seata-golang/samples/at/aggregation_svc/main.go
    
    • 依次運行 tc、order_svc、product_svc、aggragation_svc,訪問下列地址開始測試:
    http://localhost:8003/createSoCommit
    http://localhost:8003/createSoRollback
    

    TC 啟動參考參与 Seata 社區到 go 與 Seata 的邂逅

    seata-golang 後續安排

      接下來不打算再增加新的 feature。Java 版 Seata 畢竟發展了一年多時間,並且有很多社區成員一起維護,Go 版本目前主要是我在開發,時間不到2個月,現有的代碼,僅是完成了框架,還需要大量優化,改bug,後續的工作重心在於使 seata-golang 穩定運行,生產可用,希望對分佈式事務感興趣且對 Go 感興趣的同學一起加入進來,一起做些事情。進入微信群,請加我微信:scottlewis,備註進群。

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

    【其他文章推薦】

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

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • Kubernetes日誌的6個最佳實踐

    Kubernetes日誌的6個最佳實踐

    本文轉自Rancher Labs

    Kubernetes可以幫助管理部署在Pod中的上百個容器的生命周期。它是高度分佈式的並且各個部分是動態的。一個已經實現的Kubernetes環境通常涉及帶有集群和節點的幾個系統,這些系統託管着幾百個容器,而這些容器不斷地基於工作負載啟動、毀滅。

    當在Kubernetes中處理大量的容器化應用和工作負載時,主動進行監控和調試錯誤十分重要。在容器、節點或集群級別,這些錯誤都能在容器中看到。Kubernetes的日誌機制是一個十分重要的組件,可以用來管理和監控服務以及基礎設施。在Kubernetes中,日誌可以讓你跟蹤錯誤甚至可以調整託管應用程序的容器的性能。

    配置stdout(標準輸出)和stderr(標準錯誤)數據流

    圖片來源:kubernetes.io

    第一步是理解日誌是如何生成的。通過Kubernetes,日誌會被發送到兩個數據流——stdout和stderr。這些數據流將寫入JSON文件,並且此過程由Kubernetes內部處理。你可以配置將哪個日誌發送到哪個數據流中。而一個最佳實踐的建議是將所有應用程序日誌都發送到stdout並且所有錯誤日誌都發送到stderr。

    決定是否使用Sidecar模型

    Kubernetes建議使用sidecar容器來收集日誌。在這一方法中,每個應用程序容器將有一個鄰近的“streaming容器”,該容器將會將所有日誌流傳輸到stdout和stderr。Sidecar模型可以幫助避免在節點級別公開日誌,並且它可以讓你控制容器級別的日誌。

    然而,這一模型的問題是它能夠適用於小容量的日誌記錄,如果面對大規模的日誌記錄,可能會造成大量資源被佔用。因此,你需要為每個正在運行的應用程序容器單獨運行一個日誌容器。在Kubernetes文檔中,將sidecar模型形容為“幾乎沒有很大的開銷”。需要由你決定是否嘗試這一模型並在選擇它之前查看它所消耗的資源類型。

    替代方法是使用日誌代理,該代理在節點級別收集日誌。這樣可以減少開銷,並確保安全地處理日誌。Fluentd已成為大規模聚合Kubernetes日誌的最佳選擇。它充當Kubernetes與你要使用Kubernetes日誌的任意數量的端點之間的橋樑。你也可以選擇像Rancher這樣的Kubernetes管理平台,在應用商店已經集成了Fluentd,無需從頭開始安裝配置。

    確定Fluentd可以更好地匯總和路由日誌數據后,下一步就是確定如何存儲和分析日誌數據。

    選擇日誌分析工具:EFK或專用日誌記錄

    傳統上,對於以本地服務器為中心的系統,應用程序日誌存儲在系統中的日誌文件中。這些文件可以在定義的位置看到,也可以移動到中央服務器。但是對於Kubernetes,所有日誌都發送到磁盤上/var/log的JSON文件中。這種類型的日誌聚合併不安全,因為節點中的Pod可以是臨時的也可以是短暫的。刪除Pod時,日誌文件將丟失。如果你需要嘗試對部分日誌數據丟失進行故障排除時,這可能很難。

    Kubernetes官方推薦使用兩個選項:將所有日誌發送到Elasticsearch,或使用你選擇的第三方日誌記錄工具。同樣,這裏存在一個潛在的選擇。採用Elasticsearch路線意味着你需要購買一個完整的堆棧,即EFK堆棧,包括Elasticsearch、Fluentd和Kibana。每個工具都有其自己的作用。如上所述,Fluentd可以聚合和路由日誌。Elasticsearch是分析原始日誌數據並提供可讀輸出的強大平台。Kibana是一種開源數據可視化工具,可以從你的日誌數據創建漂亮的定製dashboard。這是一個完全開源的堆棧,是使用Kubernetes進行日誌記錄的強大解決方案。

    儘管如此,有些事情仍然需要牢記。Elasticsearch除了由名為Elastic的組織構建和維護,還有龐大的開源社區開發人員為其做貢獻。儘管經過大量的實踐檢驗,它可以快速、強大地處理大規模數據查詢,但在大規模操作時可能會出現一些問題。如果採用的是自我管理(Self-managed)的Elasticsearch,那麼需要有人了解如何構建大規模平台。

    替代方案是使用基於雲的日誌分析工具來存儲和分析Kubernetes日誌。諸如Sumo Logic和Splunk等工具都是很好的例子。其中一些工具利用Fluentd來將日誌路由到他們平台,而另一些可能有它們自己的自定義日誌代理,該代理位於Kubernetes中的節點級別。這些工具的設置十分簡單,並且使用這些工具可以花費最少的時間從零搭建一個可以查看日誌的dashboard。

    使用RBAC控制對日誌的訪問

    在Kubernetes中身份驗證機制使用的是基於角色訪問控制(RBAC)以驗證一個用戶的訪問和系統權限。根據用戶是否具有特權(authorization.k8s.io/decision )並向用戶授予原因(authorization.k8s.io/reason ),對在操作期間生成的審核日誌進行註釋。默認情況下,審核日誌未激活。建議激活它以跟蹤身份驗證問題,並可以使用kubectl進行設置。

    保持日誌格式一致

    Kubernetes日誌由Kubernetes架構中不同的部分生成。這些聚合的日誌應該格式一致,以便諸如Fluentd或FluentBit的日誌聚合工具更易於處理它們。例如,當配置stdout和stderr或使用Fluentd分配標籤和元數據時,需要牢記這一點。這種結構化日誌提供給Elasticsearch之後,可以減少日誌分析期間的延遲。

    在日誌收集守護進程上設置資源限制

    由於生成了大量日誌,因此很難在集群級別上管理日誌。DaemonSet在Kubernetes中的使用方式與Linux類似。它在後台運行以執行特定任務。Fluentd和filebeat是Kubernetes支持的用於日誌收集的兩個守護程序。我們必須為每個守護程序設置資源限制,以便根據可用的系統資源來優化日誌文件的收集。

    結 論

    Kubernetes包含多個層和組件,因此對其進行良好地監控和跟蹤能夠讓我們在面對故障時從容不迫。Kubernetes鼓勵使用無縫集成的外部“Kubernetes原生”工具進行日誌記錄,從而使管理員更輕鬆地獲取日誌。文章中提到的實踐對於擁有一個健壯的日誌記錄體繫結構很重要,該體繫結構在任何情況下都可以正常工作。它們以優化的方式消耗計算資源,並保持Kubernetes環境的安全性和高性能。

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

    ※回頭車貨運收費標準

  • Action的三種實現方式,struts.xml配置的詳細解釋及其簡單執行過程(二)

    Action的三種實現方式,struts.xml配置的詳細解釋及其簡單執行過程(二)

    勿以惡小而為之,勿以善小而不為————————–劉備

    勸諸君,多行善事積福報,莫作惡

    上一章簡單介紹了Struts2的’兩個蝴蝶飛,你好’ (一),如果沒有看過,請觀看上一章

    一 Action的三種實現方式

    上一章開發的HelloAction和HelloAction2,並沒有繼承任何類或者實現任何接口,但是必須有一個execute() 方法,方法返回值是String類型。

    這樣的代碼不容易理解,更並不能使人看得出這個類是干什麼的,甚至不能區分這個控制器類與普通的Java類有什麼區別,通常開發中不這樣做。

    我們開發者在開發Struts2框架的時候,希望自己寫的這個Action類能夠具有易理解性,且已經支持某些功能,如參數接收,文件上傳等。

    一.一 第一種實現方式(普通Java類,裏面只包含execute()方法)

    package com.yjl.web.action;
    import org.apache.log4j.Logger;
    /**
    * @author 兩個蝴蝶飛
    * @version 創建時間:2018年8月23日 上午9:41:32
    * @description 第一種實現方式,普通java類,
    * 有一個execute()方法,也可以多寫幾個方法,用action中的標籤method來控制,可以正常訪問。
    */
    public class Hello1Action {
    	private static Logger logger=Logger.getLogger(Hello1Action.class);
    	public String execute() {
    		logger.info("兩個蝴蝶飛,web層你好");
    		return "success";
    	}
    }
    

    不具有開發時要求的規範性,且不支持某些struts2自身提供的功能。

    方法名稱只有一個 execute()

    一.二 第二種實現方式(實現Action接口)

    package com.yjl.web.action;
    import com.opensymphony.xwork2.Action;
    /**
    * @author 兩個蝴蝶飛
    * @version 創建時間:2018年8月23日 上午10:54:03
    * @description 第二種實現方式,實現Action接口,重寫裏面的execute()方法
    * 有一個execute()方法和五個String類型的常量
    */
    public class Hello2Action implements Action{
    	@Override
    	public String execute() throws Exception {
    		return Action.SUCCESS;
    		//return Action.ERROR;
    		//return Action.LOGIN;
    		//return Action.NONE;
    		//return Action.INPUT;
    	}
    }
    

    注意,Action接口是xwork2包下的接口。

    實現了Action接口,使開發者能夠看出來這是一個Action,具有了一定程度上的開發規範,

    但是實現了Action接口,所以必須要重寫execute()方法。

    一般自己寫Action,構思好之後上來就直接add(), edit(), delete(). select() 這些業務方法,

    每次都要重寫execute()方法,不太方便。 而且這種方式不具有struts2中某些功能,如驗證框架和國際化。

    Action中接口中有五個常用的結果字符串(好多方法都返回success,error,login,input,none,故將其封裝了一下) .

    這些字符串雖然是大寫,然而真實的值是全部小寫.

    package com.opensymphony.xwork2;
    
    public abstract interface Action
    {
      public static final String SUCCESS = "success";
      public static final String NONE = "none";
      public static final String ERROR = "error";
      public static final String INPUT = "input";
      public static final String LOGIN = "login";
      
      public abstract String execute()
        throws Exception;
    }
    

    一.三 繼承ActionSupport類(官方推薦)

    	package com.yjl.web.action;
    	import com.opensymphony.xwork2.ActionSupport;
    	/**
    	* @author 兩個蝴蝶飛
    	* @version 創建時間:2018年8月23日 上午11:04:20
    	* @description 第三種方式,繼承ActionSupport類。
    	* ActionSupport類實現了Action接口,也有Action中的五個常量.
    	*/
    	public class Hello3Action extends ActionSupport{
    		public String list() {
    			return "list";
    		}
    	}
    
    

    繼承了ActionSupport類,不需要重新寫execute()方法,直接寫業務方法即可。

    ActionSupport類,已經實現了 Action接口。 其具備Action中的五個常量,並且該類還實現了其他接口,

    源代碼:

    	public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable{
        ...
    	public String execute() throws Exception
      	{
    		//默認返回的是 success 字符串 
      		 return "success";
     	}
     	...
    }
    

    如驗證框架(Validateable,ValidationAware),國際化(LocaleProvider)。

    以後開發中,使用 繼承 ActionSupport 類的形式。

    二 配置文件 struts.xml中節點的詳細解釋

    在src下有一個struts.xml的配置文件,它配置了開發者自己編寫實現的Action,是struts2框架的核心,不能改變文件名稱。(注意,是struts.xml,並不是struts2.xml,並沒有那個2)。

    在struts.xml中,最上面是一個約束, 是一個根節點。

    二.一 修改常量節點

    在struts-core.jar核心包下,有一個包org.apache.struts2包下,有一個default.properties屬性文件,裏面記錄了很多常用的常量,

    其中常見的有:

    struts.i18n.encoding=UTF-8 
    struts.multipart.maxSize=2097152
    struts.action.extension=action,,
    struts.enable.DynamicMethodInvocation = false
    struts.devMode = false
    struts.ui.theme=xhtml
    struts.ognl.allowStaticMethodAccess=false
    

    建議修改后的值為:

    ###國際化操作,編碼格式為UTF-8
    struts.i18n.encoding=UTF-8
    ###上傳文件時最大的上傳大小,默認為2M. 根據項目情況具體填寫值,建議後面加兩個00
    struts.multipart.maxSize=209715200
    ###struts的訪問後綴名, struts1框架默認的是 .do 
    struts.action.extension=action,,
    ###struts是否可以訪問靜態方法
    struts.enable.DynamicMethodInvocation =true
    ###struts是否是開發者模式
    struts.devMode =true
    ###struts中ui標籤的主題,建議為simple
    struts.ui.theme=simple
    ###ognl中是否可以訪問靜態方法,為true
    struts.ognl.allowStaticMethodAccess=true
    

    可以在struts.xml中進行相應的修改,如

     <!--修改國際化編碼 -->
    <constant name="struts.i18n.encoding" value="UTF-8"></constant>
    <!--修改是否為開發者模式 -->
    <constant name="struts.devMode" value="true"></constant>
    

    按照name,value值的形式進行填寫。

    也可以在src下新建一個struts.properties,然後將這些值放置進去,struts也會自動struts.propeties中的常量值的。

    也可以在web.xml中,在 中,以 局部參數的形式傳遞進去。

    建議使用第一種方式,在struts.xml中用 ,畢竟這個文件常常打開,出錯了也容易發現。

    二.二 分模塊開發

    在實際的項目中,有很多的模塊,如果所有的配置都放在一個struts.xml,那麼一旦這個struts.xml被其他人誤操作導致了錯誤,那麼其他人的項目將無法運行的,當配置內容過多時,struts.xml的內容太長,不便於維護,所以最好是分模塊開發,一個模塊用一個配置文件,然後再利用 進行導入, 類似 於jsp中的 靜態包含一樣。

    所以建議每一個模塊都寫一個模塊.xml,然後在struts.xml中引入即可。如有三個模塊 User模塊和Class,Course,那麼可以將User的配置放置在user.xml中,Class配置放置在class.xml中,course模塊放置在course.xml,在struts.xml中只需要

    	<include file="user.xml"></include>
    	<include file="class.xml"></include>
    	<include file="course.xml"></include>
    

    靜態包含即可。 注意,file的文件路徑引用是否輸入正確。

    正確的位置引用,點擊ctrl+模塊.xml時,可以跳轉到相應的.xml文件中。如果沒有跳轉和反應,那說明位置引用錯誤,需要重新檢查一下。

    二.三 包節點

    在struts.xml配置文件中,最重要的節點就是package節點。 package,分包。 可以將action進行分包處理。

    這樣每一個action或者每一組action用package進行隔開,便於維護,類似於java中package的概念。

    二.三.一 <package> 節點的使用

    <package name="hello" extends="struts-default" namespace="/">
            <!--具體的Action-->
    </package>
    

    package中name節點是package的名字,是獨一無二的,不能夠重複。 最好與模塊名相同或者起一個有意義的名稱。

    extends節點表示繼承,即package之間可以相互的繼承,來避免重複化功能的編寫。 默認為struts-default。

    struts-default中struts已經定義了很多功能,開發者自己寫的包只需要extends 這個包名struts-default,

    就擁有了struts已經定義好的功能。 如攔截器功能,文件上傳功能。

    用戶也可以自己繼承自己所寫的包 。如父包名為

    那麼子包只需要 , 這樣child包不但擁有struts-default的功能,也擁有parent包中的特殊功能,這也是Java的多重繼承的體現。 所以package的name 要符合標識符的規範,具有可讀性。

    namespace節點表示命名空間,以/開頭,默認是”/” 。是為了在訪問路徑和訪問請求url方面體現package的分包作用. package中的name是在配置文件中體現分包,namespace是在url中體現分包。 建議開發中,namespace的路徑名與name保持一致。 package中的namespace的值與子節點action中name的值,共同構成了完整的訪問請求路徑。

    二.三.二 <package></package> 子節點<action></action>節點的使用

    在Hello3Action中定義兩個方法,一個是list()查詢,一個是add()添加的方法。

    package com.yjl.web.action;
    import org.apache.log4j.Logger;
    import com.opensymphony.xwork2.ActionSupport;
    /**
    * @author 兩個蝴蝶飛
    * @version 創建時間:2018年8月23日 上午11:04:20
    * @description 測試action標籤中method的方法訪問
    */
    public class Hello3Action extends ActionSupport{
    	private static final long serialVersionUID = 8737138848863458260L;
    	Logger logger=Logger.getLogger(Hello3Action.class);
    	public String list() {
    		logger.info("執行list方法");
    		return "list";
    	}
    	public String add() {
    		logger.info("執行add方法");
    		return "add";
    	}
    }
    
    

    標籤,有三個基本的屬性,

    	<action name="list" class="com.yjl.web.action.Hello3Action"
            method="list">
    
    </action>
    

    其中name為action的名字,表示區別一個package包下的不同的action。 其中這個name的值,不應該隨便取,應該是要訪問的方法名。

    在瀏覽器客戶端請求的url為 /項目名/package的namespace名稱/action的name名稱.action;

    class為要訪問的那個Action的全限定名稱,是class,用.(點)進行分隔。

    其中,class 可以省略, 省略默認為 ActionSupport 類, 全限定名稱為: com.opensymphony.xwork2.ActionSupport
    method為要訪問的那個方法名稱,類 extends ActionSupport 后,有很多很多的方法,如list(), add(), delete()等,那麼怎麼知道具體要訪問哪個方法呢? 用method這個屬性. method=”要方法的方法名” ,是方法名。

    action還有一個節點是converter,表示所用的是哪一個類型轉換器。(後面會有相應的解釋)

    很清楚, action 中的 class指定了訪問的是哪一個action, method 指定了訪問的是哪一個具體的方法, 利用了反射技術實現。

    在本實例了有兩個方法,所以要進行寫兩個Action, 一個Action類中會有多個方法,難道要一個個配置多個Action嗎?

    Struts2提供了一些簡單的方式

    二.三.三 配置Action的三種形式

    二.三.三.一 通過配置method的屬性完成

    簡單舉例如下:

    	<action name="list" class="com.yjl.web.action.Hello3Action"
    		method="list">
    			
    	</action>
      <action name="add" class="com.yjl.web.action.Hello3Action"
    		method="add">
                
        </action>
    

    缺點: 有幾個方法,就要配置有幾個action,當方法過多時,不易維護。

    二.三.三.二 通過配置 通配符完成。

    簡單舉例如下:

    	<action name="Hello3_*" class="com.yjl.web.action.Hello3Action"
    		method="{1}">
    			
    		</action>
    

    name的值為: 類簡寫名(去掉Action后)_* method中的值取第一個{1},從1開始,不是從0開始。

    這樣訪問Hello3Action中的list方法,訪問路徑就是 Hello3_list

    訪問Hello3Action中的add方法,訪問路徑就是Hello3_add

    簡化了action的相關配置。

    也有的人配置的更狠, 會配置成_, 即:

    	<action name="*_*" class="com.yjl.web.action.{1}Action"
    		method="{2}">
    			
    		</action>
    

    User類中的list就是User_list, User類中的add就是User_add,

    Class類中的list就是Class_list,Class類中的add就是Class_add

    這樣雖說簡化了開發,但卻不利用 result 節點的維護 ,不建議這樣配置。

    好多類的好多方法返回值,都寫在這一個action 下面,會亂。

    二.三.三.三 動態方法訪問

    不是用 * 通配符,而是用! 號。 即:

    想訪問UserAction中list方法() 前端寫url為 userAction!list.action
    想訪問UserAction中add方法() 前端寫url為 userAction!add.action
    想訪問ClassAction中list方法() 前端寫url為 classAction!list.action
    想訪問ClassAction中add方法() 前端寫url為 classAction!add.action

    這樣訪問也特別的方便。

    這樣的話, action中只需要配置name和class即可。 method已經由外部指定了,不需要寫method的值了。

    需要先添加變量 struts.enable.DynamicMethodInvocation, 使其變成 true,開啟。

    	<constant name="struts.enable.DynamicMethodInvocation" value="true"></constant>
    

    如果是UserAction的話,配置應該是:

    <action name="userAction" class="com.yjl.web.action.UserAction" >
    			
    </action>
    

    ClassAction的話,配置應該是

    <action name="classAction" class="com.yjl.web.action.ClassAction" >
    			
    </action>
    

    二.三.四 action子節點result的配置

    result表示結果,是對方法的返回值進行相應的分析。有兩個屬性,name和type

    	<result name="success" type="dispatcher">/index.jsp</result>
    

    其中name的值要與方法的返回值保持一致。

    如 list方法返回值是return SUCCESS,那麼這個list方法的返回值對應的result的值就是 ,

    如果返回是”hello”, 那麼這個name的返回值就是

    如果在action中配置通配符, name=Hello3_*形式,method=”{1}”, 那麼為了簡化result的配置,可以將result配置成 name={1},

    相應的.jsp,可以變成 /{1}.jsp。

    但這樣必須保證Action中方法的名稱與返回值的名稱相同,並且與跳轉到的jsp的名稱也要相同, 這樣不太好。

    result中type五種常見的形式, dispatcher(轉發到jsp),redirect(重定向到jsp), chain(轉發到另外一個方法),redirectAction(重定向到另外一個方法),stream(上傳和下載流)

    其中dispathcer和redirect是跳轉到jsp,如果想要傳遞數據,用dispather,

    如果不想傳遞數據,用redirect (dispathcer是轉發,redirect是重定向)

    chain,redirectAction是跳轉到action的操作,一般用於這同一個類中兩個方法之間的跳轉,

    如add()添加成功之後,需要跳轉到list()方法進行显示結果,這時就可以配置成:

    	<result name="add" type="redirectAction">Hello3_list</result>
    

    地址url也會相應的改變,如果是chain的話,地址欄是不會改變的。 chain是轉發到action, redirectAction是重定向到action.

    也可以在不同包之間的action進行的跳轉 。

    如 add 方法 想到跳轉到 /class 命名空間下的 Hello2Action 的 list 方法。

    <result name="add" type="redirectAction">
    	<!-- 要跳轉到哪一個命名空間,即哪一個包 -->
    	<param name="namespace">/class</param>
    	<!-- 要跳轉到哪一個Action 不加後綴 -->
    	<param name="actionName">Hello2Action</param>
    	<!-- 跳轉到哪一個方法 -->
    	<param name="method">list</param>
    	<!-- 可能要傳遞的參數. 用ognl表達式,根據情況添加 -->
    	<param name="id">${id}</param>
    </result>
    

    通過 param 標籤來配置帶參還是不帶參。

    二.四 全局結果頁面與局部結果頁面。

    這個全局是相對於package來說的,是package中的全局,並不是所有的struts.xml中的全局,所以全局結果的節點位置應該放在package節點裏面,與action節點平行。 用 節點。

    常用的全局結果頁面有兩種:

    error錯誤頁面,頁面出錯了都显示這個頁面,

    login 登錄頁面, 如果沒有登錄,輸入任何url都會跳轉到login頁面(認證時用)

    noprivilege 沒有權限頁面,如果用戶沒有權限訪問了某一個頁面,會給出相應的提示(授權時用)

    <global-results>
    			<result name="error">/error/error.jsp</result>
    			<result name="login">/login.jsp</result>
                <result name="noprivilege">/noprivilege.jsp</result>
    </global-results>
    

    當全局結果頁面與局部結果頁面發生衝突時,以局部結果頁面為準。

    全局配置時:

    <global-results>
    			<result name="success">/successGlobal.jsp</result>
    </global-results>
    

    在該包下的某個action 的方法result 也返回了 success

    	<result name='success'>success.jsp</result>
    

    那麼,當邏輯視圖為 success時,最終將返回 success.jsp

    二.五 配置跳轉頁面

    在開發中,常常有這麼一種情況,

    請求login.jsp 時,為 /login, 那麼就跳轉到 login.jsp 頁面,

    語法為 register.jsp 時,為 /register, 那麼就跳轉到 register 頁面。

    這個時候,配置 為:

    	<action name="*">
    			<result>/WEB-INF/content/{1}.jsp</result>
    	</action>
    

    將頁面放置在 content 文件夾下面,避免用戶直接訪問 jsp頁面。

    注意,要將此 action 放置在最後, 當所有上面的action都不匹配時,才匹配這一個action.

    三 Struts2的執行流程

    當用戶在客戶端發送一個請求后,如常用的標準的http://localhost:8080/Struts_Hello/user/User_add.action時,

    會經過前端控制器(StrutsPrepareAndExecuteFilter) 過濾器,執行一連串的過濾器鏈,然後根據user 找到了對應的package的namespape,進入到具體的package包下。 利用通配符的方式進行訪問,User_add會進行匹配相應的action,根據class和method找到是哪一個類的哪一個方法,在實例化類Action之前,會先執行攔截器。通過反射實例化類,運行方法, 方法運行成功之後,有一個返回值,這個返回值會與剛才action下的 中的name進行相應的匹配,匹配到哪一個,就執行哪一個result。 如果是diapatcher或者redirect,就显示到相應的.jsp頁面(帶有數據), 如果是chain或者redirectAction,那麼就去執行那一個方法,之後進行返回具體的視圖。

    執行過程圖如下:

    謝謝您的觀看!!!

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

    【其他文章推薦】

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    ※別再煩惱如何寫文案,掌握八大原則!

  • 程序員實用JDK小工具歸納,工作用得到

    程序員實用JDK小工具歸納,工作用得到

    在JDK的安用裝目錄bin下,有一些有非常實用的小工具,可用於分析JVM初始配置、內存溢出異常等問題,我們接下來將對些常用的工具進行一些說明。

    JDK小工具簡介

    在JDK的bin目錄下面有一些小工具,如javac,jar,jstack,jstat等,在日常編譯運行過程中有着不少的“額外”功能,那麼它們是怎麼工作的呢?雖然這些文件本身已經被編譯成可執行二進制文件了,但是其實它們的功能都是由tools.jar這個工具包(配合一些dll或者so本地庫)完成的,每個可執行文件都對應一個包含main函數入口的java類(有興趣可以閱讀openJDK相關的源碼,它們的對應關係如下(更多可去openJDK查閱):

    javac com.sun.tools.javac.Main
    jar sun.tools.jar.Main
    jps sun.tools.jps.Jps
    jstat sun.tools.jstat.Jstat
    jstack    sun.tools.jstack.JStack
    ...

    tools.jar的使用

    我們一般開發機器上都會安裝JDK+jre,這時候,要用這些工具,直接運行二進制可執行文件就行了,但是有時候,機器上只有jre而沒有JDK,我們就無法用了么?

    如果你知道如上的對應關係的話,我們就可以”構造”出這些工具來(當然也可以把JDK安裝一遍,本篇只是介紹另一種選擇),比如我們編寫

    //Hello.java
    public class Hello{
        public static void main(String[] args)throws Exception{
            while(true){
                test1();
                Thread.sleep(1000L);
            }
        }
        public static void test1(){
            test2();
        }
        public static void test2(){
            System.out.println("invoke test2");
        }
    }

    可以驗證如下功能轉換關係

    1.編譯源文件:

    javac Hello.java => java -cp tools.jar com.sun.tools.javac.Main Hello.java

    結果一樣,都可以生成Hello.class文件
    然後我們開始運行java -cp . Hello

    2.查看java進程:

    jps => java -cp tools.jar sun.tools.jps.Jps

    結果一樣,如下:

    4615 Jps
    11048 jar
    3003 Hello

    3.動態查看內存:

    jstat -gcutil 3003 100 3 => java -cp tools.jar sun.tools.jstat.Jstat -gcutil 3003 100 3

    發現結果是一樣的

      S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
      0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
      0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
      0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000

    4.查看當前運行棧信息
    正常情況,執行如下命令結果也是一樣,可以正常輸出

    jstack 3003 =》 java -cp tools.jar sun.tools.jstack.JStack 3003

    但是有的jre安裝不正常的時候,會報如下錯誤

    Exception in thread "main" java.lang.UnsatisfiedLinkError: no attach in java.library.path

    這是因為jstack的運行需要attach本地庫的支持,我們需要在系統變量裏面配置上其路徑,假如路徑為/home/JDK/jre/bin/libattach.so
    命令轉換成

    jstack 3003 =》 java -Djava.library.path=/home/JDK/jre/bin -cp tools.jar sun.tools.jstack.JStack 3003

    就可以實現了
    在linux系統中是libattach.so,而在windows系統中是attach.dll,它提供了一個與本機jvm通信的能力,利用它可以與本地的jvm進行通信,許多java小工具就可能通過它來獲取jvm運行時狀態,也可以對jvm執行一些操作

    attach使用

    1. 編寫agent.jar代理包

    • 編寫一個Agent類
    //Agent.java
    public class Agent{
        public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
            System.out.println("agent : " + args);
        }
    }
    • 編譯Agent
    java -cp tools.jar com.sun.tools.javac.Main Agent.java
    //或者
    javac Agent.java
    • 再編manifest.mf文件
    //manifest.mf
    Manifest-Version: 1.0
    Agent-Class: Agent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    • 把Agent.class和manifest.mf進行打包成agent.jar
    java -cp tools.jar sun.tools.jar.Main -cmf manifest.mf agent.jar Agent.class
    //或者
    jar -cmf manifest.mf agent.jar Agent.class

    2.attach進程

    • 編寫如下attach類,編譯並執行
    //AttachMain.java
    public class AttachMain {
        public static void main(String[] args) throws Exception {
            com.sun.tools.attach.VirtualMachine vm = com.sun.tools.attach.VirtualMachine.attach(args[0]);
            vm.loadAgent("agent.jar", "inject params");
            vm.detach();
        }
    }
    • 編譯:
    java -cp tools.jar com.sun.tools.javac.Main -cp tools.jar AttachMain.java
    //或者
    javac -cp tools.jar AttachMain.java
    • 執行attach
    java -cp .:tools.jar AttachMain 3003
    • 查看Hello進程有如下輸出:
    invoke test2
    invoke test2
    invoke test2
    invoke test2
    invoke test2
    invoke test2
    invoke test2
    agent : inject params
    invoke test2

    說明attach成功了,而且在目標java進程中引入了agent.jar這個包,並且在其中一個線程中執行了manifest文件中agentmain類的agentmain方法,詳細原理可以見JVMTI的介紹,例如oracle的介紹

    3. 用attach製作小工具

    • 寫一個使進程OutOfMemory/StackOverFlow的工具
      有了attach的方便使用,我們可以在agentmain中新起動一個線程(為避免把attach線程污染掉),在裏面無限分配內存但不回收,就可以產生OOM或者stackoverflow
      代碼如下:
    //Agent.java for OOM
    public class Agent{
        public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
            new Thread() {
                @Override
                public void run() {
                    java.util.List<byte[]> list = new java.util.ArrayList<byte[]>();
                    try {
                        while(true) {
                            list.add(new byte[100*1024*1024]);
                            Thread.sleep(100L);
                        }
                    } catch (InterruptedException e) {
                    }
                }
            }.start();
        }
    }
    //Agent.java for stackoverflow
    public class Agent{
        public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
            new Thread() {
                @Override
                public void run() {
                    stackOver();
                }
                private void stackOver(){
                    stackOver();
                }
            }.start();
        }
    }

    當測試OOM的時候,hello進程的輸出為:

    invoke test2
    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
            at Agent$1.run(Agent.java:9)
    invoke test2
    invoke test2
    invoke test2

    說明發生OOM了, 但是OOM線程退出了,其它線程還在正常運行。

    如果我們需要進程在OOM的時候產生一些動作,我們可以在進程啟動的時候增加一些OOM相關的VM參數

    • OOM的時候直接kill掉進程:-XX:OnOutOfMemoryError=”kill -9 %p”
      結果如下:
    invoke test2
    invoke test2
    #
    # java.lang.OutOfMemoryError: Java heap space
    # -XX:OnOutOfMemoryError="kill -9 %p"
    #   Executing /bin/sh -c "kill -9 26829"...
    Killed
    • OOM的時候直接退出進程:-XX:+ExitOnOutOfMemoryError
      結果如下:
    invoke test2
    invoke test2
    Terminating due to java.lang.OutOfMemoryError: Java heap space
    • OOM的時候進程crash掉:-XX:+CrashOnOutOfMemoryError
      結果如下:
    invoke test2
    invoke test2
    Aborting due to java.lang.OutOfMemoryError: Java heap space
    invoke test2#
    # A fatal error has been detected by the Java Runtime Environment:
    #
    #  Internal Error (debug.cpp:308)
    , pid=42675, tid=0x00007f3710bf4700
    #  fatal error: OutOfMemory encountered: Java heap space
    #
    # JRE version: Java(TM) SE Runtime Environment (8.0_171-b11) (build 1.8.0_171-b11)
    # Java VM: Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode linux-amd64 compressed oops)
    # Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
    #
    # An error report file with more information is saved as:
    # /root/hanlang/test/hs_err_pid42675.log
    #
    # If you would like to submit a bug report, please visit:
    #   http://bugreport.java.com/bugreport/crash.jsp
    #
    Aborted
    • OOM的時候dump內存:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
      結果生成dump文件

    asm的應用

    1.asm使用原理

    asm是一個java字節碼工具,提供一種方便的函數/屬性級別修改已經編譯好的.class文件的方法, asm的簡單使用原理介紹如下:

    • 通過ClassReader讀取.class文件的字節碼內容,並生成語法樹;
    • ClassReader的方法accept(ClassVisitor classVisitor, int parsingOptions)功能是讓classVisitor遍歷語法樹,默認ClassVisitor是一個代理類,需要有一個具體的實現在遍歷語法樹的時候做一些處理;
    • 用ClassWriter是ClassVisitor的一個實現,它的功能是把語法樹轉換成字節碼;
    • 通常我們會定義一個自己的ClassVisitor,可以重寫裏面的一些方法來改寫類處理邏輯,然後讓ClassWriter把處理之後的語法樹轉換成字節碼;

    2.下面是具體的實現步驟:

    • 引入asm依賴包
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm</artifactId>
        <version>7.0</version>
    </dependency>
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm-commons</artifactId>
        <version>7.0</version>
    </dependency>
    //或者引入如下包
    asm-commons-7.0.jar
    asm-analysis-7.0.jar
    asm-tree-7.0.jar
    asm-7.0.jar
    • 定義一個ClassVisitor,功能是在所有方法調用前和調用後分別通過System.out.println打印一些信息
      輸入為字節碼,輸出也是字節碼
    //MyClassVisitor.java
    public class MyClassVisitor extends ClassVisitor {
        private static final Type SYSTEM;
        private static final Type OUT;
        private static final Method PRINTLN;
        static {
            java.lang.reflect.Method m = null;
            try {
                m = PrintStream.class.getMethod("println", new Class<?>[] {String.class});
            } catch (Exception e) {
            }
            SYSTEM = Type.getType(System.class);
            OUT = Type.getType(PrintStream.class);
            PRINTLN = Method.getMethod(m);
        }
    
        private String cName;
    
        public MyClassVisitor(byte[] bytes) {
            super(Opcodes.ASM7, new ClassWriter(ClassWriter.COMPUTE_FRAMES));
            new ClassReader(bytes).accept(this, ClassReader.EXPAND_FRAMES);
        }
        String format(String name) {
            return name.replaceAll("<", "_").replaceAll("\\$|>", "");
        }
        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            cName = format(name);
            super.visit(version, access, name, signature, superName, interfaces);
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            if ((access & 256) != 0) {
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
            return new MyMethodAdapter(super.visitMethod(access, name, desc, signature, exceptions), access, name, desc);
        }
    
        public byte[] getBytes() {
            return ((ClassWriter) cv).toByteArray();
        }
    
        class MyMethodAdapter extends AdviceAdapter {
            private String mName;
    
            public MyMethodAdapter(MethodVisitor methodVisitor, int acc, String name, String desc) {
                super(Opcodes.ASM7, methodVisitor, acc, name, desc);
                this.mName = format(name);
            }
    
            @Override
            protected void onMethodEnter() {
                getStatic(SYSTEM, "out", OUT);
                push(cName + "." + mName + " start");
                this.invokeVirtual(OUT, PRINTLN);
            }
    
            @Override
            protected void onMethodExit(int opcode) {
                getStatic(SYSTEM, "out", OUT);
                push(cName + "." + mName + " end");
                this.invokeVirtual(OUT, PRINTLN);
            }
        }
    }
    • 定義一個簡單的classLoader來加載轉換后的字節碼
    //MyLoader.java
    class MyLoader extends ClassLoader {
        private String cname;
        private byte[] bytes;
        public MyLoader(String cname, byte[] bytes) {
            this.cname = cname;
            this.bytes = bytes;
        }
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class<?> clazz = null;
            if (clazz == null && cname.equals(name)) {
                try {
                    clazz = findClass(name);
                } catch (ClassNotFoundException e) {
                }
            }
            if (clazz == null) {
                clazz = super.loadClass(name, resolve);
            }
            return clazz;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            Class<?> clazz = this.findLoadedClass(name);
            if (clazz == null) {
                clazz = defineClass(name, bytes, 0, bytes.length);
            }
            return clazz;
        }
    }
    • 加載轉換Hello類,然後反向調用其方法

    //將如下main函數加入MyClassVisitor.java中

    public static void main(String[] args) throws Exception {
        try (InputStream in = Hello.class.getResourceAsStream("Hello.class")) {
            byte[] bytes = new byte[in.available()];
            in.read(bytes);
            String cname = Hello.class.getName();
            Class<?> clazz = new MyLoader(cname, new MyClassVisitor(bytes).getBytes()).loadClass(cname);
            clazz.getMethod("test1").invoke(null);
        }
    }
    • 編譯
    java -cp tools.jar com.sun.tools.javac.Main -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
    //或者
    javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
    • 運行
    java -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. MyClassVisitor
    //結果如下:
    Hello.test1 start
    Hello.test2 start
    invoke test2
    Hello.test2 end
    Hello.test1 end

    asm的使用很廣泛,最常用的是在spring aop裏面切面的功能就是通過asm來完成的

    3. 利用asm與Instrument製作調試工具

    • Instrument工具

    Instrument類有如下方法,可以增加一個類轉換器

    addTransformer(ClassFileTransformer transformer, boolean canRetransform)

    執行如下方法的時候,對應的類將會被重新定義

    retransformClasses(Class<?>... classes)
    • 與asm配合使用
      當我們修改Agent.java代碼為下面內容
    //Agent
    public class Agent {
        public static void agentmain(String args, Instrumentation inst) {
            try {
                URLClassLoader loader = (URLClassLoader)Agent.class.getClassLoader();
                Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                method.setAccessible(true);//代碼級引入依賴包
                method.invoke(loader, new File("asm-7.0.jar").toURI().toURL());
                method.invoke(loader, new File("asm-analysis-7.0.jar").toURI().toURL());
                method.invoke(loader, new File("asm-tree-7.0.jar").toURI().toURL());
                method.invoke(loader, new File("asm-commons-7.0.jar").toURI().toURL());
                inst.addTransformer(new ClassFileTransformer() {
                    @Override
                    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                        ProtectionDomain protectionDomain, byte[] bytes) {
                        return new MyClassVisitor(bytes).getBytes();
                    }
                }, true);
                inst.retransformClasses(Class.forName("Hello"));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    • 編譯並打包成agent.jar
    //編譯
    javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
    //打包
    jar -cmf manifest.mf agent.jar MyLoader.class MyClassVisitor.class MyClassVisitor\$MyMethodAdapter.class Agent.class Agent\$1.class
    • attach進程修改字節碼
    //執行
    java -cp .:tools.jar AttachMain 3003
    //執行前後Hello進程的輸出變化為
    invoke test2
    invoke test2
    invoke test2
    Hello.test1 start
    Hello.test2 start
    invoke test2
    Hello.test2 end
    Hello.test1 end
    Hello.test1 start
    Hello.test2 start
    invoke test2
    Hello.test2 end
    Hello.test1 end

    利用asm及instrument工具來實現熱修改字節碼現在有許多成熟的工具,如btrace(https://github.com/btraceio/btrace,jvm-sandbox https://github.com/alibaba/jvm-sandbox)

     

    點擊關注,第一時間了解華為雲新鮮技術~

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • NetAnalyzer筆記 之 十四 NetAnalyzer 6.0 的使用方法 — 3.協議分析與統計

    NetAnalyzer筆記 之 十四 NetAnalyzer 6.0 的使用方法 — 3.協議分析與統計

    數據分析

    完成了數據的抓取,那麼接下來就是NetAnalyzer的第二個重點部分了,協議分析作為整個軟件的核心之一,在最新的NetAnalyzer中已經得到了巨大的提升。NetAnalyzer中協議分析分為單數據包分析,和聯合分析兩種分析方式,對於聯合分析會根據不同的協議特性進行形成不同的分析方案,目前支持傳輸協議(TCP/UDP)協議分析, HTTP協議分析。在數據統計部分部分還增加了針對ARP協議的圖形化分析。對於協議分析,需要了解相關的網絡知識或是有相關專業背景支持。

    單數據包分析,在獲取到數據包后,軟件工作界面數據包列表框中會显示所獲取的所用數據包,並且對這次數據做了一些簡單的分析,我們可以憑藉這些數據簡單判斷所對應的的數據包類型。

     

    數據包列表

    當我們選中一行,即選中一個數據包,我們可以看到對該數據包詳細的數據分析信息,並一樹狀結構樹呈現出來,並在右側显示該數據包原始信息。當我們選中協議樹中一個字段時,右側的數據就會定位到當前字端所分析數據的位置。

     

    數據分析

    然後通過對應的協議格式進行匹配與分析,如這部分的IP協議。

     

    IPv4協議格式

    需要注意的是,NetAnalyzer目前對於選中的字段只能精確到字節層次,對於一些協議,其中一個字節可能包含了多個字段,或是跨字節的字段,則會選擇全部的字節數據,比如IPv6協議。

     

    IPv6協議格式

    其中的版本字段只佔用了4bit(1字節為8bit),通信類型佔了8bit 也就是1字節,但是因為其中前面部分使用了版本字段所在字節後面的4bit,所以改字段為一個典型的跨字節字段,同樣流標籤字段使用了20bit,佔用第二個字節的4bit加上後面自身的2個字節(16bit)。

     

    解析后的IPv6數據

    對於類型的字段因為NetAnalyzer使用十六進制显示數據,並不能清晰表達bit層次的信息所以當選定字段后默認選中改字段所在的字節,如點擊版本選中方式如下,

     

    IPv6版本信息

    選中通信類型和流標籤則呈現方式如下。

     

    通信類型和流標籤共用數據

    數據分析標籤

    雖然NetAnalyzer盡可能多分析每個數據包所包含的信息,但是依舊存在很多數據需要我們手動去解析。所以軟件增加了數據標籤。

     

    數據分析

    數據標籤頁點擊 显示 按鈕 就可以打開數據轉換窗口,當然也可以在常規轉換中點擊任意功能可以打開轉換窗口

     

    轉換窗口管理

     

    關閉按鈕為關閉轉換窗口,清空則是清空當前窗口內的數據。

    點擊清空按鈕,則清空轉換信息。

     

    常規轉換工具

    NetAnalyzer中提供了一部分簡單的轉換功能,這些功能只有在載荷數據被選中的情況的才可以啟用,

    如點擊二進制按鈕,則對所選的數據轉換為對應的二進制字符串。如下圖所示。

     

    常規數據轉換窗口

    除了一些簡單的轉換功能,還集成了MangoScript擴展方式和插件擴展方式(無可用插件的

    時候不显示)的轉換。

     

    擴展MangoScript的解析

    如下面通過MangoScript針對某即時通信軟件的數據分析。

    針對於MangoScript和插件兩種方式的轉換,將會在在《NetAnalyzer使用說明書 二 擴展與開發》中詳細說明,此處不再贅述。

     

     

    定位轉換功能需要配合常規轉換進行使用,有時候我們確定某個字節會在一個確定的位置出現,比如IP地址字段,我們選中該位置,位置字段就會出現一串代碼 (10,1) [26]-4

    (x,y)[offset] – length

     x: 十六進制編輯器水平方向的偏移量

     y :   十六進制編輯器垂直方向的偏移量

    offset : 字節偏移量,offset = y * 16 + x

    length :  當前選擇的數據長度

     

    數據轉換

    所以代碼 (10,1) [26]-4 確定了當前IP地址的位置,此時點擊 常規轉換 -> IPv4地址 則會在模式中記錄當前的轉換模式,然後點擊定位轉換,就會在當前數據包列表中針對每個數據包這個位置執行定位操作,這對於尋找所需要的數據非常重要。

     

    選擇了IPv4轉換

     

    執行定位轉換

    對於MangoScript和插件擴展依然支持定位轉換。

     

    區塊複製,主要是對一些已經選中的字節進行複製轉為代碼,字節數組,以及保存的功能,以及數據做手動分析,腳本分析以及自定義轉換等,後續將會說明,此處不再詳細介紹。

     

    數據塊操作

     

    字節定位,與定位轉換類似,但是字節定位主要是用來在數據包列表中查找相同位置出現相同字節序列的數據包。算作一個查找功能。

     

    字節定位

    分析標籤

    分析標籤下個功能依託於數據包列表,分別有載荷數據提取,數據包標記,編碼轉換,數據查找,統計等相關功能,是聯合分析的主要功能,下面將會着重對一下功能進行說明。

     

    數據分析標籤

    TCP/UDP協議分析   前面介紹的都是基於單包的數據分析,而在協議分析中,我們大部分分析的數據都是依託於TCP/UDP的長連接數據,這部分數據的特點就是有多個數據包通過tcp或udp相關協議完成數據重組后才可以使用(基於udp的連接數據可能不是很嚴格)。

    NetAnalyzer 除了提供基於單包的數據分析,更提供了基於連接數據的分析,而分析出來的數據不僅僅是在窗口上呈現一堆亂碼,更可以通過DocBar將獲取的數據提取出來進行使用。

    開始 標籤最後一部分就是基於長連接的分析。點擊TCP/UDP 按鈕

     

    基於TCP/UDP載荷數據查看

    此時NetAnalyzer便會切換到載荷數據模式(該過程可以通過配置,使用獨立窗口打開)。在該模式下會打開專有的載荷數據菜單,數據區域也會變為對於載荷數據的分析,這裏先介紹一個NetAnalyzer中的DocBar工具,如下圖

     

    DocBar

    在文本模式下,分析載荷數據會显示該工具條,該工具條會提供針對當前數據塊的各種操作,當然在不動情況下,显示的工具和數量,都有所不同,下面是對當前各個功能的說明。

    l   對當前數據塊進行摺疊

    l   選中當前的分析數據

    l   保存當前原始數據

    l   查看原始數據(bytes數據)

    l   MangScript解析數據

    l   手動測試數據

     

    對於其他情況下的工具在這裏不會一一介紹,但是碰到的時候會有說明,並且隨着後續功能點的增加,DocBar可能會有更多的功能添加進來。

     

    tcp/udp 的分析分為 文本模式原始模式 ,文本模式主要是用於分析載荷數據為文本的數據,我們可以通過下面兩種方式更改文本編碼方式,分析數據。

    文本模式下,呈現方式如下:

     

    查看載荷數據

    原始模式分析如下,可用通過TCP/UDP的下拉菜單命令 字節數據 切換為原始數據

     

     

    字節查詢方式

     

    字節方式呈現

    對於在該功能下針對TCP的所有數據都已經進行過TCP重組,所以最終分析完成的數據並不是按照數據包方式做簡單呈現就可以的,都會做數據的篩查與整理。如果需要單包分析的使用者需要注意一下。

     

    HTTP數據分析 http作為最有網絡代表意義的協議,NetAnalyzer提供了更加完善的分析,http基於tcp協議,所以數據還原等都建立在tcp數據還原的基礎之上。通過http分析,我們可以還原很多有意義的數據,如獲取到Http所傳輸的的html、js、css數據文件,還可以獲取到基於http協議分析得到的圖片,文件等信息,如下圖分別為還原后的圖片和zip壓縮包。

     

    http方式分析出的圖片

     

    http方式分析出的文件

    對於常規的字符串或圖片可以直接在NetAnalyzer呈現,但是對於其他類型的文件,如視頻、音樂、以及上面提到的zip壓縮包文件,在在NetAnalyzer會簡單显示為二進制數據,該數據如果過長,則會截斷显示,但是在後面會加入【全部數據】下鑽選項,當點擊該數據后則會打開原始數據對話框,並且會完整显示當前的數據,如下圖所示。

     

    查看原始數據

    原始數據對話框中,提供了簡單的數據另存為和數據識別相關的功能。

     

    原始數據保存

    保存 保存當前窗口中的數據為一個文件。

    保存選擇數據 是當選擇對話框中其中的一段數據保存為文件,有時候數據可能存在偏差,或者我們需要提取選定的數據保存為文件,可以通過下拉保存選定的數據進行保存。

    數據識別功能。

    轉為… 則是將當前的數據轉到編碼轉換工具中進行進一步分析。

    自動識別 為了更加快速的實現數據提取,NetAnalyzer增加了數據識別模塊,通過整理不同文件的頭部或尾部字節形成數據識別特徵,當進行自動識別的時候,可以快速定位字節。

     

    文件識別

    添加特徵 將選定的指定字節添加為文件識別頭,並且添加相關信息,形成一個特徵。

     

    添加文件識別

    識別管理 管理特徵庫,在後續將詳細介紹該功能點。

     

    載荷數據分析出的文件

    除了使用常規的識別方式,在載荷數據提取中也加入了數據識別功能。在使用的時候點擊數據識別就可以在下方显示被識別到的數據類型,有時候可能會存在多個類型和誤識別的情況,使用的時候請務必注意。

    有時候通過HTTP協議還原部分二進制數據,如下面還原ZIP文件,文檔會以二進制數據呈現,而我們可以通過0x50 0x4B(PK)推斷出該文件很有可能是zip文件 ,所以我們點擊全部數據 ,打開原始數據窗口,這部分數據正好是zip的全部數據。

       

    保存的zip文件內容

    此時點擊將當前數據保存為zip文件。減壓就可以看到對應的文件內容。

     

    在載荷數據模式下,菜單會自動切換為,載荷模式菜單

     

    載荷數據標籤

    該菜單下提供了很多常用的字符串轉換工具

     

    格式轉換工具

    如下面通過通過Cookie格式化,格式化了http頭中的cookie字段

     

    Cookie格式化

    需要注意的是使用這些字段首先需要選中被轉換的文本,然後點擊需對應的功能項。其中如果點擊轉換為…,則啟動NetAnalyzer附帶的編碼轉換工具,進行集中處理。

     

    編碼轉換工具

    針對html字符串數據,還提供了過濾標籤和HTML預覽功能,因為該部分功能都很類型,且使用簡單,用戶自行嘗試使用即可。

     

     

    時序圖 在數據分析中,除了對於數據本身的分析之外,有時候我們還要去評測一些數據質量等方面的內容。並且可以通過圖像化的方式表現出來。

     

    TCP時序圖分析

    時序圖模擬TCP/UDP在數據網絡中的數據傳輸過程,還原網絡通信場景,如該圖可以完整的反映TCP三次握手以及斷開連接四次揮手的情景。可以作為對當前分析數據從另外一個方面的反饋,更具有參考意義。

    點擊

     

    時序圖選項

    就可以看到針對於當前tcp/udp 數據交互的情況。

     

     

    數據標記

    在分析標籤下面,有標記功能,實現對當前採集會話數據連接的進行快速識別。

     

    數據標記

    NetAnalyzer提供了四中顏色對數據包鏈接進行區分。

    如TCP數據包,就會通過源IP地址+源端口地址+目標IP地址+目標端口 作為一個特徵來進行識別,此處的源和目標具有相對性。

    注*  ctrl+鼠標左鍵 可以實現對數據會話的快速標記 顏色為紅色

     

    標記完成的數據

    通過點擊清理標記,可還原數據。

     

     

    數據包查找 

     

    數據包查找

    在數據包列表模式下使用Ctrl+F即可以打開數據包查找功能。

    該功能主要是實現快速查找數據包的功能,可以通過編號,協議,地址(mac/ip),端口,關鍵字等五種方式查找數據包。還可以通過數據列表導航按鈕進行數據包列表瀏覽。

     

     

    編碼方式

    在通過TCP/UDP 或HTTP 功能還原數據的時候,有時候會出現亂碼,尤其是對非英文字符。在HTTP協議中通常都會在頭部信息中攜帶編碼方法,通過提取就可以獲取到編碼方式,但是仍然後部分服務並不提供編碼字段,這時候就需要我們通過手動切換,來嘗試還原相關信息。

    通過菜單欄或者是狀態欄都可以對編碼方案進行切換

     

    字符編碼

     

    狀態欄字符編碼

    這裏需要注意的是如果http頭部包含了編碼方式,則使用頭部提供的編碼方式。

     

     

    數據統計

    目前NetAnalyzer显示了大量的統計方式,涵蓋了數據報表、流量分析、主機通信矩,傳輸報告、ARP報告等多種統計方式。

     

    數據報表

     

    報表信息

    對當前捕獲的數據表中的數據進行統計與歸類。呈現方式如有圖所示。

     

    報表內容

    包含一些基本信息,數據量與時間直線圖,數據量佔比,關係圖等信息

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • 為什麼阿里巴巴Java開發手冊中不允許魔法值出現在代碼中?

    為什麼阿里巴巴Java開發手冊中不允許魔法值出現在代碼中?

    在閱讀《阿里巴巴Java開發手冊》時,發現有一條關於關於常量定義的規約,具體內容如下:

    圖中的反例是將數據緩存起來,並使用魔法值加鏈路 id 組成 key,這就可能會出現其他開發人員在複製粘貼的時候,少複製 _ 的情況發生,這種錯誤很難去檢查到,因為讀取緩存不存在,可能會去數據庫讀取,很難察覺到。

    如果在生產環境中,大量的請求進來,緩存全部失效,直接請求數據庫,導致數據庫連接過多,查詢效率變低的問題發生,因此看來魔法值確實應該避免出現在代碼中。

    另外在 《Clean Code》 和 《重構》 等書中也提到了類似的問題,在代碼中出現原始形態数字通常來說是壞現象,應該用命名良好的常量類隱藏它。

    靜態常量取代魔法值

    像下面這個例子:

    if (billCount > 75) {
        //todo
    } else {
        //todo
    }
    

    如果在不了解這塊的業務的同事,在讀到這塊代碼的時候,可能會想,75 是什麼鬼,為啥和這個數比較,背後深藏着什麼秘密嗎?可能只有當時的開發人員記得了,導致代碼可讀性和可維護性極差。

    如果聲明一個常量,來替換該魔法值,可能就會使代碼的可讀性和可維護性大大增加。

    static final Integer BASIC_BILL_COUNT = 75;
    

    還有些魔法表達式,比如:

    if (value > 60 && value <= 80 && type = 1) {
        // todo
    }
    

    比如這個表達式是表示狀態為正常且項目活躍,就可以定義:

    boolean isActiveProject = value > 60 && value <= 80 && type = 1;
    

    這樣是不是可讀性就提高了,一眼就可以看出來這塊代碼的邏輯。

    枚舉類取代魔法值

    還有一種消除魔法值的方式是使用枚舉類代替,下面讓我們舉個例子:

    if (eventId == 1) {
        System.out.println("睡覺");
    } else if (eventId == 2) {
        System.out.println("吃飯");
    } else if (eventId == 3) {
        System.out.println("打豆豆");
    }
    

    如上代碼是針對事件 id 去執行相應的事件,如果事件比較少,大家還可以勉強記住每個 eventId 對應的含義,但是隨着事件 id 的增多,很可能會發生,新來的員工把事件 id 給搞混了,導致執行錯誤的事件,發生 bug。

    那麼我們可以使用枚舉類來表示相應的事件:

    public enum EventEnum {
    
        /**
         * 睡覺
         */
        SLEEP_EVENT(1, "睡覺"),
    
        /**
         * 吃飯
         */
        EAT_EVENT(2, "吃飯"),
    
        /**
         * 打豆豆
         */
        FIGHT_PEA_EVENT(3, "打豆豆");
    
        private int eventId;
        private String desc;
    
        EventEnum(int eventId, String desc) {
            this.eventId = eventId;
            this.desc = desc;
        }
    
        public int getEventId() {
            return eventId;
        }
    
        public String getDesc() {
            return desc;
        }
    }
    

    修改完之後的代碼如下:

    if (eventId == EventEnum.SLEEP_EVENT.getEventId()) {
        System.out.println("睡覺");
    } else if (eventId == EventEnum.EAT_EVENT.getEventId()) {
        System.out.println("吃飯");
    } else if (eventId == EventEnum.FIGHT_PEA_EVENT.getEventId()) {
        System.out.println("打豆豆");
    }
    

    是不是可讀性急劇提升,還不快看看自己代碼中有沒有這樣的魔法值出現,有的話趕緊改造起來。

    還有如果你需要在不同的地點引用同一數值,魔法數會讓你煩惱不已,因為一旦這些数字發生改變,就必須在程序中找到所有的魔法值,並將它們全部修改一遍,這樣就太費時費力了。

    其實不只是 Java 不應該在代碼中使用魔法值,其他語言亦是如此。

    總結

    本文主要介紹了為什麼不允許在代碼中出現魔法值以及如何將代碼中已有的魔法值去除掉。

    代碼可讀性還是比較重要的,你肯定不希望別人在接手你的代碼的時候,罵到這数字啥意思,這代碼寫得跟粑粑一樣。

    最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

    參考

    《Java開發手冊》泰山版

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

    ※回頭車貨運收費標準

  • 最近面試遇到的種種應聘者,你是這樣的嗎?

    最近面試遇到的種種應聘者,你是這樣的嗎?

    原文鏈接

    很久沒有寫文章了,一時間竟不知如何開篇?為什麼沒有寫呢?是因為太忙了。最近在忙什麼呢?工作學習還有就是招人。上班時間不忙的時候大多是在看技術文章、技術文檔,上下班公交車上也是,還有就是最近兩個月在面試一些人。其實我是不太想面的,原因有三。一是耽誤我自己的時間,二是面了十幾個只有一兩個能讓我很稱心的。還有就是太費錢了公司又不給報銷,所以我最近都會用一些會議軟件來面試。

     

    昨天面試了一個2012年開始工作的30歲程序員,面試前我心裏打鼓,畢竟我才工作三年但是說實話面下來不太理想,首先簡歷寫的一般,簡歷排版格式有點亂,多處字體不一致,還有技術棧很老,項目很小大多是內部用的,沒用過Redis,分佈式相關的東西沒有,也沒自己去了解過項目之外的東西,其次面試問到的問題回答的不到一半,但是態度還是不錯的,臨了還問我面試情況,我說了我的感受,也給了一些建議。

     

    其實稍微看看他的簡歷,待過的公司,做過的項目也,就能知道為什麼工作七八年的30歲程序員水平這麼一般了。工作這麼多年一共待過兩個公司,看樣子都是外包公司,寫的幾個項目也大多數是內部使用的一些管理系統,併發量不大,沒有技術挑戰,對自己提升不高。

     

    那麼程序員如何突破自己呢?怎麼才能擺脫中年危機呢?首先要跳出舒適圈,人都是有惰性的,都喜歡安逸的活着,如果生活過得去,沒有太大的壓力,誰又願意再努力一把呢?但是成功往往屬於那些肯逼迫自己的人,肯走出舒適圈、有目標的人。即使是30多了也是可以拼一把的,如果你是該技術的,那也可以再把技術深造深造,搞的紮實一點;如果你已經考慮轉管理了,那你就往管理方面靠,多看看管理方面的書籍,有空再考個管理的證,但是技術你也不能落下,不要求你把技術搞的多精通,但最起碼你要知道這個技術,了解一下他的基本原理,要不然有一天你要你下屬引進一個技術,他告訴你太難要花好多時間,或者說搞不了,你都不知道他說的是真的還是假的,如果你相信了,那他以後背地里就笑話你不懂技術,那以後這樣的事情還會多着呢。

     

    另外不建議搞技術的過早的去轉管理,比如你剛工作3年,你的經理建議你去轉管理,這是不建議的,原因上面也說了,你的技術還不透徹,對技術的把控你完全不懂,到時候讓你評估一個技術引入的工作量,難度等,你搞不定的話又可能還會鬧出笑話。

     

     

     

    今天遇到一個應聘者,工作經歷三年,四個項目全都是管理類的、內部使用的項目,但是人家簡歷寫的技術都是熟悉啊,符合公司的招聘標準啊。OK,面吧,來唄。

     

    專業技能這塊寫的都是熟悉,我一看會這麼多還挺棒的GOOD BOY

     

     

     

     

    廢話不多說,上來我就問,Java基礎你掌握的熟練嗎?對方說還行吧,我就先問了幾個Java語法的概念,然後問了HashMap的put操作的流程、擴容機制,什麼時候擴容的?做什麼操作的時候會發生線程不安全?統統回答的不好。

     

    我:如果想使用線程安全的Map,用哪個?

    應聘者:ConcurrentHashMap

     

    我:ConcurrentHashMap怎麼保證線程安全的?

    應聘者:這個…我平時用的少,不太知道底層

     

    然後接着我就問什麼是Spring?對方的回答是Spring是一個框架,核心是AOP和IOC。這就回答完了。

     

    我:spring有什麼優點呢?

    應聘者:有 什麼優點?…嗯…這些概念性的東西我忘了…

     

    我:那你說一下什麼是Spring AOP,可以干什麼用?使用什麼技術實現的?

    應聘者:AOP就是面向切面編程,可以用來記錄日誌,安全管理,用動態代理實現的

     

    我:Spring AOP使用的哪種動態代理?

    應聘者:JDK動態代理,CGLIB動態代理

     

    我:什麼時候用JDK動態代理,什麼還是用CGLIB動態代理

    應聘者:它有一個判斷,好像是沒有繼承類時用JDK動態代理

     

    我:BeanFactory和ApplicationContext有什麼區別?

    應聘者:….我們項目spring用的很少,用的是springboot

     

    然後我簡單的問了幾個springboot的基礎問題,還都能回答上來,可以看出來確實用了springboot。

    看他簡歷上寫的熟悉spring cloud,我心想做這些管理系統還需要微服務嗎?就問他在哪個項目里用到了,他說沒用過,是自己自學過。

     

    因為我們也沒有這套技術,我就沒再問。

    我對MQ感興趣,就問他RabbitMQ的問題。

    我:使用RabbitMQ有什麼好處啊?

    應聘者:我們發郵件使用了RabbitMQ,往MQ里發郵件。

     

    我:為什麼要用RabbitMQ啊?(我問有什麼好處,他剛沒回答,我換個問法)

    應聘者:你是說為什麼不用別的MQ嗎?項目里用的就是RabbitMQ我就用了

     

    我:發郵件不用MQ也能實現,為什麼要引入MQ呢?有什麼好處嗎?

    應聘者:不用MQ也能實現嗎?我不知道,我們發郵件就是用RabbitMQ,我就用了。

    我:(跳過這個問題吧)那你能說一下RabbitMQ的消息是基於什麼傳輸的?

    應聘者:基於什麼傳輸?你這問的好官方啊,我不知道問的啥,你能問的通俗點嗎?

     

    我:(算了跳過)那你知道RabbitMQ它的消息怎麼路由嗎?

    應聘者:這個…我不太清楚,記不清了,上個項目用到了,好久沒有用了,但是我如果有項目要使用的話,基本上再看看就能很快上手了。

     

    我:哦,我看你技術寫的都是熟悉。那你Redis用的多嗎?

    應聘者:用的少,我買過視頻看過。

     

    我:那你說一下Redis的數據類型都有哪些?各自的使用場景

    應聘者:string,hash,list,set,zset

     

    我:(這就完了?明明問的還有使用場景呢)怎麼使用Redis實現分佈式鎖呢?

    應聘者:這…嗯…我不太清楚,項目中不怎麼用redis,都是內部使用的很少用redis

     

    我:redis有哪幾種架構模式啊?

    應聘者:架構模式…呃…不知道,對redis了解的不多。你問我點業務。(老是問我不會的,你問點業務啊?)

     

    (不多,你簡歷寫熟悉⊙﹏⊙b汗,還教我問你,你是面試官還是我是啊?你的項目有毛的業務)

     

    我:那你說一下你在項目中怎麼使用Spring security的

    應聘者:….

     

    我:那你講一下SSO的流程

    應聘者:….

     

    這幾個問題,我已經沒仔細在聽他回答的是什麼了

    最後又問了幾個問題,我已經不想問了,已經快四十分鐘了。

     

    我:我今天就這麼多問題,你有什麼要問我的嗎?

    應聘者:咱們公司是在北京嗎我看手機號是北京的(…等一些關於項目的幾個問題)

     

    這個應聘者存在一個什麼問題?眼高手低,高估自己,面試準備不充分。建議近期找工作的把Java基礎,JVM,集合,併發,數據庫,redis,框架,dubbo,zookeeper弄懂,準備充分,這樣才能百戰不殆,成為offer收割機。

     

    還有的應聘者問題回答的賊6,問道到在項目中怎麼使用的,哪些地方用到了,就卡殼了。很顯然這樣的就是簡單粗暴的背面試題,所以也要結合自己的項目去準備面試,把面試題嵌入到項目中,能說出在項目里哪些地方用到了,有什麼優點等,盡量準備充分。

     

    好了今天就分享到這裏,有什麼需要交流的歡迎留言哦~

     

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

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

  • Azure AD(四)知識補充-服務主體

    Azure AD(四)知識補充-服務主體

    一,引言

      又到了新的一周了,也到了我新的分享的時間了,還記得上一周立得Flag,其中 “保證每周輸出一篇文章” ,讓我特別“在意”(這裏用詞不太恰當)。主要是我的一個大學舍友,他突然問了我一個關於寫博的事情,自己也在上周開通了賬號,也想着堅持寫博客。在我看來,這確實是一件好事,寫博不僅僅是分享的過程;也是自己提煉寫博的一個過程,以及文章組織的能力,對自己還是很有好處的。這不僅僅要寫內容要精鍊,同時也要讓別人能看的懂。加油,默默的在這裏給他打氣。(ง •_•)ง

    好了,開始今天的分析

    ————————————我是分割線————————————

      上一周有介紹到Azure AD資源託管標識的內容,其實就包括如何去操作開啟系統分配的託管標識,以及通過開啟託管標識,VM如何去訪問Azure 中的一些資源,如 “Key Vault” 等。今天去分析一波關於“Service Principal”(服務主體)。

    二,正文

    1,服務主體對象

      若要訪問受 Azure AD 租戶保護的資源,需要訪問的實體必須由安全主體來表示。 這同時適用於用戶(用戶主體)和應用程序(服務主體)。安全主體定義 Azure AD 租戶中用戶/應用程序的訪問策略和權限。 這樣便可實現核心功能,如在登錄時對用戶/應用程序進行身份驗證,在訪問資源時進行授權。當應用程序被授予了對租戶中資源的訪問權限時(根據註冊或許可),將創建一個服務主體對象。 Microsoft Graph ServicePrincipal 實體定義服務主體對象屬性的架構。

    2,應用程序和服務主體的關係

    可以將應用程序對象視為應用程序的全局表示形式(供所有租戶使用),將服務主體視為本地表示形式(在特定租戶中使用)。

    應用程序對象用作模板,常見屬性和默認屬性從其中派生,以便在創建相應服務主體對象時使用。 因此,應用程序對象與軟件應用程序存在 1 對 1 關係,而與其對應的服務主體對象存在 1 對多關係。

    必須在將使用應用程序的每個租戶中創建服務主體,讓它能夠建立用於登錄和/或訪問受租戶保護的資源的標識。 單租戶應用程序只有一個服務主體(在其宿主租戶中),在應用程序註冊期間創建並被允許使用。 多租戶 Web 應用程序/API 還會在租戶中的某個用戶已同意使用它的每個租戶中創建服務主體。

    下圖演示了應用程序的應用程序對象和對應的服務主體對象之間的關係,其上下文是在名為 HR 應用的示例多租戶應用程序中。 此示例方案中有三個 Azure AD 租戶:

    • Adatum -開發HR 應用的公司使用的租戶
    • Contoso -contoso 組織使用的租戶,即HR 應用的使用者
    • Fabrikam -fabrikam 組織使用的租戶,它也使用HR 應用

    在此示例方案中:

    表 1

    步驟 說明
    1 是在應用程序的宿主租戶中創建應用程序對象和服務主體對象的過程。
    2 當 Contoso 和 Fabrikam 的管理員完成同意並嚮應用程序授予訪問權限時,會在其公司的 Azure AD 租戶中創建服務主體對象,並向其分配管理員所授予的權限。 另請注意,HR 應用可能配置/設計為允許由用戶同意以供個人使用。
    3 HR 應用程序的使用者租戶(例如 Contoso 和 Fabrikam)各有自己的服務主體對象。 每個對象代表其在運行時使用的應用程序實例,該實例受相關管理員同意的權限控制。

    3,使用Azure CLI創建Azure服務主體(示例)

    使用 az ad sp create-for-rbac 命令創建服務主體創建服務主體時,請選擇其使用的登錄身份驗證的類型。

     注意

    如果您的帳戶無權創建服務主體,將返回一條錯誤消息,其中包含“權限不足,無法完成操作”。請與您的Azure Active Directory管理員聯繫以創建服務主體。

    3.1,在 “azure portal” 中驗證當前的Azure訂閱

    az account show
    

    3.2,显示訂閱名稱ID值的列表

    az account list --query "[].{name:name, subscriptionId:id}"
    

     

    3.3,使用 az ad sp create-for-rbac 命令,將其替換<subscription_id>為要使用的訂閱帳戶的ID

    az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/<subscription_id>"

    注意:我們將創建一個具有 “Contributor” (貢獻者角色:默認角色)的服務主體。該 “Contributor” 角色具有完全的權限讀取和寫入到Azure的賬戶,

    成功完成后,該命令將显示幾個值,包括自動生成的密碼  

     

    同時,我們可以在 “azure portal” 中可以找到對應的設置 選擇=》Azure Active Directory

     

    點擊 “App registrations”

     

     同時,我們可以在當前訂閱下的 “IAM”中找到對應的角色訪問權限信息。當然了,上面我創建服務主體的時候給的 scope 是整個訂閱,也就是我們可以通過這個服務主體去訪問azure的任何資源。

    例如 “azure devops Pipeline” 在CD的過程,需要配置 “Service Principal”

     

     例如使用Terraform 構建基礎架構資源的時候,需要配置 Service Principal

    三,總結

      使用Azure服務的自動化工具應始終具有受限權限。Azure提供服務主體,而不是讓應用程序以完全特權用戶身份登錄。Azure服務主體是為與應用程序,託管服務和自動化工具一起使用而創建的身份,以訪問Azure資源。這種訪問受到分配給服務主體的角色的限制,使您可以控制可以訪問哪些資源以及可以訪問哪個級別。出於安全原因,始終建議將服務主體與自動化工具一起使用,而不是允許他們使用用戶身份登錄。

    服務主體的默認角色是Contributor該角色具有讀取和寫入Azure帳戶的完整權限

    參考資料:RBAC內置角色:https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles

    作者:Allen 

    版權:轉載請在文章明顯位置註明作者及出處。如發現錯誤,歡迎批評指正。

     

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

    台中搬家公司費用怎麼算?

  • 微前端與項目實施方案研究

    微前端與項目實施方案研究

    一、前言

    微前端(micro-frontends)是近幾年在前端領域出現的一個新概念,主要內容是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的小塊,而在用戶看來仍然是內聚的單個產品。微前端的理念源於微服務,是將龐大的整體拆成可控的小塊,並明確它們之間的依賴關係,而它的價值在於能將低耦合的代碼與組件進行組合,基座+基礎協議模式能接入大量應用,進行統一的管理和輸出,許多公司與團隊也都在不斷嘗試和優化相關解決技術與設計方案,為這一概念的落地和推廣添磚加瓦。結合自身遇到的問題,適時引用微前端架構能起到明顯的提效賦能作用。
    

    二、背景

    目前我司擁有大量的內部系統,這些系統採用相同的技術棧,在實際開發和使用過程中,逐漸暴露出如下幾個問題:
    

    1.有大量可復用的部分,雖然有組件庫,但是依賴版本難統一;
    2.靜態資源體積過大,影響頁面加載和渲染速度;
    3.應用切換目前是通過鏈接跳轉的方式實現,會有白屏和等待時長的問題,對用戶體驗不夠友好;
    針對上述幾個問題,決定採用微前端架構對內部系統進行統一的管理,本文也是圍繞微前端落地的技術預研方案。

    三、方案調研

    目前業界有多種解決方案,有各自的優缺點,具體如下:

    • 路由轉發:路由轉發嚴格意義上不屬於微前端,多個子模塊之間共享一個導航即可 簡單,易實現 體驗不好,切換應用整個頁面刷新;

    • 嵌套 iframe:每個子應用一個 iframe 嵌套 應用之間自帶沙箱隔離 重複加載腳本和樣式;

    • 構建時組合:獨立倉儲,獨立開發,構建時整體打包,合併應用 方便依賴管理,抽取公共模塊 無法獨立部署,技術棧,依賴版本必須統一;

    • 運行時組合:每個子應用獨立構建,運行時由主應用負責應用管理,加載,啟動,卸載,通信機制 良好的體驗,真正的獨立開發,獨立部署 複雜,需要設計加載,通信機制,無法做到徹底隔離,需要解決依賴衝突,樣式衝突問題;

      開源微前端框架也有多種,例如阿里出品的qiankun,icestark,還有針對angular提出的mooa等,都能快速接入項目,但結合公司內部系統的特點,直接採用會有有些限制,例如要實現定製界面,無刷新加載應用,且不能對現有項目的開發和部署造成影響,因此決定自研相關技術。

    四、架構設計

    4.1 應用層

    應用層包括所有接入微服務工作台的內部系統,他們各自開發與部署,接入前後沒有多大影響,只是需要針對微服務層單獨輸出打包一份靜態資源;

    4.2 微服務層

    微服務層作為核心模塊,擁有資源加載、路由管理、狀態管理和用戶認證管理幾大功能,具體內容將在後面詳細闡述,架構整體工作流程如下:

    4.3 基礎支撐層

    基礎支撐層作為基座,提供微服務運行的環境和容器,同時接入其他後端服務,豐富實用場景和業務功能;

    五、技術重難點

    要實現自定義微前端架構,難點在於需要管理和整合多個應用,確保應用之間獨立運行,彼此不受影響,需要解決如下幾個問題:

    5.1 資源管理

    5.1.1資源加載

    每個應用有一個應用資源管理和註冊的文件(app.regiser.js),其中包含路由信息,應用配置信息(configs.js)和靜態資源清單,當首次切換到某應用時,首先加載app.register.js文件,完成路由和應用信息的註冊,然後根據當前瀏覽器路由地址加載對應的靜態文件,完成頁面渲染,從而將各應用的靜態資源串聯起來,其中註冊入口文件通過webpack插件來實現,具體實現如下:
    
    FuluAppRegisterPlugin.prototype.apply = function(compiler) {
       appId = extraAppId();
       var entry = compiler.options.entry;
       if (isArray(entry)) {
                for (var i = 0; i &lt; entry.length; i++) {
                    if (isIndexFile(entry[i])) { // 入口文件
                        indexFileEdit(entry[i]);
                        entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
                        i = entry.length;
                    }
                }
        } else {
                if (isIndexFile(entry)) { // 入口文件
                    indexFileEdit(entry); // 重新生成和編輯入口文件
                    compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
                }
        }
        compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {
                fs.unlinkSync(tempFilePath); // 刪除臨時文件
                return compilation;
        });
        compiler.hooks.emit.tap('fulu-app-register', function(compilation) {
            var contentStr = 'window.register("'+ appId + '", {\nrouter: [ \n ' + extraRouters() + ' \n],\nentry: {\n'; // 全局註冊方法
            var entryCssArr = [];
            var entryJsArr = [];
            for (var filename in compilation.assets) {
                if (filename.match(mainCssRegx)) { // 提取css文件
                    entryCssArr.push('\"' + filename + '\"');
                } else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件
                    entryJsArr.push('\"' + filename + '\"');
                }
            }
            contentStr += ('css: ['+ entryCssArr.join(', ') +'],\n'); // css資源清單
            contentStr += ('js: ['+ entryJsArr.join(', ') +'],\n }\n});\n'); // js資源清單
            compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件
                source: function() {
                    return contentStr;
                },
                size: function() {
                    return contentStr.length;
                }
            };
            return compilation;
        });
    };
    
    5.1.2資源文件名
    微服務輸出打包模式下,靜態資源統一打包形式以項目id開頭,形如10000092-main.js, 文件名稱的修改通過webpack的插件實現;
    

    核心實現代碼如下:

    FuluAppRegisterPlugin.prototype.apply = function(compiler) {
        ......
        compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);
        compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);
        compiler.options.plugins.forEach((c) =&gt; {
            if (c.options) {
                if (c.options.filename) {
                    c.options.filename = addIdToFileName(c.options.filename, appId);
                }
                if (c.options.chunkFilename) {
                    c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);
                }
            }
        });
       ......
    };
    

    5.2 路由管理

    路由分為應用級和菜單級兩大類,應用類以應用id為前綴,將各應用區分開,避免路由地址重名的情況,菜單級的路由由各應用的路由系統自行管理,結構如下:
    

    5.3 狀態分隔

    前端項目通過狀態管理庫來進行數據的管理,為了保證各應用彼此間獨立,因此需要修改狀態庫的映射關係,這一部分需要藉助於webpack插件來進行統一的代碼層面調整,包括model和view兩部分代碼,model定義了狀態對象,view藉助工具完成狀態對象的映射,調整規則為【應用id+舊狀態對象名稱】,下面來講解一下插件的實現;
    

    插件的實現原理是藉助AST的搜索語法匹配源代碼中的狀態編寫和綁定的相關代碼,然後加上應用編號前綴,變成符合預期的AST,最後輸出成目標代碼:
    
    module.exports = function(source) {
          var options = loaderUtils.getOptions(this);
    	stuff = 'app' + options.appId;
    	isView = !!~source.indexOf('React.createElement'); // 是否是視圖層
    	allFunc = [];
    	var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";
    	connctFnAst = parser.parse(connectFn);
    	const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });
    	traverse(ast, {
    		CallExpression: function(path) {
    			if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)
    				if (isArray(path.node.arguments)) {
    					var argNode = path.node.arguments[0];
    					if (argNode.type === 'FunctionExpression') { // connect(() => {...})
    						traverseMatchFunc(argNode);
    					} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)
    						var temp_node = allFunc.find((fnNode) => {
    							return fnNode.id.name === argNode.name;
    						});
    						if (temp_node) {
    							traverseMatchFunc(temp_node);
    						}
    					}
    				}
    			} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {
    				if (isArray(path.node.callee.expressions)) {
    					for (var i = 0; i < path.node.callee.expressions.length; i++) {
    						if (path.node.callee.expressions[i].type === 'MemberExpression'
    							&& path.node.callee.expressions[i].object.name === '_dva'
    							&& path.node.callee.expressions[i].property.name === 'connect') {
    								traverseMatchFunc(path.node.arguments[0]);
    								i = path.node.callee.expressions.length;
    						}
    					}
    				}
    			}
    		},
    		FunctionDeclaration: function(path) {
    			if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {
    				traverseMatchFunc(path.node);
    			}
    			allFunc.push(path.node);
    		},
    		ObjectExpression: function(path) {
    			if (isView) {
    				return;
    			}
    			if (isArray(path.node.properties)) {
    				var temp = path.node.properties;
    				for (var i = 0; i < temp.length; i++) {
    					if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {
    						temp[i].value.value = stuff + temp[i].value.value;
    						i = temp.length;
    					}
    				}
    			}
    		}
    	});
    	return core.transformFromAstSync(ast).code;
    };
    

    5.4 框架容器渲染

    完成以上步驟的改造,就可以實現容器中的頁面渲染,這一部分涉及到組件庫框架層面的調整,大流程如下圖:

    六、構建流程

    6.1 使用插件

    構建過程中涉及到兩款自開發的插件,分別是fulu-app-register-plugin和fulu-app-loader;

    6.1.1 安裝
    npm i fulu-app-register-plugin fulu-app-loader -D;
    
    6.1.2 配置

    webpack配置修改:

    const FuluAppRegisterPlugin = require('fulu-app-register-plugin');
    module: {
       rules: [{
             test: /\.jsx?$/,
             loader: 'fulu-app-loader',
          }
       ]
    }
    plugins: [
        new FuluAppRegisterPlugin(),
        ......
    ]
    

    6.2.編譯

    編譯過程與目前項目保持一致,相比以前,多輸出了一份微前端項目編譯代碼,流程如下:

    七、遺留問題

    7.1 js環境隔離

    由於各應用都加載到同一個運行環境,因此如果修改了公共的部分,則會對其他系統產生不可預知的影響,目前沒有比較好的辦法來解決,後續將持續關注這方面的內容,逐漸優化達到風險可制的效果。
    

    7.2.獲取token

    目前應用切換使用重定向來完成token獲取,要實現如上所述的微前端效果,需要放棄這種方式,改用接口調用異步獲取,或者其他解決方案。
    

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

    【其他文章推薦】

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    ※別再煩惱如何寫文案,掌握八大原則!