Si no comprende el principio del inicio de Spring Boot, ¡debe comprobarlo!

Cuando desarrollemos cualquier proyecto de Spring Boot, usaremos las siguientes clases de inicio

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

Como se puede ver en el código anterior, la definición de anotación ( @SpringBootApplication) y la definición de clase ( SpringApplication.run) son las más deslumbrantes, por lo que para descubrir el misterio de SpringBoot, debemos comenzar con estas dos.


1. El secreto detrás de SpringBootApplication

La anotación @SpringBootApplication es la anotación principal de Spring Boot, que en realidad es una anotación combinada:

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

Aunque la definición usa múltiples Anotaciones para anotar la información original, de hecho solo tres Anotaciones son importantes:

  • @Configuration( @SpringBootConfigurationHaga clic para ver y encontrar que todavía se aplica @Configuration)

  • @EnableAutoConfiguration

  • @ComponentScan

Eso es @SpringBootApplication= (atributo predeterminado) @Configuration+ @EnableAutoConfiguration+ @ComponentScan.

Por lo tanto, si usamos la siguiente clase de inicio SpringBoot, toda la aplicación SpringBoot aún puede ser funcionalmente equivalente a la clase de inicio anterior:

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

Es agotador escribir estos 3 cada vez, por lo que @SpringBootApplicationes conveniente escribir uno. A continuación, las tres Anotaciones se introducen por separado.

1, @Configuración

Esto @Configurationno nos es desconocido. Es el que usa la clase de configuración del contenedor Spring Ioc en forma de JavaConfig @Configuration. La comunidad SpringBoot recomienda usar el formulario de configuración basado en JavaConfig. Por lo tanto, después de marcar la clase de inicio aquí, @Configurationse es en realidad un contenedor IoC clase de configuración.

Para dar algunos ejemplos simples, revise la diferencia entre los métodos de configuración XML y config:

(1) Nivel de expresión

La configuración basada en XML es la siguiente:

<?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>

El método de configuración basado en JavaConfig es el siguiente:

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

Cualquier @Configurationdefinición de clase Java anotada es una clase de configuración JavaConfig.

(2) Nivel de definición del frijol de registro

La configuración basada en XML se ve así:

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

El formulario de configuración basado en JavaConfig es el siguiente:

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

Para cualquier @Beanmétodo marcado, su valor de retorno se registrará como una definición de bean en el contenedor IoC de Spring, y el nombre del método será el id de la definición de bean por defecto.

(3) Nivel de relación de inyección de dependencia expresa

Para expresar la relación de dependencia entre beans y beans, generalmente es así en formato XML:

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

El formulario de configuración basado en JavaConfig es el siguiente:

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

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

Si la definición de un bean depende de otros beans, puede llamar directamente al método de creación del bean dependiente en la clase JavaConfig correspondiente.

@Configuration: Cuando lo mencionas, @Configurationtienes que mencionar a su pareja @Bean. Usando estas dos anotaciones, puede crear una clase de configuración de resorte simple que se puede usar para reemplazar el archivo de configuración xml correspondiente.

<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(); 
    } 
}

@ConfigurationLa clase anotada identifica que esta clase puede usar el contenedor Spring IoC como fuente de definiciones de beans.

@BeanLa anotación le dice a Spring que un método anotado con @Bean devolverá un objeto que debe registrarse como un bean en el contexto de la aplicación Spring.

2, @ Escaneo de componentes

@ComponentScanEsta anotación es muy importante en Spring. Corresponde a los elementos en la configuración XML. @ComponentScanSu función es escanear y cargar automáticamente componentes calificados (como @Componenty @Repositoryetc.) o definiciones de bean, y finalmente cargar estas definiciones de bean en el contenedor IoC.

Podemos personalizar con precisión el alcance del escaneo automático a través de atributos como basePackages @ComponentScanSi no se especifica, la implementación predeterminada del marco Spring escaneará @ComponentScandesde el paquete donde se declara la clase.

Nota: Por lo tanto, la clase de inicio de SpringBoot se coloca mejor bajo el paquete raíz, porque basePackages no se especifica de manera predeterminada.

3, @EnableAutoConfiguration

Personalmente siento que @EnableAutoConfigurationesta Anotación es la más importante, así que la interpretaré al final. ¿Aún recuerdas la @Enabledefinición de Anotación con varios nombres proporcionada por el framework Spring? Por @EnableScheduling、@EnableCaching、@EnableMBeanExportejemplo, @EnableAutoConfigurationel concepto y la forma de hacer las cosas en realidad están en la misma línea.. Un breve resumen es que, con la ayuda @Importde soporte, recopile y registre definiciones de frijoles relacionadas con escenarios específicos.

