浏览 1747 次
锁定老帖子 主题:代码潜在故障的动态分析
精华帖 (0) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2010-11-16
最后修改:2010-11-16
引子
大家都听说过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接口都不会检查。所以这段代码会通过检查并隐含可能发生的错误,直到某一天突然无声无息的爆发,打你个措手不及。 因此,动态代码分析应运而生了。 目标
实现方案 经过以上分析,我们可以想见,这个方案是涉及到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 介绍 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |