引言

动态代理的第三篇文章中,我们知道在Proxy的ProxyClassFactory中调用了Proxy.generateProxyClass( String name,class<?>[] interfaces,int accessFlags)方法来生成真正的$Proxy0这个类的二进制字节码,这篇文章就来学习字节码到底是怎么生成的。
虽然这篇文章是在动态代理的系列中,但是字节码生成技术不仅仅限于动态代理这一个场景,所以这篇文章会对ProxyGenerator类做一个详细的讲解而不仅仅局限于动态代理中调用的方法。另外,字节码生成技术涉及到java class文件结构方面的知识,字节码生成无非是构造组成class文件的字节数组,所以如果对类文件不是很清楚,建议先阅读《深入理解java虚拟机》第三版第六章的内容。

字段概述

如果你很清楚地了解class文件的结构,就会发现ProxyGenerator中的很多字段能映射到class文件中某一部分,我们这里将字段按照映射的class文件结构的不同部分分类描述一下。

主版本号和次版本号

首先是主版本号和次版本号:

  1. private static final int CLASSFILE_MAJOR_VERSION = 49;
  2. private static final int CLASSFILE_MINOR_VERSION = 0;

不用好奇为啥没有魔数,因为魔数CAFEBABE是固定的,没有必要在这里体现。
主版本号和此版本号对应class文件中魔数后面四个字节。第五和第六个字节(魔数占四个字节)是次版本号,第七和第八字节是主版本号。

常量池中的常量类型

  1. private static final int CONSTANT_UTF8 = 1;
  2. private static final int CONSTANT_UNICODE = 2;
  3. private static final int CONSTANT_INTEGER = 3;
  4. private static final int CONSTANT_FLOAT = 4;
  5. private static final int CONSTANT_LONG = 5;
  6. private static final int CONSTANT_DOUBLE = 6;
  7. private static final int CONSTANT_CLASS = 7;
  8. private static final int CONSTANT_STRING = 8;
  9. private static final int CONSTANT_FIELD = 9;
  10. private static final int CONSTANT_METHOD = 10;
  11. private static final int CONSTANT_INTERFACEMETHOD = 11;
  12. private static final int CONSTANT_NAMEANDTYPE = 12;

这些都是常量池中常量的类型,但是这里的类型并不齐全,没有CONSTANT_MethodHandle、CONSTANT_MethodType和CONSTANT_InvokeDynamic这些,应该是在代理类中不会被用到。

访问标志

  1. private static final int ACC_PUBLIC = 1;
  2. private static final int ACC_PRIVATE = 2;
  3. private static final int ACC_STATIC = 8;
  4. private static final int ACC_FINAL = 16;
  5. private static final int ACC_SUPER = 32;

这部分就对应class文件中常量池结束之后的类的访问标志。也不齐全,没有ACC_SYNTHETIC等这些不会被用到的选项。

字节码命令

  1. private static final int opc_aconst_null = 1;
  2. private static final int opc_iconst_0 = 3;
  3. private static final int opc_bipush = 16;
  4. private static final int opc_sipush = 17;

这部分常量很多,对应于不同的字节码命令,这里只是展示出了一部分。在class文件中,字节码出现在方法表集合中方法的Code属性中。

常量池、字段表集合和方法表结合

  1. private ProxyGenerator.ConstantPool cp = new ProxyGenerator.ConstantPool();
  2. private List<ProxyGenerator.FieldInfo> fields = new ArrayList();
  3. private List<ProxyGenerator.MethodInfo> methods = new ArrayList();

在ProxyGenerator类中,针对常量池、字段表和方法表都创建了内部类,分别是ConstantPool、FieldInfo和MethodInfo,上面三个就能代表这部分结构了。

与生成代理类相关的字段

ProxyGenerator使用的是字节码生成来创建代理类,所以我们能看到与代理类相关的一些字段,比如:

  1. private static final String superclassName = "java/lang/reflect/Proxy";
  2. private static final String handlerFieldName = "h";
  3. private static Method hashCodeMethod;
  4. private static Method equalsMethod;
  5. private static Method toStringMethod;

