2011年2月23日星期三

Java字节码框架ASM-读写字节码的用法


    目前,最新的ASM是3.3.1版本的,整个学习笔记,也围绕着3.3.1这个版本来展开。
    在ASM3.3.1中,提供了7个jar包,分别是
         asm-3.3.1.jar
         asm-commons-3.3.1.jar
         asm-tree-3.3.1.jar
         asm-analysis-3.3.1.jar
         asm-util-3.3.1.jar
         asm-xml-3.3.1.jar


         参看ASM的javadoc(http://asm.ow2.org/asm33/javadoc/user/index.html),可以看到一共有7个package,package和jar的对应关系如下
         asm-3.3.1.jar 包含了org.objectweb.asm和org.objectweb.asm.signature两个packages
         asm-commons-3.3.1.jar包含了org.objectweb.asm.commons这个package
         asm-tree-3.3.1.jar 包含了org.objectweb.asm.tree这个package
         asm-analysis-3.3.1.jar包含了org.objectweb.asm.tree.analysis这个package
         asm-util-3.3.1.jar包含了org.objectweb.asm.util这个package
         asm-xml-3.3.1.jar包含了org.objectweb.asm.xml这个package


         其中asm-3.3.1.jar,是包含了核心的功能,而其他的jar,都是基于这个核心的扩展。
         那么我们就从最核心的部分说起。


         如大家所了解的,ASM是一个操作字节码(bytecode)的框架,非常的小巧和快速,这个asm-3.3.1.jar,只有43k的大小。
         asm提供了字节码的读写的功能。而asm的核心,采用的是visitor的模式,提供了ClassReader和ClassWriter这两个非常重要的类以及ClassVisitor这个核心的接口。
         ClassReader的职责是读取字节码。可以用InputStream、byte数组、类名(需要ClassLoader.getSystemResourceAsStream能够加载到的class文件)作为构造函数的参数构造ClassReader对象,来读取字节码。而ClassReader的工作,就是根据字节码的规范,从输入中读取bytecode。而通过ClassReader对象,获取bytecode信息有两种方式,一种就是采用visitor的模式,传入一个ClassVisitor对象给ClassReader的accept方法。另外一种,是使用Low Level的方式,使用ClassReader提供了readXXX以及getXXX的方法来获取信息。
         对于一般使用,用ClassReader的accept方法,使用visitor模式就可以了。


         那么接着我们来看一下ClassVisitor。
         ClassVisitor这个接口,定义了一系列的visit方法,而这些visit方法,我们通过实现ClassVisitor接口中的visit方法(其实就是一堆的回调函数),就能够得到相应的信息。
         在asm中,ClassAdapter这个类,用asm的javadoc上的话说,就是一个代理到其他ClassVisitor的一个空的ClassVisitor(An empty ClassVisitor that delegates to another ClassVisitor.)。具体来说,构造ClassAdapter对象的时候,需要传递一个ClassVisitor的对象给ClassAdapter的构造函数,而ClassAdapter对ClassVisitor的实现,就是直接调用这个传给ClassAdapter的ClassVisitor对象的对应visit方法。
         后面我们会看到一个使用ClassAdapter的例子。
         
         说到这里,我们主要介绍了ClassReader这个类,ClassVisitor这个接口以及ClassAdapter。那么我们先看一个简单的例子,是使用ClassReader和ClassVisitor的一个示例。


package org.vanadies.bytecode.example.asm3;


import java.io.IOException;


import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;


public class ClassReaderExample {
    private static class MyClassVisitor implements ClassVisitor {


        @Override
        public void visit(int version, int access, String name,
                String signature, String superName, String[] interfaces) {
            System.out.println("class name:" + name);
            System.out.println("super class name:" + superName);
            System.out.println("class version:" + version);
            System.out.println("class access:" + access);
            System.out.println("class signature:" + signature);
            if(interfaces != null && interfaces.length > 0){
                for(String str : interfaces){
                    System.out.println("implemented interface name:" + str);
                }
            }
            
        }


        @Override
        public void visitSource(String source, String debug) {
        }


        @Override
        public void visitOuterClass(String owner, String name, String desc) {
        }


        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            return null;
        }


        @Override
        public void visitAttribute(Attribute attr) {
        
        }


        @Override
        public void visitInnerClass(String name, String outerName,
                String innerName, int access) {
        }


        @Override
        public FieldVisitor visitField(int access, String name, String desc,
                String signature, Object value) {
            return null;
        }


        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                String signature, String[] exceptions) {
            return null;
        }


        @Override
        public void visitEnd() {
            
        }
    }
    
    public static void main(String args[]) throws IOException{
        ClassReader classReader = new ClassReader("java.lang.String");
        classReader.accept(new MyClassVisitor(), 0);
    }
}
    这个简单的例子,打印了String这个类的基本信息。
    从上面的例子看,ClassVisitor这个接口的方法中,有些返回值是void,有些是返回了XXXXVisitor。
    这里,返回XXXVisitor的,我们举个例子,比如visitMethod这个方法,返回的是MethodVisitor。这里,在visitMethod的方法中,给出了Method的基本信息,如果需要去获取Method内部的详细的信息(包括代码,注解等),那么需要返回一个MethodVisitor对象出去,如果返回null,那么在ClassReader中,就不会去展开对这个Method的详细信息进行遍历了。
    除了MethodVisitor,还有,AnnotationVisitor,FieldVisitor。
    另外,在org.objectweb.asm.signature这个包中,有一个SignatureVisitor以及SignatureReader和SignatureWriter,这个是处理Signature的。
    而除了ClassAdapter外还有MethodAdapter,作用类似与ClassAdapter,只是MethodAdapter是针对MethodVisitor的。




    到这里,我们已经了解了如何读取bytecode的信息了,也了解了如何使用ClassReader和ClassVisitor了。 接下来,我们要看一下,如果使用ClassWriter这个类了。

 ClassWriter,顾名思义,是用来写字节码的。ClassWriter实现了ClassVisitor这个接口。这里可能大家会觉得奇怪,ClassReader什么接口都没有实现,为啥ClassWriter要实现ClassVisitor的接口呢?ClassWriter只要提供了方法,让我写字节码就行了哇。
