宁静·致远


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • RSS

结束是为了更好的开始

发表于 2018-05-04 | 阅读次数

这篇文章是在回北京的飞机上写的开头,原文标题是《我亲手关闭了我创建的系统》,但觉得有点悲情,正好今天是耳总在公司的last day,索性改成了现在的标题。文章内容有点乱,看不懂的人您就当碎碎念吧。看懂的人,就跟我一起缅怀那段时光吧。
另外,文章的标题有点悲情,但不是想通过它去博取眼球或者哗众取宠,只是想通过这篇文章,来表达自己对这个系统的复杂情感,也向过去和现在为这块业务战斗过的同事们致敬。

文中提到的这个系统创建于2015年,是属于我们这个行业比较基础的系统。在整个业务线复杂的业务场景中,这个系统规模不算大,但就它的历程来讲,还是比较富有戏剧色彩的。
之所以说富有戏剧色彩,是因为它是两个团队“竞争”下的产物。之所以说是竞争,是因为当时这块业务由两个团队并行研发,并行接入。为什么会有这样的局面,我至今无从揣测高层的想法。也许老板们是想确保项目的成功,多一个团队参与能增大项目成功的概率,毕竟那是2015年整个事业部最重要的项目之一。也许老板们是想让两个团队合作,一起确保项目的成功。然而,这只是也许,事实就是:两个团队在做相同的事情,于是大家之间的“竞争”也就开始了,以前和谐的关系瞬间也不存在了……

为了确保项目的成功,我动员了团队所有的精兵强将参与到项目中,十几个人在一个会议室中封闭了8个月的时间。项目开始的头三个月,团队实行类似于996的作息时间。但就实际时间来讲,几乎每天是到半夜的。在那段时间里,每个人都扛着巨大的压力在工作,毕竟在系统没上线之前,你得到的信任和支持是有限的。期间,也有同学因为感受不到信任而愤然离开,也有人因为不愿趟这个浑水选择了置身事外,但参与到这个项目中的人都付出了最大的努力。也就是在那段时间,我的痛风第二次发作,瘸着一条腿,7月份整整一个月没休息过一天,居然也挺过来了。

在所有人的努力下,系统一期于7月初成功上线。上线之初,所支撑的业务就有了很大的增长,令大家备受鼓舞,更加信心百倍地在小黑屋里封闭着,直到年底。当然,这期间发生了一件震动互联网界的事情,就是公司被兼并了。由于这个业务与控股公司业务存在高度重叠,所以整个团队的前景也蒙上了一层阴影。兼并之后的整合如期而至,我们的系统整合很快也展开了。当然,最开始的时候,控股方的老板是希望停掉我们的系统的,但在经过充分的沟通之后,对方发现了我们系统的可取之处,于是系统得以继续保留。最终的模式变为双方系统互相补充,皆大欢喜。在这期间,双方的团队围绕这块业务进行了深入的沟通和合作,光QQ群里面的成员就多达几十人。

时光荏苒,转眼来到了2018年。由于购买的数据到期,加之数据成本比较高的缘故,是否继续购买数据,成了摆在我们面前的一个难题。从情感层面而言,团队是希望自己辛苦搭建的系统能够继续运转下去的。但是从事情本身来说,在两边的系统业务效果几乎没有差异的情况下,花费大几百万维持系统的运转是一件不划算的事情。在经过慎重的数据对比和评估之后,我们决定,在数据到期之时,关闭我们自己的系统,全面接入控股公司的系统。虽有不舍,但在大趋势面前,我们能做的,就是顺势而为。

回顾围绕这个系统所度过的3年的时间,虽然结局不是那么美好,但一路走来,扪心自问,自己是否有遗憾?我的答案是没有的,因为在这个过程中,自己已经尽了最大努力去做好每件事情,因此,已无遗憾……

写到这里,我们的这个系统的故事也就结束了。当然,结束是为了更好的开始,写到这里叫未完待续……

另外,这里必须要提一下当时与我们竞争的团队。那是一支非常优秀的团队,尤其是虾和岑喆,都是能力非常强的。只是很遗憾因为那段特殊的日子里,我们无法以一种更好的方式进行合作。之后的很多时候,我不止一次做过情景假设,如果让我再选择一次,我会怎么做?我想我会放下戒备心理,选择大家一起合作来把事情做好,只是这只能是假设了……虾、岑喆,如果你们能够看到这篇文章,请收下我的敬意和歉意。

在这里,也向一起战斗过的战友致敬,耿大爷、63、虹姐、苏林、马教授、小轲轲、朱大爷、maomao、跃嘉、子龙、昆哥、小杰、耳总、亚南、国英、永杰、鑫爷,你们是最棒的。那些还留在公司的同学们,愿我们能珍惜在一起的时光,开心快乐;那些已经离开的小伙伴们,世界不大不小,希望以后还能相见;如果不能,在这里愿你一切安好……

To 耳总:说到付出的牺牲,也许你是首当其冲的。当你一个人守护着老系统,无法接触新业务的孤独感,那一坨屎一样的代码一定是让你备受煎熬,以至于你今天都不能释怀。但不可否认的是,正因为你作为坚强后盾,才解了我们的后顾之忧。当然后续你依然为新业务作出了巨大的贡献,你自己也得到了极大的成长,我想这一切,已经值了。今天是你的last day,我们一天来到公司入职,如今各奔东西,虽有不舍,但天下没有不散的筵席。就像我在朋友圈中说的,来时你是青葱少年,去时已能披荆斩棘。这4年的时间,你不负众人,不负光阴。海阔凭鱼跃,天高任鸟飞,一路走好!

Caffeine-比Guava Cache更好的缓存

发表于 2018-03-26 | 阅读次数

前言

说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经,它的性能是非常不错的。但所谓长江后浪推前浪,总会有更加优秀的技术出现。今天,我就来介绍一个比Guava Cache性能更高的缓存框架:Caffeine。

下面的内容是转载的一篇译文,如果需要查看译文原文,请点击这里,英语好的同学也可以直接查看英文原作。

==========以下是原文内容==========

缓存是提升性能的通用方法,现在大多数的缓存实现都使用了经典的技术。这篇文章中,我们会发掘Caffeine中的现代的实现方法。Caffeine是一个开源的Java缓存库,它能提供高命中率和出色的并发能力。期望读者们能被这些想法激发,进而将它们应用到任何你喜欢的编程语言中。

驱逐策略

缓存的驱逐策略是为了预测哪些数据在短期内最可能被再次用到,从而提升缓存的命中率。由于简洁的实现、高效的运行时表现以及在常规的使用场景下有不错的命中率,LRU(Least Recently Used)策略或许是最流行的驱逐策略。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。

