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) {
将
字节类型的值(从-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()) {
方法是
PreferredIPV6Address 。
static boolean isPreferIPV6Address() { boolean preferIpv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); if (!preferIpv6) { 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])) {
在条件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; } ....
第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()) {
ServiceConfig类的
shouldExport方法调用在同一类中定义的
getExport方法。
private boolean shouldExport() { Boolean export = getExport();
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('/'));
函数
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]);
在
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和
else值,或者引发异常。 这就是循环主体仅执行一次的原因,因此使用此循环毫无意义。
复制粘贴在开关中
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:
开关在
WAITING和
TIMED_WAITING情况下的代码包含复制粘贴代码。 如果必须执行相同的操作,则可以通过删除
WAITING case块的内容来简化代码。 结果,将为
WAITING和
TIMED_WAITING执行相同的代码
。结论
任何对在Java中使用
RPC感兴趣的人都可能听说过Apache Dubbo。 这是一个受欢迎的开源项目,具有悠久的历史和代码,由许多开发人员编写。 该项目的代码质量很高,但PVS-Studio静态代码分析器仍设法找到一定数量的错误。 这导致以下事实:无论代码多么完美,在开发中型和大型项目时静态分析都非常重要。
注意事项 这样的一次性检查演示了静态代码分析器的功能,但表示使用它的方式完全错误。 此想法的更多细节在
此处和
此处概述。 定期使用分析!
感谢Apache Dubbo开发人员提供了如此出色的工具。 希望本文能帮助您改进代码。 本文并未介绍所有可疑的代码,因此,对于开发人员而言,最好自己检查项目并评估结果。