Carregador de classes, delegação pai, mecanismo SPI em Java

Referência:
Modelo de delegação parental Java: Por que delegação parental? Como quebrar isso? Onde está quebrado?
[Código Pipixia] leva você a fazer um balanço do mecanismo de delegação parental [princípios, vantagens e desvantagens] e como quebrá-lo?
Entre os três mecanismos SPI do JDK/Dubbo/Spring, qual é o melhor?


Prefácio

Raramente entro em contato com carregadores de classe ao desenvolver negócios, mas se você quiser aprender mais sobre projetos de código aberto como Tomcat e Spring, ou se envolver no desenvolvimento de arquitetura subjacente, é essencial entender ou até mesmo estar familiarizado com o princípios de carregamento de classe.

Quais são os carregadores de classe para Java? O que é delegação parental? Por que delegação de pais? Como quebrar isso? Tenho algum entendimento desses conceitos e até memorizei esses pontos de conhecimento para entrevistas, mas quando entro em mais detalhes sei muito pouco.


1. Carregador de classe

O carregador de classes, como o nome sugere, é uma ferramenta que pode carregar bytecode Java em instâncias java.lang.Class. Este processo inclui leitura de matrizes de bytes, verificação, análise, inicialização, etc. Além disso, também pode carregar recursos, incluindo arquivos de imagem e arquivos de configuração.

Recursos do carregador de classes:

  • O carregamento dinâmico não precisa ser carregado quando o programa começa a ser executado. Em vez disso, ele é carregado dinamicamente sob demanda enquanto o programa está em execução. Existem muitas fontes de bytecode, como jars compactados, guerras, rede, arquivos locais, etc. O recurso de carregamento dinâmico do carregador de classes fornece forte suporte para implantação e carregamento dinâmicos.
  • Totalmente responsável, quando um carregador de classes carrega uma classe, todas as outras classes das quais esta classe depende e referências são carregadas por este carregador de classes, a menos que outro carregador de classes seja explicitamente especificado no programa para carregar. Portanto, quebrar a delegação pai não pode quebrar a ordem acima do carregador de classes de extensão.

A exclusividade de uma classe é determinada pelo carregador de classes que a carrega e pela própria classe (o nome completo da classe + o ID da instância do carregador de classes como o identificador exclusivo). Comparar se duas classes são iguais (incluindo objetos de classe ​equals(), ​​isAssignableFrom()e ​​isInstance()​​​palavras ​​instanceof​​-chave, etc.) só é significativo se as duas classes forem carregadas pelo mesmo carregador de classes. Caso contrário, mesmo que essas duas classes sejam originadas do mesmo arquivo de classe e sejam carregadas por a mesma máquina virtual. Desde que os carregadores de classe que os carregam sejam diferentes, as duas classes não devem ser iguais.

Em termos de implementação, os carregadores de classes podem ser divididos em dois tipos: um é o carregador de classes de inicialização , que é implementado na linguagem C++ e faz parte da própria máquina virtual; o outro é o carregador de classes java.lang.ClassLoader​​herdado , incluindo carregadores de classes de extensão , carregadores de classes de aplicativos e carregadores de classes customizados .

  • Bootstrap ClassLoader (Bootstrap ClassLoader): Responsável por carregar o caminho ​​<JAVA_HOME>\libno diretório ou ​​-Xbootclasspathcaminho especificado pelo parâmetro, e é reconhecido pela máquina virtual (reconhecido apenas pelo nome do arquivo, como rt.jar , uma biblioteca de classes com um inconsistente nome não será carregado mesmo se for colocado no diretório lib) A biblioteca de classes é carregada na memória da máquina virtual. O carregador de classes de inicialização não pode ser referenciado diretamente por programas Java. Quando os usuários escrevem um carregador de classes personalizado, se quiserem definir o Bootstrap ClassLoader como seu pai, eles podem definir nulo diretamente.

  • Extensão ClassLoader: Responsável por carregar todas as bibliotecas de classes ​​<JAVA_HOME>\lib\ext​​​no diretório ou java.ext.dirsno caminho especificado pela variável do sistema. Este carregador de classes é ​​sun.misc.Launcher$ExtClassLoader​​​implementado por. O carregador de classes de extensão é carregado pelo carregador de classes de inicialização e seu carregador de classes pai é o carregador de classes de inicialização, ou seja, parent=null.

  • Aplicação ClassLoader: Responsável por carregar a biblioteca de classes especificada no caminho de classe do usuário (ClassPath), ​​sun.misc.Launcher$App-ClassLoaderimplementada pelo . Os desenvolvedores podem obter diretamente o carregador de classes do aplicativo por meio do método ​​java.lang.ClassLoader​​​em , portanto, ele também pode ser chamado de carregador de classes do sistema. ​​getSystemClassLoader()​​O carregador de classes do aplicativo também é carregado pelo carregador de classes de inicialização, mas seu carregador de classes pai é o carregador de classes de extensão. Em um aplicativo, o carregador de classes do sistema geralmente é o carregador de classes padrão.


