通过PVS-Studio静态代码分析器分析Apache Dubbo RPC框架

图片2

Apache Dubbo是GitHub上最受欢迎的Java项目之一。 这并不奇怪。 它创建于8年前,已广泛用作高性能RPC环境。 当然,其代码中的大多数错误早已得到修复,并且代码的质量始终保持较高水平。 但是,没有理由选择不使用PVS-Studio静态代码分析器检查这种有趣的项目。 让我们看看结果如何。

关于PVS-Studio


PVS-Studio静态代码分析器在IT市场上已经存在了10多年,它是一种多功能且易于引入的软件解决方案。 目前,分析仪支持C,C ++,C#,Java,并且可以在Windows,Linux和macOS上运行。

PVS-Studio是一种B2B解决方案,已被各个公司的许多团队使用。 如果您想估算分析仪的功能,欢迎您下载发行版并在此处索取试用密钥。

此外,还有一些选项可用于在开源项目中或您是学生的情况下免费使用PVS-Studio。

Apache Dubbo:它是什么和主要功能


如今,几乎所有大型软件系统都是分布式的 。 如果在分布式系统中,远程组件之间的交互连接具有较低的延迟,并且传递的数据相对较少,那么使用RPC (远程过程调用)环境是一个很重要的理由。

Apache Dubbo是具有基于Java的开源代码的高性能RPC (远程过程调用)环境。 与许多其他RPC系统一样,dubbo基于创建服务的想法,该服务定义了一些可以用其参数和返回值类型远程调用的方法。 在服务器端,实现了一个接口,并且启动了dubbo的服务器来处理客户查询。 在客户端,有一个存根提供与服务器相同的方法。 Dubbo建议了三个关键功能,包括接口远程调用,容错和负载平衡以及自动注册和服务发现。

关于分析


分析的动作序列非常简单,在我的情况下不需要很多时间:

  • GitHub下载Apache Dubbo;
  • 使用了启动Java分析器的说明并进行了分析;
  • 获得了分析器报告,对其进行了审查并突出显示了有趣的案例。

分析结果:针对4000多个文件发出了73个高和中等确定性警告(分别为46和27),这很好地表明了代码质量。

并非所有警告都是错误。 这是通常的结果,就像之前直接使用分析仪一样,必须对其进行配置。 之后,您可以预期误报的百分比会非常低( 例如 )。

我没有考虑与测试文件有关的9条警告(7条高和2条中号)。

结果,我收到的警告数量很少,其中还包括误报,因为我尚未配置分析仪。 深入研究所有73条警告,并在文章中进行进一步的描述,这是冗长,荒谬和乏味的,因此我选择了最简单的警告。

Java中的符号字节


V6007表达式'endKey [i] <0xff'始终为true。 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); } 

字节类型的值(从-128到127的值)与值0xff(255)比较。 在这种情况下,不会考虑Java中的字节类型是带符号的,因此该条件将始终为true,这意味着它没有意义。

相同价值的回报


V6007表达式“ isPreferIPV6Address()”始终为false。 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(); } 

方法是PreferredIPV6Address

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

在两种情况下, isPreferIPV6Address方法均返回false 。 开发人员很可能希望一种情况返回true,否则该方法没有任何意义。

没有意义的检查


V6007表达式'!掩码[i] .equals(ipAddress [i])'始终为true。 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; } 

在条件if-else-if中,选中“ *”。等于(掩码[i])||。 掩码[i] .equals(ipAddress [i])被执行。 如果不满足条件,则进行下一次签入if-else-if,这向我们表明掩码[i]ipAddress [i]不相等。 但是在以下一项检查中,如果if-else-if检查了掩码[i]ipAddress [i]不相等。 由于没有为mask [i]ipAddress [i ]分配任何值,因此第二次检查毫无意义。

V6007表达式'message.length> 0'始终为true。 不建议使用的TelnetCodec.java(302)

