An谩lisis del c贸digo fuente RPC del framework Apache Dubbo con el analizador est谩tico PVS-Studio

Imagen 2

Apache Dubbo es uno de los proyectos Java m谩s populares en GitHub. Y esto no es sorprendente. Fue creado hace 8 a帽os y es ampliamente utilizado como un entorno RPC de alto rendimiento. Por supuesto, la mayor铆a de los errores en su c贸digo se han corregido durante mucho tiempo y la calidad del c贸digo se mantiene a un alto nivel. Sin embargo, no hay raz贸n para negarse a verificar un proyecto tan interesante utilizando el analizador de c贸digo est谩tico PVS-Studio. Veamos qu茅 logramos encontrar.

Sobre PVS-Studio


El analizador de c贸digo est谩tico PVS-Studio ha existido durante m谩s de 10 a帽os en el mercado de TI y es una soluci贸n de software multifuncional y f谩cil de implementar. Por el momento, el analizador admite C, C ++, C #, lenguajes Java y funciona en plataformas Windows, Linux y macOS.

PVS-Studio es una soluci贸n B2B pagada y es utilizada por una gran cantidad de equipos en varias compa帽铆as. Si desea evaluar las capacidades del analizador, descargue el kit de distribuci贸n y solicite una clave de prueba aqu铆 .

Tambi茅n hay opciones para usar PVS-Studio gratis en proyectos de c贸digo abierto o si eres un estudiante.

Apache Dubbo: 驴qu茅 es y qu茅 come?


Actualmente, casi todos los grandes sistemas de software est谩n distribuidos . Si en un sistema distribuido hay una conexi贸n interactiva entre componentes remotos con un tiempo de respuesta corto y una cantidad relativamente peque帽a de datos transmitidos, esta es una buena raz贸n para usar el entorno RPC (llamada a procedimiento remoto).

Apache Dubbo es un entorno RPC (llamada a procedimiento remoto) basado en Java de c贸digo abierto y alto rendimiento. Al igual que con muchos sistemas RPC, dubbo se basa en la idea de crear un servicio para definir m茅todos que se puedan llamar de forma remota con sus par谩metros y tipos de datos de retorno. En el lado del servidor, se implementa una interfaz y se inicia el servidor dubbo para procesar las llamadas de los clientes. Hay un trozo en el lado del cliente que proporciona los mismos m茅todos que el servidor. Dubbo ofrece tres caracter铆sticas clave que incluyen llamadas remotas frontales, tolerancia a fallas y equilibrio de carga, as铆 como registro autom谩tico y descubrimiento de servicios.

Sobre el an谩lisis


La secuencia de pasos para el an谩lisis es bastante simple y no requiere mucho tiempo:

  • Tengo Apache Dubbo con GitHub ;
  • Us茅 las instrucciones para iniciar el analizador Java y comenc茅 el an谩lisis;
  • Recib铆 un informe del analizador, lo analic茅 y destaqu茅 casos interesantes.

Resultados del an谩lisis: se emitieron 73 advertencias de los niveles de confianza Alto y Medio (46 y 27, respectivamente) para m谩s de 4000 archivos, lo que es un buen indicador de la calidad del c贸digo.

No todas las advertencias son errores. Esta es una situaci贸n normal y, antes del uso regular del analizador, se requiere su configuraci贸n. Entonces podemos esperar un porcentaje bastante bajo de falsos positivos ( ejemplo ).

Entre las advertencias, no se consideraron 9 advertencias (7 altas y 2 medias) por archivos de prueba.

Como resultado, se mantuvo una peque帽a cantidad de advertencias, pero entre ellas tambi茅n hubo falsos positivos, ya que no configur茅 el analizador. Ordenar 73 advertencias en un art铆culo es una tarea larga, tonta y tediosa, por lo que se seleccionaron las m谩s interesantes.

Byte firmado en Java


V6007 La expresi贸n 'endKey [i] <0xff' siempre es verdadera. Opci贸nUtil.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); } 

