Analyse des RPC-Quellcodes des Apache Dubbo-Frameworks mit dem statischen Analysator PVS-Studio

Bild 2

Apache Dubbo ist eines der beliebtesten Java-Projekte auf GitHub. Und das ist nicht überraschend. Es wurde vor 8 Jahren erstellt und wird häufig als Hochleistungs-RPC-Umgebung verwendet. Natürlich sind die meisten Fehler in seinem Code seit langem behoben und die Qualität des Codes wird auf einem hohen Niveau gehalten. Es gibt jedoch keinen Grund, die Überprüfung eines so interessanten Projekts mit dem statischen Code-Analysator PVS-Studio abzulehnen. Mal sehen, was wir gefunden haben.

Über PVS-Studio


Der statische Code-Analysator PVS-Studio existiert seit mehr als 10 Jahren auf dem IT-Markt und ist eine multifunktionale und einfach zu implementierende Softwarelösung. Derzeit unterstützt der Analyzer die Sprachen C, C ++, C # und Java und funktioniert auf Windows-, Linux- und MacOS-Plattformen.

PVS-Studio ist eine kostenpflichtige B2B-Lösung und wird von einer Vielzahl von Teams in verschiedenen Unternehmen eingesetzt. Wenn Sie die Funktionen des Analysators bewerten möchten, laden Sie das Verteilungskit herunter und fordern Sie hier einen Testschlüssel an .

Es gibt auch Optionen, um PVS-Studio kostenlos in Open Source-Projekten oder als Student zu verwenden.

Apache Dubbo: Was ist das und was isst es?


Derzeit werden fast alle großen Softwaresysteme vertrieben . Wenn in einem verteilten System eine interaktive Verbindung zwischen Remote-Komponenten mit einer kurzen Antwortzeit und einer relativ geringen Menge übertragener Daten besteht, ist dies ein guter Grund, die RPC- Umgebung (Remote Procedure Call) zu verwenden.

Apache Dubbo ist eine leistungsstarke Open-Source-Java-basierte RPC- Umgebung (Remote Procedure Call). Wie bei vielen RPC-Systemen basiert Dubbo auf der Idee, einen Service zum Definieren von Methoden zu erstellen, die mit ihren Parametern und Rückgabedatentypen remote aufgerufen werden können. Auf der Serverseite wird eine Schnittstelle implementiert und der Dubbo-Server gestartet, um Clientaufrufe zu verarbeiten. Auf der Clientseite gibt es einen Stub, der dieselben Methoden wie der Server bereitstellt. Dubbo bietet drei Hauptfunktionen, darunter Front-End-Remote-Anrufe, Fehlertoleranz und Lastausgleich sowie die automatische Registrierung und Erkennung von Diensten.

Über die Analyse


Die Abfolge der Schritte für die Analyse ist recht einfach und erfordert nicht viel Zeit:

  • Habe Apache Dubbo mit GitHub ;
  • Ich habe die Anweisungen zum Starten des Java-Analysators verwendet und die Analyse gestartet.
  • Ich erhielt einen Analysatorbericht, analysierte ihn und hob interessante Fälle hervor.

Analyseergebnisse: 73 Warnungen des Konfidenzniveaus Hoch und Mittel (46 bzw. 27) wurden für mehr als 4000 Dateien ausgegeben, was ein guter Indikator für die Qualität des Codes ist.

Nicht alle Warnungen sind Fehler. Dies ist eine normale Situation, und vor der regelmäßigen Verwendung des Analysators ist dessen Konfiguration erforderlich. Dann können wir einen relativ geringen Prozentsatz an falsch positiven Ergebnissen erwarten ( Beispiel ).

Unter den Warnungen wurden 9 Warnungen (7 hoch und 2 mittel) pro Testdatei nicht berücksichtigt.

Infolgedessen blieb eine kleine Anzahl von Warnungen übrig, aber unter ihnen gab es auch falsch positive Ergebnisse, da ich den Analysator nicht konfiguriert habe. 73 Warnungen in einem Artikel zu sortieren ist eine lange, dumme und mühsame Aufgabe, daher wurden die interessantesten ausgewählt.

Signiertes Byte in Java