2. Mecanismo de delegação parental

1. Introdução ao mecanismo de delegação parental

A JVM não carrega todos os arquivos .class quando é iniciada, mas apenas carrega a classe quando o programa a utiliza durante a execução . Exceto o carregador de classes de inicialização, todos os outros carregadores de classes precisam herdar a classe abstrata ClassLoader. Essa classe abstrata define três métodos principais. É muito importante entender suas funções e relacionamentos.

public abstract class ClassLoader {
    
    
    //每个类加载器都有个父加载器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) {
    
    
        //查找一下这个类是不是已经加载过了
        Class<?> c = findLoadedClass(name);

        //如果没有加载过
        if (c == null) {
    
    
          //先委派给父加载器去加载,注意这是个递归调用
          if (parent != null) {
    
    
              c = parent.loadClass(name);
          } else {
    
    
              // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加载器没加载成功,调用自己的findClass去加载
        if (c == null) {
    
    
            c = findClass(name);
        }

        return c;
    }

    protected Class<?> findClass(String name){
    
    
        // 1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
        // ...

        // 2. 调用defineClass将字节数组转成Class对象
        return defineClass(buf, off, len)}

    // 将字节码数组解析成一个Class对象,用native方法实现
    protected final Class<?> defineClass(byte[] b, int off, int len){
    
    
        // ...
    }
}

Várias informações importantes podem ser obtidas no código acima:

  1. Os carregadores de classes JVM são hierárquicos e têm um relacionamento pai-filho. Esse relacionamento não é herança e manutenção, mas combinação. Cada carregador de classes contém um campo pai que aponta para o carregador pai.
  2. defineClass​A responsabilidade do método é chamar o método nativo para analisar o bytecode da classe Java em um objeto Class.
  3. findClass​​​A principal responsabilidade do método é encontrar o arquivo .class e ler o arquivo .class na memória para obter a matriz de bytecode e, em seguida, chamar defineClasso método para obter o objeto Class. As subclasses devem ser implementadas findClass.
  4. loadClass​​A principal responsabilidade do método é implementar o mecanismo de delegação pai: primeiro verifique se esta classe foi carregada e retorne diretamente se tiver sido carregada, caso contrário é delegada ao carregador pai para carregamento. Esta é uma chamada recursiva, delegada camada por camada ascendente.Quando o carregador de classe de nível superior (carregador de classe de inicialização) não pode carregar a classe, ele é delegado aos carregadores de subclasse camada por camada .

Insira a descrição da imagem aqui

2. O papel do mecanismo de delegação parental

A delegação parental garante que os carregadores de classes, a delegação ascendente e o carregamento descendente garantam que cada classe seja a mesma classe em cada carregador de classes.

Um propósito muito óbvio: garantir a segurança do carregamento de ​​java​​​bibliotecas de classes oficiais ​​<JAVA_HOME>\lib​​​e bibliotecas de classes estendidas ​​<JAVA_HOME>\lib\ext​​e não ser sobrescritas pelos desenvolvedores. Por exemplo ​​java.lang.Object​​​, uma classe é armazenada nele ​​rt.jar​​. Não importa qual carregador de classes queira carregar esta classe, ela eventualmente será delegada ao carregador de classes de inicialização para carregamento. Portanto, a classe Object é a mesma classe em vários ambientes do carregador de classes do programa.

Se os desenvolvedores desenvolverem sua própria estrutura de código aberto, eles também poderão personalizar o carregador de classes e usar o modelo de delegação pai para proteger as classes que precisam ser carregadas por sua própria estrutura de serem substituídas pelo aplicativo.

Suas vantagens são resumidas da seguinte forma:

