前言
緊跟上篇 spring解密 - xml解析 與 bean注冊 ,我們接著往下分析源碼,話不多說了,來一起看看詳細的介紹吧。
在 spring 的 xml 配置里面有兩大類聲明,一個是默認的如 <bean id="person" class="com.battcn.bean.person"/>
,另一類就是自定義的如<tx:annotation-driven />
,兩種標簽的解析方式差異是非常大的。parsebeandefinitions 方法就是用來區分不同標簽所使用的解析方式。通過 node.getnamespaceuri()
方法獲取命名空間,判斷是默認命名空間還是自定義命名空間,并與 spring 中固定的命名空間 http://www.springframework.org/schema/beans 進行比對,如果一致則采用parsedefaultelement(ele, delegate);
否則就是delegate.parsecustomelement(ele);
默認標簽的解析
parsedefaultelement 對 4 種不同的標簽 import、alias、bean、beans 做了不同的處理,其中 bean 標簽的解析最為復雜也最為重要,所以我們將從 bean 開始深入分析,如果能理解此標簽的解析過程,其他標簽的解析自然會迎刃而解。上一篇中只是簡單描述了一下,本篇我們圍繞解析模塊詳細的探討一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class defaultbeandefinitiondocumentreader implements beandefinitiondocumentreader { private void parsedefaultelement(element ele, beandefinitionparserdelegate delegate) { // import 標簽解析 if (delegate.nodenameequals(ele, import_element)) { importbeandefinitionresource(ele); } // alias 標簽解析 else if (delegate.nodenameequals(ele, alias_element)) { processaliasregistration(ele); } // bean 標簽解析 else if (delegate.nodenameequals(ele, bean_element)) { processbeandefinition(ele, delegate); } // import 標簽解析 else if (delegate.nodenameequals(ele, nested_beans_element)) { // beans標簽解析 遞歸方式 doregisterbeandefinitions(ele); } } } |
首先我們來分析下當類中的 processbeandefinition(ele, delegate)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
protected void processbeandefinition(element ele, beandefinitionparserdelegate delegate) { // 委托beandefinitiondelegate類的parsebeandefinitionelement方法進行元素解析 beandefinitionholder bdholder = delegate.parsebeandefinitionelement(ele); if (bdholder != null ) { // 當返回的bdholder不為空的情況下若存在默認標簽的子節點下再有自定義屬性,還需要再次對自定義標簽進行解析 bdholder = delegate.decoratebeandefinitionifrequired(ele, bdholder); try { // 解析完成后需要對解析后的bdholder進行注冊,注冊操作委托給了beandefinitionreaderutils的registerbeandefinition方法 beandefinitionreaderutils.registerbeandefinition(bdholder, getreadercontext().getregistry()); } catch (beandefinitionstoreexception ex) { getreadercontext().error( "failed to register bean definition with name '" + bdholder.getbeanname() + "'" , ele, ex); } // 最后發出響應事件,通知相關監聽器這個bean已經被加載 getreadercontext().firecomponentregistered( new beancomponentdefinition(bdholder)); } } |
這段代碼中:
- 首先委托 beandefinitionparsedelegate 對節點做了解析,并返回了一個 beandefinitionholder 的實例,在這個實例中已經包含了配置文件中配置的各種屬性了
- 如果在當前子節點中存在自定義屬性,則還需要對自定義標簽進行解析
- 解析完成后,需要對解析后的 bdholder 進行注冊,同樣注冊操作委托給了 beandefinitionreaderutils
- 最后發出響應事件,通知相關監聽器這個 bean 已經被加載
下面我們詳細分析下,spring 是如何解析各個標簽和節點的
bean 標簽解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public class beandefinitionparserdelegate { @nullable public beandefinitionholder parsebeandefinitionelement(element ele, @nullable beandefinition containingbean) { // 獲取bean標簽的id屬性 string id = ele.getattribute(id_attribute); // 獲取bean標簽的name屬性 string nameattr = ele.getattribute(name_attribute); list<string> aliases = new arraylist<>(); if (stringutils.haslength(nameattr)) { // 將name屬性的值通過,; 進行分割 轉為字符串數字(即在配置文件中如配置多個name 在此做處理) string[] namearr = stringutils.tokenizetostringarray(nameattr, multi_value_attribute_delimiters); aliases.addall(arrays.aslist(namearr)); } string beanname = id; // 如果id為空 使用配置的第一個name屬性作為id if (!stringutils.hastext(beanname) && !aliases.isempty()) { beanname = aliases.remove( 0 ); if (logger.isdebugenabled()) { logger.debug( "no xml 'id' specified - using '" + beanname + "' as bean name and " + aliases + " as aliases" ); } } if (containingbean == null ) { // 校驗beanname和aliases的唯一性 // 內部核心為使用usednames集合保存所有已經使用了的beanname和alisa checknameuniqueness(beanname, aliases, ele); } // 進一步解析其他所有屬性到genericbeandefinition對象中 abstractbeandefinition beandefinition = parsebeandefinitionelement(ele, beanname, containingbean); if (beandefinition != null ) { // 如果bean沒有指定beanname 那么使用默認規則為此bean生成beanname if (!stringutils.hastext(beanname)) { try { if (containingbean != null ) { beanname = beandefinitionreaderutils.generatebeanname(beandefinition, this .readercontext.getregistry(), true ); } else { beanname = this .readercontext.generatebeanname(beandefinition); // register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // this is expected for spring 1.2/2.0 backwards compatibility. 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( "neither xml 'id' nor 'name' specified - " + "using generated bean name [" + beanname + "]" ); } } catch (exception ex) { error(ex.getmessage(), ele); return null ; } } string[] aliasesarray = stringutils.tostringarray(aliases); // 將信息封裝到beandefinitionholder對象中 return new beandefinitionholder(beandefinition, beanname, aliasesarray); } return null ; } } |
該方法主要處理了 id、name、alias 等相關屬性,生成了 beanname,并且在重載函數 parsebeandefinitionelement(ele, beanname, containingbean)
方法中完成核心的標簽解析。
接下來重點分析parsebeandefinitionelement(element ele, string beanname, @nullable beandefinition containingbean)
看下它是如何完成標簽解析操作的
bean 節點與屬性解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
@nullable public abstractbeandefinition parsebeandefinitionelement( element ele, string beanname, @nullable beandefinition containingbean) { this .parsestate.push( new beanentry(beanname)); // 獲取bean標簽的class屬性 string classname = null ; if (ele.hasattribute(class_attribute)) { classname = ele.getattribute(class_attribute).trim(); } // 獲取bean標簽的parent屬性 string parent = null ; if (ele.hasattribute(parent_attribute)) { parent = ele.getattribute(parent_attribute); } try { // 創建用于承載屬性的abstractbeandefinition abstractbeandefinition bd = createbeandefinition(classname, parent); // 獲取bean標簽各種屬性 parsebeandefinitionattributes(ele, beanname, containingbean, bd); // 解析description標簽 bd.setdescription(domutils.getchildelementvaluebytagname(ele, description_element)); // 解析meta標簽 parsemetaelements(ele, bd); // 解析lookup-method標簽 parselookupoverridesubelements(ele, bd.getmethodoverrides()); // 解析replaced-method標簽 parsereplacedmethodsubelements(ele, bd.getmethodoverrides()); // 解析constructor-arg標簽 parseconstructorargelements(ele, bd); // 解析property標簽 parsepropertyelements(ele, bd); // 解析qualifier標簽 parsequalifierelements(ele, bd); bd.setresource( this .readercontext.getresource()); bd.setsource(extractsource(ele)); return bd; } catch (classnotfoundexception ex) { error( "bean class [" + classname + "] not found" , ele, ex); } catch (noclassdeffounderror err) { error( "class that bean class [" + classname + "] depends on not found" , ele, err); } catch (throwable ex) { error( "unexpected failure during bean definition parsing" , ele, ex); } finally { this .parsestate.pop(); } return null ; } |
進一步解析其他屬性和元素(元素和屬性很多,所以這是一個龐大的工作量)并統一封裝至 genericbeandefinition 中, 解析完成這些屬性和元素之后,如果檢測到 bean 沒有指定的 beanname,那么便使用默認的規則為 bean 生成一個 beanname。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// beandefinitionparserdelegate.java protected abstractbeandefinition createbeandefinition( @nullable string classname, @nullable string parentname) throws classnotfoundexception { return beandefinitionreaderutils.createbeandefinition( parentname, classname, this .readercontext.getbeanclassloader()); } public class beandefinitionreaderutils { public static abstractbeandefinition createbeandefinition( @nullable string parentname, @nullable string classname, @nullable classloader classloader) throws classnotfoundexception { genericbeandefinition bd = new genericbeandefinition(); // parentname可能為空 bd.setparentname(parentname); // 如果classloader不為空 // 則使用傳入的classloader同一虛擬機加載類對象 否則只記錄classloader if (classname != null ) { if (classloader != null ) { bd.setbeanclass(classutils.forname(classname, classloader)); } else { bd.setbeanclassname(classname); } } return bd; } } |
beandefinition 是 <bean> 在容器中的內部表示形式,beandefinition 和 <bean> 是一一對應的。同時 beandefinition 會被注冊到 beandefinitionregistry 中,beandefinitionregistry 就像 spring 配置信息的內存數據庫。
至此 createbeandefinition(classname, parent);
已經說完了,而且我們也獲得了 用于承載屬性的abstractbeandefinition,接下來看看 parsebeandefinitionattributes(ele, beanname, containingbean, bd);
是如何解析 bean 中的各種標簽屬性的
1
2
3
4
5
6
7
8
|
public class beandefinitionparserdelegate { public abstractbeandefinition parsebeandefinitionattributes(element ele, string beanname, @nullable beandefinition containingbean, abstractbeandefinition bd) { // ...省略詳細代碼,該部分代碼主要就是通過 if else 判斷是否含有指定的屬性,如果有就 bd.set(attribute); return bd; } } ``` |
`bean` 標簽的完整解析到這就已經全部結束了,其中 `bean` 標簽下的元素解析都大同小異,有興趣的可以自己跟蹤一下源代碼看看 `qualifier、lookup-method` 等解析方式(*相對 `bean` 而言不復雜*)。自定義標簽內容較多會在下一章詳細介紹。
最后將獲取到的信息封裝到 `beandefinitionholder` 實例中
1
2
3
4
5
6
7
|
``` java // beandefinitionparserdelegate.java @nullable public beandefinitionholder parsebeandefinitionelement(element ele, @nullable beandefinition containingbean) { // ... return new beandefinitionholder(beandefinition, beanname, aliasesarray); } |
注冊解析的 beandefinition
在解析完配置文件后我們已經獲取了 bean 的所有屬性,接下來就是對 bean 的注冊了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class beandefinitionreaderutils { public static void registerbeandefinition( beandefinitionholder definitionholder, beandefinitionregistry registry) throws beandefinitionstoreexception { // 使用 beanname 做唯一標識符 string beanname = definitionholder.getbeanname(); // 注冊bean的核心代碼 registry.registerbeandefinition(beanname, definitionholder.getbeandefinition()); // 為bean注冊所有的別名 string[] aliases = definitionholder.getaliases(); if (aliases != null ) { for (string alias : aliases) { registry.registeralias(beanname, alias); } } } } |
以上代碼主要完成兩個功能,一是使用 beanname 注冊 beandefinition,二是完成了對別名的注冊
beanname 注冊 beandefinition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
public class defaultlistablebeanfactory { @override public void registerbeandefinition(string beanname, beandefinition beandefinition) throws beandefinitionstoreexception { assert .hastext(beanname, "bean name must not be empty" ); assert .notnull(beandefinition, "beandefinition must not be null" ); if (beandefinition instanceof abstractbeandefinition) { try { // 注冊前的最后一次校驗,這里的校驗不同于xml文件校驗 // 主要是對于abstractbeandefinition屬性中的methodoverrides校驗 // 校驗methodoverrides是否與工廠方法并存或者methodoverrides對于的方法根本不存在 ((abstractbeandefinition) beandefinition).validate(); } catch (beandefinitionvalidationexception ex) { throw new beandefinitionstoreexception(beandefinition.getresourcedescription(), beanname, "validation of bean definition failed" , ex); } } beandefinition oldbeandefinition; // 獲取緩存中的 beandefinition oldbeandefinition = this .beandefinitionmap.get(beanname); if (oldbeandefinition != null ) { // 如果緩存中存在 判斷是否允許覆蓋 if (!isallowbeandefinitionoverriding()) { throw new beandefinitionstoreexception(beandefinition.getresourcedescription(), beanname, "cannot register bean definition [" + beandefinition + "] for bean '" + beanname + "': there is already [" + oldbeandefinition + "] bound." ); } else if (oldbeandefinition.getrole() < beandefinition.getrole()) { // e.g. was role_application, now overriding with role_support or role_infrastructure if ( this .logger.iswarnenabled()) { this .logger.warn( "overriding user-defined bean definition for bean '" + beanname + "' with a framework-generated bean definition: replacing [" + oldbeandefinition + "] with [" + beandefinition + "]" ); } } else if (!beandefinition.equals(oldbeandefinition)) { if ( this .logger.isinfoenabled()) { this .logger.info( "overriding bean definition for bean '" + beanname + "' with a different definition: replacing [" + oldbeandefinition + "] with [" + beandefinition + "]" ); } } else { if ( this .logger.isdebugenabled()) { this .logger.debug( "overriding bean definition for bean '" + beanname + "' with an equivalent definition: replacing [" + oldbeandefinition + "] with [" + beandefinition + "]" ); } } // 如果允許覆蓋,保存beandefinition到beandefinitionmap中 this .beandefinitionmap.put(beanname, beandefinition); } else { // 判斷是否已經開始創建bean if (hasbeancreationstarted()) { // cannot modify startup-time collection elements anymore (for stable iteration) synchronized ( this .beandefinitionmap) { // 保存beandefinition到beandefinitionmap中 this .beandefinitionmap.put(beanname, beandefinition); // 更新已經注冊的beanname list<string> updateddefinitions = new arraylist<>( this .beandefinitionnames.size() + 1 ); updateddefinitions.addall( this .beandefinitionnames); updateddefinitions.add(beanname); this .beandefinitionnames = updateddefinitions; if ( this .manualsingletonnames.contains(beanname)) { set<string> updatedsingletons = new linkedhashset<>( this .manualsingletonnames); updatedsingletons.remove(beanname); this .manualsingletonnames = updatedsingletons; } } } else { // 還沒開始創建bean this .beandefinitionmap.put(beanname, beandefinition); this .beandefinitionnames.add(beanname); this .manualsingletonnames.remove(beanname); } this .frozenbeandefinitionnames = null ; } if (oldbeandefinition != null || containssingleton(beanname)) { // 重置beanname對應的緩存 resetbeandefinition(beanname); } } } |
- 對 abstractbeandefinition 的校驗,主要是針對 abstractbeandefinition 的 methodoverrides 屬性的
- 對 beanname 已經注冊的情況的處理,如果設置了不允許 bean 的覆蓋,則需要拋出異常,否則直接覆蓋
- 使用 beanname 作為 key,beandefinition 為 value 加入 beandefinitionmap 存儲
- 如果緩存中已經存在,并且該 bean 為單例模式則清楚 beanname 對應的緩存
注冊別名
注冊好了 beandefinition,接下來就是注冊 alias。注冊的 alias 和 beanname 的對應關系存放在了 aliasmap 中,沿著類的繼承鏈會發現 registeralias 的方法是在 simplealiasregistry 中實現的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class simplealiasregistry { /** map from alias to canonical name */ private final map<string, string> aliasmap = new concurrenthashmap<>( 16 ); public void registeralias(string name, string alias) { assert .hastext(name, "'name' must not be empty" ); assert .hastext(alias, "'alias' must not be empty" ); if (alias.equals(name)) { // 如果beanname與alias相同的話不記錄alias 并刪除對應的alias this .aliasmap.remove(alias); } else { string registeredname = this .aliasmap.get(alias); if (registeredname != null ) { if (registeredname.equals(name)) { // 如果別名已經注冊過并且指向的name和當前name相同 不做任何處理 return ; } // 如果alias不允許被覆蓋則拋出異常 if (!allowaliasoverriding()) { throw new illegalstateexception( "cannot register alias '" + alias + "' for name '" + name + "': it is already registered for name '" + registeredname + "'." ); } } // 校驗循環指向依賴 如a->b b->c c->a則出錯 checkforaliascircle(name, alias); this .aliasmap.put(alias, name); } } } |
通過 checkforaliascircle()
方法來檢查 alias 循環依賴,當 a -> b 存在時,若再次出現 a -> c -> b 則會拋出異常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
protected void checkforaliascircle(string name, string alias) { if (hasalias(alias, name)) { throw new illegalstateexception( "cannot register alias '" + alias + "' for name '" + name + "': circular reference - '" + name + "' is a direct or indirect alias for '" + alias + "' already" ); } } public boolean hasalias(string name, string alias) { for (map.entry<string, string> entry : this .aliasmap.entryset()) { string registeredname = entry.getvalue(); if (registeredname.equals(name)) { string registeredalias = entry.getkey(); return (registeredalias.equals(alias) || hasalias(registeredalias, alias)); } } return false ; } |
至此,注冊別名也完成了,主要完成了以下幾個工作
- 如果 beanname 與 alias 相同的話不記錄 alias 并刪除對應的 alias
- 如果別名已經注冊過并且指向的name和當前name相同 不做任何處理
- 如果別名已經注冊過并且指向的name和當前name不相同 判斷是否允許被覆蓋
- 校驗循環指向依賴 如a->b b->c c->a則出錯
發送通知
通知監聽器解析及注冊完成
1
2
3
4
5
|
//defaultbeandefinitiondocumentreader.java protected void processbeandefinition(element ele, beandefinitionparserdelegate delegate) { // send registration event. getreadercontext().firecomponentregistered( new beancomponentdefinition(bdholder)); } |
通過 firecomponentregistered 方法進行通知監聽器解析及注冊完成工作,這里的實現只為擴展,當程序開發人員需要對注冊 beandefinition事件進行監聽時,可以通過注冊監聽器的方式并將處理邏輯寫入監聽器中,目前 spring 中并沒有對此事件做任何處理
其中 readercontext 是在類 xmlbeandefinitionreader 中調用 createreadercontext 生成的,然后調用 firecomponentregistered()
alias 標簽解析
spring 提供了 <alias name="person" alias="p"/>
方式來進行別名的配置,該標簽解析是在 processaliasregistration(element ele) 方法中完成的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class defaultbeandefinitiondocumentreader { protected void processaliasregistration(element ele) { // 獲取 alisa 標簽 name 屬性 string name = ele.getattribute(name_attribute); // 獲取 alisa 標簽 alias 屬性 string alias = ele.getattribute(alias_attribute); boolean valid = true ; if (!stringutils.hastext(name)) { getreadercontext().error( "name must not be empty" , ele); valid = false ; } if (!stringutils.hastext(alias)) { getreadercontext().error( "alias must not be empty" , ele); valid = false ; } if (valid) { try { // 進行別名注冊 getreadercontext().getregistry().registeralias(name, alias); } catch (exception ex) { getreadercontext().error( "failed to register alias '" + alias + "' for bean with name '" + name + "'" , ele, ex); } // 別名注冊后告知監聽器做相應處理 getreadercontext().firealiasregistered(name, alias, extractsource(ele)); } } } |
首先對 alias 標簽屬性進行提取校驗,校驗通過后進行別名注冊,別名注冊和 bean 標簽解析中的別名注冊一直,此處不再贅述
import 標簽解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public class defaultbeandefinitiondocumentreader { protected void importbeandefinitionresource(element ele) { // 獲取import標簽的resource屬性 string location = ele.getattribute(resource_attribute); // 如果不存在則不做任何處理 if (!stringutils.hastext(location)) { getreadercontext().error( "resource location must not be empty" , ele); return ; } // 解析占位符屬性 格式如"${user.dir}" location = getreadercontext().getenvironment().resolverequiredplaceholders(location); set<resource> actualresources = new linkedhashset<>( 4 ); // 判斷資源是絕對路徑還是相對路徑 boolean absolutelocation = false ; try { absolutelocation = resourcepatternutils.isurl(location) || resourceutils.touri(location).isabsolute(); } catch (urisyntaxexception ex) { // cannot convert to an uri, considering the location relative // unless it is the well-known spring prefix "classpath*:" } // 如果是絕對路徑則直接根據地址加載對應的配置文件 if (absolutelocation) { try { int importcount = getreadercontext().getreader().loadbeandefinitions(location, actualresources); if (logger.isdebugenabled()) { logger.debug( "imported " + importcount + " bean definitions from url location [" + location + "]" ); } } catch (beandefinitionstoreexception ex) { getreadercontext().error( "failed to import bean definitions from url location [" + location + "]" , ele, ex); } } else { try { int importcount; // 根據相對路徑加載資源 resource relativeresource = getreadercontext().getresource().createrelative(location); if (relativeresource.exists()) { importcount = getreadercontext().getreader().loadbeandefinitions(relativeresource); actualresources.add(relativeresource); } else { string baselocation = getreadercontext().getresource().geturl().tostring(); importcount = getreadercontext().getreader().loadbeandefinitions(stringutils.applyrelativepath(baselocation, location), actualresources); } if (logger.isdebugenabled()) { logger.debug( "imported " + importcount + " bean definitions from relative location [" + location + "]" ); } } catch (ioexception ex) { getreadercontext().error( "failed to resolve current resource location" , ele, ex); } catch (beandefinitionstoreexception ex) { getreadercontext().error( "failed to import bean definitions from relative location [" + location + "]" , ele, ex); } } // 解析后進行監聽器激活處理 resource[] actresarray = actualresources.toarray( new resource[actualresources.size()]); getreadercontext().fireimportprocessed(location, actresarray, extractsource(ele)); } } |
完成了對 import 標簽的處理,首先就是獲取 <import resource="beans.xml"/>
resource 屬性所表示的路徑,接著解析路徑中的屬性占位符 如 ${user.dir}
,然后判定 location 是絕對路徑還是相對路徑,如果是絕對路徑則遞歸調用 bean 的解析過程(loadbeandefinitions(location, actualresources);)
,進行另一次解析,如果是相對路徑則計算出絕對路徑并進行解析,最后通知監聽器,解析完成
總結
熬過幾個無人知曉的秋冬春夏,撐過去一切都會順著你想要的方向走…
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
說點什么
全文代碼:https://gitee.com/battcn/battcn-spring-source/tree/master/chapter1
原文鏈接:http://blog.battcn.com/2018/01/11/spring/spring-2/