程序員必備基礎:Git 命令全方位學習

前言

掌握Git命令是每位程序員必備的基礎,之前一直是用smartGit工具,直到看到大佬們都是在用Git命令操作的,回想一下,發現有些Git命令我都忘記了,於是寫了這篇博文,複習一下~

https://github.com/whx123/JavaHome

公眾號:撿田螺的小男孩

文章目錄

  • Git是什麼?
  • Git的相關理論基礎
  • 日常開發中,Git的基本常用命令
  • Git進階之分支處理
  • Git進階之處理衝突
  • Git進階之撤銷與回退
  • Git進階之標籤tag
  • Git其他一些經典命令

Git是什麼

在回憶Git是什麼的話,我們先來複習這幾個概念哈~

什麼是版本控制?

百度百科定義是醬紫的~

版本控制是指對軟件開發過程中各種程序代碼、配置文件及說明文檔等文件變更的管理,是軟件配置管理的核心思想之一。

那些年,我們的畢業論文,其實就是版本變更的真實寫照…腦洞一下,版本控制就是這些論文變更的管理~

什麼是集中化的版本控制系統?

那麼,集中化的版本控制系統又是什麼呢,說白了,就是有一個集中管理的中央服務器,保存着所有文件的修改歷史版本,而協同開發者通過客戶端連接到這台服務器,從服務器上同步更新或上傳自己的修改。

什麼是分佈式版本控制系統?

分佈式版本控制系統,就是遠程倉庫同步所有版本信息到本地的每個用戶。嘻嘻,這裏分三點闡述吧:

  • 用戶在本地就可以查看所有的歷史版本信息,但是偶爾要從遠程更新一下,因為可能別的用戶有文件修改提交到遠程哦。
  • 用戶即使離線也可以本地提交,push推送到遠程服務器才需要聯網。
  • 每個用戶都保存了歷史版本,所以只要有一個用戶設備沒問題,就可以恢複數據啦~

什麼是Git?

Git是免費、開源的分佈式版本控制系統,可以有效、高速地處理從很小到非常大的項目版本管理。

Git的相關理論基礎

  • Git的四大工作區域
  • Git的工作流程
  • Git文件的四種狀態
  • 一張圖解釋Git的工作原理

Git的四大工作區域

先複習Git的幾個工作區域哈:

  • Workspace:你電腦本地看到的文件和目錄,在Git的版本控制下,構成了工作區。
  • Index/Stage:暫存區,一般存放在 .git目錄下,即.git/index,它又叫待提交更新區,用於臨時存放你未提交的改動。比如,你執行git add,這些改動就添加到這個區域啦。
  • Repository:本地倉庫,你執行git clone 地址,就是把遠程倉庫克隆到本地倉庫。它是一個存放在本地的版本庫,其中HEAD指向最新放入倉庫的版本。當你執行git commit,文件改動就到本地倉庫來了~
  • Remote:遠程倉庫,就是類似github,碼雲等網站所提供的倉庫,可以理解為遠程數據交換的倉庫~

Git的工作流程

上一小節介紹完Git的四大工作區域,這一小節呢,介紹Git的工作流程咯,把git的操作命令和幾個工作區域結合起來,個人覺得更容易理解一些吧,哈哈,看圖:

git 的正向工作流程一般就這樣:

  • 從遠程倉庫拉取文件代碼回來;
  • 在工作目錄,增刪改查文件;
  • 把改動的文件放入暫存區;
  • 將暫存區的文件提交本地倉庫;
  • 將本地倉庫的文件推送到遠程倉庫;

Git文件的四種狀態

根據一個文件是否已加入版本控制,可以把文件狀態分為:Tracked(已跟蹤)和Untracked(未跟蹤),而tracked(已跟蹤)又包括三種工作狀態:Unmodified,Modified,Staged

  • Untracked: 文件還沒有加入到git庫,還沒參与版本控制,即未跟蹤狀態。這時候的文件,通過git add 狀態,可以變為Staged狀態
  • Unmodified:文件已經加入git庫, 但是呢,還沒修改, 就是說版本庫中的文件快照內容與文件夾中還完全一致。 Unmodified的文件如果被修改, 就會變為Modified. 如果使用git remove移出版本庫, 則成為Untracked文件。
  • Modified:文件被修改了,就進入modified狀態啦,文件這個狀態通過stage命令可以進入staged狀態
  • staged:暫存狀態. 執行git commit則將修改同步到庫中, 這時庫中的文件和本地文件又變為一致, 文件為Unmodified狀態.

一張圖解釋Git的工作原理

日常開發中,Git的基本常用命令

  • git clone
  • git checkout -b dev
  • git add
  • git commit
  • git log
  • git diff
  • git status
  • git pull/git fetch
  • git push

這個圖只是模擬一下git基本命令使用的大概流程哈~

git clone

當我們要進行開發,第一步就是克隆遠程版本庫到本地呢

git clone url  克隆遠程版本庫

git checkout -b dev

克隆完之後呢,開發新需求的話,我們需要新建一個開發分支,比如新建開發分支dev

創建分支:

git checkout -b dev   創建開發分支dev,並切換到該分支下

git add

git add的使用格式:

git add .	添加當前目錄的所有文件到暫存區
git add [dir]	添加指定目錄到暫存區,包括子目錄
git add [file1]	添加指定文件到暫存區

有了開發分支dev之後,我們就可以開始開發啦,假設我們開發完HelloWorld.java,可以把它加到暫存區,命令如下

git add Hello.java  把HelloWorld.java文件添加到暫存區去

git commit

git commit的使用格式:

git commit -m [message] 提交暫存區到倉庫區,message為說明信息
git commit [file1] -m [message] 提交暫存區的指定文件到本地倉庫
git commit --amend -m [message] 使用一次新的commit,替代上一次提交

把HelloWorld.java文件加到暫存區后,我們接着可以提交到本地倉庫啦~

git commit -m 'helloworld開發'

git status

git status,表示查看工作區狀態,使用命令格式:

git status  查看當前工作區暫存區變動
git status -s  查看當前工作區暫存區變動,概要信息
git status  --show-stash 查詢工作區中是否有stash(暫存的文件)

當你忘記是否已把代碼文件添加到暫存區或者是否提交到本地倉庫,都可以用git status看看哦~

git log

git log,這個命令用得應該比較多,表示查看提交歷史/提交日誌~

git log  查看提交歷史
git log --oneline 以精簡模式显示查看提交歷史
git log -p <file> 查看指定文件的提交歷史
git blame <file> 一列表方式查看指定文件的提交歷史

嘻嘻,看看dev分支上的提交歷史吧要回滾代碼就經常用它喵喵提交歷史

git diff

git diff 显示暫存區和工作區的差異
git diff filepath   filepath路徑文件中,工作區與暫存區的比較差異
git diff HEAD filepath 工作區與HEAD ( 當前工作分支)的比較差異
git diff branchName filepath 當前分支的文件與branchName分支的文件的比較差異
git diff commitId filepath 與某一次提交的比較差異

如果你想對比一下你改了哪些內容,可以用git diff對比一下文件修改差異哦

git pull/git fetch

git pull  拉取遠程倉庫所有分支更新併合併到本地分支。
git pull origin master 將遠程master分支合併到當前本地master分支
git pull origin master:master 將遠程master分支合併到當前本地master分支,冒號後面表示本地分支

git fetch --all  拉取所有遠端的最新代碼
git fetch origin master 拉取遠程最新master分支代碼

我們一般都會用git pull拉取最新代碼看看的,解決一下衝突,再推送代碼到遠程倉庫的。

有些夥伴可能對使用git pull還是git fetch有點疑惑,其實
git pull = git fetch+ git merge。pull的話,拉取遠程分支並與本地分支合併,fetch只是拉遠程分支,怎麼合併,可以自己再做選擇。

git push

git push 可以推送本地分支、標籤到遠程倉庫,也可以刪除遠程分支哦。

git push origin master 將本地分支的更新全部推送到遠程倉庫master分支。
git push origin -d <branchname>   刪除遠程branchname分支
git push --tags 推送所有標籤

如果我們在dev開發完,或者就想把文件推送到遠程倉庫,給別的夥伴看看,就可以使用git push origin dev~

Git進階之分支處理

Git一般都是存在多個分支的,開發分支,回歸測試分支以及主幹分支等,所以Git分支處理的命令也需要很熟悉的呀~

  • git branch
  • git checkout
  • git merge

git branch

git branch用處多多呢,比如新建分支、查看分支、刪除分支等等

新建分支:

git checkout -b dev2  新建一個分支,並且切換到新的分支dev2
git branch dev2 新建一個分支,但是仍停留在原來分支

查看分支:

git branch    查看本地所有的分支
git branch -r  查看所有遠程的分支
git branch -a  查看所有遠程分支和本地分支

刪除分支:

git branch -D <branchname>  刪除本地branchname分支

git checkout

切換分支:

git checkout master 切換到master分支

git merge

我們在開發分支dev開發、測試完成在發布之前,我們一般需要把開發分支dev代碼合併到master,所以git merge也是程序員必備的一個命令。

git merge master  在當前分支上合併master分支過來
git merge --no-ff origin/dev  在當前分支上合併遠程分支dev
git merge --abort 終止本次merge,並回到merge前的狀態

比如,你開發完需求后,發版全需要把代碼合到主幹master分支,如下:

Git進階之處理衝突

Git版本控制,還是多個人一起搞的,多個分支並存的,這就難免會有衝突出現~

Git合併分支,衝突出現

同一個文件,在合併分支的時候,如果同一行被多個分支或者不同人都修改了,合併的時候就會出現衝突。

舉個粟子吧,我們現在在dev分支,修改HelloWorld.java文件,假設修改了第三行,並且commit提交到本地倉庫,修改內容如下:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello,撿田螺的小男孩!");
    }
}

我們切回到master分支,也修改HelloWorld.java同一位置內容,如下:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello,jay!!");
    }
}

再然後呢,我們提交一下master分支的這個改動,並把dev分支合併過下,就出現衝突啦,如圖所示:

Git解決衝突

Git 解決衝突步驟如下:

  • 查看衝突文件內容
  • 確定衝突內容保留哪些部分,修改文件
  • 重新提交,done

1.查看衝突文件內容

git merge提示衝突后,我們切換到對應文件,看看衝突內容哈,,如下:

2.確定衝突內容保留哪些部分,修改文件

  • Git用<<<<<<<,=======,>>>>>>>標記出不同分支的內容,
  • <<<<<<<HEAD是指主分支修改的內容,>>>>>>> dev是指dev分支上修改的內容

所以呢,我們確定到底保留哪個分支內容,還是兩個分支內容都保留呢,然後再去修改文件衝突內容~

3.修改完衝突文件內容,我們重新提交,衝突done

Git進階之撤銷與回退

Git的撤銷與回退,在日常工作中使用的比較頻繁。比如我們想將某個修改后的文件撤銷到上一個版本,或者想撤銷某次多餘的提交,都要用到git的撤銷和回退操作。

代碼在Git的每個工作區域都是用哪些命令撤銷或者回退的呢,如下圖所示:

有關於Git的撤銷與回退,一般就以下幾個核心命令

  • git checkout
  • git reset
  • git revert

git checkout

如果文件還在工作區,還沒添加到暫存區,可以使用git checkout撤銷

git checkout [file]  丟棄某個文件file
git checkout .  丟棄所有文件

以下demo,使用git checkout — test.txt 撤銷了暫存區test.txt的修改

git reset

git reset的理解

git reset的作用是修改HEAD的位置,即將HEAD指向的位置改變為之前存在的某個版本.

為了更好地理解git reset,我們來回顧一下,Git的版本管理及HEAD的理解

Git的所有提交,會連成一條時間軸線,這就是分支。如果當前分支是master,HEAD指針一般指向當前分支,如下:

假設執行git reset,回退到版本二之後,版本三不見了哦,如下:

git reset的使用

Git Reset的幾種使用模式

git reset HEAD --file
回退暫存區里的某個文件,回退到當前版本工作區狀態
git reset –-soft 目標版本號 可以把版本庫上的提交回退到暫存區,修改記錄保留
git reset –-mixed 目標版本號 可以把版本庫上的提交回退到工作區,修改記錄保留
git reset –-hard  可以把版本庫上的提交徹底回退,修改的記錄全部revert。

先看一個粟子demo吧,代碼git add到暫存區,並未commit提交,就以下醬紫回退,如下:

git reset HEAD file 取消暫存
git checkout file 撤銷修改

再看另外一個粟子吧,代碼已經git commit了,但是還沒有push:

git log  獲取到想要回退的commit_id
git reset --hard commit_id  想回到過去,回到過去的commit_id

如果代碼已經push到遠程倉庫了呢,也可以使用reset回滾哦(這裏大家可以自己操作實踐一下哦)~

git log
git reset --hard commit_id
git push origin HEAD --force

git revert

與git reset不同的是,revert複製了那個想要回退到的歷史版本,將它加在當前分支的最前端。

revert之前:

revert 之後:

當然,如果代碼已經推送到遠程的話,還可以考慮revert回滾呢

git log  得到你需要回退一次提交的commit id
git revert -n <commit_id>  撤銷指定的版本,撤銷也會作為一次提交進行保存

Git進階之標籤tag

打tag就是對發布的版本標註一個版本號,如果版本發布有問題,就把該版本拉取出來,修復bug,再合回去。

git tag  列出所有tag
git tag [tag] 新建一個tag在當前commit
git tag [tag] [commit] 新建一個tag在指定commit
git tag -d [tag] 刪除本地tag
git push origin [tag] 推送tag到遠程
git show [tag] 查看tag
git checkout -b [branch] [tag] 新建一個分支,指向某個tag

