深入解析Java中的Class Loader类加载器

类加载的过程
类加载器的主要工作就是把类文件加载到JVM中。如下图所示,其过程分为三步:

1.加载:定位要加载的类文件,并将其字节流装载到JVM中;
2.链接:给要加载的类分配最基本的内存结构保存其信息,比如属性,方法以及引用的类。在该阶段,该类还处于不可用状态;
(1)验证:对加载的字节流进行验证,比如格式上的,安全方面的;
(2)内存分配:为该类准备内存空间来表示其属性,方法以及引用的类;
(3)解析:加载该类所引用的其它类,比如父类,实现的接口等。
3.初始化:对类变量进行赋值。


类加载器的层级
下图虚线以上是JDK提供的几个重要的类加载器,详细说明如下:

(1)Bootstrap Class Loader: 当启动包含主函数的类时,加载JAVA_HOME/lib目录下或-Xbootclasspath指定目录的jar包;
(2)Extention Class Loader:加载JAVA_HOME/lib/ext目录下的或-Djava.ext.dirs指定目录下的jar包。
(3)System Class Loader:加载classpath或者-Djava.class.path指定目录下的类或jar包。

需要了解的是:

1.除了Bootstrap Class Loader外,其它的类加载器都是java.lang.ClassLoader类的子类;
2.Bootstrap Class Loader不是用Java实现,如果你没有使用个性化类加载器,那么java.lang.String.class.getClassLoader()就为null,Extension Class Loader的父加载器也为null;
3.获得类加载器的几种方式:
(1)获得Bootstrap Class Loader:试图获得Bootstrap Class Loader,得到的必然是null。可以用如下方式验证下:使用rt.jar包内的类对象的getClassLoader方法,比如java.lang.String.class.getClassLoader()可以得到或者获得Extention Class Loader,再调用getParent方法获得;
(2)获得Extention Class Loader:使用JAVA_HOME/lib/ext目录下jar包内的类对象的getClassLoader方法或者先获得System Class Loader,再通过它的getParent方法获得;
(3)获得System Class Loader:调用包含主函数的类对象的getClassLoader方法或者在主函数内调用Thread.currentThread().getContextClassLoader()或者调用ClassLoader.getSystemClassLoader();
(4)获得User-Defined Class Loader:调用类对象的getClassLoader方法或者调用Thread.currentThread().getContextClassLoader();

类加载器的操作原则
1.代理原则
2.可见性原则
3.唯一性原则
4.代理原则
代理原则指的是一个类加载器在加载一个类时会请求它的父加载器代理加载,父加载器也会请求它的父加载器代理加载,如下图所示。

为什么要使用代理模式呢?首先这样可以减少重复的加载一个类。(还有其它原因吗?)

容易误解的地方:

一般会以为类加载器的代理顺序是Parent First的,也就是:

1.加载一个类时,类加载器首先检查自己是否已经加载了该类,如果已加载,则返回;否则请父加载器代理;
2.父加载器重复1的操作一直到Bootstrap Class Loader;
3.如果Bootstrap Class Loader也没有加载该类,将尝试进行加载,加载成功则返回;如果失败,抛出ClassNotFoundException,则由子加载器进行加载;
4.子类加载器捕捉异常后尝试加载,如果成功则返回,如果失败则抛出ClassNotFoundException,直到发起加载的子类加载器。
这种理解对Bootstrap Class Loader,Extention Class Loader,System Class Loader这些加载器是正确的,但一些个性化的加载器则不然,比如,IBM Web Sphere Portal Server实现的一些类加载器就是Parent Last的,是子加载器首先尝试加载,如果加载失败才会请父加载器,这样做的原因是:假如你期望某个版本log4j被所有应用使用,就把它放在WAS_HOME的库里,WAS启动时会加载它。如果某个应用想使用另外一个版本的log4j,如果使用Parent First,这是无法实现的,因为父加载器里已经加载了log4j内的类。但如果使用Parent Last,负责加载应用的类加载器会优先加载另外一个版本的log4j。

可见性原则
每个类对类加载器的可见性是不一样的,如下图所示。

扩展知识,OSGi就是利用这个特点,每一个bundle由一个单独的类加载器加载,因此每个类加载器都可以加载某个类的一个版本,因此整个系统就可以使用一个类的多个版本。

唯一性原则
每一个类在一个加载器里最多加载一次。

扩展知识1:准确地讲,Singleton模式所指的单例指的是在一组类加载器中某个类的对象只有一份。

扩展知识2:一个类可以被多个类加载器加载,每个类对象在各自的namespace内,对类对象进行比较或者对实例进行类型转换时,会同时比较各自的名字空间,比如:

Klass类被ClassLoaderA加载,假设类对象为KlassA;同时被ClassLoaderB加载,假设类对象为KlassB,那么KlassA不等于KlassB。同时ClassA的实例被cast成KlassB时会抛出ClassCastException异常。
为什么要个性化类加载器
个性化类加载器给Java语言增加了很多灵活性,主要的用途有:

1.可以从多个地方加载类,比如网络上,数据库中,甚至即时的编译源文件获得类文件;
2.个性化后类加载器可以在运行时原则性的加载某个版本的类文件;
3.个性化后类加载器可以动态卸载一些类;
4.个性化后类加载器可以对类进行解密解压缩后再载入类。

类的隐式和显式加载
隐式加载:当一个类被引用,被继承或者被实例化时会被隐式加载,如果加载失败,是抛出NoClassDefFoundError。

显式加载:使用如下方法,如果加载失败会抛出ClassNotFoundException。

cl.loadClass(),cl是类加载器的一个实例;
Class.forName(),使用当前类的类加载器进行加载。
类的静态块的执行
假如有如下类:

package cn.fengd; 

public class Dummy {
 static {
 System.out.println("Hi");
 }
}

另建一个测试类:

package cn.fengd; 

public class ClassLoaderTest { 

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

 try {
  /*
  * Different ways of loading.
  */
  Class c = ClassLoaderTest.class.getClassLoader().loadClass("cn.fengd.Dummy");
 } catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
 } 

}

运行后效果如何呢?

  • 不会输出Hi。由此可见使用loadClass后Class类对象并没有初始化;
  • 如果在Load语句后加上c.newInstance(); 就会有Hi输出,对该类进行实例化时才初始化类对象。
  • 如果换一种加载语句Class c = Class.forName("cn.fengd.Dummy", false, ClassLoader.getSystemClassLoader());
  • 不会输出Hi。因为参数false表示不需要初始化该类对象;
  • 如果在Load语句后加上c.newInstance(); 就会有Hi输出,对该类进行实例化时才初始化类对象。

如果换成Class.forName("cn.fengd.Dummy");或者new Dummy()呢?

都会输出Hi。

常见问题分析:

1.由不同的类加载器加载的指定类型还是相同的类型吗?
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.我们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

2.在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

//java.lang.Class.java

 publicstatic Class<?> forName(String className)throws ClassNotFoundException {

return forName0(className, true, ClassLoader.getCallerClassLoader());

}

//java.lang.ClassLoader.java

// Returns the invoker's class loader, or null if none.

static ClassLoader getCallerClassLoader() {

  // 获取调用类(caller)的类型

 Class caller = Reflection.getCallerClass(3);

  // This can be null if the VM is requesting it

 if (caller == null) {

  returnnull;

 }

 // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader

 return caller.getClassLoader0();

}

//java.lang.Class.java

//虚拟机本地实现,获取当前类的类加载器
native ClassLoader getClassLoader0();

3.在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是?
在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

//摘自java.lang.ClassLoader.java
protected ClassLoader() {

  SecurityManager security = System.getSecurityManager();

  if (security != null) {

  security.checkCreateClassLoader();

  }

  this.parent = getSystemClassLoader();

  initialized = true;

}

我们再来看一下对应的getSystemClassLoader()方法的实现:

privatestaticsynchronizedvoid initSystemClassLoader() {

  //...

  sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

  scl = l.getClassLoader();

  //...

}

我们可以写简单的测试代码来测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

本机对应输出如下:

sun.misc.Launcher$AppClassLoader@197d257

所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:

即时用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

(1)<Java_Runtime_Home>/lib下的类

(2)< Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类

(3)当前工程类路径下或者由系统变量java.class.path指定位置中的类

4.在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。
    说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

5.编写自定义类加载器时,一般有哪些注意点?
(1)一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑
一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {

 public Class<?> loadClass(String name) throws ClassNotFoundException {

  returnthis.findClass(name);

 }

 protected Class<?> findClass(String name) throws ClassNotFoundException {

  //假设此处只是到工程以外的特定目录D:/library下去加载类

  具体实现代码省略

 }

}

通过前面的分析我们已经知道,用户自定义类加载器(WrongClassLoader)的默

认的类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简

单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工

程类路径上的类都加载不上了。

问题5测试代码一

publicclass WrongClassLoaderTest {

 publicstaticvoid main(String[] args) {

  try {

  WrongClassLoader loader = new WrongClassLoader();

  Class classLoaded = loader.loadClass("beans.Account");

  System.out.println(classLoaded.getName());

  System.out.println(classLoaded.getClassLoader());

  } catch (Exception e) {

  e.printStackTrace();

  }

 }

}