Un valor de tipo byte (un valor de -128 a 127) se compara con un valor de 0xff (255). En esta condici贸n, no se tiene en cuenta que el tipo de byte en Java es significativo, por lo tanto, la condici贸n siempre se cumplir谩, lo que significa que no tiene sentido.

Devuelve el mismo valor


V6007 La expresi贸n 'isPreferIPV6Address ()' siempre es falsa. 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(); } 

IsPreferIPV6 M茅todo de direcci贸n.

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

El m茅todo isPreferIPV6Address devuelve falso en ambos casos, lo m谩s probable es que uno de los casos deber铆a haber devuelto verdadero seg煤n lo previsto por el programador, de lo contrario, el m茅todo no tiene sentido.

Verificaciones sin sentido


V6007 La expresi贸n '! M谩scara [i] .equals (ipAddress [i])' siempre es verdadera. 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; } 

En la primera de las condiciones if-else-if, se produce una comprobaci贸n "*". Igual (m谩scara [i]) || mask [i] .equals (ipAddress [i]) . Si no se cumple esta condici贸n, el c贸digo pasa a la siguiente verificaci贸n en if-else-if, y nos damos cuenta de que mask [i] e ipAddress [i] no son equivalentes. Pero una de las siguientes comprobaciones en if-else-if solo comprueba que mask [i] e ipAddress [i] no son equivalentes. Como la m谩scara [i] y ipAddress [i] no tienen asignado ning煤n valor en el c贸digo del m茅todo, la segunda verificaci贸n no tiene sentido.

V6007 La expresi贸n 'message.length> 0' siempre es verdadera. DeprecatedTelnetCodec.java (302)

V6007 Expresi贸n 'mensaje! = Nulo' siempre es verdadero. 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; } } } .... } 

Comprobando mensaje! = Nulo && mensaje.length> 0 en la l铆nea 302 no tiene sentido. Antes de la verificaci贸n, la l铆nea 302 verifica:

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

Si no se cumple la condici贸n de verificaci贸n, sabremos que el mensaje no es nulo y su longitud no es 0. De esta informaci贸n se deduce que su longitud es mayor que cero (ya que la longitud de la cadena no puede ser un n煤mero negativo). El mensaje de la variable local antes de la l铆nea 302 no tiene asignado ning煤n valor, lo que significa que en la l铆nea 302 se usa el valor de la variable del mensaje , como en el c贸digo anterior. De todo esto podemos concluir que la expresi贸n message! = Null && message.length> 0 siempre ser谩 verdadera , lo que significa que el c贸digo en el bloque else nunca se ejecutar谩.

Establecer el valor de un campo de referencia no inicializado


V6007 La expresi贸n '! ShouldExport ()' siempre es falsa. ServiceConfig.java (371)

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

El m茅todo shouldExport de la clase ServiceConfig llama al m茅todo getExport definido en la misma clase.

 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; } 

El m茅todo getExport llama al m茅todo getExport de la clase abstracta AbstractServiceConfig , que devuelve el valor del campo de exportaci贸n de tipo Boolean . Tambi茅n hay un m茅todo setExport para establecer el valor de un campo.

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

El campo de exportaci贸n en el c贸digo se establece solo mediante el m茅todo setExport . El m茅todo setExport se llama solo en el m茅todo de compilaci贸n de la clase abstracta AbstractServiceBuilder (que extiende AbstractServiceConfig ) y solo si el campo de exportaci贸n no es nulo.

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

Debido al hecho de que todos los campos de referencia se inicializan como nulos de forma predeterminada y que al campo de exportaci贸n no se le ha asignado ning煤n valor en ninguna parte del c贸digo, nunca se llamar谩 al m茅todo setExport .

Como resultado, el m茅todo getExport de la clase ServiceConfig que extiende la clase AbstractServiceConfig siempre devolver谩 nulo . El valor devuelto se utiliza en el m茅todo shouldExport de la clase ServiceConfig , por lo que el m茅todo shouldExport siempre devolver谩 verdadero . Debido a que devuelve verdadero, el valor de la expresi贸n ! ShouldExport () siempre ser谩 falso. Resulta que nunca habr谩 un retorno del m茅todo de exportaci贸n de la clase ServiceConfig antes de la ejecuci贸n del c贸digo:

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

