sayi.github.com icon indicating copy to clipboard operation
sayi.github.com copied to clipboard

运行时扫描Java注解(四)之字节码操作

Open Sayi opened this issue 8 years ago • 0 comments

Java将源码编译成面向虚拟机的字节码(ByteCode),这种程序存储格式可以实现在不同平台,不同虚拟机下运行。Oracle JVM specification

本文将对字节码作一个简单的介绍,列举一些字节码操纵工具。

ByteCode字节码

一个class文件的结构如下:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

不妨通过一段示例来一窥字节码,下面写一段代码:

public class HelloWorld{
    public static void main(String[] args){
        System.out.println("hello,world");
    }
}

使用javac HelloWorld.java进行编译后,得到class文件:

cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 000f 4865 6c6c 6f57
6f72 6c64 2e6a 6176 610c 0007 0008 0700
170c 0018 0019 0100 0b68 656c 6c6f 2c77
6f72 6c64 0700 1a0c 001b 001c 0100 0a48
656c 6c6f 576f 726c 6401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 106a
6176 612f 6c61 6e67 2f53 7973 7465 6d01
0003 6f75 7401 0015 4c6a 6176 612f 696f
2f50 7269 6e74 5374 7265 616d 3b01 0013
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d01 0007 7072 696e 746c 6e01 0015
284c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b29 5600 2100 0500 0600 0000 0000
0200 0100 0700 0800 0100 0900 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
000a 0000 0006 0001 0000 0001 0009 000b
000c 0001 0009 0000 0025 0002 0001 0000
0009 b200 0212 03b6 0004 b100 0000 0100
0a00 0000 0a00 0200 0000 0300 0800 0400
0100 0d00 0000 0200 0e

参考ClassFile的结构,我们可以获得如下信息:

  • magic魔数占了u4个字节,表示文件格式(通过后缀名判断格式是错误的,因为后缀名可能会被改,类似的图片格式也会在文件头加上一些魔数表示格式)。魔数的定义是自由的,能区分开类型即可。Java字节码的魔数是cafe babe,相信这是一个浪漫的开始(引用:《深入理解JVM虚拟机》)。
  • 0000 0034分别代表了minor_version和major_version,可以看到major_version是52,可推断出是Java版本是1.8。
  • 001d即是constant_pool的大小,即constant_pool_count。

javap是java提供给我们查看字节码的命令,我们可以通过命令查看字节码javap -c HelloWorld.class > HelloWorld.javap,得到输出如下:

Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String hello,world
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

其中aload_0把this装载到了操作数栈中,invokespecial进行方法调用。

字节码操作类库

  1. javassist:Java Programming Assistant Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建。 它提供了源码层面的API,不需要知道太多虚拟机指令的知识,就可以操纵字节码。

  2. ASM ASM短小精悍。cglib是基于ASM的。

  3. cglib cglib是一个功能强大,高性能高质量的代码生成库。 Hibernate使用cglib为持久化类生成代理。Spring、Guice也在使用cglib。cglib比java 代理更强大的地方在于不仅可以代理接口,也可以代理普通类的方法。

  4. BCEL Byte Code Engineering Library (BCEL),这是Apache Software Foundation 的Jakarta 项目的一部分,需要熟悉Java指令集。

Javassist运行时扫描Java注解

关于javassist如何动态修改字节码,增加方法,操作类文件,这里不作过多介绍,我们来看看javassit读取字节码获取Java注解。

//生成ClassFile对象,对应着Java .class文件
DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream));
ClassFile cls = new ClassFile(dis);

//获取方法
cls.getMethods();

//获取属性
cls.getFields();

//获取类的Runtime注解
(AnnotationsAttribute) cls.getAttribute(AnnotationsAttribute.visibleTag)
//获取属性的Runtime注解
(AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag);
//获取属性的Class注解
(AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.invisibleTag);
//获取方法的Runtime注解
(AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.visibleTag);

//获取方法参数的注解
List<ParameterAnnotationsAttribute> parameterAnnotationsAttributes = Lists.newArrayList((ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.visibleTag),
                (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.invisibleTag));

更多More

字节码操作让我们在更底层对Java进行处理,不仅对理解Java原理有帮助,同时对于性能优化也是个高级的主题。

不妨回到文章的开头,重新读一读《Java Virtual Machine Specification》。

Sayi avatar Sep 21 '17 09:09 Sayi