分别是生成的代理类的父类也就是Proxy类的全限定类名,代理类的InvocationHandler字段的名称和三个代理类重写的Object类的方法,这些我们在之前的讨论中都已经有了清晰的了解。
既然看到了hashCodeMethod、equalsMethod和toStringMethod,我们看下他们的初始化吧:

  1. static {
  2. try {
  3. hashCodeMethod = Object.class.getMethod("hashCode");
  4. equalsMethod = Object.class.getMethod("equals", Object.class);
  5. toStringMethod = Object.class.getMethod("toString");
  6. } catch (NoSuchMethodException var1) {
  7. throw new NoSuchMethodError(var1.getMessage());
  8. }
  9. }

是在静态块里执行的初始化。

构造方法

ProxyGenerator只有一个私有的构造方法:

  1. private ProxyGenerator(String paramString, Class<?>[] paramArrayOfClass, int paramInt) {
  2. this.className = paramString;
  3. this.interfaces = paramArrayOfClass;
  4. this.accessFlags = paramInt;
  5. }

也就是不能直接生成ProxyGenerator的实例,这三个参数,是不是很熟悉,对,就是我们的Proxy的ProxyClassFactory中调用generateProxyClass方法传入的三个参数:动态代理类的名称、动态代理类需要实现的接口和动态代理类的访问标志。
虽然不能创建ProxyGenerator的实例,但是它提供了静态方法来供外部调用来创建动态代理类。

代理类字节数组的生成

