緣起
寫這個東西的最初的原因是想搞一個基於sonar的促進代碼質量改進的插件。其大概原理就是如果你的某項指標的值不如上次(比如測試覆蓋率比上次的低),那么就直接讓構建失敗。這樣就促進代碼質量往好的方向發展。當然如果一直按照這個趨勢(越來越好)發展下去,該項指標會無限增大,到不合理的地步(比如測試覆蓋率遲早會變成100%,而且任何人不能讓它低於100%),所以可以給該項指標設置一個閾值,如果不低於該閾值,就沒有必須比上次好這種限制。
最開始的想法是做一個CI插件(比如jenkins)。但是經過一番研究,發現做成sonar的插件其實更加合適。sonar的插件部署起來很簡單,打包之后的sonar插件就是一個jar包,把這個jar包放到sonar-x.x.x/extensions/plugins/目錄下,然后重啟server即可。開發過程也不算太復雜(算是會者不難吧)。不過老實說,sonar插件的API文檔的描述信息真心不夠詳細,也沒有一些關於sonar插件體系的介紹文章,我是翻數據庫,看源代碼,de了n天bug才搞明白了sonar的運行過程是怎么個回事。
Sonar Domain探秘
要了解一個系統的業務,首先要了解它的domain。於是。。。先看一下數據庫中都有哪些表吧。我沒有看完所有的表,以下幾個是我看的比較明白,並且后來都用到的表(值得一提的是,sonar的服務器其實使用JRuby on Rails寫的,你也可以看到數據庫表的命名是符合Rails規約的):
projects:此表保存了所有被sonar分析過的項目的基本信息。值得注意的是,這里存放的不光是工程級別的東西,比如分析一個java代碼庫,整個代碼庫會作為一條記錄,每個package都會作為一條記錄,每個類亦然。也就是說sonar對每個級別都會做分析。看一下截圖可能更清楚:

