您有沒有想過跨國公司的專案原始碼中可能潛藏著哪些錯(cuò)誤?不要錯(cuò)過在開源 Apache Kafka 專案中發(fā)現(xiàn) PVS-Studio 靜態(tài)分析器偵測到的有趣錯(cuò)誤的機(jī)會。
介紹
Apache Kafka 是一個(gè)著名的開源項(xiàng)目,主要用 Java 寫。 LinkedIn 於 2011 年將其開發(fā)為訊息代理,即各種系統(tǒng)元件的資料管道。如今,它已成為同類產(chǎn)品中最受歡迎的解決方案之一。
準(zhǔn)備好看看引擎蓋下的內(nèi)容了嗎?
附註
只是想簡單說明一下標(biāo)題。它參考了弗朗茨·卡夫卡的《變形記》,其中主角變成了可怕的害蟲。我們的靜態(tài)分析器致力於防止您的專案變身為可怕的害蟲轉(zhuǎn)變?yōu)橐粋€(gè)巨大的錯(cuò)誤,所以對「變形記」說不。
喔不,蟲子
所有的幽默都源自於痛苦
這不是我的話;這句話出自理查德·普賴爾之口。但這有什麼關(guān)係呢?我想告訴你的第一件事是一個(gè)愚蠢的錯(cuò)誤。然而,在多次嘗試?yán)斫獬淌綗o法正常運(yùn)作的原因後,遇到以下範(fàn)例的情況令人沮喪:
@Override public KeyValueIterator<Windowed<K>, V> backwardFetch( K keyFrom, K keyTo, Instant timeFrom, Instant timeTo) { .... if (keyFrom == null && keyFrom == null) { // <= kvSubMap = kvMap; } else if (keyFrom == null) { kvSubMap = kvMap.headMap(keyTo, true); } else if (keyTo == null) { kvSubMap = kvMap.tailMap(keyFrom, true); } else { // keyFrom != null and KeyTo != null kvSubMap = kvMap.subMap(keyFrom, true, keyTo, true); } .... }
如您所見,這是任何開發(fā)人員都無法避免的事情——一個(gè)微不足道的拼字錯(cuò)誤。在第一個(gè)條件下,開發(fā)人員希望使用下列邏輯表達(dá)式:
keyFrom == null && keyTo == null
分析器發(fā)出兩個(gè)警告:
V6001 在「&&」運(yùn)算子的左邊和右邊有相同的子運(yùn)算式「keyFrom == null」。 ReadOnlyWindowStoreStub.java 327、ReadOnlyWindowStoreStub.java 327
V6007 表達(dá)式「keyFrom == null」總是 false。 ReadOnlyWindowStoreStub.java 329
我們可以明白為什麼。對於每個(gè)開發(fā)人員來說,這種可笑的打字錯(cuò)誤都是永恆的。雖然我們可以花很多時(shí)間尋找它們,但要回憶起它們潛伏的地方可不是小菜一碟。
在同一個(gè)類別中,另一個(gè)方法中存在完全相同的錯(cuò)誤。我認(rèn)為稱其為複製麵食是公平的。
@Override public KeyValueIterator<Windowed<K>, 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) { // <= kvSubMap = kvMap; } else if (keyFrom == null) { kvSubMap = kvMap.headMap(keyTo, true); } else if (keyTo == null) { kvSubMap = kvMap.tailMap(keyFrom, true); } else { // keyFrom != null and KeyTo != null kvSubMap = kvMap.subMap(keyFrom, true, keyTo, true); } } .... }
以下是相同的警告:
V6007 表達(dá)式「keyFrom == null」總是 false。 ReadOnlyWindowStoreStub.java 273
V6001 在「&&」運(yùn)算子的左邊和右邊有相同的子運(yùn)算式「keyFrom == null」。 ReadOnlyWindowStoreStub.java 271, ReadOnlyWindowStoreStub.java 271
不用擔(dān)心——我們不必一次查看數(shù)百行程式碼。 PVS-Studio 非常擅長處理這類簡單的事情。解決一些更具挑戰(zhàn)性的事情怎麼樣?
可變同步
Java 中 synchronized 關(guān)鍵字的用途是什麼?在這裡,我將只關(guān)注同步方法,而不是區(qū)塊。根據(jù) Oracle 文檔,synchronized 關(guān)鍵字將方法聲明為同步,以確保與實(shí)例的線程安全互動(dòng)。如果一個(gè)執(zhí)行緒呼叫該實(shí)例的同步方法,則嘗試呼叫相同實(shí)例的同步方法的其他執(zhí)行緒將被阻塞(即它們的執(zhí)行將被掛起)。它們將被阻塞,直到第一個(gè)執(zhí)行緒呼叫的方法處理其執(zhí)行。當(dāng)實(shí)例對多個(gè)執(zhí)行緒可見時(shí),需要執(zhí)行此操作。此類實(shí)例的讀取/寫入操作只能透過同步方法執(zhí)行。
開發(fā)人員違反了 Sensor 類別中的規(guī)則,如下面的簡化程式碼片段所示。實(shí)例欄位的讀取/寫入操作可以透過同步和非同步兩種方式執(zhí)行。它可能會導(dǎo)致競爭條件並使輸出變得不可預(yù)測。
private final Map<MetricName, KafkaMetric> metrics; public void checkQuotas(long timeMs) { // <= for (KafkaMetric metric : this.metrics.values()) { MetricConfig config = metric.config(); if (config != null) { .... } } .... } public synchronized boolean add(CompoundStat stat, // <= MetricConfig config) { .... if (!metrics.containsKey(metric.metricName())) { metrics.put(metric.metricName(), metric); } .... } public synchronized boolean add(MetricName metricName, // <= MeasurableStat stat, MetricConfig config) { if (hasExpired()) { return false; } else if (metrics.containsKey(metricName)) { return true; } else { .... metrics.put(metric.metricName(), metric); return true; } }
分析器警告如下:
V6102 “metrics”欄位同步不一致。考慮在所有用途上同步該欄位。感測器.java 49,感測器.java 254
如果不同的執(zhí)行緒可以同時(shí)變更實(shí)例狀態(tài),則允許此操作的方法應(yīng)該同步。如果程式?jīng)]有預(yù)料到多個(gè)執(zhí)行緒可以與實(shí)例交互,則使其方法同步是沒有意義的。最壞的情況下,甚至?xí)p害程式效能。
程式中有很多這樣的錯(cuò)誤。這是分析器發(fā)出警告的類似程式碼片段:
private final PrefixKeyFormatter prefixKeyFormatter; @Override public synchronized void destroy() { // <= .... Bytes keyPrefix = prefixKeyFormatter.getPrefix(); .... } @Override public void addToBatch(....) { // <= physicalStore.addToBatch( new KeyValue<>( prefixKeyFormatter.addPrefix(record.key), record.value ), batch ); } @Override public synchronized void deleteRange(....) { // <= physicalStore.deleteRange( prefixKeyFormatter.addPrefix(keyFrom), prefixKeyFormatter.addPrefix(keyTo) ); } @Override public synchronized void put(....) { // <= physicalStore.put( prefixKeyFormatter.addPrefix(key), value ); }
分析器警告:
V6102 “prefixKeyFormatter”欄位同步不一致。考慮在所有用途上同步該欄位。 LogicalKeyValueSegment.java 60、LogicalKeyValueSegment.java 247
Iterator, iterator, and iterator again...
In the example, there are two rather unpleasant errors within one line at once. I'll explain their nature within the part of the article. Here's a code snippet:
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)); // <= future.complete(null); } .... } }
That's what the analyzer shows us:
V6066 The type of object passed as argument is incompatible with the type of collection: String, Uuid. MockAdminClient.java 569
V6053 The 'topicNames' collection of 'ArrayList' type is modified while iteration is in progress. ConcurrentModificationException may occur. MockAdminClient.java 569
Now that's a big dilemma! What's going on here, and how should we address it?!
First, let's talk about collections and generics. Using the generic types of collections helps us avoid ClassCastExceptions and cumbersome constructs where we convert types.
If we specify a certain data type when initializing a collection and add an incompatible type, the compiler won't compile the code.
Here's an example:
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 } }
However, if we delete an incompatible type from our Set, no exception will be thrown. The method returns false.
Here's an example:
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 } }
It's a waste of time. Most likely, if we encounter something like this in the code, this is an error. I suggest you go back to the code at the beginning of this subchapter and try to spot a similar case.
Second, let's talk about the Iterator. We can talk about iterating through collections for a long time. I don't want to bore you or digress from the main topic, so I'll just cover the key points to ensure we understand why we get the warning.
So, how do we iterate through the collection here? Here is what the for loop in the code fragment looks like:
for (Type collectionElem : collection) { .... }
The for loop entry is just syntactic sugar. The construction is equivalent to this one:
for (Iterator<Type> iter = collection.iterator(); iter.hasNext();) { Type collectionElem = iter.next(); .... }
We're basically working with the collection iterator. All right, that's sorted! Now, let's discuss ConcurrentModificationException.
ConcurrentModificationException is an exception that covers a range of situations both in single-threaded and multi-threaded programs. Here, we're focusing on single-threading. We can find an explanation quite easily. Let's take a peek at the Oracle docs: a method can throw the exception when it detects parallel modification of an object that doesn't support it. In our case, while the iterator is running, we delete objects from the collection. This may cause the iterator to throw a ConcurrentModificationException.
How does the iterator know when to throw the exception? If we look at the ArrayList collection, we see that its parent, AbstactList, has the modCount field that stores the number of modifications to the collection:
protected transient int modCount = 0;
Here are some usages of the modCount counter in the ArrayList class:
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; }
So, the counter is incremented each time when the collection is modified.
Btw, the fastRemove method is used in the remove method, which we use inside the loop.
Here's the small code fragment of the ArrayList iterator inner workings:
private class Itr implements Iterator<E> { .... int expectedModCount = modCount; final void checkForComodification() { if (modCount != expectedModCount) // <= throw new ConcurrentModificationException(); } public E next() { checkForComodification(); .... } public void remove() { .... checkForComodification(); try { ArrayList.this.remove(lastRet); .... expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } .... public void add(E e) { checkForComodification(); try { .... ArrayList.this.add(i, e); .... expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } }
Let me explain that last fragment. If the collection modifications don't match the expected number of modifications (which is the sum of the initial modifications before the iterator was created and the number of the iterator operations), a ConcurrentModificationException is thrown. That's only possible when we modify the collection using its methods while iterating over it (i.e. in parallel with the iterator). That's what the second warning is about.
So, I've explained you the analyzer messages. Now let's put it all together:
We attempt to delete an element from the collection when the Iterator is still running:
topicNames.remove(topicIds.remove(topicName)); // topicsNames – Collection<String> // topicsIds – Map<String, UUID>
However, since the incompatible element is passed to ArrayList for deletion (the remove method returns a UUID object from topicIds), the modification count won't increase, but the object won't be deleted. Simply put, that code section is rudimentary.
I'd venture to guess that the developer's intent is clear. If that's the case, one way to fix these two warnings could be as follows:
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, sweet void
Where would we go without our all-time favorite null and its potential problems, right? Let me show you the code fragment for which the analyzer issued the following warning:
V6008 Potential null dereference of 'oldMember' in function 'removeStaticMember'. 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()); } }
If members doesn't contain an object with the memberId key, oldMember will be null. It can lead to a NullPointerException in the removeStaticMember method.
Boom! The parameter is checked for null:
if (oldMember != null && oldMember.instanceId() != null) {
The next error will be the last one in the article—I'd like to wrap things up on a positive note. The code below—as well as the one at the beginning of this article—has a common and silly typo. However, it can certainly lead to unpleasant consequences.
Let's take a look at this code fragment:
protected SchemaAndValue roundTrip(...., SchemaAndValue input) { String serialized = Values.convertToString(input.schema(), input.value()); if (input != null && input.value() != null) { .... } .... }
Yeah, that's right. The method actually accesses the input object first, and then checks whether it's referencing null.
V6060 The 'input' reference was utilized before it was verified against null. ValuesTest.java 1212, ValuesTest.java 1213
Again, I'll note that such typos are ok. However, they can lead to some pretty nasty results. It's tough and inefficient to search for these things in the code manually.
Conclusion
In sum, I'd like to circle back to the previous point. Manually searching through the code for all these errors is a very time-consuming and tedious task. It's not unusual for issues like the ones I've shown to lurk in code for a long time. The last bug dates back to 2018. That's why it's a good idea to use static analysis tools. If you'd like to know more about PVS-Studio, the tool we have used to detect all those errors, you can find out more here.
That's all. Let's wrap things up here. "Oh, and in case I don't see ya, good afternoon, good evening, and good night."
I almost forgot! Catch a link to learn more about a free license for open-source projects.
以上がBelay the Metamorphosis: Kafka プロジェクトの分析の詳細(xì)內(nèi)容です。詳細(xì)については、PHP 中國語 Web サイトの他の関連記事を參照してください。

ホットAIツール

Undress AI Tool
脫衣畫像を無料で

Undresser.AI Undress
リアルなヌード寫真を作成する AI 搭載アプリ

AI Clothes Remover
寫真から衣服を削除するオンライン AI ツール。

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中國語版
中國語版、とても使いやすい

ゼンドスタジオ 13.0.1
強(qiáng)力な PHP 統(tǒng)合開発環(huán)境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

Javaは、Java19での完了可能なストリーム(ProjectReactorなど)、仮想スレッドの使用など、非同期プログラミングをサポートしています。 1.CompletableFutureチェーンコールを通じてコードの読みやすさとメンテナンスを改善し、タスクオーケストレーションと例外処理をサポートします。 2。ProjectReactorは、バックプレッシャーメカニズムとリッチ演算子を備えた応答性プログラミングを?qū)g裝するためのモノとフラックスタイプを提供します。 3.仮想スレッドは、同時(shí)性コストを削減し、I/O集約型タスクに適しており、従來のプラットフォームスレッドよりも軽量で拡張が容易です。各方法には適用可能なシナリオがあり、適切なツールをお客様のニーズに応じて選択する必要があり、混合モデルはシンプルさを維持するために避ける必要があります