ProxyGenerator类中只有两个public的方法,就是下面这两个generateProxyClass,而这两个方法也是代理类字节数组生成的对外唯一入口。我们的分析也从这两个方法开始:

  1. public static byte[] generateProxyClass(String paramString, Class<?>[] paramArrayOfClass) {
  2. return generateProxyClass(paramString, paramArrayOfClass, 49);
  3. }
  4. public static byte[] generateProxyClass(final String name, Class<?>[] paramArrayOfClass, int paramInt) {
  5. ProxyGenerator proxyGenerator = new ProxyGenerator(name, paramArrayOfClass, paramInt);
  6. final byte[] classFile = proxyGenerator.generateClassFile();

ProxyGenerator提供了两个generateProxyClass方法,第一个是调用了第二个实现的,所以在不传访问标志参数的情况下,默认会设置访问标志为49,而根据前面关于访问标志的字段,49=32+16+1,也就是说,默认的访问标志是public+final+super。
再看generateProxyClass(String,class[],int)这个方法,方法的第一行代码就根据这三个参数创建了一个ProxyGenerator实例,然后调用generateClassFile方法生成的字节数组,这个字节数组就是代表代理类的class的字节数组,所以generateClassFile方法是我们需要重点分析的。
由于这个方法比较长,我们还是采用分段分析的方式:

  1. private byte[] generateClassFile() {
  2. addProxyMethod(hashCodeMethod, Object.class);
  3. addProxyMethod(equalsMethod, Object.class);
  4. addProxyMethod(toStringMethod, Object.class);
  5. for (Class<?> clazz : this.interfaces) {
  6. for (Method method : clazz.getMethods())
  7. addProxyMethod(method, clazz);
  8. }

方法开始就调用了多次addProxyMethod方法,参数分别是Object类的hashCode、equals和toString这三个方法以及代理类需要实现的接口的每个方法,所以我们得先看一下这个方法做了什么:

生成被代理方法的信息

  1. private void addProxyMethod(Method paramMethod, Class<?> paramClass) {
  2. //获取方法名称 例如hashCode
  3. String str1 = paramMethod.getName();
  4. //获取方法的参数class数组
  5. Class[] arrayOfClass1 = paramMethod.getParameterTypes();
  6. //获取方法的返回值class
  7. Class<?> clazz = paramMethod.getReturnType();
  8. //获取方法的异常class数组
  9. Class[] arrayOfClass2 = paramMethod.getExceptionTypes();
  10. //方法名称+方法参数描述符
  11. String str2 = str1 + getParameterDescriptors(arrayOfClass1);
  12. //根据方法名称+方法参数描述符 从proxyMethods中获取ProxyMethod列表
  13. List<ProxyMethod> list = this.proxyMethods.get(str2);
  14. if (list != null) {
  15. //如果存在list 对异常类型class进行兼容性计算
  16. for (ProxyMethod proxyMethod : list) {
  17. if (clazz == proxyMethod.returnType) {
  18. ArrayList<Class<?>> arrayList = new ArrayList();
  19. collectCompatibleTypes(arrayOfClass2, proxyMethod.exceptionTypes, arrayList);
  20. collectCompatibleTypes(proxyMethod.exceptionTypes, arrayOfClass2, arrayList);
  21. proxyMethod.exceptionTypes = new Class[arrayList.size()];
  22. proxyMethod
  23. .exceptionTypes = (Class[])arrayList.<Class<?>[]>toArray((Class<?>[][])proxyMethod.exceptionTypes);
  24. return;
  25. }
  26. }
  27. } else {
  28. //第一次调用map中肯定不存在这个list 生成一个新的list 然后存入map
  29. list = new ArrayList(3);
  30. this.proxyMethods.put(str2, list);
  31. }
  32. list.add(new ProxyMethod(str1, arrayOfClass1, clazz, arrayOfClass2, paramClass));
  33. }

我们需要重点关注一下getParameterDescriptors()这个方法是怎么实现的,从名称来看,它的作用应该是获取方法的参数描述符。

  1. private static String getParameterDescriptors(Class<?>[] paramArrayOfClass) {
  2. StringBuilder stringBuilder = new StringBuilder("(");
  3. //参数描述符被()包围,()里面是每个参数类型描述符
  4. for (byte b = 0; b < paramArrayOfClass.length; b++)
  5. stringBuilder.append(getFieldType(paramArrayOfClass[b]));
  6. stringBuilder.append(')');
  7. return stringBuilder.toString();
  8. }
  9. private static String getFieldType(Class<?> paramClass) {
  10. if (paramClass.isPrimitive())
  11. //如果是简单类型 就直接是简单类型对应的大写字符 例如int对应的是I
  12. return (PrimitiveTypeInfo.get(paramClass)).baseTypeString;
  13. if (paramClass.isArray())
  14. //如果是数组 直接将名称中的.用/替换掉
  15. return paramClass.getName().replace('.', '/');
  16. //如果是引用类型,就是L+类的全限定名称 例如Ljava/lang/String;
  17. return "L" + dotToSlash(paramClass.getName()) + ";";
  18. }
  19. private static String dotToSlash(String paramString) {
  20. return paramString.replace('.', '/');
  21. }

对于不同的参数类型,基本类型、引用类型和数组类型,参数描述符的获取方式是不同的,这里举一个例子:如果我们传给getParameterDescriptors方法的参数是[int.class,String.class,object[].class],那么最终生成的就是
(ILjava/lang/String;[Ljava/lang/Object;)这个字符串。
获取了方法的参数描述符之后,将方法名称+方法参数描述符作为key,去获取ProxyMethod的list,我们需要看一下proxyMethods的定义:

  1. private Map<String, List<ProxyGenerator.ProxyMethod>> proxyMethods = new HashMap();

是一个map,这里的key应该就是我们上面说的方法名称+方法参数描述符,value是ProxyMethod的list,ProxyMethod的定义如下:

  1. private class ProxyMethod {
  2. public String methodName;
  3. public Class<?>[] parameterTypes;
  4. public Class<?> returnType;
  5. public Class<?>[] exceptionTypes;
  6. public Class<?> fromClass;
  7. public String methodFieldName;
  8. private ProxyMethod(String param1String, Class<?>[] param1ArrayOfClass1, Class<?> param1Class1, Class<?>[] param1ArrayOfClass2, Class<?> param1Class2) {
  9. this.methodName = param1String;
  10. this.parameterTypes = param1ArrayOfClass1;
  11. this.returnType = param1Class1;
  12. this.exceptionTypes = param1ArrayOfClass2;
  13. this.fromClass = param1Class2;
  14. this.methodFieldName = "m" + ProxyGenerator.this.proxyMethodCount++;
  15. }

从构造方法中,我们可以知道,定义一个ProxyMethod,需要方法的名称例如toString、返回值类型、参数类型、异常类型和方法的来源类。ProxyMethod中的methodFieldName这个字段则定义了这个被代理的方法在生成的代理类中的字段名称,如果你还记得我们反编译出来的$Proxy0的这几个字段:

  1. private static Method m1;
  2. private static Method m4;
  3. private static Method m3;
  4. private static Method m2;
  5. private static Method m0;

就能很好的对应上了。
addProxyMethod方法最后调用了ProxyMethod方法的构造参数来创建ProxyMethod实例。

  1. list.add(new ProxyMethod(str1, arrayOfClass1, clazz, arrayOfClass2, paramClass));

我们可以大概总结一下addProxyMethod方法做了什么工作:对于某个要代理的方法,既包括Object的toString、hashCode和equals方法,又包括代理类要实现的所有接口的每个方法,addProxyMethod根据这个方法的名称、参数类型、返回值类型、异常类型和来源对象唯一的生成一个ProxyMethod对象,并将其存入ProxyGenerator中定义的proxyMethods这个map中,方法名称+参数描述符作为key,生成的ProxyMethod作为value,至于这个map中的value为什么是一个list而不是一个简单的ProxyMethod,我们先不用去探讨。

FiledInfo、MethodInfo与字段表、方法表

在使用addProxyMethod生成了代理类需要代理的所有的方法信息之后,就能够构造出代理类的这些方法对应的方法表集合以及字段对应的字段表集合,接着看generateClassFile()方法的代码:

  1. try {
  2. this.methods.add(generateConstructor());
  3. for (List<ProxyMethod> list : this.proxyMethods.values()) {
  4. for (ProxyMethod proxyMethod : list) {
  5. this.fields.add(new FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
  6. this.methods.add(proxyMethod.generateMethod());
  7. }
  8. }
  9. this.methods.add(generateStaticInitializer());
  10. } catch (IOException iOException) {
  11. throw new InternalError("unexpected I/O Exception", iOException);
  12. }

this.methods和this.fields就代表方法表集合和字段表集合,他们的声明如下:

  1. private List<ProxyGenerator.FieldInfo> fields = new ArrayList();
  2. private List<ProxyGenerator.MethodInfo> methods = new ArrayList();

FieldInfo和MethodInfo就是字段表和方法表。
这段代码的作用就是将代理类的构造方法、代理方法和静态块(静态块在class文件中是通过这个编译器自动生成的方法表示的)映射成MethodInfo类,将代理类的字段映射成FieldInfo类。
我们这里先介绍比较简单的字段表的映射:

FieldInfo到字段表的映射

先看FieldInfo的声明:

  1. private class FieldInfo {
  2. public int accessFlags;
  3. public String name;
  4. public String descriptor;
  5. public FieldInfo(String param1String1, String param1String2, int param1Int) {
  6. this.name = param1String1;
  7. this.descriptor = param1String2;
  8. this.accessFlags = param1Int;
  9. ProxyGenerator.this.cp.getUtf8(param1String1);
  10. ProxyGenerator.this.cp.getUtf8(param1String2);
  11. }
  12. public void write(DataOutputStream param1DataOutputStream) throws IOException {
  13. param1DataOutputStream.writeShort(this.accessFlags);
  14. param1DataOutputStream.writeShort(ProxyGenerator.this.cp.getUtf8(this.name));
  15. param1DataOutputStream.writeShort(ProxyGenerator.this.cp.getUtf8(this.descriptor));
  16. param1DataOutputStream.writeShort(0);
  17. }
  18. }
  19. private static class ExceptionTableEntry {
  20. public short startPc;
  21. public short endPc;
  22. public short handlerPc;
  23. public short catchType;
  24. public ExceptionTableEntry(short param1Short1, short param1Short2, short param1Short3, short param1Short4) {
  25. this.startPc = param1Short1;
  26. this.endPc = param1Short2;
  27. this.handlerPc = param1Short3;
  28. this.catchType = param1Short4;
  29. }
  30. }

根据class文件的结构我们可以知道,字段表的结构如下:

image.png
前三个分别是访问标志、常量池中字段的简单名称的引用、常量池中字段的描述符的引用、属性表计数器和属性集合。前三个正好对应FieldInfo的构造方法参数。由于我们代理类中的字段没有属性,所以这里没有关于属性的成员变量。
再看我们构造FieldInfo时传递的参数:

  1. this.fields.add(new FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));

回想之前生成的$Proxy0类里面的字段,它们都是Method类型,所以描述符就是Ljava/lang/reflect/Method,
简单名称就是之前我们在构造proxyMethod时构造的名称,访问标志这里给了默认的10,也就是private static,这与$Proxy0这个类中的也是一致的。

  1. ProxyGenerator.this.cp.getUtf8(param1String1);
  2. ProxyGenerator.this.cp.getUtf8(param1String2);

这两句代码也很重要,这里先不进行讲解,放到常量池的讲解里面。

MethodInfo到方法表的映射

我们先看MethodInfo的声明:

  1. private class MethodInfo {
  2. public int accessFlags;
  3. public String name;
  4. public String descriptor;
  5. public short maxStack;
  6. public short maxLocals;
  7. public ByteArrayOutputStream code = new ByteArrayOutputStream();
  8. public List<ProxyGenerator.ExceptionTableEntry> exceptionTable = new ArrayList<>();
  9. public short[] declaredExceptions;
  10. public MethodInfo(String param1String1, String param1String2, int param1Int) {
  11. this.name = param1String1;
  12. this.descriptor = param1String2;
  13. this.accessFlags = param1Int;
  14. ProxyGenerator.this.cp.getUtf8(param1String1);
  15. ProxyGenerator.this.cp.getUtf8(param1String2);
  16. ProxyGenerator.this.cp.getUtf8("Code");
  17. ProxyGenerator.this.cp.getUtf8("Exceptions");
  18. }

同样,MethodInfo的声明也对应class文件中方法表的结构,

image.png

构造方法中的前三个参数分别对应简单名称、描述符和访问标志。由于方法中会有code属性,所以MethodInfo中还有Code属性的一些字段,例如操作数栈的最大值和局部变量表需要的存储空间,等等。构造函数中关于ConstantPool的调用我们同样放到之后对ConstantPool的介绍中进行讲解。
从generateClassFile的代码中我们可以看到,除了对代理类中需要代理的方法使用proxyMethod.generateMethod()方法生成对应的MethodInfo,还生成了构造函数和静态块对应的MethodInfo,分别是通过调用generateConstructor()和generateStaticInitializer()方法实现的。

ConstantPool类与常量池

我们在将字段和方法映射成MethodInfo和FieldInfo时,通过构造方法会传入字段和方法的简单名称和描述符,但是class文件中简单名称和描述符这些都是常量池中常量的内容,字段表和方法表中存在的只是到常量池中这些常量的引用,所以我们必须构造出代表class文件常量池部分的表示,这就是ProxyGenerator.ConstantPool这个内部类的作用。
我们先想一下class文件中常量池部分的结构:
常量池中主要存放两大类常量:字面量和符号引用。字面量就是文本字符串、声明为final的常量值等等,而符号引用包括:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

每种常量都是表类型的数据,每种常量都有自己的结构,有些常量保存的直接就是字符串或者数值类型的值,例如COSNTANT_Utf8_info、CONSTANT_Integer_info,而有些常量不直接保存值而是引用其他常量例如CONSTANT_Fieldref_info就会引用CONSTANT_Class_info和CONSTANT_NameAndType_info,这就要求我们的ConstantPool这个类至少能支持这些功能。

相关类和含义

先看ConstantPool的声明:

  1. private static class ConstantPool {
  2. private List<ProxyGenerator.ConstantPool.Entry> pool;
  3. private Map<Object, Short> map;
  4. private boolean readOnly;
  5. private ConstantPool() {
  6. this.pool = new ArrayList(32);
  7. this.map = new HashMap(16);
  8. this.readOnly = false;
  9. }

pool是一个list,代表常量池中所有的常量条目(Entry),map记录的是常量到常量索引(该常量是常量池中的第几个常量)的映射,为什么map的value是short类型呢,因为class文件中常量计数器是一个u2类型的值,
Entry的声明如下:

  1. private abstract static class Entry {
  2. private Entry() {
  3. }
  4. public abstract void write(DataOutputStream var1) throws IOException;
  5. }

这是一个抽象类,有两个实现:

  1. private static class ValueEntry extends ProxyGenerator.ConstantPool.Entry {
  2. private Object value;
  3. public ValueEntry(Object var1) {
  4. super(null);
  5. this.value = var1;
  6. }
  1. private static class IndirectEntry extends ProxyGenerator.ConstantPool.Entry {
  2. private int tag;
  3. private short index0;
  4. private short index1;
  5. public IndirectEntry(int var1, short var2) {
  6. super(null);
  7. this.tag = var1;
  8. this.index0 = var2;
  9. this.index1 = 0;
  10. }
  11. public IndirectEntry(int var1, short var2, short var3) {
  12. super(null);
  13. this.tag = var1;
  14. this.index0 = var2;
  15. this.index1 = var3;
  16. }

ValueEntry就是我们说的直接保存字面量或者符号引用的那类常量,仅有的一个属性value表示保存的字面量或者符号常量的值。
IndirectEntry就是引用其他常量的那类常量,有三个属性,tag,index0和index1,因为在这类常量里面,一部分只有一个到其他常量的引用,如CONSTANT_Class_info、CONSTANT_String_info等,一部分有两个到其他常量的引用,如CONSTANT_NameAndType_info、CONSTANT_Fieldref_info等。

操作

我们把对ConstantPool的操作分为对直接常量(ValueEntry)和非直接常量(IndirectEntry)两类来分析。

直接常量(ValueEntry)的操作

ConstantPool中对直接常量的操作有getUtf8()、getInteger()和getFloat()。我们以getUtf8()为例,看看实现逻辑:

  1. public short getUtf8(String param1String) {
  2. if (param1String == null)
  3. throw new NullPointerException();
  4. return getValue(param1String);
  5. }

调用的是getValue方法:

  1. private short getValue(Object param1Object) {
  2. Short short_ = this.map.get(param1Object);
  3. if (short_ != null)
  4. return short_.shortValue();
  5. if (this.readOnly)
  6. throw new InternalError("late constant pool addition: " + param1Object);
  7. short s = addEntry(new ValueEntry(param1Object));
  8. this.map.put(param1Object, new Short(s));
  9. return s;
  10. }

首先,从ConstantPool的成员变量map中取这个值,如果有,直接返回取到的short值。如果没有,就会调用addEntry方法先增加一个条目,然后将直接常量要保存的值作为key放入map,value是addEntry方法返回的short值。
我们看addEntry方法的实现:

  1. private short addEntry(ProxyGenerator.ConstantPool.Entry var1) {
  2. this.pool.add(var1);
  3. if (this.pool.size() >= 65535) {
  4. throw new IllegalArgumentException("constant pool size limit exceeded");
  5. } else {
  6. return (short)this.pool.size();
  7. }
  8. }

也很简单,就是在ConstantPool的成员变量pool中增加这个值,然后返回pool的大小,因为list是按顺序增加的,所以pool的大小就是该常量项在常量池中的位置。
这样,一个直接常量的操作就完成了。

非直接常量(IndirectEntry)的操作

ConstantPool中对非直接常量的操作有getClass()/getString()/getFieldRef()/getMethodRef()/getInterfaceMethodRef/getNameAndType(),我们这里介绍两个,一个是getString,一个是getFieldRef.
getString实现如下:

  1. public short getString(String var1) {
  2. short var2 = this.getUtf8(var1);
  3. return this.getIndirect(new ProxyGenerator.ConstantPool.IndirectEntry(8, var2));
  4. }

它首先是调用了getUtf8这个我们已经分析过的方法,因为CONSTANT_String_info变量会引用CONSTANT_utf8_info,所以首先要把字符串值作为CONSTANT_Utf8_info条目添加到pool中,返回的值就是这个CONSTANT_Utf8_info常量在常量池中的索引,而我们的CONSTANT_String_info只需要保存这个索引就行了,然后调用的是getIndirect方法:

  1. private short getIndirect(ProxyGenerator.ConstantPool.IndirectEntry var1) {
  2. Short var2 = (Short)this.map.get(var1);
  3. if (var2 != null) {
  4. return var2;
  5. } else if (this.readOnly) {
  6. throw new InternalError("late constant pool addition");
  7. } else {
  8. short var3 = this.addEntry(var1);
  9. this.map.put(var1, new Short(var3));
  10. return var3;
  11. }
  12. }

与getValue的逻辑类似,同样会进行获取、判断然后增加的流程,最后将Entry添加到pool中,对应的索引放入map。
有个细节请注意,IndirectEntry中有一个tag字段,因为每种常量的结构的第一个字段都是tag,用来表示这个常量是哪种类型的,例如CONSTANT_String_info的tag就是8,正好与代码中的一致。下表是每种常量的结构汇总:
ConstantPool.png
那为什么ValueEntry中没有这个字段呢?这个之后会讲解到。
再看getFieldRef这个方法:

  1. public short getFieldRef(String var1, String var2, String var3) {
  2. short var4 = this.getClass(var1);
  3. short var5 = this.getNameAndType(var2, var3);
  4. return this.getIndirect(new ProxyGenerator.ConstantPool.IndirectEntry(9, var4, var5));
  5. }

因为在class文件结构中,CONSTANT_FieldRef_info这个常量会引用两个常量,分别是CONSTANT_Class_info和Costant_NameAndType_info,所以会先去执行这两个常量的初始化,这里不再赘述,最后也是调用的getIndirect方法来进行pool和map的写入。

构造常量池

在Proxy的代码中,涉及常量池构造的代码很分散,我们这里对重要的几个做简要分析。

MethodInfo和FieldInfo的构造方法

在前面分析MethodInfo和FieldInfo时,我们注意到这两个类的构造方法中有对ConstantPool的调用:

  1. public FieldInfo(String var2, String var3, int var4) {
  2. //省略初始化参数
  3. ProxyGenerator.this.cp.getUtf8(var2);
  4. ProxyGenerator.this.cp.getUtf8(var3);
  5. }
  6. public MethodInfo(String var2, String var3, int var4) {
  7. //省略初始化参数
  8. ProxyGenerator.this.cp.getUtf8(var2);
  9. ProxyGenerator.this.cp.getUtf8(var3);
  10. ProxyGenerator.this.cp.getUtf8("Code");
  11. ProxyGenerator.this.cp.getUtf8("Exceptions");
  12. }

这两个构造方法中将字段和方法使用到的简单名称和描述符添加到常量池中。还有代表Code属性和Exception属性的字面值常量。

在generateClassFile中的使用

ConstantPool另外一个用到的地方就是generateClassFile中,我把关键的代码都加上了注释,这段代码分析完,整个字节数组的生成过程也就结束了。

  1. if (this.methods.size() > 65535)
  2. //方法的数量不能超过u2类型能够表示的最大值
  3. throw new IllegalArgumentException("method limit exceeded");
  4. if (this.fields.size() > 65535)
  5. //字段的数量不能超过u2类型的最大值
  6. throw new IllegalArgumentException("field limit exceeded");
  7. //将当前类的全限定类名放入常量池
  8. this.cp.getClass(dotToSlash(this.className));
  9. //将父类的全限定类名放入常量池
  10. this.cp.getClass("java/lang/reflect/Proxy");
  11. //将需要实现的接口的全限定名放入常量池
  12. for (Class<?> clazz : this.interfaces)
  13. this.cp.getClass(dotToSlash(clazz.getName()));
  14. this.cp.setReadOnly();
  15. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  16. DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
  17. try {
  18. //写入魔数 CAFEBABE
  19. dataOutputStream.writeInt(-889275714);
  20. //写入主版本号和次版本号
  21. dataOutputStream.writeShort(0);
  22. dataOutputStream.writeShort(49);
  23. //将常量池的内容写入流
  24. this.cp.write(dataOutputStream);
  25. //写入访问标志
  26. dataOutputStream.writeShort(this.accessFlags);
  27. //写入类索引
  28. dataOutputStream.writeShort(this.cp.getClass(dotToSlash(this.className)));
  29. //写入父类索引
  30. dataOutputStream.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
  31. //写入接口计数器
  32. dataOutputStream.writeShort(this.interfaces.length);
  33. //写入每个接口索引
  34. for (Class<?> clazz : this.interfaces)
  35. dataOutputStream.writeShort(this.cp.getClass(
  36. dotToSlash(clazz.getName())));
  37. //写入字段表集合数量
  38. dataOutputStream.writeShort(this.fields.size());
  39. //写入每个字段表
  40. for (FieldInfo fieldInfo : this.fields)
  41. fieldInfo.write(dataOutputStream);
  42. //写入方法表集合数量
  43. dataOutputStream.writeShort(this.methods.size());
  44. //写入每个方法表
  45. for (MethodInfo methodInfo : this.methods)
  46. methodInfo.write(dataOutputStream);
  47. dataOutputStream.writeShort(0);
  48. } catch (IOException iOException) {
  49. throw new InternalError("unexpected I/O Exception", iOException);
  50. }
  51. return byteArrayOutputStream.toByteArray();

小结

通过分析Proxy类中的关键方法和内部类,我们整理出了代理类字节码生成中清晰的流程和映射关系。字节码生成无非就是构造出类的二进制表示,而java对二进制的格式有严格的要求,所以我们只要按照格式要求,构造出每个部分,例如常量池、字段表、方法表,就不难达成目的。
Proxy类中,MethodInfo、FiledInfo和ConstantPool三个重要的类分别用来生成方法表、字段表和常量池,理解了这三个类的存在意义和关键逻辑,就能轻松理解整个字节码的生成过程。
由于篇幅限制,一些同样很重要的方法,例如ConstantPool的write方法等,没有给出分析,读者有兴趣可以自己阅读源码。