@EnableSchedulingEs para cargar todas las definiciones de beans relacionadas con el marco de programación de Spring en el contenedor IoC a través de @Import.
@EnableMBeanExportEs cargar definiciones de beans relacionadas con JMX en el contenedor IoC a través de @Import.
Pero @EnableAutoConfigurationcon la ayuda de @Import, todas las definiciones de beans que cumplen las condiciones de configuración automática se cargan en el contenedor IoC, ¡eso es todo!

@EnableAutoConfigurationEl proyecto se configurará automáticamente de acuerdo con las dependencias de jar en la ruta de clase. Por ejemplo, si se agregan dependencias spring-boot-starter-web, las dependencias de Tomcat y Spring MVC se agregarán automáticamente, y Spring Boot configurará automáticamente Tomcat y Spring MVC.

Como anotación compuesta, @EnableAutoConfiguration define la información clave de la siguiente manera:

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

Entre ellos, lo más importante es usar el contenedor IoC @Import(EnableAutoConfigurationImportSelector.class)que EnableAutoConfigurationImportSelector,@EnableAutoConfigurationpuede ayudar a la aplicación SpringBoot a cargar todas @Configurationlas configuraciones elegibles en la creación y el uso actual de SpringBoot. SpringFactoriesLoaderComo un "pulpo", con el apoyo de una clase de herramienta original del marco Spring: ¡ @EnableAutoConfigurationse puede lograr la función de configuración automática inteligente!

Configure automáticamente los héroes detrás de escena: SpringFactoriesLoader en detalle

SpringFactoriesLoader es un esquema de extensión privado del framework Spring, y su función principal es cargar la configuración desde el archivo de configuración 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) {
        ....
    }
}

Si se usa en conjunto @EnableAutoConfiguration, proporciona un soporte más funcional para la búsqueda de configuración, es decir, de acuerdo con el @EnableAutoConfigurationnombre de clase completo de la clase org.springframework.boot.autoconfigure.EnableAutoConfigurationcomo clave de búsqueda, @Configurationse puede obtener un conjunto de clases correspondiente.

imagen

La imagen de arriba es META-INF/spring.factoriesun extracto del archivo de configuración en el paquete de dependencia de configuración automática de SpringBoot, que puede ilustrar bien el problema.

Por lo tanto, @EnableAutoConfigurationel caballero mágico de la configuración automática se convierte en: buscar todos META-INF/spring.factorieslos archivos de configuración desde el classpath e org.springframework.boot.autoconfigure.EnableutoConfigurationinstanciar los elementos de configuración correspondientes en la @Configurationclase de configuración del contenedor IoC correspondiente en forma de JavaConfig marcado a través de la reflexión (Java Refletion), luego se agrega en uno y cargado en el contenedor IoC.


2. Exploración en profundidad del proceso de ejecución de SpringApplication

La implementación del método de ejecución de SpringApplication es la ruta principal de nuestro viaje. El proceso principal de este método se puede resumir aproximadamente de la siguiente manera:

1) Si estamos utilizando el método de ejecución estático de SpringApplication, entonces, en este método, primero debemos crear una instancia de objeto SpringApplication y luego llamar al método de instancia de SpringApplication creado. Cuando se inicializa la instancia de SpringApplication, hará varias cosas por adelantado:

  • Según si hay una clase característica en el classpath, org.springframework.web.context.ConfigurableWebApplicationContextse determina si se debe crear un tipo ApplicationContext para aplicaciones Web.

  • El uso SpringFactoriesLoaderbusca y carga todos los disponibles en el classpath de la aplicación ApplicationContextInitializer.

  • El uso SpringFactoriesLoaderbusca y carga todos los disponibles en el classpath de la aplicación ApplicationListener.

  • Inferir y establecer la clase definitoria del método principal.

2) Después de que se completa la inicialización de la instancia de SpringApplication y se completa la configuración, la lógica del método de ejecución comienza a ejecutarse. Al comienzo de la ejecución del método, primero atraviesa y ejecuta todos los métodos que se pueden encontrar y cargar SpringFactoriesLoader. SpringApplicationRunListener. Llame a sus started()métodos para decirles SpringApplicationRunListener: "¡Oye, la aplicación SpringBoot está a punto de comenzar a ejecutarse!".

3) Cree y configure el entorno que utilizará la aplicación Spring Boot actual (incluida la configuración de PropertySource y el perfil que se utilizará).