Javaでは、列挙は固定定數(shù)セットを表すのに適しています。ベストプラクティスには以下が含まれます。1。列挙を使用して固定狀態(tài)またはオプションを表して、タイプの安全性と読みやすさを改善します。 2.フィールド、コンストラクター、ヘルパーメソッドなどの定義など、柔軟性を高めるために、酵素にプロパティとメソッドを追加します。 3. enummapとEnumsetを使用して、パフォーマンスとタイプの安全性を向上させ、配列に??基づいてより効率的であるためです。 4.動(dòng)的値、頻繁な変更、複雑なロジックシナリオなどの列挙の悪用を避けてください。これらは他の方法に置き換える必要があります。列挙の正しい使用は、コードの品質(zhì)を改善し、エラーを減らすことができますが、適用される境界に注意を払う必要があります。

Javanioは、Java 1.4によって導(dǎo)入された新しいIoapiです。 1)バッファとチャネルを?qū)澫螭趣筏皮い蓼埂?)バッファ、チャネル、セレクターのコアコンポーネント、3)ノンブロッキングモードをサポートし、4)従來のIOよりも効率的に並行接続を処理します。その利點(diǎn)は、次のことに反映されます。1)非ブロッキングIOはスレッドオーバーヘッドを減らし、2)データ送信効率を改善し、3)セレクターがマルチプレックスを?qū)g現(xiàn)し、4)メモリマッピングはファイルの読み取りと書き込みを速めます。注:1)バッファのフリップ/クリア操作は混亂しやすく、2)不完全なデータをブロックせずに手動(dòng)で処理する必要があります。3)セレクター登録は時(shí)間內(nèi)にキャンセルする必要があります。4)NIOはすべてのシナリオに適していません。