现代缓存扩展了对历史数据的使用,结合就近程度(recency)和访问频次(frequency)来更好的预测数据。其中一种保留历史信息的方式是使用popularity sketch(一种压缩、概率性的数据结构)来从一大堆访问事件中定位频繁的访问者。可以参考CountMin Sketch算法,它由计数矩阵和多个哈希方法实现。发生一次读取时,矩阵中每行对应的计数器增加计数,估算频率时,取数据对应是所有行中计数的最小值。这个方法让我们从空间、效率、以及适配矩阵的长宽引起的哈希碰撞的错误率上做权衡。

CountMin Sketch

Window TinyLFU(W-TinyLFU)算法将sketch作为过滤器,当新来的数据比要驱逐的数据高频时,这个数据才会被缓存接纳。这个许可窗口给予每个数据项积累热度的机会,而不是立即过滤掉。这避免了持续的未命中,特别是在突然流量暴涨的的场景中,一些短暂的重复流量就不会被长期保留。为了刷新历史数据,一个时间衰减进程被周期性或增量的执行,给所有计数器减半。

Window TinyLFU

对于长期保留的数据,W-TinyLFU使用了分段LRU(Segmented LRU,缩写SLRU)策略。起初,一个数据项存储被存储在试用段(probationary segment)中,在后续被访问到时,它会被提升到保护段(protected segment)中(保护段占总容量的80%)。保护段满后,有的数据会被淘汰回试用段,这也可能级联的触发试用段的淘汰。这套机制确保了访问间隔小的热数据被保存下来,而被重复访问少的冷数据则被回收。

如图中数据库和搜索场景的结果展示,通过考虑就近程度和频率能大大提升LRU的表现。一些高级的策略,像ARC,LIRS和W-TinyLFU都提供了接近最理想的命中率。想看更多的场景测试,请查看相应的论文,也可以在使用simulator来测试自己的场景。

过期策略

过期的实现里,往往每个数据项拥有不同的过期时间。因为容量的限制,过期后数据需要被懒淘汰,否则这些已过期的脏数据会污染到整个缓存。一般缓存中会启用专有的清扫线程周期性的遍历清理缓存。这个策略相比在每次读写操作时按照过期时间排序的优先队列来清理过期缓存要好,因为后台线程隐藏了的过期数据清除的时间开销。

鉴于大多数场景里不同数据项使用的都是固定的过期时长,Caffien采用了统一过期时间的方式。这个限制让用O(1)的有序队列组织数据成为可能。针对数据的写后过期,维护了一个写入顺序队列,针对读后过期,维护了一个读取顺序队列。缓存能复用驱逐策略下的队列以及下面将要介绍的并发机制,让过期的数据项在缓存的维护阶段被抛弃掉。

并发

由于在大多数的缓存策略中,数据的读取都会伴随对缓存状态的写操作,并发的缓存读取被视为一个难点问题。传统的解决方式是用同步锁。这可以通过将缓存的数据划成多个分区来进行锁拆分优化。不幸的是热点数据所持有的锁会比其他数据更常的被占有,在这种场景下锁拆分的性能提升也就没那么好了。当单个锁的竞争成为瓶颈后,接下来的经典的优化方式是只更新单个数据的元数据信息,以及使用随机采样、基于FIFO的驱逐策略来减少数据操作。这些策略会带来高性能的读和低性能的写,同时在选择驱逐对象时也比较困难。

另一种可行方案来自于数据库理论,通过提交日志的方式来扩展写的性能。写入操作先记入日志中,随后异步的批量执行,而不是立即写入到数据结构中。这种思想可以应用到缓存中,执行哈希表的操作,将操作记录到缓冲区,然后在合适的时机执行缓冲区中的内容。这个策略依然需要同步锁或者tryLock,不同的是把对锁的竞争转移到对缓冲区的追加写上。

在Caffeine中,有一组缓冲区被用来记录读写。一次访问首先会被因线程而异的哈希到stripped ring buffer上,当检测到竞争时,缓冲区会自动扩容。一个ring buffer容量满载后,会触发异步的执行操作,而后续的对该ring buffer的写入会被丢弃,直到这个ring buffer可被使用。虽然因为ring buffer容量满而无法被记录该访问,但缓存值依然会返回给调用方。这种策略信息的丢失不会带来大的影响,因为W-TinyLFU能识别出我们希望保存的热点数据。通过使用因线程而异的哈希算法替代在数据项的键上做哈希,缓存避免了瞬时的热点key的竞争问题。

写数据时,采用更传统的并发队列,每次变更会引起一次立即的执行。虽然数据的损失是不可接受的,但我们仍然有很多方法可以来优化写缓冲区。所有类型的缓冲区都被多个的线程写入,但却通过单个线程来执行。这种多生产者/单个消费者的模式允许了更简单、高效的算法来实现。

缓冲区和细粒度的写带来了单个数据项的操作乱序的竞态条件。插入、读取、更新、删除都可能被各种顺序的重放,如果这个策略控制的不合适,则可能引起悬垂索引。解决方案是通过状态机来定义单个数据项的生命周期。

在基准测试中,缓冲区随着哈希表的增长而增长,它的的使用相对更节省资源。读的性能随着CPU的核数线性增长,是哈希表吞吐量的33%。写入有10%的性能损耗,这是因为更新哈希表时的竞争是最主要的开销。

结论

还有许多实用的话题没有被覆盖到。包括最小化内存的技巧,当复杂度上升时保证质量的测试技术以及确定优化是否值得的性能分析方法。这些都是缓存的实践者需要关注的点,因为一旦这些被忽视,就很难重拾掌控缓存带来的复杂度的信心。

Caffeine的设计实现来自于大量的洞见和许多贡献者的共同努力。它这些年的演化离不开一些人的帮助:Charles Fry, Adam Zell, Gil Einziger, Roy Friedman, Kevin Bourrillion, Bob Lee, Doug Lea, Josh Bloch, Bob Lane, Nitsan Wakart, Thomas Müeller, Dominic Tootell, Louis Wasserman, and Vladimir Blagojevic. Thanks to Nitsan Wakart, Adam Zell, Roy Friedman, and Will Chu for their feedback on drafts of this article.

一起因限流引发的故障

发表于 2018-03-15 | 阅读次数

1.前言

限流是保证系统高可用性的一项很重要的举措,但是任何事情都有利弊,限流措施如果使用不当,有可能会引起比较大的问题,反而导致系统可用性降低。

2.详细经过

上周,我们负责的一个入口系统的几台服务器突然收到大量的线程池满的告警,根据以往的经验,初步判断是上游请求量激增导致的。通过监控也印证了我们的判断,因为激增的流量比较大,为保护集群,避免出现雪崩,我们开启了限流措施。本来这是保证集群可用性的一种常见措施,但因为其中某些环节做得不够细致,导致了后面故障的发生。

开启了限流措施之后,我们很快就收到了另外一个监控(搜索结果为空率)的告警。此监控是反映系统返回结果为空的比例的,当这个比例超过设定的阈值就会告警。当时这个告警飙升到了40%多,这个比例是非常恐怖的,意味着接近一半的搜索没有结果了。当时我们立刻断定是限流的影响,为了保护集群,同时不影响用户体验,我们将限流阈值调高,这时搜索为空率监控开始下降。

