---------- android培训java培训、期待与您交流!----------



一、了解类加载器

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

系统默认提供了三个类加载器:

  • BootStrap ClassLoader:根类加载器。----->BootStreap

  • Extension ClassLoader:扩展类加载器。----->ExtClassLoader

  • System ClassLoader:系统类加载器。------>AppClassLoader

   类加载器也是Java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是不是java类,这正是BootStrap。根类加载器不是java.lang.ClassLoader的子类,而是由java虚拟机自身实现的。


   下面举两个小例子来初识类加载器:

package itheimareview;
public class ClassLoaderTest {
    public static void main(String[] args)
    {
        System.out.println(ClassLoaderTest.class.
            getClassLoader().getClass().getName());//AppClassLoader
        //虽然返回null,但并不代表没有类加载器,而是它的类加载器为根类加载器,BootStrap
        System.out.println(System.class.getClassLoader());//null
                                                                                                                                                         
        ClassLoader loader = ClassLoaderTest.class.getClassLoader();
        while (loader != null)
        {
            System.out.println(loader.getClass().getName());
            loader = loader.getParent();
        }
        System.out.println(loader);
        /*sun.misc.Launcher$AppClassLoader
        sun.misc.Launcher$ExtClassLoader
        null*/
    }
}


二、类加载器之间的父子关系和管辖范围图

   Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。

151524388.png


三、类加载机制

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


1、全盘负责。

   就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

要注意的是:

  • 首先当前线程的类加载器去加载线程中的第一个类。

  • 如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。

  • 还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。


2、父类委托。

   就是先让parent类加载器试图加载该Class,只有在父类加载器无法加载 该类时才尝试从自己的类路径中加载该类。


3、缓存机制。

   缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器首先从缓存区中检索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,也就是字节码文件,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。


四、类加载器加载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、返回对应的java.lang.Class对象。


   其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写

loadClass()方法来实现自己的载入过程。


五、自定义类加载器

1、ClassLoader中包含了大量的protected方法,这些方法都可以被子类重写。


2、ClassLoader类有如下两个关键方法:

  • loadClass(String name, boolean resovle):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。(如果该参数为 true,则分析这个类)

  • findClass(String name):根据二进制名称来查找类。


3、如果需要实现自定义的ClassLoader,则可以通过上面两种方法来实现。但建议在自定义

加载器时,重写findClass()方法,而不是重写loadClass()方法。因为loadClass()方法有父类委托和缓冲两种机制,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略如果重写loadClass()方法,则实现逻辑更为复杂。


4、loadClass()方法的执行步骤如下:

①用findLoadedClass(String)来检查是否已经加载类,如果已经加载则直接返回。

②在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。

③调用findClass(String)方法查找类。


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


六、自定义类加载器的应用:

   自定义类加载器应用还是比较多的,我们通过使用自定义的类加载器,可以实现如下

常见功能:

  • 执行代码前自动验证数字签名

  • 根据用户提供的密码解密代码,从而可以避免通过代码混淆器来实现反编译Class文件

  • 根据用户需求来动态地加载类

  • 根据应用需求把其他数据以字节码的形式加载到应用中。


七、例子

1、编写一个对文件内容进行简单加密的程序。

package itheimareview;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader
{
    public static void main(String[] args) throws  Exception
    {
        //通过主函数传参数的形式
        String srcPath = args[0];//源文件路径名
        String destDir = args[1];//目的文件夹名,将编译后的.class文件存放的位置
        FileInputStream fis = new FileInputStream(srcPath);//对文件进行流操作
        String destFileName = srcPath.substring(srcPath.lastIndexOf("\\")+1);//这个就是源文件路径中的文件名称
        String destPath = destDir + "\\" + destFileName;//指定存入文件夹中的.class文件的文件名称
        FileOutputStream fos = new FileOutputStream(destPath);//输出到指定目的文件
        cypher(fis, fos);//加密过程
        fis.close();
        fos.close();
                                     
    }
    private static void cypher(InputStream ips, OutputStream ops)throws Exception
    {
        int b = -1;
        while((b = ips.read()) != -1)
        {
            //进行异或运算,并数据写入到输出流中
            ops.write(b ^ 0xff);
        }
    }
}

再定义一个需要被加密的测试用的类:

package itheimareview;
import java.util.Date;
public class ClassLoaderAttachment extends Date
{
        public String toString()
        {
            return "hello,itcast";
        }
}

   以此可以把需要被加密的文件(ClassLoaderAttachment.java)和把.class文件(ClassLoaderAttachment.class)需要存放的目录(itcastlib)以主函数的形式传入即可完成加密作。

   为了可以看到效果,可以用加密后的ClassLoaderAttachment.class文件覆盖加密之前的

ClassLoaderAttachment.class。在进行这些操作的时候,要注意绝对路径和相对路径的应用,否则实验不一定能成功。


2、再编写了一个自己的类装载器,可实现对加密过的类进行装载和解密。

   为了便于理解,对加密的类进行装载解密的动作在原MyClassLoader类中进行修改:

package itheimareview;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader
{
    public static void main(String[] args) throws  Exception
    {
        String srcPath = args[0];
        String destDir = args[1];
        FileInputStream fis = new FileInputStream(srcPath);
        String destFileName = srcPath.substring(srcPath.lastIndexOf("\\")+1);
        String destPath = destDir + "\\" + destFileName;
        FileOutputStream fos = new FileOutputStream(destPath);
        cypher(fis, fos);
        fis.close();
        fos.close();
               
    }
    private static void cypher(InputStream ips, OutputStream ops)throws Exception
    {
        int b = -1;
        while((b = ips.read()) != -1)
        {
            ops.write(b ^ 0xff);
        }
    }
    private String classDir;
    @Override
    //覆盖findClass()方法
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        String classFileName = classDir + "\\" + name + ".class";
        try
        {
            FileInputStream fis = new FileInputStream(classFileName);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            //再进行异或运算,就可以解密,当然实际当中不可能这么简单,这里只作测试。
            cypher(fis, bos);
            fis.close();
            byte[] bytes = bos.toByteArray();
            //return defineClass(bytes, 0, bytes.length);//这个已过时。
            return defineClass(null, bytes, 0, bytes.length);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
           
    public MyClassLoader()
    {
               
    }
    public MyClassLoader(String classDir)
    {
        this.classDir = classDir;
    }
}

再定义一个测试类:

package itheimareview;
import java.util.Date;
public class ClassLoaderTest
{
    public static void main(String[] args) throws Exception
    {
        Class clazz = new MyClassLoader("itcastlib").loadClass("ClassLoaderAttachment");
        /*这就是为什么为继承Date类,如果不继承,原ClassLoaderAttachment类将不能被友好加载
        程序运行时会报错*/
        Date d1 = (Date)clazz.newInstance();
        System.out.println(d1);
    }
}


---------- android培训java培训、期待与您交流!----------