저자: Jingdong Logistics Kong Xiangdong
1. SPI란?
SPI의 전체 이름은 서비스 인터페이스를 제공하는 서비스 공급자 인터페이스이며 서비스 검색 메커니즘입니다.SPI의 본질은 파일에서 인터페이스 구현 클래스의 전체 이름을 구성하고 서비스 로더는 구성을 읽습니다. 파일을 만들고 구현 클래스를 로드합니다. . 이러한 방식으로 구현 클래스는 런타임 시 인터페이스에 대해 동적으로 대체될 수 있습니다. 이 기능으로 인해 SPI 메커니즘을 통해 프로그램에 확장 기능을 쉽게 제공할 수 있습니다.
아래 그림과 같이:
시스템 설계의 각 추상화에는 종종 다양한 구현 체계가 있습니다.객체 지향 설계에서는 일반적으로 인터페이스를 기반으로 모듈 간 프로그래밍을 권장하며 모듈 구현은 하드 코딩되지 않습니다.코드에 특정 구현 클래스가 포함되면 플러그형을 위반합니다. 당기는 원리. Java SPI는 특정 인터페이스에 대한 서비스 구현을 찾는 메커니즘을 제공하는데, 이는 모듈화에서 특히 중요한 어셈블리 제어를 프로그램 외부로 이동시키는 IOC의 아이디어와 다소 유사합니다. SPI는 자바에서 제공하는 서비스 디스커버리 메커니즘이라기보다는 디커플링 아이디어라고 말하는 편이 낫다.
2. 사용 시나리오?
- 데이터베이스 드라이버 로딩 인터페이스는 다음과 같은 클래스 로딩을 실현합니다: JDBC는 Mysql, Oracle을 로드합니다...
- 로그 파사드 인터페이스는 다음과 같은 클래스 로딩을 구현합니다. SLF4J는 log4j 및 logback을 지원합니다.
- SPI는 Spring, 특히 spring-boot에서 자동 구성 구현에 널리 사용됩니다.
- Dubbo는 또한 프레임워크의 확장을 구현하기 위해 많은 SPI를 사용하며, 원래 SPI를 캡슐화하고 사용자가 필터 인터페이스를 확장하고 구현할 수 있도록 합니다.
3. 사용 소개
Java SPI를 사용하려면 다음 규칙을 따라야 합니다.
- 서비스 공급자가 인터페이스의 특정 구현을 제공하는 경우 JAR 패키지의 META-INF/services 디렉토리에 "인터페이스 정규화된 이름"이라는 파일을 생성해야 하며 콘텐츠는 구현의 정규화된 이름입니다. 수업;
- 인터페이스 구현 클래스가 있는 JAR은 기본 프로그램의 클래스 경로 아래에 배치됩니다. 즉, 종속성이 도입됩니다.
- 기본 프로그램은 java.util.ServiceLoder를 통해 구현 모듈을 동적으로 로드합니다. META-INF/services 디렉토리의 파일을 스캔하여 구현 클래스의 정규화된 이름을 찾고 클래스를 JVM에 로드한 다음 인스턴스화합니다.
- SPI의 구현 클래스는 매개변수가 없는 생성자를 전달해야 합니다.
예:
spi 인터페이스 모듈 정의
定义一组接口:public interface MyDriver
spi-jd-드라이버
스파이 알리 드라이버
实现为:public class JdDriver implements MyDriver
public class AliDriver implements MyDriver
src/main/resources/ 아래에 /META-INF/services 디렉토리를 만들고 인터페이스 이름을 딴 파일(org.MyDriver 파일)을 추가합니다.
내용은 com.jd.JdDriver와 com.ali.AliDriver 각각 적용할 구현 클래스이다.
스피 코어
일반적으로 구현 클래스를 로드하고 사용하는 전략 등을 포함하여 플랫폼에서 제공하는 핵심 패키지입니다. , 별도로 인쇄
public void invoker(){
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
boolean isNotFound = true;
while (drivers.hasNext()){
isNotFound = false;
drivers.next().load();
}
if(isNotFound){
throw new RuntimeException("一个驱动实现类都不存在");
}
}
스파이 테스트
public class App
{
public static void main( String[] args )
{
DriverFactory factory = new DriverFactory();
factory.invoker();
}
}
1. spi-core 패키지 도입 및 결과 실행
2. spi-core, spi-jd-driver 패키지 소개
3. 인입 spi-core, spi-jd-driver, spi-ali-driver
4. 원리 분석
지금 우리가 어떻게 특정 구현 클래스를 얻었는지 보십니까?
단 두 줄의 코드:
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
따라서 먼저 ServiceLoader 클래스를 살펴봅니다.
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器,真正加载服务的类
private LazyIterator lookupIterator;
//服务提供者查找的迭代器
private class LazyIterator
implements Iterator<S>
{
.....
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//全限定名:com.xxxx.xxx
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//通过反射获取
c = Class.forName(cn, false, loader);
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
}
........
대략적인 프로세스는 다음과 같습니다.
-
애플리케이션이 ServiceLoader.load 메서드를 호출합니다.
-
응용 프로그램이 반복자를 통해 개체 인스턴스를 얻을 때 먼저 공급자 개체에 캐시된 예제 개체가 있는지 여부를 확인하고 존재하는 경우 직접 반환합니다.
-
존재하지 않는 경우 클래스 재인쇄를 수행하고 META-INF/services 아래의 구성 파일을 읽어서 인스턴스화할 수 있는 모든 클래스의 이름을 얻고 구성 파일은 JAR을 통해 얻을 수 있습니다. .forName() 및 use Instance() 인스턴스화된 클래스 메서드는 공급자 개체의 인스턴스화된 클래스를 캐시하고 동기식으로 반환합니다.
5. 요약
장점: 디커플링
SPI를 사용하면 호출자의 비즈니스 코드에서 타사 서비스 모듈의 어셈블리 제어 논리를 분리하고 함께 결합하지 않습니다.응용 프로그램은 실제 비즈니스 조건에 따라 프레임워크 확장을 활성화하고 프레임워크 구성 요소를 교체할 수 있습니다.
SPI를 사용하면 다음과 같은 방법으로 구현 클래스를 얻을 필요가 없습니다.
-
코드 하드코딩 가져오기 가져오기
-
JDBC4.0 이전과 같이 리플렉션 획득을 위한 클래스의 정규화된 이름을 지정합니다. Class.forName("com.mysql.jdbc.Driver")
결점:
ServiceLoader는 지연 로딩으로 간주될 수 있지만 순회를 통해서만 얻을 수 있습니다. 즉, 인터페이스의 모든 구현 클래스가 한 번 로드되고 인스턴스화됩니다. 일부 구현 클래스를 사용하지 않으려는 경우 로드되고 인스턴스화되어 낭비가 발생합니다. 특정 구현 클래스를 얻는 방법은 충분히 유연하지 않고 Iterator 형식으로만 얻을 수 있으며 특정 매개 변수에 따라 해당 구현 클래스를 얻을 수 없습니다.