代码潜在故障的动态分析

引子
大家都听说过FindBugs的大名。这是一款静态代码分析的工具。能够直接对字节码文件加以分析,并发现潜在的反模式(anti-pattern),从而有效地促进代码质量的改善。

但FindBugs只能用于 静态代码分析。这也就意味着对于一些运行时的问题,例如,对于指定对象所属类型的校验、对于文件的打开和关闭是否相互对应,对于HashMap中的对象是否被修改过导致永远无法再次获得等情况,FindBugs根本无从下手。为此,本文提出了动态分析的思想并给出演示实现。

动态代码分析
所谓动态代码分析,就是相对于静态代码的分析。这是一句废话,就当立论了吧。

OK,所谓动态代码分析,就是指在程序运行期间能够主动检查代码运行的机制、模式、问题,收集代码的各种运行信息,并分阶段执行汇总分析,根据指定的一些标准,获得代码质量相关判断结果。

这样说比较枯燥乏味,我们举一些比较有趣的例子来说明问题。

例如以下的代码,看看我们能够发现什么问题:
// Hello.java
public class Hello implements Serializable
{
    public void sayTo(String name)
    {
        System.out.println("Hello, " + name + "! Nice to meet U!");
    }
}

// Runner.java
public class Runner implements Runnable, Serializable
{
    public void run()
    {
        Hello hello = new Hello(){};
        OutputStream baos = new ByteArrayOutputStream();
        ObjectOutput oo = null;
        try {
            oo = new ObjectOutputStream(baos);
            oo.writeObject(hello);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oo != null) {
                try {
                    oo.flush();
                    oo.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        hello.sayTo("Regular");
    }
}

看出问题的请举手。

不过我可以保证,这段代码可以安全通过FindBugs检查。因为这段代码从静态类文件来看,基本上没有什么毛病。而且运行一万次,一万次结果正确。这分明就是正确的代码嘛!

……

但是,如果我们现在有收端和发端,从一方把Hello类对象发到另一方接收,那么……

还是不会错!这分明就是完全正确的代码嘛!

……

但是我们还不知足,把收端和发端分别编译,然后再重新尝试刚才的操作,那么……

竟然还是不会错!

……

最后,我们把以上代码修改如下:
public class Runner implements Runnable, Serializable
{
    private static final long serialVersionUID = 1L;

    public void run()
    {
        Hello hello1 = new Hello(){
            private static final long serialVersionUID = 2L;};
        Hello hello2 = new Hello(){
            private static final long serialVersionUID = 3L;};
// ...

在这种情况下,Hello类将同时拥有两个匿名类,两个类的名称并非顺序排列,在不同的编译环境中可能产生不同的类名,因此序列化和反序列化 可能会导致失败。

而ObjectOutputStream的writeObject方法根本不会检查对象是否为匿名类实例,甚至连是否实现了Serializable接口都不会检查。所以这段代码会通过检查并隐含可能发生的错误,直到某一天突然无声无息的爆发,打你个措手不及。

因此,动态代码分析应运而生了。

目标
  • 能够监管代码的运行
  • 能够记录代码的某些操作
  • 能够发现代码的某些反模式
  • 不能对代码文件造成任何改变
  • 不能让代码的运行依赖于检查
  • 不能过多干涉代码的运行,乃至重建JVM实现(过于厚重)

实现方案
经过以上分析,我们可以想见,这个方案是涉及到AOP的。AOP的概念不用多解释了,大多数同学都风闻已久。我们这里为了实现最轻量级的方案原型,采用了ASM库并自行实现了ClassLoader。

具体原理如下:

FileClassLoader加载入口类的对象,然后由入口类对象启动一根线程,然后所有的操作过程中需要的类就都会经由FileClassLoader获得。对于我们要监控的操作,会通过RegularClassAdapter动态插入一些检查代码。若发现问题则收集或者直接显示在界面上。

以下是一些主要类的代码:
// 检查模块入口类 Main.java
// ...
public class Main
{
    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws Exception
    {
        ClassLoader loader = new FileClassLoader(".\\classes\\");
        // bug包是可能存在问题的代码包,以后会用这个包名确定需要插入代码的类文件
        Class<Runnable> cls = (Class<Runnable>) loader.loadClass("bug.Runner");
        System.out.println("ClassLoader: " + cls.getClassLoader());
        Constructor<Runnable> ctor = cls.getConstructor(new Class[0]);
        ctor.setAccessible(true);
        Runnable runner = ctor.newInstance(new Object[0]);
        Thread thread = new Thread(runner);
        thread.start();
    }
}

// FileClassLoader.java
public class FileClassLoader extends ClassLoader
{
    private String root;

    public FileClassLoader(String rootDir)
    {
        if (rootDir == null) {
            throw new IllegalArgumentException("Null root directory");
        }
        root = rootDir;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        // Since all support classes of loaded class use same class loader
        // must check subclass cache of classes for things like Object
        // Class loaded yet?
        Class<?> c = findLoadedClass(name);
        if (c != null) {
            System.out.println("O: " + name);
        } else {
            try {
                c = findSystemClass(name);
                System.out.println("@: " + name);
            } catch (Exception e) {
                // Ignore these
            }
        }
        if (c == null) {
            System.out.println("X: " + name);
            // Convert class name argument to filename
            // Convert package names into subdirectories
            String filename = name.replace('.', File.separatorChar) + ".class";

            try {
                // Load class data from file and save in byte array
                // Convert byte array to Class
                // If failed, throw exception
                byte data[] = loadClassData(filename);
                if (name.startsWith("bug.")) {
                    System.out.println("#: " + name);
                    ClassReader cr = new ClassReader(data);
                    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassAdapter ca = new RegularClassAdapter(cw);
                    cr.accept(ca, 0); // ClassReader.SKIP_DEBUG
                    data = cw.toByteArray();
                    c = defineClass(name, data, 0, data.length);
                } else {
                    c = defineClass(name, data, 0, data.length);
                    if (c == null) {
                        throw new ClassNotFoundException(name);
                    }
                }
            } catch (IOException ex) {
                throw new ClassNotFoundException(filename, ex);
            }
        }
        // Resolve class definition if approrpriate
        if (resolve) {
            resolveClass(c);
        }
        // Return class just created
        return c;
    }

    private byte[] loadClassData(String filename) throws IOException
    {
        // Create a file object relative to directory provided
        File f = new File(root, filename);

        // Get size of class file
        int size = (int) f.length();

        // Reserve space to read
        byte buff[] = new byte[size];

        // Get stream to read from
        FileInputStream fis = new FileInputStream(f);
        DataInputStream dis = new DataInputStream(fis);

        // Read in data
        dis.readFully(buff);

        // close stream
        dis.close();

        // return data
        return buff;
    }
}

// RegularClassAdapter.java
public class RegularClassAdapter extends ClassAdapter
{
    public RegularClassAdapter(ClassVisitor cv)
    {
        super(cv);
    }

    @Override
    public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature,
        final String[] exceptions)
    {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null) {
            mv = new CheckAnonySerMethodAdapter(mv);
        }
        return mv;
    }
}

class CheckAnonySerMethodAdapter extends MethodAdapter
{
    private static final String OWNER = "java/io/ObjectOutputStream",
            NAME = "<init>",
            DESC = "(Ljava/io/OutputStream;)V";
    private static final String MOCK  = "mock/ObjectOutputStream";

    public CheckAnonySerMethodAdapter(MethodVisitor mv)
    {
        super(mv);
    }

    @Override
    public void visitTypeInsn(int opcode, String type)
    {
        if (type.equals(OWNER)) {
            type = MOCK;
        }
        super.visitTypeInsn(opcode, type);
    }

    public void visitMethodInsn(int opcode, String owner, String name, String desc)
    {
        if (opcode == Opcodes.INVOKESPECIAL
            && OWNER.equals(owner) && NAME.equals(name) && DESC.equals(desc)) {
            owner = MOCK;
        }
        super.visitMethodInsn(opcode, owner, name, desc);
    }
}

package mock;

import java.io.IOException;
import java.io.OutputStream;

public class ObjectOutputStream extends java.io.ObjectOutputStream
{
    private final java.io.ObjectOutputStream oos;

    public ObjectOutputStream(OutputStream os) throws IOException
    {
        super();
        oos = new java.io.ObjectOutputStream(os);
    }

    @Override
    protected void writeObjectOverride(Object obj) throws IOException
    {
        Class cls = obj.getClass();
        if (cls.isAnonymousClass()) {
            System.err.println("ANONYMOUS CLASS SERIALIZATION PATTERN: " + cls);
            Thread.dumpStack();
        }
        oos.writeObject(obj);
    }

    // 所有java.io.ObjectOutputStream的方法都需要采用如下的方式代理实现
    public void writeUnshared(Object obj) throws IOException
    {
        oos.writeUnshared(obj);
    }

    //...

效果
X: bug.Runner
#: bug.Runner
@: java.lang.Runnable
@: java.io.Serializable
@: java.lang.Object
ClassLoader: regular.FileClassLoader@19821f
@: java.lang.Throwable
@: java.io.IOException
@: java.io.OutputStream
@: java.io.ByteArrayOutputStream
@: java.io.ObjectOutput
X: bug.Hello
#: bug.Hello
X: bug.Runner$1
#: bug.Runner$1
@: mock.ObjectOutputStream
ANONYMOUS CLASS SERIALIZATION PATTERN: class bug.Runner$1
java.lang.Exception: Stack trace
	at java.lang.Thread.dumpStack(Thread.java:1158)
	at mock.ObjectOutputStream.writeObjectOverride(ObjectOutputStream.java:21)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:298)
	at bug.Runner.run(Runner.java:44)
	at java.lang.Thread.run(Thread.java:595)
@: sun.reflect.SerializationConstructorAccessorImpl
@: java.lang.String
@: java.lang.System
@: java.lang.StringBuilder
@: java.io.PrintStream
Hello, Regular! Nice to meet U!


参考网页:
Writing Your Own ClassLoader

AOP 的利器:ASM 3.0 介绍

你可能感兴趣的:(java,AOP,thread,c,OO)