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) {
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()) {
IsPreferIPV6 M茅todo de direcci贸n.
static boolean isPreferIPV6Address() { boolean preferIpv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); if (!preferIpv6) { 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])) {
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; } ....
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()) {
El m茅todo
shouldExport de la clase
ServiceConfig llama al m茅todo
getExport definido en la misma clase.
private boolean shouldExport() { Boolean export = getExport();
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('/'));
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]);
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:
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 .