기술나눔

DangerWind-RPC-프레임워크---4.

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

SPI는 Service Provider Interface의 약자로, 프레임워크 기능을 확장하는 서비스 공급자나 개발자에게 특별히 제공되는 인터페이스로 이해될 수 있습니다. SPI는 서비스 인터페이스를 특정 서비스 구현에서 분리하고 서비스 호출자와 서비스 구현자를 분리하며 프로그램의 확장성과 유지 관리성을 향상시킬 수 있습니다. 서비스 구현을 수정하거나 교체하는 경우 호출자를 수정할 필요가 없습니다. 많은 프레임워크는 Spring 프레임워크, 데이터베이스 로딩 드라이버, 로그 인터페이스, Dubbo 확장 구현 등과 같은 Java의 SPI 메커니즘을 사용합니다. 이 RPC 프레임워크의 SPI 부분을 구현하려면 Dubbo의 SPI 메커니즘을 참조하세요.

예를 들어 클라이언트가 서버와 통신할 때 메시지를 직렬화해야 합니다. Hessian, Kryo 및 ProtoStuff를 포함하여 직렬화 중에 사용할 수 있는 많은 직렬화 알고리즘이 있습니다. 시스템 요구 사항은 메시지의 직렬화 알고리즘 이름을 기반으로 해당 직렬화 알고리즘에 해당하는 클래스의 메서드를 호출하여 직렬화 및 역직렬화를 수행하는 것입니다. 또한 확장을 용이하게 하려면 디커플링에 SPI를 사용해야 합니다. .

SPI는 다음과 같이 사용됩니다.

  1. Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
  2. .getExtension(codecName);

codecName은 직렬화 알고리즘의 이름이며, 이 이름에 따라 해당 클래스를 로드해야 합니다.

  1. private final Class<?> type;
  2. private ExtensionLoader(Class<?> type) {
  3. this.type = type;
  4. }
  5. // 每个SPI接口都有自身的ExtensionLoader
  6. public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {
  7. if (type == null) {
  8. throw new IllegalArgumentException("Extension type should not be null.");
  9. }
  10. if (!type.isInterface()) {
  11. throw new IllegalArgumentException("Extension type must be an interface.");
  12. }
  13. if (type.getAnnotation(SPI.class) == null) {
  14. throw new IllegalArgumentException("Extension type must be annotated by @SPI");
  15. }
  16. // firstly get from cache, if not hit, create one
  17. ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
  18. if (extensionLoader == null) {
  19. EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
  20. extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
  21. }
  22. return extensionLoader;
  23. }

각 SPI 인터페이스에는 자체 ExtensionLoader가 있습니다. getExtensionLoader를 호출하면 먼저 일련의 적법성 검사 작업을 수행한 다음 인터페이스의 ExtensionLoader를 얻으려고 시도합니다. 그렇지 않으면 로컬 캐시 CHM에서 얻으려고 합니다. 얻은 후 Loader 개체를 만듭니다.

이후 getExtension을 통해 인스턴스를 획득하고, 로컬에서도 인스턴스를 캐시합니다. 캐시에 없으면 인스턴스를 다시 생성합니다.

  1. private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
  2. public T getExtension(String name) {
  3. if (StringUtil.isBlank(name)) {
  4. throw new IllegalArgumentException("Extension name should not be null or empty.");
  5. }
  6. // firstly get from cache, if not hit, create one
  7. // 缓存holder
  8. Holder<Object> holder = cachedInstances.get(name);
  9. if (holder == null) {
  10. cachedInstances.putIfAbsent(name, new Holder<>());
  11. holder = cachedInstances.get(name);
  12. }
  13. // create a singleton if no instance exists
  14. // holder为空,双重检查锁创建示例
  15. Object instance = holder.get();
  16. if (instance == null) {
  17. synchronized (holder) {
  18. instance = holder.get();
  19. if (instance == null) {
  20. instance = createExtension(name);
  21. holder.set(instance);
  22. }
  23. }
  24. }
  25. return (T) instance;
  26. }

클래스의 Class 객체를 얻은 후 리플렉션을 통해 이 객체를 생성할 수 있습니다.

  1. // 缓存
  2. private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
  3. private T createExtension(String name) {
  4. // load all extension classes of type T from file and get specific one by name
  5. // SPI接口对应的实现类,其标识名与class文件的映射,根据标识名获取class
  6. Class<?> clazz = getExtensionClasses().get(name);
  7. if (clazz == null) {
  8. throw new RuntimeException("No such extension of name " + name);
  9. }
  10. T instance = (T) EXTENSION_INSTANCES.get(clazz);
  11. if (instance == null) {
  12. try {
  13. // 缓存中不存在,则创建实例
  14. EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  15. instance = (T) EXTENSION_INSTANCES.get(clazz);
  16. } catch (Exception e) {
  17. log.error(e.getMessage());
  18. }
  19. }
  20. return instance;
  21. }