5分钟后,上游系统同事反馈系统请求量在下降,我们自己的监控也有所体现,为了避免伤害用户,我们关掉了限流,于是故障恢复。

3.故障分析

3.1 Hash策略不合理

事后分析原因,发现是平台上有个代理商发布了一个错误的航线价格,这个价格非常低,导致大量的用户前来搜索和购买。由于我们的集群是根据航线+日期进行hash,导致短时间内,特定的几台机器收到了大量的请求,导致机器负载过高,最终不可用。如果能够让集群的请求分布更加均匀,那么也不会导致雪崩的风险而不得不限流。

3.2 限流措施不合理

故障以前系统的限流措施是:被限流后,请求立即返回为空的结果。但这里更加合理的措施是:如果被限流,让请求在队列里面等待一会再尝试请求。这样能够达到削峰的效果,让请求更加平滑,同时也降低对用户体验的伤害。

3.3 请求合并不合理

为减轻对后端系统的压力,我们系统中保留了本地缓存,这样能够针对相同的请求使用缓存,也能提高缓存的利用率。但因为系统单次请求的结果集比较大,为降低对本地缓存的占用,我们并未将最终结果进行缓存,而是缓存了初始结果,这样就意味着在利用缓存的时候,还需要在本地经过一些计算才能返回结果,这无疑增加了系统的压力。

所以后续我们可以做进一步优化,在一个时间片内,如果存在相同的请求,如果结果还没有计算出来,就等结果计算出来再返回;如果结果已经计算出来并在缓存中有效,直接返回结果即可。这样也能进一步降低系统的负载。

3.4 限流粒度太粗

故障以前系统是对所有请求进行限流,但故障时,只是某条航线收到了大量的请求,其余航线请求是正常的。开启限流之后,虽然大量的问题航线请求被限制住了,但同时也限制了正常的用户请求。所以在限流的粒度上,我们还可以做得更加细致。当然这里有个前提,就是我们需要在第一时间知道哪条航线请求量飙升,这样才能有针对性地进行限流。

4. 总结

高可用是一个很大的话题,就其中的限流措施来说,也可以做得非常细致。这里只是针对本次故障做一个简单的总结,对此如果你有任何意见或建议,还望不吝赐教。

服务化架构-服务的熔断

发表于 2018-02-28 | 阅读次数

什么是熔断

软件行业中的很多技术都是借鉴于工业的,熔断也不例外。熔断机制类似于工厂的保险丝/盒,当电流过大时,为了保护电器不受损,将电路熔断或者跳闸,断电后人工更换保险丝或者合闸。当然,在软件系统里面的熔断机制,其实可以实现自动熔断和恢复。

为什么要熔断

有人可能要问了,软件系统如果健壮性做得很好,需要熔断吗?
前面我们说过,熔断是针对下游的系统来说的。举个栗子,如果下游C系统出问题了,服务100%超时,这时候你可以先不去请求C了,直到C恢复再去请求。有人说我去请求了也没关系吧,反正请求和不请求都是失败,我干嘛要把事情搞复杂了?我又不知道C什么时候能恢复,权当去测试C的可用性了呗。

嗯,你说得有道理,不过需要每个请求都要去测试吗?当然没必要,而且很多时候下游系统的超时有可能会拖垮你的系统。纳尼?这又是为啥?

我们继续举例子,如果你的系统与C系统是通过同步的方式交互,那么意味着在C返回结果前,你的系统的工作线程是要阻塞在那里的,这也就会导致你的系统吞吐量的降低,毕竟线程是不能无限创建的。

熔断的实现

原理

熔断的实现思路很简单,简而言之就是失败率统计,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

实现要点

熔断器工作过程中,有3个状态:闭合、打开、半开。

  • 闭合(Closed)状态:我们需要一个调用失败的计数器,如果调用失败,则使失败次数加 1。如果最近失败次数超过了在给定时间内允许失败的阈值,则切换到断开 (Open) 状态。此时开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误,以回到正常工作的状态。在 Closed 状态下,错误计数器是基于时间的。在特定的时间间隔内会自动重置。这能够防止由于某次的偶然错误导致熔断器进入断开状态。也可以基于连续失败的次数。

  • 打开 (Open) 状态:在该状态下,对应用程序的请求会立即返回错误响应,而不调用后端的服务。这样也许比较粗暴,有些时候,我们可以 cache 住上次成功请求,直接返回缓存(当然,这个缓存放在本地内存就好了),如果没有缓存再返回错误(缓存的机制最好用在全站一样的数据,而不是用在不同的用户间不同的数据,因为后者需要缓存的数据有可能会很多)。

  • 半开(Half-Open)状态:允许应用程序一定数量的请求去调用服务。如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到闭合状态 (并且将错误计数器重置)。

如果这一定数量的请求有调用失败的情况,则认为导致之前调用失败的问题仍然存在,熔断器切回到断开状态,然后重置计时器来给系统一定的时间来修正错误。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。

Hystrix

在业界,一说到熔断器,很多人很容易就会联想到Netflix开源的Hystrix。是的Hyxtrix是一个非常优秀的保证系统高可用的组件,其中的熔断、舱壁(bulkhead)实现都能够帮助我们提高系统的可用性。

熔断的其他注意事项

在实现熔断器模式的时候,以下这些因素需可能需要考虑。

  • 避免侵入代码:熔断器应该作为一种比较基础的服务组件存在于系统中,尽量避免对业务代码的侵入,从而提高系统的可维护性。所以通用、可配置化是熔断组件的基本要求。

  • 状态监控:熔断器处于打开、半开状态时,应该能够在第一时间通知维护人员,让相关人员及时获知熔断器的状态,并根据其他信息决定下一步采取的措施。

  • 日志记录:熔断器应该能够记录所有失败的请求,以及一些可能会尝试成功的请求,使得管理员能够监控使用熔断器保护的服务的执行情况。

  • 性能考虑:熔断器的实现不应该阻塞并发的请求或者增加每次请求调用的负担。尤其是其中的对调用结果的统计,一般来说会成为一个共享的数据结构,这个会导致有锁的情况。在这种情况下,最好使用一些无锁的数据结构,或是 atomic 的原子操作。这样会带来更好的性能。

  • 错误区分。要能够区分出正常的调用失败和熔断失败,这样能够针对不同的失败制定不同的策略。比如对于正常的调用失败,可以采取重试机制;而对于熔断失败,那么快速失败即可。

  • 服务测试。在断开状态下,熔断器可以采用定期地 ping 一下远程的服务的健康检查接口,来判断服务是否恢复,而不是使用计时器来自动切换到半开状态。这样做的一个好处是,在服务恢复的情况下,不需要真实的用户流量就可以把状态从半开状态切回关闭状态。否则在半开状态下,即便服务已恢复了,也需要用户真实的请求来恢复,这会影响用户的真实请求。

  • 手动重置:在系统中对于失败操作的恢复时间是很难确定的,提供一个手动重置功能能够使得管理员可以手动地强制将熔断器切换到闭合状态。同样的,如果受熔断器保护的服务暂时不可用的话,管理员能够强制将熔断器设置为断开状态。

  • 熔断隔离:对于不同的服务,要设置不同的熔断器,使他们相互隔离,避免互相影响。

