java7:核心技术与最佳实践读书笔记——类加载

流程:class -> 加载 ->  jvm虚拟机 -> 链接 。

一、类加载器概述
1、引出
     类加载器也是一个java类,java.lang.ClassLoader类是所有由java代码创建的类加载器的父类。通过调用类加载器的loadClass方法可以加载Java类。由于Java类都需要由类加载器来加载,那ClassLoader类由谁加载?     
     其实Java平台提供了一个启动类加载器(BootStrapClassLoader),它是由原生代码来实现的(C语言)。它负责加载Java自身的核心类到虚拟机中,ClassLoader也在此时被加载进来,接下来继承自ClassLoader类的类加载器都可以正常工作了。
2、Class类与ClassLoader类
     Class类的getClassLoader方法可以获取到加载它的类加载器对象,ClassLoader类的loadClass可以加载对应的Java类,传入参数为Java类的二进制名称,返回值为Java类的Class类的对象。
public void loadClass() throws Exception{
     ClassLoader current = getClass().getClassLoader();
     Class<?> clazz = current.loadClass("java.lang.String");
     Object str = clazz.newInstance();
     System.out.println(str.getClass());// java.lang.String
}
3、类加载器分类
  • 启动类加载器:原生代码实现
  • 用户自定义的类加载器:继承自ClassLoader类
    • Java平台默认提供
      • 扩展类加载器:从特定的路径加载Java平台的扩展库
      • 系统类加载器(应用类加载器):根据应用程序的类路径(ClassPath)来加载Java类(默认加载器
    • 程序中创建
4、定义类加载器和初始类加载器
(1)类加载器的根本作用:从字节代码中定义出表示Java类的Class类的对象,由ClassLoader的defineClass方法来表示。
(2)如果一个Java类是由某个类加载器对象的defineClass()方法定义的,则称这个类加载器是该Java类的定义类加载器。当使用类加载器对象的loadClass()方法来加载一个Java类时,称这个类加载器对象为该Java类的初始类加载器
(3)两者关系:一个Java类的定义类加载器是该类所引用的其他Java类的初始类加载器。因为加载类是只有一次的,通过调用类加载器对象的loadClass方法进行类加载,并得到了一个Class类的对象之后,虚拟机会把类加载器对象和它加载的Class类的对象之间的关联记录下来。
public class School{
     private Teacher t;
}
//某个类加载器对象加载School类时,Teacher类也将被加载,
//则这个类加载器是是School的定义类加载器和初始类加载器,
//但它只是Teacher的初始类加载器。
二、类加载器的层次结构与代理模式
1、双亲类加载器对象
(1)重要特征:类加载器的一个重要特征是所有类加载器对象都可以有一个作为其双亲的类加载器对象。
(2)获取双亲类加载器:通过ClassLoader类的getParent方法可以获取双亲类加载器对象,但如果双亲类加载器是启动类加载器则返回null
(3)指定双亲类加载器:ClassLoader类提供的构造器方法允许在创建指定类加载器对象的双亲加载器对象。不过ClassLoader类本身抽象,无法直接创建ClassLoader类。则自定义类加载器的构造方法中可以调用父类ClassLoader类的构造方法来自定双亲加载器对象的值;如果不设置,则默认选择系统类加载器。
public void displayParents(){
     ClassLoader current = getClass().getClassLoader();
     while(current != null){
          System.out.println(current.toString());
          current = current.getParent();    
     }
}
//result:
//sun.misc.Launcher$AppClassLoader@177b3cd(系统类加载器)
//sun.misc.Launcher$ExtClassLoader@1bd7848(拓展类加载器)
//null,代表启动类加载
三、创建类加载器
1、使用自定义类加载器?
(1)对Java类的字节代码进行特殊的查找和处理,如:Java字节代码放在了磁盘上特定位置或者远程服务器上,或者字节代码的数据经过了加密的处理。
(2)利用类加载器产生的隔离特性来满足特殊的需求
2、创建自定义加载器(继承ClassLoader并覆写一些方法)
(1)defineClass:从字节代码中定义出表示Java类的Class类的对象,原生代码实现,final修饰,无法覆写。
(2)loadClass
  • 默认实现流程:
    • findLoadedClass方法查找Java类是否被加载过,如果被加载则直接返回已加载的Class对象。
    • getParent方法获取双亲类加载器对象,调用双亲类加载器的loadClass方法,若getParent为null,则使用启动类加载来进行加载(代理模式生效的地方,代理给双亲类加载器来处理)。
    • 由当前类加载器通过findClass方法来进行查找。
    • 若上面都找不到就抛出java.lang.ClassNotFoundException异常。
  • 如果loadClass方法第二个参数设置为true,即需要对找到的类进行链接操作,则loadClass方法会调用resolveClass方法进行链接。
(3)findLoadedClass:类加载过程中,虚拟机会记录下已经加载的Java类的初始类加载器。如果已经加载的Java类的初始类加载器是当前类的加载器对象,同时类的名称也要与加载的类的名称相同,则Java类对应的Class类的对象作为结果返回。
(4)findClass:默认情况下,代理模式无法使用双亲类加载器对象成功加载Java类时,findClass方法被调用。只有在需要改变默认的双亲优先代理模式的类加载器中才需要覆写该方法,因为ClassLoader类中的findClass方法只是简单抛出了ClassNotFoundException异常
(5)resolveClass:作用是链接一个定义好的Class类的对象,下次分享给大家。
3、例子:从磁盘文件中加载class文件
package classloader;
public class Sample {
     private String name = "andy";
     public String getName() {
          return name;
     }
     public void setName(String name) {
          this.name = name;
     }
}
 
package classloader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileSystemClassLoader extends ClassLoader {
     private Path path;
 
     public FileSystemClassLoader(Path path) {
          this.path = path;
     }
 
     protected Class<?> findClass(String name) throws ClassNotFoundException {
          try {
              byte[] classData = getClassData(name);
              return defineClass(name, classData, 0, classData.length);
          } catch (IOException e) {
              throw new ClassNotFoundException();
          }
     }
 
     private byte[] getClassData(String className) throws IOException {
          Path classFilePath = classNameToPath(className);
          return Files.readAllBytes(classFilePath);
     }
 
     private Path classNameToPath(String className) {
          return path.resolve(className.replace('.', File.separatorChar)
                   + ".class");
     }
 
     public static void main(String[] args) throws Exception {
          Path path = Paths.get("E:/project/eclipse_J2EE/java-test/bin/");
          FileSystemClassLoader classLoader = new FileSystemClassLoader(path);
          Class<?> clazz = classLoader.findClass("classloader.Sample");
          Object object = clazz.newInstance();
          Method method = clazz.getMethod("getName");
          String name  =(String)method.invoke(object);
          System.out.println(name);
     }
}
 
//result:
//andy
四、类加载器的隔离作用
1、虚拟机判断两个对象的类是否相等
(1)Class类的对象表示的Java类的全名是否相等
(2)Class类的对象的定义类加载器对象是否相同,即不同的类加载器对象来加载并定义,所得到的Class类的对象是不相等的。
2、隔离的名称空间
(1)好处:让同名的Java类可以在虚拟机中共存。
(2)用途:版本更新
(3)例子:
public interface Versionized{
     String getVersion();
}
 
public Service implements Versionized{
}
//版本1 编译产生的serviceV1.class保留
//版本2 编译产生新的serviceV2.calss
//放在同一个目录下,使用FileSystemClassLoader来读取
 
public class ServiceFactory{
     public static versionized getService(String className ,String version) throws Exception{
          Path path = Paths.get("service",version);
          FileSystemClassLoader loader = new FileSystemClssLoader(path);
          Class<?> clazz = loader.loadClass(className);
          return (Versionized)clazz.newInstance();
     }
}
//使用
public class ServiceConsumer{
     public void consume()throws Exception{
          String serviceName = "xxx.xxx.SampleService";
          Versionized v1 = ServiceFactory.getService(serviceName,"v1");
          Versionized v2 = ServiceFactory.getService(serviceName,"v2");
     }
}
五、线程上下文类加载器(Thread.getContextClassLoader和setContextClassLoader)
(1)程序启动的第一个线程的上下文类加载器默认是Java平台的系统类加载器。因此,在默认情况下,通过当前线程的getContextClassLoader方法获取的类加载器对象和使用当前类的getClassLoader方法获取到的类加载器对象是相同的,都是系统类加载器。
(2)线程上下文类加载器提供了一种直接的方式在程序的各部分间共享ClassLoader类的对象。一般获取需要使用的类加载由三种方式:
  • 创建新的ClassLoader类的对象
  • 加载当前Java类的ClassLoader类的对象
  • 使用Java平台提供的系统类加载器或者扩展类加载器
但如果这个场景:Java类A和B,需要由同一个类加载来加载,否则会出现问题,那么上面的3中都无法轻易实现,因为我们需要用加载类A的ClassLoader类的对象去加载B,所以我们使加载类A和B的代码在同一个线程中运行,使用线程上下文类加载器来完成。
(3)线程上下文类加载器的重要作用是解决Java平台的服务提供者接口(SPI)带来的类加载问题。
    <1> ScriptEngineManager类用来管理当前程序中的可用脚本执行引擎,脚本语言开发者可以实现javax.script.ScriptEngine接口,使开发人员可以通过脚本语言支持API来使用这种脚本语言。当开发人员下载了某脚本语言对应的ScriptEngine接口的实现,并将其添加到程序的类路径(ClassPath)之后,新的脚本执行引擎对ScriptEngineManager类来说必须是可见的,则开发人员才能通过工厂方法得到对应的ScriptEngine接口的实现对象。ScriptEngineManager类的对象为了能够提供SctiptEngine接口的具体实现对象,需要加载对应的Java类并创建新的对象。
    <2>但是使用代理模式无法完成SPI实现类的加载,SPI接口相关类是Java标准库的一部分,由启动类加载器来加载,即ScriptEngineManager类是启动类加载器来加载。在ScriptEngineManager类的实现中需要查找程序的ClassPATH来找到SPI实现类,并创建出相应的对象。程序的ClassPath中的类一般有系统类加载器负责,启动类加载器无法完成,且启动类加载器也无法代理给它来完成,因为启动类加载器是系统类加载器的祖先。虽然可以在启动类加载内部保存一个系统类加载器的引用,在加载SPI实现类时dialing给系统类加载器来完成,但是SPI实现可能不出现在ClassPath中,而是需要自定义的类加载器对象来完成加载。
    <3>所以我们使用线程上下文加载器来解决。默认上下文类加载器为系统类加载器,但如需要使用自定义类加载器来加载SPI实现类,可把当前线程上下文类加载器的值设置为自定义类加载器
六、Class.forName方法
(1)作用:根据Java类的名称得到对应的Class类的对象。
(2)重载:
  • public static Class<?> forName(String name,boolean initialize, ClassLoader loader)//Java类名、初始化Java类、加载Java类的类加载器对象
  • public static Class<?> forName(String name) // true、this.getClass().getClassLoader()
(3)区别
classLoader.loadClass(className)
Class.forName(className) //会进行Java类初始化
七、加载资源
(1)资源目录:与class处于同一级目录下
(2)例子:
//config.properties
mode = debug
 
//LoadResoource.java
public Properties loadConfig() throws IOException {
     ClassLoader loader = this.getClass().getClassLoader();
     InputStream input = loader
               .getResourceAsStream("classloader/config.properties");
     if (input == null) {
          throw new IOException("找不到配置文件。");
     }
     Properties props = new Properties();
     props.load(input);
     return props;
}
 
public static void main(String[] args) throws IOException {
     LoadResource lr = new LoadResource();
     Properties props = lr.loadConfig();
     System.out.println(props.getProperty("mode"));//debug
}

  

 

 

posted @ 2017-03-31 09:41  月下小魔王  阅读(277)  评论(0编辑  收藏  举报