Git其他一些經典命令

git rebase

rebase又稱為衍合,是合併的另外一種選擇。

假設有兩個分支master和test

      D---E test
      /
 A---B---C---F--- master

執行 git merge test得到的結果

       D--------E
      /          \
 A---B---C---F----G---   test, master

執行git rebase test,得到的結果

A---B---D---E---C‘---F‘---   test, master

rebase好處是: 獲得更優雅的提交樹,可以線性的看到每一次提交,並且沒有增加提交節點。所以很多時候,看到有些夥伴都是這個命令拉代碼:git pull –rebase

git stash

stash命令可用於臨時保存和恢復修改

git stash  把當前的工作隱藏起來 等以後恢復現場後繼續工作
git stash list 显示保存的工作進度列表
git stash pop stash@{num} 恢復工作進度到工作區
git stash show :显示做了哪些改動
git stash drop stash@{num} :刪除一條保存的工作進度
git stash clear 刪除所有緩存的stash。

git reflog

显示當前分支的最近幾次提交

git blame filepath

git blame 記錄了某個文件的更改歷史和更改人,可以查看背鍋人,哈哈

git remote

git remote   查看關聯的遠程倉庫的名稱
git remote add url   添加一個遠程倉庫
git remote show [remote] 显示某個遠程倉庫的信息

參考與感謝

感謝各位前輩的文章:

  • 一個小時學會Git
  • 【Git】(1)—工作區、暫存區、版本庫、遠程倉庫
  • Git Reset 三種模式
  • Git恢復之前版本的兩種方法reset、revert(圖文詳解)
  • Git撤銷&回滾操作(git reset 和 get revert)
  • 為什麼要使用git pull –rebase?

公眾號

  • 歡迎關注我個人公眾號,交個朋友,一起學習哈~
  • 如果文章有錯誤,歡迎指出哈,感激不盡~

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

利用Tu Share獲取股票交易信息,c#實現

 一、什麼是Tu Share

Tushare是一個免費、開源的python財經數據接口包。主要實現對股票等金融數據從數據採集清洗加工 到 數據存儲的過程,用戶可以免費(部分數據的下載有積分限制)的通過它提供的財經接口獲取股票交易、期貨等財經信息,功能非常強大。該接口和直接到各財經網站爬數據相比,最大的優勢就是快,去傳統財經網站爬數據,好多關鍵性的股票信息只能一隻股一隻股爬,而Tu Share的API,一個調用可以獲得一天的全部數據,速度差了好幾個數量級。另外一方面各財經網站的接口的API沒有對外文檔化,隨時可能變化,而Tu Share的API有正式的文檔化相對比較穩定。

二、如何註冊

該網站使用積分制來控制數據的訪問權限,如果想要訪問數據,先要到下面這個網址完成註冊,https://tushare.pro/register。註冊完成后,可以需要到個人主頁中拷貝Token,這個Token會在以後的訪問中用到,步驟如下

1、登錄成功后,點擊右上角->個人主頁

2、 在“用戶中心”中點擊“接口TOKEN”

 

3、 可以點擊右側複製按鈕複製token

三、Http API說明

Tushare HTTP數據獲取的方式,採用了post的機制,通過提交JSON body參數,就可以獲得您想要的數據。具體參數說明如下:

輸入參數

api_name:接口名稱,比如stock_basic

token :用戶唯一標識,可通過登錄pro網站獲取

params:接口參數,如daily接口中start_date和end_date

fields:字段列表,用於接口獲取指定的字段,以逗號分隔,如”open,high,low,close”

輸出參數

code: 接口返回碼,2002表示權限問題。

msg:錯誤信息,比如“系統內部錯誤”,“沒有權限”等

data:數據,data里包含fields和items字段,分別為字段和數據內容

四、c#(.net core)實現

1、在Visual Studio中安裝下面幾個包:Microsoft.Extensions.Http、Newtonsoft.Json

2、封裝方法,實現對REST web service的調用

public interface IHttpClientUtility
{
     string HttpClientPost(string url, object datajson);
}
public class HttpClientUtility : IHttpClientUtility
    {
        

        public HttpClientUtility()
        {
            
        }
        public  string HttpClientPost(string url, object datajson)
        {
            using (HttpClient httpClient = new HttpClient()) //http對象
            {
                httpClient.DefaultRequestHeaders.Accept.Clear();
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                httpClient.Timeout = new TimeSpan(0, 0, 5);
                //轉為鏈接需要的格式
                HttpContent httpContent = new JsonContent(datajson);
                //請求
                HttpResponseMessage response = httpClient.PostAsync(url, httpContent).Result;
                if (response.IsSuccessStatusCode)
                {
                    Task<string> t = response.Content.ReadAsStringAsync();
                    return t.Result;
                }
                throw new Exception("調用失敗");
            }
                                                                       
        }
    }

 

    public class JsonContent : StringContent
    {
        public JsonContent(object value)
            : base(JsonConvert.SerializeObject(value), Encoding.UTF8,
                "application/json")
        {
        }

        public JsonContent(object value, string mediaType)
            : base(JsonConvert.SerializeObject(value), Encoding.UTF8, mediaType)
        {
        }
    }

3、封裝對Tu Share API的調用

public  class TuShareUtility 
    {
        private IHttpClientUtility _httpClientUtility;
        private string _url = "http://api.waditu.com/";
        
        public TuShareUtility(IHttpClientUtility httpClientUtility)
        {
            _httpClientUtility = httpClientUtility;
        }

        /// <summary>
        /// 調用TuShare API
        /// </summary>
        /// <param name="apiName"></param>
        /// <param name="parmaMap"></param>
        /// <param name="fields"></param>
        /// <returns></returns>
        public  DataTable GetData(string apiName,Dictionary<string,string> parmaMap,params string[] fields)
        {
            var tuShareParamObj=new TuShareParamObj(){ ApiName = apiName ,Params = parmaMap,Fields = string.Join(",",fields)};
            //做Http調用
            var result=_httpClientUtility.HttpClientPost(_url, tuShareParamObj);
            //將返回結果序列化成對象
            var desResult=JsonConvert.DeserializeObject<TuShareResult>(result);
            //如果調用失敗,拋出異常
            if(!string.IsNullOrEmpty(desResult.Msg))
                throw new Exception(desResult.Msg);
            //返回結果分成兩部分,一部分是列頭信息,另一部分是數據本身,用這兩部分數據可以構建DataTable
            DataTable dt = new DataTable();
            foreach (var dataField in desResult.Data.Fields)
            {
                dt.Columns.Add(dataField);
            }

            foreach (var dataItemRow in desResult.Data.Items)
            {
                var newdr=dt.NewRow();
                for (int i=0;i< dataItemRow.Length;i++)
                {
                    newdr[i] = dataItemRow[i];
                }

                dt.Rows.Add(newdr);
            }
            return dt;
        }

        private class TuShareParamObj
        {
            [JsonProperty("api_name")]
            public string ApiName { get; set; }

            [JsonProperty("token")]
            public string Token { get; } = "****************";//你的Token

            [JsonProperty("params")]
            public Dictionary<string, string> Params { get; set; }

            [JsonProperty("fields")]
            public string Fields { get; set; }
        }

        private class TuShareData
        {
            [JsonProperty("fields")]
            public string[] Fields { get; set; }

            [JsonProperty("items")]
            public string[][] Items { get; set; }
        }

        private class TuShareResult
        {
            [JsonProperty("code")]
            public string Code { get; set; }

            [JsonProperty("msg")]
            public string Msg { get; set; }

            [JsonProperty("data")]
            public TuShareData Data { get; set; }
        }
    }

4、調用示例

獲得日線行情,整個過程1秒左右,返回6月24日,股票相關交易信息,代碼如下,(該網站的其它接口定義可以到https://tushare.pro/document/2查看)

var tuShareUtility=new TuShareUtility();
Dictionary<string, string> p = new Dictionary<string, string>();
p["trade_date"] = "20200624";
var table = tuShareUtility.GetData("daily", p, "");

返回如下結果

返回字段說明

名稱 類型 描述
ts_code str 股票代碼
trade_date str 交易日期
open float 開盤價
high float 最高價
low float 最低價
close float 收盤價
pre_close float 昨收價
change float 漲跌額
pct_chg float 漲跌幅 (未復權,如果是復權請用 通用行情接口 )
vol float 成交量 (手)
amount float 成交額 (千元)

 

 

 

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

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

SpringSceurity(5)—短信驗證碼登陸功能

SpringSceurity(5)—短信驗證碼登陸功能

有關SpringSceurity系列之前有寫文章

1、SpringSecurity(1)—認證+授權代碼實現

2、SpringSecurity(2)—記住我功能實現

3、SpringSceurity(3)—圖形驗證碼功能實現

4、SpringSceurity(4)—短信驗證碼功能實現

一、短信登錄驗證機制原理分析

了解短信驗證碼的登陸機制之前,我們首先是要了解用戶賬號密碼登陸的機制是如何的,我們來簡要分析一下Spring Security是如何驗證基於用戶名和密碼登錄方式的,

分析完畢之後,再一起思考如何將短信登錄驗證方式集成到Spring Security中。

1、賬號密碼登陸的流程

一般賬號密碼登陸都有附帶 圖形驗證碼記住我功能 ,那麼它的大致流程是這樣的。

1、 用戶在輸入用戶名,賬號、圖片驗證碼後點擊登陸。那麼對於springSceurity首先會進入短信驗證碼Filter,因為在配置的時候會把它配置在
UsernamePasswordAuthenticationFilter之前,把當前的驗證碼的信息跟存在session的圖片驗證碼的驗證碼進行校驗。

2、短信驗證碼通過後,進入 UsernamePasswordAuthenticationFilter 中,根據輸入的用戶名和密碼信息,構造出一個暫時沒有鑒權的
 UsernamePasswordAuthenticationToken,並將 UsernamePasswordAuthenticationToken 交給 AuthenticationManager 處理。

3、AuthenticationManager 本身並不做驗證處理,他通過 for-each 遍歷找到符合當前登錄方式的一個 AuthenticationProvider,並交給它進行驗證處理
,對於用戶名密碼登錄方式,這個 Provider 就是 DaoAuthenticationProvider。

4、在這個 Provider 中進行一系列的驗證處理,如果驗證通過,就會重新構造一個添加了鑒權的 UsernamePasswordAuthenticationToken,並將這個
 token 傳回到 UsernamePasswordAuthenticationFilter 中。

5、在該 Filter 的父類 AbstractAuthenticationProcessingFilter 中,會根據上一步驗證的結果,跳轉到 successHandler 或者是 failureHandler。

流程圖

2、短信驗證碼登陸流程

因為短信登錄的方式並沒有集成到Spring Security中,所以往往還需要我們自己開發短信登錄邏輯,將其集成到Spring Security中,那麼這裏我們就模仿賬號

密碼登陸來實現短信驗證碼登陸。

1、用戶名密碼登錄有個 UsernamePasswordAuthenticationFilter,我們搞一個SmsAuthenticationFilter,代碼粘過來改一改。
2、用戶名密碼登錄需要UsernamePasswordAuthenticationToken,我們搞一個SmsAuthenticationToken,代碼粘過來改一改。
3、用戶名密碼登錄需要DaoAuthenticationProvider,我們模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。

這個圖是網上找到,自己不想畫了

我們自己搞了上面三個類以後,想要實現的效果如上圖所示。當我們使用短信驗證碼登錄的時候:

1、先經過 SmsAuthenticationFilter,構造一個沒有鑒權的 SmsAuthenticationToken,然後交給 AuthenticationManager處理。

2、AuthenticationManager 通過 for-each 挑選出一個合適的 provider 進行處理,當然我們希望這個 provider 要是 SmsAuthenticationProvider。

3、驗證通過後,重新構造一個有鑒權的SmsAuthenticationToken,並返回給SmsAuthenticationFilter。
filter 根據上一步的驗證結果,跳轉到成功或者失敗的處理邏輯。

二、代碼實現

1、SmsAuthenticationToken

首先我們編寫 SmsAuthenticationToken,這裏直接參考 UsernamePasswordAuthenticationToken 源碼,直接粘過來,改一改。

說明

principal 原本代表用戶名,這裏保留,只是代表了手機號碼。
credentials 原本代碼密碼,短信登錄用不到,直接刪掉。
SmsCodeAuthenticationToken() 兩個構造方法一個是構造沒有鑒權的,一個是構造有鑒權的。
剩下的幾個方法去除無用屬性即可。

代碼

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名,
     * 在這裏就代表登錄的手機號碼
     */
    private final Object principal;

    /**
     * 構建一個沒有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 構建擁有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2、SmsAuthenticationFilter

然後編寫 SmsAuthenticationFilter,參考 UsernamePasswordAuthenticationFilter 的源碼,直接粘過來,改一改。

說明

原本的靜態字段有 usernamepassword,都幹掉,換成我們的手機號字段。
SmsCodeAuthenticationFilter() 中指定了這個 filter 的攔截 Url,我指定為 post 方式的 /sms/login
剩下來的方法把無效的刪刪改改就好了。

代碼

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form表單中手機號碼的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = "mobile";
    /**
     * 是否僅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        //短信驗證碼的地址為/sms/login 請求也是post
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

3、SmsAuthenticationProvider

這個方法比較重要,這個方法首先能夠在使用短信驗證碼登陸時候被 AuthenticationManager 挑中,其次要在這個類中處理驗證邏輯。

說明

實現 AuthenticationProvider 接口,實現 authenticate() 和 supports() 方法。

代碼

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * 處理session工具類
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // 此時鑒權成功后,應當重新 new 一個擁有鑒權的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 從session中獲取圖片驗證碼
        SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX);
        String inputCode = request.getParameter("smsCode");
        if(smsCodeInSession == null) {
            throw new BadCredentialsException("未檢測到申請驗證碼");
        }

        String mobileSsion = smsCodeInSession.getMobile();
        if(!Objects.equals(mobile,mobileSsion)) {
            throw new BadCredentialsException("手機號碼不正確");
        }

        String codeSsion = smsCodeInSession.getCode();
        if(!Objects.equals(codeSsion,inputCode)) {
            throw new BadCredentialsException("驗證碼錯誤");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

4、SmsCodeAuthenticationSecurityConfig

既然自定義了攔截器,可以需要在配置里做改動。

代碼

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private SmsUserService smsUserService;
    @Autowired
    private AuthenctiationSuccessHandler authenctiationSuccessHandler;
    @Autowired
    private AuthenctiationFailHandler authenctiationFailHandler;

    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //需要將通過用戶名查詢用戶信息的接口換成通過手機號碼實現
        smsCodeAuthenticationProvider.setUserDetailsService(smsUserService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

5、SmsUserService

因為用戶名,密碼登陸最終是通過用戶名查詢用戶信息,而手機驗證碼登陸是通過手機登陸,所以這裏需要自己再實現一個SmsUserService

@Service
@Slf4j
public class SmsUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RolesUserMapper rolesUserMapper;

    @Autowired
    private RolesMapper rolesMapper;

    /**
     * 手機號查詢用戶
     */
    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        log.info("手機號查詢用戶,手機號碼 = {}",mobile);
        //TODO 這裏我沒有寫通過手機號去查用戶信息的sql,因為一開始我建user表的時候,沒有建mobile字段,現在我也不想臨時加上去
        //TODO 所以這裏暫且寫死用用戶名去查詢用戶信息(理解就好)
        User user = userMapper.findOneByUsername("小小");
        if (user == null) {
            throw new UsernameNotFoundException("未查詢到用戶信息");
        }
        //獲取用戶關聯角色信息 如果為空說明用戶並未關聯角色
        List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId());
        if (CollectionUtils.isEmpty(userList)) {
            return user;
        }
        //獲取角色ID集合
        List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList());
        List<Roles> rolesList = rolesMapper.findByIdIn(ridList);
        //插入用戶角色信息
        user.setRoles(rolesList);
        return user;
    }
}

