jvm-sanbox详解-SPI机制

什么是SPI机制

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口。我们知道JDK代码提供了大量的方便的工具类给我们使用,JDK会对经常使用接口进行抽象统一。如链接数据库我们可以使用java.sql.DriverManager,但各种数据库的实现各自不同,所以为了给用户统一使用,屏蔽底层各自难懂的细节,我们这种SPI机制产生了。

mysql加载驱动

回想我们经常使用mysql的过程

0.配置文件:
    url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: xxx
    password: xxxxxx
1.注册驱动(现在已经不需要这一步加载了,写了也兼容不报错)
Class.forName("com.mysql.jdbc.Driver");
2.创建连接:
conn = (Connection) DriverManager.getConnection(url,username,password);
3.执行sql
stat = conn.createStatement();
String sql = "SELECT * FROM tb_person";
ResultSet rs = stat.executeQuery(sql);

让我们看看java.sql.DriverManager做了什么事情

//类初始化代码加载驱动
static {
     
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}


private static void loadInitialDrivers() {
     
    String drivers;
    try {
     
        //加载系统属性jdbc.drivers指定的drivers
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
     
            public String run() {
     
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
     
        drivers = null;
    }
    
    //如果driver驱动是Service Provider形式的,直接加载,并且替换上面系统属性指定方式
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
     
        public Void run() {
     
            
            //使用ServiceLoader加载Driver
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            //遍历driver,driver有可能不存在或者配置错误,直接catch异常并忽略
            try{
     
                while(driversIterator.hasNext()) {
     
                    driversIterator.next();
                }
            } catch(Throwable t) {
     
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
     
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
     
        try {
     
            println("DriverManager.Initialize: loading " + aDriver);
            //这里注意使用了系统类加载器加载驱动
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
     
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

java.sql.Driver.class 就定义了几个接口,实现都交给厂商,然后我们看ServiceLoader干了个啥

public final class ServiceLoader<S>
    implements Iterable<S>
{
     
	//这里配置写死 SPI机制加载配置文件的路径
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    
    //DriverManager.load调用此方法
    public static <S> ServiceLoader<S> load(Class<S> service) {
     
        //注意这里使用的线程上下文类加载器,加载这个类的是系统类加载器,因此加载驱动是使用系统类加载器来加载的,打破了类加载器的双亲委托机制。
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
     
        return new ServiceLoader<>(service, loader);
    }
    //真实调用的方法,初始化赋值
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
     
        //保留加载的类
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //保留加载的classloader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    public void reload() {
     
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    //DriverManager加载完Driver.class后,调用此方法
    public Iterator<S> iterator() {
     
        return new Iterator<S>() {
     
            //上面声明的属性providers是已加载的驱动   
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            
            //下面依次iterator依次优先遍历已加载,其次是懒加载lookupIterator
            public boolean hasNext() {
     
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
     
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
     
                throw new UnsupportedOperationException();
            }

        };
    }
}

懒加载中重点方法如下:JDK源码java.util.ServiceLoader.LazyIterator

//核心逻辑:解析SPI位置的配置文件,确定实现接口的类
private boolean hasNextService() {
     
    if (nextName != null) {
     
        return true;
    }
    if (configs == null) {
     
        try {
     
            //如sql的驱动:META-INF/services/java.sql.Driver
            //这里就是SPI的核心实现,从这个文件位置加载实现java.sql.Driver接口的class类
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
     
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
     
        if (!configs.hasMoreElements()) {
     
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
//根据上个方法实现的类,去初始化类,并保存到providers中
private S nextService() {
     
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
     
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
     
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
     
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
     
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
     
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

让我们看看Service Provider的概念

Files in the META-INF/services directory are service provider configuration files. A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers may be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers may also be made available by adding them to the applet or application class path or by some other platform-specific means.
A service is represented by an abstract class. A provider of a given service contains one or more concrete classes that extend this service class with data and code specific to the provider. This provider class will typically not be the entire provider itself but rather a proxy that contains enough information to decide whether the provider is able to satisfy a particular request together with code that can create the actual provider on demand. The details of provider classes tend to be highly service-specific; no single class or interface could possibly unify them, so no such class has been defined. The only requirement enforced here is that provider classes must have a zero-argument constructor so that they may be instantiated during lookup.

简述就是在jar包 META-INF/services目录下定义一个文件,文件名就是接口或者抽象类,文件内容是定义的实现类,可以有多个实现类。如mysql驱动包 META-INF/services/java.sql.Driver 这个文件下有两个驱动

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

那么mysql如何知道加载哪个驱动呢,我们来看 java.sql.DriverManager#getConnection 方法

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
     
    /*
     * 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.
     */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    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 + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;
    //我们可以看到这里是循环遍历所有的驱动创建连接,碰见能够加载的合适驱动加载成功后就立刻返回
    for(DriverInfo aDriver : registeredDrivers) {
     
        // If the caller does not have permission to load the driver then
        // skip it.
        if(isDriverAllowed(aDriver.driver, callerCL)) {
     
            try {
     
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
     
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
     
                if (reason == null) {
     
                    reason = ex;
                }
            }

        } else {
     
            println("    skipping: " + aDriver.getClass().getName());
        }

    }

    // if we got here nobody could connect.
    if (reason != null)    {
     
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}

至此,驱动的加载就彻底完成。回顾一下JDBC链接MySql过程:JDK使用了SPI的机制,让不同服务提供方提供服务,注册驱动,在使用驱动的时候,遍历所有注册过的驱动,并尝试创建链接,链接成功就返回。

SPI机制使用限制

  • 需要启动加载配置,所以在运行时无法增加新的服务(猜测更多是为了安全,因为三方服务如mysql都是通过系统类加载器加载的)

SPI注解

我们在使用SPI特性的时候需要写 /META-INF/services/xxx 文件,手动将SPI的配置写入配置文件很是不方便,我们可以使用 @MetaInfServices注解
引入包


  org.kohsuke.metainf-services
  metainf-services
  xxx
  true

总结/回顾

  • 代码编写要点:
    • 实现某个接口类,接口类可以是JDK源码中的,也可以是自定义的(如sl4j日志打印门面)
    • 打包时需要添加 /META-INF/services/xx 文件,xx文件名是接口名,文件内容是实现的接口类
  • 加载逻辑要点:
    • 对于JDK原生接口类,会使用系统类加载器来加载,而不是用户类加载器加载,打破了类加载器的双亲委派模型
    • 为防止service的无效加载,JDK采用懒加载(同时给我们提供了懒加载的方法),只有真正使用驱动的时候加载

使用方式以及参考地址:http://metainf-services.kohsuke.org/index.html

参考文档

serviceloader:https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html
SPI说明以及官方示例:https://docs.oracle.com/javase/tutorial/ext/basics/spi.html

你可能感兴趣的:(jvm-sandbox,源码解析,jvm-sandbox)