Java SPI 메커니즘 분석

13208 단어 자바
SPI 개요
SPI 는 모두(Service Provider Interface)라 고 부 르 며 JDK 에 내 장 된 서비스 제공 발견 메커니즘 입 니 다.주로 프레임 워 크 개발 자 들 이 사용 합 니 다.예 를 들 어 자바.sql.Driver 인터페이스,데이터베이스 업 체 가 이 인 터 페 이 스 를 실현 하면 됩 니 다.물론 시스템 에 구체 적 인 실현 클래스 의 존 재 를 알 리 려 면 고정된 저장 규칙 을 사용 해 야 합 니 다.classpath 의 META-INF/services/디 렉 터 리 에 서비스 인터페이스 이름 을 가 진 파일 을 만들어 야 합 니 다.이 파일 의 내용 은 바로 이 인터페이스의 구체 적 인 실현 류 이다.다음은 JDBC 를 실례 로 구체 적 인 분석 을 진행 하 겠 습 니 다.
JDBC 구동
1.드라이브 백 준비

            mysql
            mysql-connector-java
            5.1.47
        
        
            org.postgresql
            postgresql
            42.2.2
        
        
            com.microsoft.sqlserver
            mssql-jdbc
            7.0.0.jre8
        

각각 my sql,postgresql,sqlserver 를 준 비 했 습 니 다.jar 를 열 수 있 습 니 다.모든 jar 가방 의 META-INF/services/에 자바.sql.driver 파일 이 존재 합 니 다.파일 에 하나 이상 의 클래스 가 존재 합 니 다.예 를 들 어 my sql:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

제공 하 는 모든 구동 류 는 한 줄 을 차지 하고 분석 할 때 줄 에 따라 읽 으 며 구체 적 으로 사용 하 는 것 은 url 에 따라 결정 합 니 다.
2.간단 한 실례
String url = "jdbc:mysql://localhost:3306/db3";
String username = "root";
String password = "root";
String sql = "update travelrecord set name=\'bbb\' where id=1";
Connection con = DriverManager.getConnection(url, username, password);