(说明:D:"classes"beans"Account.class物理存在的)

输出结果:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)

 at java.io.FileInputStream.open(Native Method)

 at java.io.FileInputStream.<init>(FileInputStream.java:106)

 at WrongClassLoader.findClass(WrongClassLoader.java:40)

 at WrongClassLoader.loadClass(WrongClassLoader.java:29)

 at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)

 at java.lang.ClassLoader.defineClass1(Native Method)

 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

 at WrongClassLoader.findClass(WrongClassLoader.java:43)

 at WrongClassLoader.loadClass(WrongClassLoader.java:29)

 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object

 at java.lang.ClassLoader.defineClass1(Native Method)

 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

 at WrongClassLoader.findClass(WrongClassLoader.java:43)

 at WrongClassLoader.loadClass(WrongClassLoader.java:29)

 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass(…)引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
问题5测试二

//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {

 protected Class<?> findClass(String name) throws ClassNotFoundException {

  //假设此处只是到工程以外的特定目录D:/library下去加载类

  具体实现代码省略

 }

}

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

beans.Account

WrongClassLoader@1c78e57

这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。

这其中的原因分析,我想这里就不必解释了,大家应该可以分析的出来了。

(2)正确设置父类加载器
通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
(3)保证findClass(String )方法的逻辑正确性
事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6.如何在运行时判断系统类加载器能加载哪些路径下的类?
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;

二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")

7.如何在运行时判断标准扩展类加载器能加载哪些路径下的类?
方法之一:

