使用 Findbugs 编写检测器以搜索“System.out.println”的使用

Posted

技术标签:

【中文标题】使用 Findbugs 编写检测器以搜索“System.out.println”的使用【英文标题】:Writing a detector to search for uses of "System.out.println" using Findbugs 【发布时间】:2011-12-03 23:48:13 【问题描述】:

我正在尝试编写一个错误检测器来使用 Findbugs 查找方法调用“System.out.println”的实例。

我了解字节码中的“System.out.println”被编译为对 GETSTATIC 的调用,which pushes "System.out" onto the stack。对 INVOKEVIRTUAL 的调用会从堆栈中弹出“System.out”并调用该方法。

我已经准备了一些代码(见下文),可以找到正确的 GETSTATIC 和 INVOKEVIRTUAL 调用,但无法将两者链接在一起。我怀疑我可能需要以某种方式使用 OpcodeStack,但在理解如何使用它时遇到了麻烦。任何帮助,将不胜感激。

    @Override 
    public void sawOpcode(int seen)  
            // if opcode is getstatic 
            if (seen == GETSTATIC)  
                    String clsName = getClassConstantOperand(); 
                    if ("java/lang/System".equals(clsName))  
                            String fldName = getNameConstantOperand(); 
                            if ("out".equals(fldName))  
                                    System.out.println("SYSTEM.OUT here"); 
                             
                     
             

            // if opcode is invokevirtual 
            if (seen == INVOKEVIRTUAL)  
                    String cls = getDottedClassConstantOperand(); 
                    if ("java.io.PrintStream".equals(cls))  
                            String methodName = getNameConstantOperand(); 
                            if ("println".equals(methodName))  
                                    bugReporter.reportBug(new BugInstance("SYSTEM_OUT_PRINTLN", 
                                                    NORMAL_PRIORITY).addClassAndMethod(this) 
                                                    .addSourceLine(this)); 
                             
                     
             

    

【问题讨论】:

必须在字节码上运行,还是可以在源文件上运行? 抱歉,编辑了帖子,我忘了指明我使用的是 Findbugs(因此它必须在字节码上运行)。谢谢。 好的。因为 PMD 对 pmd.sourceforge.net/rules/logging-java.html 有完全正确的规则 谢谢,但是我很想知道探测器在 Findbugs 中的外观,因为有了这个“简单”示例,我将能够处理更复杂的示例。 【参考方案1】:

我发现,对于我的用例,确定使用 System.out 或 System.err 就足够了 - 在 99% 的情况下,它们将用于调用 . print 或 .println 稍后在块中。我的检测器检测到加载 System.err 或 System.out 的 GET_STATIC 操作码。代码如下,显示了确定发生这种情况的 3 种替代方法。

package my.findbugs.detectors.forbiddencalls;

import org.apache.log4j.Logger; // it is not trivial to use a logger with FindBugs in Eclipse, leave it out if there are problems

import my.findbugs.detectors.util.DetectorUtil;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;


public class CallToSystemOutPrintlnDetector2 extends OpcodeStackDetector 

    private static final Logger LOGGER = Logger.getLogger(CallToSystemOutPrintlnDetector2.class);
    private BugReporter bugReporter;


    public CallToSystemOutPrintlnDetector2(BugReporter bugReporter) 
        super();
        this.bugReporter = bugReporter;
        LOGGER.debug("Instantiated.");
    


    public void sawOpcode(int seen) 

        // find occurrences of:  
        //2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;
//2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;

        if (seen == GETSTATIC)

            try 
//              LOGGER.debug(operand); // static java.lang.System.out Ljava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.analysis.FieldInfo
//              LOGGER.debug(operand.getName()); // err
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;

                FieldDescriptor operand = getFieldDescriptorOperand();
                ClassDescriptor classDescriptor = operand.getClassDescriptor();
                if ("java/lang/System".equals(classDescriptor.getClassName()) && 
                        ("err".equals(operand.getName())||"out".equals(operand.getName()))) 
                    reportBug();
                
             catch (Exception e) 
                //ignore
            

            // could be used
//          try 
//              MethodDescriptor operand = getMethodDescriptorOperand();
//              LOGGER.debug(operand); // java.lang.System.outLjava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.MethodDescriptor
//              LOGGER.debug(operand.getName()); // err 
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;
//           catch (Exception e) 
//              //ignore
//          

            // could be used
//          try 
//              String operand = getRefConstantOperand();
//              LOGGER.debug(operand); // java.lang.System.out : Ljava.io.PrintStream;
//              if (operand != null && (
//                  operand.startsWith("java.lang.System.out :") || operand.startsWith("java.lang.System.err :"))) 
//                  reportBug();
//              
//           catch (Exception e) 
//              //ignore
//          
        
    

    private void reportBug()
        this.bugReporter.reportBug(getBugInstance());
    


    private BugInstance getBugInstance() 
        return new BugInstance(this, "MY_CALL_TO_SYSTEM_OUT_BUG", DetectorUtil.MY_PRIORITY)
            .addClassAndMethod(this)
            .addSourceLine(this);
    


