Compartilhamento de tecnologia

Estrutura DangerWind-RPC --- 4.

2024-07-12

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

SPI significa Service Provider Interface, que pode ser entendida como uma interface fornecida especificamente para provedores de serviços ou desenvolvedores que estendem funções de framework. O SPI separa a interface de serviço da implementação de serviço específica, separa o chamador do serviço e o implementador do serviço e pode melhorar a escalabilidade e a capacidade de manutenção do programa. Modificar ou substituir uma implementação de serviço não requer a modificação do chamador. Muitas estruturas usam o mecanismo SPI do Java, como: estrutura Spring, driver de carregamento de banco de dados, interface de log e implementação de extensão Dubbo, etc. Consulte o mecanismo SPI do Dubbo para implementar a parte SPI desta estrutura RPC.

Por exemplo, quando o cliente se comunica com o servidor, ele precisa serializar a mensagem. Existem muitos algoritmos de serialização que podem ser usados ​​durante a serialização, incluindo Hessian, Kryo e ProtoStuff. O requisito do sistema é chamar o método na classe correspondente ao algoritmo de serialização relevante com base no nome do algoritmo de serialização na mensagem para realizar a serialização e desserialização. Além disso, para facilitar a expansão, o SPI precisa ser usado para desacoplamento. .

SPI é usado da seguinte forma:

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

codecName é o nome do algoritmo de serialização e a classe correspondente precisa ser carregada de acordo com este nome.

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

Cada interface SPI tem seu próprio ExtensionLoader. Ao chamar getExtensionLoader, ele primeiro executará uma série de operações de verificação legal e, em seguida, tentará obter o ExtensionLoader da interface. Primeiro, tente obtê-lo do cache local CHM. obtido, crie um objeto Loader.

Depois disso, a instância é obtida por meio de getExtension e também é armazenada em cache localmente. Se não estiver no cache, crie a instância novamente.

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

Depois de obter o objeto Class da classe, você pode criar esse objeto por meio de reflexão.

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

A chave é o processo de obtenção do objeto Class, ou seja, o método 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

A chamada do método camada por camada realiza o processo de carregamento do arquivo de configuração SPI correspondente no caminho META-INF/extensions/ para carregar o objeto Class e obter a instância. Consulte os comentários para partes importantes.

Deve-se observar que o nome do arquivo em META-INF/extensions/ precisa ser consistente com o código. O nome do arquivo especificado no código é o nome completo da classe da interface SPI. O conteúdo do arquivo também precisa ser escrito de acordo com (identificador da classe de implementação = nome completo da classe de implementação), para que possa ser consistente com o código, e o programa possa analisar corretamente o arquivo e usar o carregador de classes para carregue a classe correspondente. Finalmente, armazene em cache de acordo com &lt;identificador de classe de implementação, objeto de classe de implementação&gt;.

Devido à existência dos três caches (identificador de classe, objeto de classe), (objeto de classe, instância de objeto) e (identificador de classe, instância de objeto), você pode passar diretamente o identificador para obter a instância da classe correspondente, que também otimiza o desempenho da estrutura RPC.