  1. Evite carregamento repetido de aulas
  2. Proteja a segurança do programa e evite que as APIs principais sejam adulteradas à vontade

Desvantagens : Em alguns cenários, o sistema de delegação parental é muito restritivo, por isso às vezes é necessário quebrar o mecanismo de delegação parental para atingir o objetivo. Por exemplo: mecanismo SPI

3. Limitações da delegação parental e carregadores de classes de contexto de thread

Considere o JDBC para criar uma conexão de banco de dados como exemplo para apresentar as limitações do mecanismo de delegação pai:

Ao executar o código a seguir para criar uma conexão, DriverManagera classe precisa ser inicializada

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "root");

Ao inicializar DriverManageruma classe, a seguinte linha de código será executada para carregar classpathtodas as seguintes Driverclasses de implementação que implementam a interface:

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

Aí vem o problema: a classe
localizada rt.jarabaixo java.sql.DriverManageré carregada pelo carregador de classes de inicialização, então quando o código acima for encontrado durante o carregamento, ele tentará carregar todas as classes de implementação do driver (SPI), mas essas classes de implementação são basicamente de terceiros Desde que classes de terceiros não possam ser carregadas pelo carregador de classes de inicialização.

Como resolver este problema? JDBC quebra o princípio da delegação parental ao usar o carregador de classes do aplicativo
introduzindo (carregador de contexto de thread, que é AppClassLoader por padrão).ThreadContextClassLoaderAppClassLoader

ServiceLoader.load()concluir:

public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

O carregador de classes de contexto de thread é na verdade um mecanismo de entrega do carregador de classes. Você pode definir um carregador de classe de contexto para um thread por meio do método java.lang.Thread#setContextClassLoader. Este carregador de classe pode ser recuperado durante a execução subsequente do thread (java.lang.Thread#getContextClassLoader) ​) para uso.

Se o carregador de classes de contexto não for definido ao criar um thread, ele será obtido do thread pai (parent = currentThread()).Se não for definido no escopo global do aplicativo, o carregador de classes do aplicativo será carregado por padrão.dispositivo.

O carregador de classes de contexto de thread parece facilitar a destruição da delegação parental:

Um exemplo típico é o serviço JNDI. JNDI agora é um serviço padrão em Java. Seu código é carregado pelo carregador de classes de inicialização (rt.jar colocado no JDK 1.3), mas o objetivo do JNDI é centralizar recursos. Para gerenciamento e pesquisa , ele precisa chamar o código do provedor de interface JNDI (SPI, Service Provider Interface) implementado por um fornecedor independente e implantado no ClassPath do aplicativo, mas é impossível para o carregador de classes de inicialização carregar classes no ClassPath.

Mas com o carregador de classes de contexto de thread, é mais fácil de manusear. O serviço JNDI usa o carregador de classes de contexto de thread para carregar o código SPI necessário. Ou seja, o carregador de classes pai solicita ao carregador de classes filho para concluir a ação de carregamento de classe. Isso comportamento Na verdade, ele abre a estrutura hierárquica do modelo de delegação parental para usar o carregador de classes inversamente. Na verdade, viola os princípios gerais do modelo de delegação parental, mas não há nada que você possa fazer a respeito.

Todas as ações de carregamento envolvendo SPI em Java utilizam basicamente este método, como JNDI, JDBC, JCE, JAXB e JBI, etc.

Extraído de "Compreensão aprofundada da máquina virtual Java" Zhou Zhiming

Exemplo de Tomcat quebrando o mecanismo de delegação parental:

O Tomcat é um contêiner da web, portanto, um contêiner da web pode precisar implantar vários aplicativos. Aplicativos diferentes podem contar com versões diferentes da mesma biblioteca de classes de terceiros, mas o nome do caminho completo de uma classe em versões diferentes da biblioteca de classes pode ser o mesmo.

Se o mecanismo padrão de carregamento de classe delegado pelo pai for usado, várias classes idênticas não poderão ser carregadas. Portanto, o Tomcat destrói o princípio da delegação parental, fornece um mecanismo de isolamento e fornece um carregador WebAppClassLoader separado para cada contêiner da web.