핵심은 Class 객체, 즉 getExtensionCalsses 메소드를 획득하는 프로세스입니다.

  1. // 该SPI接口所有实现类的标识与其Class对象的缓存
  2. private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
  3. private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
  4. private Map<String, Class<?>> getExtensionClasses() {
  5. // get the loaded extension class from the cache
  6. // 根据Interface实现类的类名获取对应类的缓存
  7. Map<String, Class<?>> classes = cachedClasses.get();
  8. // double check
  9. if (classes == null) {
  10. synchronized (cachedClasses) {
  11. classes = cachedClasses.get();
  12. if (classes == null) {
  13. classes = new HashMap<>();
  14. // load all extensions from our extensions directory
  15. loadDirectory(classes);
  16. // 将Map集合存储在Holder中进行缓存
  17. cachedClasses.set(classes);
  18. }
  19. }
  20. }
  21. return classes;
  22. }
  23. private void loadDirectory(Map<String, Class<?>> extensionClasses) {
  24. // 固定路径下的文件,SPI接口的类名作为文件名,在此文件中规定需要加载的实现类
  25. String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
  26. try {
  27. Enumeration<URL> urls;
  28. // 系统类加载器,它能够加载用户类路径(ClassPath)上的类和资源。对于SPI机制尤为重要,因为SPI的实现类通常是由应用程序提供并放置在应用程序的类路径下的
  29. ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
  30. // 获取当前类加载器加载的URL资源,文件名确定一般urls是唯一的
  31. urls = classLoader.getResources(fileName);
  32. if (urls != null) {
  33. while (urls.hasMoreElements()) {
  34. URL resourceUrl = urls.nextElement();
  35. // 使用classLoader加载资源,资源目标在resourceUrl下,加载后的class存储在extensionClasses Map集合当中
  36. loadResource(extensionClasses, classLoader, resourceUrl);
  37. }
  38. }
  39. } catch (IOException e) {
  40. log.error(e.getMessage());
  41. }
  42. }
  43. private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
  44. try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
  45. String line;
  46. // read every line
  47. // #是注释,截取注释之前的部分
  48. while ((line = reader.readLine()) != null) {
  49. // get index of comment
  50. final int ci = line.indexOf('#');
  51. if (ci >= 0) {
  52. // string after # is comment so we ignore it
  53. line = line.substring(0, ci);
  54. }
  55. line = line.trim();
  56. if (line.length() > 0) {
  57. try {
  58. final int ei = line.indexOf('=');
  59. // 标识与类名
  60. String name = line.substring(0, ei).trim();
  61. String clazzName = line.substring(ei + 1).trim();
  62. // our SPI use key-value pair so both of them must not be empty
  63. if (name.length() > 0 && clazzName.length() > 0) {
  64. // 加载类
  65. Class<?> clazz = classLoader.loadClass(clazzName);
  66. // 在map中保存
  67. extensionClasses.put(name, clazz);
  68. }
  69. } catch (ClassNotFoundException e) {
  70. log.error(e.getMessage());
  71. }
  72. }
  73. }
  74. } catch (IOException e) {
  75. log.error(e.getMessage());
  76. }
  77. }
  1. kyro=github.javaguide.serialize.kyro.KryoSerializer
  2. protostuff=github.javaguide.serialize.protostuff.ProtostuffSerializer
  3. hessian=github.javaguide.serialize.hessian.HessianSerializer

레이어별 메소드 호출은 META-INF/extensions/ 경로 아래에 해당 SPI 구성 파일을 로드하여 클래스 객체를 로드하고 인스턴스를 얻는 프로세스를 구현합니다. 중요한 부분은 주석을 참조하세요.

META-INF/extensions/ 아래의 파일 이름은 코드와 일치해야 합니다. 코드에 지정된 파일 이름은 SPI 인터페이스 클래스의 전체 클래스 이름입니다. 파일의 내용도 (구현 클래스 식별자 = 구현 클래스의 전체 클래스 이름)에 따라 작성해야 코드와 일치할 수 있으며 프로그램은 파일을 올바르게 구문 분석하고 클래스 로더를 사용하여 다음을 수행할 수 있습니다. 해당 클래스를 로드합니다. 마지막으로 &lt;구현 클래스 식별자, 구현 클래스 Class 객체&gt;에 따라 캐시합니다.

세 가지 캐시(클래스 식별자, 클래스 객체), (클래스 객체, 객체 인스턴스), (클래스 식별자, 객체 인스턴스)가 존재하므로 식별자를 직접 전달하여 해당 클래스의 인스턴스를 얻을 수 있습니다. 또한 RPC 프레임워크 성능을 최적화합니다.