引子
大家都听说过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 介绍