vlambda博客
学习文章列表

双亲委派模型以及SpringFactoriesLoader详解(最全最简单的介绍)

前言

前面我们介绍了JavaConfig和常用的Annotation,这一篇文章我们来聊聊 SpringFactoriesLoader,在讲 SpringFactoriesLoader之前我会先说到JVM的类加载器以及双亲委派模型。闲话少叙,直入主题。

类加载的过程

加载类时,JVM必须完成如下如下三个步骤:

  1. 通过一个类的全限定名来获取此类的二进制字节流。

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。在JVM中,完成类加载,尤其是获取类的二进制信息的组件就是ClassLoader类加载器,JVM按照文件名识别加载jar包。

类加载器

JVM一共有三种类加载器,分别是:

  1. 启动类加载器( BootstrapClassLoader)加载Java核心类库(%java.home%lib下面的核心类库 或 -Xbootclasspath选项指定的jar包);

  2. 扩展类加载器( ExtClassLoader)加载扩展类库(%java.home%/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 );

  3. 应用类加载器( AppClassLoader)加载应用的类路径(用户类路径(java -classpath或-Djava.class.path变量所指的目录)下的类库。类的继承关系如下图所示:  JVM通过双亲委派模型进行类的加载,我们可以通过继承 java.lang.classLoader实现自己的类加载器。

何为双亲委派模型

当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终的加载任务都会传递到最顶层的 BootstrapClassLoader(启动类加载器),只有当父加载器无法完成加载任务时,才会尝试自己来加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载。其加载流程图如下:

下面就是ClassLoader类的loadClass方法

ClassLoader类的loadClass方法

 
   
   
 
  1. protected Class<?> loadClass(String name, boolean resolve)

  2. throws ClassNotFoundException

  3. {

  4. synchronized (getClassLoadingLock(name)) {

  5. // First, check if the class has already been loaded

  6. //首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回。

  7. Class<?> c = findLoadedClass(name);

  8. if (c == null) {

  9. long t0 = System.nanoTime();

  10. try {

  11. //遵循双亲委派的模型,首先通过递归从父加载器开始找

  12. //直到父类加载器是BootstrapClassLoader为止

  13. if (parent != null) {

  14. c = parent.loadClass(name, false);

  15. } else {

  16. c = findBootstrapClassOrNull(name);

  17. }

  18. } catch (ClassNotFoundException e) { }


  19. if (c == null) {

  20. // If still not found, then invoke findClass in order

  21. // to find the class.

  22. //如果还找不到,则尝试通过findClass方法去寻找

  23. //findClass是留给开发者自己实现的,也就是说自定义类加载器时,

  24. //重写此方法即可。

  25. c = findClass(name);

  26. }

  27. }

  28. if (resolve) {

  29. resolveClass(c);

  30. }

  31. return c;

  32. }

  33. }

采用双亲委派的一个好处主要有如下两点:

  1. 防止类被重复加载: Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类被重复加载, 当父类已经加载了该类时,子类就不会再加载一次。保证了使用不同类加载器最终得到的是同一个对象。

  2. 保证核心库的类型安全: Java核心api中定义的类不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

双亲委派模型存在的问题

使用双亲委派模型也存在一些问题,例如:Java提供了很多服务提供者接口(ServiceProvinderInterface,SPI),允许第三方为这些接口提供实现,常见的SPI有JDBC,JNDI等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在了一个问题:SPI的接口是Java核心库的一部分,是由BootStrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootStrapClassLoader是无法找到SPI的实现类的。因为它只加载Java的核心库,它不能代理给AppClassLoader,因为他是最顶层的类加载器,也就是说,双亲委派模型并不能解决这个问题。那么如何解决这个问题呢?

解决办法

线程上下文加载器(ContextClassLoader)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是Thread类的一个变量而已,可以通过setContextClassLoader(ClassLoadercl) 和getContextClassLoader()来设置和获取该对象,如果不做任何的设置。Java应用的线程上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器。就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。

以JDBC驱动管理为例

mysql-connector-java-6.0.6.jar 下的 META-INF/services目录下有一个以 接口全限定名 (java.sql.Driver)为命名的文件,内容为实现类的全限定名。主程序通过 java.util.ServiceLoader动态装载实现模块,它通过扫描 META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM中。需要注意的是SPI的实现类必须携带一个不带参数的构造方法,用于反射生成实例。如下:

 
   
   
 
  1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {

  2. //

  3. // Register ourselves with the DriverManager

  4. //

  5. static {

  6. try {

  7. java.sql.DriverManager.registerDriver(new Driver());

  8. } catch (SQLException E) {

  9. throw new RuntimeException("Can't register driver!");

  10. }

  11. }


  12. /**

  13. * Construct a new driver and register it with DriverManager

  14. *

  15. */

  16. public Driver() throws SQLException {

  17. // Required for Class.forName().newInstance()

  18. }

  19. }

ServiceLoader 类装载实现模块的代码如下:

 
   
   
 
  1. public final class ServiceLoader<S>

  2. implements Iterable<S>

  3. {

  4. private static final String PREFIX = "META-INF/services/";

  5. // The class loader used to locate, load, and instantiate providers

  6. private final ClassLoader loader;

  7. /**

  8. * Creates a new service loader for the given service type, using the

  9. * current thread's {@linkplain java.lang.Thread#getContextClassLoader

  10. * context class loader}.

  11. *

  12. * */

  13. public static <S> ServiceLoader<S> load(Class<S> service) {

  14. ClassLoader cl = Thread.currentThread().getContextClassLoader();

  15. return ServiceLoader.load(service, cl);

  16. }

  17. }

加载资源

类加载器除了加载Class外,还有一个非常重要的功能,就是加载资源, 它可以从jar包中读取任何资源文件,比如:ClassLoader.getResource(Stringname)方法就是用于读取jar包中的资源文件。

 
   
   
 
  1. public Enumeration<URL> getResources(String name) throws IOException {

  2. Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];

  3. if (parent != null) {

  4. tmp[0] = parent.getResources(name);

  5. } else {

  6. tmp[0] = getBootstrapResources(name);

  7. }

  8. tmp[1] = findResources(name);


  9. return new CompoundEnumeration<>(tmp);

  10. }

