Code Style不是个人喜好问题,它会影响工作效率,团队应将其当做工程实践予以重视
Code Style需要端到端的工具支持,尽早解决问题,避免技术债
以Checkstyle作为核心工具支撑Java项目的Code Style实施方案
我是右侧风格的忠实拥趸,如果在我工作的项目中看到有左侧风格的代码,你猜猜我的反应是什么
嗯,可能我确实对代码风格有些强迫症,但事实上,Code Style并不仅仅是代码是否好看那么简单,如果没有按照惯例来编写代码,甚至会让阅读者产生疑惑。
private Listener listener = new Listener() // So Listener looks like a class?
{}; // Oops, it is an interface
如果代码可读性还不足以打动你,那么想象一下这个场景,你的同事告诉你他修复了两个空指针问题,请你帮忙Code Review,你查看了这个文件的修订历史,乍看之下有许多改动,看来是个大动作。然而事实上,绝大部分改动是代码格式调整,只有两处改动与需要Review的问题相关的。
看来这位同事的IDE使用了不同的自动缩进设置,导致所有行都产生了缩进
之所以会产生以上这些影响工作效率的问题,是因为团队没有重视Code Style,没有把它当做一项工程实践,既没有对其达成一致,也没有正确地使用工具帮助实施。
本文将重点介绍Java项目的Code Style的工具支持,但在此之前,你的团队需要一起做一些决定:
我们都知道人工监督检查的方式是不可持续和不可靠的,来看看有哪些工具可以提供帮助吧。
工程实践不能没有自动化工具支持,在java生态圈中,Code Style工具最出名的应该是Checkstyle了,它可以通过XML形式的外部DSL来定义Code Style的检查风格,比如你可以从这里找到Google的Java Checkstyle配置文件。这里我不会详细介绍Checkstyle本身,相反,我会更多地探讨如何工程化地使用Checkstyle,在交付代码的各个活动中,我们都可以用到Checkstyle,360°无死角检查。
和Code Style相关的代码交付生命周期
为了贯彻不让不符合约定的代码流入代码库的决定,可以优先在服务端设置Code Style的检查关卡。
优先守住代码提交时的服务端检查,可以考虑使用CI服务器来实现
从实现层面上说,有两种方式:
一是在SCM(Source Control Management,例如Git/SVN)服务端设置检查项,如果不达标则拒绝提交,但这种方式相对不容易实现,而且一般SCM服务端也不由开发团队管理,设置起来不灵活也不方便。
二是利用持续集成服务器,开发团队的每一次提交都会触发一次构建,我们可以在构建脚本中加入Checkstyle检查,如果有不达标的代码则让构建失败以便告诉提交者立即修复Style问题。我更推荐这个方案,因为相关的工具支持都很成熟,实现简单,而且构建过程可以在开发者的本地环境复制,以便在后续改进中将Checkstyle检查前移,提供更快的反馈。如果团队使用Maven/Gradle等构建工具,可以用插件的方式实现Checkstyle检查并嵌入到整个构建过程中。这样CI服务器只要调用构建脚本就行了。
在开发者本地实现验证,反馈关口前移
在实现了CI验证后,就可以着手实现开发者本地验证了,这样开发者就不用等到提交代码到服务端后才会获得反馈了。由于之前采用的是构建工具的插件方案,所以开发者在本地运行构建就能实现验证了。这里以Gradle为例,简单介绍一下所需的配置
# build.gradle
# omitted plugins
apply plugin: 'checkstyle'
checkstyle {
configFile = file("config/checkstyle.xml") //指定checkstyle配置文件
toolVersion = "7.4" //指定checkstyle工具的版本,部分style规则有版本要求
}
checkstyleTest.exclude "**/ContractVerifierTest**" // 忽略检查生成代码,这个锅我们不背
// 如果出现checkstyle warning也使构建失败,插件默认只支持checkstyle error失败 https://github.com/gradle/gradle/issues/881
tasks.withType(Checkstyle).each { checkstyleTask ->
checkstyleTask.doLast {
reports.all { report ->
def outputFile = report.destination
if (outputFile.exists() && outputFile.text.contains("<error ")) {
throw new GradleException("There were checkstyle warnings! For more info check $outputFile")
}
}
}
}
现在只需要一条命令,每个开发者就能在本地验证Code Style了。你可以在这里找到Gradle Checkstyle Plugin的详细配置文档,如果你使用Maven,则可以参考这里。
➜ court-booking-backend (master) ✗ ./gradlew check
Starting a Gradle Daemon (subsequent builds will be faster)
:compileJava
:compileGroovy UP-TO-DATE
:processResources UP-TO-DATE
:classes
:checkstyleMain
[ant:checkstyle] [WARN] /Users/twer/Workspace/restbucks/court-booking-backend/src/main/java/com/restbucks/courtbooking/http/CourtRestController.java:16: 'method def' child have incorrect indentation level 4, expected level should be 8. [Indentation]
:checkstyleMain FAILED
FAILURE: Build failed with an exception.
* Where:
Build file '/Users/twer/Workspace/restbucks/court-booking-backend/build.gradle' line: 83
* What went wrong:
Execution failed for task ':checkstyleMain'.
> There were checkstyle warnings! For more info check /Users/twer/Workspace/restbucks/court-booking-backend/build/reports/checkstyle/main.xml
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 15.539 secs
让机器代劳琐事
有时候,开发者修改了代码后会忘记执行本地检查就提交代码了,最好能够在提交代码前自动执行检查。如果你使用Git的话,可能会想到Git commit hook,比如这是我常用的pre-commit hook
#!/bin/sh
# From gist at https://gist.github.com/chadmaughan/5889802
# stash any unstaged changes
git stash -q --keep-index
# run the tests with the gradle wrapper
./gradlew clean build
# store the last exit code in a variable
RESULT=$?
# unstash the unstashed changes
git stash pop -q
# return the './gradlew build' exit code
exit $RESULT
将该脚本拷贝到.git/hooks/
下,在执行git commit
的时候就会自动触发检查了,如果检查失败则提交失败。但问题是.git
并不能提交到远程代码仓库,那么除了人工分发和拷贝外,有没有更好的方式在团队中共享这个机制呢?
可以曲线救国!把pre-commit纳入版本控制(如下面的config/pre-commit
),再使用构建工具的扩展机制来自动完成拷贝工作,这样可以间接实现git hooks的团队间共享。
# build.gradle
task installGitHooks(type: Copy) { //将pre-commit拷贝到指定位置
from new File(rootProject.rootDir, 'config/pre-commit')
into {
new File(rootProject.rootDir, '.git/hooks')
}
fileMode 0755
}
build.dependsOn installGitHooks //设置执行build任务时会自动触发installGitHooks任务
实时反馈
之前基于构建工具的方案都很好,但是对于开发者来说,最好的还能将反馈前移到编辑时,并且可视化。所幸的是,Checkstyle的生态系统非常成熟,各主流IDE都有插件支持,以Intellij Idea为例,可以使用checkstyle-idea插件,让团队成员手工设置插件,使用项目的checkstyle配置文件即可(我目前还没有找到自动化配置的方式,或许gradle idea插件可以?)
checkstyle-idea插件配置和效果
有了自动实时检查,最好还能将IDE的自动格式化与Checkstyle配置文件挂钩,否则自动格式化反倒给你添麻烦了。
为IDE导入checkstyle配置文件作为自动格式化的依据
如果你连自动格式化都懒得按,那可以试试Save Actions插件,它可以在Intellij保存文件时自动执行代码格式化等动作。
这个插件目前对部分文件有些问题,可以通过File path exclusion忽略
好了,希望以上这些招数可以解救Java Code Style强迫症 :)
事件:全球知名IT咨询公司ThoughtWorks于2016年11月发布了其最新一期技术雷达,其中将“Jenkins as a deployment pipeline”列为“暂缓”。
Jenkins以持续集成闻名,进入持续交付时代后,常被人们用来尝试搭建deployment pipeline,我本人以前也乐此不疲。遗憾的是Jenkins的设计是以单个job为核心,deployment pipeline的实现需要通过官方或社区插件来支持,看上去都能实现,但实际使用中总让我觉得差了点什么。最近的一则广告可以完美表达这种体验:
不拼凑,纯pipeline?
ThoughtWorks坦言作此评价是相当冒险的,因为其在此领域有一款竞争产品:GoCD。GoCD曾是一款商业产品,现已追随开源大潮,身边一些朋友尝试之后褒贬不一,有人反馈它忠实还原了《持续交付》中提到的pipeline,也有人反馈较难上手。我想趁技术雷达这个热点分享一些GoCD的使用经验,希望对正在尝试GoCD的同学有所帮助。
利益相关:本人目前就职于ThoughtWorks,但在此之前就已经是GoCD的用户
用惯Jenkins job做持续集成的同学,往往一上来就被GoCD的配置界面搞得晕头转向:
我只是想运行一下
mvn clean package
怎么有这么多东西要设置,stage,job,task都是什么鬼?
信息量好大
GoCD在设计之初就以deployment pipeline作为“一等公民”,实现复杂交付流程是其强项,但如果你需要的只是持续集成,就有点杀鸡用牛刀了。因此,重要的话说三遍:
如果你不需要deployment pipeline,不要使用GoCD。
如果你不需要deployment pipeline,不要使用GoCD。
如果你不需要deployment pipeline,不要使用GoCD。
如果看到这里,你还没有关闭页面,那让我们来看一下GoCD的pipeline元素吧。
P代表pipeline,S代表stage,J代表job,T代表task
pipeline可由若干个stage组成,stage之间可以设置依赖关系,默认上游stage失败的时候不会触发下游stage。stage可由多个job组成,但多个job一般用在并行任务的用例中(例如并行构建多个模块),它们之间是没有依赖关系的,所以如果你希望某个stage执行一系列有依赖关系动作,应该使用单个job并为其设置多个task,而不是多个job。这里比较容易产生误会的是job,因为它和Jenkins job同名。一个典型的pipeline可能会按如下设计:
每个stage代表一个阶段,build&test负责构建和单元测试,Int_Deploy负责自动化端到端测试,UAT_Deploy负责手工测试,Prd_Deploy则负责部署生产环境,每个stage最简可由一个job组成,job中的task依次完成自动化任务
pipeline, stage, job, task使得GoCD可以组合串行、并行执行,实现复杂、精巧的工作流。但就像硬币的另一面,这些概念也提高了入门门槛,再加上原有的UI交互设计得比较繁琐,往往需要来回地切换编辑页面才能完成整个pipeline的设置,也难怪用户抱怨了。
不过好消息是,“Have a better getting-started experience”已列入GoCD roadmap,一个全新的”quick edit”功能也已经发布(需要16.9.0
以上)。
全新的”quick edit”功能,让你可以在单个页面即完成pipeline的配置,操作更简便
而对于熟手来说,本文后面提到的“实现pipeline as code”更合胃口
曾经有朋友向我吐槽GoCD的task太难用了,每个task只能执行一条命令导致每个job都有十几个task。
琐碎的tasks,使用前
其实,你需要的是一款叫做“script-executor-task”的GoCD插件。是的,你没有看错,GoCD也是有插件的!有了这款插件后,你就可以像shell脚本一样编排指令从而愉快地合并臃肿的task了
使用后
但是,值得一提的是,这个插件的初衷是简化task中命令的书写和排序,而不提倡滥用它编排大量琐碎的指令。不管是用Jenkins还是GoCD,最佳实践是将指令放到脚本文件中并纳入代码版本仓库(SCM)。可以签出的脚本方便团队所有人查看,更改也有迹可循,便于协作;另一方面脚本与工具的耦合也最小(往往就是一行命令),我们将在“实现Pipeline as Code”一节中继续讨论这个话题。
pipeline的各个环节本质上是在验证构建出的artifact(以下翻译为二进制包)是否符合质量标准,这就要求pipeline能够正确识别和传递artifact。只生成一次二进制包是pipeline设计中的一条重要原则,下游步骤应该重用上游步骤生成的二进制包。 相比每次从源代码构建二进制包,这节约了宝贵的反馈时间,更重要的是它实现了“你所测试的二进制包就是将要发布的二进制包”的配置管理需求。
GoCD对此提供内建支持:publish artifacts和fetch artifact task(相比Jenkins需要copy artifact plugin并且需要细心选择上游job,详见基于Jenkins实现的部署流水线中共享二进制包)。
上游构建stage将artifact到GoCD自带的artefact repository
下游部署stage从构建stage抓取artifact
一个容易出现误解的地方是,在没有使用publish/fetch artifact功能的情况下,试图在同一个pipeline的stage间共享artifacts,这很可能造成artifact传递错误,严重的时候可能造成向生产环境发布未经测试的二进制包。
如上例中,分别在commit-stage和acceptance-stage中取消publish/fetch
build/version
,只要这两个stage都分配在同一个go-agent上执行,也不会报错。假设现在pipeline build number为892
,运行pipeline build number为890
的acceptance-stage也会取得一份build/version
文件,但这份文件的来源是该go-agent上最近一次commit-stage运行后生成的(很可能是由pipeline build number892
),未必是pipeline build number为890
的commit-stage生成的,这样就出现了artifact版本错位。
隐蔽的artifact版本错误
重视artifact repository并且正确实现artifact共享是一条合格的deployment pipeline的重要标志,只有这样artifact的来源才能够回溯,才能检查它是否符合了发布的标准,才有信心真正实现一键发布到生产环境。
应用publish/fetch artifact是生成正确的Value Stream Map的前提,通过Value Stream Map可以直观地观测artifact经历的质量检查步骤和结果,作为是否发布此artifact的前置条件。
最后多嘴一句,虽然GoCD提供了内置的artifact repository,但我强烈推荐使用专用的artifact repository产品(例如java常见的sonatype nexus或者私有Docker Registry)。这些产品往往有更好的GUI和周边工具支持,可以帮助你更好地管理artifacts。在这种方案中,我建议使用GoCD的artifact repository来publish/fetch artifact的唯一标识符(通常以文件形式),在各pipeline及其stage之间共享这个唯一标识符,而artifact本身的publish/fetch则交给专用的artifact repository,并通过唯一标识符来识别。
专用artifact repository方案
严格来说,不管是GoCD还是Jenkins,早就可以通过编辑config文件或使用API来实现pipeline as code了,但它们都不易使用。前者因为config文件掌管着全局配置,粒度太粗,实际上只可能由专人维护,成为瓶颈。后者则往往需要开发客户端程序。随着infrastructure as code概念的流行,开发团队希望更灵活且更可靠地管理自己的pipeline(如果你用过Travis CI,会对这种方式很熟悉)。从16.7
开始,GoCD提供了更友好的pipeline as code支持,可以将通过yaml或json定义pipeline,并将配置文件放到SCM(git或其他)中,GoCD会自动获取定义文件并生成pipeline。
GoCD可以兼容手工配置和文件配置,所以你可以在部分pipeline上尝试这种技术
那么pipeline定义文件是放在应用源代码仓库还是单独放在独立代码仓库呢?我的建议是:都可以。但是如果deployment-pipeline含有部署环节,且部署不同环境需要不同的环境变量时,我建议把流水线本身拆开:
构建环节作为一条单独的pipeline,这条pipeline由自动触发的stage组成,目标是构建artifact,如果有条件的话还进行一些端到到的自动化验证。这条pipeline的定义文件可以和项目源码仓库放在一起,因为pipeline的改变常常也影响了artifact本身的构建,它们的变化节奏应该是一致的。
部署环节作为一条(或多条,视环境数量决定)单独的pipeline,这条pipeline由fetch artifact开头,其定义文件可以和部署脚本及环境变量放在一起,它们的变化节奏应该是一致的。与负责构建的pipeline分开的原因是,当你想为QA环境部署一次配置变更时(如果你使用了特性开关,这种情况很常见),往往并不希望等待pipeline重新再构建一次artifact。
部署pipeline与构建pipeline分离,可以实现 configuration deployment,单独部署环境变量变更
deployment-pipeline不是设计出来的,而是演化来的
——出自模板:XXX不是设计出来的,而是演化来的
deployment-pipeline的初衷是希望能够通过自动化和可视化来消除交付活动中的瓶颈,但如果不精心维护,pipeline自身可能也会出现瓶颈。例如随着自动化测试用例逐渐增多,反馈周期也会随之变长,这时需要重构pipeline以便消除瓶颈,但如何重构,重构的效果是需要用数据来度量的。Jenkins有一些插件可以统计job的平均执行时间,job失败后的平均恢复时间等指标,可以用来指导团队重构pipeline。遗憾的是,GoCD对此没有内建的功能支持,而plugin还不够丰富,暂时存在空白。让我们期待官方和社区在这方面有所作为吧 :)
感谢你耐心看完本文,最后把重要的话再说三遍:
如果你不需要deployment pipeline,不要使用gocd。
如果你不需要deployment pipeline,不要使用gocd。
如果你不需要deployment pipeline,不要使用gocd。
但是这年头,应用软件交付怎么会不需要deployment pipeline呢?