使用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中字节类型是有效的,因此将始终满足该条件,这意味着这没有任何意义。

返回相同的值


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

IsPreferIPV6Address方法。

 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])||。 mask [i] .equals(ipAddress [i]) 。如果不满足此条件,则代码继续进行if-else-if的下一个检查,并且我们知道mask [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; } .... //   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; } } } .... } 

在第302行检查message!= Null && message.length> 0没有意义。 在检查之前,第302行检查:

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

如果不满足验证条件,则我们将知道消息不为并且消息的长度也不为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方法设置。 仅当Export字段不为null时,才在AbstractServiceBuilder抽象类的构建方法(扩展AbstractServiceConfig )中调用setExport方法。

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

由于默认情况下所有引用字段都初始化为null ,并且在代码中的任何位置都没有为导出字段分配任何值,因此永远不会调用setExport方法。

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

 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变量的多次检查,并且if和in else总是返回该方法的值或引发异常。 因此,循环主体仅执行一次,因此,使用do-while循环是没有意义的。

复制粘贴到开关


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开关代码包含复制粘贴代码。 如果确实需要执行相同的操作,则可以通过删除WAITINGcase块中的内容来简化代码。 结果,将为WAITINGTIMED_WAITING执行记录在单个副本中的相同代码。

结论


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

注意事项 这样的一次性检查演示了静态代码分析器的功能,但是使用它是完全错误的方式。 这个想法在这里这里都有更详细的介绍。 定期使用分析!

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



如果您想与说英语的读者分享这篇文章,请使用以下链接:Valery Komarov。 通过PVS-Studio静态代码分析器分析Apache Dubbo RPC框架

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


All Articles