Análise detalhada do princípio de inicialização do Springboot

Insira a descrição da imagem aqui
Desenvolvemos qualquer projeto Spring Boot, usaremos a seguinte classe de inicialização

@SpringBootApplication
public class Application {
    
    
     public static void main(String[] args) {
    
    
         SpringApplication.run(Application.class, args);
     }
 }

Como pode ser visto no código acima, a definição de anotação (@SpringBootApplication) e a definição de classe (SpringApplication.run) são as mais deslumbrantes, portanto, para desvendar o mistério do SpringBoot, temos que começar com essas duas.

Um, o segredo por trás do SpringBootApplication

@SpringBootApplication注解是Spring Boot的核心注解,它其实是一个组合注解:
@Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
 @SpringBootConfiguration
 @EnableAutoConfiguration
 @ComponentScan(excludeFilters = {
    
    
         @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
         @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
 public @interface SpringBootApplication {
    
    
 ...
 }

Embora a definição use várias anotações para marcar as informações originais, apenas três anotações são realmente importantes:

  • @Configuration (@SpringBootConfiguration clique para verificar e descobrir que @Configuration ainda é aplicado)
  • @EnableAutoConfiguration
  • @ComponentScan

Ou seja, @SpringBootApplication =
(propriedade padrão) @Configuration + @ EnableAutoConfiguration + @ComponentScan.

Portanto, se usarmos a seguinte classe de inicialização SpringBoot, todo o aplicativo SpringBoot ainda pode ser funcionalmente equivalente à classe de inicialização anterior:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(Application.class, args);
    }
}

É cansativo escrever esses 3 a cada vez, então é conveniente escrever um @SpringBootApplication. A seguir, apresentaremos essas 3 anotações, respectivamente.

1 、 @ Configuração

O @Configuration aqui não é estranho para nós. É o @Configuration usado pela classe de configuração do contêiner Spring Ioc na forma de JavaConfig. A comunidade SpringBoot recomenda o uso do formulário de configuração baseado em JavaConfig. Na verdade, é uma classe de configuração de um contêiner IoC.

Dê alguns exemplos simples para revisar, a diferença entre XML e métodos de configuração de configuração:

(1) Nível de expressão

A maneira baseada na configuração XML é a seguinte:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-lazy-init="true">
    <!--bean定义-->
</beans>

O método de configuração baseado em JavaConfig é assim:

@Configuration
public class MockConfiguration{
    
    
    //bean定义
}

Qualquer definição de classe Java marcada com @Configuration é uma classe de configuração JavaConfig.

(2) Registre o nível de definição do feijão

O formulário de configuração baseado em XML é assim:

<bean id="mockService" class="..MockServiceImpl">
    ...
</bean>

O formulário de configuração baseado em JavaConfig se parece com isto:

@Configuration
public class MockConfiguration{
    
    
    @Bean
    public MockService mockService(){
    
    
        return new MockServiceImpl();
    }
}

Para qualquer método marcado com @Bean, seu valor de retorno será registrado como uma definição de bean no contêiner IoC do Spring, e o nome do método será o padrão para o id da definição de bean.

(3) Expressar nível de relacionamento de injeção de dependência

Para expressar a dependência entre bean e bean, geralmente é assim no formato XML:

<bean id="mockService" class="..MockServiceImpl">
   <propery name ="dependencyService" ref="dependencyService" />
</bean>
<bean id="dependencyService" class="DependencyServiceImpl"></bean>

O formulário de configuração baseado em JavaConfig se parece com isto:

@Configuration
public class MockConfiguration{
    
    
    @Bean
    public MockService mockService(){
    
    
        return new MockServiceImpl(dependencyService());
    }

    @Bean
    public DependencyService dependencyService(){
    
    
        return new DependencyServiceImpl();
    }
}

Se a definição de um bean depende de outros beans, basta chamar o método de criação do bean dependente diretamente na classe JavaConfig correspondente.

@Configuration: Mencionar @Configuration é mencionar seu parceiro @Bean. Usando essas duas anotações, você pode criar uma classe de configuração spring simples, que pode ser usada para substituir o arquivo de configuração xml correspondente.

<beans> 
    <bean id = "car" class="com.test.Car"> 
        <property name="wheel" ref = "wheel"></property> 
    </bean> 
    <bean id = "wheel" class="com.test.Wheel"></bean> 
</beans>

É equivalente a:

@Configuration 
public class Conf {
    
     
    @Bean 
    public Car car() {
    
     
        Car car = new Car(); 
        car.setWheel(wheel()); 
        return car; 
    }

    @Bean 
    public Wheel wheel() {
    
     
        return new Wheel(); 
    } 
}

A classe de anotação @Configuration indica que esta classe pode usar o contêiner Spring IoC como fonte de definições de bean.

A anotação @Bean diz ao Spring que um método anotado com @Bean retornará um objeto que deve ser registrado como um bean no contexto do aplicativo Spring.

2 、 @ ComponentScan

A anotação @ComponentScan é muito importante no Spring. Ela corresponde aos elementos na configuração XML. A função de @ComponentScan é, na verdade, fazer a varredura e carregar automaticamente componentes qualificados (como @Component e @Repository, etc.) ou definições de bean e, finalmente, definir esses beans Carregue no contêiner IoC.

Podemos refinar o escopo da varredura automática @ComponentScan por meio de atributos como basePackages. Se não for especificado, a implementação da estrutura Spring padrão fará a varredura do pacote onde a classe @ComponentScan está declarada.

Nota: Portanto, a classe de inicialização do SpringBoot é melhor posicionada no pacote raiz, porque basePackages não é especificado por padrão.

3 、 @ EnableAutoConfiguration

Pessoalmente, acho que @EnableAutoConfiguration é a Anotação mais importante. Você ainda se lembra das definições de Anotação que começam com @Enable fornecida pela estrutura Spring?
Por exemplo, @EnableScheduling,
@EnableCaching , @EnableMBeanExport, etc., a filosofia e a maneira de fazer as coisas de @EnableAutoConfiguration estão na mesma linha. Um breve resumo é coletar e registrar definições de bean relacionadas a cenários específicos com o suporte de @Import.

@EnableScheduling carrega todas as definições de bean relacionadas à estrutura de agendamento Spring para o contêiner IoC por meio de @Import.

@EnableMBeanExport deve carregar as definições de bean relacionadas ao JMX no contêiner IoC por meio de @Import.

E @EnableAutoConfiguration também usa a ajuda de @Import para carregar todas as definições de bean que atendem às condições de configuração automática no contêiner IoC, nada mais!

@EnableAutoConfiguration configurará automaticamente o projeto de acordo com as dependências jar no classpath. Por exemplo, se a dependência spring-boot-starter-web for adicionada, as dependências Tomcat e Spring MVC serão adicionadas automaticamente e Spring Boot configurará automaticamente Tomcat e Spring MVC .

Insira a descrição da imagem aqui
@EnableAutoConfiguration, como uma anotação composta, define as informações principais da seguinte forma:

@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    
    
    ...
}

Entre eles, o mais crítico é @Import (EnableAutoConfigurationImportSelector.class). Com a ajuda de EnableAutoConfigurationImportSelector, @EnableAutoConfiguration pode ajudar os aplicativos SpringBoot a carregar todas as configurações @Configuration elegíveis no contêiner IoC atual criado e usado pelo SpringBoot. Assim como um "polvo", com o suporte de uma classe de ferramenta original do framework Spring: SpringFactoriesLoader, @EnableAutoConfiguration pode configurar de forma inteligente e automática a função para ter sucesso!
Insira a descrição da imagem aqui
O herói por trás da configuração automática: SpringFactoriesLoader explica
SpringFactoriesLoader é uma extensão privada do framework Spring.Sua função principal é carregar a configuração do arquivo de configuração especificado META-INF / spring.factories.

public abstract class SpringFactoriesLoader {
    
    
    //...
    public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
    
    
        ...
    }
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    
    
        ....
    }
}

Quando usado com @EnableAutoConfiguration, é mais para fornecer um suporte à função de pesquisa de configuração, ou seja, de acordo com o nome completo da classe de @EnableAutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration como a chave a ser pesquisada, um conjunto de classes @Configuration correspondentes é obtido.

Insira a descrição da imagem aqui
A imagem acima é um trecho do arquivo de configuração META-INF / spring.factories no pacote de dependência autoconfigure do SpringBoot, que pode ilustrar bem o problema.

Portanto, o cavaleiro mágico da configuração automática de @EnableAutoConfiguration torna-se: pesquise todos os arquivos de configuração META-INF / spring.factories do classpath e passe os itens de configuração correspondentes a org.springframework.boot.autoconfigure.EnableutoConfiguration por meio de reflexão (Java Refletion) é instanciado na classe de configuração do contêiner IoC correspondente na forma de JavaConfig marcado com @Configuration e, em seguida, agregado em um e carregado no contêiner IoC.

Dois, exploração em profundidade do processo de execução SpringApplication

A implementação do método run de SpringApplication é a rota principal de nossa jornada. O processo principal desse método pode ser resumido da seguinte forma:

(1) Se estivermos usando o método de execução estática de SpringApplication, então, neste método, devemos primeiro criar uma instância de objeto SpringApplication e, em seguida, chamar o método de instância SpringApplication criado. Quando a instância SpringApplication é inicializada, ela fará várias coisas com antecedência:

  • De acordo com a existência de uma classe de recurso org.springframework.web.context.ConfigurableWebApplicationContext no classpath, é determinado se um tipo ApplicationContext para aplicativos da web deve ser criado.
  • Use SpringFactoriesLoader para localizar e carregar todos os ApplicationContextInitializers disponíveis no classpath do aplicativo.
  • Use SpringFactoriesLoader para localizar e carregar todos os ApplicationListeners disponíveis no classpath do aplicativo.
  • Inferir e definir a classe de definição do método principal.

(2) Após a inicialização da instância SpringApplication ser concluída e as configurações concluídas, a lógica do método run é executada. No início da execução do método, todos os SpringApplicationRunListeners que podem ser encontrados e carregados por SpringFactoriesLoader são percorridos e carregados. Chame seu método started () para dizer a estes SpringApplicationRunListener, "Ei, o aplicativo SpringBoot está prestes a iniciar a execução!".

(3) Crie e configure o Ambiente a ser usado pelo aplicativo Spring Boot atual (incluindo PropertySource e Perfil a serem configurados).

(4) Percorra os métodos environmentPrepared () que chamam todos SpringApplicationRunListener e diga a eles: "O ambiente usado pelo aplicativo SpringBoot atual está pronto!".

(5) Se a propriedade showBanner de SpringApplication for definida como verdadeira, o banner será impresso.

(6) De acordo com se o usuário definiu explicitamente o tipo applicationContextClass e o resultado da inferência da fase de inicialização, decida que tipo de ApplicationContext deve ser criado para o aplicativo SpringBoot atual e crie-o e, em seguida, decida se deseja adicionar ShutdownHook de acordo com as condições, decidir se usar um BeanNameGenerator personalizado e decidir Para usar um ResourceLoader customizado, é claro, o mais importante é definir o Environment previamente preparado para o ApplicationContext criado.

(7) Depois que o ApplicationContext for criado, SpringApplication usará novamente Spring-FactoriesLoader para encontrar e carregar todos os ApplicationContext-Initializers disponíveis no classpath e, em seguida, percorrer e chamar os métodos de inicialização (applicationContext) desses ApplicationContextInitializers para processar posteriormente o ApplicationContext criado .

(8) Percorra e chame todos os métodos contextPrepared () de SpringApplicationRunListener.

(9) A etapa mais importante é carregar todas as configurações obtidas anteriormente por meio de @EnableAutoConfiguration e outras formas de configuração de contêiner IoC no ApplicationContext preparado.

(10) Percorra e chame todos os métodos contextLoaded () de SpringApplicationRunListener.

(11) Chame o método refresh () de ApplicationContext para concluir o último processo disponível para o contêiner IoC.

(12) Descubra se há CommandLineRunners registrados no ApplicationContext atual e, em caso afirmativo, percorra e execute-os.

(13) Em circunstâncias normais, atravesse e execute o método finalizado () de SpringApplicationRunListener, (se todo o processo for anormal, o método finalizado () de todos SpringApplicationRunListener ainda é chamado, mas neste caso, as informações de exceção serão passadas para processamento)

Depois de remover o ponto de notificação de evento, todo o processo é o seguinte:
Insira a descrição da imagem aqui
Este artigo pega a depuração de um programa de inicialização SpringBoot real como exemplo e se refere ao diagrama de classe principal no processo para analisar sua lógica de inicialização e princípios de configuração automática.

Insira a descrição da imagem aqui
Visão geral:

A imagem acima é o diagrama da estrutura de inicialização do SpringBoot. Descobrimos que o processo de inicialização é dividido principalmente em três partes:

  • A primeira parte é o módulo de inicialização do SpringApplication e configura algumas variáveis ​​básicas de ambiente, recursos, construtores e ouvintes;
  • A segunda parte implementa o plano de inicialização específico do aplicativo, incluindo o módulo de monitoramento do processo de inicialização, o módulo de ambiente de configuração de carregamento e o módulo de ambiente de contexto de criação de núcleo;
  • A terceira parte é o módulo de configuração automática, que é o núcleo da configuração automática do springboot, que será discutido em detalhes na análise a seguir. No procedimento de inicialização a seguir, conectaremos as funções principais da estrutura em série.

comece:

Cada programa SpringBoot tem uma entrada principal, ou seja, o método principal. Em main, SpringApplication.run () é chamado para iniciar todo o programa spring-boot. A classe onde o método está localizado precisa ser anotada com @SpringBootApplication e @ImportResource (se necessário), @ SpringBootApplication inclui três anotações, as funções são as seguintes:

  • @EnableAutoConfiguration: SpringBoot configura automaticamente a estrutura Spring com base nas dependências declaradas pelo aplicativo.
  • @SpringBootConfiguration (internamente @Configuration): A classe marcada é igual ao arquivo de configuração XML do spring (applicationContext.xml), que monta todas as transações de bean e fornece um ambiente de contexto do spring.
  • @ComponentScan: verificação de componentes, que pode descobrir e montar Beans automaticamente. Por padrão, o arquivo no caminho do pacote de Booter.class no método de execução de SpringApplication é verificado, portanto, é melhor colocar a classe de inicialização no caminho do pacote raiz.

Insira a descrição da imagem aqui
Classe de inicialização SpringBoot

Primeiro, insira o método run: No
Insira a descrição da imagem aqui
método run, uma instância SpringApplication é criada.No método de construção, podemos descobrir que ele chamou um método de inicialização inicializado.

Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Aqui é principalmente para atribuir alguns valores iniciais ao objeto SpringApplication. Depois que o construtor é executado, retornamos ao método run

Insira a descrição da imagem aqui
As seguintes etapas principais são implementadas neste método:
1. Crie o ouvinte do aplicativo SpringApplicationRunListeners e comece a ouvir

2. Carregue o ambiente de configuração SpringBoot (ConfigurableEnvironment), se ele for publicado por meio do contêiner da web, ele carregará o StandardEnvironment, que eventualmente herda o ConfigurableEnvironment, o diagrama de classe é o seguinte:
Insira a descrição da imagem aqui

Pode-se ver que * Environment finalmente implementa a interface PropertyResolver.Quando normalmente obtemos o método de valor correspondente à chave especificada no arquivo de configuração através do objeto de ambiente, chamamos o método getProperty da interface propertyResolver.

3. Configure o ambiente (Environment) para se juntar ao objeto ouvinte (SpringApplicationRunListeners)

4. Crie o objeto de retorno do método de execução: ConfigurableApplicationContext (contexto de configuração do aplicativo), podemos olhar para o método de criação: o
Insira a descrição da imagem aqui
método primeiro obterá o contexto do aplicativo explicitamente definido (applicationContextClass), se não existir, em seguida, carregará a configuração do ambiente padrão (por meio de É um julgamento de ambiente da web), o contexto de anotação AnnotationConfigApplicationContext é selecionado por padrão (verificando todas as classes de anotação para carregar o bean) e, finalmente, o objeto de contexto é instanciado por meio de BeanUtils e retornado.

O diagrama de classes ConfigurableApplicationContext é o seguinte:

Insira a descrição da imagem aqui
Depende principalmente das duas direções de sua herança:

  • LifeCycle: classe de ciclo de vida, que define se o início começa, a parada termina e se isRunning está executando um método de valor nulo de ciclo de vida médio
  • ApplicationContext: classe de contexto do aplicativo, que herda principalmente beanFactory (classe de fábrica de feijão)

5. De volta ao método run, o método prepareContext associa componentes importantes, como ouvintes, ambiente, applicationArguments e banner com o objeto de contexto

6. O próximo método refreshContext (contexto) (o método de inicialização é o seguinte) será a chave para realizar a configuração automática de spring-boot-starter- * (mybatis, redis, etc.), incluindo o carregamento de spring.factories, a instanciação de beans e outras tarefas essenciais .

Insira a descrição da imagem aqui
Após a configuração, Springboot fez alguns trabalhos básicos de acabamento e retornou o contexto do ambiente do aplicativo. Relembrando o processo geral, a inicialização do Springboot cria principalmente um ambiente de configuração (ambiente), ouvintes de eventos (ouvintes) e contexto de aplicativo (applicationContext). Com base nas condições acima, começamos a instanciar os beans de que precisamos no contêiner. Até agora, começamos por meio do SpringBoot. O programa foi construído, vamos discutir como realizar a configuração automática.

Configuração automática:

No diagrama de estrutura de inicialização anterior, notamos que tanto a inicialização do aplicativo quanto o processo de execução específico invocaram o módulo de configuração automática SpringBoot.
Insira a descrição da imagem aqui
Módulo de configuração automática SpringBoot

O módulo de configuração usa principalmente SpringFactoriesLoader, o carregador de fábrica Spring. O objeto fornece o método loadFactoryNames. Os parâmetros de entrada são factoryClass e classLoader, o que significa que o nome da classe de fábrica e o carregador de classe correspondente na figura acima precisam ser transmitidos. O método será baseado em O classLoader especificado carrega o arquivo especificado no caminho de pesquisa do somador de classe, ou seja, o arquivo spring.factories. A classe de fábrica passada é a interface e a classe correspondente no arquivo é a classe de implementação da interface ou, finalmente, como a classe de implementação. O arquivo geralmente é uma coleção de nomes de classes um para muitos, conforme mostrado na figura abaixo. Depois de obter os nomes das classes dessas classes de implementação, o método loadFactoryNames retorna uma coleção de nomes de classes. Depois que o chamador do método obtém essas coleções, os objetos de classe dessas classes são obtidos por meio de reflexão. , Método de construção e, finalmente, gere uma instância.
Insira a descrição da imagem aqui
Interface de fábrica e seus vários nomes de interface de classe de implementação

A figura a seguir nos ajuda a entender o processo de configuração automática.
Insira a descrição da imagem aqui
Diagrama de relacionamento do componente-chave de configuração automática do SpringBoot

Os arquivos META-INF de mybatis-spring-boot-starter, spring-boot-starter-web e outros componentes contêm o arquivo spring.factories. No módulo de configuração automática, SpringFactoriesLoader coleta o nome completo da classe no arquivo e retorna o nome completo da classe O nome completo da classe retornada é instanciado por meio de reflexão para formar uma instância de fábrica concreta, e a instância de fábrica gera os beans de que o componente precisa especificamente.

Anteriormente, mencionamos a anotação EnableAutoConfiguration e seu diagrama de classe é o seguinte:
Insira a descrição da imagem aqui
você pode descobrir que ela finalmente implementa ImportSelector (seletor) e BeanClassLoaderAware (middleware do carregador de classe de bean), com foco no método selectImports de AutoConfigurationImportSelector.
Insira a descrição da imagem aqui
Este método é executado antes que o bean de processo de inicialização do springboot seja instanciado e retorna uma lista de informações de classe a serem instanciadas. Sabemos que se as informações da classe forem obtidas, o spring pode carregar naturalmente a classe no jvm por meio do carregador de classes. Agora, contamos com os componentes de que precisamos por meio do método de dependência do spring-boot starter, então as informações da classe desses componentes estão no método select O meio também pode ser obtido, não se preocupe, continuamos a analisar para baixo.

