머리말
이전 기사에 이어 스프링 암호 해독 -XML 파싱 및 Bean 등록에 이어 소스 코드를 계속 분석하겠습니다. 더 이상 고민하지 않고 자세한 소개를 함께 살펴 보겠습니다.
암호 해독
Spring의 XML 구성에는 두 가지 주요 범주의 선언이 있습니다. 하나는 <bean id="person"/> 와 같은 기본값이고, 다른 하나는 <tx:annotation-driven /> 와 같은 사용자 정의 하나입니다. 두 태그는 매우 다른 구문 분석 방법을 가지고 있습니다. ParseBeendefinitions 방법은 다른 태그로 사용되는 구문 분석 방법을 구별하는 데 사용됩니다. node.getNamespaceURI() 메소드를 통해 네임 스페이스를 가져 와서 기본 네임 스페이스인지 사용자 정의 네임 스페이스인지 확인한 다음 봄에 고정 된 네임 스페이스 http://www.springframework.org/schema/beans와 비교하십시오. 일관된 경우 parseDefaultElement(ele, delegate); 그렇지 않으면, 그것은 delegate.parseCustomElement(ele);
기본 태그의 구문 분석
ParsedeFaultElement는 4 개의 다른 태그 가져 오기, 별명, 콩 및 콩에 대해 다른 처리를 수행했습니다. 그중에서도 Bean 태그 분석이 가장 복잡하고 중요하므로 Bean에서 심도있는 분석을 시작할 것입니다. 이 태그의 분석 프로세스를 이해할 수 있다면 다른 태그의 분석이 자연스럽게 해결됩니다. 이전 기사에서는 간단히 설명합니다. 이 기사에서는 분석 모듈에 대해 자세히 설명합니다.
공개 클래스 DefaultBeanDefinitionDocumentReader는 BeanDefinitionDocumentReader {private void parsedEfaultElement (Element Ele, BeanDefinitionParserdelegate delegate) {// import tag parsing if (delegate.nodenameequals (ele, import_element)) {importbeanteRes (elevindefinitionres); } // alias tag parsing else if (delegate.nodenameequals (ele, alias_element)) {processaliasregistration (ele); } // bean tag 해상도 else if (delegate.nodenameequals (ele, bean_element)) {processBeendefinition (ele, delegate); } // import tag 해상도 else if (delegate.nodenameequals (ele, nested_beans_element)) {// beans 태그 해상도 재귀 메소드 doregisterbeandefinitions (ele); }}} 먼저, 수업에서 processBeanDefinition(ele, delegate) 분석하겠습니다
보호 된 void processBeendefinition (요소 Ele, BeanDefinitionParserdelegate delegate) {// 요소를 구문 분석하기위한 BeanDefinitionDelegate 클래스의 parsebeanDefinitionElement 메소드를 딜 로이트. if (bdholder! = null) {// 반환 된 bdholder가 비어 있지 않은 경우, 기본 레이블의 자식 노드 아래에 사용자 정의 속성이 있으면 사용자 정의 레이블을 다시 구문 분석해야합니다. 시도 {// 구문 분석이 완료되면 구문 분석 된 BDHolder를 등록해야합니다. 등록 작업은 BeanDefinitionReaderUtils.registerBeanDefinition의 RegisterBeendEfinition 메소드에 위임됩니다 (bdholder, getReaderConText (). getRegistry ()); } catch (beanDefinitionStoreException ex) {getReaderConText (). 오류 ( "이름으로 Bean 정의를 등록하지 못했습니다." + bdholder.getBeanName () + " '", ele, ex); } // 마지막으로, 관련 리스너에게 Bean에로드되었음을 알리기 위해 응답 이벤트가 발행됩니다. }}이 코드에서 :
스프링이 각 태그와 노드를 구문 분석하는 방법을 자세히 분석합시다.
콩 태그 분석
Public Class BeanDefinitionParserDelegate {@nullable public beandefinitionholder parsebeanDefinitionEment (요소 ele, @nullable beandefinition 포함) {// bean 태그 문자열의 ID 속성을 가져옵니다. // bean 태그 문자열의 이름 속성을 가져옵니다. nameattr = ele.getAttribute (name_attribute); List <string> aliases = new ArrayList <> (); if (stringUtils.haslength (nameattr)) {// 이름 속성의 값을 전달하고 문자열 번호로 나누고 (즉, 여러 이름이 구성 파일에서 구성된 경우 여기에서 처리하는 경우) String [] namearr = StringUtils.tokenizetoStringArray (nameattr, multi_value_attribute_delimiters); aliases.addall (arrays.aslist (namearr)); } 문자열 beanname = id; // ID가 비어 있으면 구성된 이름 속성을 ID (! stringUtils.hastext (beanname) &&! aliases.isempty ()) {beanname = aliases.remove (0); if (logger.isdebugenabled ()) {logger.debug ( "no xml 'id'지정 - '" + beanname + "' ''a bean name 및" + aliases + "를 별칭으로 사용하여 지정); }} if (containingbean == null) {// beanname의 고유성 및 별명을 확인하십시오. // 내부 코어는 사용 된 모든 Beanname 및 AliaseSuniqueness (Beanname, Aliases, ELE)를 저장하기 위해 사용 된 이름 컬렉션을 사용하는 것입니다. } // 다른 모든 속성을 GenericBeendefinition 객체에 추가로 구문 분석합니다. AbstractBeanDefinition BeanDefinition = parseBeanDefinitionElement (Ele, Beanname, ContainedBean); if (beandefinition! = null) {// Bean이 BeanName을 지정하지 않으면 기본 규칙을 사용 하여이 Bean if (! stringUtils.hastext (BeanName)) {if (containingBean! = null) {BeanName = beanname = eANNAME (beanname = eAdeRerateRutils.generateBeanname,) 진실); } else {beanname = this.readerConText.generateBeanName (beandefinition); // 일반 Bean 클래스 이름에 대한 별칭을 등록하십시오. 가능하면 // 생성기가 클래스 이름과 접미사를 반환 한 경우. // 이것은 스프링 1.2/2.0 거꾸로 호환성에 대해 예상됩니다. 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 ( "xml 'id'또는 'name'지정 -" + "생성 된 bean 이름을 사용하여 [" + beanname + "]); }} catch (예외 예) {error (ex.getMessage (), ele); 널 리턴; }} string [] aliasesArray = stringUtils.toStringArray (별칭); // BeanDefinitionHolder 객체에 정보를 캡슐화하면 새로운 BeanDefinitionHolder (BeanDefinition, Beanname, AliasesArray); } return null; }} 이 방법은 주로 ID, 이름, 별명 등과 같은 관련 속성을 처리하고 BeanName을 생성하고 과부하 기능 parseBeanDefinitionElement(ele, beanName, containingBean) 메소드에서 코어 태그 구문 분석을 완료합니다.
다음으로 parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean)
태그 구문 분석 작업을 완료하는 방법을 확인하십시오
콩 노드 및 속성 분석
@NullablePublic AbstractBeanDefinition ParsebeanDefinitionElement (요소 Ele, String Beanname, @Nullable BeanDefinition 포함 비) {this.parsestate.push (new Beanentry (BeanName)); // bean 태그 문자열의 클래스 속성을 가져옵니다. classname = null; if (ele.hasattribute (class_attribute)) {classname = ele.getAttribute (class_attribute) .trim (); } // bean 태그 문자열의 부모 속성을 가져옵니다. parent = null; if (ele.hasattribute (parent_attribute)) {parent = ele.getAttribute (parent_attribute); } try {// AbstractBeanDefinition을 호스트 속성으로 만들어 AbstractBeanDefinition bd = CreateBeanDefinition (className, parent); // Bean tag parsebeandefinitionattributes의 다양한 속성을 가져옵니다 (Ele, beanname, canningbean, bd); // 구문 분석 tag bd.setDescription (domutils.getChildElementValueByTagName (ele, descriptionElement)); // 메타 태그 구문 분석 (ELE, BD); // 구문 분석 lookup-method tag parselookupoverrideubelements (ele, bd.getMethodoverrides ()); // 구문 분석 대체 메드 태그 ParserePlacedMethodSubelements (ele, bd.getMethodoverrides ()); // 구문 분석 대체 메드 태그 ParserePlacedMethodSubelements (ele, bd.getMethodoverrides ()); // 구조 생성자-ARG 태그 ParseconstructorArgelements (ELE, BD); // Parse ParsePropertyElements (ELE, BD)를 구문 분석합니다. // 구문 분석 한정자 태그 구문 분석 (ELE, BD); bd.setResource (this.readerConText.getResource ()); Bd.SetSource (Extressource (ELE)); 반환 BD; } catch (classNotFoundException ex) {error ( "bean class [" " + classname +"] 찾을 수 없음 ", ele, ex); } catch (noclassDeffoundError err) {error ( "bean class [" " + classname +"]는 찾을 수 없음에 따라 다름 ", ele, err); } catch (Throwable ex) {error ( "Bean 정의 구문 분석 중"예기치 않은 실패 ", Ele, Ex); } 마침내 {this.parsestate.pop (); } return null;}다른 속성과 요소 (많은 요소와 속성이 있으므로 큰 워크로드이므로)를 추가로 구문 분석하고 일반적인 정의로 패키지하십시오. 이러한 속성과 요소를 구문 분석 한 후 Bean에 지정된 Beanname이 없음을 감지하면 기본 규칙을 사용하여 Bean의 콩나무를 생성합니다.
// beandefinitionparserdelegate.javaprotected AbstractBeanDefinition CreateBeanDefinition (@Nullable String ClassName, @Nullable String Parentname)이 ClassNotFoundException {return beanDefinitionReaderItils.createBeanDefinition (parentname, classname, thist.get.readerContext. beanDefinitionReaderUtils {public static acpractBeanDefinition CreateBeanDefinition (@Nullable String ParentName, @Nullable String ClassName, @Nullable ClassLoader ClassLoader)이 ClassNoTfoundException {GenericBeanDefinition BD = New GenericBeanDefinition (); // parentname은 bd.setparentName (parentname) 일 수 있습니다. // 클래스 로더가 비어 있지 않은 경우 // 전달 된 클래스 로더를 사용하여 동일한 가상 시스템으로 클래스 객체를로드하십시오. 그렇지 않으면 (className! = null) {if (classLoader! = null) {bd.setBeanClass (classUtils.Forname (className, classLoader)) if (className! = null) {ClassLoader 만 기록됩니다. } else {bd.setBeanClassName (className); }} 반환 bd; }}Beandefinition은 컨테이너에서 <ean>의 내부 표현이며 Beandefinition과 <ean>은 일대일입니다. 동시에 BeanDefinition은 Spring 구성 정보의 메모리 데이터베이스와 같은 BeanDefinitionRegistry에 등록됩니다.
지금까지 createBeanDefinition(className, parent); 완료되었으며, 우리는 속성을 호스팅하는 데 사용되는 초록 비밀 정화를 얻었습니다. parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); parsebeandefinitionattributes (ele, beanname, 포함 된 Bd); 콩의 다양한 태그 속성을 구문 분석하십시오.
Public Class BeanDefinitionParserdelegate {public acpractBeanDefinition ParseBeendefinitionAttributes (요소 Ele, String Beanname, @Nullable BeanDefinition, AbstractBeanDefinition Bd) {// ... 코드 의이 부분은 주로 지정된 특성을 포함하는지 여부를 결정합니다. 있는 경우 Bd.set (속성); 반환 BD; }}```'Bean'태그의 완전한 구문 분석이 종료되었습니다. 'Bean'태그 아래의 요소 구문 분석은 비슷합니다. 관심이 있으시면 소스 코드를 추적하고`Qualifier, Lookup-Method` (*'Bean'*보다 복잡하지 않음)과 같은 구문 분석 방법을 볼 수 있습니다. 사용자 정의 태그 컨텐츠는 다음 장에서 더 자세히 설명되어 있습니다.
마지막으로, 얻은 정보를 'BeanDefinitionHolder` 인스턴스로 캡슐화하십시오
``java // beandefinitionparserdelegate.java@nullablepublic beandefinitionholder parsebeandefinitionElement (요소, @nullable beandefinition이 포함) {// ... return new beandefinitionholder (beandefinition, beanname, aliasarray);구문 분석 된 Beandefinition을 등록하십시오
구성 파일을 구문 분석 한 후 Bean의 모든 특성을 얻었고 다음 단계는 Bean을 등록하는 것입니다.
public class beandefinitionreaderutils {public static void registerBeanDefinition (BeanDefinitionHolder DefinitedHolder, BeanDefinitionRegistry Registry)은 BeanDefinitionStoreException {// BeanName을 고유 한 식별자로 사용합니다. // Bean Registry.registerBeanDefinition의 핵심 코드를 등록합니다 (BeanName, DefinitionHolder.getBeanDefinition ()); // bean 문자열에 대한 모든 별칭을 등록 [] aliases = definitionHolder.getaliases (); if (aliases! = null) {for (String alias : aliases) {registry.registeralias (beanname, alias); }}}}위의 코드는 주로 두 가지 기능을 완료합니다. 하나는 Beanname을 사용하여 BeanDefinition을 등록하고 다른 하나는 별칭 등록을 완료하는 것입니다.
Beanname Register Beandefinition
공개 클래스 defaultListableBeanFactory {@override public void registerBeanDefinition (String BeanName, BeanDefinition BeanDefinition)은 BeanDefinitionStoreException {assert.hastext (BeanName, "Bean 이름이 비어 있지 않아야")를 던졌습니다. assert.notnull (beandefinition, "beandefinition은 무효가되어서는 안됩니다"); if (beandefinition instancef actractbeandefinition) {try {// 등록 전 마지막 확인, 여기서 확인은 XML 파일 확인과 다릅니다. // 주로 초록 비밀 정화 속성에서 메소드 오버 리드 검증을위한 것입니다. beandefinition) .validate (); } catch (beanDefinitionValidationException ex) {Throw New BeanDefinitionStoreException (BeanDefinition.getResourcedEscription (), BeanName, "Bean 정의 유효성 검증", Ex); }} beandefinition oldbeandefinition; // 캐시에서 beandefinition을 가져옵니다. OldBeanDefinition = this.beanDefinitionMap.get (beanname); if (OldBeanDefinition! = null) {// 캐시가있는 경우, 덮어 쓰기가 허용되는지 (! isallowBeanDefinitionOverriding ()) {throw new beandefinitionStoreException (beandefinition.getResourcedEscription (), beanname, " + beanmame +" 'ever beanname + "]를 등록 할 수 없습니다. OldBeanDefinition + "] Bound."); } else if (OldBeanDefinition.getRole () <beanDefinition.getRole ())) {// eg는 역할_Application입니다. 이제 role_support 또는 roble_infrastructure if (this.logger.iswarnenabled ()) {this.logger.warn ( " + eanmame eforeant wornated and a beanname eforeated bean wanterated and a beanname offornated 정의 : [ " + OldBeanDefinition +"]를 [ " + beandefinition +"]로 대체합니다. }} else if (! beandefinition.equals (OldBeanDefinition))) {if (this.logger.isinfoenabled ()) {this.logger.info ( " + beanname +" + " + beanname +" + "] +" + beandefinition + "]; }} else {if (if (this.logger.isdebugenabled ()) {this.logger.debug ( "Bean에 대한 Bean 정의 오리칭 '" + beanname + "' ''등가 정의로 : [" + OldBeanDefinition + "]를 [" + beandefinition + "]로 대체합니다. }} // 덮어 쓰기가 허용되면 beandefinition을 beandefinitionMap에 저장하십시오. } else {// Bean Creation이 시작되었는지 여부를 결정합니다. // 등록 된 BeanName 목록 업데이트 <String> 업데이트 된 Definitions = New ArrayList <> (this.BeanDefinitionNames.size () + 1); updatedDefinitions.Addall (this.BeanDefinitionNames); UpdatedDefinitions.add (Beanname); this.beanDefinitionNames = updatedDefinitions; if (th리 updatedsingletons.remove (beanname); this.manualsingletonnames = updatedsingletons; }}} else {// 아직 콩을 만들기 시작하지 않았습니다. this.beandefinitionnames.add (beanname); this.manualsingletonnames.remove (beanname); } this.frozenBeanDefinitionNames = null; } if (OldBeanDefinition! = null || containsingleton (beanname)) {// beanname restbeandefinition (beanname)에 해당하는 캐시를 재설정합니다. }}}별칭을 등록하십시오
BeanDefinition을 등록한 후 다음 단계는 별명을 등록하는 것입니다. 등록 된 별칭과 Beanname 간의 해당 관계는 별칭에 저장됩니다. RegisterAlias 메소드가 SimplealiasRegistry에서 구현됩니다.
공개 클래스 simplealiasregistry { / ** 별명에서 표준 이름으로 맵 < / private final map <string, string> aliasmap = new ConcurrenthashMap <> (16); public void Registeralias (문자열 이름, 문자열 별칭) {assert.hastext (이름, '이름'이 비어 있지 않아야한다 "); assert.hastext (별칭, 'alias'는 비어 있지 않아야한다 "); if (alias.equals (name)) {// beanname이 별칭과 동일하다면 별칭이 기록되지 않고 해당 별칭을 삭제합니다. } else {string registeredName = this.aliasmap.get (alias); if (registeredName! = null) {if (registeredName.equals (name)) {// 별명이 등록되었고 이름이 현재 이름과 동일하면 처리가 수행되지 않습니다. } // 별칭이 덮어 쓰기를 허용하지 않으면 (! allowaliasoverriding ()) {new new ImperalStateException ( "name ' + alias +"' ' + name + "': 이미 이름에 등록되어 있습니다. }} // 체크 루프는 a-> b b-> c c-> a와 같은 종속성을 가리 킵니다. 오류는 checkforaliascircle (이름, 별명); this.aliasmap.put (별칭, 이름); }}} checkForAliasCircle() 메소드를 통해 별칭 원형 의존성을 확인하십시오. A-> B가 존재하면 A-> C-> B가 다시 나타나면 예외가 발생합니다.
보호 된 void checkforaliascircle (문자열 이름, 문자열 별칭) {if (hasalias (alias, name)) {new alias ' " + alias +"'name ' + name + "'를 등록 할 수 없음 ' + 이름 +":' + name + " '' + alias +"이미 "); }} public boolean hasalias (문자열 이름, 문자열 별칭) {for (map.entry <string, string> entry : this.aliasmap.entryset ()) {string registeredname = actory.getValue (); if (registeredName.equals (name)) {String registeredalias = entry.getKey (); return (registeredalias.equals (별칭) || hasalias (Registeredalias, alias)); }} return false;}이 시점에서 별명 등록이 완료되었으며 다음과 같은 작업이 완료되었습니다.
알림을 보냅니다
청취자에게 구문 분석 및 등록을 알립니다
//defaultBeanDefinitionDocumentReader.javaprotected void ProcessBeanDefinition (Element Ele, BeanDefinitionParserDelegeTegeate Delegate) {// 등록 이벤트를 보냅니다. getReaderConText (). FireComponentRegistered (New BeanComponentDefinition (BDHolder));}FireComponent Registered Method는 리스너에게 구문 분석하고 작업을 등록하도록 알리는 데 사용됩니다. 여기서 구현은 확장 용입니다. 프로그램 개발자가 등록 된 BeanDefinition 이벤트를들을 필요가 있으면 청취자를 등록하고 처리 로직을 리스너에 쓸 수 있습니다. 현재 Spring 은이 이벤트 에서이 이벤트를 처리하지 않습니다.
readerContext는 클래스 XMLBeanDefinitionReader에서 CreateReaderContext를 호출 한 다음 fireComponentRegistered() 호출하여 생성됩니다.
별칭 태그 분석
Spring은 <alias name="person" alias="p"/> 에 대한 별칭 구성을 제공합니다. 태그 해상도는 프로세스리스트 리스트레이션 (요소 ELE) 메소드에서 수행됩니다.
공개 클래스 DefaultBeanDefinitionDocumentReader {Protected Void ProcessaliasRegistration (요소 Ele) {// Alisa 태그 이름 속성 문자열 이름 = ele.getAttribute (name_attribute); // ALISA 태그 ALISA 태그 ALIAS 속성 속성 문자열 별칭 = ele.getAttribute (alias_attribute); 부울 유효한 = true; if (! stringUtils.hastext (name)) {getReaderConText (). 오류 ( "이름은 비어 있지 않아야합니다", ele); 유효 = 거짓; } if (! stringUtils.hastext (alias)) {getReaderConText (). ERROR ( "별명이 비어 있지 않아야합니다", ele); 유효 = 거짓; } if (valiv) {try {// register alias getReaderConText (). getRegistry (). RegisterAlias (이름, 별칭); } catch (예외) {getReadErcOnText (). 오류 ( " + name +" ", ele, ex)가있는 Bean의 경우" + alias + "등록 실패" + alias + "; } // 별칭을 등록한 후 청취자에게 해당 프로세싱을 수행하도록 지시하십시오. }}}먼저, 별칭 태그 속성이 추출되고 확인됩니다. 확인이 통과 된 후 별명 등록이 수행됩니다. Bean 태그 분석에서 별명 등록 및 별칭 등록이 수행되었습니다. 나는 여기서 그것을 반복하지 않을 것입니다.
가져 오기 태그 분석
공개 클래스 DefaultBeanDefinitionDocumentReader {Protected void importBeandEfinitionResource (element ele) {// 가져 오기 태그 문자열의 리소스 속성 가져옵니다. // 존재하지 않으면 (! stringUtils.Hastext (location)) {getReaderConText (). 오류 ( "자원 위치가 비어 있지 않아야", ele); 반품; } // "$ {user.dir}"위치 = getReaderConText (). getEnvironment ()와 같은 자리 표시 자 속성 형식을 구문 분석합니다. set <Resource> realyResources = new LinkedHashset <> (4); // 리소스가 절대 경로인지 또는 상대 경로인지를 결정합니다. 부울 절대 elocation = false; {absolutelocation = resourcepatternutils.isurl (location) ||를 시도하십시오 resourceutils.touri (location) .isabsolute (); } catch (urisyntaxexception ex) {// 유명한 스프링 접두사 "classpath*:"} //가 아닌 경우 // 위치를 고려하여 // uri로 변환 할 수 없습니다. // 절대 경로 인 경우 해당 구성 파일은 주소에 따라 직접로드됩니다. 실제 소송); if (logger.isdebugenabled ()) {logger.debug (URL 위치에서 "imported" + importCount + "bean 정의 [" + location + "]); }} catch (beanDefinitionStoreException ex) {getReaderConText (). 오류 ( "URL 위치에서 Bean 정의를 가져 오지 못했습니다 [" + location + "]", ele, ex); }} else {try {int importCount; // 상대 경로 자원에 따라 리소스를로드합니다. RelativerSource = getReaderConText (). getResource (). CreaterElative (location); if (eleveriversource.exists ()) {importCount = getReaderConText (). getReader (). loadBeanDefinitions (RelativeResource); realyResources.add (RelativerSource); } else {string baseLocation = getReaderConText (). getResource (). getUrl (). toString (); importCount = getReaderConText (). getReader (). loadBeanDefinitions (stringUtils.AppLyRelativePath (BaseLocation, Location), RealdeResources); } if (logger.isdebugenabled ()) {logger.debug ( "imported" + importCount + "Bean 정의 상대 위치 [" + location + "]); }} catch (ioexception ex) {getReaderConText (). 오류 ( "현재 리소스 위치를 해결하지 못했습니다", Ele, ex); } catch (beanDefinitionStoreException ex) {getReaderConText (). 오류 ( "상대 위치에서 Bean 정의를 가져 오지 못했습니다 [" + location + "]", ele, ex); }} // 구문 분석 후 리스너 활성화 처리가 수행됩니다. resource [] actresarray = realyResources.toArray (새로운 resource [actualResources.size ()]); getReaderConText (). FireImportProcessed (위치, actresArray, ExtractSource (ELE)); }} 가져 오기 태그의 처리를 완료 한 후 첫 번째는 <import resource="beans.xml"/> resource 속성으로 표시된 경로를 얻은 다음 ${user.dir} 와 같은 경로에서 속성 자리 표시기를 구문 분석 한 다음 위치가 절대 경로인지 상대 경로인지를 결정하는 것입니다. 절대적인 경로 인 경우, 콩의 구문 분석 프로세스를 재귀 적으로 (loadBeanDefinitions(location, actualResources);) 라고 불리며 다른 분석을 수행합니다. 상대 경로 인 경우 절대 경로를 계산하고 구문 분석하십시오. 마지막으로, 리스너에게 알리고 구문 분석이 완료되었습니다.
요약
알려지지 않은 가을, 겨울, 봄, 여름이 몇 번 후에 모든 것이 원하는 방향을 따라갑니다 ...
좋아, 위는이 기사의 전체 내용입니다. 이 기사의 내용에 모든 사람의 연구 나 작업에 대한 특정 참조 가치가 있기를 바랍니다. 궁금한 점이 있으면 의사 소통을 위해 메시지를 남길 수 있습니다. Wulin.com을 지원 해주셔서 감사합니다.
말 해주세요
전체 텍스트 코드 : https://gitee.com/battcn/battcn-spring-source/tree/master/chapter1 (로컬 다운로드)