SonarQube是代码检查工具的技术标杆之一,除了用来检查项目,它本身也是开源的,源码(代码结构/技术文档等)也必然是值得一读。

通过阅读源码,我们可以学到

  • 一款顶尖复杂的软件项目的结构与模块如何划分
  • 一个复杂前端的React实现
  • 如何混合使用ES与数据库

在阅读本文前,众所周知源码分析非常耗费时间,重复一下源码分析方法论

  • 能够充分使用过此项目,并阅读文档,这一步主要掌握上下文与术语
  • 分析项目的依赖,并全部过一遍
  • 找到免编译的路由断点与关键日志位置
  • 充分使用FindUsage快速Jump

预先准备

由于编译SonarQube非常繁琐耗时,我建议提前下载好编译好的二进制文件,导入源码后配置Remote断点以实现降低分析耗时(类似以前写的通过GDB断点JVM)

下载源码

  • 下载SonarQube编译好的二进制文件,并确保已经有数据
  • 通过Github下载源码,导入IDEA中,并将Head切到与二进制一样的版本

配置断点

在IDEA中配置Remote断点,并在源码中如下位置打下断点

org.sonar.ce.app.CeServer#start

在SonarQube中配置sonar.properties

sonar.ce.javaOpts=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5006sonar.web.javaOpts=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5007

然后运行SonarQube,如果Ok的话断点就断上了

SonarQube组件构成

服务端(ServerSide)

在服务端(org.sonar.application.App)启动时,会依此启动如下Process(通过ps aux|grep sonar分析)

org.sonar.application.App org.elasticsearch.bootstrap.Elasticsearch org.sonar.ce.app.CeServer org.sonar.server.app.WebServer

它们的作用分别是

  • ElasticSearch: 内嵌的ElasticSearch,版本为5,它内部还没有用推荐的_doc作为type
  • Compute Engine(CE): 计算引擎,通过解析计算客户端上传的Zip,显示到前端
  • Web: 本质是数据仓库的前台。主要有用React实现的前端与API代理,通过内嵌的Tomcat实现部署

它们的PID分别如下

p_es=`ps aux|grep Elasticsearch|grep java |awk '{print $2}'`p_ce=`ps aux|grep CeServer|grep java |awk '{print $2}'`p_web=`ps aux|grep WebServer|grep java |awk '{print $2}'`

通过lsof查看每个服务的监听情况,并在源码中全局搜索

lsof -i -n -P |grep java|grep LISTEN|grep ${p_es}# --> 9001(Elastic默认TCP端口)lsof -i -n -P |grep java|grep LISTEN|grep ${p_ce}# --> 54918(没查到,可能是随机端口)lsof -i -n -P |grep java|grep LISTEN|grep ${p_web}# --> 9000(HTTP端口), 9092(H2数据库端口)

客户端(ScannerSide)

  • Analyser: 源码分析器,在客户机上计算,通过HTTP将ZIP发给WebServer

组件实现

内嵌H2数据库

直接免密码访问如下JDBC即可

jdbc:h2:tcp://127.0.0.1:9092/sonar

ElasticSearch里存的啥

在SonarQube中,ElasticSearch的配置文件是程序生成的。由于默认关闭了HTTP端口的,导致难以通过外部工具进行连接,我们可以进行如下Hack实现在启动ElasticSearch前修改配置

# vi elasticsearch/bin/elasticsearch# 这里改成你的安装位置SONAR_HOME=Downloads/sonarqube-7.4sed -i -e 's/false/true/g' ${SONAR_HOME}/temp/conf/es/elasticsearch.ymlecho "http.host: 'localhost'" >> ${SONAR_HOME}/temp/conf/es/elasticsearch.ymlecho "http.port: '9300'" >> ${SONAR_HOME}/temp/conf/es/elasticsearch.yml

这样启动后,通过Elasticsearch Head等工具访问http://localhost:9300/就可以知道里面存的啥了。至于存了什么,请接着看下面具体分析。

WebServer

它部署了一个嵌入的Tomcat,通过Java(而不是通过Spring的DSL)手动实现添加webApp

org.sonar.server.app.TomcatContexts#addContext

等Tomcat部署后,将调用PlatformServletContextListener启动Platform(这里并没有使用Spring作为依赖注入,而是使用了picocontainer实现,使用Java编码而没有用注解/XML等DSL进行描述依赖关系),启动API业务

最终部署如下业务

部署类型

ContextPath

ByWho

API业务

/api

WebServiceFilter, 类似于Struts

React静态文件

默认是/, 详见sonar.web.context

web

插件Jar仓库静态文件

/deploy

data/web/deploy

所有的API请求均可以通过如下位置进行断点分析到业务中

org.sonar.server.ws.WebServiceEngine#execute

这样本文的引导作用就达到了,剩下具体业务自行断点分析

我在这里耗费了较多时间,本以为API业务是由Servlet进行处理,没想到居然是通过全手写Filter与Action的方法处理(10年前这种方法很先进),可以看出SonarQube也是有历史债务的,但是它的代码质量经过长期maintain后仍然清晰。

注意项目中的StaticResourcesServlet已经事实上废弃,因为已经没有static文件夹了

CE如何录入计算结果?

总流程如下

Scanner-(HTTP)->Web-(MQ)->CE

首先在Scanner通过Maven等工具在Jenkins等平台(占用这些平台的计算资源)计算出项目的各种分析报告,然后Scanner调用Web中如下接口

// http://localhost:9000/api/ce/submit?projectKey=xxx&projectName=yyyorg.sonar.server.ce.ws.SubmitAction#handle

WebServer将原始RAW文件录入ce_task_input,接着通过消息队列(DB Based)

org.sonar.ce.queue.CeQueueImpl#submit(org.sonar.ce.queue.CeTaskSubmit)

CE侧通过线程池每隔两秒轮询查询任务

org.sonar.ce.taskprocessor.CeWorkerImpl#call

最终任务将通过路由到如下位置,执行数据仓库录入等任务

org.sonar.ce.task.step.ComputationStepExecutor#executeStep

疑问解答

SonarQube如何实现存储源码?

通过访问表file_sources中的BINARY_DATA与protobuf实现存储,它与File通过FILE_UUID进行关联

org.sonar.server.source.ws.LinesAction#handle

SonarQube与Markdown

通过基于正则表达式的规则引擎实现,这个做的比较简单,没有实现AST

org.sonar.channel.ChannelDispatcher#consume

SonarQube的React如何实现

前台使用了React与JSX实现业务,使用Webpack进行打包,使用WebPackDevServer作为API代理,前端通过如下启动外壳业务,打包脚本见server/sonar-web/config/webpack.config.js

# 启动业务(使用NodeJS提供Server)node server/sonar-web/scripts/start.js# 打包(后续交给Tomcat处理)node server/sonar-web/scripts/build.js

SonarQube如何分析代码AST?

这里采用插件实现,比如Java在这里可以找到,分析后将转为通用格式发给Web进行处理

如果项目组需要定制Custom Rule,就可以通过访问onMethodInvocationFound实现自己的规则

Appendix

学到的其它技巧

// ThreadLocal更优雅的启动方法ThreadLocal<Boolean> CACHING_ENABLED = ThreadLocal.withInitial(() -> Boolean.FALSE);