Insira a descrição da imagem aqui
O método getCandidateConfigurations neste método, aprendido através da anotação do método, ele retorna uma lista de nomes de classes de classes de configuração automática, o método chama o método loadFactoryNames, verifique este método
Insira a descrição da imagem aqui

No código acima, você pode ver que o autoconfigurador encontrará a chave correspondente em todos os arquivos spring.factories no caminho do sistema do projeto de acordo com o factoryClass.getName () importado e carregará as classes dentro dele. Vamos selecionar o arquivo spring.factories em mybatis-spring-boot-autoconfigure

Insira a descrição da imagem aqui

Digite org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration, observe principalmente o cabeçalho da classe:

Insira a descrição da imagem aqui
Descobri que @Configuration do Spring é como um springBean marcado por anotações. Continue a olhar para baixo:

  • @ConditionalOnClass ({SqlSessionFactory.class,
    SqlSessionFactoryBean.class}): A
    classe de configuração MybatisAutoConfiguration é analisada quando há duas classes de SqlSessionFactory.class e SqlSessionFactoryBean.class, caso contrário, essa classe de configuração não será usada, faça o mybatis Para retornar o objeto de sessão, deve haver uma classe relacionada à fábrica de sessão.
  • @CondtionalOnBean (DataSource.class): Processa apenas dataSource que foi declarada como bean.
  • @ConditionalOnMissingBean (MapperFactoryBean.class) Esta anotação significa que se o bean especificado pelo nome não existir no contêiner, a injeção de bean será criada, caso contrário, não será executada (o código-fonte desta classe é mais longo e o limite de espaço não é suficiente para colar)

A configuração acima pode garantir que os componentes exigidos por mybatis, como sqlSessionFactory, sqlSessionTemplate e dataSource, podem ser configurados automaticamente. A anotação @Configuration forneceu o ambiente de contexto Spring, de modo que o método de configuração dos componentes acima é configurado por meio do arquivo mybatis.xml quando Spring inicia. Para um efeito.

Por meio da análise, podemos descobrir que, desde que haja SqlSessionFactory.class, SqlSessionFactoryBean.class no classpath de um projeto SpringBoot e o dataSourceBean tenha sido registrado no contêiner, a configuração automatizada pode ser acionada, o que significa que só precisamos adicionar mybatis ao projeto maven As dependências necessárias podem acionar a configuração automática, mas se a dependência nativa de mybatis for introduzida, a classe de configuração automática de cada função integrada deve ser modificada e o efeito não será obtido fora da caixa.

Portanto, o Spring-boot nos fornece um iniciador unificado que pode configurar diretamente as classes relacionadas, e as dependências (mybatis) necessárias para acionar a configuração automática são as seguintes:

Insira a descrição da imagem aqui
Aqui estão todas as dependências do arquivo pom.xml no código-fonte de mybatis-spring-boot-starter:

Insira a descrição da imagem aqui
Por causa da transitividade da dependência do maven, podemos contar com todas as classes que precisam ser configuradas automaticamente, contanto que contemos com o iniciador para obter uma funcionalidade pronta para o uso. Também mostra que o Springboot simplifica a grande quantidade de configuração XML e gerenciamento de dependências complexas trazido pelo framework Spring, permitindo que os desenvolvedores prestem mais atenção ao desenvolvimento da lógica de negócios.

Se você acha que está tudo bem, você pode dar um joinha. Se houver alguma deficiência, você pode aconselhar.

O trabalho não é fácil, a tecnologia é assim mesmo, o salário do CRUD não é alto todos os dias, hey! A vida é tão difícil! Especialmente este ano é mais difícil, então se houver um pequeno parceiro que precisa encontrar um emprego ou um pequeno parceiro que precisa aprender, eu compilei materiais de aprendizagem Java e as perguntas da entrevista deste ano, que podem ser fornecidas a todos gratuitamente. Você pode clicar para entrar se precisar , e o código: cszq , você também pode seguir + mensagem privada para mim. Espero ajudar a todos!

Insira a descrição da imagem aqui

Insira a descrição da imagem aqui

Por fim, desejo-lhe as maiores felicidades no seu trabalho!

Acho que você gosta

Origin blog.csdn.net/m0_45270667/article/details/108752083
Recomendado
Clasificación