4) Atraviese y llame a todos SpringApplicationRunListenerlos environmentPrepared()métodos y dígales: "¡El entorno utilizado por la aplicación SpringBoot actual está listo!".

5) Si la propiedad showBanner de SpringApplication se establece en verdadero, imprima el banner.

6) Según si el usuario ha establecido explícitamente applicationContextClassel tipo y el resultado de la inferencia de la fase de inicialización, decida qué tipo crear para la aplicación SpringBoot actual ApplicationContexty complete la creación, y luego decida si agregar ShutdownHook de acuerdo con las condiciones, decida si para usar uno personalizado BeanNameGenerator, y decidir si usar uno personalizado Por ResourceLoadersupuesto, lo más importante es configurar el Entorno previamente preparado para el creado ApplicationContext.

7) Después de que se crea ApplicationContext, SpringApplication lo usará nuevamente Spring-FactoriesLoaderpara buscar y cargar todos los disponibles en classpath ApplicationContext-Initializer, y luego atravesará y llamará a estos métodos ApplicationContextInitializer( applicationContext) para procesar aún más initializelos creados .ApplicationContext

8) Atraviesa y llama a todos SpringApplicationRunListenerlos métodos contextPrepared().

9) El paso principal es @EnableAutoConfigurationcargar todas las configuraciones obtenidas antes y otras formas de configuraciones de contenedores de IoC en la configuración preparada ApplicationContext.

10) Atraviesa y llama a todos SpringApplicationRunListenerlos métodos contextLoaded().

11) El método llamado ApplicationContextpara refresh()completar el último proceso disponible para el contenedor IoC.

12) Averiguar ApplicationContextsi existen registros en el sistema actual CommandLineRunner, y en caso afirmativo recorrerlos y ejecutarlos.

13) En circunstancias normales, atraviese el SpringApplicationRunListenermétodo de ejecución finished()(si ocurre una excepción en todo el proceso, se seguirán llamando a todos los métodos SpringApplicationRunListener, finished()pero en este caso, la información de la excepción se pasará para su procesamiento)

Después de eliminar el punto de notificación de eventos, todo el proceso es el siguiente:


Este artículo toma como ejemplo la depuración de un programa de inicio SpringBoot real y analiza su lógica de inicio y los principios de configuración automática con referencia al diagrama de clase principal en el proceso.

Descripción general:

La imagen de arriba muestra el diagrama de la estructura de inicio de SpringBoot. Descubrimos que el proceso de inicio se divide principalmente en tres partes:

  • La primera parte es inicializar el módulo de SpringApplication y configurar algunas variables de entorno básicas, recursos, constructores y oyentes;

  • La segunda parte implementa el esquema de inicio específico de la aplicación, incluido el módulo de monitoreo del proceso de inicio, el módulo del entorno de configuración de carga y el módulo de contexto de creación del núcleo;

  • La tercera parte es el módulo de configuración automática, que es el núcleo de la configuración automática de springboot y se discutirá en detalle en el siguiente análisis. En la siguiente rutina de inicio, conectaremos las funciones principales en la estructura.

puesta en marcha:

Cada programa SpringBoot tiene una entrada principal, que es el método principal, que llama e inicia SpringApplication.run()todo el programa spring-boot. La clase donde se encuentra el método necesita usar @SpringBootApplicationanotaciones y @ImportResourceanotaciones (si es necesario), @SpringBootApplicationincluidas tres anotaciones, las funciones son como sigue:

  • @EnableAutoConfiguration: SpringBoot configura automáticamente Spring Framework en función de las dependencias declaradas por la aplicación.

  • @SpringBootConfiguration(Interno @Configuration): la clase marcada es igual a ( applicationContext.xml) en el archivo de configuración Spring XML, ensamblando todas las transacciones de beans y proporcionando un contexto Spring.

  • @ComponentScan: Escaneo de componentes, que puede descubrir y ensamblar beans automáticamente. De manera predeterminada, se escanean los archivos debajo de la ruta del paquete en el método de ejecución de SpringApplication Booter.class, por lo que es mejor colocar la clase de inicio en la ruta del paquete raíz.

imagen

Clase de inicio de SpringBoot

Primero ingrese el método de ejecución

imagen.gif

En el método de ejecución, se crea una instancia de SpringApplication.En este método de construcción, podemos encontrar que llama a un método de inicialización inicializado.

Esto es principalmente para asignar algunos valores iniciales al objeto SpringApplication. Después de ejecutar el constructor, volvemos al método de ejecución

Los siguientes pasos clave se implementan en este método:

1. Cree un detector de aplicaciones SpringApplicationRunListenersy comience a escuchar

2. Cargue el entorno de configuración de SpringBoot ( ConfigurableEnvironment), si se publica a través del contenedor web, se cargará StandardEnvironmenty eventualmente se heredará ConfigurableEnvironment, el diagrama de clases es el siguiente

imagen

Se puede ver que *Environment finalmente implementa la interfaz PropertyResolver.Cuando generalmente obtenemos el método de valor correspondiente a la clave especificada en el archivo de configuración a través del objeto de entorno, llamamos al método getProperty de la interfaz propertyResolver.

3. El entorno de configuración ( Environment) se agrega al objeto de escucha ( SpringApplicationRunListeners)

4. Cree el objeto de retorno del método de ejecución: ConfigurableApplicationContext(contexto de configuración de la aplicación), podemos ver el método de creación:

El método primero obtendrá el contexto de aplicación establecido explícitamente ( applicationContextClass), si no existe, luego cargará la configuración de entorno predeterminada (juzgando si lo es web environment), seleccionará AnnotationConfigApplicationContextel contexto de anotación predeterminado (cargará beans escaneando todas las clases de anotación) y finalmente crea una instancia a través de BeanUtils del objeto de contexto y devuélvelo.

El diagrama de clases de ConfigurableApplicationContext es el siguiente:

Depende principalmente de las dos direcciones de su herencia:

  • Ciclo de vida: clase de ciclo de vida, que define inicio inicio, fin de parada, está en ejecución si se ejecuta el método de valor nulo del ciclo de vida medio

  • ApplicationContext: clase de contexto de aplicación, que hereda principalmente beanFactory (clase de fábrica de frijoles)

5. De vuelta en el método de ejecución, el método prepareContext listeners、environment、applicationArguments、bannerasocia otros componentes importantes con el objeto de contexto.

6. El siguiente refreshContext(context)método (el método de inicialización es el siguiente) será spring-boot-starter-*la clave para realizar la configuración automática (mybatis, redis, etc.), incluido spring.factoriesel trabajo central de carga, creación de instancias de beans, etc.

Después de la configuración, Springboot realizó un trabajo de acabado básico y devolvió el contexto del entorno de la aplicación. Mirando hacia atrás en el proceso general, el inicio de Springboot crea principalmente el entorno de configuración (entorno), los detectores de eventos (escuchas) y el contexto de la aplicación (applicationContext) y, en función de las condiciones anteriores, comenzamos a crear instancias de los beans que necesitamos en el contenedor. Hasta ahora, a través del inicio de SpringBoot El programa ha sido construido A continuación, analicemos cómo realizar la configuración automática.


Configuración automática:

En el diagrama de estructura de inicio anterior, notamos que tanto la inicialización de la aplicación como el proceso de ejecución específico invocaron el módulo de configuración automática SpringBoot.

Módulo de configuración automática SpringBoot

El uso principal de este módulo de configuración SpringFactoriesLoaderes el cargador de fábrica de Spring. Este objeto proporciona loadFactoryNamesmétodos. Los parámetros de entrada son factoryClass y classLoader, es decir, se debe pasar el nombre de la clase de fábrica en la figura anterior y el cargador de clases correspondiente. El método se basará en el classLoader especificado, cargará el archivo especificado en la ruta de búsqueda del sumador de clases, es decir, spring.factoriesel archivo, la clase de fábrica entrante es la interfaz y la clase correspondiente en el archivo es la clase de implementación del interfaz, o finalmente como la clase de implementación, por lo que el archivo generalmente es el siguiente Este tipo de colección de nombres de clase de uno a muchos, después de obtener los nombres de clase de estas clases de implementación, el método devuelve la colección de nombres de clase y el llamador del loadFactoryNamesmétodo obtiene estas colecciones, y luego obtiene los objetos de clase y los métodos de construcción de estas clases a través de la reflexión, y finalmente genera una instancia.

Interfaz de fábrica y varios nombres de interfaz de clase de implementación

La siguiente figura nos ayuda a visualizar el proceso de configuración automática.

Diagrama de relación de componentes clave de configuración automática de SpringBoot

mybatis-spring-boot-starter, spring-boot-starter-weby los archivos META-INF de otros componentes contienen spring.factoriesarchivos. En el módulo de configuración automática, SpringFactoriesLoaderse recopila el nombre completo de la clase en el archivo y se devuelve una matriz del nombre completo de la clase. El nombre completo devuelto de la clase es instanciado a través de la reflexión, formando una instancia específica de The factory, la instancia de fábrica para generar los beans que el componente necesita específicamente.