6、總結

到這裏思路就很清晰了,我這裡在總結下。

1、首先從獲取驗證的時候,就已經把當前驗證碼信息存到session,這個信息包含驗證碼和手機號碼。

2、用戶輸入驗證登陸,這裡是直接寫在SmsAuthenticationFilter中先校驗驗證碼、手機號是否正確,再去查詢用戶信息。我們也可以拆開成用戶名密碼登陸那樣一個
過濾器專門驗證驗證碼和手機號是否正確,正確在走驗證碼登陸過濾器。

3、在SmsAuthenticationFilter流程中也有關鍵的一步,就是用戶名密碼登陸是自定義UserService實現UserDetailsService后,通過用戶名查詢用戶名信息而這裡是
通過手機號查詢用戶信息,所以還需要自定義SmsUserService實現UserDetailsService后。

三、測試

1、獲取驗證碼

獲取驗證碼的手機號是 15612345678 。因為這裏沒有接第三方的短信SDK,只是在後台輸出。

向手機號為:15612345678的用戶發送驗證碼:254792

2、登陸

1)驗證碼輸入不正確

發現登陸失敗,同樣如果手機號碼輸入不對也是登陸失敗

2)登陸成功

當手機號碼 和 短信驗證碼都正確的情況下 ,登陸就成功了。

參考

1、Spring Security技術棧開發企業級認證與授權(JoJo)

2、SpringBoot 集成 Spring Security(8)——短信驗證碼登錄

別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(21)

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

【【其他文章推薦】

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

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

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

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

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

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

【思考】URI和URL的區別?以及URL的結構

URI = Universal Resource Identifier
URL = Universal Resource Locator

在學習中,我們難免會遇到 URI 和 URL,有時候都傻傻分不清,為啥這邊是 URI 那邊又是 URL,這兩者到底有什麼區別呢?

我們從名字上看

  • 統一資源標識符(Uniform Resource Identifier, URI):是一個用於標識某一互聯網資源名稱的字符串。
  • 統一資源定位符(Uniform Resource Locator, URL):是一個用於標識和定位某一互聯網資源名稱的字符串。

可能大家就比較困惑了,這倆好像是一樣的啊?那我們就類比一下我們現實生活中的情況:
我們要找一個人——張三,我們可以通過他的唯一的標識來找,比如說身份證,那麼這個身份證就唯一的標識了一個人,這個身份證就是一個 URI
而要找到張三,我們不一定要用身份證去找,我們還可以根據地址去找,如 在清華大學18號宿舍樓的404房間第一個床鋪的張三,我們也可以唯一確定一個張三,
動物住址協議://地球/中國/北京市/清華大學/18號宿舍樓/404號寢/張三.人。而這個地址就是我們用於標識和定位的 URL
我們從上面可以很明顯的看出,URI 通過任何方法標識一個人即可,而 URL 雖然也可以標識一個人,但是它主要是通過定位地址的方法標識一個人,所以 URL 其實是 URI 的一個子集,即 URL 是靠標識定位地址的一個 URI

Url 的構成

URL(Uniform Resource Locator,統一資源定位符),用於定位網絡上的資源,每一個信息資源都有統一的且在網上唯一的地址。

Url一般有以下部分組成
scheme://host:port/path?query#fragment

Scheme: 通信協議,一般為http、https等;
Host: 服務器的域名主機名或ip地址;
Port: 端口號,此項為可選項,默認為80;
Path: 目錄,由“/”隔開的字符串,表示的是主機上的目錄或文件地址;
Query: 查詢,此項為可選項,可以給動態網頁傳遞參數,用“&”隔開,每個參數的名和值用“=”隔開;
Fragment: 信息片段,字符串,用於指定網絡資源中的某片斷;

其實,把 URL 說成是網址其實是很不嚴謹的說法,因為 URL 有很嚴格的結構,表示也很靈活、有彈性。
在 RFC 3986: Uniform Resource Identifier (URI): Generic Syntax 的 Syntax Components 把 URL 描述為如下圖:

如圖所示,把 URL 分成幾個部分,這樣便可以了解URL的構成。 在 URI scheme – Wikipedia 頁面中對 URL 的描述更為詳細,如下圖:

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

IDEA創建SpringBoot的多模塊項目教程

最近在寫一個多模塊的SpringBoot項目,基於過程總了一些總結,故把SpringBoot多個模塊的項目創建記錄下來。

首先,先建立一個父工程:

(1)在IDEA工具欄選擇File->New->Project

(2)選擇Spring Initializr,默認選擇Default,然後點擊Next:    

(3)在輸入框填寫以下截圖內容,點擊Next

(4)直接點Next,無需選擇

(5)直接點擊Finish完成創建

(6)按照以上步驟,可以生成以下的項目目錄結構:

(7)這時把沒用的.mvn目錄,src目錄,mvnw還有mvnw.cmd都刪除,最終只保留.gitignore和pom.xml,若是web項目,可在該pom.xml里添加以下依賴:

1 <!--web特徵-->
2 <dependency>
3     <groupId>org.springframework.boot</groupId>
4     <artifactId>spring-boot-starter-web</artifactId>
5     <version>2.3.1.RELEASE</version>
6 </dependency>

最終得到以下的父結構目錄:

 

以上是創建父模塊,下面創建子模塊:

(1)在父模塊的根目錄fte上點右鍵,在彈出的框里選擇New->Module

(2)選擇Maven,點擊Next

(3)填寫以下內容,點擊Next

(4)填寫Module,點擊Finish

(5)同理添加fte-controller,fte-dao,fte-service,fte-web,最終得到以下的目錄結構:

(6)增加模塊之間的依賴:

controller層添加以下依賴:

 1 <dependencies>
 2     <dependency>
 3         <groupId>com.example</groupId>
 4         <artifactId>fte-common</artifactId>
 5         <version>0.0.1-SNAPSHOT</version>
 6     </dependency>
 7 
 8     <dependency>
 9         <groupId>com.example</groupId>
10         <artifactId>fte-dao</artifactId>
11         <version>0.0.1-SNAPSHOT</version>
12     </dependency>
13 
14     <dependency>
15         <groupId>com.example</groupId>
16         <artifactId>fte-service</artifactId>
17         <version>0.0.1-SNAPSHOT</version>
18     </dependency>
19 </dependencies>

service層添加以下依賴:

1 <dependencies>
2     <dependency>
3         <groupId>com.example</groupId>
4         <artifactId>fte-dao</artifactId>
5         <version>0.0.1-SNAPSHOT</version>
6     </dependency>
7 </dependencies>

(7)測試

在fte-controller創建com.zhu.fte.web包,增加以下兩個類:

fteWebApplication類:

 1 package com.zhu.fte.web;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 
 6 @SpringBootApplication
 7 public class fteWebApplication {
 8     public static void main(String[] args) {
 9         SpringApplication.run(fteWebApplication.class,args);
10     }
11 }

DemoController類

 1 package java.com.zhu.fte.web;
 2 
 3 import org.springframework.web.bind.annotation.GetMapping;
 4 import org.springframework.web.bind.annotation.RequestMapping;
 5 import org.springframework.web.bind.annotation.RestController;
 6 
 7 @RestController
 8 @RequestMapping("demo")
 9 public class DemoController {
10 
11     @GetMapping("test")
12     public String test(){
13         return "hello world";
14     }
15 
16 }

運行發現出現錯誤:

