跳到主要内容

5 篇博文 含有标签「java」

查看所有标签

ASM Bytecode Framework探索与使用

· 阅读需 7 分钟

SM_cover.png) -->

ASM是一款基于java字节码层面的代码分析和修改工具。无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析。ASM可以直接产生二进制class文件,也可以在类被加入JVM之前动态修改类行为。

ASM库的结构

.png) -->

  • Core 为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换
  • Tree提供了Java字节码在内存中的表现
  • Analysis为存储在tree包结构中的java方法字节码提供基本的数据流统计和类型检查算法
  • Commons提供一些常用的简化字节码生成转化和适配器
  • Util包含一些帮助类和简单的字节码修改,有利于在开发或者测试中使用
  • XML提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化。
class文件结构

在了解ASM之前,有必要先了解一下class文件结构。对于每个class文件其实都是有固定的结构信息,而且保留了源码文件中的符号。下图是class文件的格式图。其中带 * 号的表示可重复的结构。

.png)

  • 类结构体中所有的修饰符、字符常量和其他常量都被存储在class文件开始的一个常量堆栈(Constant Stack)中,其他结构体通过索引引用。
  • 每个类必须包含headers(包括:class name, super class, interface, etc.)和常量堆栈(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部属性(attributes)可以选择显示或者不显示。
  • 每个字段块(Field section)包括名称、修饰符(public, private, etc.)、描述符号(descriptor)和字段属性。
  • 每个方法区域(Method section)里面的信息与header部分的信息类似,信息关于最大堆栈(max stack)和最大本地变量数量(max local variable numbers)被用于修改字节码。对于非abstract和非native的方法有一个方法指令表,exceptions表和代码属性表。除此之外,还可以有其他方法属性。
  • 每个类、字段、方法和方法代码的属性有属于自己的名称记录在类文件格式的JVM规范的部分,这些属性展示了字节码多方面的信息,例如源文件名、内部类、签名、代码行数、本地变量表和注释。JVM规范允许定义自定义属性,这些属性会被标准的VM(虚拟机)忽略,但是可以包含附件信息。
  • 方法代码表包含一系列对java虚拟机的指令。有些指令在代码中使用偏移量,当指令从方法代码被插入或者移除时,全部偏移量的值可能需要调整。
基于事件字节码处理

在Core包中逻辑上分为2部分:

  • 字节码生产者,例如ClassReader
  • 字节码消费者,例如writers(ClassWriter, FieldWriter, MethodWriter和AnnotationWriter),adapters(ClassAdapter和MethodAdapter)

下图是生产者和消费者交互的时序图:

.png)

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用Push模型来对树进行遍历。

  • ASM中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。
  • 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者
原java类型与class文件内部类型对应关系
Java typeType descriptor
booleanZ
charC
byteB
shortS
intI
floatF
longJ
doubleD
ObjectLjava/lang/Object;
int[][I
Object[][][[Ljava/lang/Object;
原java方法声明与class文件内部声明的对应关系
Method declaration in source fileMethod descriptor
void method(String str,int i,float f)(Ljava/lang/String;IF)V
Object method(byte [] b)([B)Ljava/lang/Object;
int[] method(double d)(D)[I
遍历CLASS字节码类信息

以java.lang.Runnable作为例子

.png)

输出:

superName=java/lang/Object,name=java/lang/Runnable
run()V
end

ClassReader类的accept方法中,有个int类型的flag参数有以下几种:

  • SKIP_DEBUG 用于忽略debug信息,例如,源文件,行数和变量信息。
  • SKIP_FRAMES 用于忽略StackMapTable(栈图)信息。Java 6 之后JVM引入栈图概念。
  • EXPAND_FRAMES 扩展StackMapTable数据,允许访问者获取全部本地变量类型与当前堆栈位置的信息。
  • SKIP_CODE 排除代码访问的所有方法,同时还通过方法参数属性和注释。
通过ASM生产自定义类对应的class

目标class内容:

.png)

生产目标class的代码:

这里需要注意,平时我们写类的时候,默认的构造方法是可以不写的,但使用ASM框架生产class的话,默认的构造方法是需要写的,不然,无法实例化对象。

创建类、构造函数与字段:

.png)

创建showInfo方法

.png)

创建get、set方法

.png)

最后生产出Person.class之后,我们可以使用JD-GUI打开:

.png)

动态加载生产出的class字节码并实例化该类

我们可以通过ClassWriter中的toByteArray() 方法可以获取生成的字节码数据。然后使用ClassLoaderdefineClass()方法进行反射实例化对象,并调用showInfo()方法。

0.png)

动态修改class字节码,进行AOP编程

通过加载上面生成的Person.class文件,在showInfo()方法里面添加一行打印当前时间。

通过继承ClassVisitor,重写visitMethod(),拦截showInfo()方法。