它的逻辑其实跟类加载的逻辑是一样的。首先判断父类加载器是否为空,如果不为空则委托父类加载器执行资源查找任务,直到到达 BootstrapClassLoader,只有当父类加载器找不到时,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的jar包。就如同加载class一样,最后会扫描所有的jar包,找到符合条件的资源文件。findResources(name)方法会遍历其负责加载的所有jar包。找到jar包中名称为name的资源文件,这里的资源可以是任何文件,甚至是.class文件。比如下面的实例:用于查找String.class文件。

 
   
   
 
  1. //寻找String.class文件

  2. public static void main(String[] args) throws IOException {

  3. String name = "java/lang/String.class";

  4. Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);

  5. while (urls.hasMoreElements()) {

  6. URL url = urls.nextElement();

  7. System.out.println(url.toString());

  8. }

  9. }

运行得到如下结果:

 
   
   
 
  1. $JAVA_HOME/jre/lib/rt.jar!/java/lang/String.class

SpringFactoriesLoader详解

说完了类加载器,以及双亲委派模型还有资源文件的查找,下面就开始介绍我们本篇文章的真正主角, SpringFactoriesLoader 它本质上属于Spring框架私有的一种扩展方案,类似于SPI,Spring Boot在Spring基础上的很多核心的功能都是基于此。根据资源文件的URL,就可以构造相依的文件来读取资源内容。

 
   
   
 
  1. public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

  2. //spring.factories文件的格式为:key=value1,value2,value3

  3. //从所有的jar中找到META-INF/spring.factories文件

  4. //然后,从文件中解析出key=factoryClass类名称的所有value值

  5. public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {

  6. String factoryClassName = factoryClass.getName();

  7. //获取资源文件的URL

  8. Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :

  9. ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));

  10. List<String> result = new ArrayList<String>();

  11. //遍历所有的URL

  12. while (urls.hasMoreElements()) {

  13. URL url = urls.nextElement();

  14. //根据资源文件URL解析properties文件

  15. Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));

  16. String factoryClassNames = properties.getProperty(factoryClassName);

  17. //组装并返回

  18. result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));

  19. }

  20. return result;

  21. }

  22. }

有了前面关于ClassLoader的知识铺垫,再来看上面的代码就简单了。首先从classpath下每个jar包下搜寻文件名是 META-INF/spring.factories的配置文件,然后将解析properties文件,找到指定名称的配置后返回,需要注意的是,这里不仅仅是在classpath路径下查找,会扫描所有路径下的jar包,只不过这个文件只会在classpath下的jar包中。简单看下spring.factories吧。

 
   
   
 
  1. // 来⾃ org.springframework.boot.autoconfigure下的META-INF/spring.factories

  2. //EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能

  3. # Auto Configure

  4. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

  5. org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\

  6. org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\

  7. org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\

执行 loadFactoryNames((EnableAutoConfiguration.class,classLoader)后,得到对应的一组 @Configuration类,我们就可以通过反射实例化这些类然后注入到IOC容器中,最后容器里就有了一系列标注了 @Cofiguration的JavaConfig形式的配置类。

总结

本文首先介绍了JVM中的三种类加载器,分别是启动类加载器,扩展类加载器,以及应用类加载器。然后说到了双亲委派模型以及它的缺点。根据它的缺点引出了线程上下文加载器(ContextClassLoader) 以及他在SPI的实现上的运用。最后就是详细介绍了SpringFactoriesLoader的实现原理。