클래스 경로 에 여러 개의 드라이버 패키지 가 존재 합 니 다.구체 적 으로 DriverManager.getConnection 을 사용 할 때 어떤 드라이버 를 사용 해 야 url 을 분석 하여 식별 할 수 있 는 지,서로 다른 데이터 베 이 스 는 서로 다른 url 접두사 가 있 습 니 다.
3.구동 류 로드 분석
구체 적 인 META-INF/services/의 구동 류 는 언제 불 러 왔 습 니까?DriverManager 는 정적 코드 블록 이 있 습 니 다.
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
 
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction() {
            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() {
        public Void run() {
 
            ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator driversIterator = loadedDrivers.iterator();
 
            /* 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 java.util.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(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);
        }
    }
}

DriverManager 클래스 를 불 러 올 때 loadInitialDrivers 방법 을 실행 합 니 다.방법 은 시스템 변수 방식 과 ServiceLoader 로 딩 방식 을 사용 하 는 두 가지 로 구동 류 를 불 러 옵 니 다.시스템 변수 방식 은 변수 jdbc.drivers 에 구동 류 를 설정 한 다음 에 Class.forName 을 사용 하여 불 러 오 는 것 입 니 다.다음은 ServiceLoader 방식 을 살 펴 보 겠 습 니 다.여기 서 load 방법 을 호출 했 지만 구동 류 를 진정 으로 불 러 오지 않 고 Lazy Iterator 를 되 돌려 주 었 습 니 다.뒤의 코드 는 순환 변수 교체 기 입 니 다.
private static final String PREFIX = "META-INF/services/";
 
private class LazyIterator
        implements Iterator
    {
 
        Class service;
        ClassLoader loader;
        Enumeration configs = null;
        Iterator pending = null;
        String nextName = null;
 
        private LazyIterator(Class service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
 
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    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;
        }
 
        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
        }
        ......
    }

클래스 에 정적 상수 PREFIX="META-INF/services/"를 지정 한 다음 자바.sql.Driver 와 연결 하여 fullName 을 구성 한 다음 클래스 로 더 를 통 해 모든 종류의 경 로 를 가 져 옵 니 다.자바.sql.Driver 파일 을 가 져 온 후 configs 에 저 장 됩 니 다.모든 요 소 는 하나의 파일 에 대응 합 니 다.파일 마다 여러 개의 드라이버 가 존재 할 수 있 습 니 다.따라서 모든 파일 에 있 는 드라이버 정 보 를 저장 하기 위해 pending 을 사용 합 니 다.드라이버 정 보 를 가 져 온 후 nextService 에서 Class.forName 로 클래스 정 보 를 불 러 오고 초기 화 하지 않 을 것 을 지정 합 니 다.동시에 아래 에서 new Instance 를 사용 하여 구동 류 를 예화 작업 하 였 습 니 다.모든 드라이버 클래스 에 정적 등록 코드 블록 을 제공 합 니 다.예 를 들 어 my sql:
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

여기에 드라이버 관리자 에 등록 하 는 드라이버 클래스 를 실례 화 했다.다음은 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");
   }

이 방법 은 주로 이전에 등 록 된 DriverInfo 를 옮 겨 다 니 며 url 정 보 를 가지 고 모든 구동 류 에 연결 을 만 드 는 것 입 니 다.물론 모든 구동 류 에서 url 일치 검 사 를 하고 성공 한 후에 Connection 으로 돌아 갑 니 다.중간 에 실패 한 연결 이 있 으 면 새로운 구동 연결 을 시도 하 는 데 영향 을 주지 않 습 니 다.옮 겨 다 니 고도 연결 을 가 져 올 수 없 으 면 이상 을 던 집 니 다.
4.확장
새로운 드라이버 클래스 를 확장 하려 면 클래스 경로 에서 META-INF/services/폴 더 를 만 드 는 동시에 자바.sql.driver 파일 을 만 들 고 파일 에 구체 적 인 드라이버 이름 을 기록 해 야 합 니 다.물론 자바.sql.driver 인터페이스 류 를 계승 해 야 합 니 다.예 를 들 어 인 스 턴 스 에서 제공 하 는 TestDriver.
직렬 화 실전
1.인터페이스 클래스 준비
public interface Serialization {
 
    /**
     *    
     * 
     * @param obj
     * @return
     */
    public byte[] serialize(Object obj) throws Exception;
 
    /**
     *     
     * 
     * @param param
     * @param clazz
     * @return
     * @throws Exception
     */
    public  T deserialize(byte[] param, Class clazz) throws Exception;
 
    /**
     *      
     * 
     * @return
     */
    public String getName();
 
}

2.실현 류 준비
각각 JSonSerialization 과 ProtobufSerialization 을 준비 합 니 다.
3.인터페이스 파일
META-INF/services/디 렉 터 리 에 파일 com.spi.serializer.Serialization 을 만 듭 니 다.내용 은 다음 과 같 습 니 다.
com.spi.serializer.JsonSerialization
com.spi.serializer.ProtobufSerialization

4.관리자 클래스 제공
public class SerializationManager {
 
    private static Map map = new HashMap<>();
 
    static {
        loadInitialSerializer();
    }
 
    private static void loadInitialSerializer() {
        ServiceLoader loadedSerializations = ServiceLoader.load(Serialization.class);
        Iterator iterator = loadedSerializations.iterator();
 
        try {
            while (iterator.hasNext()) {
                Serialization serialization = iterator.next();
                map.put(serialization.getName(), serialization);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
 
    public static Serialization getSerialization(String name) {
        return map.get(name);
    }
}

DriverManager 와 유사 한 SerializationManager 클래스 를 제공 합 니 다.클래스 를 불 러 올 때 모든 설정 의 직렬 화 방식 을 불 러 옵 니 다.getSerialization 의 오늘 방법 을 제공 합 니 다.getConnection 과 유사 합 니 다.
총결산
본 고 는 JDBC 구동 을 사례 로 하여 ServiceLoader 방식 의 서비스 발견 을 분석 하 는 데 중심 을 두 고 직렬 화 된 간단 한 실전 을 제공 했다.dubbo 도 비슷 한 SPI 방식 을 제공 했다.핵심 클래스 는 ExtensionLoader 로 자바 가 공식 적 으로 제공 하 는 ServiceLoader 보다 기능 이 강하 다.후속 적 으로 dubbo 의 SPI 방식 을 분석 한 다음 에 대 비 를 한다.
예제 코드 주소
https://github.com/ksfzhaohui...https://gitee.com/OutOfMemory...

좋은 웹페이지 즐겨찾기