Javaのクラスロードメカニズムはクラスローダーを介して実裝されており、そのコアワークフローは、読み込み、リンク、初期化の3つの段階に分けられます。ローディングフェーズ中、クラスローダーはクラスのバイトコードを動(dòng)的に読み取り、クラスオブジェクトを作成します。リンクには、クラスの正しさの確認(rèn)、靜的変數(shù)へのメモリの割り當(dāng)て、およびシンボル?yún)⒄栅谓馕訾蓼欷蓼?。初期化は、靜的コードブロックと靜的変數(shù)割り當(dāng)てを?qū)g行します。クラスの読み込みは、親クラスローダーに優(yōu)先順位を付けてクラスを見つけ、ブートストラップ、拡張機(jī)能、およびアプリケーションクラスローダーを順番に試して、コアクラスライブラリが安全であり、重複した負(fù)荷を回避することを確認(rèn)します。開発者は、urlclasslなどのクラスローダーをカスタマイズできます

Java例外処理の鍵は、チェックされた例外と未確認(rèn)の例外を區(qū)別し、最後に合理的にログを記録するTry-Catchを使用することです。 1. IOExceptionなどのチェックされた例外は、予想される外部問題に適した処理を強(qiáng)制される必要があります。 2。nullpointerexceptionなどのチェックされていない例外は、通常、プログラムロジックエラーによって引き起こされ、ランタイムエラーです。 3。例外をキャッチする場合、例外の一般的なキャプチャを避けるために、それらは具體的かつ明確でなければなりません。 4.リソース付きのTry-Resourcesを使用して、コードの手動(dòng)清掃を減らすためにリソースを自動(dòng)的に閉鎖することをお?jiǎng)幛幛筏蓼埂?5。例外処理では、詳細(xì)情報(bào)をログフレームワークと組み合わせて記録して後で容易にする必要があります

