FindBugs的检测器大多以下面五种方式来实现,且这五种实现方式findbugs都提供了接口:
- 检查类、方法、字段结构
- 微模式,简单的字节码模式
- 基于栈的模式
- 数据流分析
- 内部过程的分析
本文将介绍findbugs中stack-based pattern的实现过程和需要用到的接口。在这之前,我们必须要有一些必备的java知识,如JVM栈内存、JVM字节码指令、 class文件结构 。
字节码指令
JVM为每一个线程都分配一个java栈,且栈以栈帧的形式进行管理,每调用一个方法都向栈中添加一个栈帧,栈帧由局部变量区、操作数区、和帧数据区组成。看一个简单的字节码指令示例:
Code: 0: new #28 // class java/math/BigDecimal 3 : dup 4: ldc2_w #30 // double 0.11d 7: invokespecial #32 // Method java/math/BigDecimal."<init>":(D)V 10 : astore_1 11: return |
public static void main(String[] args) { BigDecimal a = new BigDecimal(0.11 ); } |
main方法中仅有一条语句,它创建了一个BigDecimal类实例,并把它赋值给本地变量a。左边方框是main函数的字节码指令,我们来看看这些指令对main方法栈帧的操作。(new)创建BigDecimal对象,并向栈顶压入引用值;(dup)复制栈顶引用,压栈;(ldc2_w)从常量池中将0.11d推送到栈顶;(invokespecial)调用构造函数,并弹出对象引用和参数,调用结束将对象引用压栈;(astore_1)弹出对象引用,并存储在main函数的局部变量1位置(0位置为main方法参数);(return)返回。了解JVM的栈结构和字节码指令对栈的操作,这样我们才能使用stack来分析java代码中一些不好的模式。
OpcodeStackDetector
使用Stack-based来实现findbugs的检测器都要继承OpcodeStackDetector这个类,并且实现sawOpcode(int)方法。这个方法传入的操作码的值,根据这个值我们可以得到操作数的信息,如操作码是函数调用,则能获取到函数的名称、描述符等信息。另外我们还能获取到方法栈数据,程序计数器等数据,使用这些数据便能实现想要检测的代码模式。
先来看下OpcodeStackDetector的继承结构:
这些类的主要作用是:
BetterVisitor 定义了许多visit方法,这些方法用来实现对class文件对象的访问(JavaClass,Method等)。
DismantleBytecode 用来分析字节码,提取操作码、操作数、计数器等数据。这个类实现了visit(Code)方法,并为每一个操作码调用sawOpcode(int)方法。
OpcodeStackDetector 类有操作数stack数据,对于stack based模式检测是必不可少的。另外还定义了sawOpcode(int)方法,我们的检测代码在该方法中实现。
检测器例子
仍以 从定义最简单Findbugs Detector做起 提到的检测器做为例子,用来检测BigDecimal实例使用Double进行构造,另外每次调用sawOpcode函数,都打印出方法栈信息:
public void sawOpcode( int seen) { // System.out.println("visit seen:" + seen); // TODO Auto-generated method stub if (seen == INVOKESPECIAL && getClassConstantOperand().equals("java/math/BigDecimal" ) && getNameConstantOperand().equals("<init>") && getSigConstantOperand().equals("(D)V" )) { OpcodeStack.Item top = stack.getStackItem(0 ); Object value = top.getConstant(); System.out.println( "stack num local values:" + stack.getNumLocalValues() + " stack depth"+ stack.getStackDepth()); if (value instanceof Double) { double arg = ((Double) value).doubleValue(); String dblString = Double.toString(arg); String bigDecimalString = new BigDecimal(arg).toString(); boolean ok = dblString.equals(bigDecimalString) || dblString.equals(bigDecimalString + ".0" ); if (! ok) { boolean scary = dblString.length() <= 8 && dblString.toUpperCase().indexOf("E") == -1 ; bugReporter.reportBug( new BugInstance( this , "TUTORIAL_BUG", scary ? NORMAL_PRIORITY : LOW_PRIORITY) .addClassAndMethod( this ).addString(dblString).addSourceLine( this )); } } } System.out.println( "stack num local values in return:" + stack.getNumLocalValues() + " stack depth:"+ stack.getStackDepth()); for ( int i=0; i<stack.getStackDepth(); ++ i) System.out.println( "stack item "+i+":" + stack.getStackItem(i)); }
首先if语句判断操作码是一个构造函数调用,且方法名、类名、方法描述符都符合,以验证这是一个BigDecimal初始化;再取出执行此字节命令时的栈顶操作数stack.getStackItem(0),上面的代码用该double构造BigDecimal实例,判断dblString.equals(bigDecimalString)来确定是否报告这个警告。
将该检测器放到findbugs中,检测第一部分提到的字节码,栈信息输出如下:
前面提到了visit方法,这些方法在findbugs分析到相应的class文件部分时会被调用,如visit(JavaClass obj)方法在分析class文件时调用;visit(Method me)方法在findbugs分析方法时调用;visit(Code code)在findbugs分析字节码指令是调用。需要注意的是,调用visit(Code code)时,一定要调用super.visit(code)方法,否则我们实现的sawOpcode方法将不会调用,因为sawOpcode是在super.visit(code)方法中调用的。
public void visit(Code code){ System.out.println( "visit code!!!!!!!" ); // System.out.println("code:" + code.toString()); // yi ding yao fang wen zhe ge super .visit(code); }
使用visit方法可以实现必要的初始化操作,如某些变量在对字节码检测之前设置出事状态,那么可以把这些操作放在visit(Code)中,这样在每一次分析方法字节码时,状态都将被重置。