Java类加载器进阶

本文深入探讨Java类加载机制中的父亲委托原则,解析类加载过程及其安全性保障机制,并通过实例演示如何实现自定义类加载器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

再谈类加载的父亲委托(Parent Delegation)机制
在父亲委托机制中,各个加载器按照父子关系形成了树状结构,除了根类加载器以外,其余的类加载器有且只有一个父加载器。
假设loader2的父亲为loader1,loader1的父亲为系统类加载器。假设Java程序要求loader2加载Sample类,代码如下:

Class sampleClass = loader2.loadClass("Sample");

loader2首先从自己的命名空间中查找Sample类是否已经加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。

如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将Sample类所对应的Class对象的引用返回给loader1,loader1再将引用返回给loader2,从而成功将Sample类加载进虚拟机。
若系统类加载器不能加载Smaple类,则loader1尝试加载Sample类,若loader1也不能成功加载,则loader2尝试加载。若所有的父加载器和loader2本身都不能加载,则抛出ClassNotFoundException异常。

父亲委托机制的优点?
父亲委托机制的优点是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。
例如,java.lang.Object类总是由根类加载器加载(BootStrap),其他任何用户自定义的类加载器都不可能加载含有恶意代码的。
java.lang.Object类。

有几个重要概念需要理解一下。(与安全有关)
1.命名空间
每个类加载器都有自己的命名空间,命名空间由加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,
有可能会出现类的完整名字(包括类的包名)相同的两个类。

2.运行时包
由同一个类加载器加载的属于相同包的类组成运行时包。决定两个类是不是属于同一个运行时包,不仅
要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问可见(即默认访问级别)
的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。假设用户自己定义了一个类
java.lang.Spy,并由用户自定义的类加载器加载,由于jang.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行
时包,所以java.lang.Spy不能访问核心类库java.lang包中的可见成员。


创建用户自定义的类加载器
要创建用户自己的类加载器,只需扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)即可,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
package com.jfans;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
 
 //给类加载器指定一个名字,在本例中只是为了便于区分不同的加载器对象
 private String name;
 private String path = "d://";
 private final String fileType = ".class";
 
 public MyClassLoader(String name) {
  super();
  this.name = name;
 }

 public MyClassLoader(ClassLoader parent,String name) {
  super(parent);
  this.name = name;
 }

 public String getPath() {
  return path;
 }

 public void setPath(String path) {
  this.path = path;
 }
 
 
 @Override
 public String toString(){
  return this.name;
 }
 
  //自定义私有方法
 private byte[] loadClassData(String name) throws ClassNotFoundException{
     FileInputStream fis = null;
     byte[] data = null;
     ByteArrayOutputStream baos = null;
    
     try{
      //把name字符串中的"."替换为"/",从而把类中的包名转变为路径名
      //例如,如果name为com.jfans.Sample,那么将转变为"com/jfans/Sample"
            name = name.replaceAll("//.","////");
            fis = new FileInputStream(new File(path + name + fileType));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while( (ch=fis.read()) !=  -1){
             baos.write(ch);
            }
            data = baos.toByteArray();
     }catch(IOException e){
      //System.out.println("exception e: " + e);
      //异常转换
      throw new ClassNotFoundException("class is not found:"+name,e);
     }finally{
      try{
       if(fis != null){
       fis.close();}
       if(baos != null){
       baos.close();}
      }catch(IOException ioe){
       ioe.printStackTrace();
      }
     }
  return data;
 }
 
 
 //自定义加载器要重写的方法
 protected Class findClass(String name) throws ClassNotFoundException{
  byte[] data = loadClassData(name);
  return defineClass(name,data, 0, data.length);
 }

}


测试类:
package com.jfans;

public class MyClassLoaderTest {

 public static void main(String[] args) {

  MyClassLoader loader1 = new MyClassLoader("loader1");
  loader1.setPath("D:/myapp/serverlib/");
  
  MyClassLoader loader2 = new MyClassLoader(loader1,"loader2");
  loader2.setPath("D:/myapp/clientlib/");
  
  MyClassLoader loader3 = new MyClassLoader(null,"loader3");
  loader3.setPath("D:/myapp/otherlib/");
  
  
  test(loader1);
  test(loader2);
  test(loader3);
  
 }
 private static void test(ClassLoader classLoader){
   try {
   Class personClass = classLoader.loadClass("com.jfans.Person");
   Object obj = personClass.newInstance();
  } catch (ClassNotFoundException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (InstantiationException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalAccessException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
 }

}

//Person类
package com.jfans;

public class Person {

 private String name;

 public Person(){
  this.name = "cuser";//default value
  System.err.println("Person is load by: " + this.getClass().getClassLoader());//由此观察打印结果
 }
 
 public Personx(String name){
  this.name = name;
 }
 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }
}

 


测试方法:可以将Person类分别放到上面三个加载器(loader1,loader2,loader3所指定的目录(path)下面。通过不同的组合,删除等,进行测试。相信能观察到Person到底由哪个加载器加载的,同时什么情况下会出现ClassNotFoundException。
测试前,阅读代码,记住父亲委托机制就可以了。


不同类加载器的命名空间存在以下关系:
●同一个命名空间内的类是相互可见的
●子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能够看见父加载器加载的类。例如系统类加载器加载的类能够看见根类加载器加载的类。

●由父加载器加载的类不能看见子加载器加载的类。
●如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
所谓类A能看见类B,就是批类A的程序代码中可以引用类B的名字。例如:
class A{
     B b = new B();
}


URLClassLoader类
在JDK的java.net包中,提供了一个功能比较强大的URLClassLoader类,它扩展了ClassLoader类。它不仅能从本地文件系统中加载类,还可以从网上下载类。Java程序可直接用URLClassLoader类作为用户自定义的类加载器。URLClassLoader类提供了以下形式的构造方法:
URL(URL[] urls) //父加载器为系统类加载器
URL(URL[] urls,ClassLoader parent) //parent参数指定父加载器
以上构造方法中的参数urls用来存放所有的URL路径,URLClassLoader将从这些路径中加载类。

类的卸载
当Sample类被加载、连接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。这是一个较大的话题。

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期内,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器,扩展类加载器,系统类加载器,Java虚拟机本身会始终引用这些类加载空对空,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象是始终可触及的。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值