资讯专栏INFORMATION COLUMN

猫头鹰的深夜翻译:理解java的classloader

Eminjannn / 2112人阅读

摘要:它们是通过来自远程的服务器的连接发送字节码并在本地运行,这一点令人兴奋。中有一个自定义的,它不是从本地文件系统加载类文件,而是从远程服务器上获取,通过加载原始字节码,再在中转化为类。它将字节码解析为运行时的数据结构,检查其有效性等。

前言

Java ClassLoader是java运行系统中一个至关重要但是经常被忽略的组件。它负责在运行时寻找并加载类文件。创建自定义的ClassLoader可以彻底重定义如何将类文件加载至系统。

这个教程对Java的ClassLoader进行总体概述,并给了一个自定义ClassLoader的例子。这个ClassLoader会在加载代码之前自动编译。你将会了解ClassLoader是做什么的,以及如何创建自定义ClassLoader。

本教程需要阅读者对Java编程有基础了解,包括创建,编译和执行简单的命令行Java程序。

阅读完本教程之后,你会知道如何:

扩展JVM的功能

创建一个自定义的ClassLoader

学习如何将ClassLoader集成至Java应用

修改ClassLoader使其符合Java2版本

什么是ClassLoader

在所有的编程语言中,Java以运行在Java虚拟机上而独树一帜。这意味着编译的程序将以一种独特的,与平台无关的形式运行在目标机器上,而不是目标机器的格式。这种格式在很多方面和传统的可执行程序相比,有很大的区别。

Java程序与C或C++程序最大的不同在于,它不是单个可执行文件,而是由许多多带带的类文件构成,每个类文件对应一个Java类。

不仅如此,这些类文件并不是一次性加载到内存的,而是按需加载的。ClassLoader是JVM的一部分,它将类加载到内存中。

此外,Java ClassLoader是用Java编写的。这意味着可以轻松的创建自己的ClassLoader,无需了解JVM更多的细节。

为什么编写ClassLoader

如果JVM已经有一个ClassLoader了,为什么还要再写一个?好问题,默认的ClassLoader只知道如何从本地的文件系统中加载类文件。一般场景下,当你在本地编写代码并且在本地编译时,完全足够了。

但是,JAVA语言最新颖的特点之一就是可以从本地硬盘或是互联网之外的地方获取类。比如,浏览器使用自定义的ClassLoader从网站上获取可执行内容。

还有很多其它获取类文件的方法。除了从本地或是网上加载类文件,还可以用类加载器来:

在执行不受信任的代码之前自动验证数字签名

使用用户提供的密码透明的解密代码

根据用户的特定需求创建自定义的动态类

任何生成Java字节码的内容都可以集成到你的应用程序中去。

自定义ClassLoader的例子

如果你曾经使用过applet,你肯定用到了一个自定义的类加载器。

在Sun发布Java语言的时候,最令人兴奋的事情之一就是观察这项技术是如何执行从远程Web服务器及时加载代码的。它们是通过来自远程的Web服务器的HTTP连接发送字节码并在本地运行,这一点令人兴奋。

Java语言支持自定义ClassLoader的功能使这一想法成为可能。applet中有一个自定义的ClassLoader,它不是从本地文件系统加载类文件,而是从远程Web服务器上获取,通过Http加载原始字节码,再在jvm中转化为类。

浏览器和Applet中的类加载器还有别的功能:安全管理,防止不同页面上的applet相互影响等。

下面我们将会创建一个自定义的类加载器叫做CompilingClassLoader(CCL)、CCL会帮我们编译Java代码。它基本上就像是在运行系统中直接构建一个简单的make程序。

ClassLoader结构

ClassLoader的基本目的是为类的请求提供服务。JVM需要一个类,于是它通过类的名字询问ClassLoader来加载这个类。ClassLoader试着返回一个代表该类的对象。

通过覆盖此过程不同阶段对应的方法,可以创建自定义的ClassLoader。