1.png)

然后让继承AdviceAdapter的类中的onMethodEnter()方法修改showInfo()方法。

2.png)

这样就可以实现修改class字节码的操作了。重新生成class文件。使用JD-GUI验证一下。不出意料,结果是我们所预期的。

3.png)

虽然例子简单,但是是进行AOP"无损注入"的基础展示。著名的Spring框架也是利用这种技术实现AOP的。至此,对ASM框架的一些简单的使用就是这样了,其中会涉及到一些JVM操作的理解,可以查看我的另一篇文章:JVM指令

另外,可以到github仓库查看本次的demo工程:ASMTest

java 数组

· 阅读需 1 分钟

一维数组

数组初始化
//静态初始化
int[] a = {1,2,3};
int[] a = new int[]{1,2,3};

//动态初始化
int[] a = new int[3];
数组遍历
//普通for循环
for(int i=0;i<a.length;i++){
System.out.println(a[i]);
}

//增强for循环
for(int i:a){
System.out.println(i);
}

二维数组

初始化
//静态初始化
int[][] a = {{1,2},{3,4}};

//动态初始化
int[][] a = new int[3][3];
int[][] a = new int[3][];
a[0] = new int[3];

数组复制

System.arraycopy(src, srcPos, dest, destPos, length);

数组排序

Arrays.sort(a);

数组查找

//二分查找(前提是已排序)
Arrays.binarySearch(a, key);

java集合

· 阅读需 5 分钟
  • 所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

  • Collection接口派生的两个常用接口是List和Set。

  • List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组,List允许有相同的元素。

  • 实现List接口的常用类有LinkedList,ArrayList,Vector和Stack

List接口

LinkedList类

LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。 注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List: List list = Collections.synchronizedList(new LinkedList(...));

ArrayList类

ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。 size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。 每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。 和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

Vector类

Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

Stack 类

Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

Set接口

Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。 请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。

Map接口

请注意,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

Hashtable类

Hashtable继承Map接口,Hashtable是同步的,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。

作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。

如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。

HashMap类

HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。

WeakHashMap类

WeakHashMap是一种改进的HashMap,它对key实行"弱引用",如果一个key不再被外部所引用,那么该key可以被GC回收。

总结

  • 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
  • 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
  • 要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
  • 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程.

java 面向对象——抽象类与接口

· 阅读需 1 分钟

抽象类

抽象类只是比普通类多了一些抽象方法而已,抽象方法没有方法体。

abstract class Person{
public abstract void walk();
}

注意:

  • 抽象类不能直接实例化(new Person()是错误的),只能通过子类实例化
  • 抽象类也包含构造方法(子类调用父类构造方法)
  • 抽象类也可以包含普通方法

接口

接口是抽象类的更一步抽象,接口中所有的方法都是抽象方法

interface Person{
public void walk();
public void eat();
}

注意:

  • 接口不能被实例化
  • 接口没有构造方法
  • 接口中的方法必须是抽象方法(jdk1.8后可以有default方法)
  • 接口中的常量必须是public static final

抽象类与接口的区别

抽象类接口
成员变量无特殊要求必须是public static final
构造方法
方法可以有抽象方法也可以有普通方法只能是抽象方法(jdk1.8后可以有default)
继承只能单继承可以多实现
成员可以有普通成员只能是常量

java 面向对象——类与对象的概念和使用

· 阅读需 2 分钟

方法

方法就是一段可重复调用的代码段

方法重载

方法的重载:方法名称相同,但是参数的类型和个数不同,通过传递参数的个数和类型不同来完成不同的功能。

方法重写

父类与子类之间的多态性,对父类的函数进行重新定义。如果子类中定义某方法与其父类有相同的名称和参数,我们说这个方法被重写,方法重写又称方法覆盖。

方法递归

一种特殊的调用形式,就是方法自己调用自己。

public static int add(int num){
if(num == 1){
return 1;
}else{
return num+add(num-1);
}
}

类是对某一类事物的描述,是抽象的、概念上的意义,对象是实际存在的该类事物的每一个个体,也被称为实例。

内存分配
Person persion = new Person();

面向对象最重要的特征

  • 封装,对外部不可见
  • 继承,扩展类的功能
  • 多态,方法的重载,对象的多态性,面向对象的精髓所在
封装性

使用关键字private修饰属性和方法

多态性
  • 向上转型,父类对象 = 子类实例
  • 向下转型,子类对象 = (子类)父类实例
匿名对象

匿名对象就是没有名字的对象,如果程序只是用一次该对象,就可以使用匿名对象的方式。

构造方法
  • 构造方法名称必须与类名一致
  • 构造方法没有返回值
  • 每个类实例化之后都会调用构造方法,如果没有构造方法,程序在编译的时候会创建一个无参的构造方法
  • 构造方法可以重载