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静态文件 | 默认是 | 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); |