缘起
写这个东西的最初的原因是想搞一个基于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插件开发的详细信息还是要查看官网,这里只是一些经验分享,希望对你有用。