Mencionamos EnableAutoConfigurationanotaciones antes, y su diagrama de clases es el siguiente:

Se puede encontrar que finalmente implementa ImportSelector(selector) y BeanClassLoaderAware(bean class loader middleware), centrándose en los siguientes AutoConfigurationImportSelectormétodos selectImports.

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-Dm6tiUWw-1597051375071) (https://upload-images.jianshu.io/ upload_images/18688925-97932faefd1184cf?imageMogr2 /orientación automática/strip%7CimageView2/2/w/1240)]

Este método se ejecuta antes del proceso de inicio de springboot: creación de instancias de beans y devuelve una lista de información de clase para crear instancias. Sabemos que si se obtiene la información de la clase, Spring puede cargar naturalmente la clase en el jvm a través del cargador de clases Ahora que hemos confiado en los componentes que necesitamos a través de la dependencia del iniciador Spring-Boot, la información de clase de estos componentes está en el método seleccionado También se puede obtener, no se preocupe, sigamos analizando hacia abajo.

El método en este método getCandidateConfigurations, aprendido a través de la anotación del método, devuelve una lista de nombres de clase de la clase de configuración automática, el método llama al loadFactoryNamesmétodo, ver el método

imagen.gif

En el código anterior, puede ver que el configurador automático encontrará la clave correspondiente en factoryClass.getName()todos spring.factorieslos archivos bajo la ruta del sistema del proyecto pasada, para cargar las clases dentro. Seleccionaremos el archivo mybatis-spring-boot-autoconfigurebajo estespring.factories

imagen.gif

Ingresando org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration, mira principalmente el encabezado de la clase:

Descubrí que Spring @Configurationes como un springBean marcado con anotaciones, continúe mirando hacia abajo,

  • @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class}): cuando estas dos clases existen SqlSessionFactory.class, la clase de configuración se analizará; de lo contrario, esta clase de configuración no se analizará, tiene sentido, necesitamos que mybatis nos devuelva el objeto de sesión y debe haber clases relacionadas con la fábrica de sesiones.SqlSessionFactoryBean.classMybatisAutoConfiguration

  • @CondtionalOnBean(DataSource.class): Trate solo con fuentes de datos que hayan sido declaradas como beans.

  • @ConditionalOnMissingBean(MapperFactoryBean.class)Esta anotación significa que si el bean especificado por nombre no existe en el contenedor, se creará la inyección de bean; de lo contrario, no se ejecutará (el código fuente de esta clase es largo y el límite de espacio no se pega por completo)

La configuración anterior puede garantizar sqlSessionFactory、sqlSessionTemplate、dataSourceque los componentes requeridos por mybatis se puedan configurar automáticamente. @ConfigurationLa anotación ha proporcionado el contexto de Spring, por lo que el método de configuración de los componentes anteriores tiene el mismo efecto que la configuración a través del archivo mybatis.xml cuando se inicia Spring.

A través del análisis, podemos encontrar que siempre que exista una ruta de clase basada en el proyecto SpringBoot y el dataSourceBean se haya registrado en el contenedor SqlSessionFactory.class, SqlSessionFactoryBean.classse puede activar la configuración automática, lo que significa que solo necesitamos agregar varias dependencias requeridas por mybatis para el proyecto maven Se puede activar la configuración automática, pero si se introduce la dependencia nativa de mybatis, la clase de configuración automática debe modificarse para cada función integrada, por lo que no se obtendrá el efecto listo para usar.

Entonces, Spring-boot nos proporciona un iniciador unificado que puede configurar directamente las clases relacionadas, y las dependencias (mybatis) requeridas para activar la configuración automática son las siguientes:

Aquí están mybatis-spring-boot-startertodas las dependencias en el archivo pom.xml en el código fuente interceptado:

Debido a la naturaleza transitiva de las dependencias de maven, podemos confiar en todas las clases que deben configurarse automáticamente, siempre que confiemos en el iniciador para lograr funciones listas para usar. También muestra que Springboot simplifica la gran cantidad de configuración XML y la gestión de dependencias complejas que trae Spring Framework, lo que permite a los desarrolladores prestar más atención al desarrollo de la lógica empresarial.

por fin

Atentos a la cuenta oficial: Programador Chasing the Wind. Responda 003 Obtenga el último manual de preguntas de la entrevista Java 2020 (más de 200 páginas de documentos PDF)

Supongo que te gusta

Origin blog.csdn.net/Design407/article/details/107917917
Recomendado
Clasificación