HashMapは、Javaのハッシュテーブルを介してキーと値のペアストレージを?qū)g裝し、そのコアはデータの位置をすばやく配置することにあります。 1.最初にキーのHashCode()メソッドを使用して、ハッシュ値を生成し、ビット操作を介して配列インデックスに変換します。 2。異なるオブジェクトは、同じハッシュ値を生成し、競合をもたらす場合があります。この時(shí)點(diǎn)で、ノードはリンクされたリストの形式で取り付けられています。 JDK8の後、リンクされたリストが長すぎ(デフォルトの長さ8)、効率を改善するために赤と黒の木に変換されます。 3.カスタムクラスをキーとして使用する場合、equals()およびhashcode()メソッドを書き直す必要があります。 4。ハッシュマップは容量を動(dòng)的に拡大します。要素の數(shù)が容量を超え、負(fù)荷係數(shù)(デフォルト0.75)を掛けた場合、拡張して再ハッシュします。 5。ハッシュマップはスレッドセーフではなく、マルチスレッドでconcuを使用する必要があります

多型は、Javaオブジェクト指向プログラミングの中核的な特徴の1つです。そのコアは、「1つのインターフェイス、複數(shù)の実裝」にあります。継承、メソッドの書き換え、上向き変換を通じて、異なるオブジェクトの動(dòng)作を処理するための統(tǒng)一されたインターフェイスを?qū)g裝します。 1。多型により、親クラスはサブクラスオブジェクトを參照することができ、対応する方法はランタイム中の実際のオブジェクトに従って呼び出されます。 2。実裝は、相続関係の3つの條件、方法の書き換え、上向きの変換を満たす必要があります。 3.さまざまなサブクラスオブジェクト、コレクションストレージ、フレームワーク設(shè)計(jì)を均一に処理するためによく使用されます。 4.使用すると、親クラスによって定義された方法のみを呼び出すことができます。サブクラスに追加された新しい方法は、下方に変換してアクセスし、タイプの安全性に注意を払う必要があります。

Javaの列挙は、定數(shù)を表すだけでなく、動(dòng)作をカプセル化し、データをキャリーし、インターフェイスを?qū)g裝することもできます。 1.列挙は、週や狀態(tài)などの固定インスタンスを定義するために使用されるクラスであり、文字列や整數(shù)よりも安全です。 2。コンストラクターに値を渡すことやアクセス方法の提供など、データとメソッドを運(yùn)ぶことができます。 3.スイッチを使用して、明確な構(gòu)造を持つさまざまなロジックを処理できます。 4.さまざまな列挙値の差別化された動(dòng)作を作成するためのインターフェイスまたは抽象的なメソッドを?qū)g裝できます。 5.虐待、ハードコードの比較、順序の値への依存、合理的に命名とシリアル化を避けるために注意してください。