【讨论】:

【参考方案2】:

您的任务比看起来要复杂一些。一个简单的案例:

System.out.println("abc");

也被翻译成简单的字节码:

getstatic   #2; //java/lang/System.out
ldc #3; //String abc
invokevirtual   #4; //Calling java/io/PrintStream.println(String)

但是,如果您尝试打印除简单常量/已知值之外的任何内容,则变得更加困难:

int x = 42;
System.out.println(x + 17);

将被翻译成:

bipush  42
istore_1  //x = 42
getstatic   #2; //java/lang/System.out
iload_1  //x
bipush  17
iadd  //x + 17 on the stack
invokevirtual   #5; //Calling java/io/PrintStream.println(int)

但是等等,情况可能会变得更糟:

System.out.println("x times 27 is " + x * 27);

什么? StringBuilder: ?

new #6; //new java/lang/StringBuilder()
dup
invokespecial   #7; //Calling java/lang/StringBuilder()
ldc #8; //String x times 2 is
invokevirtual   #9; //Calling java/lang/StringBuilder.append(String)
iload_1  //x
bipush  27
imul  //x * 27 on the stack
invokevirtual   #10; //Calling java/lang/StringBuilder.append:(int) with 'x * 27' argument
invokevirtual   #11; //Calling java/lang/StringBuilder.toString:()
invokevirtual   #4; //Calling java/io/PrintStream.println(String)

有趣的是,原始代码被翻译成(这是已知的 Java 5 (?) 优化):

System.out.println(
  new StringBuilder().
    append("x times 27 is ").
    append(x * 27).
    toString()
  );

解决方案

确实如此 - 您将需要一个堆栈,并且您必须跟踪bytecode instruction 中定义的每个推送/弹出操作。这么简单的任务需要做很多工作......

但是如果你走这条路,解决问题很简单:当你遇到 INVOKEVIRTUAL 时,堆栈的顶部应该包含一些值,并且顶部下方的值应该是“java/lang/System.out”。

话虽如此,我 100% 确定 Findbugs 已经实现了这一点,并且您可能可以使用一些 FindBugs API 让您的生活更轻松。

【讨论】:

我想确认我之前在这里检查过这样的检测器:findbugs.sourceforge.net/bugDescriptions.html 并且还编写了一个包含“System.out.println”的测试用例,它没有被标记为漏洞。我知道问题会根据正在打印的字符串而增长。我可以使用以下方法弹出堆栈顶部: OpcodeStack.Item item = stack.getStackItem(0);但是,将其与使用 GETSTATIC 推入堆栈的内容进行比较时,我遇到了一些困难。感谢您的详细回复。【参考方案3】:

使用类 OpcodeStack。

当您看到 GETSTATIC 并意识到您已经“出局”时,将生成的 OpcodeStack.Item 上的用户值设置为 Boolean.TRUE。这样做

try 
     //process opcodes
 finally 
    super.sawOpcode(seen);
    if (pseudocode-saw System.out.println) 
        OpcodeStack.Item item = stack.getStackItem(0);
        item.setUserValue(Boolean.TRUE);

那么当你处理 println 调用时,查看 tos,如果用户值设置为 Boolean.TRUE,你就知道你处于报告错误的状态。

【讨论】:

【参考方案4】:

我过去提出的一个解决方案发现 System.out.println 调用“括号内没有太多计算”,即它们之间最多存在 MAX_WILDCARDS 指令。

我扩展了 ByteCodePatternDetector,我的模式如下:

ByteCodePattern pattern = new ByteCodePattern();
// as this is a pattern, I cannot specify here that this is supposed to be System.out
pattern.add(new Load(SYSO_FIELD, "sysoResult")); 
pattern.add(new Wild(MAX_WILDCARDS)); 
pattern.add(new Invoke("java.io.PrintStream", "println", "/.*", Invoke.INSTANCE, null).label(LABEL_PRINT));

我稍后确保加载和调用操作的字节码是正确的,并比较我从 match.getBindingSet().lookup(...) 检索到的类和字段名称,以确保它是 System.out 被调用。

我已经看到了现有的答案,我正在考虑改变我的解决方案。为了完整起见,我只是添加了这个。

【讨论】:

以上是关于使用 Findbugs 编写检测器以搜索“System.out.println”的使用的主要内容,如果未能解决你的问题,请参考以下文章

如何编写自定义的 gradle 任务以不忽略 Findbugs 违规但在分析完成后失败

IDEAL葵花宝典:java代码开发规范插件 FindBugs-IDEA

findbugs 可以检测未使用的公共方法吗

NonNull Lombok 构建器属性的 FindBugs 检测器

findbugs自定义规则并配置实现检测

检查结果