在本文的剩余部分,你会了解到ClassLoader中的一些关键方法。你会了解到每个方法的用途以及它在类加载过程中是如何调用的。你还会了解当你在自定义ClassLoader时需要完成的工作。

loadClass方法##、

ClassLoader.loadClass()方法是ClassLoader的入口。它的方法标签如下:

Class loadClass(String name, boolean resolve)

name参数代表JVM需要的类的名称,比如Foo或是java.lang.Object

resolve参数说明类是否需要被解析。可以把类的解析理解为完全的准备好执行类。解析并不是必要的。如果JVM只需要确定该类存在或是找出其父类,则无需解析。

在java1.1版本以前,自定义ClassLoader只需要重写loadClass方法。

defineClass方法

defineClass方法是整个ClassLoader的核心。此方法将原始字节数组转化为一个Class对象。原始字节数组包含从本地或是远程得到的数据。

defineClass负责处理JVM的许多复杂,神秘而且依赖于具体实现的部分。它将字节码解析为运行时的数据结构,检查其有效性等。不用担心,这些你不用自己实现。事实上,你根本没法重写它,因为该方法为final方法。

findSystemClass方法

findSysetmClass方法从本地文件系统中加载文件。它在本地文件系统中查找类文件,如果存在,使用defineClass将其从原始字节转化为类对象。这是JVM在运行Java应用程序时加载类的默认机制。

对于自定义的ClassLoader,我们只会在尝试了别的方法来加载类内容之后,才调用findSystemClass方法。道理很简单:自定义的ClassLoader包含加载特殊类的一些步骤,但是并非所有的类都是特殊类。比如,即便ClassLoader需要从远程网站上获取一些类,还是有许多类需要从本地的Java库中加载。这些类并不是我们关注的重点,因此我们需要JVM用默认的方式来获取。

整个流程如下:

请求自定义ClassLoader加载一个类

查看远程服务器是否有该类

如果有,则获取并返回

如果没有,我们假设该类是位于本地的一个基础类,并调用findSystemClass从文件系统中加载出来。

在大多数自定义的ClassLoader中,你需要先滴啊用findSystemClass来减少对远程网站的访问,因为大多数Java类都位于本地的类库中。但是,在下一节中你会看到,在自动将应用代码编译之前,我们不希望JVM从本地文件系统加载类。

resolveClass方法

如前文所说,类的加载是可以部分进行(不进行解析)或是彻底进行的(进行解析)。当我们实现自己的loadClass方法时,我们或许需要调用resolveClass方法,这取决于loadClass中的resolve参数的值。

findLoadedClass方法

findLoadedClass方法充当一个缓存调用机制:当loadClass方法被调用时,他会调用这个方法来查看类是否已经被加载过了,省去了重复加载。这个方法应当最先被调用。

整合一下

我们的例子中loadClass执行以下几步(这里我们不会特别关注到底采用了什么神奇的方法来获取类文件。它可以是从本地,网络或者是压缩文件中获得的,总之我们获得了原始类文件的字节码):

调用findLoadedClass查看是否已经加载过该类

如果没有,则使用神奇的魔法来获得原始字节码

如果获得字节码,调用defineClass将其转化为Class对象

如果没有获得字节码,则调用findSystemClass,看是否能从本地文件系统获得类

如果resolve值为true,则调用resolveClass来解析Class对象

如果还是没有找到类,则抛出ClassNotFoundException

否则,将类返回给调用者

CompilingClassLoader

CCL的作用是确保代码已经被编译,并且是最新版本的。
以下是该类的描述:

当需要一个类时,查看该类是否在磁盘上,在当前的目录或是相应的子目录下

如果该类不存在,但是其源码存在,在调用Java编译器来生成类文件

如果类文件存在,查看他是否比源码的版本旧,如果低于源码的版本,则重新生成类文件

如果编译失败,或者其他的原因导致无法从源码中生成类文件,抛出ClassNotFoundException

如果还是没有类文件,那么它或许在其他的一些库中,调用findSystemClass看是否有用

如果还是找不到类,抛出ClassNotFoundException