Mecanismo de carregamento de classes do Tomcat: Para obter o isolamento, as classes definidas pela aplicação Web são carregadas primeiro. Portanto, o acordo de delegação pai não é seguido. Cada aplicação possui seu próprio carregador de classes - WebAppClassLoader, que é responsável por carregar os arquivos de classe em seu próprio diretório.Quando chegar a hora, ele será entregue ao CommonClassLoader para carregamento, que é exatamente o oposto da delegação parental.

4. Como o mecanismo de delegação parental é quebrado

Existem duas maneiras pelas quais o mecanismo de delegação parental é quebrado:

  1. Carregador de classes de contexto de thread. Use ThreadContextClassLoader para carregar classes que não podem ser carregadas pelo carregador de classes superior (mencionado na criação JDBC da conexão de banco de dados acima)
  2. Carregador de classes personalizado. Vamos apresentar este método para destruir o mecanismo de delegação parental.

Se quiser personalizar o carregador de classes, você precisa herdar o ClassLoader e reescrevê-lo ​​findClass​​​. Se quiser destruir a ordem de carregamento de classes delegada pelos pais, você precisa reescrevê-lo​​loadClass​​​ . Aqui está um carregador de classes personalizado que substitui loadClass para quebrar a delegação parental:

package co.dreampointer.test.classloader;

import java.io.*;

public class MyClassLoader extends ClassLoader {
    
    
    public MyClassLoader(ClassLoader parent) {
    
    
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    
    
        // 1.找到ExtClassLoader,并首先委派给它加载
        // 为什么?
        // 双亲委派的破坏只能发生在"AppClassLoader"及其以下的加载委派顺序,"ExtClassLoader"上面的双亲委派是不能破坏的!
        // 因为任何类都是继承自超类"java.lang.Object",而加载一个类时,也会加载继承的类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。
        ClassLoader classLoader = getSystemClassLoader();
        while (classLoader.getParent() != null) {
    
    
            classLoader = classLoader.getParent();
        }
        Class<?> clazz = null;
        try {
    
    
            clazz = classLoader.loadClass(name);
        } catch (ClassNotFoundException ignored) {
    
    
        }
        if (clazz != null) {
    
    
            return clazz;
        }

        // 2.自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
    
    
            return clazz;
        }

        // 3.自己加载不了,再调用父类loadClass,保持双亲委派模式
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        // 1.获取class文件二进制字节数组
        byte[] data;
        try {
    
    
            ByteArrayOutputStream baOS = new ByteArrayOutputStream();
            // 加载class文件内容到字节数组
            FileInputStream fIS = new FileInputStream("test/target/classes/co/dreampointer/test/classloader/MyTarget.class");
            byte[] bytes = new byte[1024];

            int len;
            while ((len = fIS.read(bytes)) != -1) {
    
    
                baOS.write(bytes, 0, len);
            }
            data = baOS.toByteArray();
        } catch (IOException e) {
    
    
            e.printStackTrace();
            return null;
        }

        // 2.字节码数组加载到 JVM 的方法区,
        // 并在 JVM 的堆区建立一个java.lang.Class对象的实例
        // 用来封装 Java 类相关的数据和方法
        return this.defineClass(name, data, 0, data.length);
    }
}

Insira a descrição da imagem aqui

programa de teste:

package co.dreampointer.test.classloader;

public class Main {
    
    
    public static void main(String[] args) throws ClassNotFoundException {
    
    
        // 初始化MyClassLoader
        // 将加载MyClassLoader类的类加载器设置为MyClassLoader的parent
        MyClassLoader myClassLoader = new MyClassLoader(MyClassLoader.class.getClassLoader());

        System.out.println("MyClassLoader的父类加载器:" + myClassLoader.getParent());
        // 加载 MyTarget
        Class<MyTarget> clazz = (Class<MyTarget>) myClassLoader.loadClass("co.dreampointer.test.classloader.MyTarget");
        System.out.println("MyTarget的类加载器:" + clazz.getClassLoader());
    }
}

//控制台打印
MyClassLoader的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
MyTarget的类加载器:co.dreampointer.test.classloader.MyClassLoader@5b1d2887