大家还记得,通过ClassReader获取类的字节码,有两种方式,一种是使用Visitor模式,这种方法很简单,前面也有demo,另外是使用low level的get和read来进行,这个很复杂。而如果ClassWriter提供的是low level的put和write这类的方法,会提高门槛,很不好用。而ClassWriter实现了ClassVisitor接口,那么就能够很好的跟ClassReader的Visitor模式结合起来。并且,我们使用ASM操作字节码,在写方面更多的是修改、添加和删除,而不是用ASM来完全去写新的class。
ClassWriter中,还有一个toByteArray的方法,这个是把ClassWriter对象生成的字节码,写到一个byte数组中。用于我们来获取字节码的最终结果。
那么,我们可以做一个很简单的例子。就是把ClassWriter对象作为ClassReader accept方法的参数,传给ClassReader,然后在accept方法结束后,我们用ClassWriter对象的toByteArray方法,就能够获得类的字节码了。
这个例子,就不贴源码了,有兴趣的同学,可以自己搞一下。
那么,我们把ClassWriter用在哪里呢?上文提到了,删除、修改、增加。那么一般来说,删除我们是不太用的。主要是修改和增加。
比方说我们如果自己要做AOP,用到的就是修改和增加。假如我有一个类,其中有一个execute方法,我如果自己做AOP,我可以把这个execute方法改名,比如叫做execute$1,然后我增加一个execute方法,新的execute方法中,我可以调用原来的execute方法(现在是execute$1)了,并且在调用前后做一个处理。
我们来看一下具体代码


package org.vanadies.bytecode.example.asm3;


import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


public class ClassWriterAopExample {
    public static class Foo {
        public void execute(){
            System.out.println("Hello World");
        }
    }
    
    public static void main(String[] args) throws Exception{
        Foo foo = new Foo();
        foo.execute();
        ClassReader cr = new ClassReader(Foo.class.getName());
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassAdapter(cw){
            @Override
            public void visit(
                    final int version,
                    final int access,
                    final String name,
                    final String signature,
                    final String superName,
                    final String[] interfaces)
                {
                    cv.visit(version, access, name + "$1", signature, superName, interfaces);
                }


            @Override
            public MethodVisitor visitMethod(
                    final int access,
                    final String name,
                    final String desc,
                    final String signature,
                    final String[] exceptions)
                {
                    if("execute".equals(name)){
                        //这里只是简单的比较了方法名字,其实还需要比较方法参数,参数信息在desc中
                        return cv.visitMethod(access, name + "$1", desc, signature, exceptions);
                    }
                    return cv.visitMethod(access, name, desc, signature, exceptions);
                }
            
        }, 0);
        //到这里,如果调用cr.toByteArray,生成的字节码中,已经没有execute方法了,而是execute$1
        