否则,返回类

Java是如何编译的

在深入研究之前,我们应该回过头来看一下Java的编译机制。总的来说,当你请求一个类的时候,Java不只是编译各种类信息,它还编译了别的相关联的类。

CCL会按需一个接一个的编译相关的类。但是,当CCL编译完一个类之后试着去编译其它相关类的时候会发现,其它的类已经编译完成了。为什么呢?Java编译器遵循一个规则:如果一个类不存在,或者它相对于源码已经过时了,就需要编译它。从本质上讲,Java编译器先CCL一步完成了大部分的工作。

CCL在编译类的时候会打印其编译的应用程序。在大多数场景里面,你会看到它在程序的主类上调用编译器。

但是,有一种情况是不会在第一次调用时编译所有类的的。如果你通过类名Class.forNasme加载一个类,Java编译器不知道该类需要哪些信息。在这种场景下,你会看到CCL会再次运行Java编译器。

如何使用CompilingClassLoader

为了使用CCL,我们需要用一种独特的方式启动程序。正常的启动程序如下:

% java Foo arg1 arg2

而我们启动方式如下:

% java CCLRun Foo arg1 arg2

CCLRun是一个特殊的桩程序,它会创建一个CompilingClassLoader并使用它来加载程序的main方法,确保整个程序的类会通过CompilingClassLoader加载。CCLRun使用Java反射API来调用main方法并传参

Java2中ClassLoader的变化

Java1.2以后ClassLoader有一些变动。原有版本的ClassLoader还是兼容的,而且在新版本下开发ClassLoader更容易了

新的版本下采用了delegate模型。ClassLoader可以将类的请求委托给父类。默认的实现会先调用父类的实现,在自己加载。但是这种模式是可以改变的。所有的ClassLoader的根节点是系统ClassLoader。它默认会从文件系统中加载类。

loadClass默认实现

一个自定义的loadClass方法通常会尝试用各种方法来获得一个类的信息。如果你写了大量的ClassLoader,你会发现基本上是在重复写复杂而变化不大的代码。

java1.2的loadClass的默认实现中允许你直接重写findClass方法,loadClass将会在合适的时候调用该方法。

这种方式的好处在于你无须重写loadClass方法。

新方法:findClass

该方法会被loadClass的默认实现调用。findClass是为了包含ClassLoader所有特定的代码,而无需写大量重负的其他代码

新方法:getSystenClassLoader

无论你是否重写了findClass或是loadClass方法,getSystemClassLoader允许你直接获得系统的ClassLoader(而不是隐式的用findSystemClass获得)

新方法:getParent

该方法允许类加载器获取其父类加载器,从而将请求委托给它。当你自定义的加载器无法找到类时,可以使用该方法。父类加载器是指包含创建该类加载代码的加载器。

源码
// $Id$
 
import java.io.*;
 
/*
 
A CompilingClassLoader compiles your Java source on-the-fly.  It
checks for nonexistent .class files, or .class files that are older
than their corresponding source code.

*/
 