Preste atenção à posição de destruição da delegação pai : o mecanismo de carregamento de classe personalizado é primeiro delegado ao ExtClassLoader para carregamento, e ExtClassLoader é então delegado ao BootstrapClassLoader. Se nenhum deles puder ser carregado, personalize o carregador de classes MyClassLoader. Se MyClassLoader não puder carregar, ele é entregue ao AppClassLoader. Por que não podemos simplesmente deixar o carregador de classes personalizado carregá-lo diretamente?

Como a destruição da delegação parental só pode ocorrer na sequência de carregamento de delegação de AppClassLoader e abaixo, a delegação parental acima de ExtClassLoader não pode ser destruída!

Causa raiz: qualquer classe herda da superclasse java.lang.Object. Ao carregar uma classe, a classe herdada também será carregada. Se outras classes também forem referenciadas na classe, elas serão carregadas sob demanda e os carregadores de classe serão todos os carregadores de classe que carregam a classe atual.

Por exemplo, a classe MyTarget herda apenas Object implicitamente. Se o carregador de classe personalizado MyClassLoader carregar MyTarget, ele também carregará Object. Se loadClass chamar diretamente findClass de MyClassLoader, um erro java.lang.SecurityException: Prohibited package name: java.lang será relatado.

Por motivos de segurança, Java não permite que carregadores de classes diferentes do BootStrapClassLOader carreguem bibliotecas de classes no diretório oficial Java. No código-fonte defineClass, o método nativo defineClass1 será eventualmente chamado para obter o objeto Class. Antes disso, será verificado se o nome completo da classe é Iniciando com java.​​​. (Se você deseja ignorar completamente o carregamento de classe Java, você mesmo precisa implementar defineClass. No entanto, devido à minha capacidade pessoal limitada, não estudei a reescrita de defineClass em profundidade e geralmente isso não destruirá ExtClassLoader. ​​O acima delegação parental, a menos que java não seja mais usado)

O código-fonte defineClass é o seguinte:

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    
    
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}
private ProtectionDomain preDefineClass(String name,
                                        ProtectionDomain pd)
{
    
    
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    if ((name != null) && name.startsWith("java.")) {
    
    
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
    
    
        pd = defaultDomain;
    }

    if (name != null) checkCerts(name, pd.getCodeSource());

    return pd;
}

Casos de destruição da delegação parental por meio de carregadores de classes personalizados são muito comuns no desenvolvimento diário. Por exemplo, para Tomcatobter isolamento de carregamento entre aplicativos da Web, um carregador de classes é personalizado. Cada contexto representa um aplicativo da Web e possui um ​webappClassLoader​​. Outro exemplo é que a implementação de implantação e carregamento a quente requer um carregador de classes customizado. O local da destruição é pular AppClassLoader.

5. Resumo

  1. O carregamento de classe em java consiste em obter a matriz de bytecode binário do arquivo .class e carregá-lo na área de método da JVM e criar uma área de heap na JVM para encapsular os dados e métodos relacionados à classe java .instância do objeto java.lang.Class.

  2. Existem três carregadores de classes em Java por padrão: carregador de classes de inicialização (BootstrapClassLoader), carregador de classes de extensão (ExtClassLoader) e carregador de classes de aplicativo (também chamado de carregador de classes do sistema) (AppClassLoader). Existe um relacionamento pai-filho entre carregadores de classe.Esse relacionamento não é um relacionamento de herança, mas um relacionamento de combinação. Se parent=null, seu pai será o carregador de classes de inicialização. O carregador de classes de inicialização não pode ser referenciado diretamente pelo programa Java.

  3. A delegação parental é o relacionamento hierárquico entre carregadores de classes. O processo de carregamento de uma classe é um processo de chamada recursiva. Primeiro, o carregador de classes pai é delegado camada por camada para carregar até atingir o carregador de classes de inicialização de nível superior. O carregador de classes de inicialização não pode Ao carregar, ele é delegado camada por camada ao carregador de subclasse para carregamento.

  4. O objetivo da delegação pai é principalmente garantir a segurança do carregamento da biblioteca de classes java oficial <JAVA_HOME>\lib e da biblioteca de classes estendida <JAVA_HOME>\lib\ext. Substituída pelos desenvolvedores.

  5. Existem duas maneiras de destruir a delegação pai: a primeira é um carregador de classe personalizado, que deve substituir findClass e loadClass; a segunda é através da transitividade do carregador de classe de contexto do thread, permitindo que o carregador de classe pai chame a ação de carregamento da subclasse carregador.