        //我们接着需要增加一个execute方法
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "execute",
                "()V", null,
                null);
        //开始增加代码
        mv.visitCode();
        //接下来,我们需要把新的execute方法的内容,增加到这个方法中
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Before execute");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); 
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/vanadies/bytecode/example/asm3/ClassWriterAopExample$Foo$1", "execute$1", "()V");
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("End execute");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 0); //这个地方,最大的操作数栈和最大的本地变量的空间,是自动计算的,是因为构造ClassWriter的时候使用了ClassWriter.COMPUTE_MAXS
        mv.visitEnd();
        //到这里,就完成了execute方法的添加。
        //可以把字节码写入到文件,用javap -c,来看下具体的内容
    }
}
这个代码,要比之前的ClassReader的例子要复杂一些。主要复杂的地方在重写execute方法那里,其实就是类似在用汇编在写代码。具体怎么来写或者生产这个代码,有很多办法。
我们这里只是先展示一下如果能够去修改字节码。这个例子展示了,修改原有方法名字,以及增加方法的做法,其他的增加字段等,都可以通过ClassWriter来处理。
具体在运行时能够改变类的行为,我们可以通过Instrumentation的方式或者采用自己实现的ClassLoader的方式来处理。上面例子中,只是展示了如何修改字节码,这个和实际在运行时达到AOP的效果,还是有距离的。

2011年2月22日星期二

Java Basic Diagnosis Tool

    之前遇到过几次Java占用CPU过多的问题,去定位问题的办法也很简单,主要是就是通过 top -H -p javapid,看一下哪些Java的线程占用CPU多,然后通过jstack,dump java的线程,然后去看下这些线程具体再做什么。
    不过人肉去做这个事情,很麻烦,写了一个简单的工具,放在了google code上,也希望能偶有更多人把一些类似这样比较基础的、但是能够减少人肉的工具也放上来,方便需要去查问题的人。
   具体 google code中project的地址是 http://code.google.com/p/java-basic-diag-tool/

2011年2月4日星期五

Java HashMap的并发问题

    使用Java的同学应该都是知道HashMap是线程不安全的,不能够并发的去put和get,如果有并发操作,会抛出ConcurrentModificationException这个异常。不过可能很多同学没有注意,这个异常并不是一定会抛出的。而并发的去对HashMap对象进行put和get的结果,是可能造成死循环。
     年前在线上,也遇到了这么一次。当时是有同学找过来,说有台机器的load很高,已经启动了流量控制,也没有请求进到容器中。后来dump了thread,也通过top -H看到占用CPU比较高的线程,发现都是在执行HashMap.get,想到可能就是上面的原因,后来看了源码,发现确实是这么一个问题。
具体对于HashMap在并发操作时陷入死循环的分析,可以参看这篇博客 http://pt.alibaba-inc.com/wp/dev_related_969/hashmap-result-in-improper-use-cpu-100-of-the-problem-investigated.html

充实与效率

    先讲一个发生在自己身上的故事。
    最近跟IP地址搞上了, 需要把形如a.b.c.d的ip地址转为数值型表示,然后根据网段的起始和结束地址,确定所在的ISP。
在调试和运行的过程中,会需要把数值型的地址转为形如a.b.c.d,然后需要用whois去查询这个a.b.c.d所属的ISP。
开始的时候,是手工的处理了几个地址,用计算器,把数值型转为a.b.c.d;通过命令行的whois去查询信息,然后人肉看一下whois的结果。
发现这个过程真的烦,写了一个python程序,代替计算器做的事情,爽了一些。
后面,用程序实现了命令行whois的功能。
再后来,自己分析了whois查询后的结果。
到这三步做完,基本上是从原来纯手工的工作,变为了自动化的过程。
其实这三步工作变为程序来做,本身没什么很大的困难,这里想记录的,也是想提醒自己和大家的是,大家要能够了解,让自己每天都很充实是没错的,但是需要注意自己的效率。
这也就是很多人会说的要聪明的读书、聪明的学习,我想就是这个道理了,让自己充实、忙应该是很容易的,重要的是,自己对自己时间的投入产出比。我们单位时间是不是做了足够的事情。我们也需要多思考思考,自己是不是聪明的在忙。

