1.介紹
熟悉JDBC的同學(xué)都知道,在jdbc4.0之前,在使用DriverManager獲取DB連接之前,我們總是需要顯示的實(shí)例化DB驅(qū)動(dòng)。比如,對(duì)mysql,典型的代碼如下:
Connection conn = null;
Statement stmt = null;
try{
// 注冊(cè) JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// 打開(kāi)連接
conn = DriverManagger.getConnection(DB_URL,USER,PASSWD);
// 執(zhí)行一條sql
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 數(shù)據(jù)解包
while(ts.next()){
// 根據(jù)列名獲取列值
// ...
} catch(SQLException se) {
// ...
} final {
try {
if (stmt!=null) stmt.close();
} catch(Exception e) {/*ignored*/}
try {
if (conn!=null) conn.close();
} catch(Exception e) {/*ignored*/}
}
}
JDBC的開(kāi)始,總是需要通過(guò)Class.forName顯式實(shí)例化驅(qū)動(dòng),否則將找不到對(duì)應(yīng)DB的驅(qū)動(dòng)。但是JDBC4.0開(kāi)始,這個(gè)顯式的初始化不再是必選項(xiàng)了,它存在的意義只是為了向上兼容。那么JDBC4.0之后,我們的應(yīng)用是如何找到對(duì)應(yīng)的驅(qū)動(dòng)呢?
答案就是SPI(Service Provider Interface)。Java在語(yǔ)言層面為我們提供了一種方便地創(chuàng)建可擴(kuò)展應(yīng)用的途徑。SPI提供了一種JVM級(jí)別的服務(wù)發(fā)現(xiàn)機(jī)制,我們只需要按照SPI的要求,在jar包中進(jìn)行適當(dāng)?shù)呐渲?,jvm就會(huì)在運(yùn)行時(shí)通過(guò)懶加載,幫我們找到所需的服務(wù)并加載。如果我們一直不使用某個(gè)服務(wù),那么它不會(huì)被加載,一定程度上避免了資源的浪費(fèi)。
2.一個(gè)簡(jiǎn)單的例子
我們通過(guò)一個(gè)簡(jiǎn)單的例子看看如何最小化構(gòu)建一個(gè)基于SPI的服務(wù)。
2.1 創(chuàng)建一個(gè)默認(rèn)的maven項(xiàng)目
$ mvn archetype:generate -DgroupId=cn.jinlu.spi.demo -DartifactId=simplespi -Dversion=0.1-SNAPSHOT -DpackageName=cn.jinlu.spi.demo -DarchetypeArtifactId=maven-archetype-quickstart
...
[INFO] Using property: groupId = cn.jinlu.spi.demo
[INFO] Using property: artifactId = simplespi
[INFO] Using property: version = 0.1-SNAPSHOT
[INFO] Using property: package = cn.jinlu.spi.demo
Confirm properties configuration:
groupId: cn.jinlu.spi.demo
artifactId: simplespi
version: 0.1-SNAPSHOT
package: cn.jinlu.spi.demo
Y: :[回車(chē)]
2.2 添加一個(gè)interface或abstract class
Java SPI并沒(méi)有強(qiáng)制必須使用interface或abstract class,完全可以將class注冊(cè)為SPI注冊(cè)服務(wù),但是作為可擴(kuò)展服務(wù),使用interface或abstract class是一個(gè)好習(xí)慣。
在包 “cn.jinlu.spi.demo”中定義一個(gè)接口Animal:
package cn.jinlu.spi.demo;
public interface Animal {
void eat();
void sleep();
}
2.3 提供實(shí)現(xiàn)類(lèi)
package cn.jinlu.spi.demo.impl;
import cn.jinlu.spi.demo.Animal;
public class Elephant implements Animal {
@Override
public void eat() {
System.out.println("Elephant is eating");
}
@Override
public void sleep() {
System.out.println("Elephant is sleeping");
}
}
2.4 服務(wù)注冊(cè)
在main目錄下創(chuàng)建目錄 "resources/META-INF/services"
mkdir -p resources/META-INF/services
再在該目錄下創(chuàng)建以接口Animal全限定名為名的配置文件,文件內(nèi)容為該接口的實(shí)現(xiàn)類(lèi)的全限定名,即
echo "cn.jinlu.spi.demo.impl.Elephant" > resources/META-INF/services/cn.jinlu.spi.demo.Animal
完成此步驟后,在當(dāng)前maven項(xiàng)目的 src/main/resources/META-INF/services下有這么一個(gè)配置文件:"cn.jinlu.spi.demo.Animal",并且它的內(nèi)容為"cn.jinlu.spi.demo.impl.Elephant"。
注意本步驟的要點(diǎn):
- 必須放在JAR包或項(xiàng)目的指定路徑,即 META-INF/services 下
- 必須以服務(wù)的全限定名命名配置文件,比如本例中,配置文件必須命名為 cn.jinlu.spi.demo.Animal,java會(huì)根據(jù)此名進(jìn)行服務(wù)查找
- 內(nèi)容必須是一個(gè)實(shí)現(xiàn)類(lèi)的全限定名,如果要注冊(cè)多個(gè)實(shí)現(xiàn)類(lèi),按行分割。注釋以#開(kāi)頭。
2.5 增加單元測(cè)試:
注意,如果找不到@Test,可能是junit版本太低,在pom.xml中將其改為 4.0 或更高版本(maven-archetype-quickstart模板默認(rèn)的JUNIT目前是3.8.1版本)。
package cn.jinlu.spi.demo;
import org.junit.Test;
import java.util.ServiceLoader;
public class AnimalTest {
@Test
public void animalTest() {
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
for(Animal animal: animals) {
animal.eat();
animal.sleep();
}
}
}
2.6 執(zhí)行結(jié)果:
Elephant is eating
Elephant is sleeping
可見(jiàn),雖然我們沒(méi)有顯式使用Animal的實(shí)現(xiàn)類(lèi)Elephant,但是java幫我們自動(dòng)加載了改實(shí)現(xiàn)類(lèi)。
3.源碼分析
接下來(lái)從代碼層面看看SPI都為我們做了什么。首先看看java.util.ServiceLoader的實(shí)現(xiàn)。在2.5節(jié)中,我們看到ServiceLoader使用非常簡(jiǎn)單,只需要調(diào)用一個(gè)靜態(tài)方法load并以要加載的服務(wù)的父類(lèi)(通常是一個(gè)interface或abstract class)作為參數(shù),jvm就會(huì)幫我們構(gòu)建好當(dāng)前進(jìn)程中所有注冊(cè)到 META-INF/services/[service full qualified class name] 的服務(wù)。
3.1 創(chuàng)建ServiceLoader實(shí)例
下面是構(gòu)造ServiceLoader實(shí)例的相關(guān)代碼。ServiceLoader必須通過(guò)靜態(tài)方法load(Class<?> service)的方式加載服務(wù),默認(rèn)會(huì)使用當(dāng)前線程的上下文class loader。構(gòu)造完ServiceLoader后,ServiceLoader實(shí)例并不會(huì)立刻掃描當(dāng)前進(jìn)程中的服務(wù)實(shí)例,而是創(chuàng)建一個(gè)LazyIterator懶加載迭代器,在實(shí)際使用時(shí)再掃描所有jar包找到對(duì)應(yīng)的服務(wù)。懶加載迭代器被保存在一個(gè)內(nèi)部成員lookupIterator中。
public final class ServiceLoader<S> implements Iterable<S>
{
...
/**
* 重新load指定serivice的實(shí)現(xiàn)。通過(guò)LazyIterator實(shí)現(xiàn)懶加載。
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
/**
* ServiceLoader構(gòu)造函數(shù),私有類(lèi)型,必須通過(guò)ServiceLoader.load(Class<?>)靜態(tài)方法來(lái)創(chuàng)建ServiceLoader實(shí)例
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
/**
* 構(gòu)建ServiceLoader實(shí)例
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
/**
* 通過(guò)service的class創(chuàng)建ServiceLoader實(shí)例,默認(rèn)使用上下文classloader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
...
}
3.2 服務(wù)加載和遍歷
- 上一節(jié)(3.1)的代碼中,我們可以看到,在調(diào)用了
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class)之后,ServiceLoader會(huì)返回一個(gè)Animal.class類(lèi)型的迭代器,但此時(shí)在ServiceLoader內(nèi)部只是創(chuàng)建了一個(gè)LazyIterator,而不會(huì)真正通過(guò)classloader在classpath中尋找相關(guān)的服務(wù)實(shí)現(xiàn)。 - 相反,
ServiceLoader通過(guò)實(shí)現(xiàn)Iterable<?>接口(public final class ServiceLoader<S> implements Iterable<S>),將對(duì)服務(wù)實(shí)現(xiàn)的尋址延后倒了對(duì)animals的遍歷時(shí)執(zhí)行。它在ServiceLoader內(nèi)通過(guò)LazyIterator實(shí)現(xiàn)。
public final class ServiceLoader<S> implements Iterable<S>
{
...
// 緩存的service provider,按照初始化順序排列。
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 當(dāng)前的LazyIterator迭代器指針,服務(wù)懶加載迭代器
private LazyIterator lookupIterator;
...
// 創(chuàng)建ServiceLoader迭代器,隱藏了LazyIterator的實(shí)現(xiàn)細(xì)節(jié)
public Iterator<S> iterator() {
return new Iterator<S>() {
// 創(chuàng)建Iterator迭代器時(shí)的ServiceLoader.providers快照,
// 因此在首次迭代時(shí),iterator總是會(huì)通過(guò)LazyIterator進(jìn)行懶加載
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 如果已經(jīng)掃描過(guò),則對(duì)providers進(jìn)行迭代;
if (knownProviders.hasNext())
return true;
// 如果沒(méi)有掃描過(guò),則通過(guò)lookupIterator進(jìn)行掃描和懶加載
return lookupIterator.hasNext();
}
public S next() {
// 如果已經(jīng)掃描過(guò),則對(duì)providers進(jìn)行迭代;
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 如果沒(méi)有掃描過(guò),則通過(guò)lookupIterator進(jìn)行掃描和懶加載
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
...
}
ServiceLoader的迭代器很簡(jiǎn)單:
- 未進(jìn)行迭代操作時(shí),不對(duì)jar包作任何掃描
- 首次迭代時(shí),因?yàn)镾erviceLoader.providers中沒(méi)有任何緩存,總是會(huì)通過(guò)LazyIterator進(jìn)行懶加載,并將service實(shí)現(xiàn)的全限定名與加載的service實(shí)例作為key-value緩存到ServiceLoader.providers中。
- 之后再進(jìn)行迭代時(shí),總是在ServiceLoader.providers中進(jìn)行。
3.3 懶加載迭代器LazyIterator
懶加載迭代器LazyIterator主要實(shí)現(xiàn)以下功能:
- 首次迭代時(shí),通過(guò)ClassLoader.getResources(String)獲得指定services文件的URL集合
- 如果是首次遍歷懶加載器,或者對(duì)上一個(gè)URL內(nèi)容解析獲得的service實(shí)現(xiàn)類(lèi)集合完成了迭代,則從configs中取下一個(gè)services文件URL進(jìn)行解析,按行獲得具體的service實(shí)現(xiàn)類(lèi)集合,并進(jìn)行迭代。
- 對(duì)當(dāng)前URL中解析得到的實(shí)現(xiàn)類(lèi)集合進(jìn)行迭代,每次返回一個(gè)service實(shí)現(xiàn)類(lèi)。
下面是LazyIterator的源碼及注釋?zhuān)?/p>
public final class ServiceLoader<S> implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
...
// Private inner class implementing fully-lazy provider lookup
private class LazyIterator implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
// 當(dāng)前service配置文件的內(nèi)容迭代器
// 即對(duì)services進(jìn)行遍歷,取出一個(gè)services配置文件,再對(duì)該文件按行解析,每行代表一個(gè)具體的service實(shí)現(xiàn)類(lèi),pending是某個(gè)services配置文件中service實(shí)現(xiàn)類(lèi)的迭代器
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 首次迭代時(shí),configs為空,嘗試通過(guò)classloader獲取名為:
// "META-INF/services/[服務(wù)全限定名]"的所有配置文件
if (configs == null) {
try {
// 注意fullName的定義:"META-INF/services/[服務(wù)全限定名]"
String fullName = PREFIX + service.getName();
// 通過(guò)ClassLoader.getResources()獲得資源URL集合
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果pending為空,或者pending已經(jīng)迭代到迭代器末尾,則嘗試解析下一個(gè)services配置文件
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 對(duì)當(dāng)前pending內(nèi)容進(jìn)行遍歷,每一項(xiàng)代表services的一個(gè)實(shí)現(xiàn)類(lèi)
nextName = pending.next();
return true;
}
}
...
}
最后,附上parse及parseLine的代碼,可以發(fā)現(xiàn),parseLine中會(huì)對(duì)服務(wù)實(shí)現(xiàn)類(lèi)進(jìn)行去重,所以在一個(gè)或多個(gè)services配置文件中配置多次的服務(wù)實(shí)現(xiàn)類(lèi)只會(huì)被處理一次。
public final class ServiceLoader<S> implements Iterable<S>
{
...
// 按行解析給定配置文件。如果解析出的服務(wù)實(shí)現(xiàn)類(lèi)沒(méi)有被其他已解析的配置文件配置過(guò),則通過(guò)參數(shù)nams返回給parse方法
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 去重,防止重復(fù)配置服務(wù),每個(gè)服務(wù)實(shí)現(xiàn)類(lèi)只會(huì)被解析一次
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
/**
* 解析指定的作為SPI配置文件的URL的內(nèi)容
*/
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
...
}
4.JDBC中對(duì)SPI的使用
最后,以JDBC為例,看一個(gè)SPI的實(shí)際使用場(chǎng)景。在文章開(kāi)始,我們提到過(guò),JDBC4.0之前,我們總是需要在業(yè)務(wù)代碼中顯式地實(shí)例化DB驅(qū)動(dòng)實(shí)現(xiàn)類(lèi):
Class.forName("com.mysql.jdbc.Driver");
為什么JDBC4.0之后不需要了呢?答案就在下面的代碼中。在系統(tǒng)啟動(dòng)時(shí),DriverManager靜態(tài)初始化時(shí)會(huì)通過(guò)ServiceLoader對(duì)所有jar包中被注冊(cè)為 java.sql.Driver 服務(wù)的驅(qū)動(dòng)實(shí)現(xiàn)類(lèi)進(jìn)行初始化,這樣就避免了上面通過(guò)Class.forName手動(dòng)初始化的繁瑣工作。
public class DriverManager {
// JDBC驅(qū)動(dòng)注冊(cè)中心,所有加載的JDBC驅(qū)動(dòng)都注冊(cè)在該CopyOnWriteArrayList中
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
...
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
// 如果通過(guò)jdbc.drivers配置了驅(qū)動(dòng),則在本方法最后進(jìn)行實(shí)例化
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("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.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通過(guò)ServiceLoader加載所有通過(guò)SPI方式注冊(cè)的"java.sql.Driver"服務(wù)
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 遍歷ServiceLoader實(shí)例進(jìn)行強(qiáng)制實(shí)例化,因此除了遍歷不做任何其他操作
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
}
);
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 強(qiáng)制加載"jdbc.driver"環(huán)境變量中配置的DB驅(qū)動(dòng)
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);
}
}
}
...
}
以mySql驅(qū)動(dòng)為例看看驅(qū)動(dòng)實(shí)例化時(shí)做了什么:
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
// 向DriverManager注冊(cè)自己
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
再看看mysql驅(qū)動(dòng)jar包中對(duì)service的配置:
因此,只要某個(gè)驅(qū)動(dòng)以這種方式被引用并被上下文class loader加載,那么該驅(qū)動(dòng)就會(huì)通過(guò)SPI的方式被自動(dòng)發(fā)現(xiàn)和加載。實(shí)際使用時(shí),Driver.getDriver(url)會(huì)通過(guò)DB連接url獲取到正確的驅(qū)動(dòng)并建立與DB的連接。