出現錯誤:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test (default-test) on project fte-common: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test failed: Plugin org.apache.maven.plugins:maven-surefire-plugin:2.22.2 or one of its dependencies could not be resolved: Could not transfer artifact junit:junit:jar:4.12 from/to central (https://repo.maven.apache.org/maven2): Connect to repo.maven.apache.org:443 [repo.maven.apache.org/151.101.52.215] failed: Connection timed out: connect -> [Help 1]

把缺少的org.apache.maven.plugins手動放到父工程的pom.xml里

 1 <build>
 2    <plugins>
 3       <plugin>
 4          <groupId>org.apache.maven.plugins</groupId>
 5          <artifactId>maven-clean-plugin</artifactId>
 6          <version>2.5</version>
 7       </plugin>
 8       <plugin>
 9          <groupId>org.apache.maven.plugins</groupId>
10          <artifactId>maven-source-plugin</artifactId>
11          <version>2.2</version>
12       </plugin>
13       <plugin>
14          <groupId>org.apache.maven.plugins</groupId>
15          <artifactId>maven-compiler-plugin</artifactId>
16          <version>3.0</version>
17          <configuration>
18             <source>1.8</source>
19             <target>1.8</target>
20             <encoding>${file.encoding}</encoding>
21             <!--編譯的時候方法不改變方法參數名稱,用於支持使用反射獲取方法參數名稱-->
22             <compilerArgument>-parameters</compilerArgument>
23          </configuration>
24       </plugin>
25       <plugin>
26          <groupId>org.apache.maven.plugins</groupId>
27          <artifactId>maven-install-plugin</artifactId>
28          <version>2.4</version>
29       </plugin>
30       <plugin>
31          <groupId>org.apache.maven.plugins</groupId>
32          <artifactId>maven-jar-plugin</artifactId>
33          <version>2.4</version>
34          <configuration>
35             <archive>
36                <manifest>
37                   <addDefaultImplementationEntries>true
38                   </addDefaultImplementationEntries>
39                </manifest>
40             </archive>
41          </configuration>
42       </plugin>
43 
44       <plugin>
45          <groupId>org.apache.maven.plugins</groupId>
46          <artifactId>maven-surefire-plugin</artifactId>
47          <version>2.13</version>
48          <configuration>
49             <argLine>-Xmx512M -Dfile.encoding=${file.encoding}</argLine>
50          </configuration>
51       </plugin>
52    </plugins>
53 </build>

運行fteWebApplication類里的main方法,默認端口為8080,訪問http://localhost:8080/demo/test,正常出現以下情況:

 

按照以上步驟,就可以初步建立SpringBoot多模塊的項目,下一章將基於這個基礎搭建Mybatis以及其逆向工程。

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

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

學寫PEP,參与Python語言的設計

如果你為Python寫了一篇PEP,這篇PEP成功的被Python指導委員會接受了,那麼以後你在吹牛皮的時候你就可以說我主導了Python語言某個特性的設計工作.

                              					-- 跬蟒

我就問你主導Python語言特性設計牛不牛皮,今天我就寫一篇文章告訴大家如何去為Python設計一篇PEP,並且整個PEP從一個想法到Python語言去實現它的這一套流程:

假設你已經是一個Python高手了,在使用Python給過程中你覺得Python語言在某方面還不夠完善,你有一個不錯的想法可以去改善Python這方面的不足,你打算把你的想法加入到Python語言裏面,所以你打算寫一篇PEP,為Python的發展獻言建策,那首先需要做什麼呢?

  1. 首先你要確保你的想法是個新的想法是個比較大的想法,是一個由必要去建立一個PEP的想法,也許你發現了Python的一些小問題,但是這些小問題如果提交一個小補丁就可以解決了,那就沒必要提PEP
  2. 當你確定自己的想法很牛B之後,你也不是馬上就要提PEP,你首先要做的事情是引發社區的討論,看看其他人怎麼看,然後自己去實現一下這個想法看是否是可行的,並且發帖到 python-list@python.org mailing list或者到 python-ideas@python.org mailing list 進行進一步的確定,看看大家對你的想法是否認同,如果你能讓大多數人都認同,那你就有戲,在你發帖之前最好準備一份高質量的PEP草稿,這樣的話才會更容易的被接受
  3. 總之就是先討論,得到大家的認可,避免後期不必要的撕逼,然受自己也要做好準備,最好有個簡單的實現,然後還有個高質量的PEP草稿

寫PEP你不得不知道的幾個Python社區角色

PEP champion : PEP擁護者 也就是PEP的發起人,也就是跟大家說我有個非常XXX的想法的人

PEP author: PEP作者 就是寫PEP的人,PEP從一個想法到一篇PEP草稿,再到一篇擁有官方PEP編號的PEP文檔,到後面PEP審核通過,PEP複審出現改動,PEP被接受這個過程中維護PEP文檔的人就是PEP的作者,大部分PEP作者就是PEP擁護者本人

PEP reviewer: 這個角色不是單指某一個人,一個PEP從想法到實現需要經過很多此review, 每一次參与review的人都可以被稱作 PEP reviewer

PEP editor: PEP編輯者 就是對PEP進行初步審核的人,審核通過的PEP進入到github上面的PEP倉庫的master分支,進行下一輪的評審

Python Core Developers: Python核心開發人員 就是開發Cpython解釋器的那群人,都是大佬,都是大佬

Python’s Steering Council: Python指導委員會 大佬中的大佬,從Python核心開發人員中選擇出來的指導Python語言開發工作的一群人,對於PEP是否接受有着最終發言權

PEP的工作流程是這樣的:

  1. PEP champion 先有一個高質量的idear(經過討論分析和理性驗證)
  2. 你去github上面去fork PEP倉庫
  3. 在倉庫中創建一個 pep-9999.rst的文件去把你的PEP草稿粘貼進去
  4. 確定你的PEP的類型,PEP的狀態設為草稿,PEP頭部按照模板寫一波
  5. 把你的pep-9999.rst push到PEP倉庫
  6. 然後PEP editors 會去審核你的提交
  7. 如果審核通過,這個本來是草稿的PEP會拿到一個正規的PEP編號,如果沒有審核通過那PEP editors 會打回去讓 PEP author 去修改
  8. 如果PEP審核通過拿到了PEP編號 PEP editor 會把這個新提交的PEP合併到PEP倉庫的 master 分支
  9. 如果你的PEP的類型是Standards Track類,那你提交的PEP還會被發送給Python-dev list 成員進行再次review, 確保你的新PEP沒有坑
  10. 有些聽起很不錯的PEP在實現的時候其實是非常蛋疼的,沒做的時候想的挺好,真正去實現的時候才知道是否靠譜,最好的情況時你在提交PEP的時候你手裡就已經有一個這個PEP的原型實現了,所以如果你的PEP類型是Standards Track類型那你就不僅需要準備設計文檔,你還需要準備一個參考實現,以此來避免一些不切實際的想法

當然凡事都有例外,有些Python的核心開發者是不會走這個流程的因為他們本身的權限比較大,他們有直接push內容到PEP倉庫的權限,所以有時候他們會直接給自己的PEP分配一個PEP編號push進入PEP倉庫的master分支,當然這並不意味着這個PEP就被接受了,他只是繞過了PEP editor的審批而已,PEP被接受和PEP通過審批是完全兩碼事兒,只有通過Python指導委員會的同意,PEP計劃實現,才能叫做PEP被接受.

如果我寫的PEP無法審核通過被拒怎麼辦?

PEP被拒絕是很正常的事情,不要灰心,只要能夠堅信自己的PEP是真正對Python有用的東西,真正好的idear,修改一下繼續上就行了,但是被拒肯定是有原因的,最主要的原因就是下面幾條:

  1. 該特性已經存在了
  2. 技術上不合理
  3. Python不需要去實現這樣的特性,也就是說偽需求
  4. 無法進行後向兼容
  5. 不符合Python的設計哲學(Python設計哲學可以在Python交互解釋器中輸入import this獲取)其實在PEP的審批階段可以拿着自己的PEP idear去諮詢Python指導委員會,因為PEP最終會不會被接受其實是由Python指導委員會所決定的,所以如果真的想要自己的PEP被接受,做好提前的溝通還是非常有必要的
  6. 奧對了還有一個蛋疼的要求,就是你的PEP草稿必須帶着至少一名Python核心開發人員一起寫,或者有一個Python核心開發人員指導你寫,或者有一個經過Python指導委員會批準的非Python核心開發人員一起寫,反正就是需要有一個能夠被Python指導委員會所信任的人參與了你的PEP設計,如果沒能滿足這個條件 PEP editor有權直接駁回你的PEP草稿

PEP的複審和決定機制

一篇PEP是否最終被接受並且決定去實現是需要經過層層複審的,反正要經過很麻煩了一個流程,下面有個Python官方畫的簡單流程圖:

但是實際情況比較複雜,有時候不會按照這個流程圖來,但是這個流程圖給人們提供了一個比較清晰的PEP工作流的概覽

PEP格式和模板

這年頭寫啥文檔沒個模板真不行,PEP也是文檔,所以模板搞起來:

  1. 首先PEP是UTF-8編碼的rst文件,首先你需要去指導rst文件的格式,如果rst的語法格式你已經會了,那你就可以閱讀官方的PEP 12--Sample reStructuredText PEP Template,沒錯PEP12是介紹rst格式PEP模板的PEP(有點繞),為什麼要用rst格式?官方給出的解釋是 容易轉成html進行在線發布和閱讀
  2. 每一篇PEP必須有一個標準的PEP頭部,如下所示,帶* 號是可寫可不寫的,不帶* 號的是必須要寫的,記住寫PEP頭的時候,頭的各個字段的順序,必須按照下圖的內容去寫,先後順序不能亂

寫道這裏就講的差不多了,但是其實PEP的書寫還有很多的內容比如:

  1. 如何判斷PEP是不是一個成功的PEP
  2. PEP提交之後發現內容有bug怎麼解決
  3. PEP所有權以及所有權轉移問題
  4. PEP editor的詳細職責和工作流
  5. 等等問題,我就不寫了,寫不動了…..

想寫PEP的可以先根據上面流程走一波,
然後等到遇到問題的時候再去查資料吧.

如果感覺本篇內容還不錯,微信的朋友請點個在看,其他平台的朋友可以(近距離)掃描下方的二維碼關注我的公眾號 早睡蟒更多優質原創無廣告內容等你來看.

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

這一次搞懂Spring代理創建及AOP鏈式調用過程

@

目錄

  • 前言
  • 正文
    • 基本概念
    • 代理對象的創建
    • 小結
    • AOP鏈式調用
    • AOP擴展知識
      • 一、自定義全局攔截器Interceptor
      • 二、循環依賴三級緩存存在的必要性
      • 三、如何在Bean創建之前提前創建代理對象
  • 總結

前言

AOP,也就是面向切面編程,它可以將公共的代碼抽離出來,動態的織入到目標類、目標方法中,大大提高我們編程的效率,也使程序變得更加優雅。如事務、操作日誌等都可以使用AOP實現。這種織入可以是在運行期動態生成代理對象實現,也可以在編譯期類加載時期靜態織入到代碼中。而Spring正是通過第一種方法實現,且在代理類的生成上也有兩種方式:JDK Proxy和CGLIB,默認當類實現了接口時使用前者,否則使用後者;另外Spring AOP只能實現對方法的增強。

正文

基本概念

AOP的術語很多,雖然不清楚術語我們也能很熟練地使用AOP,但是要理解分析源碼,術語就需要深刻體會其含義。

  • 增強(Advice):就是我們想要額外增加的功能
  • 目標對象(Target):就是我們想要增強的目標類,如果沒有AOP,我們需要在每個目標對象中實現日誌、事務管理等非業務邏輯
  • 連接點(JoinPoint):程序執行時的特定時機,如方法執行前、后以及拋出異常后等等。
  • 切點(Pointcut):連接點的導航,我們如何找到目標對象呢?切點的作用就在於此,在Spring中就是匹配表達式。
  • 引介(Introduction):引介是一種特殊的增強,它為類添加一些屬性和方法。這樣,即使一個業務類原本沒有實現某個接口,通過AOP的引介功能,我們可以動態地為該業務類添加接口的實現邏輯,讓業務類成為這個接口的實現類。
  • 織入(Weaving):即如何將增強添加到目標對象的連接點上,有動態(運行期生成代理)、靜態(編譯期、類加載時期)兩種方式。
  • 代理(Proxy):目標對象被織入增強后,就會產生一個代理對象,該對象可能是和原對象實現了同樣的一個接口(JDK),也可能是原對象的子類(CGLIB)。
  • 切面(Aspect、Advisor):切面由切點和增強組成,包含了這兩者的定義。

代理對象的創建

在熟悉了AOP術語后,下面就來看看Spring是如何創建代理對象的,是否還記得上一篇提到的AOP的入口呢?在AbstractAutowireCapableBeanFactory類的applyBeanPostProcessorsAfterInitialization方法中循環調用了BeanPostProcessorpostProcessAfterInitialization方法,其中一個就是我們創建代理對象的入口。這裡是Bean實例化完成去創建代理對象,理所當然應該這樣,但實際上在Bean實例化之前調用了一個resolveBeforeInstantiation方法,這裏實際上我們也是有機會可以提前創建代理對象的,這裏放到最後來分析,先來看主入口,進入到AbstractAutoProxyCreator類中:

	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		//創建當前bean的代理,如果這個bean有advice的話,重點看
		// Create proxy if we have advice.
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		//如果有切面,則生成該bean的代理
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			//把被代理對象bean實例封裝到SingletonTargetSource對象中
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

先從緩存中拿,沒有則調用wrapIfNecessary方法創建。在這個方法裏面主要看兩個地方:getAdvicesAndAdvisorsForBeancreateProxy。簡單一句話概括就是先掃描后創建,問題是掃描什麼呢?你可以先結合上面的概念思考下,換你會怎麼做。進入到子類AbstractAdvisorAutoProxyCreatorgetAdvicesAndAdvisorsForBean方法中:

	protected Object[] getAdvicesAndAdvisorsForBean(
			Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

		//找到合格的切面
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

findEligibleAdvisors方法中可以看到有兩個步驟,第一先找到所有的切面,即掃描所有帶有@Aspect註解的類,並將其中的切點(表達式)增強封裝為切面,掃描完成后,自然是要判斷哪些切面能夠連接到當前Bean實例上。下面一步步來分析,首先是掃描過程,進入到AnnotationAwareAspectJAutoProxyCreator類中:

	protected List<Advisor> findCandidateAdvisors() {
		// 先通過父類AbstractAdvisorAutoProxyCreator掃描,這裏不重要
		List<Advisor> advisors = super.findCandidateAdvisors();
		// 主要看這裏
		if (this.aspectJAdvisorsBuilder != null) {
			advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
		}
		return advisors;
	}

這裏委託給了BeanFactoryAspectJAdvisorsBuilderAdapter類,並調用其父類的buildAspectJAdvisors方法創建切面對象:

	public List<Advisor> buildAspectJAdvisors() {
		List<String> aspectNames = this.aspectBeanNames;

		if (aspectNames == null) {
			synchronized (this) {
				aspectNames = this.aspectBeanNames;
				if (aspectNames == null) {
					List<Advisor> advisors = new ArrayList<>();
					aspectNames = new ArrayList<>();
					//獲取spring容器中的所有bean的名稱BeanName
					String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
							this.beanFactory, Object.class, true, false);
					for (String beanName : beanNames) {
						if (!isEligibleBean(beanName)) {
							continue;
						}
						Class<?> beanType = this.beanFactory.getType(beanName);
						if (beanType == null) {
							continue;
						}
						//判斷類上是否有@Aspect註解
						if (this.advisorFactory.isAspect(beanType)) {
							aspectNames.add(beanName);
							AspectMetadata amd = new AspectMetadata(beanType, beanName);
							if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
								// 當@Aspect的value屬性為""時才會進入到這裏
								// 創建獲取有@Aspect註解類的實例工廠,負責獲取有@Aspect註解類的實例
								MetadataAwareAspectInstanceFactory factory =
										new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);

								//創建切面advisor對象
								List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
								if (this.beanFactory.isSingleton(beanName)) {
									this.advisorsCache.put(beanName, classAdvisors);
								}
								else {
									this.aspectFactoryCache.put(beanName, factory);
								}
								advisors.addAll(classAdvisors);
							}
							else {
								MetadataAwareAspectInstanceFactory factory =
										new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
								this.aspectFactoryCache.put(beanName, factory);
								advisors.addAll(this.advisorFactory.getAdvisors(factory));
							}
						}
					}
					this.aspectBeanNames = aspectNames;
					return advisors;
				}
			}
		}
		return advisors;
	}

這個方法裏面首先從IOC中拿到所有Bean的名稱,並循環判斷該類上是否帶有@Aspect註解,如果有則將BeanName和Bean的Class類型封裝到BeanFactoryAspectInstanceFactory中,並調用ReflectiveAspectJAdvisorFactory.getAdvisors創建切面對象:

	public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
		//從工廠中獲取有@Aspect註解的類Class
		Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		//從工廠中獲取有@Aspect註解的類的名稱
		String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
		validate(aspectClass);

		// 創建工廠的裝飾類,獲取實例只會獲取一次
		MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
				new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

		List<Advisor> advisors = new ArrayList<>();

		//這裏循環沒有@Pointcut註解的方法
		for (Method method : getAdvisorMethods(aspectClass)) {

			//非常重要重點看看
			Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
			advisors.add(0, instantiationAdvisor);
		}

		//判斷屬性上是否有引介註解,這裏可以不看
		for (Field field : aspectClass.getDeclaredFields()) {
			//判斷屬性上是否有DeclareParents註解,如果有返回切面
			Advisor advisor = getDeclareParentsAdvisor(field);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		return advisors;
	}

	private List<Method> getAdvisorMethods(Class<?> aspectClass) {
		final List<Method> methods = new ArrayList<>();
		ReflectionUtils.doWithMethods(aspectClass, method -> {
			// Exclude pointcuts
			if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
				methods.add(method);
			}
		});
		methods.sort(METHOD_COMPARATOR);
		return methods;
	}