try {
  URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();

  for (int i = 0; i < extURLs.length; i++) {

   System.out.println(extURLs[i]);

  }

 } catch (Exception e) {//…}

本机对应输出如下:

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar
时间: 2016-03-15

java 详解类加载器的双亲委派及打破双亲委派

java 详解类加载器的双亲委派及打破双亲委派 一般的场景中使用Java默认的类加载器即可,但有时为了达到某种目的又不得不实现自己的类加载器,例如为了达到类库的互相隔离,例如为了达到热部署重加载功能.这时就需要自己定义类加载器,每个类加载器加载各自的类库资源,以此达到资源隔离效果.在对资源的加载上可以沿用双亲委派机制,也可以打破双亲委派机制. 一.沿用双亲委派机制自定义类加载器很简单,只需继承ClassLoader类并重写findClass方法即可.如下例子: ①先定义一个待加载的类Test,它

Java中的类加载器_动力节点Java学院整理

从java的动态性到类加载机制 Java是一种动态语言.那么怎样理解这个"动态"呢?或者说一门语言具备了什么特性,才能称之为动态语言呢?对于java,我是这样理解的. JVM(java虚拟机)执行的不是本地机器码指令,而是执行一种称之为字节码的指令(存在于class文件中).这就要求虚拟机在真正执行字节码之前,先把相关的class文件加载到内存中.虚拟机不是一次性加载所有需要的class文件,因为它在执行的时候根本不会知道以后会用到哪些class文件.它是每用到一个类,就会在运行时&q

java 类加载与自定义类加载器详解

类加载 所有类加载器,都是ClassLoader的子类. 类加载器永远以.class运行的目录为准. 读取classpath根目录下的文件有以下几种方式: 1 在Java项目中可以通过以下方式获取classspath下的文件: public void abc(){ //每一种读取方法,使用某个类获取Appclassloader ClassLoader cl = ReadFile.class.getClassLoader(); URL url = cl.getResource("a.txt&quo

java类加载器和类反射使用示例

一.一个命令对应一个进程. 当我们启动一个Java程序,即启动一个main方法时,都将启动一个Java虚拟机进程,不管这个进程有多么复杂.而不同的JVM进程之间是不会相互影响的.这也就是为什么说,Java程序只有一个入口--main方法,让虚拟机调用.而两个mian方法,对应的是2个JVM进程,启动的是两个不同的类加载器,操作的实际上是不同的类.故而不会互相影响. 二.类加载. 当我们使用一个类,如果这个类还未加载到内存中,系统会通过加载.连接.初始化对类进行初始化. 1.类加载:指的是将类的c

JAVA提高第七篇 类加载器解析

今天我们学习类加载器,关于类加载器其实和JVM有很大关系,在这里这篇文章只是简单的介绍下类加载器,后面学习到JVM的时候还会详细讲到类加载器,本文分为下面几个小节讲解: 一.认识类加载器 1.什么是类加载器? 所谓的类加载器可以从其作用来理解,其功能就是将classpath目录下.class文件,加载到内存中来进行一些处理,处理完的结果就是一些字节码.那是谁把这些class类加载到内存中来的呢?就是类加载器. 2.JVM中默认的类加载器有哪些? java虚拟机中可以安装多个类加载器,系统默认三个

java基础学习笔记之类加载器

类加载器 java类加载器就是在运行时在JVM中动态地加载所需的类,java类加载器基于三个机制:委托,可见,单一. 把classpath下的那些.class文件加载进内存,处理后成为字节码,这些工作是类加载器做的. 委托机制指的是将加载类的请求传递给父加载器,如果父加载器找不到或者不能加载这个类,那么再加载他. 可见性机制指的是父加载器加载的类都能被子加载器看见,但是子加载器加载的类父加载器是看不见的. 单一性机制指的是一个类只能被同一种加载器加载一次. 默认类加载器 系统默认三个类加载器:

classloader类加载器_基于java类的加载方式详解

基础概念 Classloader 类加载器,用来加载 Java 类到 Java 虚拟机中.与普通程序不同的是.Java程序(class文件)并不是本地的可执行程序.当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader. JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,BootstrapClassLoader是用本地代码实现

java类的加载过程以及类加载器的分析

我们知道,我们写的java代码保存的格式是 .java, java文件被编译后会转换为字节码,字节码可以在任何平台通过java虚拟机来运行,这也是java能够跨平台的原因. 那JVM是如何来让我们写的java文件运行的呢? 这个问题通常的问法好像是:类是如何被加载的. 记得第一次遇见这个问题的时候,同学给我的回答是: 1.虚拟机会加载JDK里类的核心包 2.虚拟机会加载JDK里类的扩展包 3.虚拟机会加载JDK里类的系统包 4.虚拟机再会加载我们写好的java类. 初学的时候,大家都这么说,好像

详解Java 类的加载、连接和初始化

系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类.本节将会详细介绍类加载.连接和初始化过程中的每个细节. JVM 和类 当调用 java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里.正如前面介绍的,同一个 JVM 的所有线程.所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区.当系统出现以下几种情况时,JVM 进程将被终止. 程序运行到最

详解Java 类的加载机制

一.类的加载机制 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接

Java类的加载连接和初始化实例分析

本文实例讲述了Java类的加载连接和初始化.分享给大家供大家参考,具体如下: 一 点睛 1 类加载 当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载.连接.初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化. 类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序使用任何类时,系统都会为之建立一个java.lang.Class对象. 2 类数据的来源 通过

基于Java中的数值和集合详解

数组array和集合的区别: (1) 数值是大小固定的,同一数组只能存放一样的数据. (2) java集合可以存放不固定的一组数据 (3) 若程序事不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用 数组转换为集合: Arrays.asList(数组) 示例: int[] arr = {1,3,4,6,6}; Arrays.asList(arr); for(int i=0;i<arr.length;i++){ System.out.println(arr[

基于java 线程的几种状态(详解)

线程可以有六种状态: 1.New(新创建) 2.Runnable(可运行)(运行) 3.Blocked(被阻塞) 4.Waiting(等待) 5.Timed waiting(计时等待) 6.Terminated(被终止) 新创建线程: 当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行,它的当前状态为new,在线程运行之前还有一些基础工作要做. 可运行线程: 一旦线程调用start方法,线程处于runnable状态.在这个状态下的线程可能正在运行也可能没有运行(

Java数据库连接_jdbc-odbc桥连接方式(详解)

jdbc-odbc桥连接方式操作数据库SU(Course) 步骤: 1.配置数据源 控制面板下搜索管理工具->ODBC数据源(32位)->添加->选择sql server(填写名称mytest,服务器local或者.)->下一步->更改默认的数据库为SU->下一步->测试数据源至成功 用户数据源会多一条mytest,至此配置数据源成功. 2.在程序中连接数据源 打开eclipse,编写程序. public class Demo_1 { public static

基于js文件加载优化(详解)

在js引擎部分,我们可以了解到,当渲染引擎解析到script标签时,会将控制权给JS引擎,如果script加载的是外部资源,则需要等待下载完后才能执行. 所以,在这里,我们可以对其进行很多优化工作. 放置在BODY底部 为了让渲染引擎能够及早的将DOM树给渲染出来,我们需要将script放在body的底部,让页面尽早脱离白屏的现象,即会提早触发DOMContentLoaded事件. 但是由于在IOS Safari, Android browser以及IOS webview里面即使你把js脚本放到

Java 配置加载机制详解及实例

前言 现如今几乎大多数Java应用,例如我们耳熟能详的tomcat, struts2, netty-等等数都数不过来的软件,要满足通用性,都会提供配置文件供使用者定制功能. 甚至有一些例如Netty这样的网络框架,几乎完全就是由配置驱动,这样的软件我们也通常称之为"微内核架构"的软件.你把它配置成什么,它就是什么. It is what you configure it to be. 最常见的配置文件格式是XML, Properties等等文件. 本文探讨加载配置中最通用也是最常见的场