Java spi机制

 

从哪里来

以java 最原始的jdbc编程开始

private static Connection getConn() {
  String driver = "com.mysql.jdbc.Driver";
  String url = "jdbc:mysql://localhost:33066/mysql";
  String username = "root";
  String password = "root";
  Connection conn = null;
  try {
    Class.forName(driver); //classLoader,加载对应驱动
    conn = DriverManager.getConnection(url, username, password);
  } catch (Exception e) {
    e.printStackTrace();
  }
  return conn;
}

这是一段jdbc连接mysql数据库的代码. 但是如果把Class.forName(driver);这行注释掉其实也是可以正常运行的(jdk6+).

理解这段代码涉及到的概念有

  • 面向接口编程
  • 反射
  • spi机制
  • jdbc版本
  • 类加载器
  • 解耦

反射

我们知道Class.forName()是反射的一种,作用是初始化字符串对应的类,这里是com.mysql.jdbc.Driver.初始化的同时执行静态代码块.

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  public Driver() throws SQLException {}
  static {
    try {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {
      throw new RuntimeException("Can't register driver!");
    }
  }
}

可以看到执行Class.forName("com.mysql.jdbc.Driver")仅仅是将com.mysql.jdbc.Driver注册给DriverManager.

JDBC版本和java sdk的对应关系

  1. JDBC 1.0 随 JDK1.1 发布
  2. JDBC 2.0 随 JDK1.2 和 JDK1.3 发布
  3. JDBC 3.0 随 JDK1.4 发布
  4. JDBC 4.0 随 JDK1.6 发布
  5. JDBC 4.1 随 JDK1.7 发布
  6. JDBC 4.2 随 JDK1.8 发布

spi机制

一言不合就扒代码

conn = DriverManager.getConnection(url, username, password);

继续查看getConnection()

public static Connection getConnection(String url,String user, String password) throws SQLException {
  java.util.Properties info = new java.util.Properties();
  // 获取当前的类加载器
  ClassLoader callerCL = DriverManager.getCallerClassLoader();

  if (user != null) {
    info.put("user", user);
  }
  if (password != null) {
    info.put("password", password);
  }
  return (getConnection(url, info, callerCL));
}

继续查看getConnection()重载方法

//  Worker method called by the public getConnection() methods.
private static Connection getConnection(String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
  java.util.Vector drivers = null;
  /*
  * When callerCl is null, we should check the application's
  * (which is invoking this class indirectly)
  * classloader, so that the JDBC driver class outside rt.jar
  * can be loaded from here.
  */
  synchronized(DriverManager.class) {	 
    // synchronize loading of the correct classloader.
    if(callerCL == null) {
      callerCL = Thread.currentThread().getContextClassLoader();
    }
  }

  if(url == null) {
    throw new SQLException("The url cannot be null", "08001");
  }
  println("DriverManager.getConnection(\"" + url + "\")");
  if (!initialized) {
    //重点在这个初始化方法
    initialize();
  }

  ...省略

}

// Class initialization.
static void initialize() {
  if (initialized) {
      return;
  }
  initialized = true;
  //继续扒这个方法
  loadInitialDrivers();
  println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
  String drivers;

  try {
    drivers = (String) java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("jdbc.drivers"));
  } catch (Exception ex) {
    drivers = null;
  }

  // If the driver is packaged as a Service Provider,
  // load it.

  // Get all the drivers through the classloader 
  // exposed as a java.sql.Driver.class service.

  DriverService ds = new DriverService();

  // Have all the privileges to get all the 
  // implementation of java.sql.Driver
  //调用实现的run方法进行接口的所有实现自注册
  java.security.AccessController.doPrivileged(ds);

  println("DriverManager.initialize: jdbc.drivers = " + drivers);
  if (drivers == null) {
    return;
  }
  ...省略
}

DriverService 实现了 java.security.PrivilegedAction接口

public interface PrivilegedAction<T> {
    /**
     * Performs the computation.  This method will be called by
     * <code>AccessController.doPrivileged</code> after enabling privileges.
     *
     * @return a class-dependent value that may represent the results of the
     *	       computation. Each class that implements
     *         <code>PrivilegedAction</code>
     *	       should document what (if anything) this value represents.
     * @see AccessController#doPrivileged(PrivilegedAction)
     * @see AccessController#doPrivileged(PrivilegedAction,
     *                                     AccessControlContext)
     */
    T run();
}

java.security.PrivilegedAction接口只有一个run方法 看注释知道通过AccessController.doPrivileged()去调用run().

再看DriverService.run()

public Object run() {
  // uncomment the followin line before mustang integration 	
  // Service s = Service.lookup(java.sql.Driver.class);
  // ps = s.iterator();
  ps = Service.providers(java.sql.Driver.class);

	/* Load these drivers, so that they can be instantiated. 
	 * It may be the case that the driver class may not be there
         * i.e. there may be a packaged driver with the service class
         * as implementation of java.sql.Driver but the actual class
         * may be missing. In that case a sun.misc.ServiceConfigurationError
         * will be thrown at runtime by the VM trying to locate 
	 * and load the service.
         * 
	 * Adding a try catch block to catch those runtime errors
         * if driver not available in classpath but it's 
	 * packaged as service and that service is there in classpath.
	 */
		
  try {
    while (ps.hasNext()) {
      ps.next();
    } // end while
  } catch(Throwable t) {
    // Do nothing
  }
  return null;
}

