前言
紧跟上篇 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/