1 本文概要
本章介绍Java类的加载、连接和初始化的深入知识,并重点介绍Java反射相关的内容。本章知识偏底层点,这些运行原理有助于我们更好的把我java程序的运行。而且Java类加载器除了根加载器之外,其他类加载器都是使用Java语言编写的,所以我们完全可以开发自己的类加载器,通过使用自定义的类加载器,完成一些特定的功能。
本章重点介绍java.lang.reflect包下的接口和类,包括Class、Method、Field、Constructor和Array,这些类分别代表类、方法、属性、构造器和数组,Java程序可以使用这些类动态地获取某个对象、某个类的运行时信息,并可以动态地创建Java对象,动态地调用Java方法,访问并修改指定对象的属性值。本章还介绍该包下的Type和ParameterizedType两个接口,其中Type是Class类所实现的接口,而ParameterizedType则代表一个带泛型参数的类型。
除此之外,本章还介绍利用Proxy和InvocationHandler来创建JDK动态代理,并会通过JDK动态代理介绍高层次解耦的方法,还会介绍JDK动态代理和AOP(面向切面编程)之间的内在关系。
2 类的加载、连接和初始化
系统可能在第一次使用某个类时加载该类,也可能采用预先加载机制来预加载某个类,本节将会详细介绍类加载、连接和初始化过程中的每个细节。
2.1 JVM和类
当我们调用Java命令运行某个Java程序时,该命令会启动一条Java虚拟机进程,也就是JVM进程。不管该JAVA程序多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。正如前所述,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止。
1.程序运行到最后正常结束。
2.程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序。
3.程序执行过程中遇到未捕获的异常或错误而结束
4.程序所在平台强制结束了JVM进程
从上面的介绍可以看出,当Java程序运行结束时,JVM进程结束,该进程在内存中状态将会丢失。下面我们以类的静态属性来说明这个问题。先定义一个包含静态属性的类。
package chapter10; public class A {
//定义类的静态属性
public static int a=7;
}
下面定义一个类创建A类的实例,并访问A对象的a属性
package chapter10; public class TestA1 {
public static void main(String[] args){
A a= new A();
(a.a)++;
System.out.println(a.a);
}
}
下面的类也是访问A的属性a
package chapter10; public class TestA2 {
public static void main(String[] args){
A b = new A();
System.out.println(b.a);
}
}
分析:
在TestA1.java程序中创建了A类的实例,并让该实例的a属性自加,程序输出该实例的a属性将看到8,这个答案是显然的。关键是运行第二个程序TestA2时,程序再次创建了A对象,并输出A对象的a属性值。这个结果依然是7并不是8.为什么呢?a属性不是static吗?关键是因为运行TestA1和TestA2是两次JVM进程,第一次JVM运行结束后,它对A类所做的修改将全部丢失。第二次运行JVM时将再次初始化A类。两个JVM之间并不会共享数据。
2.2 类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不通的类加载器,可以从不同来源加载类的二进制数据,通常有以下几种来源:
1.从本地文件系统来加载class文件,这是前面绝大部分示例程序的类加载方式
2.从JAR包中加载class文件,这种方式也场景,JDBC编程时用到的数据库驱动类就是防止JAR文件中,JVM可以从JAR文件中直接加载该class文件
3.通过网络加载class文件
4.把一个Java源文件动态编译、并执行加载
类加载器通常无须等到首次使用该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
2.2 类的连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段将会负责把类的二进制数据合并到JRE中。类连接又可分为3个阶段。
1.验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
2.准备:类准备阶段则负责为类的静态属性分配内存,并设置默认初始值。
3.解析:将类的二进制数据中的符号引用缓存直接引用。
2.3 类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态属性进行初始化。在Java类中对静态属性指定初始值有两种方式:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。
package chapter10; @SuppressWarnings("unused")
public class Test {
static{
//使用静态初始化块为变量赋值
int b = 9;
System.out.println("--------------------");
}
static int a = 2;
static int b = 3;
static int c;
public static void main(String[] args){
System.out.println(Test.b);
}
}
声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们。
上面代码先在静态初始化块中为b变量赋值,此时静态属性b的值为9;接着程序向下执行,执行到一般的定义b的初始化语句static int b = 3;,程序再次为静态属性b赋值,也就是说Test类初始化结束后,该类的静态属性b的值为3
2.3.1 JVM初始化一个类包含如下几个步骤
1.假如这个类还没有被加载和连接,程序先加载并连接该类。
2.假如该类的直接父类还没有被初始化,则先初始化其直接父类。
3.假如类中有初始化语句,则系统依次执行这些初始化语句。
当执行第二步骤时,系统同样会按照三个步骤,依次递归。所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
2.3.2 类初始化的时机
当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口:
1.创建类的实例。为某个类创建实例的方式包括使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
2.调用某个类的静态方法
3.访问某个类或接口的静态属性,或为该静态属性赋值。
4.使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:
Class.forName("Person"),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。
5.初始化某个类的子类,当初始化某个类的子类,该子类的所有父类都会被初始化。
6.直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。
除此之外,下面有几种情形需要特别指出:
对于一个final型的静态属性,如果该属性可以在编译时就得到属性值,则可认为该属性可被当成编译时常量。当程序使用编译时常量,系统会认为这是对该类的被动使用,所以不会导致该类的初始化。
package chapter18; class MyTest{
static{
System.out.println("静态初始化块。。。");
}
static final String compileConstant = "Buenas Noches";
} public class TestCompileConstant {
public static void main(String[] args){
//访问、输出MyTest的compileConstant属性
System.out.println(MyTest.compileConstant);①
}
}
上面程序的MyTest类中有一个compileConstant的静态属性,该变量使用了final修饰,而且它的值可以在编译时得到,因为这个compileConstant被当成变量常量处理。程序中所有使用编译常量的地方会在编译时被直接替换成该常量的值————也就是说,上面程序中①处的代码在编译时就会被替换成Buenas Noches,所以①行代码不可能初始化MyTest类
当某个静态属性使用final修饰,而且它的值可以在编译时得到,那么程序其他地方使用该静态属性时,实际上并不会使用该静态属性,而是相当于使用常量。反之,如果final类型的静态属性的值不能在编译时得到,必须等到运行时才可以确定该属性的值,如果通过该类来访问该静态属性,则可以认为是主动访问使用该类,将会导致该类被初始化。例如把上面程序中定义compileConstant的代码改为如下:
//采用系统当前时间为static final属性赋值
static final String compileConstant = System.currentTimeMillis() + "";
此时的compileConstant属性值必须在运行时才可以确定,所以必须保留为对MyTest类静态属性的引用,这行代码就变成了使用MyTest的静态属性,这将导致MyTest类被初始化。
当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。当使用Class的forName()静态方法才会导致强制初始化该类。如下面代码:
class Tester{
static{
System.out.println("Tester类的静态初始化...");
}
} public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException{
ClassLoader cl = ClassLoader.getSystemClassLoader();
//下面语句仅仅是加载Tester类
cl.loadClass("Tester");
System.out.println("系统加载Tester类");
//下面语句才会初始化Tester类
Class.forName("Tester");
}
}
输出结果:
系统加载Tester类
Tester类的静态初始化...
从上面运行结果可以看出,必须等到执行Class.forName("Tester")时才完成对Tester类的初始化
3 类加载器
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
类装载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是怎么样才算同一个类。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
同理,载入JVM的类也有一个唯一的标识,在Java中,一个类用其全限定类名(包括包名和类名)作为标识。但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。因此,如果在pg的包中,有一个名为Person的类,被类加载器KlassLoader的实例kl负责加载。则该Person类对应的Class对象在JVM中表示为(Person、pg、kl),这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的,他们所加载的类也是完全不同的,互不兼容。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:
Bootstrap ClassLoader:根类加载器
Extension ClassLoader:扩展类加载器
System ClassLoader:系统类加载器
Bootstrap ClassLoader,被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。在Sun的JVM种,当执行java.exe的命令时使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。
根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。下面程序可以获得根类加载器加载了哪些核心类库:
package chapter18; import java.net.*; public class BootstrapTest {
public static void main(String[] args){
//获取根类加载器所加载的全部URL数组
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//遍历、输出根类加载器所加载的全部URL
for(int i = 0; i < urls.length; i++){
System.out.println(urls[i].toExternalForm());
}
}
}
编译上面程序,可能会有一条小叉叉警告信息,无须理会该警告信息,运行可以看到如下结果:
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/lib/modules/jdk.boot.jar
file:/C:/Program%20Files/Java/jdk1.6.0_27/jre/classes
看到这个结果,是因为String、System这些核心类库都在jdk的jre/lib/rt.jar文件中。
Extention ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR的类包
通过这种方式,我们就可以为Java扩展核心类以外的新功能,只要我们把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。
System Classloader,被称为系统(也称为应用)类加载器,它负责在JVM启动时,加载来自命令java中的-classpath选项或java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。
程序可以通过ClassLoader的静态方法getSystemClassLoader()获取该类加载器,如果没有特别指定,则用户自定义的类加载器都以该类加载器作为它的父加载器。
3.1 类加载机制
JVM的类加载机制主要有如下三种机制:
1.全盘负责:所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
2.父类委托:所谓父类委托则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
3.缓存机制:缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在该Class对象时,系统才会重新读取该类对应的二进制数据,并将其转换成Class对象,并存入cache。这就睡为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。
类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。JVM中这四种类加载器的层次结构如下:
类加载器的父子关系:
用户类加载器————>系统类加载器————>扩展类加载器————>根类加载器
类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
package chapter18; import java.io.IOException;
import java.net.URL;
import java.util.Enumeration; public class ClassLoaderPropTest {
public static void main(String[] args)throws IOException{
//获取系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
/*
获取系统类加载器的加载路径————通常由CLASSPATH环境变量指定,如果操作
系统没有指定CLASSPATH环境变量,默认以当前路径作为系统类加载器的加载
路径
*/
Enumeration<URL> eml = systemLoader.getResources("");
while(eml.hasMoreElements()){
System.out.println(eml.nextElement());
}
//获取系统类加载器的父类加载器————应该得到扩展类加载器
ClassLoader extension = systemLoader.getParent();
System.out.println("扩展类加载器:" + extension);
System.out.println("扩展类加载器的加载路径: "
+ System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent:" + extension.getParent());
}
}
运行结果:
系统类加载器:sun.misc.Launcher$AppClassLoader@addbf1
file:/E:/CodeLibrary/JavaExcercise/bin/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@42e816
扩展类加载器的加载路径: C:\Program Files\Java\jdk1.6.0_27\jre\lib\ext;C:\windows\Sun\Java\lib\ext
扩展类加载器的parent:null
从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是C:\Program Files\Java\jdk1.6.0_27\jre\lib\ext;C:\windows\Sun\Java\lib\ext。但我们看到扩展类加载器的父加载器是null,并不是跟类加载器,这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回null,但实际上,根类加载器确实是扩展类加载器的父类加载器。
程序看到系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例,实际上这两个类都是URLClassLoader类的实例。
类加载器加载Class大致需要经过如下8个步骤:
1.检测此Class是否载入过(即在缓存中是否有此Class),如果有则直接进入第8步,否则接着执行第2步
2.如果父加载器不存在(如果没有父加载器,则要么parent一定是根加载器,要么本身就是跟加载器),则跳到第4步执行。如果父加载器存在,则接着执行第3步
3.请求父加载器载入目标类,如果成功载入则跳到第8步,不成功接着执行第5步。
4.请求使用根加载器来载入目标类,如果成功到8,如果不成功跳到第7步。
5.寻找Class文件(从与此ClassLoader相关的类路径中寻找)。如果找到则执行第6步,如果找不到则跳到第7步·
6.从文件中载入Class,成功载入后跳到第8步
7.抛出ClassNotFoundException
8.返回Class
其中5、6步允许重写ClassLoader的findClass方法来实现自己的载入策略,甚至重写loadClass方法来实现自己的载入过程。
3.2 创建并使用自定义的类加载器
JVM中除根加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量protected方法————这些方法都可被子类重写。
ClassLoader类有如下三个关键方法:
1.loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
2.findClass(String name):根据二进制名称来查找类
如果需要实现自定的ClassLoader,可以通过重写以上两个方法来实现,当然我们推荐重写findClass()方法,而不是重写loadClass()方法。因为loadClass方法的执行步骤如下:
1.用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回
2.在父类加载器上调用loadClass方法,如果父类加载器为null,则使用根类加载器来加载
3.调用findClass(String)方法查找类
从上面步骤可以看出,重新findClass方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略,如果重写loadClass方法,则实现逻辑更为复杂。
在ClassLoader里还有一个核心方法:Class defineClass(String name,byte[]b,int off,int len),该方法负责将指定类的字节码文件(即class文件,如Hello.class)读入字节数组:byte[] b内,并把它转化为Class对象,该字节码文件可以来源于文件、网络等。
defineClass管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等等。不过不用担心,程序员无须重写该方法。事实上,该方法是final型,即使我们想重写也没有机会。
除此之外,ClassLoader里还包含如下一些普通方法:
下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass方法来实现自定义的类加载机制,这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,从而允许使用该ClassLoader来直接运行Java源文件。
1 package chapter18;
2
3 import java.io.*;
4 import java.lang.reflect.InvocationTargetException;
5 import java.lang.reflect.Method;
6
7 public class CompileClassLoader extends ClassLoader{
8 //读取一个文件的内容
9 private byte[] getBytes(String filename) throws IOException{
10 File file = new File(filename);
11 long len = file.length();
12 byte[] raw = new byte[(int)len];
13 FileInputStream fin = new FileInputStream(file);
14 //一次读取class文件的全部二进制数据
15 int r = fin.read(raw);
16 if(r != len){
17 throw new IOException("无法读取全部文件: " + r + "!=" + len);
18 }
19 fin.close();
20 return raw;
21 }
22 //定义编译指定Java文件的方法
23 private boolean compile(String javaFile) throws IOException{
24 System.out.println("CompileClassLoader:正在编译" + javaFile + "....");
25 //调用系统的javac命令本身
26 Process p = Runtime.getRuntime().exec("javac" + javaFile);
27 try{
28 //其他线程都等待这个线程完成
29 p.waitFor();
30 }catch(InterruptedException ie){
31 ie.printStackTrace();
32 System.out.println(ie);
33 }
34 //获取javac线程的退出值
35 int ret = p.exitValue();
36 //返回编译是否成功
37 return ret == 0;
38 }
39 //重写ClassLoader的findClass方法
40 protected Class<?> findClass(String name) throws ClassNotFoundException{
41 Class clazz = null;
42 //将包路径中的点.换成斜线/
43 String fileStub = name.replace(".", "/");
44 String javaFilename = fileStub + ".java";
45 String classFilename = fileStub + ".class";
46 File javaFile = new File(javaFilename);
47 File classFile = new File(classFilename);
48 //当指定Java源文件存在,且class文件不存在或者Java源文件
49 //的修改时间比class文件修改时间晚时,重新编译
50 if(javaFile.exists() && (!classFile.exists())
51 || javaFile.lastModified() > classFile.lastModified()){
52 try{
53 //如果编译失败或者该class文件不存在
54 if(!compile(javaFilename) || !classFile.exists()){
55 throw new ClassNotFoundException(
56 "ClassNotFoundException:" + javaFilename
57 );
58 }
59 }catch(IOException ex){
60 ex.printStackTrace();
61 }
62 }
63 //如果class文件存在,则系统负责将该文件转换成Class对象
64 if(classFile.exists()){
65 try{
66 //将class文件的二进制数据读入数组
67 byte[] raw = getBytes(classFilename);
68 //调用 ClassLoader的defineClass方法将二进制数据转换成class对象
69 clazz = defineClass(name, raw, 0, raw.length);
70 }catch(IOException ex){
71 ex.printStackTrace();
72 }
73 //如果clazz为null,表明加载失败,则抛出异常
74 if(clazz == null){
75 throw new ClassNotFoundException(name);
76 }
77 }
78 return clazz;
79 };
80 public static void main(String[] args) throws Exception{
81 //如果运行时没有参数,即没有目标类
82 if(args.length < 1){
83 System.out.println("缺少运行的目标类,请按如下格式运行java源文件:");
84 System.out.println("java CompileClassLoader ClassaName");
85 }
86 //第一个参数是需要运行的类
87 String progClass = args[0];
88 //剩下的参数将作为运行目标类时的参数,所以将这些参数复制到一个新数组中
89 String progArgs[] = new String[args.length - 1];
90 //加载需要运行的类
91 System.arraycopy(args, 1, progArgs, 0, progArgs.length);
92 CompileClassLoader ccl = new CompileClassLoader();
93 //加载需要运行的类
94 Class<?> clazz = ccl.loadClass(progClass);
95 //获取运行类的主方法
96 Method main = clazz.getMethod("main", (new String[0]).getClass());
97 Object argsArray[] = {progArgs};
98 main.invoke(null, argsArray);
99 }
100 }
上面程序重写了findClass方法,通过重写该方法就可以实现自定义的类加载机制,在本类的findClass方法中先检查需要加载类的class文件是否存在,如果不存在则先编译源文件,再调用ClassLoader的defineClass()方法来加载这个class文件,并生成相应的Class对象。