最终找到了自动获取驱动的sun.misc.Service类. 变量ps持有一个Iterator的实现

public boolean hasNext() throws ServiceConfigurationError {
  if (this.nextName != null) {
    return true;
  } else {
    if (this.configs == null) {
      try {
        //根据拼接文件路径 最终结果是 META-INF/services/java.sql.Driver
        String var1 = "META-INF/services/" + this.service.getName();
        if (this.loader == null) {
            this.configs = ClassLoader.getSystemResources(var1);
        } else {
            //类加载器加载对应的文件
            this.configs = this.loader.getResources(var1);
        }
      } catch (IOException var2) {
        Service.fail(this.service, ": " + var2);
      }
    }

    while(this.pending == null || !this.pending.hasNext()) {
      if (!this.configs.hasMoreElements()) {
        return false;
      }

      this.pending = Service.parse(this.service, (URL)this.configs.nextElement(), this.returned);
    }

    this.nextName = (String)this.pending.next();
    return true;
  }
}

public Object next() throws ServiceConfigurationError {
  if (!this.hasNext()) {
    throw new NoSuchElementException();
  } else {
    String var1 = this.nextName;
    this.nextName = null;
    Class var2 = null;

    try {
      //将实现类的初始化放到这里 系统自动进行初始化,实现驱动的自动注册.
      var2 = Class.forName(var1, false, this.loader);
    } catch (ClassNotFoundException var5) {
      Service.fail(this.service, "Provider " + var1 + " not found");
    }

    if (!this.service.isAssignableFrom(var2)) {
      Service.fail(this.service, "Provider " + var1 + " not a subtype");
    }

    try {
      return this.service.cast(var2.newInstance());
    } catch (Throwable var4) {
      Service.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4);
      return null;
    }
  }
}

这种自动获取接口对应的实现的机制就是java的spi机制.

是什么

经过上面的代码,可以说明的是,在jdbc4.0以前,也可以说在jdk6之前,进行jdbc连接数据库编程,是需要手动注册驱动的. 升级到jdbc4.0之后,jdbc4也基于spi的机制来自动发现驱动提供商了,驱动供应商可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.

JavaSPI 实际上是 “基于接口的编程+策略模式+配置文件” 组合实现的动态加载机制。

SPI机制的约定:

  1. 在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名
  2. 使用ServiceLoader类动态加载META-INF中的实现类
  3. 如SPI的实现类为Jar则需要放在主程序classPath中
  4. Api具体实现类必须有一个不带参数的构造方法

具体而言:

  1. 定义一组接口,假设是 weihai4099.github.io.spi.DemoService
  2. 写出接口的一个或多个实现(weihai4099.github.io.spi.Demo1ServiceImpl, weihai4099.github.io.spi.Demo2ServiceImpl);
  3. src/main/resources/(maven构建模式) 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 weihai4099.github.io.spi.DemoService, 内容是要应用的实现类(weihai4099.github.io.spi.Demo1ServiceImpl\r\nweihai4099.github.io.spi.Demo2ServiceImpl);
  4. 使用 ServiceLoader 来加载配置文件中指定的实现。
public static void main(String[] args) {
  ServiceLoader<DemoService> services = ServiceLoader.load(DemoService.class);
  Iterator<DemoService> it = services.iterator();
  while (it.hasNext()) {
    DemoService service = it.next();
    service.sayHello();
  }
}

可以在Github查看源码

在以前,我们一般是这种形式的写法,这种在生产上应该是尽量避免的,死耦合.

DemoService service = new Demo1ServiceImpl();
service.sayHello();

使用了spring,如果接口只有一个实现,spring ioc 自动将实现进行注入.如果有多个实现,我们还可以使用@Qualifier注解根据beanname选择性注入.

如果进行编程式注入,还可以使用StringValueResolver+@Value动态注入.

现在又多了一种原生的可插拔式的声明方式.

使用场景

  1. 最早的common-logging apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可.
  2. 之后的jdbc
  3. jsr303 Bean Validation 在spring mvc 中 ,我们可以对参数进行校验, 只需要在方法参数 添加@Valid注解,申明org.hibernate:hibernate-validator:5.4.2.Final依赖. hibernate-validator作为依赖传递了javax.validation:validation-api的依赖.其中javax.validation:validation-api是接口定义,是标准.hibernate-validator是接口的实现.spring mvc自动在classpath下扫描发现验证接口的实现.进行验证,所依赖的就是java spi机制.使用spi的好处就是,可以随时替换其他实现,比如Apache BVal
  4. Dubbo的自动服务发现机制
  5. servlet3.0 之后的ServletContainerInitializer初始化器 在spring4.0之后,我们可以实现org.springframework.web.WebApplicationInitializer并实现void onStartup(ServletContext servletContext) throws ServletException;方法提前做一些组件的初始化操作,我们一般是进行java config @Bean 申明 bean. 同样在spring-web.jar的META-INF/services/会发现以接口javax.servlet.ServletContainerInitializer为名的文件,文件内容是该接口对应的实现org.springframework.web.WebApplicationInitializer.

参考链接

  1. JDK源码分析之细说SPI机制之实现原理剖析
  2. Java的SPI机制分析
  3. 谈java SPI机制、spring-mvc启动及servlet3.0
  4. JavaSPI机制学习笔记
  5. java中的SPI机制
  6. Java的SPI机制浅析与简单示例
  7. JSR 303 - Bean Validation 介绍及最佳实践
  8. ServletContainerInitializer初始化器