3. Mecanismo SPI

SPI significa Service Provider Interface e é um mecanismo de descoberta de serviço. A essência do SPI é configurar o nome totalmente qualificado da classe de implementação da interface em um arquivo, e o carregador de serviço lê o arquivo de configuração e carrega a classe de implementação. Isso permite substituir dinamicamente classes de implementação por interfaces em tempo de execução. Devido a esse recurso, podemos facilmente fornecer funções estendidas para nossos programas por meio do mecanismo SPI.

1. JDK SPI

A classe principal da função SPI fornecida no JDK é java.util.ServiceLoader. A função é obter vários arquivos de implementação de configuração em "META-INF/services/" através do nome da classe.

Exemplo: javax.servlet.ServletContainerInitializerO conteúdo do arquivo é o nome completo da classe de implementação da interface representada pelo nome do arquivo.org.springframework.web.SpringServletContainerInitializer
Insira a descrição da imagem aqui

Como a ordem de carregamento (classpath) é especificada pelo usuário, quer carreguemos o primeiro ou o último, é possível que a configuração definida pelo usuário não possa ser carregada.

Portanto, esta também é uma desvantagem do mecanismo JDK SPI. É impossível confirmar qual implementação está carregada e também é impossível carregar uma implementação específica. Depender apenas da ordem do ClassPath é um método muito impreciso.

2. Primavera SPI

O arquivo de configuração SPI do Spring é um arquivo fixo META-INF/spring.factories, semelhante em função ao JDK. Cada interface pode ter várias implementações de extensão e é muito simples de usar:

//Configura o LoggingSystemFactory em todos os arquivos de fábricas

List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

A seguir está uma configuração de spring.factories no SpringBoot

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

No Spring SPI, todas as configurações são colocadas em um arquivo fixo, eliminando o problema de configurar muitos arquivos. Quanto à configuração estendida de múltiplas interfaces, é uma questão de opinião se é melhor usar um arquivo ou cada arquivo separado (eu pessoalmente gosto do Spring, que é limpo e organizado).

Embora o SPI do Spring pertença ao spring-framework (core), atualmente ele é usado principalmente no SpringBoot. Como os dois mecanismos SPI anteriores, o Spring também suporta a existência de vários arquivos spring.factories no ClassPath. Ao carregar, esses arquivos spring.factories serão carregados sequencialmente na ordem do caminho de classe e adicionados a um ArrayList. Como não há aliases, não existe o conceito de remoção de duplicações. Basta adicionar quantos houver.

No entanto, como o SPI do Spring é usado principalmente no Spring Boot, o ClassLoader no Spring Boot dará prioridade ao carregamento de arquivos no projeto em vez de arquivos no pacote de dependência. Portanto, se você definir um arquivo spring.factories em seu projeto, os arquivos do seu projeto serão carregados primeiro, e entre as fábricas obtidas, a classe de implementação configurada em spring.factories no projeto também será classificada em primeiro lugar.

Se quisermos estender uma interface, basta criar um novo META-INF/spring.factoriesarquivo no seu projeto (SpringBoot) e adicionar a configuração desejada.

Por exemplo, se eu quiser apenas adicionar uma nova implementação LoggingSystemFactory, então só preciso criar um novo arquivo META-INF/spring.factories em vez de copiar e modificar completamente:

org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory

3. Dubo SPI

Dubbo carrega todos os componentes através do mecanismo SPI. No entanto, Dubbo não usa o mecanismo SPI nativo do Java, mas o aprimora para melhor atender às necessidades. No Dubbo, o SPI é um módulo muito importante. Com base no SPI, podemos expandir facilmente o Dubbo. Se você quiser aprender o código-fonte do Dubbo, você deve entender o mecanismo SPI. A seguir, vamos primeiro entender o uso de Java SPI e Dubbo SPI e, em seguida, analisar o código-fonte do Dubbo SPI.
Um novo mecanismo SPI é implementado no Dubbo, que é mais poderoso e complexo. A lógica relevante é encapsulada na classe ExtensionLoader. Através do ExtensionLoader, podemos carregar a classe de implementação especificada. Os arquivos de configuração exigidos pelo Dubbo SPI precisam ser colocados no caminho META-INF/dubbo. O conteúdo da configuração é o seguinte (a demonstração a seguir é da documentação oficial do dubbo).

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

