Los principios básicos y conceptos básicos del código fuente de Spring Learning


Principios básicos

AnnotationConfigApplicationContext

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();

Rara vez usamos Spring de la manera anterior, pero usamos Spring MVC o Spring Boot, pero se basan esencialmente en este método y necesitan crear un ApplicationContext internamente.

  • SpringBoot crea AnnotationConfigApplicationContext
  • SpringMVC crea XmlWebApplicationContext, que se basa en xml similar a ClassPathXmlApplicationContext.

AnnotationConfigApplicationContext es la clase principal para investigación y estudio.

Proceso de carga de contenedores de COI

El proceso de carga del contenedor IoC se puede dividir en dos pasos.

  • Prepare, registre algunas clases de herramientas de infraestructura subyacentes para usar en el proceso de inicialización del contenedor, luego registre la clase de configuración y luego actualice el contenedor.
  • Escanee, analice varios beans configurados en BeanDefinition y guárdelos en BeanDefinitionMap
  • Recorra BeanDefinitionMap, produzca singletons y guárdelos en caché en el grupo singleton singletonObjects

Cómo crear un objeto en primavera

El proceso del nuevo AnnotationConfigApplicationContext (Config.class) es el proceso de carga del contenedor IoC, escaneado para generar BeanDefinition, recorrido para generar Bean singleton y finalmente llamar manualmente a getBean (beanName) para obtener el Bean correspondiente del caché.

Proceso de creación de beans (ciclo de vida)

clase -> Reflexión newInstance -> Objeto original -> Inyección de dependencia (asignación de propiedad) -> Un montón de Aware -> Antes de la inicialización (@PostConstruct) -> Inicialización (InitailizedBean) -> Después de la inicialización (AOP) -> Objeto proxy (Proxy objeto .target=objeto original) -> Bean

  • Creación de instancias, crear un objeto llamando a un determinado constructor de la clase a través de la reflexión. Si hay varios constructores, se realizará una selección, lo que se denomina constructor inferido.
  • Inyección de dependencia (asignación de propiedad), recorrido de campos en el objeto, asignación automática con @Autowired
  • Tejido consciente, determine si el objeto es (es decir, si la clase está implementada) BeanNameAware, ApplicationContextAware y otros conscientes, si es así, fuerce la llamada al método establecido definido por la estructura
  • @PostConstruct procesa, determina si hay un método con esta anotación y, de ser así, ejecuta el método
  • Inicializando el procesamiento de Bean, determine si el objeto es una instancia de la interfaz; de ser así, ejecute el método afterPropertiesSet definido por la interfaz
  • Método de método de inicio personalizado
  • Determine si se necesita AOP. Si no, devuelva el objeto actual. Si es necesario, genere un objeto proxy y devuélvalo.
  • Si es un frijol singleton, se almacena en el grupo singleton.

Prototipo singleton

El alcance de Bean se divide en dos tipos: singleton y prototipo.

  • Singleton: cada vez que getBean obtiene el mismo objeto, se almacenará en caché en el grupo singleton después de que se cree por primera vez.
  • Prototipo: cada vez que se crea getBean, no se colocará en el grupo singleton.

Los beans cargados de forma diferida no se crearán ni almacenarán en caché cuando se cargue el contenedor IoC, pero se crearán y almacenarán en caché cuando se utilicen.

constructor inferido

Al crear instancias, el constructor sin parámetros se utiliza de forma predeterminada (si se escriben parámetros, no habrá ningún constructor sin parámetros de forma predeterminada, a menos que se defina explícitamente)

Si hay un constructor sin parámetros, use el constructor sin parámetros. Si no hay un constructor sin parámetros, determine cuántos constructores parametrizados hay. Si solo hay un constructor parametrizado, use este constructor parametrizado. Si hay varios constructores sin parámetros, no puede confirmar cuál usar, por lo que Error: no se puede encontrar el constructor sin parámetros predeterminado.

Si hay varios constructores parametrizados, también puede agregar @Autowired para especificar qué constructor parametrizado usar.