根據Aspect的Class拿到所有不帶@Pointcut註解的方法對象(為什麼是不帶@Pointcut註解的方法?仔細想想不難理解),另外要注意這裏對method進行了排序,看看這個METHOD_COMPARATOR比較器:

	private static final Comparator<Method> METHOD_COMPARATOR;

	static {
		Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
				new InstanceComparator<>(
						Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
				(Converter<Method, Annotation>) method -> {
					AspectJAnnotation<?> annotation =
						AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
					return (annotation != null ? annotation.getAnnotation() : null);
				});
		Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
		METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
	}

關注InstanceComparator構造函數參數,記住它們的順序,這就是AOP鏈式調用中同一個@Aspect類中Advice的執行順序。接着往下看,在getAdvisors方法中循環獲取到的methods,分別調用getAdvisor方法,也就是根據方法逐個去創建切面:

	public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {

		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());

		//獲取pointCut對象,最重要的是從註解中獲取表達式
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		if (expressionPointcut == null) {
			return null;
		}

		//創建Advisor切面類,這才是真正的切面類,一個切面類裏面肯定要有1、pointCut 2、advice
		//這裏pointCut是expressionPointcut, advice 增強方法是 candidateAdviceMethod
		return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
				this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
	}

	private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
			Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
			
	private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
		//從候選的增強方法裏面 candidateAdviceMethod  找有有註解
		//Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class
		//並把註解信息封裝成AspectJAnnotation對象
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}

		//創建一個PointCut類,並且把前面從註解裏面解析的表達式設置進去
		AspectJExpressionPointcut ajexp =
				new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
		ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
		if (this.beanFactory != null) {
			ajexp.setBeanFactory(this.beanFactory);
		}
		return ajexp;
	}

之前就說過切面的定義,是切點和增強的組合,所以這裏首先通過getPointcut獲取到註解對象,然後new了一個Pointcut對象,並將表達式設置進去。然後在getAdvisor方法中最後new了一個InstantiationModelAwarePointcutAdvisorImpl對象:

	public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
			Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		this.declaredPointcut = declaredPointcut;
		this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
		this.methodName = aspectJAdviceMethod.getName();
		this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
		this.aspectJAdviceMethod = aspectJAdviceMethod;
		this.aspectJAdvisorFactory = aspectJAdvisorFactory;
		this.aspectInstanceFactory = aspectInstanceFactory;
		this.declarationOrder = declarationOrder;
		this.aspectName = aspectName;

		if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			// Static part of the pointcut is a lazy type.
			Pointcut preInstantiationPointcut = Pointcuts.union(
					aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

			// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
			// If it's not a dynamic pointcut, it may be optimized out
			// by the Spring AOP infrastructure after the first evaluation.
			this.pointcut = new PerTargetInstantiationModelPointcut(
					this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
			this.lazy = true;
		}
		else {
			// A singleton aspect.
			this.pointcut = this.declaredPointcut;
			this.lazy = false;
			//這個方法重點看看,創建advice對象
			this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
		}
	}

這個就是我們的切面類,在其構造方法的最後通過instantiateAdvice創建了Advice對象。注意這裏傳進來的declarationOrder參數,它就是循環method時的序號,其作用就是賦值給這裏的declarationOrder屬性以及Advice的declarationOrder屬性,在後面排序時就會通過這個序號來比較,因此Advice的執行順序是固定的,至於為什麼要固定,後面分析完AOP鏈式調用過程自然就明白了。

	public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		//獲取有@Aspect註解的類
		Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		validate(candidateAspectClass);

		//找到candidateAdviceMethod方法上面的註解,並且包裝成AspectJAnnotation對象,這個對象中就有註解類型
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}
		
		AbstractAspectJAdvice springAdvice;

		//根據不同的註解類型創建不同的advice類實例
		switch (aspectJAnnotation.getAnnotationType()) {
			case AtPointcut:
				if (logger.isDebugEnabled()) {
					logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
				}
				return null;
			case AtAround:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAroundAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtBefore:
				//實現了MethodBeforeAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJMethodBeforeAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfter:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfterReturning:
				//實現了AfterReturningAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJAfterReturningAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterReturningAnnotation.returning())) {
					springAdvice.setReturningName(afterReturningAnnotation.returning());
				}
				break;
			case AtAfterThrowing:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterThrowingAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
					springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
				}
				break;
			default:
				throw new UnsupportedOperationException(
						"Unsupported advice type on method: " + candidateAdviceMethod);
		}

		// Now to configure the advice...
		springAdvice.setAspectName(aspectName);
		springAdvice.setDeclarationOrder(declarationOrder);
		String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
		if (argNames != null) {
			springAdvice.setArgumentNamesFromStringArray(argNames);
		}

		//計算argNames和類型的對應關係
		springAdvice.calculateArgumentBindings();

		return springAdvice;
	}

這裏邏輯很清晰,就是拿到方法上的註解類型,根據類型創建不同的增強Advice對象:AspectJAroundAdvice、AspectJMethodBeforeAdvice、AspectJAfterAdvice、AspectJAfterReturningAdvice、AspectJAfterThrowingAdvice。完成之後通過calculateArgumentBindings方法進行參數綁定,感興趣的可自行研究。這裏主要看看幾個Advice的繼承體系:

可以看到有兩個Advice是沒有實現MethodInterceptor接口的:AspectJMethodBeforeAdvice和AspectJAfterReturningAdvice。而MethodInterceptor有一個invoke方法,這個方法就是鏈式調用的核心方法,但那兩個沒有實現該方法的Advice怎麼處理呢?稍後會分析。
到這裏切面對象就創建完成了,接下來就是判斷當前創建的Bean實例是否和這些切面匹配以及對切面排序。匹配過程比較複雜,對理解主流程也沒什麼幫助,所以這裏就不展開分析,感興趣的自行分析(AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply())。下面看看排序的過程,回到AbstractAdvisorAutoProxyCreator.findEligibleAdvisors方法:

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

sortAdvisors方法就是排序,但這個方法有兩個實現:當前類AbstractAdvisorAutoProxyCreator和子類AspectJAwareAdvisorAutoProxyCreator,應該走哪個呢?

通過類圖我們可以肯定是進入的AspectJAwareAdvisorAutoProxyCreator類,因為AnnotationAwareAspectJAutoProxyCreator的父類是它。

	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors = new ArrayList<>(advisors.size());
		for (Advisor element : advisors) {
			partiallyComparableAdvisors.add(
					new PartiallyComparableAdvisorHolder(element, DEFAULT_PRECEDENCE_COMPARATOR));
		}
		List<PartiallyComparableAdvisorHolder> sorted = PartialOrder.sort(partiallyComparableAdvisors);
		if (sorted != null) {
			List<Advisor> result = new ArrayList<>(advisors.size());
			for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) {
				result.add(pcAdvisor.getAdvisor());
			}
			return result;
		}
		else {
			return super.sortAdvisors(advisors);
		}
	}

這裏排序主要是委託給PartialOrder進行的,而在此之前將所有的切面都封裝成了PartiallyComparableAdvisorHolder對象,注意傳入的DEFAULT_PRECEDENCE_COMPARATOR參數,這個就是比較器對象:

	private static final Comparator<Advisor> DEFAULT_PRECEDENCE_COMPARATOR = new AspectJPrecedenceComparator();

所以我們直接看這個比較器的compare方法:

	public int compare(Advisor o1, Advisor o2) {
		int advisorPrecedence = this.advisorComparator.compare(o1, o2);
		if (advisorPrecedence == SAME_PRECEDENCE && declaredInSameAspect(o1, o2)) {
			advisorPrecedence = comparePrecedenceWithinAspect(o1, o2);
		}
		return advisorPrecedence;
	}

	private final Comparator<? super Advisor> advisorComparator;
	public AspectJPrecedenceComparator() {
		this.advisorComparator = AnnotationAwareOrderComparator.INSTANCE;
	}

第一步先通過AnnotationAwareOrderComparator去比較,點進去看可以發現是對實現了PriorityOrderedOrdered接口以及標記了PriorityOrder註解的非同一個@Aspect類中的切面進行排序。這個和之前分析BeanFacotryPostProcessor類是一樣的原理。而對同一個@Aspect類中的切面排序主要是comparePrecedenceWithinAspect方法:

	private int comparePrecedenceWithinAspect(Advisor advisor1, Advisor advisor2) {
		boolean oneOrOtherIsAfterAdvice =
				(AspectJAopUtils.isAfterAdvice(advisor1) || AspectJAopUtils.isAfterAdvice(advisor2));
		int adviceDeclarationOrderDelta = getAspectDeclarationOrder(advisor1) - getAspectDeclarationOrder(advisor2);

		if (oneOrOtherIsAfterAdvice) {
			// the advice declared last has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has lower precedence
				return LOWER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return HIGHER_PRECEDENCE;
			}
		}
		else {
			// the advice declared first has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has higher precedence
				return HIGHER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return LOWER_PRECEDENCE;
			}
		}
	}

	private int getAspectDeclarationOrder(Advisor anAdvisor) {
		AspectJPrecedenceInformation precedenceInfo =
			AspectJAopUtils.getAspectJPrecedenceInformationFor(anAdvisor);
		if (precedenceInfo != null) {
			return precedenceInfo.getDeclarationOrder();
		}
		else {
			return 0;
		}
	}

這裏就是通過precedenceInfo.getDeclarationOrder拿到在創建InstantiationModelAwarePointcutAdvisorImpl對象時設置的declarationOrder屬性,這就驗證了之前的說法(實際上這裏排序過程非常複雜,不是簡單的按照這個屬性進行排序)。
當上面的一切都進行完成后,就該創建代理對象了,回到AbstractAutoProxyCreator.wrapIfNecessary,看關鍵部分代碼:

	//如果有切面,則生成該bean的代理
	if (specificInterceptors != DO_NOT_PROXY) {
		this.advisedBeans.put(cacheKey, Boolean.TRUE);
		//把被代理對象bean實例封裝到SingletonTargetSource對象中
		Object proxy = createProxy(
				bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}

注意這裏將被代理對象封裝成了一個SingletonTargetSource對象,它是TargetSource的實現類。

	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}

		//創建代理工廠
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				//proxyTargetClass 是否對類進行代理,而不是對接口進行代理,設置為true時,使用CGLib代理。
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		//把advice類型的增強包裝成advisor切面
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		////用來控制代理工廠被配置后,是否還允許修改代理的配置,默認為false
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		//獲取代理實例
		return proxyFactory.getProxy(getProxyClassLoader());
	}

這裏通過ProxyFactory對象去創建代理實例,這是工廠模式的體現,但在創建代理對象之前還有幾個準備動作:需要判斷是JDK代理還是CGLIB代理以及通過buildAdvisors方法將擴展的Advice封裝成Advisor切面。準備完成則通過getProxy創建代理對象:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//根據目標對象是否有接口來判斷採用什麼代理方式,cglib代理還是jdk動態代理
		return createAopProxy().getProxy(classLoader);
	}

	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
		return getAopProxyFactory().createAopProxy(this);
	}

	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

首先通過配置拿到對應的代理類:ObjenesisCglibAopProxy和JdkDynamicAopProxy,然後再通過getProxy創建Bean的代理,這裏以JdkDynamicAopProxy為例:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//advised是代理工廠對象
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

這裏的代碼你應該不陌生了,就是JDK的原生API,newProxyInstance方法傳入的InvocationHandler對象是this,因此,最終AOP代理的調用就是從該類中的invoke方法開始。至此,代理對象的創建就完成了,下面來看下整個過程的時序圖:

小結

代理對象的創建過程整體來說並不複雜,首先找到所有帶有@Aspect註解的類,並獲取其中沒有@Pointcut註解的方法,循環創建切面,而創建切面需要切點增強兩個元素,其中切點可簡單理解為我們寫的表達式,增強則是根據@Before、@Around、@After等註解創建的對應的Advice類。切面創建好后則需要循環判斷哪些切面能對當前的Bean實例的方法進行增強並排序,最後通過ProxyFactory創建代理對象。

AOP鏈式調用

熟悉JDK動態代理的都知道通過代理對象調用方法時,會進入到InvocationHandler對象的invoke方法,所以我們直接從JdkDynamicAopProxy的這個方法開始:

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;

		//從代理工廠中拿到TargetSource對象,該對象包裝了被代理實例bean
		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		try {
			//被代理對象的equals方法和hashCode方法是不能被代理的,不會走切面
			.......
			
			Object retVal;

			// 可以從當前線程中拿到代理對象
			if (this.advised.exposeProxy) {
				// Make invocation available if necessary.
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}

			//這個target就是被代理實例
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);
			
			//從代理工廠中拿過濾器鏈 Object是一個MethodInterceptor類型的對象,其實就是一個advice對象
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			//如果該方法沒有執行鏈,則說明這個方法不需要被攔截,則直接反射調用
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				retVal = proxy;
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