public class CompilingClassLoader extends ClassLoader
{
  // Given a filename, read the entirety of that file from disk
  // and return it as a byte array.
  private byte[] getBytes( String filename ) throws IOException {
    // Find out the length of the file
    File file = new File( filename );
    long len = file.length();
 
    // Create an array that"s just the right size for the file"s
    // contents
    byte raw[] = new byte[(int)len];
 
    // Open the file
    FileInputStream fin = new FileInputStream( file );
 
    // Read all of it into the array; if we don"t get all,
    // then it"s an error.
    int r = fin.read( raw );
    if (r != len)
      throw new IOException( "Can"t read all, "+r+" != "+len );
 
    // Don"t forget to close the file!
    fin.close();
 
    // And finally return the file contents as an array
    return raw;
  }
 
  // Spawn a process to compile the java source code file
  // specified in the "javaFile" parameter.  Return a true if
  // the compilation worked, false otherwise.
  private boolean compile( String javaFile ) throws IOException {
    // Let the user know what"s going on
    System.out.println( "CCL: Compiling "+javaFile+"..." );
 
    // Start up the compiler
    Process p = Runtime.getRuntime().exec( "javac "+javaFile );
 
    // Wait for it to finish running
    try {
      p.waitFor();
    } catch( InterruptedException ie ) { System.out.println( ie ); }
 
    // Check the return code, in case of a compilation error
    int ret = p.exitValue();
 
    // Tell whether the compilation worked
    return ret==0;
  }
 
  // The heart of the ClassLoader -- automatically compile
  // source as necessary when looking for class files
  public Class loadClass( String name, boolean resolve )
      throws ClassNotFoundException {
 
    // Our goal is to get a Class object
    Class clas = null;
 
    // First, see if we"ve already dealt with this one
    clas = findLoadedClass( name );
 
    //System.out.println( "findLoadedClass: "+clas );
 
    // Create a pathname from the class name
    // E.g. java.lang.Object => java/lang/Object
    String fileStub = name.replace( ".", "/" );
 
    // Build objects pointing to the source code (.java) and object
    // code (.class)
    String javaFilename = fileStub+".java";
    String classFilename = fileStub+".class";
 
    File javaFile = new File( javaFilename );
    File classFile = new File( classFilename );
 
    //System.out.println( "j "+javaFile.lastModified()+" c "+
    //  classFile.lastModified() );
 
    // First, see if we want to try compiling.  We do if (a) there
    // is source code, and either (b0) there is no object code,
    // or (b1) there is object code, but it"s older than the source
    if (javaFile.exists() &&
         (!classFile.exists() ||
          javaFile.lastModified() > classFile.lastModified())) {
 
      try {
        // Try to compile it.  If this doesn"t work, then
        // we must declare failure.  (It"s not good enough to use
        // and already-existing, but out-of-date, classfile)
        if (!compile( javaFilename ) || !classFile.exists()) {
          throw new ClassNotFoundException( "Compile failed: "+javaFilename );
        }
      } catch( IOException ie ) {
 
        // Another place where we might come to if we fail
        // to compile
        throw new ClassNotFoundException( ie.toString() );
      }
    }
 
    // Let"s try to load up the raw bytes, assuming they were
    // properly compiled, or didn"t need to be compiled
    try {
 
      // read the bytes
      byte raw[] = getBytes( classFilename );
 
      // try to turn them into a class
      clas = defineClass( name, raw, 0, raw.length );
    } catch( IOException ie ) {
      // This is not a failure!  If we reach here, it might
      // mean that we are dealing with a class in a library,
      // such as java.lang.Object
    }
 
    //System.out.println( "defineClass: "+clas );
 
    // Maybe the class is in a library -- try loading
    // the normal way
    if (clas==null) {
      clas = findSystemClass( name );
    }
 
    //System.out.println( "findSystemClass: "+clas );
 
    // Resolve the class, if any, but only if the "resolve"
    // flag is set to true
    if (resolve && clas != null)
      resolveClass( clas );
 
    // If we still don"t have a class, it"s an error
    if (clas == null)
      throw new ClassNotFoundException( name );
 
    // Otherwise, return the class
    return clas;
  }
}
import java.lang.reflect.*;
 
/*
 
CCLRun executes a Java program by loading it through a
CompilingClassLoader.
 
*/
 
public class CCLRun
{
  static public void main( String args[] ) throws Exception {
 
    // The first argument is the Java program (class) the user
    // wants to run
    String progClass = args[0];
 
    // And the arguments to that program are just
    // arguments 1..n, so separate those out into
    // their own array
    String progArgs[] = new String[args.length-1];
    System.arraycopy( args, 1, progArgs, 0, progArgs.length );
 
    // Create a CompilingClassLoader
    CompilingClassLoader ccl = new CompilingClassLoader();
 
    // Load the main class through our CCL
    Class clas = ccl.loadClass( progClass );
 
    // Use reflection to call its main() method, and to
    // pass the arguments in.
 
    // Get a class representing the type of the main method"s argument
    Class mainArgType[] = { (new String[0]).getClass() };
 
    // Find the standard main method in the class
    Method main = clas.getMethod( "main", mainArgType );
 
    // Create a list containing the arguments -- in this case,
    // an array of strings
    Object argsArray[] = { progArgs };
 
    // Call the method
    main.invoke( null, argsArray );
  }
}
public class Foo
{
  static public void main( String args[] ) throws Exception {
    System.out.println( "foo! "+args[0]+" "+args[1] );
    new Bar( args[0], args[1] );
  }
}
import baz.*;
 
public class Bar
{
  public Bar( String a, String b ) {
    System.out.println( "bar! "+a+" "+b );
    new Baz( a, b );
 
    try {
      Class booClass = Class.forName( "Boo" );
      Object boo = booClass.newInstance();
    } catch( Exception e ) {
      e.printStackTrace();
    }
  }
}
 
package baz;
 
public class Baz
{
  public Baz( String a, String b ) {
    System.out.println( "baz! "+a+" "+b );
  }
}
public class Boo
{
  public Boo() {
    System.out.println( "Boo!" );
  }
}


想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76502.html

相关文章

