最近利用业余时间写了一个Java代码静态分析工具的聚合器。集成了三种主流的静态分析工具:pmd,checkstyle,findbugs。可以用这三种工具提供的几千种规则集,来给你的项目进行全面体检,同时附带了归纳整理并提供邮件通知。代码开源在github上,取名为health4j。
开发这个工具的初衷是希望它能约束自己代码的规范性。同时,引导自己采用一些已被业界认可的“最佳实践”,保证至少自己的代码更加“health”。写它的另一个目的,是继续我的个人爱好——“自动化”工具。
代码的整洁、规范,对于一个软件项目的重要性不言而喻。特别是那些曾经维护过别人遗留代码的人,相信你们都深有感触。但不要说别人,即便是我们自己写的代码,隔个三五个月自己再回头看,有时也是不知所云。当然对于代码缺陷而言,更可能会成为影响项目健壮性的一个定时炸弹。因此我认为,此举至少对我个人而言是有利的。
分析工具简介
这三个Java界的主流静态代码分析工具的对比:
需要说明的是,checkstyle主要用于检查代码书写规范。pmd,findbugs则是非常出名的缺陷分析工具。它们收集了非常多的检查规则和缺陷模式。其中pmd用于分析java源码,而findbugs分析的是java编译后的字节码,jtest是商业工具,目前并未集成。
设计
逻辑结构图:
该项目目前提供了三个分析工具的实现。每个工具都被拆分为三个部件:EnvVerifier(环境验证器),CommandInvoker(命令执行器),ReportExtractor(报告提取器)。每个工具的执行,在内部都会触发这三个部件的依次执行。并且每个工具都被内聚为一个任务,以供在线程池中并发执行。
每个工具的运行,都会伴随着输出自己的xml格式的报表文件。根据配置文件中是否启用了合并器,来决定是否需要对每个工具输出的报表文件作解析并提取(ReportExtractor),以供下面的聚合器聚合之用。
如果开启了聚合器,从每个工具输出的报表文件中,提取出公共的数据信息(来自于每个工具数据信息的抽象),然后根据给定的聚合模板,生成一份聚合后的html报表。
如果开启了通知器,那将会根据通知器的实现(默认给出的是邮件通知),将聚合后的html发送给目标。
类图结构:
从代码的设计图中可看出,每个动作都有与之对应的接口抽象。因此虽然只提供了三个工具的实现,但它仍然有良好的扩展性。只需提供相应的接口实现,并定义好需要的配置文件信息,可以添加更多的分析工具。在每个工具的实现上,添加图中的Tool注解,并标明其名称(name属性)。health4j在运行时,会自动扫描到该实现,并将其加入到线程池中执行。
Java SE的服务加载器
在开发web项目的时候,我们通常会引入spring来作为Ioc容器,这样我们可以将抽象与具体的实现隔离。但对于一个小的standalone项目,这么做似乎有些杀鸡用牛刀的感觉。不过幸好JDK自1.6版本后就提供了SPI(ServiceProvider Interface)的默认实现。有了它,你在写一些小工具的时候,不用spring这么“重”的bean容器,也可以实现Ioc。具体的做法很简单。它提供了一个ServiceLoader的泛型类。会从一个指定的位置加载对某个Service(该Service通常被定义为一个接口或抽象类)实现的配置。
比如这里的两个服务接口:ReportMerger,ReportNotifier。我们首先在一个项目的资源文件夹内创建一个名为“META-INF.services”文件夹。然后里面创建两个配置文件。每个配置文件都以接口的完全限定名作为文件名称:
而每个文件的内容也异常简单,你只需要指定该服务接口的提供者(实现类)的完全限定名即可:
com.freedom.health4j.api.impl.common.DefaultMerger
这样在获取该接口提供者的时候,我们便无需显示将该提供者的实例化过程与该服务绑定。示例:
private static void merge(ReportInfo reportInfo) { if (Boolean.valueOf(commonConfig.getProperty(Constants.COMMON_ENABLE_MERGE_KEY))) { ServiceLoader<ReportMerger> serviceLoader = ServiceLoader.load(ReportMerger.class); Iterator<ReportMerger> mergerIterator = serviceLoader.iterator(); if (!mergerIterator.hasNext()) { throw new RuntimeException("can not load service provider for service : ReportMerger"); } ReportMerger merger = mergerIterator.next(); merger.setCommonConfig(commonConfig); merger.merge(reportInfo); } }
虽然这种方式不及Spring等专业Ioc容器那样强大。不过对于开发一些工具类的小应用而言,却是非常简单并且实用。
使用与集成
该工具可作为独立的jar执行,也可以构建为unix-like-service(见)。当然作为自动化的一部分,我们仍然希望有非人为因素之外的其他触发条件。这里,可以列举几个常用的触发条件:
- 时间触发:利用linux定时任务在指定的时间触发
- 事件触发:在VCS等代码版本控制软件的hook中触发
- 持续集成:通过maven/ant等构建工具的task触发