¿De dónde viene el objeto de parámetro del constructor parametrizado?

  • Primero, busque el contenedor según el tipo. Si solo hay uno, úselo directamente. Si hay varios, filtre según el nombre. Si el nombre coincide, úselo directamente. Si no hay ninguna coincidencia, se generará un error. informó.

inyección de dependencia

Primero filtre el contenedor por tipo. Si solo hay una coincidencia, úsela directamente. Si hay varias coincidencias, entonces busque por nombre. Si hay una coincidencia, úsela directamente. Si no hay ninguna coincidencia, se informará un error .

Proxy dinámico AOP

En el último paso de la creación del bean, se juzgará si se necesita AOP y, de ser así, se utilizará un proxy dinámico.

El proceso general para determinar si se necesita AOP

  • Encuentra todos los aspectos
  • Recorra cada método y determine si se escriben anotaciones como @Before y @After.
  • Si está escrito, determine si el PointCut correspondiente coincide con el Bean creado actualmente.
  • Si coincide, significa que el Bean actual necesita realizar AOP.

El proceso general de hacer AOP con CGLib

  • Genere una clase proxy que herede la clase original. La clase proxy contiene el objetivo del objeto de clase original. Tenga en cuenta que el objeto de clase original pasa por el proceso de creación del Bean, incluida la inyección de dependencia, la inicialización y otros procesos.
  • La clase proxy anula los métodos de la clase original y eventualmente llamará al método correspondiente del objetivo, pero agregará lógica de aspecto antes y después, aproximadamente de la siguiente manera
  • El último bean creado devuelve un objeto proxy que contiene el objeto original.
public class UserService {
    
    
	@Autowired
	private OrderService orderService;
	public void test() {
    
    
		sout;
	}
}
class UserServiceProxy extend UserService {
    
    
	UserService target;
	public void test() {
    
    
		// @Before 的逻辑
		target.test();
		// @After 的逻辑
	}
}

La clase proxy hereda de la clase original, por lo que los campos de la clase original también están presentes en la clase proxy. Sin embargo, Spring no realiza inyección de dependencia para la clase proxy porque no es necesario.

El objeto proxy solo se usa para mejorar un determinado método del objeto original. Si necesita usar los campos de inyección de dependencia del objeto original en la lógica de aspecto, también puede hacer que el objeto original funcione y cada campo en el JoinPoint.getTarget()original El objeto ya ha sido inyectado.

asuntos

Después de agregar la anotación @Transactional a un método, se generarán una clase de proxy y un objeto de proxy para esta clase durante la etapa AOP.

El proceso del método de ejecución del objeto proxy de transacción.

  • Determinar si hay una anotación @Transactional en el método actual
  • Si es así, el administrador de transacciones crea una conexión a la base de datos (¿esta conexión no proviene del grupo de conexiones?)
  • Establezca la confirmación automática de la conexión en falso (cuando no hay anotaciones de transacción, la conexión se obtiene del grupo de conexiones y se envía automáticamente cada vez que se ejecuta un SQL)
  • Método de ejecución, SQL en método de ejecución
  • Llame al método de confirmación o reversión de la conexión

Razones por las que las anotaciones de transacciones están programadas para caducar

El fracaso de la transacción depende de qué objeto ejecuta el método de anotación @Transactional.

  • Objeto proxy: no caducará
  • Objeto original: no válido, porque lo que se ejecuta es un método normal y el objeto proxy no realiza ninguna mejora.

El ejemplo más común es que hay dos métodos de transacción a y b en la clase, y b se llamará en a. La ejecución de las transacciones de a y b por separado no será inválida, pero cuando b se ejecuta en a, la configuración en la anotación de transacción de b será inválida

Debido a que el proceso de ejecución de a es así, obtenga el objeto proxy de la clase, ejecute su a, primero pase por la lógica de aspecto, cree la conexión, configúrelo para que no se envíe automáticamente y luego ejecute target.a, que es a método del objeto original, el cuerpo principal en este momento Es el objeto original en lugar del objeto proxy. Cuando se ejecuta el método b, la esencia es esta.b, el sujeto sigue siendo el objeto original y no hay aspecto lógica, por lo que la configuración de anotación de transacción del método b en a no será válida.