metrics:此表保存的是測試指標,比如測試覆蓋率,代碼復雜度等等。
rules_profiles:所有的測試指標存儲在metrics表中。rules_profiles這個表保存的就是這些metrics的一個子集,可以認為是定制化的測試標准集合。每個project都會有相應的rules_project與之對應。
snapshots:有了projects,有了rules_profiles。按照某種rules_profile對某個project進行一次sonar分析,就會產生一些snapshots。但是這里其實並沒有存儲真正的分析出來的指標值。而是存放在project_measures這個表中。snapshots和project_measures通過外鍵關聯。
好了,關於domain model,就大概先提到這些。如果還有什么疑問,請自己安裝好sonar,然后查看數據庫結構。
Sonar基本流程
下一步就是要說說sonar的擴展點了,這里直接用代碼說話了。sonar運行的核心代碼在這個包中:sonar-batch-3.1.1.jar。其源代碼可以到http://grepcode.com/去下載。在源碼包中找到這個類:org.sonar.batch.phases.Phases.java。從87行開始看這段代碼:
1 public void execute(Project project) { 2 eventBus.fireEvent(new ProjectAnalysisEvent(project, true)); 3 mavenPluginsConfigurator.execute(project); 4 mavenPhaseExecutor.execute(project); 5 initializersExecutor.execute(); 6 7 persistenceManager.setDelayedMode(true); 8 sensorsExecutor.execute(sensorContext); 9 decoratorsExecutor.execute(); 10 persistenceManager.dump(); 11 persistenceManager.setDelayedMode(false); 12 13 if (project.isRoot()) { 14 if (updateStatusJob != null) { 15 updateStatusJob.execute(); 16 } 17 postJobsExecutor.execute(sensorContext); 18 } 19 cleanMemory(); 20 eventBus.fireEvent(new ProjectAnalysisEvent(project, false)); 21 }
從這段代碼我們大概可以看出一二。我們從第5行開始看起,首先對整個分析過程做初始化,包括加載所有的分析任務,這些分析任務有些是有先后次序的,初始化的時候也會給它們排一下執行順序等等。
第7行是說我所有的measure數據都是暫存到內存中,然后最后一起寫入數據庫。
接下來第8行運行一系列sensor來完成某些任務。
第9行運行一些列decorator來完成某些任務。
第10行把第8,9行產生的measure數據保存到數據庫。
最后再執行一些PostJob。
這里我們至少可以看到有三個擴展點:sensor,decorator,postJob。他們分別對應org.sonar.api.batch.Decorator,org.sonar.api.batch.Sensor,org.sonar.api.batch.PostJob這三個類。如果你想做點什么定制化的任務,需要做的就是繼承這三個類中的某一個,然后把這個類注冊到sonar分析系統即可。一個注冊的代碼實例:
public class CustomizePostJob implements PostJob { private DatabaseSession session; public CheckCoverageDelta(DatabaseSession session) { this.session = session; } public void executeOn(Project project, SensorContext sensorContext) { //do something } } public final class MantraPlugin extends SonarPlugin { public static final String MY_PROPERTY = "com.thoughtworks.mantra"; public List getExtensions() { return Arrays.asList(CustomizePostJob.class); } }
只需要定義一個SonarPlugin的實例,然后在它的getExtensions方法里面返回你定義的其他擴展類的數組。這些擴展類包含了上面提到的那三個類的繼承類。在上面這個例子里面就是定義並注冊了一個PostJob的繼承類。
那么我到底應該實現哪種擴展類來實現我的功能呢。基本上取決於時序關系,比如如果我希望在數據都保存到數據庫之后再執行點什么事情,那么就一定要選擇PostJob了。另外即使你決定了我應該實現一個Decorator,也可以控制該Decorator在眾多decorators中的執行位置。下面我會把我在嘗試實現最開始那個需求的過程中嘗試的方法列出來。
嘗試使用Metric
前一篇提到了sonar中有alert這樣一個機制,即針對某個metric,定義一個閾值和一個操作符,如果滿足了操作符之於閾值,那么就報警。抽象嗎,看下面的截圖:

上圖中所示的就是一個Complexity的Alert,如果其值大於10,那么就報警。
於是我想到的就是自定義一個Metric叫“Coverage Improvement”,然后在定義一個該值上的Alert:如果該值小於0(即覆蓋率下降了),那么就報警。
嗯,那么開始自定義一個Metric吧,定義方式是要實現這個接口:org.sonar.api.measures.Metrics。然后在其getMetrics方法中返回自己定義的Metric列表,當然實現Metrics接口的類本身也需要注冊到sonar中,注冊方法和注冊一個Sensor,Decorator是一樣的:
1 public class CoverageDeltaMetric implements Metrics{ 2 public static Metric COVERAGE_IMPROVEMENT = new Metric.Builder("coverage-COVERAGE_IMPROVEMENT", "Coverage Improvement", Metric.ValueType.FLOAT) 3 .setDescription("Coverage Improvement (%)") 4 .setDirection(Metric.DIRECTION_BETTER) 5 .setDomain(CoreMetrics.COVERAGE_KEY) 6 .setQualitative(true) 7 .create(); 8 public List<Metric> getMetrics() { 9 COVERAGE_IMPROVEMENT.setFormula(new CoverageImprovementFormula()); 10 return Arrays.asList(COVERAGE_IMPROVEMENT); 11 } 12 class CoverageImprovementFormula implements Formula { 13 public List<Metric> dependsUponMetrics() { 14 return Arrays.asList(CoreMetrics.COVERAGE); 15 } 16 public Measure calculate(FormulaData formulaData, FormulaContext formulaContext) { 17 Measure measure = new Measure(COVERAGE_IMPROVEMENT); 18 if(formulaContext.getResource().getScope().equals(Project.SCOPE)) { 19 measure.setValue(formulaData.getMeasure(CoreMetrics.COVERAGE).getVariation1()); 20 return new Measure(COVERAGE_IMPROVEMENT); 21 } 22 return measure; 23 } 24 } 25 } 26 27 public final class MantraPlugin extends SonarPlugin { 28 public static final String MY_PROPERTY = "com.thoughtworks.mantra"; 29 public List getExtensions() { 30 return Arrays.asList(CheckCoverageMetric.class); 31 } 32 }
等等,你剛才明明說有三種擴展點:Sensor, Decorator, PostJob,怎么又蹦出來一個Metrics也可以注冊到Sonar中?其實是這樣的,Metrics本身是不提供任何運算的,那就是一個衡量指標,我會在其他的擴展點中去給當前項目為這個衡量指標計算一個值出來,然后生成這個衡量尺度(Metric)的實例(Measure)保存到數據庫中。所以這個自定義Metric的信息應該是在Sonar Server啟動的時候就讀取出來了,然后保存到了數據庫的metrics這個表里面。
我剛才說Metric本身是不提供任何運算邏輯的,但是如果你給一個Metric的實例調用了setForumla的方法后就不一樣了,就像上面代碼第9行干的那樣。一個Forumula顧名思義,就是一個公式,用來計算該Metric值的公式。再等等!那就是說Forumula的實現類(就像上面的CoverageImprovementFormula)里面的代碼會被執行了?那么這些代碼的執行豈不是又獨立與剛才提到的那三個擴展方式了?事實的真相是:sonar會把每個Forumula包裝成為一個Decorator,然后在眾多Decorator的列表中找個合適的位置把它塞進去。。。還是看下例子:

現在你可以回顧下我們列出來的第一段代碼的第9行,上圖顯示的是從那行代碼執行進去的樣子。execute的第一行計算出來了要運行的decorators,並且給他們排好了順序。看到了吧,其中有很多的類型都是FormularDecorator。因此使用帶Formular的Metric本質上是插入了一個Decorator。嗯,不錯,看起來我上面寫的那段CoverageDeltaMetric的代碼應該是可以工作了,但事實上不然。。。
時序之殤
說到底一切都是時序的問題。剛才說了上面圖中的那些decorators是排序好了的。但是如何排序呢?其方式是在定義每個擴展點的時候就指定它依賴於哪些其它擴展點,把所有的這些順序限制總結起來就可以最終排出一個滿足所有約束的列表。那么具體指定依賴的方式有哪些呢?CoverageImprovementFormula的dependsUponMetrics方法就是一個例子:它指定了我希望在執行完Coverage這個Metric的Formula之后再執行我這個Formula(肯定是有了覆蓋率信息,我才能計算覆蓋率的變化率嘛)。第二種方式是使用org.sonar.api.batch.DependedUpon這樣的Annotation,但是我還沒有試成功過。。。
那么時序上遇到了什么問題呢?請看下CoverageImprovementFormula這段代碼的第19行。因為我想得到的是Coverage Measure值的變化量,因此要從getVariation1()這個函數取得,但是取出來竟然是null。細究其原因,發現,雖然我指定依賴了Coverage先計算出來,但是在Coverage計算出來的時候variation1還沒有被計算出來。看看下面這張圖:

在眾多decorators的最后有一個VariationDecrator,它的作用就是用來計算一系列的variation的。而因為它要計算的是所有Metric在兩次分析中的差值,所以它依賴於所有Metric的值都得到之后才會進行。但是偏偏我自定義的那個Metric就必須要等這個計算完了才能計算,因此就出現循環依賴了。不過想想也對,Metric本來表示的就是單次的結果,而且每次的Measure都會有Variation的記錄,所以把一個已經是Variation的東西再表示成為一個Metric自然是不合適的了。
PostJob是正解
之前想做成Metric是因為可以體面的在Alert中做配置,但是事實上是做不到的。於是不得不求助於PostJob了。還記得前面說的嗎,PostJob是在所有數據保存到數據庫之后做的,那時候什么信息都有了,所以只要下面這樣一段簡單的代碼即可完成功能:
public class CheckCoverageDelta implements PostJob { private DatabaseSession session; public CheckCoverageDelta(DatabaseSession session) { this.session = session; } public void executeOn(Project project, SensorContext sensorContext) { Double coverageChanges = sensorContext.getMeasure(CoreMetrics.COVERAGE).getVariation1(); if(coverageChanges < 0) { throw new RuntimeException("Your code coverage decrease by " + coverageChanges); } } }
一切皆插件
自己寫一個Decorator是插件。那么分明我一個插件沒寫,為什么上面圖里面會出現那么多decorators呢?答案是,他們是sonar自帶的deocrators,也就是說一些sonar本身自帶的一些功能就是通過插件的方式提供的,本質上和我們自己寫的插件沒什么區別。有興趣的同學可以看看sonar-x.x.x/lib/core-plugins/sonar-core-plugin-3.2.jar這個jar包,里面包含了像剛才提到的Coverage,VaviationDecorator等核心插件。
如何開始sonar插件開發
剛才說了這么多基本上是一些自己的經驗分享,並不是一個from scratch的sonar插件開發教程。關於如何初始化一個插件開發工程,如何調試,請參閱這里。
再多分享一些東西,下來的那些jar包都是沒有source的,我都是從grepcode一個一個的把代碼下下來,然后attach到那些jar包上的,不知道有沒有更好的辦法。另外你知道本地新裝好的sonar server的等登陸用戶名和密碼是什么嗎,不知道是不是我漏掉了,反正我沒在文檔上找到。不過竟然自己試出來了。。。admin/admin。
最后再重復一句,關於sonar插件開發的詳細信息還是要查看官網,這里只是一些經驗分享,希望對你有用。
