Apache Dubbo est l'un des projets Java les plus populaires sur GitHub. Et ce n'est pas surprenant. Il a été créé il y a 8 ans et est largement utilisé comme environnement RPC hautes performances. Bien sûr, la plupart des erreurs dans son code sont corrigées depuis longtemps et la qualité du code est maintenue à un niveau élevé. Cependant, il n'y a aucune raison de refuser de vérifier un projet aussi intéressant en utilisant l'analyseur de code statique PVS-Studio. Voyons ce que nous avons réussi à trouver.
À propos de PVS-Studio
L' analyseur de code statique
PVS-Studio existe depuis plus de 10 ans sur le marché informatique et est une solution logicielle multifonctionnelle et facile à mettre en œuvre. À l'heure actuelle, l'analyseur prend en charge les langages C, C ++, C #, Java et fonctionne sur les plates-formes Windows, Linux et macOS.
PVS-Studio est une solution B2B payante et est utilisée par un grand nombre d'équipes dans diverses entreprises. Si vous souhaitez évaluer les capacités de l'analyseur, téléchargez le kit de distribution et demandez une clé d'essai
ici .
Il existe également des
options pour utiliser PVS-Studio gratuitement dans des projets open source ou si vous êtes étudiant.
Apache Dubbo: qu'est-ce que c'est et que mange-t-il?
Actuellement, presque tous les grands systèmes logiciels sont
distribués . Si, dans un système distribué, il existe une connexion interactive entre des composants distants avec un temps de réponse court et une quantité relativement faible de données transmises, c'est une bonne raison d'utiliser l'environnement
RPC (appel de procédure distante).
Apache Dubbo est un environnement
RPC (appel de procédure distante) basé sur Java et à hautes performances. Comme avec de nombreux systèmes RPC, dubbo est basé sur l'idée de créer un service pour définir des méthodes qui peuvent être appelées à distance avec leurs paramètres et types de retour. Côté serveur, une interface est implémentée et le serveur dubbo est lancé pour traiter les appels clients. Il existe un stub côté client qui fournit les mêmes méthodes que le serveur. Dubbo offre trois fonctionnalités clés qui incluent les appels distants frontaux, la tolérance aux pannes et l'équilibrage de charge, ainsi que l'enregistrement et la découverte automatiques des services.
À propos de l'analyse
La séquence des étapes de l'analyse est assez simple et ne nécessite pas beaucoup de temps:
- Got Apache Dubbo avec GitHub ;
- J'ai utilisé les instructions pour démarrer l'analyseur Java et commencé l'analyse;
- J'ai reçu un rapport d'analyseur, je l'ai analysé et j'ai mis en évidence des cas intéressants.
Résultats de l'analyse: 73 avertissements des niveaux de confiance élevé et moyen (46 et 27, respectivement) ont été émis pour plus de 4000 fichiers, ce qui est un bon indicateur de la qualité du code.
Tous les avertissements ne sont pas des erreurs. Il s'agit d'une situation normale et avant une utilisation régulière de l'analyseur, sa configuration est requise. On peut alors s'attendre à un pourcentage assez faible de faux positifs (
exemple ).
Parmi les avertissements, 9 avertissements (7 élevés et 2 moyens) par fichier de test n'ont pas été pris en compte.
En conséquence, un petit nombre d'avertissements est resté, mais parmi eux il y avait aussi des faux positifs, car je n'ai pas configuré l'analyseur. Trier 73 avertissements dans un article est une tâche longue, idiote et fastidieuse, donc les plus intéressants ont été sélectionnés.
Octet signé en Java
V6007 L' expression 'endKey [i] <0xff' est toujours vraie. 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) {
Une valeur de type
octet (une valeur de -128 à 127) est comparée à une valeur de 0xff (255). Dans cette condition, il n'est pas pris en compte que le type d'
octet en Java est significatif, donc la condition sera toujours remplie, ce qui signifie qu'elle n'a pas de sens.
Renvoie la même valeur
V6007 L' expression 'isPreferIPV6Address ()' est toujours fausse. NetUtils.java (236)
private static Optional<InetAddress> toValidAddress(InetAddress address) { if (address instanceof Inet6Address) { Inet6Address v6Address = (Inet6Address) address; if (isPreferIPV6Address()) {
Méthode
IsPreferIPV6Address .
static boolean isPreferIPV6Address() { boolean preferIpv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); if (!preferIpv6) { return false;
La méthode
isPreferIPV6Address renvoie
false dans les deux cas, très probablement que l'un des cas aurait dû renvoyer
true comme prévu par le programmeur, sinon la méthode n'a pas de sens.
Des contrôles dénués de sens
V6007 L' expression «! Mask [i] .equals (ipAddress [i])» est toujours vraie. 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])) {
Dans la première des conditions if-else-if, une vérification
"*" se produit
. Equals (mask [i]) || mask [i] .equals (ipAddress [i]) . Si cette condition n'est pas remplie, le code passe à la vérification suivante if-else-if, et nous constatons que
mask [i] et
ipAddress [i] ne
sont pas équivalents. Mais l'une des vérifications suivantes dans if-else-if vérifie simplement que le
masque [i] et
ipAddress [i] ne
sont pas équivalents. Étant donné que
mask [i] et
ipAddress [i] ne reçoivent aucune valeur dans le code de méthode, la deuxième vérification n'a pas de sens.
V6007 L' expression '
message.length > 0' est toujours vraie. DeprecatedTelnetCodec.java (302)
V6007 L' expression 'message! = Null' est toujours vraie. DeprecatedTelnetCodec.java (302)
protected Object decode(.... , byte[] message) throws IOException { .... if (message == null || message.length == 0) { return NEED_MORE_INPUT; } ....
Vérification du
message! = Null && message.length> 0 sur la ligne 302 n'a aucun sens. Avant la vérification, la ligne 302 vérifie:
if (message == null || message.length == 0) { return NEED_MORE_INPUT; }
Si la condition de vérification n'est pas remplie, alors nous saurons que le
message n'est pas
nul et sa longueur n'est pas 0. Il résulte de ces informations que sa longueur est supérieure à zéro (puisque la
longueur de la chaîne ne peut pas être un nombre négatif). Aucune valeur n'est attribuée au
message de variable locale avant la ligne 302, ce qui signifie qu'à la ligne 302, la valeur de la variable de
message est utilisée, comme dans le code ci-dessus. De tout cela, nous pouvons conclure que l'expression
message! = Null && message.length> 0 sera toujours
vraie , ce qui signifie que le code dans le bloc
else ne sera jamais exécuté.
Définition de la valeur d'un champ de référence non initialisé
V6007 L' expression '! ShouldExport ()' est toujours fausse. ServiceConfig.java (371)
public synchronized void export() { checkAndUpdateSubConfigs(); if (!shouldExport()) {
La méthode
shouldExport de la classe
ServiceConfig appelle la méthode
getExport définie dans la même classe.
private boolean shouldExport() { Boolean export = getExport();
La méthode
getExport appelle la méthode
getExport de la classe abstraite
AbstractServiceConfig , qui renvoie la valeur du champ d'
exportation de type
Boolean . Il existe également une méthode
setExport pour définir la valeur d'un champ.
protected Boolean export; .... public Boolean getExport() { return export; } .... public void setExport(Boolean export) { this.export = export; }
Seule la méthode
setExport définit le champ d'
exportation dans le code. La méthode
setExport est appelée uniquement dans la méthode de
génération de la classe abstraite AbstractServiceBuilder (étendant
AbstractServiceConfig ) et uniquement si le champ d'exportation n'est pas nul.
@Override public void build(T instance) { .... if (export != null) { instance.setExport(export); } .... }
Étant donné que tous les champs de référence sont initialisés à
null par défaut et que le champ d'
exportation n'a reçu aucune valeur n'importe où dans le code, la méthode
setExport ne
sera jamais appelée.
Par conséquent, la méthode
getExport de la classe
ServiceConfig étendant la classe
AbstractServiceConfig renverra toujours
null . La valeur renvoyée est utilisée dans la méthode
shouldExport de la classe
ServiceConfig , donc la méthode
shouldExport renverra toujours
true . En raison du retour de true, la valeur de l'expression
! ShouldExport () sera toujours false. Il s'avère qu'il n'y aura jamais de retour de la méthode d'
exportation de la classe
ServiceConfig avant l'exécution du code:
if (shouldDelay()) { DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), ....); } else { doExport(); }
Valeur de paramètre non négative
V6009 La fonction 'substring' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez l'argument: 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('/'));
Le résultat de la fonction
lastIndexOf est transmis par le deuxième paramètre à la fonction de
sous -
chaîne , dont le deuxième paramètre ne doit pas être un nombre négatif, bien que
lastIndexOf puisse renvoyer
-1 s'il ne trouve pas la valeur dans la chaîne. Si le deuxième paramètre de la méthode de
sous -
chaîne est inférieur au premier (-1 <0), une
exception StringIndexOutOfBoundsException sera
levée . Pour corriger l'erreur, vous devez vérifier le résultat renvoyé par la fonction
lastIndexOf , et s'il est correct (au moins non négatif), puis le transmettre à la fonction de
sous -
chaîne .
Compteur de cycles inutilisé
V6016 Accès suspect à l'élément de l'objet 'types' par un index constant à l'intérieur d'une boucle. 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]);
La boucle
for utilise l'index constant
0 pour faire référence à l'élément du tableau
types . Peut-être était-il destiné à utiliser la variable
i comme index pour accéder aux éléments du tableau, mais ils ne l'ont pas ignoré, comme ils disent.
Do-while sans signification
V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. 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());
L'expression dans la condition de boucle
do -
while (frame.readable ()) est du code inaccessible, car la méthode quitte toujours la première itération de la boucle. Dans le corps de la boucle, plusieurs vérifications de la variable
msg sont effectuées à l'aide de if-else, et dans
if et in
else toujours renvoyer une valeur de la méthode ou lever une exception. C'est pour cette raison que le corps du cycle ne sera exécuté qu'une seule fois, par conséquent, l'utilisation de la boucle do-while n'a pas de sens.
Copier coller dans le commutateur
V6067 Deux ou plusieurs branches de cas effectuent les mêmes actions. 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:
Le code de
commutateur pour
WAITING et
TIMED_WAITING contient un code copier-coller. Si vous devez vraiment faire les mêmes choses, vous pouvez simplifier le code en supprimant le contenu dans le bloc de
cas pour
WAITING . Par conséquent, le même code enregistré en une seule copie sera exécuté pour
WAITING et
TIMED_WAITING .
Conclusion
Quiconque souhaite utiliser
RPC en Java a probablement entendu parler d'Apache Dubbo. Il s'agit d'un projet open source populaire avec une longue histoire et du code écrit par de nombreux développeurs. Le code de ce projet est d'excellente qualité, mais l'analyseur statique PVS-Studio a réussi à trouver un certain nombre d'erreurs. De cela, nous pouvons conclure que l'analyse statique est très importante lors du développement de projets de moyenne et grande envergure, peu importe la perfection de votre code.
Remarque De telles vérifications ponctuelles démontrent les capacités d'un analyseur de code statique, mais sont une manière complètement erronée de l'utiliser. Cette idée est présentée plus en détail
ici et
ici . Utilisez l'analyse régulièrement!
Merci aux développeurs d'Apache Dubbo pour cet excellent outil. J'espère que cet article vous aidera à améliorer le code. L'article ne décrit pas toutes les sections suspectes du code, il est donc préférable pour les développeurs de vérifier le projet par eux-mêmes et d'évaluer les résultats.

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Valery Komarov.
Analyse du framework Apache Dubbo RPC par l'analyseur de code statique PVS-Studio .