Java的Spi机制研究

        摘要:之前研究过dubbo、spring源码。然后发现大量使用一个叫做spi的东西,发现这确实是一个很好的实现方法,特别是在设计框架中。

一、SPI机制概念

      SPI的全称是Service Provider Interface。简单来说,SPI机制提供了一个表达接口和其具体实现类之间的绑定关系的方案。具体是在JAR包的"META-INF/services/"目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名。

      SPI可以理解是为接口寻找服务实现类。现在公司的系统都是进行了模块的划分,系统抽象为多个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。于是就有了SPI这种服务发现机制。

二、JDBC中的SPI机制

  在java中,Java.sql.Driver接口是Java对外公开的一个加载驱动接口,Java并未实现,至于实现这个接口由各个Jdbc厂商去实现就行了,好处是解藕,使得更具有灵活性,当然这也是面向对象的好处之一。真正的实现是不同提供商提供的。

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from Users");
Class.forName("com.mysql.jdbc.Driver");这里虽是加载mysql的driver,但是无论是oracle还是其它的jdbc驱动包,它们的原理都是spi机制。

首先看java.sql.driver

package java.sql;

import java.util.logging.Logger;
public interface Driver {
    boolean acceptsURL(String url) throws SQLException;

    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;

    int getMinorVersion();

    boolean jdbcCompliant();

    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

这个类是一个接口类, 在整个JDK包中都没有它的实现类,它的实现要由各个jdbc的开发产商去实现,但是我们发现DriverManager这个类中还是有去加载Driver类,那它是怎么发现其它开发商实现的Driver类?

再来看看DriverManager

这里将driver封成一个driverinfo对象,然后在DriverManager 这个类加载时就初始化一次,那初始化它做什么呢?其实就是其发现driver类的实现类,并加载到当前类中。注意看loadInitialDrivers方法。

public class DriverManager {


// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();

static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}


AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {


ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();

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);
}
}
}
...
}

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);这一句就是真实的发现driver类的实现类。

在mysql-connector-java-.jar包下面META-INF.services包下有个java.sql.Driver文件打开文件有下面两行




com.mysql.jdbc.Driver这样就实现了在我们程序运行时引入mysql-connector-java-.jar这个包。然后在JDK源码中,当我们调用到

DriverManager.getConnection的方法,就会将程序中使用到的Driver接口类用mysql驱动包的Driver实现类来使用。

这样做有什么好处?

1、不用在JDK里实现Driver实现类的硬编码,然后每次使用JDK里的DriverManager 类时,都会自动去发现Driver类的实现类,并根据这个实现类来做数据库连接。

2、可以满足不同的产商实现各不相同,但对外暴露一样的接口。使用方只要按照JDK的标准方法来调用即可,即实现了接口的可拔插。

三、使用实例

        看完介绍,不自己写一个实例总是理解不深刻,下面来看看笔者自己写的一个实例。

本文示例工程下载

1、接口工程


接口类:

package com.lin.service;

public interface Service {

	void say();

}

2、实现工程1


实现类:

package com.lin;

import com.lin.service.Service;

public class Service1 implements Service {

	public void say() {
		System.out.println("say hello from ServiceImpl1");
		
	}

}
引用接口工程:

	<dependencies>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiService</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>

并配置Spi:


3、实现工程2



实现类:

package com.lin;

import com.lin.service.Service;

public class Service2 implements Service {

	public void say() {
		System.out.println("say hello from ServiceImpl2");
		
	}

}
引用接口:

	<dependencies>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiService</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>

并配置spi:



4、使用工程

引入上面三个工程

  	<dependencies>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiService</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiServiceImpl1</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiServiceImpl2</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>

java代码:

package com.lin;

import java.util.ServiceLoader;

import com.lin.service.Service;

public class UseDemo {

	public static void main(String[] args) throws Exception {
		ServiceLoader<Service> services = ServiceLoader.load(Service.class);
		for (Service service : services) {
			service.say();
		}

	}

}
输出结果:

say hello from ServiceImpl1
say hello from ServiceImpl2

如果只引入实现1:

  	<dependencies>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiService</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiServiceImpl1</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>
那么就只会发现实现1,打印结果如下:

如果只引入接口包,那什么都不会打印。

  	<dependencies>
		<dependency>
			<groupId>com.lin</groupId>
			<artifactId>SpiService</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>

本文示例工程下载

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值