服务化架构-服务的限流与熔断

发表于 2018-02-03 | 阅读次数

前言

服务的限流、熔断是保证系统高可用的两个非常重要的举措,但有人会把限流和熔断的概念混淆,在这里做一个简单的区分。

为了方便说明,我们先做一个情景设定:有3个系统,A、B、C,他们之间的调用关系是这样的:A->B->C,也就是说,A是B的上游,C是B的下游。这里针对B系统做限流、熔断的说明。

限流

限流是针对上游系统的请求来说的,对于B来说,如果上游A的请求量过大,那么就需要对A做限流,来保护自己不被流量冲垮。
关于限流,请看这篇文章:《服务的限流》

熔断

熔断是针对下游的系统来说的。举个栗子,如果下游C系统出问题了,服务100%超时,这时候你可以先不去请求C了,直到C恢复再去请求。这种措施就叫熔断。
关于熔断,请看这篇文章:《服务的熔断》

StringBuilder你应该知道的几件事情

发表于 2018-01-02 | 阅读次数

字符串拼接是我们在编写程序时经常要写的代码,在很多的系统中,字符串相关的处理甚至占用了系统非常多的资源,尤其是内存。因此,在一些高性能的场景中,字符串拼接使用方式不合理,往往会导致无谓的内存开销,增加GC压力。

有些同学会直接使用”+”的方式进行字符串拼接,在Jdk7u40之前,这种方式我们是不推荐使用的,因为String是不可变对象,每次对String的”+”操作都会产生一个新的String对象,尤其是在对多个String进行拼接处理的时候,从而导致内存的浪费。但在Jdk7u40之后,-XX:+OptimizeStringConcat被默认打开,这样Jdk在进行Jit编译的时候,会自动将”+”形式的字符串拼接优化成StringBuilder append的方式。

但是,让编译器帮你优化这种方式你就可以高枕无忧了吗?答案是否定的,请看下面的内容。

1.new StringBuilder()合理吗?

很多时候我们使用new StringBuilder()来初始化实例,但这种方式是否合理呢?

通过阅读StringBuilder的源码我们可以看到,StringBuilder的内部存储是使用char[]来实现的,char[]的初始容量是16。在进行append时,首先会检查char[]容量是否够用,如果不够,则会创建一个新的char[],容量为原来容量的2倍,原来的char[]则会丢弃。看到这里,细心的同学可能会想了,这不是浪费了吗?是的,这就是不设置初始容量所带来的弊端,会导致一定的内存浪费。举个例子:

1
2
3
4
5
6
String a1 = "1234567890";//length 10
String a2 = "0123456789";//length 10
String a3 = "123456789";//length 9
String a4 = "987654321";//length 9

String a5 = new StringBuilder().append(a1).append(a2).append(a3).append(a4).toString();

上面这段代码的执行共创建了几个char[]呢?答案是4个,详细过程看下面的描述。

  1. new StringBuilder()的时候,如果我们不指定StringBuilder的容量,那么会按照默认值是16创建第一个数组,我们命名为C1。在append(a1)的时候,容量是够用的;

  2. 在append(a2)的时候,C1容量不够了需要扩容,于是创建一个容量为以前2倍的数组C2,并将旧的数组C1的内容以及a2的内容按顺序copy到新数组C2中,这时候C2的大小是32;

  3. 在append(a3)的时候,C2的容量是够用的,所以直接将a3的内容copy进C2;

  4. 在append(a4)的时候,C2的容量也不够用了,于是再创建一个容量为C2两倍的数组C3,并将旧的数组C2的内容以及a4的内容按顺序copy到新数组C3中,这时候C3的大小是64;

  5. 在toString的时候,是调用的new String(value, 0, count)。这样并不是将C3的内容直接转换成String,而是会copy出一个副本C4再根据C4创建出一个String来;这一点,建议大家仔细去看看StringBuilder的toString代码,你会发现更多的真相……

看完上面的描述,你会觉得这个过程中浪费了多少内存呢?最起码C1、C2是没用了的,也就是说浪费掉了48byte的空间。那如果你最初根据预估的字符串大小,设置了StringBuilder初始容量为38,那么就不会存在这样的浪费了,同时也会省去StringBuilder扩容的时间。

所以,在使用StringBuilder的时候,还是估算一下字符串大小,乖乖的设置一个初始容量吧。

2.不要重复创建StringBuilder

这里说的重复是指在循环中重复new StringBuilder实例。前面我们也说过,StringBuilder的扩容和toString()方法会造成大量的char[]浪费,如果你在一个循环里面创建了很多StringBuilder实例,那么可以想象浪费的内存有多大……所以在一个循环里面复用StringBuilder比较好的方式是调用StringBuilder.setLength(0)重置指针进行复用,这样最起码就能避免再次扩容的内存消耗了。

3.StringBuilder.toString()造成的内存浪费

前面我们也提到过,每次StringBuilder.toString的时候,会copy一个char[]生成一个String对象,这样就相当于白白浪费了StringBuilder里面的数组。当然这里可以采用一些黑科技来避免这种浪费,比如用Unsafe这种黑科技,绕过构造函数直接给String的char[]属性赋值,示例代码如下:

1
2
3
4
5
6
Unsafe unsafe =
long fieldOffset = Unsafe.getUnsafe();
unsafe.objectFieldOffset(String.class.getDeclaredField("value"));
String ret = new String();

unsafe.putObjectVolatile(ret, fieldOffset, char[]);//char[]为StringBuilder中的char[]

这种方式虽然能够避免内存浪费,但也存在一定的风险,所以使用的时候还是要慎重。

4.StringBuilder vs StringBuffer

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

使用SonarLint编写高质量代码

发表于 2017-12-08 | 阅读次数

