Prefacio
Después del artículo anterior del descifrado de primavera: análisis XML y registro de frijoles, continuemos analizando el código fuente. Sin más preámbulos, echemos un vistazo a la introducción detallada juntos.
Descifrado
Hay dos categorías principales de declaraciones en la configuración XML de Spring. Uno es el predeterminado, como <bean id="person"/> , y el otro es el personalizado, como <tx:annotation-driven /> . Las dos etiquetas tienen métodos de análisis muy diferentes. El método ParseBeanDefinitions se utiliza para distinguir los métodos de análisis utilizados por diferentes etiquetas. Obtenga el espacio de nombres a través node.getNamespaceURI() , determine si es un espacio de nombres predeterminado o un espacio de nombres personalizado, y compare con el espacio de nombres fijo http://www.springframework.org/schema/Beans en Spring. Si es consistente, use parseDefaultElement(ele, delegate); de lo contrario, es delegate.parseCustomElement(ele);
Analización de etiquetas predeterminadas
ParsedefaultElement ha realizado un procesamiento diferente para 4 etiquetas diferentes, alias, frijoles y frijoles. Entre ellos, el análisis de las etiquetas de frijoles es el más complejo e importante, por lo que comenzaremos un análisis en profundidad del Bean. Si podemos entender el proceso de análisis de esta etiqueta, el análisis de otras etiquetas se resolverá naturalmente. En el artículo anterior, lo describimos brevemente. En este artículo, lo discutiremos en detalle sobre el módulo de análisis.
clase pública DefaultBeanDefinitionDocumentReader implementa beandefinitionDocumentReader {private void parsedefaultElement (elemento ele, beandefinitionParserDelegate delegate) {// Parsing de importación de importación if (delegate.nodenameSequals (ele, import_element)) {importeDefinitionResource (ele); } // Elabsing de la etiqueta de alias más if (delegate.nodenameEquals (ele, alias_element)) {ProcessaliaSregistration (ELE); } // resolución de etiqueta de bean más if (delegate.nodenameEquals (ele, bean_element)) {ProcessBeanDefinition (ele, delegado); } // Importar resolución de etiqueta más if (delegate.nodenameEquals (ele, nested_beans_element)) {// beats de la etiqueta de etiqueta método recursivo doregisterBeanDefinitions (ELE); }}} Primero, analicemos processBeanDefinition(ele, delegate) en la clase
Process Process BeanDefinition protegido (Element ELE, BeandefinitionParserDelegate Delegate) {// delegar el método ParseBeanDefinitionElement de la clase BeandefinitionDelegate para el elemento Parsing BeanDefinitionHolder bdholder = delegate.parsebeanDefinitionElement (ELE); if (bdholder! = null) {// Cuando el bdholder devuelto no está vacío, si hay un atributo personalizado bajo el nodo infantil de la etiqueta predeterminada, debe analizar la etiqueta personalizada nuevamente bdholder = delegate.decorateBeanDefinitionInitionIntionRequired (ELE, bdholder); Pruebe {// Después de completar el análisis, el bdholder analizado debe registrarse. La operación de registro se delega al método de registro de BeandefinitionReaderUtilss.registerBeanDefinition (bdholder, getReaderContext (). GetRegistry ()); } Catch (BeandefinitionStoreException ex) {getReaderContext (). Error ("No se pudo registrar la definición de bean con el nombre '" + bdholder.getBeanName () + "'", ele, ex); } // Finalmente, se emite un evento de respuesta para notificar al oyente relevante que el bean ha sido cargado getReaderContext (). FireComponentRregistered (nuevo beanComponentDefinition (bdholder)); }}En este código:
Analicemos en detalle cómo se analiza la primavera cada etiqueta y nodo
Análisis de etiquetas de frijoles
clase pública beandefinitionParserDelegate {@nullable public beandefinitionholder ParseBeanDefinitionElement (Element ele, @nullable Beandefinition ContainsBean) {// Obtener el atributo de identificación de la etiqueta de bean String id = ELE.GetAttribute (id_attribute); // Obtener el atributo de nombre de la cadena de la etiqueta de bean nameattr = ele.getattribute (name_attribute); List <String> aliases = new ArrayList <> (); if (stringUtils.hasLength (nameAttr)) {// Pase el valor del atributo de nombre, y divídelo en un número de cadena (es decir, si se configuran múltiples nombres en el archivo de configuración, procesarlo aquí) String [] namearr = stringUtils.tokenizeToToStringArray (nameattr, multi_value_attribute_delimiters); aliases.addall (arrays.aslist (namearr)); } String beanName = id; // Si la ID está vacía, use el atributo de primer nombre configurado como ID if (! StringUtils.hastext (beanName) &&! Aliases.isEmpty ()) {beanName = aliases.remove (0); if (logger.isdeBugeNabled ()) {logger.debug ("no xml 'id' especificado - usando '" + beanName + "' como nombre de bean y" + alias + "como aliases"); }} if (contenerbean == null) {// Verifique la singularidad del nombre de frijoles y los alias // El núcleo interno es usar la colección usada de Names para guardar todos los nombre de frijoles y aliassunicidad (nombre de frijoles, alias, ele); } // analizar más todas las demás propiedades en el objeto genérico del objeto abstracto abstracto de beandefinition = parseBeanDefinitionElement (ele, beanname, contenedBean); if (beandefinition! = null) {// Si el bean no especifica beanName, luego use la regla predeterminada para generar beanName para este bean if (! stringUtils.hastext (beanName)) {try {if (conteningBean! = null) {beanName = beandefinitionRuTiLil.GenerateBeanName (beandefinition, this.readeREntEntEnter (), (verdadero). } else {beanName = this.ReaderContext.GenerateBeanName (beandefinition); // Registre un alias para el nombre de la clase de frijol simple, si aún es posible, // si el generador devolvió el nombre de la clase más un sufijo. // Esto se espera para la compatibilidad de primavera 1.2/2.0 hacia atrás. String beanClassName = beandefinition.getBeanClassName (); if (beanClassName! = null && beanName.startswith (beanClassName) && beanName.length ()> beanClassName.length () &&! this.readerContext.getRegistry (). IsBeanNameInuse (beanClassName) {aliases.add (BeanClassName); }} if (logger.isdeBugeNabled ()) {logger.debug ("ni xml 'id' ni 'nombre' especificado -" + "usando el nombre generado de frijoles [" + beanName + "]"); }} capt (excepción ex) {error (ex.getMessage (), ele); regresar nulo; }} String [] aliasesArray = stringUtils.ToStringArray (aliases); // Encapsular información en el beandefinitionholder Object Return New BeanDefinitionHolder (Beandefinition, BeanName, aliasesArray); } return null; }} Este método procesa principalmente atributos relacionados como ID, nombre, alias, etc., genera BeanName y completa el análisis de la etiqueta central en la función de sobrecarga parseBeanDefinitionElement(ele, beanName, containingBean) .
A continuación, concéntrese en parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean)
Vea cómo completa la operación de análisis de la etiqueta
Análisis de nodo de frijoles y atributos
@NullablePublic AbstractBeanDefinition ParseBeanDefinitionElement (Element ele, String BeanName, @Nullable Beandefinition ContAnsBean) {this.parsestate.push (new BeanEntry (BeanName)); // Obtener el atributo de clase de la cadena de la etiqueta de bean className = null; if (ele.hasattribute (class_attribute)) {classname = ele.getattribute (class_attribute) .trim (); } // Obtenga el atributo principal de la cadena de la etiqueta de bean parent = null; if (ele.hasattribute (parent_attribute)) {parent = ele.getattribute (parent_attribute); } try {// Cree AbstractBeanDefinition para alojar atributos AbstractBeanDefinition bd = createBeanDefinition (className, Parent); // Obtenga varios atributos de la etiqueta de bean parseBeanDefinitionAttributes (ele, beanName, que contiene bean, bd); // PARSE DESCRIPCIÓN Etiqueta bd.setDescription (domutils.getchildelementValueBytagName (ele, descripción_element)); // Parse Meta Tag Parsemetaelements (ele, BD); // etiqueta del método de búsqueda de analizador Parselookupoverridesubelements (ele, bd.getMethodoverrides ()); // etiqueta de método reemplazado ParsepraedMethodSubelements (ele, bd.getMethodoverRides ()); // etiqueta de método reemplazado ParsepraedMethodSubelements (ele, bd.getMethodoverRides ()); // Constructor de analizador-arg etiqueta parseconstructorArgelements (ELE, BD); // Propiedad de analizador ParsePropertyElements (ELE, BD); // PARSE EL Etiqueta del calificador Parsqualifierelements (ELE, BD); bd.setResource (this.readerContext.getResource ()); bd.setSource (ExtractSource (ELE)); regresar bd; } Catch (ClassNotFoundException ex) {Error ("Bean Class [" + ClassName + "] no encontrado", ELE, EX); } catch (noClassDefFoundError err) {error ("clase que bean class [" + classname + "] depende de no encontrado", ele, err); } catch (throwable ex) {error ("falla inesperada durante el análisis de la definición de frijoles", ele, ex); } finalmente {this.parsestate.pop (); } return null;}Más allá de otros atributos y elementos (hay muchos elementos y atributos, por lo que esta es una gran carga de trabajo) y empaquétalos en GenericBeanDefinition. Después de analizar estos atributos y elementos, si detecta que el bean no tiene un nombre de frijoles especificado, usa las reglas predeterminadas para generar un nombre de frijoles para el bean.
// beandefinitionParserDelegate.javaprotected AbstractBeanDefinition CreateBeanDefinition (@nullable String className, @nullable String ParentName) lanza classNotFoundException {return beandefinitionRitionRiTerUtils.createBeanDefinition (parentName, className, this.Readercontext.getBeanClassLoSlOnter ();); BeandefinitionReaderUtils {public static abstractBeanDefinition createBeanDefinition (@nullable String ParentName, @nullable String className, @nullable classloader classloader) lanza ClassNotFoundException {GenericBeanDefinition bd = new GenericBeanDefinition (); // ParentName puede estar vacío bd.setParentName (ParentName); // Si el carga de clases no está vacía //, use el cargador de clases pasado para cargar el objeto de clase con la misma máquina virtual. De lo contrario, solo se registra classLoader si (classname! = Null) {if (classloader! = Null) {bd.setBeanClass (classUtils.forname (className, classLoader)); } else {bd.setBeanClassName (className); }} return bd; }}Beandefinition es una representación interna de <Bean> en un contenedor, y Beandefinition y <Bean> son uno a uno. Al mismo tiempo, Beandefinition se registrará en BeanDefinitionRegistry, que es como la base de datos en memoria de la información de configuración de Spring.
Hasta ahora, createBeanDefinition(className, parent); se ha terminado, y también hemos obtenido la BeanDefinition de Abstracts utilizada para alojar los atributos. Echemos un vistazo a cómo parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); parseBeanDefinitionAttributes (ele, beanName, que contiene bean, bd); Analice varios atributos de etiqueta en frijoles.
clase pública BeandefinitionParserDelegate {public AbstractBeanDefinition ParseBeanDefinitionAttributes (Element ELE, String BeanName, @nullable Beandefinition contenSBean, AbstractBeanDefinition bd) {// ... omitir el código detallado, esta parte del código principalmente determina si contiene el atributo especificado a través de si más. Si lo hay, bd.set (atributo); regresar bd; }} `` `` `` `El análisis completo de la etiqueta `bean` ha terminado. El análisis del elemento debajo de la etiqueta `bean` es similar. Si está interesado, puede rastrear el código fuente y ver los métodos de análisis como `calificador, método de búsqueda '(*no complicado que` bean`*). El contenido de etiqueta personalizado es más detallado en el próximo capítulo.
Finalmente, encapsula la información obtenida en la instancia de 'beandefinitionholder`
`` `java // beandefinitionParserDelegate.java@nullablePublic BeandefinitionHolder ParseBeanDefinitionElement (Element ele, @nullable Beandefinition contiene) {// ... return New BeandefinitionLoholder (BeanDefinition, BeanName, aliasesArray);}Registre una grandefinición de beansed
Después de analizar el archivo de configuración, hemos obtenido todas las propiedades del bean, y el siguiente paso es registrar el bean
clase pública beandefinitionReaderUtils {public static void registerBeanDefinition (beandefinitionholder DefinitionHolder, BeandefinitionRegistry Registry) lanza BeandefinitionStoreException {// Use BeanName como una cadena Identificadora única = Definitivamente Tolder.getBeanName (); // Registre el código central del registro de Bean. // Registre todos los alias para la cadena de bean [] aliases = definitionHolder.getaliases (); if (aliases! = null) {for (string alias: alias) {registry.registeralias (beanName, alias); }}}}El código anterior completa principalmente dos funciones: una es registrar Beandefinition usando BeanName, y el otro es completar el registro de alias.
BeanName Register Beandefinition
Public Class DefaultListableBeanFactory {@Override Void RegisterBeanDefinition (String BeanName, Beandefinition Beandefinition) arroja beandefinitionStoreException {afirmo.hastext (beanName, "el nombre de bean no debe estar vacío"); Afirmar.notnull (beandefinition, "beandefinition no debe ser nulo"); if (beandefinition instance of abstractBeanDefinition) {try {// La última verificación antes del registro, la verificación aquí es diferente de la verificación de archivos XML // es principalmente para el método Overrides La verificación en la propiedad de BeanDefinition // Compruebe si el método Coexist coexist con el método de fábrica o el método Overrides no existe en total ((((Abstract BeanDefinition) beandefinition) .validate (); } Catch (beandefinitionValidationException ex) {tire nuevo beandefinitionstoreException (beandefinition.getResourceDescription (), beanName, "validación de la definición de bean fallida", ex); }} Beandefinition OldBeanDefinition; // Obtener la beandefinition en el caché OldBeanDefinition = this.BeanDefinitionMap.get (BeanName); if (OldBeanDefinition! = NULL) {// Si hay un caché, determine si la sobrescribe está permitida if (! isLlowBeanDefinitionOverRiding ()) {Throw New BeanDefinitionStoreException (beandefinition.getResourcedescription (), beanNeMe, "no se puede registrar la definición de bean [" + beandefinition + "] para bean '" OldBeanDefinition + "] Bound."); } más if (OldBeanDefinition.getRole () <beandefinition.getRole ()) {// eg fue role_application, ahora anulando con role_support o role_infrastructure if (this.logger.iseSwarnenabled () {this.logger.warn ("anular la definición de bean de bean" + "Gean ' +" Guetwork con un bean de bean' + "Geathere-Geatreated con un bean de bean ' +" + "Gework con una definición de Bean' +" Geathere-Geatreated con una definición de bean ' + "Gehork con una definición de bean' +" Gework con la definición de Bean ' + "Gework con la definición de Bean' +" Definición: reemplazar [" + OldBeanDefinition +"] con [" + beandefinition +"] "); }} else if (! beandefinition.equals (OldBeanDefinition)) {if (this.logger.isInfoEnabled ()) {this.logger.info ("Definición de bean de bean para bean" + beanName + "'con una definición diferente: reemplazar [" + OldBeanDefinition + "] con [" + "" " }} else {if (this.logger.isdeBugeNabled ()) {this.logger.debug ("Definición de frijol anulante para bean '" + beanName + "' con una definición equivalente: reemplazar [" + OldBeanDefinition + "] con [" + beandefinition + "]"); }} // Si se permite sobrescribir, guarde beandefinition en beandefinitionmap this.beandefinitionmap.put (beanName, beandefinition); } else {// Determine si la creación de bean ha comenzado si (hasBeanCreationStarted ()) {// ya no puede modificar los elementos de recolección de tiempo de inicio (para iteración estable) sincronizada (this.beanDefinitionMap) {// Guardar beandefinition en beandefinitionmap this.beanDefinitionMap.put (beanName, beandefinition); // Actualizar la lista registrada de BeanName <String> UpdatedDefinitions = new ArrayList <> (this.BeanDefinitionNames.size () + 1); actualizadoDefinitions.Addall (this.BeanDefinitionNames); actualizadoDefinitions.Add (BeanName); this.BeanDefinitionNames = UpdateDEDEFINITIONS; if (this.ManualSingLetonNames.Contains (beanName)) {set <string> updatedSingletons = new LinkedHashset <> (this.ManualSingLetonNames); UpdateDsingletons.remove (BeanName); this.ManualSingLetOnNames = UpdateDsingletons; }}} else {// No he comenzado a crear frijoles todavía esto.beandefinitionmap.put (beanName, beandefinition); this.BeanDefinitionNames.Add (BeanName); this.ManualsingletonNames.remove (BeanName); } this.frozenBeanDefinitionNames = null; } if (OldBeanDefinition! = NULL || contieneSingleton (beanName)) {// Restablecer el caché correspondiente a BeanName ResetBeanDefinition (BeanName); }}}Registre un alias
Después de registrar BeanDefinition, el siguiente paso es registrar alias. La relación correspondiente entre el alias registrados y el nombre de la frijoles se almacena en aliasmap. Encontrará que el método de registro de registro se implementa en simpliaaliasregistry
Clase pública simpliaLiaSegistry { / ** Mapa del alias al nombre canónico* / Mapa final privado <String, String> aliasmap = new ConcurrenthashMap <> (16); public void RegisterAlias (nombre de cadena, alias de cadena) {afirmo.hastext (nombre, "'nombre' no debe estar vacío"); Afirmar.hastext (alias, "'alias' no debe estar vacío"); if (alias.equals (name)) {// Si el nombre de la frijoles es el mismo que el alias, el alias no se registrará y eliminará el alias correspondiente this.aliasmap.remove (alias); } else {String registrado de registro = this.aliasmap.get (alias); if (RegisterEdName! = NULL) {if (RegisterEdName.equals (name)) {// Si el alias ha sido registrado y el nombre señalado es el mismo que el nombre actual, no se realizará ningún procesamiento; } // Si el alias no permite sobrescribir, se lanzará una excepción si (! UnlInsisIsAverriding ()) {Throw New IlegalStateException ("No se puede registrar alias '" + alias + "' para el nombre '" + name + "': ya está registrado para el nombre '" + registrado del nombre registrado + "'."); }} // El bucle de verificación puntos a dependencias como A-> B B-> C C-> A, se produce un error checkForAliaScircle (nombre, alias); this.aliasmap.put (alias, nombre); }}} Verifique la dependencia circular de alias a través del método checkForAliasCircle() . Cuando A -> B existe, si A -> C -> B aparece nuevamente, se lanzará una excepción:
protegido void checkForAliaScircle (name de cadena, alias de cadena) {if (Hasalias (alias, name)) {throw new IlegalStateException ("No se puede registrar alias '" + alias + "' para el nombre '" + name + ": referencia circular -'" + name + "'es un alias directo o indirecto para'" + alias + "ya"); }} public boolean Hasalias (name de cadena, alias de cadena) {for (map.entry <string, string> Entry: this.aliasmap.entryset ()) {String RegisterEdName = Entry.getValue (); if (registredName.equals (name)) {String RegisterEdalias = Entry.getKey (); return (registredalias.equals (alias) || Hasalias (Registredalias, alias)); }} return false;}En este punto, se ha completado el registro de alias y se han completado las siguientes tareas.
Enviar notificación
Notifique al oyente que esté analizado y registrado
//DefaultBeanDefinitionDocumentReader.javaprotected Void ProcessBeanDefinition (Element ELE, BeandefinitionParserDelegate Delegate) {// Envía un evento de registro. getReaderContext (). FireComponentRegistered (nuevo BeanComponentDefinition (bdholder));}El método FireComponentRregistered se utiliza para notificar al oyente a analizar y registrar el trabajo. La implementación aquí es solo para la extensión. Cuando el desarrollador del programa necesita escuchar el evento registrado de BeanDefinition, puede registrar el oyente y escribir la lógica de procesamiento en el oyente. Actualmente, Spring no maneja este evento en este evento
El ReaderContext se genera llamando a CreateReReContext en la clase XMLBeanDefinitionReader, y luego llamando fireComponentRegistered()
Análisis de etiquetas de alias
Spring proporciona configuración de alias para <alias name="person" alias="p"/> . La resolución de la etiqueta se realiza en el método Processaliassregistration (Element ELE).
Clase pública DefaultBeanDefinitionDocumentReader {protegido Void ProcessaliasRregistration (Element ele) {// Obtener el nombre de la etiqueta Alisa String String Name = Ele.getAttribute (name_attribute); // Obtener la etiqueta Alisa TAG ALISA ATRIBITE DE ATRIBUTO DE ATRIBUTO DE ATRIBILLO = ELE.GETATTRIBUD (alias_attribute); boolean válido = verdadero; if (? válido = falso; } if (? válido = falso; } if (válido) {try {// Registre alias getReaderContext (). getRegistry (). RegisterAlias (name, alias); } catch (Exception Ex) {getReaderContext (). Error ("Error al registrar alias" + alias + "'para bean con nombre'" + nombre + "'", ele, ex); } // Después de registrar el alias, dígale al oyente que realice el procesamiento correspondiente GetReaderContext (). FirealiasRegistered (Nombre, Alias, ExtractSource (ELE)); }}}Primero, el atributo de la etiqueta de alias se extrae y verifica. Después de aprobar la verificación, se lleva a cabo el registro de alias. Se han realizado registro de alias y registro de alias en el análisis de la etiqueta de frijoles. No lo repetiré aquí.
Análisis de etiqueta de importación
clase pública DefaultBeanDefinitionDocumentReader {protegido void importeBeanDefinitionResource (elemento ele) {// Obtenga el atributo de recursos de la ubicación de la cadena de la etiqueta de importación = ele.getattribute (resource_attribute); // Si no existe, no se realiza ningún procesamiento si (! StringUtils.hastext (ubicación)) {getReaderContext (). Error ("La ubicación del recurso no debe estar vacía", ele); devolver; } // formato de atributo de marcador de posición de análisis como "$ {user.dir}" ubicación = getReaderText (). GetenVironment (). ResolverequiredplaceHolders (ubicación); Establecer <RUCHUSCE> REALRESOURCES = new LinkedHashset <> (4); // Determinar si el recurso es una ruta absoluta o una ruta relativa boolean absolutelocation = false; Pruebe {Absolutelocation = ResourcePatternUtils.isurl (ubicación) || Recursceutils.touri (ubicación) .IsabSolute (); } Catch (UrisyntaxException ex) {// No se puede convertir a un URI, considerando la ubicación relativa // a menos que sea el conocido prefijo de primavera "classpath*:"} // Si es una ruta absoluta, el archivo de configuración correspondiente se cargará directamente de acuerdo con la dirección if (absolutelocational) {intent {int importcunt = = getReaderContext (). if (logger.isDebugeNabled ()) {logger.debug ("importado" + importación + "definiciones de frijoles desde la ubicación de la url [" + ubicación + "]"); }} Catch (BeandefinitionStoreException ex) {getReaderContext (). Error ("Error al importar las definiciones de Bean desde la ubicación de URL [" + ubicación + "]", ele, ex); }} else {try {int importación; // Cargue el recurso de acuerdo con la ruta relativa RECURSE RELATIGINGRESource = GetReaderContext (). GetResource (). CreaterLative (ubicación); if (relativeResource.exists ()) {importeCount = getReaderContext (). getReader (). LoadBeanDefinitions (RelativeResource); RealResources.Add (RelativeResource); } else {string baselocation = getReaderContext (). getResource (). getUrl (). toString (); importCount = getReaderContext (). getReader (). LoadBeanDefinitions (StringUtils.applyRelativePath (Baselocation, ubicación), RealResources); } if (logger.isdeBugeNabled ()) {logger.debug ("importado" + importación + "definiciones de frijoles desde la ubicación relativa [" + ubicación + "]"); }} catch (ioException ex) {getReaderContext (). Error ("Error a resolver la ubicación de recursos actuales", ELE, EX); } Catch (BeandefinitionStoreException ex) {getReaderContext (). Error ("No se pudo importar las definiciones de Bean desde la ubicación relativa [" + ubicación + "]", ele, ex); }} // Después de analizar, se realiza el procesamiento de activación del oyente. Recursos [] actresArray = realresources.toarray (nuevo recurso [realresurces.size ()]); getReaderContext (). FireImprocessed (ubicación, ActresArray, ExtractSource (ELE)); }} Después de completar el procesamiento de la etiqueta de importación, lo primero es obtener la ruta representada por el atributo de recursos <import resource="beans.xml"/> , luego analizar el marcador de posición del atributo en la ruta, como ${user.dir} , y luego determinar si la ubicación es una ruta absoluta o una ruta relativa. Si se trata de una ruta absoluta, el proceso de análisis del bean se llama recursivamente (loadBeanDefinitions(location, actualResources);) para realizar otro análisis. Si se trata de una ruta relativa, calcule la ruta absoluta y analízala. Finalmente, notifique al oyente y se completa el análisis.
Resumir
Después de unos pocos otoño, invierno, primavera y verano desconocidos, todo seguirá la dirección que desee ...
De acuerdo, lo anterior es todo el contenido de este artículo. Espero que el contenido de este artículo tenga cierto valor de referencia para el estudio o el trabajo de todos. Si tiene alguna pregunta, puede dejar un mensaje para comunicarse. Gracias por su apoyo a Wulin.com.
Di algo
Código de texto completo: https://gitee.com/battcn/battcn-spring-source/tree/master/chapter1 (descarga local)