疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

类加载机制与反射 --- 类加载器


 

       (一)类加载器

  类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。

  (二)类加载器简介

  类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入。

  正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

  当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构:

  (1)Bootstrap ClassLoader: 根类加载器

  (2)Extension ClassLoader:扩展类加载器

  (3)System ClassLoader:系统类加载器

  BootStrap ClassLoader 被称为引导类加载器,它负责加载Java的核心类。根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。

  Extension Classloader 被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的目录%)中JAR包的类。

  System Classloader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。(程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都是以类加载器作为父类加载器。)

  (三)类加载机制

  JVM的类加载机制主要有如下3种:

  (1)全盘负责:所谓全盘负责,就是当一种类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。

  (2)父类委托:所谓父类委托,则是先让parent(类)加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

  (3)缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。

  下面程序示范了访问JVM的类加载器:

  复制代码

  public class ClassLoaderPropTest

  {

  public static void main(String[] args)

  {

  //获取系统类加载器

  ClassLoader systemLoader = ClassLoader.getSystemClassLoader();

  System.out.println("系统类加载器: " + systemLoader);

  //获取系统类加载器的加载路径--通常由CLASSPATH环境变量指定

  //如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为

  //系统类加载器的加载路径

  ClassLoader extensionLader = systemLoader.getParent();

  System.out.println("扩展类加载器: " + extensionLader);

  System.out.println("扩展类加载器的加载路径: " + System.getProperty("java.ext.dirs"));

  System.out.println("扩展类加载器的parent: " + extensionLader.getParent());

  }

  }

  运行结果:

  系统类加载器: sun.misc.Launcher$AppClassLoader@105d88a

  扩展类加载器: sun.misc.Launcher$ExtClassLoader@cb6009

  扩展类加载器的加载路径: D:javaJDKlibext;C:WindowsSunJavalibext

  扩展类加载器的parent: null

  类加载器加载Class大致要经过如下8个步骤:

  (1)检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第(8)步,否则接着执行第(2)步。

  (2)如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第(4)步执行;如果父类加载器存在,则接着执行第(3)步。

  (3)请求使用父类加载器去载入目标类,如果成功载入则跳到第(8)步,否则接着执行第(5)步。

  (4)请求使用根类加载器来载入目标类,如果成功载入则跳到第(8)步,否则跳到第(7)步

  (5)当前类加载器尝试寻找Class文件,如果找到则执行第(6)步,如果找不到则跳到第(7)步

  (6)从文件中载入Class,成功载入后跳到第8步。

  (7)抛出ClassNotFoundException异常

  (8)返回对应的java.lang.Class对象

  (四)创建并使用自定义的类加载器

  JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoaer所包含的方法来实现自定义的类加载器。

  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()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略。

  在ClassLoader里还有一个核心方法:Class defineClass(String name,byte[] b,int off,int len),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。

  下面程序开发一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制:

  复制代码

  import java.io.File;

  import java.io.FileInputStream;

  import java.io.IOException;

  import java.lang.reflect.Method;

  public class CompileClassLoader extends ClassLoader

  {

  //读取一个文件的内容

  private byte[] getBytes(String filename) throws IOException

  {

  File file = new File(filename);

  long len = file.length();

  byte[] raw = new byte[(int)len];

  try

  {

  FileInputStream fin = new FileInputStream(file);

  //一次读取Class文件的全部二进制数据

  int r = fin.read(raw);

  if(r != len)

  {

  throw new IOException("无法读取全部文件:" + r + " != " + len);

  }

  }

  catch(Exception e)

  {

  e.printStackTrace();

  }

  return raw;

  }

  //编译指定Java文件的方法

  private boolean compile(String javaFile) throws IOException

  {

  System.out.println("ComplileClassLoader:正在编译: " + javaFile + " ...");

  //调用系统的javac命令

  Process p = Runtime.getRuntime().exec("javac " + javaFile);

  try

  {

  //其他线程都等待这个线程完成

  p.waitFor();

  }

  catch(InterruptedException ie)

  {

  System.out.println(ie);

  }

  //获取javac线程的退出值

  int ret = p.exitValue();

  //返回编译是否成功

  return ret == 0;

  }

  //重写ClassLoader的findClass方法

  @Override

  protected Class findClass(String name) throws ClassNotFoundException

  {

  Class clazz = null;

  //将包路径中的点(.)替换成斜线(/)

  String fileStub = name.replace(".", "/");

  String javaFilename = fileStub + ".java";

  String classFilename = fileStub + ".class";

  File javaFile = new File(javaFilename);

  File classFile = new File(classFilename);

  //当指定Java源文件存在,且class文件不存在,

  //或者Java源文件的修改时间比Class文件的修改时间更晚,重新编译

  if(javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified()))

  {

  try

  {

  //如果编译失败,或者该Class文件不存在

  if(!compile(javaFilename) || !classFile.exists())

  {

  throw new ClassNotFoundException("ClassNotFoundException : " + javaFilename);

  }

  }

  catch(IOException e)

  {

  e.printStackTrace();

  }

  }

  //如果class文件存在,系统负责将该文件转换成Class对象

  if(classFile.exists())

  {

  try

  {

  //将class文件的二进制数据读入数组

  byte[] raw = getBytes(classFilename);

  //调用ClassLoader的defineClass方法将二进制数据转换成Class对象

  clazz = defineClass(name,raw,0,raw.length);

  }

  catch(IOException e)

  {

  e.printStackTrace();

  }

  }

  //如果clazz为null,表明加载失败,则抛出异常

  if(clazz == null)

  {

  throw new ClassNotFoundException(name);

  }

  return clazz;

  }

  public static void main(String[] args) throws Exception

  {

  //如果运行该程序时没有参数,即没有目标类

  if(args.length < 1)

  {

  System.out.println("缺少目标类,请按如下格式运行Java源文件");

  System.out.println("java CompileClassLoader ClassName");

  }

  //第一个参数是需要运行的类

  String progClass = args[0];

  //剩下的参数将作为运行目标类时的参数

  //将这些参数复制到一个新数组中

  String[] progArgs = new String[args.length - 1];

  System.arraycopy(args, 1, progArgs, 0, progArgs.length);

  CompileClassLoader ccl = new CompileClassLoader();

  //加载需要运行的类

  Class clazz = ccl.loadClass(progClass);

  //获取需要运行的类的主方法

  Method main = clazz.getMethod("main", (new String[0]).getClass());

  Object argsArray[] = {progArgs};

  main.invoke(null, argsArray);

  }

  }

  复制代码

  (五)URLClassLoader类

  Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类。URLClassLoader类功能强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

  URLClassLoader类提供了如下两个构造器:

  (1)URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。

  (2)URLClassLoader(URL[] urls,ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。

  一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。

  下面程序示范如何直接从文件系统中加载MYSQL驱动:

  复制代码

  import java.net.URL;

  import java.net.URLClassLoader;

  import java.sql.Connection;

  import java.sql.Driver;

  import java.util.Properties;

  public class URLClassLoaderTest

  {

  private static Connection conn;

  public static Connection getConn(String url,String user,String pass)

  throws Exception

  {

  if(conn == null)

  {

  //创建一个URL数组

  URL[] urls = {

  new URL("file:D:\原程序包\mysql\mysql-connector-java-5.1.7-bin.jar")

  };

  //以默认的ClassLoader作为父ClassLoader,创建URLClassLoader

  URLClassLoader myClassLoader = new URLClassLoader(urls);

  //加载MYSQL的JDBC驱动,并创建默认实例

  Driver driver = (Driver)myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();

  //创建一个设置JDBC连接属性的Properties对象

  Properties props = new Properties();

  //至少需要为该对象传入user和password两个属性

  props.setProperty("user", user);

  props.setProperty("password", pass);

  //调用Driver对象的connect方法来获取数据库连接

  conn = driver.connect(url, props);

  }

  return conn;

  }

  public static void main(String[] args) throws Exception

  {

  System.out.println(getConn

  ("jdbc:mysql://localhost:3306/mysql","root","199100"));

  }

  }

  复制代码

  运行结果:

  com.mysql.jdbc.JDBC4Connection@1d22104