在过去的十年中,开源运动一直是IT行业发展的关键驱动力之一,并且是其关键组成部分。 开源项目的作用不仅在数量上而且在质量上都越来越重要,这改变了它们在IT市场中的定位概念。 我们勇敢的PVS-Studio团队并不袖手旁观,而是通过在大量代码库中发现隐藏的错误并为此类项目的作者提供免费的许可证选项,积极参与增强开源软件的功能。 本文只是该活动的另一部分! 今天,我们将讨论Apache Hive。 我有报告-有些事情值得一看。
关于PVS-Studio
静态代码分析器
PVS-Studio已有10多年的历史了,它是一种多功能且易于集成的软件解决方案。 目前,它支持C,C ++,C#和Java,并且可以在Windows,Linux和macOS上运行。
PVS-Studio是一种付费的B2B解决方案,已被许多公司的众多团队使用。 如果要尝试使用分析仪,请访问此
页面以下载发行版并请求试用密钥。
如果您是开源怪胎,或者说是学生,则可以使用我们的免费许可证
选项之一 。
关于Apache Hive
在过去的几年中,数据量以惊人的速度增长。 标准数据库已无法应对这种快速增长,这是大数据一词与其他相关概念(例如处理,存储以及对大数据的其他操作)一起来自的地方。
目前,
Apache Hadoop被认为是大数据技术的先驱之一。 它的主要任务是存储,处理和管理大量数据。 组成框架的主要组件是Hadoop Common,
HDFS ,
Hadoop MapReduce和
Hadoop YARN 。 随着时间的流逝,围绕Hadoop开发了一个庞大的相关项目和技术生态系统,其中许多最初是作为项目的一部分开始,然后逐渐发展成为独立的。
Apache Hive是其中之一。
Apache Hive是一个分布式数据仓库。 它管理存储在HDFS中的数据,并提供基于SQL的查询语言(HiveQL)来处理该数据。 有关该项目的更多详细信息,请参见
此处 。
运行分析
无需花费太多精力或时间即可开始分析。 这是我的算法:
- 从GitHub下载Apache Hive;
- 阅读有关启动Java分析器的指南并启动分析;
- 得到了分析仪的报告,对其进行了研究,并写出了最有趣的案例。
分析结果如下:在6500+文件上的1456警告为高和中级别(分别为602和854)。
并非所有警告都涉及真正的错误。 那很正常; 您必须先调整分析仪的设置,然后才能开始定期使用它。 之后,您通常会期望误报率较低(
例如 )。
我忽略了由测试文件触发的407警告(177高级别和230中级别)。 我也忽略了
V6022诊断程序(因为当您不熟悉该代码时,您无法可靠地区分错误片段和正确片段),它被触发了482次。 我也没有检查
V6021诊断程序生成的179条警告。
最后,我仍然有足够的警告可以使用,并且由于我没有调整设置,因此其中仍然存在一定百分比的误报。 像这样的文章中包括太多警告是没有意义的:)。 因此,我们只讨论引起我注意并看起来足够好奇的是什么。
预定条件
在为进行此分析而检查的诊断程序中,
V6007保留了发出的警告数量的记录。 超过200条消息!!! 有些看上去无害,有些则可疑,有些则毕竟是真正的错误! 让我们看看其中的一些。
V6007表达式'key.startsWith(“ hplsql。”)'始终为true。 执行Java(675)
void initOptions() { .... if (key == null || value == null || !key.startsWith("hplsql.")) {
那是一个很长的if-else-if构造! 分析器不喜欢最后一个
if(key.startsWith(“ hplsql。”))中的条件,因为如果执行达到条件,则意味着它是正确的。 确实,如果您查看整个if-else-if构造的第一行,您会发现它已经包含相反的check,因此,如果字符串不是以
“ hplsql”开头。 ,执行将立即跳至下一个迭代。
V6007表达式'columnNameProperty.length()== 0'始终为false。 OrcRecordUpdater.java(238)
private static TypeDescription getTypeDescriptionFromTableProperties(....) { .... if (tableProperties != null) { final String columnNameProperty = ....; final String columnTypeProperty = ....; if ( !Strings.isNullOrEmpty(columnNameProperty) && !Strings.isNullOrEmpty(columnTypeProperty)) { List<String> columnNames = columnNameProperty.length() == 0 ? new ArrayList<String>() : ....; List<TypeInfo> columnTypes = columnTypeProperty.length() == 0 ? new ArrayList<TypeInfo>() : ....; .... } } } .... }
columnNameProperty字符串的长度与零的比较将始终返回
false 。 发生这种情况是因为该比较遵循
!Strings.isNullOrEmpty(columnNameProperty)检查。 因此,如果执行达到我们的条件,那将意味着
columnNameProperty字符串肯定不是null也不为空。
一行之后的
columnTypeProperty字符串也是如此:
- V6007表达式'columnTypeProperty.length()== 0'始终为false。 OrcRecordUpdater.java(239)
V6007表达式'colOrScalar1.equals(“ Column”)'始终为false。 GenVectorCode.java(3469)
private void generateDateTimeArithmeticIntervalYearMonth(String[] tdesc) throws Exception { .... String colOrScalar1 = tdesc[4]; .... String colOrScalar2 = tdesc[6]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column"))
好旧的复制粘贴。 从当前逻辑的角度来看,字符串
colOrScalar1可能一次具有两个不同的值,这是不可能的。 显然,检查应在左侧具有变量
colOrScalar1 ,在右侧具有
colOrScalar2 。
类似的警告如下几行:
- V6007表达式'colOrScalar1.equals(“ Scalar”)'始终为false。 GenVectorCode.java(3475)
- V6007表达式'colOrScalar1.equals(“ Column”)'始终为false。 GenVectorCode.java(3486)
结果,这个if-else-if构造将永远不会做任何事情。
另外一些
V6007警告:
- V6007表达式'characters == null'始终为false。 RandomTypeUtil.java(43)
- V6007表达式'writeIdHwm> 0'始终为false。 TxnHandler.java(1603)
- V6007表达式'fields.equals(“ *”)'始终为true。 Server.java(983)
- V6007表达式“ currentGroups!= Null”始终为true。 GenericUDFCurrentGroups.java(90)
- V6007表达式'this.wh == null'始终为false。 新的返回非空引用。 StorageBasedAuthorizationProvider.java(93),StorageBasedAuthorizationProvider.java(92)
- 等等...
NPE
V6008可能会取消引用“ dagLock”。 QueryTracker.java(557),QueryTracker.java(553)
private void handleFragmentCompleteExternalQuery(QueryInfo queryInfo) { if (queryInfo.isExternalQuery()) { ReadWriteLock dagLock = getDagLock(queryInfo.getQueryIdentifier()); if (dagLock == null) { LOG.warn("Ignoring fragment completion for unknown query: {}", queryInfo.getQueryIdentifier()); } boolean locked = dagLock.writeLock().tryLock(); ..... } }
捕获,记录了一个空对象,并且...该程序一直在运行。 结果,检查之后是空指针取消引用。 !
开发人员必须确实希望程序在获取空引用的情况下退出函数或引发一些特殊异常。
V6008函数“ unlockSingleBuffer”中对“ buffer”的空引用。 MetadataCache.java(410),MetadataCache.java(465)
private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) { LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer(); if (buffer != null) {
另一个潜在的NPE。 如果执行到达
unlockSingleBuffer方法,则意味着
缓冲区对象为null。 假设发生了什么! 如果查看
unlockSingleBuffer方法,您会注意到在第一行中如何取消引用我们的对象。 知道了!
转变变得疯狂
V6034按“ bitShiftsInWord-1”的值进行移位可能与以下类型的大小不一致:“ bitShiftsInWord-1” = [-1 ... 30]。 UnsignedInt128.java(1791)
private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord;
这是-1的潜在偏移。 如果用例如
wordShifts == 3和
bitShiftsInWord == 0调用该方法,则报告的行将以1 << -1结尾。 这是有计划的行为吗?
V6034移位'j'的值可能与类型的大小不一致:'j'= [0 ... 63]。 IoTrace.java(272)
public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j);
在报告的行中,
j变量的值可以在[0 ... 63]范围内。 因此,循环中
val值的计算可能会以意外的方式运行。 在
(1 << j)表达式中,值1是
int类型,因此将其移位32位或更多会使我们超出类型范围的限制。 这可以通过写
((long)1 << j)来解决 。
被记录迷住了
V6046格式错误。 预计会有不同数量的格式项。 未使用的参数:1、2。StatsSources.java(89)
private static ImmutableList<PersistedRuntimeStats> extractStatsFromPlanMapper (....) { .... if (stat.size() > 1 || sig.size() > 1) { StringBuffer sb = new StringBuffer(); sb.append(String.format( "expected(stat-sig) 1-1, got {}-{} ;",
在编写代码以使用
String.format()格式化字符串时,开发人员使用了错误的语法。 结果,传递的参数永远不会到达结果字符串。 我的猜测是,开发人员在编写此代码之前一直在进行日志记录,这是他们从中借用语法的地方。
被盗的异常
V6051在“ finally”块中使用“ return”语句可能会导致丢失未处理的异常。 ObjectStore.java(9080)
private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } }
从
finally块返回任何内容都是非常糟糕的做法,此示例生动地说明了原因。
在
try块中,程序正在形成请求并访问存储。 默认情况下,
提交的变量的值为
false ,并且仅在成功执行
try块中的所有先前操作之后,才更改其状态。 这意味着,如果引发异常,则该变量将始终为
false 。
catch块将捕获异常,对其进行一些调整,然后将其抛出。 因此,当轮到
finally块时,执行将输入条件,从中返回空列表。 这个退货给我们带来了什么? 好吧,这使我们无法阻止任何捕获到的异常被抛出到可以正确处理的外部。 方法签名中指定的任何异常都不会抛出; 他们只是误导。
类似的诊断消息:
- V6051在“ finally”块中使用“ return”语句可能会导致丢失未处理的异常。 ObjectStore.java(808)
杂项
V6009函数'compareTo'收到一个奇数参数。 对象'o2.getWorkerIdentity()'用作其自身方法的参数。 LlapFixedRegistryImpl.java(244)
@Override public List<LlapServiceInstance> getAllInstancesOrdered(....) { .... Collections.sort(list, new Comparator<LlapServiceInstance>() { @Override public int compare(LlapServiceInstance o1, LlapServiceInstance o2) { return o2.getWorkerIdentity().compareTo(o2.getWorkerIdentity());
导致这种愚蠢错误的原因可能有多种:复制粘贴,粗心,仓促等等。 我们经常在开源项目中看到类似的错误,甚至有整篇
文章都涉及到。
V6020除以零。 “除数”分母值的范围包括零。 SqlMathUtil.java(265)
public static long divideUnsignedLong(long dividend, long divisor) { if (divisor < 0L) { return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L; } if (dividend >= 0) {
这是一件很琐碎的事。 一系列的检查无助于避免除数为零。
更多警告:
- V6020 Mod归零。 “除数”分母值的范围包括零。 SqlMathUtil.java(309)
- V6020除以零。 “除数”分母值的范围包括零。 SqlMathUtil.java(276)
- V6020除以零。 “除数”分母值的范围包括零。 SqlMathUtil.java(312)
V6030该方法位于“ |”右侧 无论左操作数的值如何,都会调用操作符。 也许最好使用“ ||”。 OperatorUtils.java(573)
public static Operator<? extends OperatorDesc> findSourceRS(....) { .... List<Operator<? extends OperatorDesc>> parents = ....; if (parents == null | parents.isEmpty()) {
程序员编写了按位运算符| 而不是逻辑||。 这意味着无论左边的结果如何,都将执行右边的部分。 如果
父母== null ,则此错字将在下一个逻辑子表达式中以NPE结尾。
V6042已检查表达式是否与类型'A'兼容,但将其
强制转换为类型'B'。 VectorColumnAssignFactory.java(347)
public static VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch, int outColIndex, PrimitiveCategory category) throws HiveException { VectorColumnAssign outVCA = null; ColumnVector destCol = outputBatch.cols[outColIndex]; if (destCol == null) { .... } else if (destCol instanceof LongColumnVector) { switch(category) { .... case LONG: outVCA = new VectorLongColumnAssign() { .... } .init(.... , (LongColumnVector) destCol); break; case TIMESTAMP: outVCA = new VectorTimestampColumnAssign() { .... }.init(...., (TimestampColumnVector) destCol);
我们对
LongColumnVector扩展ColumnVector和
TimestampColumnVector扩展ColumnVector的类感兴趣。 检查
destCol对象是
LongColumnVector的一个实例时,明确表明它是此类的对象,将在条件语句的正文中处理。 尽管如此,它仍然被强制转换为
TimestampColumnVector ! 如您所见,这些是不同的类,除了它们是从同一父级派生的。 结果,我们得到一个
ClassCastException 。
在强制转换为
IntervalDayTimeColumnVector的情况下也是如此:
- V6042已检查表达式是否与类型'A'兼容,但将其强制转换为类型'B'。 VectorColumnAssignFactory.java(390)
V6060在对null进行验证之前,已使用'var'参考。 Var.java(402),Var.java(395)
@Override public boolean equals(Object obj) { if (getClass() != obj.getClass()) {
在这里,您已经看到取消引用之后对
var对象是否为
null的奇怪检查。 在这种情况下,
var和
obj是同一个对象(
var =(Var)obj )。
空检查的存在意味着所传递的对象可能为空。 因此,调用
equals(null)将在第一行产生一个NPE,而不是预期的
false 。 是的,检查
在那儿,但可悲的是,它在错误的地方。
其他一些类似的情况,其中在检查之前使用了一个对象:
- V6060在验证是否为空之前,已使用“值”参考。 ParquetRecordReaderWrapper.java(168),ParquetRecordReaderWrapper.java(166)
- V6060在对null进行验证之前,已使用'defaultConstraintCols'引用。 HiveMetaStore.java(2539),HiveMetaStore.java(2530)
- V6060在对null进行验证之前,已使用'projIndxLst'引用。 RelOptHiveTable.java(683),RelOptHiveTable.java(682)
- V6060在对null进行验证之前使用了“ oldp”参考。 ObjectStore.java(4343),ObjectStore.java(4339)
- 等等...
结论
如果您只是对Big Data感兴趣,那么您几乎不会意识到Apache Hive的重要性。 这是一个受欢迎的项目,而且是一个很大的项目,包含6500多个源文件(* .java)。 许多开发人员已经写了很多年了,这意味着静态分析器可以在其中找到很多东西。 这再一次证明了静态分析在开发大中型项目时非常重要和有用!
注意事项 像我在这里所做的那样的单次检查很好地展示了分析仪的功能,但是使用它是完全不正确的情况。 在
这里和
这里详细阐述了这个想法。 静态分析应定期使用!
对Hive的检查发现了很多缺陷和可疑片段。 如果Apache Hive的作者遇到本文,我们将很乐意为改善项目提供辛勤的工作。
您无法想象没有Apache Hadoop的Apache Hive,因此PVS-Studio的Unicorn也很可能会造访该平台。 但这就是今天。 同时,我邀请您
下载分析仪并检查您自己的项目。