這段代碼比較長,我刪掉了不關鍵的地方。首先來看this.advised.exposeProxy這個屬性,這在@EnableAspectJAutoProxy註解中可以配置,當為true時,會將該代理對象設置到當前線程的ThreadLocal對象中,這樣就可以通過AopContext.currentProxy拿到代理對象。這個有什麼用呢?我相信有經驗的Java開發都遇到過這樣一個BUG,在Service實現類中調用本類中的另一個方法時,事務不會生效,這是因為直接通過this調用就不會調用到代理對象的方法,而是原對象的,所以事務切面就沒有生效。因此這種情況下就可以從當前線程的ThreadLocal對象拿到代理對象,不過實際上直接使用@Autowired注入自己本身也可以拿到代理對象。
接下來就是通過getInterceptorsAndDynamicInterceptionAdvice拿到執行鏈,看看具體做了哪些事情:

	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, @Nullable Class<?> targetClass) {

		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
		//從代理工廠中獲得該被代理類的所有切面advisor,config就是代理工廠對象
		Advisor[] advisors = config.getAdvisors();
		List<Object> interceptorList = new ArrayList<>(advisors.length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		Boolean hasIntroductions = null;

		for (Advisor advisor : advisors) {
			//大部分走這裏
			if (advisor instanceof PointcutAdvisor) {
				// Add it conditionally.
				PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
				//如果切面的pointCut和被代理對象是匹配的,說明是切面要攔截的對象
				if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
					MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
					boolean match;
					if (mm instanceof IntroductionAwareMethodMatcher) {
						if (hasIntroductions == null) {
							hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
						}
						match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
					}
					else {
						//接下來判斷方法是否是切面pointcut需要攔截的方法
						match = mm.matches(method, actualClass);
					}
					//如果類和方法都匹配
					if (match) {

						//獲取到切面advisor中的advice,並且包裝成MethodInterceptor類型的對象
						MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
						if (mm.isRuntime()) {
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
			//如果是引介切面
			else if (advisor instanceof IntroductionAdvisor) {
				IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
				if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
					Interceptor[] interceptors = registry.getInterceptors(advisor);
					interceptorList.addAll(Arrays.asList(interceptors));
				}
			}
			else {
				Interceptor[] interceptors = registry.getInterceptors(advisor);
				interceptorList.addAll(Arrays.asList(interceptors));
			}
		}

		return interceptorList;
	}

這也是個長方法,看關鍵的部分,因為之前我們創建的基本上都是InstantiationModelAwarePointcutAdvisorImpl對象,該類是PointcutAdvisor的實現類,所以會進入第一個if判斷里,這裏首先進行匹配,看切點當前對象以及該對象的哪些方法匹配,如果能匹配上,則調用getInterceptors獲取執行鏈:

	private final List<AdvisorAdapter> adapters = new ArrayList<>(3);
	public DefaultAdvisorAdapterRegistry() {
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}

	public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<>(3);
		Advice advice = advisor.getAdvice();
		//如果是MethodInterceptor類型的,如:AspectJAroundAdvice
		//AspectJAfterAdvice
		//AspectJAfterThrowingAdvice
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}

		//處理 AspectJMethodBeforeAdvice  AspectJAfterReturningAdvice
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[0]);
	}

這裏我們可以看到如果是MethodInterceptor的實現類,則直接添加到鏈中,如果不是,則需要通過適配器去包裝后添加,剛好這裡有MethodBeforeAdviceAdapterAfterReturningAdviceAdapter兩個適配器對應上文兩個沒有實現MethodInterceptor接口的類。最後將Interceptors返回。

if (chain.isEmpty()) {
	Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
	retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
	// We need to create a method invocation...
	invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
	// Proceed to the joinpoint through the interceptor chain.
	retVal = invocation.proceed();
}

返回到invoke方法后,如果執行鏈為空,說明該方法不需要被增強,所以直接反射調用原對象的方法(注意傳入的是TargetSource封裝的被代理對象);反之,則通過ReflectiveMethodInvocation類進行鏈式調用,關鍵方法就是proceed

	private int currentInterceptorIndex = -1;
	
	public Object proceed() throws Throwable {
		//如果執行鏈中的advice全部執行完,則直接調用joinPoint方法,就是被代理方法
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				return proceed();
			}
		}
		else {
			//調用MethodInterceptor中的invoke方法
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

這個方法的核心就在兩個地方:invokeJoinpointinterceptorOrInterceptionAdvice.invoke(this)。當增強方法調用完后就會通過前者調用到被代理的方法,否則則是依次調用Interceptorinvoke方法。下面就分別看看每個Interceptor是怎麼實現的。

  • AspectJAroundAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		if (!(mi instanceof ProxyMethodInvocation)) {
			throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
		}
		ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
		ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
		JoinPointMatch jpm = getJoinPointMatch(pmi);
		return invokeAdviceMethod(pjp, jpm, null, null);
	}
  • MethodBeforeAdviceInterceptor -> AspectJMethodBeforeAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		return mi.proceed();
	}

	public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
		invokeAdviceMethod(getJoinPointMatch(), null, null);
	}
  • AspectJAfterAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		finally {
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		}
	}
  • AfterReturningAdviceInterceptor -> AspectJAfterReturningAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object retVal = mi.proceed();
		this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		return retVal;
	}

	public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
		if (shouldInvokeOnReturnValueOf(method, returnValue)) {
			invokeAdviceMethod(getJoinPointMatch(), returnValue, null);
		}
	}
  • AspectJAfterThrowingAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		catch (Throwable ex) {
			if (shouldInvokeOnThrowing(ex)) {
				invokeAdviceMethod(getJoinPointMatch(), null, ex);
			}
			throw ex;
		}
	}

這裏的調用順序是怎樣的呢?其核心就是通過proceed方法控制流程,每執行完一個Advice就會回到proceed方法中調用下一個Advice。可以思考一下,怎麼才能讓調用結果滿足如下圖的執行順序

以上就是AOP的鏈式調用過程,但是這隻是只有一個切面類的情況,如果有多個@Aspect類呢,這個調用過程又是怎樣的?其核心思想和“棧”一樣,就是“先進后出,後進先出”。

AOP擴展知識

一、自定義全局攔截器Interceptor

在上文創建代理對象的時候有這樣一個方法:

	protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) {
		//自定義MethodInterceptor.拿到setInterceptorNames方法注入的Interceptor對象
		Advisor[] commonInterceptors = resolveInterceptorNames();

		List<Object> allInterceptors = new ArrayList<>();
		if (specificInterceptors != null) {
			allInterceptors.addAll(Arrays.asList(specificInterceptors));
			if (commonInterceptors.length > 0) {
				if (this.applyCommonInterceptorsFirst) {
					allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
				}
				else {
					allInterceptors.addAll(Arrays.asList(commonInterceptors));
				}
			}
		}

		Advisor[] advisors = new Advisor[allInterceptors.size()];
		for (int i = 0; i < allInterceptors.size(); i++) {
			//對自定義的advice要進行包裝,把advice包裝成advisor對象,切面對象
			advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
		}
		return advisors;
	}

這個方法的作用就在於我們可以擴展我們自己的Interceptor,首先通過resolveInterceptorNames方法獲取到通過setInterceptorNames方法設置的Interceptor,然後調用DefaultAdvisorAdapterRegistry.wrap方法將其包裝為DefaultPointcutAdvisor對象並返回:

	public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
		if (adviceObject instanceof Advisor) {
			return (Advisor) adviceObject;
		}
		if (!(adviceObject instanceof Advice)) {
			throw new UnknownAdviceTypeException(adviceObject);
		}
		Advice advice = (Advice) adviceObject;
		if (advice instanceof MethodInterceptor) {
			return new DefaultPointcutAdvisor(advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				return new DefaultPointcutAdvisor(advice);
			}
		}
		throw new UnknownAdviceTypeException(advice);
	}

	public DefaultPointcutAdvisor(Advice advice) {
		this(Pointcut.TRUE, advice);
	}

需要注意DefaultPointcutAdvisor構造器裏面傳入了一個Pointcut.TRUE,表示這種擴展的Interceptor是全局的攔截器。下面來看看如何使用:

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        System.out.println("自定義攔截器");
        return invocation.proceed();
    }
}

首先寫一個類實現MethodInterceptor 接口,在invoke方法中實現我們的攔截邏輯,然後通過下面的方式測試,只要UserService 有AOP攔截就會發現自定義的MyMethodInterceptor也生效了。

    public void costomInterceptorTest() {
        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        UserService userService = applicationContext.getBean(UserService.class);
        userService.queryUser("dark");
    }

但是如果換個順序,像下面這樣:

    public void costomInterceptorTest() {

        UserService userService = applicationContext.getBean(UserService.class);

        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        userService.queryUser("dark");
    }

這時自定義的全局攔截器就沒有作用了,這是為什麼呢?因為當執行getBean的時候,如果有切面匹配就會通過ProxyFactory去創建代理對象,注意Interceptor是存到這個Factory對象中的,而這個對象和代理對象是一一對應的,因此調用getBean時,還沒有myMethodInterceptor這個對象,自定義攔截器就沒有效果了,也就是說要想自定義攔截器生效,就必須在代理對象生成之前註冊進去。

二、循環依賴三級緩存存在的必要性

在上一篇文章我分析了Spring是如何通過三級緩存來解決循環依賴的問題的,但你是否考慮過第三級緩存為什麼要存在?我直接將bean存到二級不就行了么,為什麼還要存一個ObjectFactory對象到第三級緩存中?這個在學習了AOP之後就很清楚了,因為我們在@Autowired對象時,想要注入的不一定是Bean本身,而是想要注入一個修改過後的對象,如代理對象。在AbstractAutowireCapableBeanFactory.getEarlyBeanReference方法中循環調用了SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference方法,AbstractAutoProxyCreator對象就實現了該方法:

	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (!this.earlyProxyReferences.contains(cacheKey)) {
			this.earlyProxyReferences.add(cacheKey);
		}
		// 創建代理對象
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

因此,當我們想要對循壞依賴的Bean做出修改時,就可以像AOP這樣做。

三、如何在Bean創建之前提前創建代理對象

Spring的代理對象基本上都是在Bean實例化完成之後創建的,但在文章開始我就說過,Spring也提供了一個機會在創建Bean對象之前就創建代理對象,在AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation方法中:

	protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
		Object bean = null;
		if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
			// Make sure bean class is actually resolved at this point.
			if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
				Class<?> targetType = determineTargetType(beanName, mbd);
				if (targetType != null) {
					bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
					if (bean != null) {
						bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
					}
				}
			}
			mbd.beforeInstantiationResolved = (bean != null);
		}
		return bean;
	}

	protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
		for (BeanPostProcessor bp : getBeanPostProcessors()) {
			if (bp instanceof InstantiationAwareBeanPostProcessor) {
				InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
				Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}

主要是InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation方法中,這裏又會進入到AbstractAutoProxyCreator類中:

	public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if (targetSource != null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}

	protected TargetSource getCustomTargetSource(Class<?> beanClass, String beanName) {
		// We can't create fancy target sources for directly registered singletons.
		if (this.customTargetSourceCreators != null &&
				this.beanFactory != null && this.beanFactory.containsBean(beanName)) {
			for (TargetSourceCreator tsc : this.customTargetSourceCreators) {
				TargetSource ts = tsc.getTargetSource(beanClass, beanName);
				if (ts != null) {
					return ts;
				}
			}
		}

		// No custom TargetSource found.
		return null;
	}

看到這裏大致應該明白了,先是獲取到一個自定義的TargetSource對象,然後創建代理對象,所以我們首先需要自己實現一個TargetSource類,這裏直接繼承一個抽象類,getTarget方法則返回原始對象:

public class MyTargetSource extends AbstractBeanFactoryBasedTargetSource {
    @Override
    public Object getTarget() throws Exception {
        return getBeanFactory().getBean(getTargetBeanName());
    }
}

但這還不夠,上面首先判斷了customTargetSourceCreators!=null,而這個屬性是個數組,可以通過下面這個方法設置進來:

	public void setCustomTargetSourceCreators(TargetSourceCreator... targetSourceCreators) {
		this.customTargetSourceCreators = targetSourceCreators;
	}

所以我們還要實現一個TargetSourceCreator類,同樣繼承一個抽象類實現,並只對userServiceImpl對象進行攔截:

public class MyTargetSourceCreator extends AbstractBeanFactoryBasedTargetSourceCreator {
    @Override
    protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class<?> beanClass, String beanName) {

        if (getBeanFactory() instanceof ConfigurableListableBeanFactory) {
            if(beanName.equalsIgnoreCase("userServiceImpl")) {
                return new MyTargetSource();
            }
        }

        return null;
    }
}

createBeanFactoryBasedTargetSource方法是在AbstractBeanFactoryBasedTargetSourceCreator.getTargetSource中調用的,而getTargetSource就是在上面getCustomTargetSource中調用的。以上工作做完后,還需要將其設置到AnnotationAwareAspectJAutoProxyCreator對象中,因此需要我們注入這個對象:

@Configuration
public class TargetSourceCreatorBean {

    @Autowired
    private BeanFactory beanFactory;

   @Bean
    public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator() {
        AnnotationAwareAspectJAutoProxyCreator creator = new AnnotationAwareAspectJAutoProxyCreator();
        MyTargetSourceCreator myTargetSourceCreator = new MyTargetSourceCreator();
        myTargetSourceCreator.setBeanFactory(beanFactory);
        creator.setCustomTargetSourceCreators(myTargetSourceCreator);
        return creator;
    }
}