V6007 Der Ausdruck 'endKey [i] <0xff' ist immer wahr. OptionUtil.java (32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

Ein Wert vom Typ Byte (ein Wert von -128 bis 127) wird mit einem Wert von 0xff (255) verglichen. In dieser Bedingung wird nicht berücksichtigt, dass der Bytetyp in Java von Bedeutung ist, daher wird die Bedingung immer erfüllt, was bedeutet, dass dies keinen Sinn ergibt.

Geben Sie den gleichen Wert zurück


V6007 Ausdruck 'isPreferIPV6Address ()' ist immer falsch. NetUtils.java (236)

 private static Optional<InetAddress> toValidAddress(InetAddress address) { if (address instanceof Inet6Address) { Inet6Address v6Address = (Inet6Address) address; if (isPreferIPV6Address()) { // <= return Optional.ofNullable(normalizeV6Address(v6Address)); } } if (isValidV4Address(address)) { return Optional.of(address); } return Optional.empty(); } 

IsPreferIPV6Address- Methode.

 static boolean isPreferIPV6Address() { boolean preferIpv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); if (!preferIpv6) { return false; // <= } return false; // <= } 

Die isPreferIPV6Address- Methode gibt in beiden Fällen false zurück. Höchstwahrscheinlich sollte einer der Fälle true zurückgegeben haben, wie vom Programmierer beabsichtigt, andernfalls ist die Methode nicht sinnvoll.

Sinnlose Schecks


V6007 Ausdruck '! Mask [i] .equals (ipAddress [i])' ist immer wahr. NetUtils.java (476)

 public static boolean matchIpRange(....) throws UnknownHostException { .... for (int i = 0; i < mask.length; i++) { if ("*".equals(mask[i]) || mask[i].equals(ipAddress[i])) { continue; } else if (mask[i].contains("-")) { .... } else if (....) { continue; } else if (!mask[i].equals(ipAddress[i])) { // <= return false; } } return true; } 

In der ersten der if-else-if-Bedingungen erfolgt eine "*" - Prüfung . Gleich (Maske [i]) || mask [i] .equals (ipAddress [i]) . Wenn diese Bedingung nicht erfüllt ist, fährt der Code mit der nächsten Prüfung in if-else-if fort, und wir stellen fest, dass mask [i] und ipAddress [i] nicht gleichwertig sind. Eine der folgenden Überprüfungen in if-else-if überprüft jedoch nur, ob mask [i] und ipAddress [i] nicht gleichwertig sind. Da mask [i] und ipAddress [i] im Methodencode keine Werte zugewiesen bekommen, ist die zweite Prüfung nicht sinnvoll.

V6007 Der Ausdruck 'message.length> 0' ist immer wahr. DeprecatedTelnetCodec.java (302)

V6007 Ausdruck 'message! = Null' ist immer wahr. DeprecatedTelnetCodec.java (302)

 protected Object decode(.... , byte[] message) throws IOException { .... if (message == null || message.length == 0) { return NEED_MORE_INPUT; } .... //   message  ! .... if (....) { String value = history.get(index); if (value != null) { byte[] b1 = value.getBytes(); if (message != null && message.length > 0) { // <= byte[] b2 = new byte[b1.length + message.length]; System.arraycopy(b1, 0, b2, 0, b1.length); System.arraycopy(message, 0, b2, b1.length, message.length); message = b2; } else { message = b1; } } } .... } 

Nachricht prüfen! = Null && message.length> 0 in Zeile 302 macht keinen Sinn. Vor der Prüfung prüft Zeile 302:

 if (message == null || message.length == 0) { return NEED_MORE_INPUT; } 

Wenn die Überprüfungsbedingung nicht erfüllt ist, wissen wir, dass die Nachricht nicht null und ihre Länge nicht 0 ist. Aus diesen Informationen folgt, dass ihre Länge größer als Null ist (da die Länge der Zeichenfolge keine negative Zahl sein kann). Der lokalen Variablennachricht vor Zeile 302 werden keine Werte zugewiesen, was bedeutet, dass in Zeile 302 der Wert der Nachrichtenvariablen wie im obigen Code verwendet wird. Daraus können wir schließen, dass der Ausdruck message! = Null && message.length> 0 immer wahr ist , was bedeutet, dass der Code im else- Block niemals ausgeführt wird.

Festlegen des Werts eines nicht initialisierten Referenzfelds


V6007 Ausdruck '! ShouldExport ()' ist immer falsch. ServiceConfig.java (371)

 public synchronized void export() { checkAndUpdateSubConfigs(); if (!shouldExport()) { // <= return; } if (shouldDelay()) { .... } else { doExport(); } 

Die shouldExport- Methode der ServiceConfig- Klasse ruft die in derselben Klasse definierte getExport- Methode auf.

 private boolean shouldExport() { Boolean export = getExport(); // default value is true return export == null ? true : export; } .... @Override public Boolean getExport() { return (export == null && provider != null) ? provider.getExport() : export; } 

Die Methode getExport ruft die Methode getExport der abstrakten Klasse AbstractServiceConfig auf, die den Wert des Exportfelds vom Typ Boolean zurückgibt. Es gibt auch eine setExport- Methode zum Festlegen des Werts eines Feldes.

 protected Boolean export; .... public Boolean getExport() { return export; } .... public void setExport(Boolean export) { this.export = export; } 

Das Exportfeld im Code wird nur von der setExport- Methode festgelegt. Die setExport- Methode wird nur in der Erstellungsmethode der abstrakten Klasse AbstractServiceBuilder (Erweiterung von AbstractServiceConfig ) aufgerufen und nur, wenn das Exportfeld nicht null ist.

 @Override public void build(T instance) { .... if (export != null) { instance.setExport(export); } .... } 

Aufgrund der Tatsache, dass alle Referenzfelder standardmäßig auf Null initialisiert sind und dem Exportfeld an keiner Stelle im Code ein Wert zugewiesen wurde, wird die setExport- Methode niemals aufgerufen.

Infolgedessen gibt die getExport- Methode der ServiceConfig- Klasse, die die AbstractServiceConfig- Klasse erweitert, immer null zurück . Der zurückgegebene Wert wird in der shouldExport- Methode der ServiceConfig- Klasse verwendet, sodass die shouldExport- Methode immer true zurückgibt . Aufgrund der Rückgabe von true ist der Wert des Ausdrucks ! ShouldExport () immer false. Es stellt sich heraus, dass die Exportmethode der ServiceConfig- Klasse vor der Codeausführung niemals zurückgegeben wird:

 if (shouldDelay()) { DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), ....); } else { doExport(); } 

Nicht negativer Parameterwert


V6009 Die Funktion 'Teilzeichenfolge' könnte den Wert '-1' empfangen, während ein nicht negativer Wert erwartet wird. Argument überprüfen: 2. AbstractEtcdClient.java (169)

 protected void createParentIfAbsent(String fixedPath) { int i = fixedPath.lastIndexOf('/'); if (i > 0) { String parentPath = fixedPath.substring(0, i); if (categories.stream().anyMatch(c -> fixedPath.endsWith(c))) { if (!checkExists(parentPath)) { this.doCreatePersistent(parentPath); } } else if (categories.stream().anyMatch(c -> parentPath.endsWith(c))) { String grandfather = parentPath .substring(0, parentPath.lastIndexOf('/')); // <= if (!checkExists(grandfather)) { this.doCreatePersistent(grandfather); } } } } 

Das Ergebnis der Funktion lastIndexOf wird vom zweiten Parameter an die Teilzeichenfolgenfunktion übergeben , deren zweiter Parameter keine negative Zahl sein sollte, obwohl lastIndexOf -1 zurückgeben kann, wenn der Wert in der Zeichenfolge nicht gefunden wird. Wenn der zweite Parameter der Teilstring- Methode kleiner als der erste ist (-1 <0), wird eine StringIndexOutOfBoundsException ausgelöst . Um den Fehler zu beheben, müssen Sie das von der Funktion lastIndexOf zurückgegebene Ergebnis überprüfen. Wenn es korrekt (zumindest nicht negativ) ist, übergeben Sie es an die Teilzeichenfolgenfunktion .

Unbenutzter Zykluszähler


V6016 Verdächtiger Zugriff auf ein Element vom Typ 'Typ' durch einen konstanten Index innerhalb einer Schleife. RpcUtils.java (153)

 public static Class<?>[] getParameterTypes(Invocation invocation) { if ($INVOKE.equals(invocation.getMethodName()) && invocation.getArguments() != null && invocation.getArguments().length > 1 && invocation.getArguments()[1] instanceof String[]) { String[] types = (String[]) invocation.getArguments()[1]; if (types == null) { return new Class<?>[0]; } Class<?>[] parameterTypes = new Class<?>[types.length]; for (int i = 0; i < types.length; i++) { parameterTypes[i] = ReflectUtils.forName(types[0]); // <= } return parameterTypes; } return invocation.getParameterTypes(); } 

Die for- Schleife verwendet den konstanten Index 0 , um auf das Element des Typarrays zu verweisen. Vielleicht sollte die Variable i als Index für den Zugriff auf die Elemente des Arrays verwendet werden, aber sie haben nicht übersehen, wie sie sagen.

Sinnlose Pause


V6019 Nicht erreichbarer Code erkannt. Möglicherweise liegt ein Fehler vor. GrizzlyCodecAdapter.java (136)

 @Override public NextAction handleRead(FilterChainContext context) throws IOException { .... do { savedReadIndex = frame.readerIndex(); try { msg = codec.decode(channel, frame); } catch (Exception e) { previousData = ChannelBuffers.EMPTY_BUFFER; throw new IOException(e.getMessage(), e); } if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) { frame.readerIndex(savedReadIndex); return context.getStopAction(); } else { if (savedReadIndex == frame.readerIndex()) { previousData = ChannelBuffers.EMPTY_BUFFER; throw new IOException("Decode without read data."); } if (msg != null) { context.setMessage(msg); return context.getInvokeAction(); } else { return context.getInvokeAction(); } } } while (frame.readable()); // <= .... } 

Der Ausdruck in der Schleifenbedingung do - while (frame.readable ()) ist ein nicht erreichbarer Code, da die erste Iteration der Schleife die Methode immer beendet. Im Hauptteil der Schleife werden mehrere Überprüfungen der Variablen msg mit if-else durchgeführt, und sowohl in if als auch in else wird immer ein Wert aus der Methode zurückgegeben oder eine Ausnahme ausgelöst. Aus diesem Grund wird der Hauptteil des Zyklus nur einmal ausgeführt, weshalb die Verwendung der do-while-Schleife keinen Sinn ergibt.

Kopieren Einfügen in Schalter


V6067 Zwei oder mehr Fallzweige führen dieselben Aktionen aus. JVMUtil.java (67), JVMUtil.java (71)

 private static String getThreadDumpString(ThreadInfo threadInfo) { .... if (i == 0 && threadInfo.getLockInfo() != null) { Thread.State ts = threadInfo.getThreadState(); switch (ts) { case BLOCKED: sb.append("\t- blocked on " + threadInfo.getLockInfo()); sb.append('\n'); break; case WAITING: // <= sb.append("\t- waiting on " + threadInfo.getLockInfo()); // <= sb.append('\n'); // <= break; // <= case TIMED_WAITING: // <= sb.append("\t- waiting on " + threadInfo.getLockInfo()); // <= sb.append('\n'); // <= break; // <= default: } } .... } 

Der Switch- Code für WAITING und TIMED_WAITING enthält einen Code zum Kopieren und Einfügen. Wenn Sie wirklich die gleichen Dinge tun müssen, können Sie den Code vereinfachen, indem Sie den Inhalt im case- Block für WAITING löschen . Infolgedessen wird für WAITING und TIMED_WAITING derselbe Code ausgeführt, der in einer einzelnen Kopie aufgezeichnet ist .

Fazit


Jeder, der sich für die Verwendung von RPC in Java interessiert, hat wahrscheinlich von Apache Dubbo gehört. Dies ist ein beliebtes Open Source-Projekt mit einer langen Geschichte und Code, die von vielen Entwicklern geschrieben wurden. Der Code für dieses Projekt ist von ausgezeichneter Qualität, aber der statische Analysator PVS-Studio konnte eine Reihe von Fehlern feststellen. Daraus können wir schließen, dass die statische Analyse bei der Entwicklung mittlerer und großer Projekte sehr wichtig ist, unabhängig davon, wie perfekt Ihr Code ist.

Hinweis Solche einmaligen Überprüfungen demonstrieren die Fähigkeiten eines statischen Code-Analysators, sind jedoch eine völlig falsche Verwendung. Diese Idee wird hier und hier ausführlicher vorgestellt. Verwenden Sie die Analyse regelmäßig!

Vielen Dank an die Apache Dubbo-Entwickler für dieses großartige Tool. Ich hoffe, dieser Artikel hilft Ihnen, den Code zu verbessern. Der Artikel beschreibt nicht alle verdächtigen Codeabschnitte. Daher ist es für Entwickler besser, das Projekt selbst zu überprüfen und die Ergebnisse auszuwerten.



Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Valery Komarov. Analyse des Apache Dubbo RPC Frameworks mit dem PVS-Studio Static Code Analyzer .

Source: https://habr.com/ru/post/de465431/


All Articles