Valor de par谩metro no negativo


V6009 La funci贸n 'subcadena' podr铆a recibir el valor '-1' mientras se espera un valor no negativo. Inspeccionar argumento: 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); } } } } 

El resultado de la funci贸n lastIndexOf se pasa por el segundo par谩metro a la funci贸n de subcadena , cuyo segundo par谩metro no debe ser un n煤mero negativo, aunque lastIndexOf puede devolver -1 si no encuentra el valor en la cadena. Si el segundo par谩metro del m茅todo de subcadena es menor que el primero (-1 <0), se generar谩 una StringIndexOutOfBoundsException . Para corregir el error, debe verificar el resultado devuelto por la funci贸n lastIndexOf , y si es correcto (al menos no negativo), luego pasarlo a la funci贸n de subcadena .

Contador de ciclos no utilizado


V6016 Acceso sospechoso al elemento del objeto 'tipos' por un 铆ndice constante dentro de un bucle. 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(); } 

El bucle for utiliza el 铆ndice constante 0 para referirse al elemento de la matriz de tipos . Quiz谩s estaba destinado a usar la variable i como 铆ndice para acceder a los elementos de la matriz, pero no pasaron por alto, como dicen.

Hacer tiempo sin sentido


V6019 C贸digo inalcanzable detectado. Es posible que haya un error presente. 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()); // <= .... } 

La expresi贸n en la condici贸n de bucle do - while (frame.readable ()) es un c贸digo inalcanzable, ya que la primera iteraci贸n del bucle siempre sale del m茅todo. En el cuerpo del bucle, se realizan varias comprobaciones de la variable msg utilizando if-else, y tanto en if como en else siempre devuelven un valor del m茅todo o arrojan una excepci贸n. Es por esto que el cuerpo del ciclo se ejecutar谩 solo una vez, como resultado, usar el ciclo do-while no tiene sentido.

Copiar y pegar en el interruptor


V6067 Dos o m谩s ramificaciones de casos realizan las mismas acciones. 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: } } .... } 

El c贸digo de cambio para WAITING y TIMED_WAITING contiene un c贸digo de copiar y pegar. Si realmente necesita hacer lo mismo, puede simplificar el c贸digo eliminando el contenido en el bloque de casos para WAITING . Como resultado, el mismo c贸digo registrado en una sola copia se ejecutar谩 para WAITING y TIMED_WAITING .

Conclusi贸n


Cualquier persona interesada en usar RPC en Java probablemente haya o铆do hablar de Apache Dubbo. Este es un proyecto de c贸digo abierto popular con una larga historia y c贸digo escrito por muchos desarrolladores. El c贸digo para este proyecto es de excelente calidad, pero el analizador est谩tico PVS-Studio logr贸 encontrar una serie de errores. De esto podemos concluir que el an谩lisis est谩tico es muy importante al desarrollar proyectos medianos y grandes, no importa cu谩n perfecto sea su c贸digo.

Nota Estas comprobaciones 煤nicas demuestran las capacidades de un analizador de c贸digo est谩tico, pero son una forma completamente incorrecta de usarlo. Esta idea se presenta con m谩s detalle aqu铆 y aqu铆 . 隆Usa el an谩lisis regularmente!

Gracias a los desarrolladores de Apache Dubbo por esta gran herramienta. Espero que este art铆culo te ayude a mejorar el c贸digo. El art铆culo no describe todas las secciones sospechosas del c贸digo, por lo que es mejor que los desarrolladores verifiquen el proyecto por su cuenta y eval煤en los resultados.



Si desea compartir este art铆culo con una audiencia de habla inglesa, utilice el enlace a la traducci贸n: Valery Komarov. An谩lisis del marco Apache Dubbo RPC por el analizador de c贸digo est谩tico PVS-Studio .

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


All Articles