通过编程获取Whois的信息

       近期帮助同事在处理从纯真网络上拿到的IP信息,确定这些IP所在的城市、区县,以及这些IP属于哪些ISP。后来需要用到whois上查询的信息。
       开始的几次,通过whois命令简单的查询了一下。后来同事看到http://blog.chinaunix.net/space.php?uid=9950859&do=blog&cuid=1300091这篇博客介绍的用法,把教育网、电信、铁通、网通(之前是网通,现在算联通)的信息都拿下来了。从这些ISP中提取各自负责的IP段,处理了很多IP信息所对应的ISP,不过还是有些没有能够得到处理。就像写程序去自动处理。
       网上搜索了一下,发现其实whois的协议是非常简单的文本协议。whois服务器的端口,是43,可以直接telnet连接到whois服务器(比如 whois.apnic.net)的43端口,然后输入help,就可以查看支持的协议。
       Trying 202.12.29.222...
Connected to whois.apnic.net (202.12.29.222).
Escape character is '^]'.
% [whois.apnic.net node-2]
% Whois data copyright terms    http://www.apnic.net/db/dbcopyright.html

help
% -l <ip-lookup>   Returns first level less specific inetnum,
%                  inet6num or route objects, excluding exact matches.
% -L <ip-lookup>   Returns all level less specific inetnum,
%                  inet6num or route objects, including exact matches.
% -m <ip-lookup>   Returns first level more specific inetnum,
%                  inet6num or route objects, excluding exact matches.
% -M <ip-lookup>   Returns all level more specific inetnum,
%                  inet6num or route objects, excluding exact matches.
% -x <ip-lookup>   Requests that only an exact match on a prefix be
%                  performed.  If no exact match is found no objects are
%                  returned.
% -c <ip-lookup>   Requests first level less specific inetnum or inet6num
%                  objects with the "mnt-irt:" attribute.
% -b <ip-lookup>   Requests first level less specific inetnum or inet6num
%                  objects with the "mnt-irt:" attribute. Only object keys
%                  and "abuse-mailbox:" attributes are visible.
% -d <ip-lookup>   Enables use of the -m, -M, -l and -L flags for lookups on
%                  reverse delegation domains.
%
% -i <attribute-name> <inverse-key> Perform an inverse query.
%
% -F               Produce output using short hand notation for attribute
%                  names.
% -K               Requests that only the primary keys of an object to be
%                  returned.  The exceptions are set objects, where the
%                  members attributes will also be returned. This flag does
%                  not apply to person and role objects.
% -k (optional normal query) Requests a persistent connection. After
%                  returning the result the connection will not be closed by
%                  the server and a client may issue multiple queries on the
%                  same connection.
%                  Note, that server implements 'stop-and-wait' protocol,
%                  when no next query can be sent before receiving a reply
%                  for the previous one.  Use RIPE whois3 client to be able
%                  to send queries in batch mode.
%                  Except the first -k query, -k without an argument closes
%                  the persistent connection.
% -g (mirroring request) Request a NRTM stream from the server.
%                  See [REF], section 4. "Mirroring the RIPE Database" for
%                  more information".
% -G               Disables the grouping of objects by relevance.
% -B               Disables the filtering of "notify:", "changed:" and "e-mail:"
%                  attributes.
%
% -R               Switches off use referral mechanism for domain lookups,
%                  so that the database returns an object in the RIPE
%                  database with the exact match with the lookup argument,
%                  rather than doing a referral lookup.
% -r               Switches off recursion for contact information after
%                  retrieving the objects that match the lookup key.
% -T (comma separated list of object types, no white space is allowed)
%                  Restricts the types of objects to lookup in the query.
% -a               Specifies that the server should perform lookups in all
%                  available sources.  See also -q sources" query.
% -s (comma separated list of sources, no white space is allowed) Specifies
%                  which sources and in which order are to be looked up when
%                  performing a query.
%
% -q sources       Returns the current set of sources along with the
%                  information required for mirroring. See [REF], section
%                  2.9 "Other server features" for more information.
% -q version       Displays the current version of the server.
% -t <object-type> Requests a template for the specified object type.
% -V<client-tag>   Sends information about the client to the server.
% -v <object-type> Requests a verbose template for the specified object
%                  type.
%
% [REF] RIPE Database Reference Manual.
%       http://www.ripe.net/ripe/docs/databaseref-manual.html
   whois的每次请求结束后,服务器会自动断开连接。
   有了上述的信息,我们就可以很简单去实现一个自己的whois查询功能了。比如,我这边针对单个ip的查询,就可以使用-l这个参数。
    具体就是,先创建和服务端连接的socket,
    然后通过socket发送"-l ip\r\n"给服务端
    读取响应,直到socket被关闭。
    具体代码就不贴了,非常简单,需要的同学,分分钟就写好了。