V6007表达式'message!= Null'始终为true。 不建议使用的TelnetCodec.java(302)

 protected Object decode(.... , byte[] message) throws IOException { .... if (message == null || message.length == 0) { return NEED_MORE_INPUT; } .... // Here the variable <i>message </i> doesn't change! .... 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; } } } .... } 

第302行中的check message!= Null && message.length> 0是多余的。 在302行中的检查之前,执行以下检查:

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

如果检查的条件不满足,我们将知道消息不为null且消息的长度不等于0。由此得出消息的长度大于0(因为字符串的长度不能为负数)。 在第302行之前,本地变量消息未分配任何值,这意味着在第302行中, 消息变量的值与上面的代码相同。 可以得出结论,表达式message!= Null && message.length> 0将始终为true,但是else块中的代码将永远不会执行。

设置未初始化参考字段的值


V6007表达式'!ShouldExport()'始终为false。 ServiceConfig.java(371)

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

ServiceConfig类的shouldExport方法调用在同一类中定义的getExport方法。

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

getExport方法调用抽象AbstractServiceConfig类的getExport方法,该类返回Boolean类型的export字段的 。 还有一个setExport方法来设置字段值。

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

只能通过setExport方法在代码中设置导出字段。 仅在字段不为null的情况下,才仅在abstract AbstractServiceBuilder类的构建方法(扩展AbstractServiceConfig )中调用setExport方法。

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

由于默认情况下所有引用字段均初始化为null且未为export字段分配值,因此将永远不会调用setExport方法。

结果,扩展了AbstractServiceConfig类的ServiceConfig类的getExport方法将始终返回null 。 返回值用于ServiceConfig类的shouldExport方法中,因此shouldExport方法将始终返回true 。 由于返回true,因此!ShouldExport()表达式的值将始终为false。 事实证明,在执行以下代码之前,永远不会返回ServiceConfig类的导出字段:

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

非负参数值


V6009 “子字符串”功能可以接收“ -1”值,而预期为非负值。 检查参数: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); } } } } 

函数lastIndexOf的结果将作为第二个参数传递给子字符串函数,该子字符串的第二个参数不能为负数,即使lastIndexOf如果在字符串中找不到所需的值也可以返回-1 。 如果子字符串方法的第二个参数小于第一个参数(-1 <0),则将引发异常StringIndexOutOfBoundsException 。 要修复该错误,我们需要检查由lastIndexOf函数返回的结果。 如果正确(至少不是负数),则将其传递给子字符串函数。

未使用的循环计数器


V6016通过循环内的常量索引可疑地访问“类型”对象的元素。 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(); } 

for循环中,使用0常数索引访问数组类型的元素。 可能已经打算将i变量用作访问数组元素的索引。 但是作者没有提到这一点。

毫无意义的做


V6019检测不到代码。 可能存在错误。 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()); // <= .... } 

循环条件下的表达式为-while(frame.read())是不可访问的代码,因为该方法在循环的第一次迭代过程中退出。 使用if-else在循环主体中对msg变量进行了多次检查。 这样做会同时从方法返回ifelse值,或者引发异常。 这就是循环主体仅执行一次的原因,因此使用此循环毫无意义。

复制粘贴在开关中


V6067两个或更多案件分支执行相同的操作。 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: } } .... } 

开关WAITINGTIMED_WAITING情况下的代码包含复制粘贴代码。 如果必须执行相同的操作,则可以通过删除WAITING case块的内容来简化代码。 结果,将为WAITINGTIMED_WAITING执行相同的代码

结论


任何对在Java中使用RPC感兴趣的人都可能听说过Apache Dubbo。 这是一个受欢迎的开源项目,具有悠久的历史和代码,由许多开发人员编写。 该项目的代码质量很高,但PVS-Studio静态代码分析器仍设法找到一定数量的错误。 这导致以下事实:无论代码多么完美,在开发中型和大型项目时静态分析都非常重要。

注意事项 这样的一次性检查演示了静态代码分析器的功能,但表示使用它的方式完全错误。 此想法的更多细节在此处此处概述。 定期使用分析!

感谢Apache Dubbo开发人员提供了如此出色的工具。 希望本文能帮助您改进代码。 本文并未介绍所有可疑的代码,因此,对于开发人员而言,最好自己检查项目并评估结果。

Source: https://habr.com/ru/post/zh-CN465427/


All Articles