保护变形:分析 Kafka 项目
您有没有想过跨国公司的项目源代码中可能潜藏着哪些错误?不要错过在开源 apache kafka 项目中发现 pvs-studio 静态分析器检测到的有趣错误的机会。
介绍
apache kafka 是一个著名的开源项目,主要用 java 编写。 linkedin 于 2011 年将其开发为消息代理,即各种系统组件的数据管道。如今,它已成为同类产品中最受欢迎的解决方案之一。
准备好看看引擎盖下的内容了吗?
附注
只是想简单说明一下标题。它参考了弗朗茨·卡夫卡的《变形记》,其中主角变成了可怕的害虫。我们的静态分析器致力于防止您的项目变身为可怕的害虫转变为一个巨大的错误,所以对“变形记”说不。
哦不,虫子
所有的幽默都源于痛苦
这不是我的话;这句话出自理查德·普赖尔之口。但这有什么关系呢?我想告诉你的第一件事是一个愚蠢的错误。然而,在多次尝试理解程序无法正常运行的原因后,遇到如下示例的情况令人沮丧:
@override public keyvalueiterator<windowed>, v> backwardfetch( k keyfrom, k keyto, instant timefrom, instant timeto) { .... if (keyfrom == null && keyfrom == null) { // <p>如您所见,这是任何开发人员都无法避免的事情——一个微不足道的拼写错误。在第一个条件下,开发人员希望使用以下逻辑表达式:<br></p>keyfrom == null && keyto == null分析器发出两个警告:
v6001 在“&&”运算符的左侧和右侧有相同的子表达式“keyfrom == null”。 readonlywindowstorestub.java 327、readonlywindowstorestub.java 327
v6007 表达式“keyfrom == null”始终为 false。 readonlywindowstorestub.java 329
我们可以明白为什么。对于每个开发人员来说,这种可笑的打字错误都是永恒的。虽然我们可以花很多时间寻找它们,但要回忆起它们潜伏的地方可不是小菜一碟。
在同一个类中,另一个方法中存在完全相同的错误。我认为称其为复制面食是公平的。
@override public keyvalueiterator<windowed>, v> fetch( k keyfrom, k keyto, instant timefrom, instant timeto) { .... navigablemap<k v> kvmap = data.get(now); if (kvmap != null) { navigablemap<k v> kvsubmap; if (keyfrom == null && keyfrom == null) { // <p>以下是相同的警告:</p> <p>v6007 表达式“keyfrom == null”始终为 false。 readonlywindowstorestub.java 273 </p> <p>v6001 在“&&”运算符的左侧和右侧有相同的子表达式“keyfrom == null”。 readonlywindowstorestub.java 271, readonlywindowstorestub.java 271</p> <p>不用担心——我们不必一次查看数百行代码。 pvs-studio 非常擅长处理此类简单的事情。解决一些更具挑战性的事情怎么样?</p> <h3> 可变同步 </h3> <p>java 中 <em>synchronized</em> 关键字的用途是什么?在这里,我将只关注同步方法,而不是块。根据 <a style="color:#f60; text-decoration:underline;" href="https://www.php.cn/zt/15715.html" target="_blank">oracle</a> 文档,<em>synchronized</em> 关键字将方法声明为同步,以确保与实例的线程安全交互。如果一个线程调用该实例的同步方法,则尝试调用同一实例的同步方法的其他线程将被阻塞(即它们的执行将被挂起)。它们将被阻塞,直到第一个线程调用的方法处理其执行。当实例对多个线程可见时,需要执行此操作。此类实例的读/写操作只能通过同步方法执行。 </p> <p>开发人员违反了 <em>sensor</em> 类中的规则,如下面的简化代码片段所示。对实例字段的读/写操作可以通过同步和非同步两种方式执行。它可能会导致竞争条件并使输出不可预测。<br></p>private final map<metricname kafkametric> metrics; public void checkquotas(long timems) { // <p>分析器警告如下所示:</p> <p>v6102 “metrics”字段同步不一致。考虑在所有用途上同步该字段。传感器.java 49,传感器.java 254</p> <p>如果不同的线程可以同时更改实例状态,则允许此操作的方法应该同步。如果程序没有预料到多个线程可以与实例交互,那么使其方法同步是没有意义的。最坏的情况下,甚至会损害程序性能。</p> <p>程序中有很多这样的错误。这是分析器发出警告的类似代码片段:<br></p>private final prefixkeyformatter prefixkeyformatter; @override public synchronized void destroy() { // ( prefixkeyformatter.addprefix(record.key), record.value ), batch ); } @override public synchronized void deleterange(....) { // <p>分析器警告:</p> <p>v6102 “prefixkeyformatter”字段同步不一致。考虑在所有用途上同步该字段。 logicalkeyvaluesegment.java 60、logicalkeyvaluesegment.java 247</p> <h3> 迭代器、迭代器、迭代器…… </h3> <p>在示例中,一行中同时出现两个相当令人不快的错误。我将在文章的一部分中解释它们的性质。这是一个代码片段:<br></p>private final map<string uuid> topicids = new hashmap(); private map<string kafkafuturevoid> handledeletetopicsusingnames(....) { .... collection<string> topicnames = new arraylist(topicnamecollection); for (final string topicname : topicnames) { kafkafutureimpl<void> future = new kafkafutureimpl(); if (alltopics.remove(topicname) == null) { .... } else { topicnames.remove(topicids.remove(topicname)); // <p>这就是分析仪向我们展示的内容:</p> <p>v6066 作为参数传递的对象类型与集合类型不兼容:string、uuid。 mockadminclient.java 569</p> <p>v6053 'arraylist' 类型的 'topicnames' 集合在迭代过程中被修改。可能会发生concurrentmodificationexception。 mockadminclient.java 569</p> <p>现在这是一个很大的困境!这是怎么回事,我们该如何解决?! </p> <p>首先,我们来谈谈集合和泛型。使用集合的泛型类型可以帮助我们避免 <em>classcastexceptions</em> 以及转换类型时的繁琐构造。 </p> <p>如果我们在初始化集合时指定了某种数据类型并添加了不兼容的类型,编译器将不会编译代码。 </p> <p>这是一个例子:<br></p>public class test { public static void main(string[] args) { set<string> set = new hashset(); set.add("str"); set.add(uuid.randomuuid()); // java.util.uuid cannot be converted to // java.lang.string } } </string>但是,如果我们从集合中删除不兼容的类型,则不会抛出异常。该方法返回false。
这是一个例子:
public class test { public static void main(string[] args) { set<string> set = new hashset(); set.add("abc"); set.add("def"); system.out.println(set.remove(new integer(13))); // false } } </string>这是浪费时间。如果我们在代码中遇到类似的情况,很可能这是一个错误。我建议你回到本章开头的代码并尝试找出类似的情况。
其次,我们来谈谈迭代器。关于集合的迭代我们可以讨论很长时间。我不想让您感到厌烦或偏离主题,因此我将只介绍要点,以确保我们理解为什么会收到警告。
那么,我们如何迭代这里的集合呢?代码片段中的 for 循环如下所示:
for (type collectionelem : collection) { .... }for 循环条目只是语法糖。其结构与此相同:
for (iterator<type> iter = collection.iterator(); iter.hasnext();) { type collectionelem = iter.next(); .... } </type>我们基本上使用集合迭代器。好了,就这样安排好了!现在,我们来讨论concurrentmodificationexception。
concurrentmodificationexception 是一个涵盖单线程和多线程程序中的一系列情况的异常。在这里,我们重点关注单线程。我们很容易找到解释。让我们看一下 oracle 文档:当方法检测到不支持它的对象的并行修改时,它可以抛出异常。在我们的例子中,当迭代器运行时,我们从集合中删除对象。这可能会导致迭代器抛出 concurrentmodificationexception。
迭代器如何知道何时抛出异常?如果我们查看 arraylist 集合,我们会看到它的父级 abstactlist 具有 modcount 字段,用于存储对集合的修改次数:
protected transient int modcount = 0;以下是 arraylist 类中 modcount 计数器的一些用法:
public boolean add(e e) { modcount++; add(e, elementdata, size); return true; } private void fastremove(object[] es, int i) { modcount++; final int newsize; if ((newsize = size - 1) > i) system.arraycopy(es, i + 1, es, i, newsize - i); es[size = newsize] = null; }因此,每次修改集合时,计数器都会递增。
顺便说一句,fastremove 方法用于 remove 方法,我们在循环中使用该方法。
这是 arraylist 迭代器内部工作的小代码片段:
private class itr implements iterator<e> { .... int expectedmodcount = modcount; final void checkforcomodification() { if (modcount != expectedmodcount) // <p>让我解释一下最后一个片段。如果集合修改与预期的修改数量(即创建迭代器之前的初始修改与迭代器操作的数量之和)不匹配,则会抛出 <em>concurrentmodificationexception</em> 。只有当我们在迭代集合的同时使用其方法修改集合时(即 <strong> 与迭代器并行 </strong> ),这才有可能。这就是第二个警告的内容。</p> <p>所以,我已经向您解释了分析器消息。现在让我们把它们放在一起:</p> <p>当 <em>iterator</em> 仍在运行时,我们尝试从集合中删除一个元素:<br></p>topicnames.remove(topicids.remove(topicname)); // topicsnames – collection<string> // topicsids – map<string uuid></string></string>但是,由于不兼容的元素被传递到arraylist进行删除(remove方法从topicids返回一个uuid对象),因此修改计数不会增加,但是对象不会被删除。简而言之,该代码部分是初级的。
我斗胆猜测开发者的意图是明确的。如果是这种情况,解决这两个警告的一种方法可能如下:
collection<string> topicnames = new arraylist(topicnamecollection); list<string> removableitems = new arraylist(); for (final string topicname : topicnames) { kafkafutureimpl<void> future = new kafkafutureimpl(); if (alltopics.remove(topicname) == null) { .... } else { topicids.remove(topicname); removableitems.add(topicname); future.complete(null); } .... } topicnames.removeall(removableitems); </void></string></string>空虚,甜蜜的空虚
如果没有我们一直以来最喜欢的null及其潜在问题,我们会去哪里,对吧?让我向您展示分析器发出以下警告的代码片段:
v6008 函数“removestaticmember”中可能存在对“oldmember”的空取消引用。 consumergroup.java 311、consumergroup.java 323
@override public void removemember(string memberid) { consumergroupmember oldmember = members.remove(memberid); .... removestaticmember(oldmember); .... } private void removestaticmember(consumergroupmember oldmember) { if (oldmember.instanceid() != null) { staticmembers.remove(oldmember.instanceid()); } }如果 members 不包含带有 memberid 键的对象,oldmember 将为 null。它可能会导致 removestaticmember 方法中出现 nullpointerexception。
繁荣!检查参数是否为 null:
if (oldmember != null && oldmember.instanceid() != null) {下一个错误将是本文中的最后一个错误 - 我想以积极的态度来总结。下面的代码以及本文开头的代码有一个常见且愚蠢的拼写错误。然而,它肯定会导致不愉快的后果。
让我们看一下这个代码片段:
protected SchemaAndValue roundTrip(...., SchemaAndValue input) { String serialized = Values.convertToString(input.schema(), input.value()); if (input != null && input.value() != null) { .... } .... }是的,没错。该方法实际上首先访问 input 对象,然后检查它是否引用 null.
v6060 “输入”引用在验证为空之前已被使用。 valuestest.java 1212、valuestest.java 1213
再次,我会指出这样的拼写错误是可以的。然而,它们可能会导致一些非常糟糕的结果。手动在代码中搜索这些内容既困难又低效。
结论
总之,我想回到上一点。手动搜索代码以查找所有这些错误是一项非常耗时且乏味的任务。对于像我所展示的那些长期潜伏在代码中的问题来说,这并不罕见。最后一个错误可以追溯到 2018 年。这就是为什么使用静态分析工具是个好主意。如果您想了解有关 pvs-studio(我们用来检测所有这些错误的工具)的更多信息,您可以在此处了解更多信息。
仅此而已。让我们把事情总结到这里吧。 “哦,如果我看不到你,下午好,晚上好,晚安。”
我差点忘了!抓住链接以了解有关开源项目免费许可证的更多信息。
以上就是保护变形:分析 Kafka 项目的详细内容,更多请关注其它相关文章!