Diferente da configuração da classe de implementação Java SPI, o Dubbo SPI é configurado por meio de pares chave-valor, para que possamos carregar a classe de implementação especificada sob demanda. Além disso, a anotação @SPI precisa ser marcada na interface ao usá-la. Vamos demonstrar o uso do Dubbo SPI:

@SPI
public interface Robot {
    
    
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    
    @Override
    public void sayHello() {
    
    
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {
    
    
    @Override
    public void sayHello() {
    
    
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class DubboSPITest {
    
    
    @Test
    public void sayHello() throws Exception {
    
    
        ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

A maior diferença entre Dubbo SPI e JDK SPI é que ele suporta "aliases" .Você pode obter um ponto de extensão fixo através do alias de um ponto de extensão. Assim como no exemplo acima, posso obter a implementação com o alias "optimusPrime" entre as múltiplas implementações SPI do Robot, e também posso obter a implementação com o alias "bumblebee".Esta função é muito útil!

Através do atributo value da anotação @SPI, você também pode usar como padrão uma implementação de "alias". Por exemplo, no Dubbo, o padrão é o protocolo privado Dubbo: protocolo dubbo - dubbo://
**
Vamos dar uma olhada na interface do protocolo no Dubbo:

@SPI("dubbo")
public interface Protocol {
    
    
    // ...
}

Na interface do Protocolo é adicionada uma anotação @SPI, e o valor da anotação é dubbo. Ao obter a implementação através do SPI, será obtida a implementação com o alias dubbo na configuração do Protocolo SPI. O com.alibaba.dubbo. O arquivo rpc.Protocol é o seguinte:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

Então você só precisa obter a implementação da extensão correspondente ao valor na anotação @SPI por meio de getDefaultExtension.

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol

Existe também um mecanismo Adaptativo, embora seja muito flexível, seu uso não é muito “elegante” e não vou apresentá-lo aqui.

Há também uma "prioridade de carregamento" no SPI do Dubbo. O integrado (interno) é carregado primeiro e, em seguida, o externo (externo) é carregado em ordem de prioridade. Se forem encontradas duplicatas, elas serão ignoradas e não serão carregado.

Portanto, se você quiser confiar na ordem de carregamento do caminho de classe para substituir extensões integradas, também é uma abordagem irracional. O motivo é o mesmo acima - a ordem de carregamento não é estrita.

4. Comparação

Comparando os três mecanismos SPI, o mecanismo integrado do JDK é o mais fraco, mas por estar integrado no JDK, ainda possui certos cenários de aplicação. Afinal, não são necessárias dependências adicionais; Dubbo tem as funções mais ricas, mas o mecanismo é um pouco complicado e só pode ser usado com Dubbo e não pode ser considerado completamente como um módulo independente; as funções do Spring são quase as mesmas do JDK. A maior diferença é que todos os pontos de extensão são escritos em um spring.factories arquivo, o que também é uma melhoria, e o IDEA suporta perfeitamente prompts de sintaxe.

JDK-SPI DUBBO SPI Primavera SPI
Modo de arquivo Um arquivo separado para cada ponto de extensão Um arquivo separado para cada ponto de extensão Todos os pontos de extensão em um arquivo
Obtenha uma implementação fixa Não suportado, você só pode colocar todas as implementações em ordem Existe o conceito de "alias". Você pode obter uma implementação fixa do ponto de extensão através do nome. É muito conveniente cooperar com as anotações Dubbo SPI. Não suportado, todas as implementações só podem ser obtidas em ordem. No entanto, como o Spring Boot ClassLoader dará prioridade ao carregamento de arquivos no código do usuário, ele pode garantir que o arquivo spring.factoires definido pelo usuário seja o primeiro, e a extensão customizada pode ser obtida fixamente obtendo a primeira fábrica.
outro nenhum Suporta injeção de dependência dentro do Dubbo, distingue o SPI integrado e o SPI externo do Dubbo por meio de diretórios e carrega os internos primeiro para garantir que os internos tenham a prioridade mais alta. nenhum

Acho que você gosta

Origin blog.csdn.net/qq_45867699/article/details/132100088
Recomendado
Clasificación