Por supuesto, hay muchas otras razones que requieren un análisis detallado.

Muchas otras fallas de AOP tienen el mismo motivo, todas causadas por autollamadas.

Hay una solución, que es la autorreferencia. La clase depende de inyectarse a sí misma en self. En este momento, self es el objeto proxy. Cuando llame a b en a, use self.b. De esta manera, el cuerpo principal es el objeto proxy. Hay aspectos para fortalecer la lógica y los asuntos de b. La configuración tendrá efecto

¿Por qué la transacción @Configuration entra en vigor después de agregarla a continuación?

@Configuration
public class Config {
    
    
	@Bean
	public TransactionManager transationManager() {
    
    
		return new DataSourceTransactionManager(dataSource());
	}
	@Bean
	public JdbcTemplate jdbcTemplate() {
    
    
		return new JdbcTemplate(dataSource())
	}
	@Bean
	public DataSource dataSource() {
    
    
		return new DataSource();
	}
}
  • Sin agregar tiempo, llamar a dataSource() dos veces genera dos fuentes de datos diferentes. Al final, el administrador de transacciones y la plantilla usan fuentes de datos diferentes.
  • Además, habrá un procesamiento especial: dataSource() se considerará un Bean y el mismo objeto se pasará a ambos.

Al obtener una conexión en JdbcTemplate, comprobará si el actual es un entorno de transacción. Si es así, la conexión enlazada a subprocesos se obtendrá TransactionSynchronizationManager.getResource(dataSource);de la conexión creada por el administrador de transacciones. Debe utilizar la misma fuente de datos. objeto para obtener la misma conexión. De esta manera, las operaciones de confirmación y reversión del administrador de transacciones tendrán efecto en JdbcTemplate.

Idea principal

Hay muchas abstracciones y herramientas en el código fuente de Spring, que deben comprenderse de antemano para facilitar la lectura del código fuente.

Definición de frijol

BeanDefinition se utiliza para registrar información diversa sobre la configuración de Bean.

  • clase, indicando el tipo de Bean
  • alcance, que indica alcance de Bean, singleton o prototipo, etc.
  • lazyInit: indica si el Bean se carga de forma diferida
  • initMethodName: Indica el método que se ejecutará cuando se inicialice el Bean.
  • destroyMethodName: indica el método que se ejecutará cuando se destruya el Bean
  • Hay muchos más…

Hay dos formas de definir beans: declarativa y programática. Los beans definidos de varias maneras eventualmente se analizarán en BeanDefinition y se almacenarán en caché.

  • Declaración:
    • < frijol/>
    • @Frijol
    • @Componente(@Servicio,@Controlador)
  • Programático
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    
    // 生成一个BeanDefinition对象,并设置beanClass为User.class,并注册到ApplicationContext中
    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(User.class);
    context.registerBeanDefinition("user", beanDefinition);
    
    System.out.println(context.getBean("user"));
    

BeanDefinitionReader

Se utiliza para analizar recursos en BeanDefinition de acuerdo con ciertas reglas.

Lector de definiciones de frijol anotado

Una clase se puede analizar en BeanDefinition, incluidas las anotaciones en la clase (@Conditional, @Scope, @Lazy, @Primary, @DependsOn, @Role, @Description)

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(context);

// 将User.class解析为BeanDefinition
reader.register(User.class);

System.out.println(context.getBean("user"));

Lector de definiciones de xmlBean

Puede analizar beans configurados con etiquetas <bean/>

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context);
int i = reader.loadBeanDefinitions("spring.xml");

System.out.println(context.getBean("user"));

ClassPathBeanDefinitionScanner

Scanner, pero su función es similar a BeanDefinitionReader: puede escanear, escanear una determinada ruta de paquete y analizar las clases escaneadas.

Si la anotación @Component existe en la clase escaneada, la clase se analizará en una BeanDefinition

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.refresh();

ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.scan("com.coder");