  • 头鹰深夜翻译:JDK Vs. JRE Vs. JVM之间区别

    摘要:什么是为执行字节码提供一个运行环境。它的实现主要包含三个部分,描述实现规格的文档,具体实现和满足要求的计算机程序以及实例具体执行字节码。该类先被转化为一组字节码并放入文件中。字节码校验器通过字节码校验器检查格式并找出非法代码。 什么是Java Development Kit (JDK)? JDK通常用来开发Java应用和插件。基本上可以认为是一个软件开发环境。JDK包含Java Run...

    blair 评论0 收藏0
  • 头鹰深夜翻译:使用JAVA CompletableFuture20例子

    摘要:这个例子想要说明两个事情中以为结尾的方法将会异步执行默认情况下即指没有传入的情况下,异步执行会使用实现,该线程池使用一个后台线程来执行任务。这个例子展示了如何使用一个固定大小的线程池来实现大写操作。 前言 这篇博客回顾JAVA8的CompletionStageAPI以及其在JAVA库中的标准实现CompletableFuture。将会通过几个例子来展示API的各种行为。 因为Compl...

    AZmake 评论0 收藏0
  • 头鹰深夜翻译Java 2D Graphics, 简单仿射变换

    摘要:什么是仿射变换一组设备无关的坐标被用来将所有的坐标信息传递给对象。对象作为对象状态的一部分。类代表一个的仿射变化,将一组的坐标进行线性映射到另一组保留了平行关系和竖直关系的坐标中。 什么是仿射变换 一组设备无关的坐标被用来将所有的坐标信息传递给Graphics2D对象。AffineTransform对象作为Graphics2D对象状态的一部分。该对象定义了如何将用户空间的坐标转化为设备...

    whinc 评论0 收藏0
  • 头鹰深夜翻译Java WeakHashMap

    摘要:本文简介类概览类构造器总结类构造方法类使用举例类概览是一个实现了接口,并且键为型的哈希表。中的条目不再被正常使用时,会被自动删除。它的键值均支持。和绝大多数的集合类一样,这个类不是同步的。 本文简介 WeakHashMap类概览 WeakHashMap类构造器总结 WeakHashMap类构造方法 WeakHasjMap类使用举例 1. WeakHashMap类概览 Wea...

    BothEyes1993 评论0 收藏0
  • 头鹰深夜翻译:Volatile原子性, 可见性和有序性

    摘要:有可能一个线程中的动作相对于另一个线程出现乱序。当实际输出取决于线程交错的结果时,这种情况被称为竞争条件。这里的问题在于代码块不是原子性的,而且实例的变化对别的线程不可见。这种不能同时在多个线程上执行的部分被称为关键部分。 为什么要额外写一篇文章来研究volatile呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile的博客,但是大多数要么不完整,要么难...

    Lionad-Morotar 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<