這樣,當我們通過getBean獲取userServiceImpl的對象時,就會優先生成代理對象,然後在調用執行鏈的過程中再通過TargetSource.getTarget獲取到被代理對象。但是,為什麼我們在getTarget方法中調用getBean就能拿到被代理對象呢?
繼續探究,通過斷點我發現從getTarget進入時,在resolveBeforeInstantiation方法中返回的bean就是null了,而getBeanPostProcessors方法返回的Processors中也沒有了AnnotationAwareAspectJAutoProxyCreator對象,也就是沒有進入到AbstractAutoProxyCreator.postProcessBeforeInstantiation方法中,所以不會再次獲取到代理對象,那AnnotationAwareAspectJAutoProxyCreator對象是在什麼時候移除的呢?
帶着問題,我開始反推,發現在AbstractBeanFactoryBasedTargetSourceCreator類中有這樣一個方法buildInternalBeanFactory

	protected DefaultListableBeanFactory buildInternalBeanFactory(ConfigurableBeanFactory containingFactory) {
		DefaultListableBeanFactory internalBeanFactory = new DefaultListableBeanFactory(containingFactory);

		// Required so that all BeanPostProcessors, Scopes, etc become available.
		internalBeanFactory.copyConfigurationFrom(containingFactory);

		// Filter out BeanPostProcessors that are part of the AOP infrastructure,
		// since those are only meant to apply to beans defined in the original factory.
		internalBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor ->
				beanPostProcessor instanceof AopInfrastructureBean);

		return internalBeanFactory;
	}

在這裏移除掉了所有AopInfrastructureBean的子類,而AnnotationAwareAspectJAutoProxyCreator就是其子類,那這個方法是在哪裡調用的呢?繼續反推:

	protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) {
		synchronized (this.internalBeanFactories) {
			DefaultListableBeanFactory internalBeanFactory = this.internalBeanFactories.get(beanName);
			if (internalBeanFactory == null) {
				internalBeanFactory = buildInternalBeanFactory(this.beanFactory);
				this.internalBeanFactories.put(beanName, internalBeanFactory);
			}
			return internalBeanFactory;
		}
	}

	public final TargetSource getTargetSource(Class<?> beanClass, String beanName) {
		AbstractBeanFactoryBasedTargetSource targetSource =
				createBeanFactoryBasedTargetSource(beanClass, beanName);
		
		// 創建完targetSource后就移除掉AopInfrastructureBean類型的BeanPostProcessor對象,如AnnotationAwareAspectJAutoProxyCreator
		DefaultListableBeanFactory internalBeanFactory = getInternalBeanFactoryForBean(beanName);

		......
		return targetSource;
	}

至此,關於TargetSource接口擴展的原理就搞明白了。

總結

本篇篇幅比較長,主要搞明白Spring代理對象是如何創建的以及AOP鏈式調用過程,而後面的擴展則是對AOP以及Bean創建過程中一些疑惑的補充,可根據實際情況學習掌握。

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

【其他文章推薦】

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

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

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

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

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

WebGPU+光線追蹤Ray Tracing 開發三個月總結

大家好~這三個月以來,我一直在學習和實現“基於WebGPU的混合光線追蹤實時渲染”的技術,使用了Ray Tracing管線(如.rgen、.rmiss等着色器)。
現在與大家分享和介紹我目前的學習成果,希望對大家有所幫助!謝謝!

通過國外的開源項目,可在WebGPU中使用Ray Tracing管線

這三個月我對Ray Tracing的研究有了質的突破,主要歸功於我發現的WebGPU Node開源項目!
該作者首先在dawn-ray-tracing開源項目中對“dawn項目:Chrome對WebGPU的實現”進行了擴展,加入了光追的API;
然後在WebGPU Node開源項目中,底層封裝了Vulkan SDK,上層使用了dawn-ray-tracing項目,提供了WebGPU API,實現了在Nodejs環境中使用WebGPU API和Ray Tracing管線來實現硬件加速的光線追蹤(電腦需要使用nvdia的RTX顯卡)!

相關介紹參見:
Real-Time Ray-Tracing in WebGPU

搭建運行環境

有兩種方法來搭建運行環境:
1、給Chrome瀏覽器打補丁,使其與下載DXR驅動(DirectX Raytracing)關聯,從而在該瀏覽器中運行
詳見該作者最近寫的開源項目:chromium-ray-tracing
(我沒有測試過,不知道是否能使用)

2、編譯dawn-ray-tracing和WebGPU Node項目,從而在Nodejs環境中運行
我使用的是這個方法(不過我使用的WebGPU Node項目是今年3月份時的代碼,最新的代碼我還沒有編譯成功)。

我的操作系統是win7,顯卡是RTX 2060s,vulkan sdk是1.1.126.0版本

/* 最新代碼我還沒有編譯成功哈哈!請先不要進行下面的編譯操作!

編譯的步驟為(需要使用VPN翻牆):

# 編譯dawn-ray-tracing項目

## Clone the repo as "dawn-ray-tracing"
git clone https://github.com/maierfelix/dawn-ray-tracing

cd dawn-ray-tracing

## Bootstrap the gclient configuration
cp scripts/standalone.gclient .gclient

## Fetch external dependencies and toolchains with gclient
gclient sync


set DEPOT_TOOLS_WIN_TOOLCHAIN=0

npm install --global --production windows-build-tools

gn gen out/Shared --ide=vs --target_cpu="x64" --args="is_component_build=true is_debug=false is_clang=false"

ninja -C out/Shared


# 編譯webgpu node項目

npm install webgpu

在webgpu node的根目錄中創建名為“PATH_TO_DAWN”的文件,在其中指定dawn-ray-tracing項目的絕對路徑,如:
D:/Github/dawn-ray-tracing

在webgpu node的根目錄中執行:
npm run all --dawnversion=0.0.1
(
這裏要注意的是,需要先安裝Vulkan SDK和python;

可以通過“npm config set python C:\depot_tools\python.bat”來設置python路徑,或者指定python路徑:
npm run all --dawnversion=0.0.1 --python="C:\Users\Administrator\Downloads\depot_tools\bootstrap-3_8_0_chromium_8_bin\python\bin\python.exe"
)


# 在nodejs中運行ray tracing示例,驗證是否成功

進入webgpu node的根目錄

cd examples & cd ..
node --experimental-modules examples/ray-tracing/index.mjs

*/

應用場景

考慮到WebGPU還沒有正式發布,並且可能在三年內瀏覽器都不會支持Ray Tracing管線,所以我把渲染放到雲端,這樣就可以在雲端自行搭建環境(如使用WebGPU Node開源項目),然後通過網絡傳輸將渲染結果傳輸到客戶端,從而在客戶端瀏覽器不支持的情況下仍能显示光追渲染的畫面。

因此,我的應用場景為:
1、雲渲染
2、雲遊戲

這兩個應用場景有不同的需求:
“雲渲染”屬於離線渲染,我們關心的是:

  • 畫質要好
  • 渲染時間可以長點

因此:

  • 每幀可採樣多次,即n spp(n >= 30)
  • 支持多種渲染效果,如“焦射”(causicts)等
  • 全局光照可使用n次bounce(n >= 2)

“雲遊戲”屬於實時渲染,我們關心的是:

  • 畫質可以差點
  • 渲染時間要短(每幀30ms以內)

因此:

  • 每幀只採樣一次,即1 spp
  • 全局光照只使用一次或兩次bounce
  • 對“焦射”(causicts)等場景用性能好的方案達到接近的渲染效果,通過犧牲畫質來減少渲染時間

介紹我目前的實現方案

主要技術框架是“實時混合光線追蹤”,主要包含下面的pass:
1、gbuffer pass
創建gbuffer
2、ray tracing pass
直接從gbuffer中獲取world position、diffuse等數據,用來計算直接光照,從而減少了每個像素髮射的光線數量;
每個像素髮射1個shadow ray,用來計算直接光照的陰影;
如果只用1個bounce來計算全局光照的話,每個像素髮射1個indirect ray+1個shadow ray,用來計算間接光照。
3、denoise pass
基於BMFR算法來實現降噪,具體可參考本文後面的“實現降噪Denoise”部分。
4、taa pass
使用taa來抗鋸齒

相關代碼可見我的開源項目:
WebGPU-RTX

介紹我學習的整個流程,分享相關資料

了解光線追蹤的相關領域

我通過下面的文章進行了初步的了解:
一篇光線追蹤的入門
光線追蹤與實時渲染的未來
實時光線追蹤技術:業界發展近況與未來挑戰
Introduction to NVIDIA RTX and DirectX Ray Tracing
如何評價微軟的 DXR(DirectX Raytracing)?

實現第一個光追的Demo

通過學習下面的資料:
Ray Tracing in One Weekend
Ray Tracing: The Next Week
Ray Tracing in One Weekend和Ray Tracing: The Next Week的詳解
基於OpenGL的GPU光線追蹤

我參考資料中的代碼,用WebGL 2實現一個Demo:

該場景的紅圈中是一個球,附近有一個球形光源和一個矩形光源

因為沒有進行降噪,所以噪點太多了哈哈!

相關代碼可見我的開源項目:
Wonder-RayTrace

學習和實現Ray Tracing管線

通過學習NVIDIA Vulkan Ray Tracing Tutorial教程,我用 js語言+WebGPU Node開源項目 基於Ray Tracing管線依次實現了陰影、反射等基礎渲染效果。

該教程使用了VK_KHR_ray_tracing擴展,而WebGPU Node開源項目也使用了該擴展(Vulkan SDK),因此該教程的shader代碼幾乎可以直接用到該開源項目中。

教程代碼

用Reason重寫

我用Reason語言重寫了示例代碼,提煉了一個基礎架構。

學習GBuffer+Ray Tracing混合管線

因為我希望優先減少渲染時間,所以我要通過混合管線來進行實時渲染。

我通過A Gentle Introduction To DirectX Raytracing教程來學習和實現。

教程代碼下載

我學習了該教程的第一篇到第11篇,分別實現了創建GBuffer、使用Lambertian材質渲染、多光源的陰影等內容。

實現降噪Denoise

教程的第9篇通過每個像素對每個光源發射一個shadow ray,最後累加並計算平均值,實現了多光源的陰影。

教程的第11篇對第9篇進行了改進:為了減少每個像素髮射的shadow ray的數量,每個像素只隨機向一個光源發射一個shadow ray。
這樣會導致噪點,如下圖所示:

我們可以通過累計採樣數來不斷逼近無噪點的圖片(如該教程的第6篇一樣),但這樣需要經過長時間后才會收斂,所以只適合“雲渲染”這種離線渲染的應用場景。

累加一定幀數后,結果如下圖所示:

實現taa

降噪算法通常需要先實現“幀間的數據復用”,而TAA抗鋸齒也需要實現“幀間數據復用”的技術;而且降噪算法會使用TAA作為最後一個pass來抗鋸齒。所以我決定先實現taa,將其作為實現降噪算法的鋪墊。

我參考了下面的資料來實現taa:
DX12渲染管線(2) – 時間性抗鋸齒(TAA)、 相關代碼
Unity Temporal AA的改進與提高、 相關代碼
unit Temporal Anti-Aliasing

實現BMFR降噪算法

為了能應用於“雲遊戲”這種實時渲染的應用場景,我們需要快速降噪。因此我實現了BMFR算法來降噪。

降噪前場景:

降噪后場景:

我參考了下面的資料:
BLOCKWISE MULTI-ORDER FEATURE REGRESSION FOR REAL-TIME PATH TRACING RECONSTRUCTION
參考代碼

學習蒙特卡羅積分(monte carlo)的理論

教程的第11篇隨機向一個光源發射一個shadow ray,這其實已經使用了蒙特卡羅積分的理論。

我們可以通過下面的資料深入學習該理論,了解概率密度函數(pdf)、重要性採樣等相關概念,為我們後面實現全局光照打下理論基礎:
【RAY TRACING THE REST OF YOUR LIFE 超詳解】 光線追蹤 3-1 蒙特卡羅 (一) 到 【RAY TRACING THE REST OF YOUR LIFE 超詳解】 光線追蹤 3-7 混合概率密
光線追蹤器Ray Tracer:進階篇

實現全局光照

通過學習教程的第12篇,我實現了one bounce的全局光照。

更多參考資料:
Global Illumination and Path Tracing
Global Illumination and Monte Carlo

這裏我遇到的問題主要是處理indirect specular noise:噪點不穩定,導致降噪后不穩定(高光周圍有明顯波動)。
我首先以為是pdf寫錯了,結果修改了pdf后還是沒有改進;
然後希望通過clamp等方法移除這些高光的fireflies噪點,結果影響到了畫質;
最後採用了“採樣indirect specular/diffuse多次”來穩定噪點。這適用於“雲渲染”的離線渲染,但不適用於“雲遊戲”的實時渲染。

基於GGX模型,實現disney BRDF

通過學習教程的第14篇,我引入了pbr材質,實現了GGX模型,加入了多bounce的全局光照。

我對教程代碼進行了改進:
在.rgen着色器中使用for循環而不是遞歸來實現的多bounce;
實現了disney BRDF,在pbr材質中有diffuse、roughness、metallic、specular這幾個參數。

更多參考資料:
基於物理着色(二)- Microfacet材質和多層材質
基於物理着色(三)- Disney和UE4的實現
基於物理的渲染(PBR)白皮書 | 迪士尼原則的BRDF與BSDF相關總結
WebGPU-Path-Tracer 實現了disney BRDF

目前的渲染效果

我目前的實現需要改進的地方

在Ray Tracing pass中支持紋理

使用bindless texture或者virtual texture來實現

擴展disney BRDF,實現BSDF,支持透明、折射效果

增加后處理

如gamma矯正等

在雲端環境下多線程渲染

雲端天然具有并行的優勢,因此可將渲染任務分配到多個顯卡/服務器中執行。

改進降噪效果