System.out.println(context.getBean("userService"));

fábrica de frijoles

La interfaz raíz del contenedor Spring, la fábrica Bean, es responsable de crear y obtener beans y proporciona la definición de varios métodos getBean().

DefaultListableBeanFactory

BeanFactory tiene una clase de implementación central DefaultListableBeanFactory , que se puede usar directamente como BeanFactory y se puede usar en lugar de ApplicationContext, pero tiene menos funciones.

DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(User.class);

beanFactory.registerBeanDefinition("user", beanDefinition);

System.out.println(beanFactory.getBean("user"));

DefaultListableBeanFactory

La arquitectura de DefaultListableBeanFactory es la anterior, con muchas interfaces (capacidades) y clases.

  • AliasRegistry : admite la función de alias, define las funciones de registro/adquisición/juicio/eliminación de alias y admite la función de un BeanDefinition/Bean con múltiples nombres.
  • SimpleAliasRegistry , mantenido Map<String, String> aliasMap, aquí está la relación de muchos a uno entre alias y beanName, lo que hace que sea fácil encontrar el nombre original del alias (los alias también pueden tener alias), por supuesto, también puede encontrar todos los alias del original. nombre
  • BeanDefinitionRegistry : define las funciones de registro/adquisición/existencia/eliminación de BeanDefinition
  • SingletonBeanRegistry : define funciones como registro/existencia/obtener/obtener cantidad/obtener nombre de beans singleton
  • BeanFactory : Define las funciones de adquisición/existencia/obtención de alias/determinación del alcance de Bean, etc.
  • ListableBeanFactory : extiende BeanFactory y proporciona métodos para enumerar y recuperar instancias de beans en el contenedor. Puede obtener fácilmente los nombres de todos los beans, obtener beans por tipo y recuperar beans por condiciones.
  • HierarchicalBeanFactory : extiende BeanFactory y proporciona una estructura de contenedor jerárquico y un mecanismo de herencia. Cada subcontenedor puede administrar de forma independiente su propia definición e instancia de bean. Los contenedores secundarios pueden heredar los beans definidos en el contenedor principal y pueden anular o ampliar las definiciones de beans en el contenedor principal en el contenedor secundario. Puede lograr una mejor modularización y organización, y administrar y personalizar de manera flexible las definiciones y alcances de los beans.
  • AutowireCapableBeanFactory : extiende BeanFactory, proporciona la capacidad de ensamblar beans automáticamente y puede realizar funciones como inyección de dependencia, ensamblaje automático y desacoplamiento.
  • ConfigurableBeanFactory , además de BeanFactory Bean, también proporciona herramientas para configurar BeanFactory. Se agregaron configuraciones para configurar BeanFactory principal, cargador de clases (lo que indica que se puede especificar un cargador de clases para cargar clases) y configurar el analizador de expresiones Spring EL (lo que indica que BeanFactory puede analizar expresiones EL), establecer servicios de conversión de tipos (lo que indica que BeanFactory puede realizar la conversión de tipos), agregar BeanPostProcessor (lo que indica que BeanFactory admite postprocesadores de Bean), fusionar BeanDefinitions, destruir un Bean, etc.
  • ConfigurableListableBeanFactory : además de ConfigurableBeanFactory, también proporciona herramientas para analizar y modificar definiciones de beans y crear instancias previas de singletons.
  • DefaultSingletonBeanRegistry : Se utiliza principalmente para administrar y mantener el registro y la adquisición de beans singleton. Los objetos singleton están aquí
  • FactoryBeanRegistrySupport : Se utiliza principalmente para respaldar el registro y la adquisición de FactoryBean, factoryBeanObjectCache está aquí
  • AbstractBeanFactory : La función ya es muy completa, pero no puede ensamblar ni obtener nombres de bean automáticamente. Los beanPostProcessors están aquí
  • AbstractAutowireCapableBeanFactory : tiene la función de ensamblaje automático
  • DefaultListableBeanFactory : Es un componente clave del contenedor Spring y es responsable de gestionar el registro, fusión y búsqueda de BeanDefinitions, así como de la creación, ensamblaje y destrucción de Beans. Proporciona una rica funcionalidad y opciones de configuración flexibles y es una de las implementaciones de BeanFactory comúnmente utilizadas en aplicaciones Spring.