Sonarqube是一个功能非常强大的代码质量检查、管理的工具。能够识别多种常用的编程语言,并能够通过设置不同的Rule
Sonar是一个代码质量管理的开源工具,它通过插件的形式能够识别常见的多种编程语言(例如Java, C#, PHP, Pythod等)代码质量问题。Sonar可以帮你分析出以下代码质量问题:
1.不遵循代码标准;
2.潜在的缺陷,比如空指针、bug;
3.代码重复;
4.注释率不足或过高;
5.糟糕的复杂度,比如if/循环嵌套太多、类/方法太大;
6.缺乏单元测试;

在公司中,一般是把SonarQube部署在服务器端,当开发人员提交代码时,Jenkins触发SonarQube进行代码检查,开发人员根据代码检查结果进行问题修复。但是这样,开发人员往往只会关注被阻断的代码问题,对于Sonar提示的设计、性能方面的问题往往视而不见。
所幸现在SonarSource开发了SonarLint插件,可以在IDE(Intellij Idea、Eclipse)中嵌入,这样开发人员不仅能使用SonarLint中内置的代码检查规则进行代码检查,也可以连接到远端服务器拉取远端规则。有了它,我们就可以在编写代码的过程中根据SonarLint的提示编写高质量代码了。

下面,将以Intellij Idea为基础介绍SonarLint插件的使用。

1 安装

在Idea的Plugins界面搜索“SonarLint”,在检索结果列表页找到对应的SonarLint,点击右侧的“Install”按钮,安装完毕重启Idea后即可完成插件的安装。

安装插件

2 代码检查

SonarLint插件安装后,就可以使用它对Idea中的项目进行代码检查了。SonarLint能够对单个文件、整个项目、从VCS(版本控制系统,比如git、svn等)拉取的被修改文件这3类进行检查。

开启SonarLint的检查有3个入口:

1.Analyze->SonarLint,如下图:

2.在打开的文件编辑区域点击右键,在弹出的上下文菜单中选择,如下图:

此方式只能对当前文件进行检查。

3.编辑区底部的Panel,这里可以在”Current file” Tab区域对当前文件进行检查,也可以在“Project files”Tab区域对文件进行全量和从VCS(版本控制系统,比如git、svn等)拉取的被修改文件进行检查,如下图:

执行代码检查后,在下面的SonarLint Panel区域就能显示代码检查的结果了:

3 远程配置

前面提到的代码检查,默认使用SonarLint内置的规则。我们也可以连接远端的SonarQube服务器,拉取服务器上配置的规则对代码进行检查,这样就能与代码提交时触发的检查规则保持一致了,具体方式如下:

1.首先在下图的界面点击“+”创建新的SonarQube Server:

2.在New SonarQube Server界面中选择右侧的选项,并输入SonarQube URL,点击next:

3.在Authentication界面选择Login/Password,并输入用户名和密码:

验证通过后点击Finish即可完成SonarQube Server的配置。

4.在SonarLint界面选择Server中配置的project,要与当前项目一致,否则可能会拉取错误的规则配置。

经过上面4步,你就可以通过SonarLint拉取的服务器上的代码规则进行代码检查了。Have fun:)

Sonar自定义插件开发指南

发表于 2017-11-29 | 阅读次数

1 Sonar简介

SonarQube是一个非常流行和强大的静态代码检查工具。它可以针对很多语言进行分析,比如Java、C#、JavaScript、PL/SQL等。SonarQube中默认提供了很多代码检查规则,但是有时候我们需要根据自己的需求编写规则,这篇文章会涵盖如何编写自定义规则的各个步骤。

2 环境准备

  • JDK 1.8
  • Intellij Idea
  • Maven 3.x或更高
  • SonarQube LTS 5.6
  • Sonar-runner-dist 2.4

3 开发步骤

3.1 创建Plugin项目

首先创建一个SonarPlugin项目,建议从SonarSource官方github站点下载模板项目,这样不至于遗漏配置。下载的地址是:下载链接

3.2 引入Pom依赖

将模板项目中的pom文件修改为自己项目对应的名称,比如groupId、artifactId、version、name、description,其他的不必修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.qunar.sonar</groupId>
<artifactId>qunar-java</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>sonar-plugin</packaging>

<name>Qunar SonarQube Java Rules</name>
<description>Qunar Java Rules for SonarQube</description>
<inceptionYear>2016</inceptionYear>

<properties>
<sonar.version>6.3</sonar.version> <!-- this 6.3 is only required to be compliant with SonarLint and it is required
even if you just want to be compliant with SonarQube 5.6 -->
<java.plugin.version>4.10.0.10260</java.plugin.version>
<sslr.version>1.21</sslr.version>
<gson.version>2.6.2</gson.version>
</properties>