BMFR對高光specular處理得不好。
為了應用在“雲渲染”中,需要提高畫質。因此可考慮:

  • 改進BMFR對specular的處理
    BMFR論文中已有相關的討論
  • 使用專門對多個spp採樣進行降噪的降噪器來替代BMFR
    因為BMFR主要是針對1 spp採樣,所以需要使用針對蒙托卡羅積分路徑追蹤的降噪器來替代

改進indirect specular/diffuse noise

現在我通過增加spp來增加噪點的穩定性,這在“雲遊戲”中行不通,因為只能有1 spp。因此可考慮:

  • 使用blue noise
    可參考: http://psgraphics.blogspot.com/2018/10/flavors-of-sampling-in-ray-tracing.html
    https://hal.archives-ouvertes.fr/hal-02158423/file/blueNoiseTemporal2019_slides.pdf
    https://belcour.github.io/blog/research/2019/06/18/animation-bluenoise.html
    https://zhuanlan.zhihu.com/p/90017623
  • 對GGX模型使用VNDF來代替NDF採樣
  • 對多bounce的indirect specular noise進行優化
    可能的解決方案:
    使用reflection denoise filter;
    adaptive multiple bounce;
  • 使用photon mapping來降低噪點

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

Java筆試面試總結—try、catch、finally語句中有return 的各類情況

前言

之前在刷筆試題和面試的時候經常會遇到或者被問到 try-catch-finally 語法塊的執行順序等問題,今天就抽空整理了一下這個知識點,然後記錄下來。

正文

本篇文章主要是通過舉例的方式來闡述各種情況,我這裏根據 try-catch-finally 語法塊分為兩種大情況討論:try-catch 語法塊和 try-catch-finally 語句塊,然後再在每種情況里再去具體討論。

一、try-catch 語句塊

我們可以看看下面程序:

public static void main(String[] args) {

    System.out.println(handleException0());
  }

  /**
   * try,catch都有return
   * @return
   */
  private static String handleException0() {
    try{
      System.out.println("try開始");
      String s = null;
      int length = s.charAt(0);
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }
  }

執行結果

try開始
捕獲到了異常
catch的返回值

分析:程序首先執行 try 塊裏面的代碼,try 塊裏面發現有異常,try 塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個 catch 塊,然後進入 catch 塊裏面將代碼執行完畢,當執行到 catch 裏面的return 語句的時候,程序中止,然後將此 return 的最終結果返回回去。

二、try-catch-finally 語句塊

這種語法塊我分為了 4 種情況討論,下面進行一一列舉。

1、第一種情況,try 塊裏面有 return 的情況,並且捕獲到異常

例1:

public static void main(String[] args) {
  String result = handleException1();
  System.out.println(result);
}
private static String handleException1() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例1執行的結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

例2:

public static void main(String[] args) {
  String result = handleException2();
  System.out.println(result);
}
private static String handleException2() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例2的執行結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

分析:首先 例1 和 例2 的結果是很顯然的,當遇到異常的時候,直接進入匹配到相對應的 catch 塊,然後繼續執行 finallly 語句塊,最後將 return 結果返回回去。

第二種情況:try塊裏面有return的情況,但是不會捕獲到異常

例3:

思考:下面代碼try語句塊中有return語句,那麼是否執行完try語句塊就直接return退出方法了呢?

public static void main(String[] args) {
  String result = handleException3();
  System.out.println(result);
}
private static String handleException3() {
  try{
  	System.out.println("");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例3的執行結果如下

finally塊執行完畢了
try塊的返回值

分析:例3的結果其實我們可以通過打斷點的方式去看看程序的具體執行流程,通過打斷點我們可以發現,代碼先執行 try塊 里的代碼,當執行到 return 語句的時候,handleException3方法並沒有立刻結束,而是繼續執行finally塊里的代碼,finally塊里的代碼執行完后,緊接着回到 try 塊的 return 語句,再把最終結果返回回去, handleException 方法執行完畢。

第三種情況:try塊和finally裏面都有return的情況

例4:

public static void main(String[] args) {
    System.out.println(handleException4());
  }

  /**
   * 情況3:try和finally中均有return
   * @return
   */
  private static String handleException4() {
    try{
      System.out.println("");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
  //  return "最終的結果";//不能再有返回值
  }

例4的執行結果

finally塊執行完畢了
finally的返回值

分析:需要注意的是,當 try 塊和 finally 裏面都有 return 的時候,在 try/catch/finally 語法塊之外不允許再有return 關鍵字。我們還是通過在程序中打斷點的方式來看看代碼的具體執行流程。代碼首先執行 try 塊 里的代碼,當執行到 return 語句的時候,handleException4 方法並沒有立刻結束,而是繼續執行 finally 塊里的代碼,當發現 finally 塊里有 return 的時候,直接將 finally 里的返回值(也就是最終結果)返回回去, handleException4 方法執行完畢。

第四種情況:try塊,catch塊,finally塊都有return

例5:

public static void main(String[] args) {
    System.out.println(handleException5());
  }

  /**
   * 情況4:try,catch,finally都有return
   * @return
   */
  private static String handleException5() {
    try{
      System.out.println("try開始");
      int[] array = {1, 2, 3};
      int i = array[10];
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      e.printStackTrace();//這行代碼其實就是打印輸出異常的具體信息
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
//    return "最終的結果";
  }

例5的執行結果

try開始
捕獲到了異常
finally塊執行完畢了
finally的返回值
java.lang.ArrayIndexOutOfBoundsException: 10
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.handleException5(TryCatchFinally.java:25)
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.main(TryCatchFinally.java:14)

分析:程序首先執行try塊裏面的代碼,try塊裏面發現有異常,try塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個catch塊,然後進入catch塊裏面將代碼執行完畢,當執行到catch裏面的return語句的時候,程序不會馬上終止,而是繼續執行finally塊的代碼,最後執行finally裏面的return,然後將此return的最終結果返回回去。

總結

其實,我們通過以上例子我們可以發現,不管return關鍵字在哪,finally一定會執行完畢。理論上來說try、catch、finally塊中都允許書寫return關鍵字,但是執行優先級較低的塊中的return關鍵字定義的返回值將覆蓋執行優先級較高的塊中return關鍵字定義的返回值。也就是說finally塊中定義的返回值將會覆蓋catch塊、try塊中定義的返回值;catch塊中定義的返回值將會覆蓋try塊中定義的返回值。
再換句話說如果在finally塊中通過return關鍵字定義了返回值,那麼之前所有通過return關鍵字定義的返回值都將失效——因為finally塊中的代碼一定是會執行的。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

Spring IoC 循環依賴的處理

前言

本系列全部基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的源碼解析。

本篇文章主要介紹 Spring IoC 是怎麼解決循環依賴的問題的。

正文

什麼是循環依賴

循環依賴就是循環引用,就是兩個或多個 bean 相互之間的持有對方,比如A引用B,B引用A,像下面偽代碼所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

Spring 如何解決循環依賴

Spring IoC 容器對循環依賴的處理有三種情況:

  1. 構造器循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。
  2. 單例作用域下的 setter 循環依賴:此依賴 Spring 通過三級緩存來解決。
  3. 非單例的循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。

構造器循環依賴

還是假設上面的A和B類是構造器循環依賴,如下所示:

public class A {
    private B b;
    
    public A(B b) {
        this.b = b;
    }
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    public B(A a) {
        this.a = a;
    }
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了構造器自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="constructor" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="constructor" />

</beans>

那麼我們在獲取 A 時,首先會進入 doGetBean() 方法(該方法在Spring IoC bean 的加載中分析過),會進行到如下代碼塊:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

    // 省略其它代碼...
    
    // 如果 bean 的作用域是單例
    if (mbd.isSingleton()) {
        // 創建和註冊單例 bean
        sharedInstance = getSingleton(beanName, () -> {
            try {
                // 創建 bean 實例
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                destroySingleton(beanName);
                throw ex;
            }
        });
        // 獲取bean實例
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    
    // 省略其它代碼...
 
}

上面方法中的 getSingleton() 方法會判斷是否是第一次創建該 bean,如果是第一次會先去創建 bean,也就是調用 ObjectFacotygetObject() 方法,即調用 createBean() 方法創建 bean 前,會先將當前正要創建的 bean 記錄在緩存 singletonsCurrentlyInCreation 中。

在創建A時發現依賴 B,便先去創建 B;B在創建時發現依賴A,此時A因為是通過構造函數創建,所以沒創建完,便又去創建A,發現A存在於 singletonsCurrentlyInCreation,即正在創建中,便拋出 BeanCurrentlylnCreationException 異常。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 一級緩存中不存在當前 bean,也就是當前 bean 第一次創建
        if (singletonObject == null) {
            // 如果當前正在銷毀 singletons,拋出異常
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            // 創建單例 bean 之前的回調
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 獲取 bean 實例
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
				}
            // 省略異常處理...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 創建單例 bean 之後的回調
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 將 singletonObject 放入一級緩存,並從二級和三級緩存中移除
                addSingleton(beanName, singletonObject);
            }
        }
        // 返回 bean 實例
        return singletonObject;
    }
}

// 單例 bean 創建前的回調方法,默認實現是將 beanName 加入到當前正在創建 bean 的緩存中,
// 這樣便可以對循環依賴進行檢測
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

// 單例 bean 創建后的回調方法,默認實現是將 beanName 從當前正在創建 bean 的緩存中移除
protected void afterSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
        throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 這邊bean已經初始化完成了,放入一級緩存
        this.singletonObjects.put(beanName, singletonObject);
        // 移除三級緩存
        this.singletonFactories.remove(beanName);
        // 移除二級緩存
        this.earlySingletonObjects.remove(beanName);
        // 將 beanName 添加到已註冊 bean 緩存中
        this.registeredSingletons.add(beanName);
    }
}

setter循環依賴

還是假設上面的A和B類是 field 屬性依賴注入循環依賴,如下所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了按照類型自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="byType" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="byType" />

</beans>

Spring 在解決單例循環依賴時引入了三級緩存,如下所示:

// 一級緩存,存儲已經初始化完成的bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二級緩存,存儲已經實例化完成的bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三級緩存,存儲創建bean實例的ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 按先後順序記錄已經註冊的單例bean
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);

首先在創建A時,會進入到 doCreateBean() 方法(前面的流程可以查看Spring IoC bean 的創建一文),如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
    // 獲取bean的實例
    BeanWrapper instanceWrapper = null;
    if (instanceWrapper == null) {
        // 通過構造函數反射創建bean的實例,但是屬性並未賦值
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    // 獲取bean的實例
    final Object bean = instanceWrapper.getWrappedInstance();
    
    // 省略其它代碼...

    // bean的作用域是單例 && 允許循環引用 && 當前bean正在創建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 如果允許bean提前曝光
    if (earlySingletonExposure) {
        // 將beanName和ObjectFactory形成的key-value對放入singletonFactories緩存中
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    
    // 省略其它代碼...
    
}

在調用 addSingletonFactory() 方法前A的實例已經創建出來了,只是還未進行屬性賦值和初始化階段,接下來將它放入了三級緩存中,如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        // 如果一級緩存中不包含當前bean
        if (!this.singletonObjects.containsKey(beanName)) {
            // 將ObjectFactory放入三級緩存
            this.singletonFactories.put(beanName, singletonFactory);
            // 從二級緩存中移除
            this.earlySingletonObjects.remove(beanName);
            // 將beanName加入到已經註冊過的單例bean緩存中
            this.registeredSingletons.add(beanName);
        }
    }
}

接下來A進行屬性賦值階段(會在後續文章中單獨分析這個階段),發現依賴B,便去獲取B,發現B還沒有被創建,所以走創建流程;在B進入屬性賦值階段時發現依賴A,就去調用 getBean() 方法獲取A,此時會進入 getSingleton() 方法(該方法的調用流程在Spring IoC bean 的加載一文中分析過),如下:

public Object getSingleton(String beanName) {
    // allowEarlyReference設置為true表示允許早期依賴
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先從一級緩存中,檢查單例緩存是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果為空,並且當前bean正在創建中,鎖定全局變量進行處理
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從二級緩存中獲取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 二級緩存為空 && bean允許提前曝光
            if (singletonObject == null && allowEarlyReference) {
                // 從三級緩存中獲取bean對應的ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 調用預先設定的getObject(),獲取bean實例
                    singletonObject = singletonFactory.getObject();
                    // 放入到二級緩存中,並從三級緩存中刪除
                    // 這時bean已經實例化完但還未初始化完
                    // 在該bean未初始化完時如果有別的bean引用該bean,可以直接從二級緩存中取出返回
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

嘗試一級緩存 singletonObjects (肯定沒有,因為A還沒初始化完全),嘗試二級緩存 earlySingletonObjects(也沒有),嘗試三級緩存 singletonFactories,由於A通過 ObjectFactory 將自己提前曝光了,所以B能夠通過 ObjectFactory.getObject() 拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀)。B拿到A后順利創建並初始化完成,調用上面分析過的 addSingleton() 方法將自己放入一級緩存中。此時返回A中,A也能順利拿到完全初始化的B進行後續的階段,最後也將自己放入一級緩存中,並從二級和三級緩存中移除。

過程圖如下所示:

非單例循環依賴

對於非單例的 bean,Spring 容器無法完成依賴注入,因為 Spring 容器不進行緩存,因此無法提前暴露一個創建中的 bean

總結

本文主要介紹了 Spring 對三種循環依賴的處理,其實還有一種字段循環依賴,比如 @Autowired 註解標註的字段,但它和 setter 循環依賴的解決方法一樣,這裏就沒有多說。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

參考

  • 《Spring 源碼深度解析》—— 郝佳
  • https://juejin.im/post/5c98a7b4f265da60ee12e9b2

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

【【其他文章推薦】

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

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

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

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

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

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