Contexto de aplicación

BeanFactory tiene una subinterfaz principal ApplicationContext , que se define de la siguiente manera

public interface ApplicationContext 
extends EnvironmentCapable, 
ListableBeanFactory, 
HierarchicalBeanFactory, 
MessageSource, 
ApplicationEventPublisher, 
ResourcePatternResolver

Insertar descripción de la imagen aquí

  • HierarchicalBeanFactory: Tiene la función de obtener el BeanFactory padre
  • ListableBeanFactory: Tiene la función de obtener beanNames
  • ResourcePatternResolver: cargador de recursos, que puede obtener múltiples recursos (recursos de archivos, etc.) al mismo tiempo
  • EnvironmentCapable: puede obtener el entorno de ejecución (no hay ninguna función para configurar el entorno de ejecución)
  • ApplicationEventPublisher: tiene la función de transmitir eventos (no tiene función de agregar detectores de eventos)
  • MessageSource: tiene capacidades de internacionalización

ApplicationContext se posiciona como el contexto de aplicación de Spring y es responsable de administrar y organizar varias partes de la aplicación. Desde un nivel de código, ApplicationContext es un BeanFactory. Desde un nivel arquitectónico, ApplicationContext es una existencia más avanzada que BeanFactory. Gobierna BeanFactory. EnvironmentCapable, MessageSource y otros componentes completan las funciones correspondientes, y BeanFactory es solo una parte de ellas.

De acuerdo con esta idea, GenericApplicationContext no hereda DefaultListableBeanFactory, sino que lo usa como un atributo. Todas las funciones heredadas de BeanFactory se confían a DefaultListableBeanFactory que posee para su ejecución, lo cual es muy razonable.

La interfaz ApplicationContext hereda ListableBeanFactory y HierarchicalBeanFactory, pero se posiciona como un BeanFactory de alto nivel y solo se centra en un cierto grado de funciones básicas de BeanFactory, y no requiere todas las funciones más potentes y detalladas de las capas media e inferior.

ApplicationContext tiene dos clases de implementación importantes

  • AnnotationConfigApplicationContext
  • ClassPathXmlApplicationContext
AnnotationConfigApplicationContext

Insertar descripción de la imagen aquí

  • Ciclo de vida : una interfaz común que define los métodos de control del ciclo de vida de inicio/detención.
  • ConfigurableApplicationContext : agregado, agregando detectores de eventos, agregando BeanFactoryPostProcessor, configurando el entorno, obteniendo ConfigurableListableBeanFactory y otras funciones
  • AbstractApplicationContext : implementa la función de contexto universal, el famoso método de actualización está aquí
  • GenericApplicationContext : clase de implementación concreta del contexto general de la aplicación
  • AnnotationConfigRegistry : se utiliza para anotar y configurar el contexto de la aplicación. Puede registrar una clase como BeanDefinition por separado (puede manejar la anotación @Configuration @Bean en esta clase)
  • AnnotationConfigApplicationContext : es una implementación de contexto de aplicación potente y cómoda, adecuada para el desarrollo basado en anotaciones. Realiza el registro y ensamblaje automatizado de Bean mediante el escaneo y el procesamiento de anotaciones, lo que reduce la tediosa configuración XML.
ClassPathXmlApplicationContext

Insertar descripción de la imagen aquí
También hereda AbstractApplicationContext, pero en comparación con AnnotationConfigApplicationContext, sus funciones no son tan poderosas como AnnotationConfigApplicationContext, por ejemplo, BeanDefinition no se puede registrar.

Mensaje InternacionalFuente

Insertar descripción de la imagen aquí