<dependencies>
<!--请不要调整下面三项依赖的顺序,避免查看依赖的类时无法查看源码-->
<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>${sonar.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>java-frontend</artifactId>
<version>${java.plugin.version}</version>
</dependency>

<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>sonar-java-plugin</artifactId>
<!--<type>sonar-plugin</type>-->
<version>${java.plugin.version}</version>
<scope>provided</scope>
</dependency>
<!--请不要调整上述三项依赖的顺序,避免查看依赖的类时无法查看源码-->

<dependency>
<groupId>org.sonarsource.sslr-squid-bridge</groupId>
<artifactId>sslr-squid-bridge</artifactId>
<version>2.6.1</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.sonar.sslr</groupId>
<artifactId>sslr-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-plugin-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.sonar.sslr</groupId>
<artifactId>sslr-xpath</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>java-checks-testkit</artifactId>
<version>${java.plugin.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>

<dependency>
<groupId>org.sonarsource.sslr</groupId>
<artifactId>sslr-testing-harness</artifactId>
<version>${sslr.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>0.9.30</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.17</version>
<extensions>true</extensions>
<configuration>
<pluginKey>qunar-java</pluginKey>
<pluginName>QunarJava</pluginName>
<pluginClass>com.qunar.sonar.java.JavaRulesPlugin</pluginClass>
<sonarLintSupported>true</sonarLintSupported>
<sonarQubeMinVersion>5.6</sonarQubeMinVersion> <!-- allow to depend on API 6.x but run on LTS -->
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

<!-- only required to run UT - these are UT dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>test-compile</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
<type>jar</type>
</artifactItem>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/test-jars</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

3.3 编写具体的Rule

一个自定义规则的实现,首先在规则类前面使用@Rule注解声明规则的相关信息,比如key、name、description、priority、tag。其次要继承IssuableSubscriptionVisitor或者继承BaseTreeVisitor并实现JavaFileScanner接口,这两种实现方式存在一些差异,IssuableSubscriptionVisitor是一个抽象类,继承自SubscriptionVisitor,而SubscriptionVisitor则实现了JavaFileScanner接口;而BaseTreeVisitor则是实现了TreeVisitor接口。相比来说,JavaFileScanner更偏底层一些,直接是面向Java文件的分析,而TreeVisitor则是针对于Java文件的语法结构了,比如类、防止、赋值语句、catch块之类的了。一般情况下,推荐使用继承IssuableSubscriptionVisitor的方式,因为这个类的封装层次更高,更简便。在规则类中,你只需要重载nodesToVisit和visitNode方法就可以了。
nodesToVisit方法是说明该规则做检查的类型集合,比如METHOD_INVOCATION(方法调用)、CATCH(catch块)、MEMBER_SELECT(属性选择)等。比如下面的代码,就是对源码中的方法调用做规则检查。
visitNode是自定义规则中的核心部分,你所要实现的代码检查逻辑就是在这个方法中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import com.google.common.collect.ImmutableList;
import com.qunar.sonar.java.util.QualifiedNameUtil;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;

import java.util.List;

/**
* Created by fengfu on 2017/11/10.
*/
@Rule(
key = "DisallowedFDUsageCheck",
name = "Qunar Flight disallowed FD usage in InfoCenter check",
description = "Some kinds of usage for FD are forbidden in Flight BU of Qunar.com.",
priority = Priority.BLOCKER,
tags = {"bug"}
)
public class DisallowedFDUsageCheck extends IssuableSubscriptionVisitor {
private static final String[] FORBIDDEN_METHODS = {
"Infocenter.reloadFDData", "Infocenter.getFliterDateFDData",
"Infocenter.getFDData", "Infocenter.getValidFDPrice",
"Infocenter.getFDPrice"
};

@Override
public List<Tree.Kind> nodesToVisit() {
return ImmutableList.of(Tree.Kind.METHOD_INVOCATION);
}

@Override
public void visitNode(Tree tree) {
MethodInvocationTree mTree = (MethodInvocationTree)tree;
String methodName = QualifiedNameUtil.fullQualifiedName(mTree.methodSelect());

if (isForbiddenMethod(methodName)){
reportIssue(tree, methodName + "已被禁用,详情请查看Wiki:http://wiki.corp.qunar.com/confluence/pages/viewpage.action?pageId=182623492");
}

super.visitNode(tree);
}

private static boolean isForbiddenMethod(String methodName) {
for (String name : FORBIDDEN_METHODS) {
if (name.equals(methodName)) {
return true;
}
}
return false;
}

@Override
public void leaveNode(Tree tree) {
super.leaveNode(tree);
}
}

当然如果一些复杂的业务规则,可能你不得不实现JavaFileScanner接口了。如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import com.qunar.sonar.java.util.QualifiedNameUtil;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.*;

/**
* Created by fengfu on 2017/2/27.
*/
@Rule(
key = "QunarFlightDisallowedClassCheck",
name = "Qunar Flight disallowed class check",
description = "Some classes are forbidden in Flight BU of Qunar.com.",
priority = Priority.BLOCKER,
tags = {"bug"}
)
public class DisallowedClassCheck extends BaseTreeVisitor implements JavaFileScanner {
private static Logger logger = Loggers.get(DisallowedClassCheck.class);
private final String IMPORT_NAME = "com.qunar.base.meerkat.monitor.QMonitor";

public void scanFile(JavaFileScannerContext context) {
logger.info("Start to scan file {}", context.getFile().getName());
CompilationUnitTree cut = context.getTree();

for (ImportClauseTree importClauseTree : cut.imports()){
ImportTree importTree = null;
if (importClauseTree.is(Tree.Kind.IMPORT)) {
importTree = (ImportTree) importClauseTree;
}

if (importTree == null || importTree.isStatic()) {
continue;
}

String importName = QualifiedNameUtil.fullQualifiedName(importTree.qualifiedIdentifier());

if (IMPORT_NAME.equals(importName)){
logger.error("Disallowed class {} found.", IMPORT_NAME);
context.reportIssue(this, importTree, IMPORT_NAME + "已被禁用.");
}
}

scan(cut);
}
}

3.4 单元测试

规则检查代码写完之后,就可以针对这个规则进行单元测试了。单元测试的方法比较简单,就是使用JUnit框架编写test case,针对特定的目标文件,调用Sonar提供的JavaCheckVerifier.verify方法进行测试,示例代码如下:

1
2
3
4
5
6
7
8
9
10
import com.qunar.flight.sonar.java.checks.DisallowedClassCheck;
import org.junit.Test;
import org.sonar.java.checks.verifier.JavaCheckVerifier;

public class DisallowedClassCheckTest {
@Test
public void test() {
JavaCheckVerifier.verify("src/test/files/flight/DisallowedClassCase.java", new DisallowedClassCheck());
}
}

如果目标文件存在问题,那么控制台会打印出问题在目标文件中的位置:

1
2
3
4
5
6
7
8
9
java.lang.AssertionError: Unexpected at [4]

at org.sonar.java.checks.verifier.CheckVerifier.assertMultipleIssue(CheckVerifier.java:209)
at org.sonar.java.checks.verifier.CheckVerifier.checkIssues(CheckVerifier.java:181)
at org.sonar.java.checks.verifier.JavaCheckVerifier.scanFile(JavaCheckVerifier.java:274)
at org.sonar.java.checks.verifier.JavaCheckVerifier.scanFile(JavaCheckVerifier.java:256)
at org.sonar.java.checks.verifier.JavaCheckVerifier.scanFile(JavaCheckVerifier.java:222)
at org.sonar.java.checks.verifier.JavaCheckVerifier.verify(JavaCheckVerifier.java:105)
at com.qunar.flight.sonar.java.checks.test.DisallowedClassCheckTest.test(DisallowedClassCheckTest.java:10)

如果目标文件中不存在任何问题,会提示“java.lang.IllegalStateException: At least one issue expected”,意思就是目标文件中需要至少存在一个规则所要检查的问题。

3.5 编写RulesList

单元测试完成之后,就可以往RuleList的集合中中添加自定义规则规则了。这个RuleList只是一个简单的集合类,包含了所有你需要注册到Sonar中的规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.google.common.collect.ImmutableList;
import java.util.List;

import com.qunar.flight.sonar.java.checks.DisallowedClassCheck;
import com.qunar.flight.sonar.java.checks.DisallowedFDUsageCheck;
import com.qunar.flight.sonar.java.checks.GuavaCacheLoaderReturnNullCheck;
import org.sonar.plugins.java.api.JavaCheck;

public final class RulesList {

private RulesList() {
}

public static List<Class> getChecks() {
return ImmutableList.<Class>builder().addAll(getJavaChecks()).addAll(getJavaTestChecks()).build();
}

/**
* 在此方法中添加需要注册的规则
* @return
*/
public static List<Class<? extends JavaCheck>> getJavaChecks() {
return ImmutableList.<Class<? extends JavaCheck>>builder()
.add(DisallowedClassCheck.class)
.add(GuavaCacheLoaderReturnNullCheck.class)
.add(DisallowedFDUsageCheck.class)

.build();
}

public static List<Class<? extends JavaCheck>> getJavaTestChecks() {
return ImmutableList.<Class<? extends JavaCheck>>builder()
.build();
}
}

3.6 编写CustomRulesDefinition

CustomRulesDefinition可以理解为自定义规则集的元数据,在这个类中,首先要声明自定义规则集的repository,另外还需要将相应的规则集加入到前面声明的repository中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
mport com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.debt.DebtRemediationFunction;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.api.server.rule.RulesDefinitionAnnotationLoader;
import org.sonar.api.utils.AnnotationUtils;
import org.sonar.check.Cardinality;
import org.sonar.squidbridge.annotations.RuleTemplate;

import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import org.sonar.plugins.java.Java;
import java.util.List;
import java.util.Locale;

/**
* Declare rule metadata in server repository of rules.
* That allows to list the rules in the page "Rules".
*/
public class JavaRulesDefinition implements RulesDefinition {

// don't change that because the path is hard coded in CheckVerifier
private static final String RESOURCE_BASE_PATH = "/org/sonar/l10n/java/rules/squid";

public static final String REPOSITORY_KEY = "qunar-java";

private final Gson gson = new Gson();

@Override
public void define(Context context) {
NewRepository repository = context
.createRepository(REPOSITORY_KEY, Java.KEY)//定义Repository key
.setName("QunarJava");//定义Repository名称

List<Class> checks = RulesList.getChecks();//获取自定义的check列表
new RulesDefinitionAnnotationLoader().load(repository, Iterables.toArray(checks, Class.class));//获取注解信息

for (Class ruleClass : checks) {
newRule(ruleClass, repository);//将规则添加到Repository中
}
repository.done();
}

@VisibleForTesting
protected void newRule(Class<?> ruleClass, NewRepository repository) {

org.sonar.check.Rule ruleAnnotation = AnnotationUtils.getAnnotation(ruleClass, org.sonar.check.Rule.class);
if (ruleAnnotation == null) {
throw new IllegalArgumentException("No Rule annotation was found on " + ruleClass);
}
String ruleKey = ruleAnnotation.key();
if (StringUtils.isEmpty(ruleKey)) {
throw new IllegalArgumentException("No key is defined in Rule annotation of " + ruleClass);
}
NewRule rule = repository.rule(ruleKey);
if (rule == null) {
throw new IllegalStateException("No rule was created for " + ruleClass + " in " + repository.key());
}
ruleMetadata(ruleClass, rule);

rule.setTemplate(AnnotationUtils.getAnnotation(ruleClass, RuleTemplate.class) != null);
if (ruleAnnotation.cardinality() == Cardinality.MULTIPLE) {
throw new IllegalArgumentException("Cardinality is not supported, use the RuleTemplate annotation instead for " + ruleClass);
}
}

private String ruleMetadata(Class<?> ruleClass, NewRule rule) {
String metadataKey = rule.key();
org.sonar.java.RspecKey rspecKeyAnnotation = AnnotationUtils.getAnnotation(ruleClass, org.sonar.java.RspecKey.class);
if (rspecKeyAnnotation != null) {
metadataKey = rspecKeyAnnotation.value();
rule.setInternalKey(metadataKey);
}
addHtmlDescription(rule, metadataKey);
addMetadata(rule, metadataKey);
return metadataKey;
}

private void addMetadata(NewRule rule, String metadataKey) {
URL resource = JavaRulesDefinition.class.getResource(RESOURCE_BASE_PATH + "/" + metadataKey + "_java.json");
if (resource != null) {
RuleMetatada metatada = gson.fromJson(readResource(resource), RuleMetatada.class);
rule.setSeverity(metatada.defaultSeverity.toUpperCase(Locale.US));
rule.setName(metatada.title);
rule.addTags(metatada.tags);
rule.setType(RuleType.valueOf(metatada.type));
rule.setStatus(RuleStatus.valueOf(metatada.status.toUpperCase(Locale.US)));
if (metatada.remediation != null) {
rule.setDebtRemediationFunction(metatada.remediation.remediationFunction(rule.debtRemediationFunctions()));
rule.setGapDescription(metatada.remediation.linearDesc);
}
}
}

private static void addHtmlDescription(NewRule rule, String metadataKey) {
URL resource = JavaRulesDefinition.class.getResource(RESOURCE_BASE_PATH + "/" + metadataKey + "_java.html");
if (resource != null) {
rule.setHtmlDescription(readResource(resource));
}
}

private static String readResource(URL resource) {
try {
return Resources.toString(resource, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException("Failed to read: " + resource, e);
}
}

private static class RuleMetatada {
String title;
String status;
@Nullable
Remediation remediation;

String type;
String[] tags;
String defaultSeverity;
}

private static class Remediation {
String func;
String constantCost;
String linearDesc;
String linearOffset;
String linearFactor;

public DebtRemediationFunction remediationFunction(DebtRemediationFunctions drf) {
if (func.startsWith("Constant")) {
return drf.constantPerIssue(constantCost.replace("mn", "min"));
}
if ("Linear".equals(func)) {
return drf.linear(linearFactor.replace("mn", "min"));
}
return drf.linearWithOffset(linearFactor.replace("mn", "min"), linearOffset.replace("mn", "min"));
}
}
}

3.7 编写CustomJavaFileCheckRegistrar

CustomJavaFileCheckRegistrar要实现CheckRegistrar接口。CustomJavaFileCheckRegistrar类的作用是注册代码扫描时需要的规则类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.List;
import org.sonar.plugins.java.api.CheckRegistrar;
import org.sonar.plugins.java.api.JavaCheck;

/**
* Provide the "checks" (implementations of rules) classes that are going be executed during
* source code analysis.
*
* This class is a batch extension by implementing the {@link org.sonar.plugins.java.api.CheckRegistrar} interface.
*/
public class JavaFileCheckRegistrar implements CheckRegistrar {

/**
* Register the classes that will be used to instantiate checks during analysis.
*/
@Override
public void register(RegistrarContext registrarContext) {
// Call to registerClassesForRepository to associate the classes with the correct repository key
registrarContext.registerClassesForRepository(JavaRulesDefinition.REPOSITORY_KEY, checkClasses(), testCheckClasses());
}

/**
* Lists all the main checks provided by the plugin
*/
public static List<Class<? extends JavaCheck>> checkClasses() {
return RulesList.getJavaChecks();//返回RulesList中定义的checks
}

/**
* Lists all the test checks provided by the plugin
*/
public static List<Class<? extends JavaCheck>> testCheckClasses() {
return RulesList.getJavaTestChecks();//返回RulesList中定义的check classes
}
}

3.8 编写CustomJavaRulesPlugin

这个类是Sonar插件的入口,它继承了org.sonar.api.SonarPlugin类,包含了SonarQube启动时需要实例化的server扩展(CustomRulesDefinition)和代码分析时需要实例化的批量扩展(JavaFileCheckRegistrar)。

这个类基本不需要修改(除包名外),按照以下内容copy一份即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.sonar.api.Plugin;

/**
* Entry point of your plugin containing your custom rules
*/
public class JavaRulesPlugin implements Plugin {

@Override
public void define(Context context) {

// server extensions -> objects are instantiated during server startup
context.addExtension(CustomRulesDefinition.class);

// batch extensions -> objects are instantiated during code analysis
context.addExtension(CustomJavaFileCheckRegistrar.class);

}
}

至此,一个Sonar自定义插件所需的代码工作就结束了。剩下的就是进行整合性的测试,毕竟单元测试通过不代表插件不会对现有系统造成影响,有时候插件中的错误会导致SonarQube启动失败的。

5 整合测试

通过mvn package命令编译、打包,形成jar文件。将jar文件上传到SonarQube extensions/plugins目录下,重启SonarQube服务即可完成Sonar自定义规则插件的部署。部署结束,通过SonarQube bin目录下的脚本重启SonarQube,如果SonarQube启动成功,说明插件没问题,整合测试成功,你可以在sonar的rules模块中,搜索到自定义的规则了。

6 启用规则

插件部署到SonarQube之后,默认是不启用的,需要在SonarQube的管理界面中启用规则,才能让此规则在代码检查过程中生效。

至此,Sonar插件开发的流程就介绍完了。如前面所言,SonarQube是个强大的代码检查工具,一篇文章是无法介绍完的,尤其Sonar的语法树部分,涵盖了各种语言各种语法的方方面面,大家可以通过Sonar的官方文档了解更多的细节。

git命令大全

发表于 2017-11-14 | 阅读次数
命令 说明
git init 初始化本地git仓库(创建新仓库)
git config –global user.name “xxx” 配置用户名
git config –global user.email “xxx@xxx.com“ 配置邮件
git config –global color.ui true git status等命令自动着色
git config –global color.status auto
git config –global color.diff auto
git config –global color.branch auto
git config –global color.interactive auto
git config –global –unset http.proxy remove proxy configuration on git
git clone git+ssh://git@192.168.53.168/VT. clone远程仓库
git status 查看当前版本状态(是否修改)
git add xyz 添加xyz文件至index
git add . 增加当前子目录下所有更改过的文件至index
git commit -m ‘xxx’ 提交
git commit –amend -m ‘xxx’ 合并上一次提交(用于反复修改)
git commit -am ‘xxx’ 将add和commit合为一步
git rm xxx 删除index中的文件
git rm -r * 递归删除
git log 显示提交日志
git log -1 显示1行日志 -n为n行
git log -5
git log –stat 显示提交日志及相关变动文件
git log -p -m
git show dfb02e6e4f2f7b573337763e5c0013802e392818 显示某个提交的详细内容
git show dfb02 可只用commitid的前几位
git show HEAD 显示HEAD提交日志
git show HEAD^ 显示HEAD的父(上一个版本)的提交日志 ^^为上两个版本 ^5为上5个版本
git tag 显示已存在的tag
git tag -a v2.0 -m ‘xxx’ 增加v2.0的tag
git show v2.0 显示v2.0的日志及详细内容
git log v2.0 显示v2.0的日志
git diff 显示所有未添加至index的变更
git diff –cached 显示所有已添加index但还未commit的变更
git diff HEAD^ 比较与上一个版本的差异
git diff HEAD – ./lib 比较与HEAD版本lib目录的差异
git diff origin/master..master 比较远程分支master上有本地分支master上没有的
git diff origin/master..master –stat 只显示差异的文件,不显示具体内容
git remote add origin git+ssh://git@192.168.53.168/VT. 增加远程定义(用于push/pull/fetch)
git branch 显示本地分支
git branch –contains 50089 显示包含提交50089的分支
git branch -a 显示所有分支
git branch -r 显示所有原创分支
git branch –merged 显示所有已合并到当前分支的分支
git branch –no-merged 显示所有未合并到当前分支的分支
git branch -m master master_copy 本地分支改名
git checkout -b master_copy 从当前分支创建新分支master_copy并检出
git checkout -b master master_copy 上面的完整版
git checkout features/performance 检出已存在的features/performance分支
git checkout –track hotfixes/BJVEP933 检出远程分支hotfixes/BJVEP933并创建本地跟踪分支
git checkout v2.0 检出版本v2.0
git checkout -b devel origin/develop 从远程分支develop创建新本地分支devel并检出
git checkout – README 检出head版本的README文件(可用于修改错误回退)
git merge origin/master 合并远程master分支至当前分支
git cherry-pick ff44785404a8e 合并提交ff44785404a8e的修改
git push origin master 将当前分支push到远程master分支
git push origin :hotfixes/BJVEP933 删除远程仓库的hotfixes/BJVEP933分支
git push –tags 把所有tag推送到远程仓库
git fetch 获取所有远程分支(不更新本地分支,另需merge)
git fetch –prune 获取所有原创分支并清除服务器上已删掉的分支
git pull origin master 获取远程分支master并merge到当前分支
git mv README README2 重命名文件README为README2
git reset –hard HEAD 将当前版本重置为HEAD(通常用于merge失败回退)
git rebase
git branch -d hotfixes/BJVEP933 删除分支hotfixes/BJVEP933(本分支修改已合并到其他分支)
git branch -D hotfixes/BJVEP933 强制删除分支hotfixes/BJVEP933
git ls-files 列出git index包含的文件
git show-branch 图示当前分支历史
git show-branch –all 图示所有分支历史
git whatchanged 显示提交历史对应的文件修改
git revert dfb02e6e4f2f7b573337763e5c0013802e392818 撤销提交dfb02e6e4f2f7b573337763e5c0013802e392818
git ls-tree HEAD 内部命令:显示某个git对象
git rev-parse v2.0 内部命令:显示某个ref对于的SHA1 HASH
git reflog 显示所有提交,包括孤立节点
git show HEAD@{5}
git show master@{yesterday} 显示master分支昨天的状态
git log –pretty=format:’%h %s’ –graph 图示提交日志
git show HEAD~3
git show -s –pretty=raw 2be7fcb476
git stash 暂存当前修改,将所有至为HEAD状态
git stash list 查看所有暂存
git stash show -p stash@{0} 参考第一次暂存
git stash apply stash@{0} 应用第一次暂存
git grep “delete from” 文件中搜索文本“delete from”
git grep -e ‘#define’ –and -e SORT_DIRENT
git gc
git fsck

为什么Synchronized不能加在String和Integer等基本包装类型上

发表于 2017-09-05 | 阅读次数

同步块不应该加在String或基本包装类型上,如Byte、Short、Integer、Long、Float、Double、Boolean、Character。

String不能用作同步块的参数是因为String为不可变对象,任何String对象的改变都将产生一个新的String对象,这也将导致前面加的锁不会被释放。

Integer、Boolean、Double、Long不能作为同步块参数的原因是他们是基本包装类型,包装类型有特殊的逻辑,用一句话说就是Java的自动封箱和解箱操作会导致这些对象在经过运算后不再是原来的对象。

用复杂的话说就是:当把基本变量赋值给包装类型的变量(其实编译过后的操作就是调用包装类型的静态方法valueOf)或者调用静态valueOf方法时:

  • Boolean返回的是缓存的对象。
  • 整型(Byte,Short,Integer,Long)会检查该数字是否在1个字节可表示的有符号整数范围内(-128~127),是则返回缓存对象,否则返回新对象。
  • Character会缓存整型值为0~127的字符,同样会检查字符是否落在缓存范围中,是则返回,否则返回新对象。
  • Double和Float的valueOf方法始终返回新对象。
123…6
宁静·致远

宁静·致远

60 日志
9 标签
友情链接
  • 卡拉搜索
© 2020 宁静·致远
由 Hexo 强力驱动
主题 - NexT.Gemini