# messages.properties
test = 你好啊
# messages_en_US.properties
test = Hello
# messages_zh_CN.properties
test = 你好
@Configuration
public class MessageSourceConfig {
    
    

	@Bean
	public MessageSource messageSource() {
    
    
		ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
		resourceBundleMessageSource.setDefaultEncoding("UTF-8");
		resourceBundleMessageSource.setBasename("i18n.messages");
		return resourceBundleMessageSource;
	}

}
	public static void main(String[] args) {
    
    
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
		System.out.println(Locale.getDefault()); // zh_CN
		System.out.println(context.getMessage("test", null, Locale.getDefault())); // 你好
		System.out.println(context.getMessage("test", null, new Locale("zh_CN"))); // 你好
		System.out.println(context.getMessage("test", null, new Locale("en_US"))); // Hello
		System.out.println(context.getMessage("test", null, new Locale("de_DE"))); // 你好, 不存在, 走默认
	}
Carga de recursos getResource/getResources
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);

Resource resource1 = context.getResource("file:/C:/coder/develop/workspace/idea/mrathena/spring/spring-analyze-test/src/main/java/com/coder/UserService.java");
System.out.println(resource1.contentLength());
System.out.println(resource1.getFilename());

Resource resource2 = context.getResource("https://www.baidu.com");
System.out.println(resource2.contentLength());
System.out.println(resource2.getURL());

// Resource resource3 = context.getResource("classpath:spring.xml");
// System.out.println(resource3.contentLength());
// System.out.println(resource3.getURL());

Resource[] resources = context.getResources("classpath:com/coder/**/*.class");
for (Resource resource : resources) {
    
    
	System.out.println(resource.contentLength());
	System.out.println(resource.getFilename());
}
Obtener el entorno de ejecución
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);

Map<String, Object> systemEnvironment = context.getEnvironment().getSystemEnvironment();
for (Map.Entry<String, Object> entry : systemEnvironment.entrySet()) {
    
    
	System.out.println(entry.getKey() + "\t\t" + entry.getValue());
}

System.out.println("=======");

Map<String, Object> systemProperties = context.getEnvironment().getSystemProperties();
for (Map.Entry<String, Object> entry : systemProperties.entrySet()) {
    
    
	System.out.println(entry.getKey() + "\t\t" + entry.getValue());
}

System.out.println("=======");

// 会包含所有的属性源, 包括上面的 SystemEnvironment 和 SystemProperties
MutablePropertySources propertySources = context.getEnvironment().getPropertySources();
for (PropertySource<?> propertySource : propertySources) {
    
    
	System.out.println(propertySource);
}

System.out.println("=======");

System.out.println(context.getEnvironment().getProperty("NO_PROXY"));
System.out.println(context.getEnvironment().getProperty("sun.jnu.encoding"));
System.out.println(context.getEnvironment().getProperty("Path"));

Nota: Puede utilizar @PropertySource("classpath:spring.properties")el método para agregar parámetros en un archivo de propiedades al entorno de ejecución.

lanzamiento del evento
@ComponentScan
@Configuration
public class ApplicationListenerTest {
    
    

	@Bean
	public ApplicationListener<ApplicationEvent> applicationListener() {
    
    
		return event -> {
    
    
			System.out.println();
            System.out.println("接收到了一个事件\t\t" + event.getClass());
            System.out.println(event);
			if (event instanceof PayloadApplicationEvent) {
    
    
				System.out.println(((PayloadApplicationEvent) event).getPayload());
			}
        };
	}

	public static void main(String[] args) {
    
    
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationListenerTest.class);
		context.publishEvent("测试事件");
	}

}

conversión de tipo

En el código fuente de Spring, puede ser necesario convertir String a otros tipos, por lo que el código fuente de Spring proporciona algunas tecnologías para hacer que la conversión de tipos de objetos sea más conveniente. Con respecto a los escenarios de aplicación de conversión de tipos, lo encontrará cuando lea el código fuente. código más tarde mucho de.

Editor de propiedades

Supongo que te gusta

Origin blog.csdn.net/mrathena/article/details/132474313
Recomendado
Clasificación