OOPcoder

最近一直在折腾 jvm,想自己编译一番,可是在 Windows 上用 CLion 怎么也搞不定,CMakeLists.txt 不对导致各种报错,后面换到 centos 上终于编译成功了,但是又想调试,就用 CLion 的远程调试功能,虽然可以调试,但是因为网速或者其他不知道的原因导致调试速度太慢了,毕竟项目这么大而且还是远程调试,只好作罢,另寻他法。

想到周老师的《深入理解Java虚拟机》里有提到如何自己编译JDK, 于是参照周老师的方法终于编译成功了,还实现了调试功能,以后就能好好学习 jvm 源码了。

准备工作

  • 系统:ubuntu18.04.2 (我是在虚拟机中安装的,也可以直接在电脑中安装, 官网下载即可)
  • 调试软件:CLion2019.3.3 (官网下载即可)
  • 下载 openjdk 源码

方法1. 可以通过Mercurial工具

1
2
3
hg clone http://hg.openjdk.java.net/jdk8u/jdk8u
cd jdk8u
sh get_source.sh

ps: 一直报错,只好改用其他方式

方法2. 在 https://hg.openjdk.java.net/jdk8u/jdk8u/ 中, 点击左边的 “browse”, 然后再点你需要的格式 bz2 / zip / gz 进行下载

ps: 方法是对的,但事实证明,如何下载都下载不完全,多次尝试,要么报错下载不下来,要么不报错下载下来的压缩文件只有几百k, 文件损坏无法解压,只好改用其他方式

方法3. 在github仓库 https://github.com/unofficial-openjdk/openjdk 中把jdk8u/jdk8u分支拉下来就可以了

ps: 真是的,下载个源码也那么费劲, 还是github好

搭建环境

1. 安装一个jdk作为 bootstrap jdk
1
sudo apt-get install openjdk-8-jdk
2. 安装编译调试工具(如在安装CLion时安装过了下面工具可直接跳到下一步)
1
2
3
4
sudo apt-get install gcc
sudo apt-get install g++
sudo apt-get install gdb
sudo apt-get install make
3. 安装其他依赖包
1
2
3
4
5
6
7
8
sudo apt-get install ccache

sudo apt-get install libfreetype6-dev
sudo apt-get install libcups2-dev
sudo apt-get install libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev
sudo apt-get install libasound2-dev
sudo apt-get install libffi-dev
sudo apt-get install autoconf

编译

编译一般没那么顺利,可提前看看下一小节的坑

1 . 在源码根目录下执行./configure命令,进行依赖项检查、参数配置和构建输出目录结构等工作,如果一切顺利的话,将会看到如下配置成功的提示。如果不顺利可按后面提示安装工具或依赖即可。

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
lanbing@ubuntu1804:~/openjdk/openjdk-jdk8u$ ./configure

Running generated-configure.sh
configure: Configuration created at Wed Feb 12 22:27:32 EST 2020.
configure: configure script generated at timestamp 1468207795.
checking for basename... /usr/bin/basename
checking for bash... /bin/bash
...
...
...
====================================================
A new configuration has been successfully created in
/home/lanbing/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release
using default settings.

Configuration summary:
* Debug level: release
* JDK variant: normal
* JVM variants: server
* OpenJDK target: OS: linux, CPU architecture: x86, address length: 64

Tools summary:
* Boot JDK: openjdk version "1.8.0_242" OpenJDK Runtime Environment (build 1.8.0_242-8u242-b08-0ubuntu3~18.04-b08) OpenJDK 64-Bit Server VM (build 25.242-b08, mixed mode) (at /usr/lib/jvm/java-8-openjdk-amd64)
* C Compiler: x86_64-linux-gnu-gcc-7 (Ubuntu 7.4.0-1ubuntu1~18.04.1) version 7.4.0 (at /usr/bin/x86_64-linux-gnu-gcc-7)
* C++ Compiler: x86_64-linux-gnu-g++-7 (Ubuntu 7.4.0-1ubuntu1~18.04.1) version 7.4.0 (at /usr/bin/x86_64-linux-gnu-g++-7)

Build performance summary:
* Cores to use: 1
* Memory limit: 2963 MB
* ccache status: installed, but disabled (version older than 3.1.4)

WARNING: The result of this configuration has overridden an older
configuration. You *should* run 'make clean' to make sure you get a
proper build. Failure to do so might result in strange build problems.

2 . 执行make命令进行编译,编译成功, 将会出现下面提示

1
2
3
4
5
6
7
8
9
10
11
12
----- Build times -------
Start 2020-02-17 12:00:21
End 2020-02-17 12:18:17
00:00:37 corba
00:11:06 hotspot
00:00:21 jaxp
00:00:30 jaxws
00:05:22 jdk
00:00:00 langtools
00:17:56 TOTAL
-------------------------
Finished building OpenJDK for target 'default'

3 . 在编译好的jdk目录~/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/jdk/bin下执行./java -version命令进行验证

1
2
3
4
5
lanbing@ubuntu1804:~/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/jdk/bin$ ./java -version

openjdk version "1.8.0-internal"
OpenJDK Runtime Environment (build 1.8.0-internal-lanbing_2020_02_17_11_43-b00)
OpenJDK 64-Bit Server VM (build 25.71-b00, mixed mode)

1. 编译内核版本问题

打开 /hotspot/make/linux/Makefile 文件

1
2
3
4
# /hotspot/make/linux/Makefile 第236行
SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3% 4%
后面加上 5% 变成
SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3% 4% 5%

从下面报错信息看到,系统内核版本为:Linux ubuntu1804 5.3.0-28-generic ,并不在上面支持列表中,所以在后面加上 5% 就可以了

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
lanbing@ubuntu1804:~/openjdk/openjdk-jdk8u$ make 

Building OpenJDK for target 'default' in configuration 'linux-x86_64-normal-server-release'

## Starting langtools
Compiling 2 files for BUILD_TOOLS
Compiling 32 properties into resource bundles
Compiling 782 files for BUILD_BOOTSTRAP_LANGTOOLS
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Creating langtools/dist/bootstrap/lib/javac.jar
Compiling 785 files for BUILD_FULL_JAVAC
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Creating langtools/dist/lib/classes.jar
Updating langtools/dist/lib/src.zip
## Finished langtools (build time 00:01:04)

## Starting hotspot

*** This OS is not supported: Linux ubuntu1804 5.3.0-28-generic #30~18.04.1-Ubuntu SMP Fri Jan 17 06:14:09 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/make/linux/Makefile:242: recipe for target 'check_os_version' failed
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/make/linux/Makefile:263: recipe for target 'linux_amd64_compiler2/debug' failed
Makefile:230: recipe for target 'generic_build2' failed
Makefile:177: recipe for target 'product' failed
HotspotWrapper.gmk:44: recipe for target '/home/lanbing/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/hotspot/_hotspot.timestamp' failed
make[5]: *** [check_os_version] Error 1
make[4]: *** [linux_amd64_compiler2/debug] Error 2
make[3]: *** [generic_build2] Error 2
make[2]: *** [product] Error 2
make[1]: *** [/home/lanbing/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/hotspot/_hotspot.timestamp] Error 2
/home/lanbing/openjdk/openjdk-jdk8u//make/Main.gmk:108: recipe for target 'hotspot-only' failed
make: *** [hotspot-only] Error 2
2. -Werror=deprecated-declarations问题,什么原因导致的暂不清楚

打开 /hotspot/make/linux/makefiles/gcc.make 文件

1
2
3
4
# /hotspot/make/linux/makefiles/gcc.make 第200行
WARNINGS_ARE_ERRORS = -Werror
改成
WARNINGS_ARE_ERRORS = -Wno-all

按上面方式修改好文件,许多警告就会被忽略了,直到编译成功。

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
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/os/linux/vm/os_linux.inline.hpp:127:42: error: 'int readdir_r(DIR*, dirent*, dirent**)' is deprecated [-Werror=deprecated-declarations]
if((status = ::readdir_r(dirp, dbuf, &p)) != 0) {
^
In file included from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/os/linux/vm/jvm_linux.h:44:0,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/prims/jvm.h:30,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/utilities/debug.hpp:29,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/runtime/globals.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/memory/allocation.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/memory/iterator.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/memory/genOopClosures.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/oops/klass.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/runtime/handles.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/memory/universe.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/code/oopRecorder.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/asm/codeBuffer.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/asm/assembler.hpp:28,
from /home/lanbing/openjdk/openjdk-jdk8u/hotspot/src/share/vm/precompiled/precompiled.hpp:29:
/usr/include/dirent.h:183:12: note: declared here
extern int readdir_r (DIR *__restrict __dirp,
^~~~~~~~~
cc1plus: all warnings being treated as errors
make[6]: *** [precompiled.hpp.gch] Error 1
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/make/linux/makefiles/vm.make:309: recipe for target 'precompiled.hpp.gch' failed
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/make/linux/makefiles/top.make:119: recipe for target 'the_vm' failed
/home/lanbing/openjdk/openjdk-jdk8u/hotspot/make/linux/Makefile:298: recipe for target 'product' failed
Makefile:230: recipe for target 'generic_build2' failed
Makefile:177: recipe for target 'product' failed
HotspotWrapper.gmk:44: recipe for target '/home/lanbing/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/hotspot/_hotspot.timestamp' failed
make[5]: *** [the_vm] Error 2
make[4]: *** [product] Error 2
make[3]: *** [generic_build2] Error 2
make[2]: *** [product] Error 2
make[1]: *** [/home/lanbing/openjdk/openjdk-jdk8u/build/linux-x86_64-normal-server-release/hotspot/_hotspot.timestamp] Error 2
/home/lanbing/openjdk/openjdk-jdk8u//make/Main.gmk:108: recipe for target 'hotspot-only' failed
make: *** [hotspot-only] Error 2

调试

1.我们用 CLion 进行调试,新建项目,选择 “New CMake Project from Sources”,然后再选源码根目录,CLion 帮我们选好了要导入的源码,直接点 ok(如图), 此时会自动创建 CMakeLists.txt, 不过不能使用,我们暂时也不需要使用到它

2.在 Run/Debug Configurations 中编辑或者新增一个 CMake Application , Executable 中选择我们前面编译出来的 java 命令, arguments 加上 -version, 把下方 Before launch 中的 Build 任务去掉,如图

3.在 jdk/src/share/bin/java.c 第 354 行的 JavaMain() 里设置断点,运行就可以进行调试了

提醒:本文是基于Spring 3.0.0.RELEASE 版本进行讲解的,其他版本可能稍有差异,在贴源码的时候,部分不影响流程的代码也在本文省略了

经过上面【Spring加载过程源码解析系列】的学习,我们完成了 Bean配置的解析和注册过程,容器中存在的是 Bean 对应的 BeanDefinition,Bean并没有完成实例化,接下来我们看看到底是怎么实例化的。

开门见山,Bean 的实例化是在使用前完成的,即在方法 getBean 中 实例化。

1. getBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}

public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
return doGetBean(name, requiredType, null, false);
}

public Object getBean(String name, Object... args) throws BeansException {
return doGetBean(name, null, args, false);
}

public <T> T getBean(String name, Class<T> requiredType, Object... args) throws BeansException {
return doGetBean(name, requiredType, args, false);
}

四个重载方法都指向了 doGetBean 方法

2. doGetBean
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
private <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, 
boolean typeCheckOnly) throws BeansException {

// 将传入的name转换为实际的beanName, a -> a ; &a -> a;
// 如果该bean类型class是FactoryBean, 无论是getBean(a),还是getBean(&a),
// 都是先实例化FactoryBean, 然后再返回实际需要的bean
final String beanName = transformedBeanName(name);
Object bean;
// 先从缓存中获取单例
Object sharedInstance = getSingleton(beanName);
// 缓存中存在单例,并且 args 为空(因为只有prototype才能带参数 args),才进入 getObjectForBeanInstance
if (sharedInstance != null && args == null) {
// 获取实际需要的bean, sharedInstance 可能为 FactoryBean
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// 检查是否为 当前线程正在创建的prototype 实例
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 检查是否存在当前beanFactory中,不存在则去父工厂(存在的话)中找
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
String nameToLookup = originalBeanName(name);
if (args != null) {
return (T) parentBeanFactory.getBean(nameToLookup, args);
} else {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
// 检查是否只是为了类型检查而创建bean, 不是的话将beanName添加到已创建bean容器中
// removeSingletonIfCreatedForTypeCheckOnly() 方法可移除只用来检查类型创建的bean
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}
// 获取合并后的根 BeanDefinition,BeanDefintiton 跟类一样具有继承关系
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// 将依赖的bean提前创建
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dependsOnBean : dependsOn) {
getBean(dependsOnBean);
// 注册依赖 bean
registerDependentBean(dependsOnBean, beanName);
}
}
// 创建单例实例
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
try {
// 创建bean
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 创建原型实例
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 创建自定义Scope实例, CustomScopeConfigurer / SimpleThreadScope
else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {}
}
}

// 检查获取到的bean与参数 requiredType 是否匹配
if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return (T) bean;
}

doGetBean 方法里面调用了几个比较重要的方法:

  • getSingleton(String beanName);
  • getObjectForBeanInstance(Object beanInstance, String name, String beanName, RootBeanDefinition mbd);
  • getMergedLocalBeanDefinition(String beanName);
  • getSingleton(String beanName, ObjectFactory singletonFactory);
  • createBean(String beanName, RootBeanDefinition mbd, Object[] args)。
3. getSingleton

我们先看看getSingleton(String beanName, ObjectFactory singletonFactory)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object getSingleton(String beanName, ObjectFactory singletonFactory) {
synchronized (this.singletonObjects) {
// 先从缓存中获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 标记当前 beanName 正在创建
beforeSingletonCreation(beanName);
try {
// 调用 createBean()
singletonObject = singletonFactory.getObject();
} catch (BeanCreationException ex) {
throw ex;
} finally {
afterSingletonCreation(beanName);
}
// 添加到缓存中
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
4. createBean
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
protected Object createBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
// Make sure bean class is actually resolved at this point.
// 将bean标签中class属性解析为Class类
resolveBeanClass(mbd, beanName);

// 检查 MethodOverrides, 主要是检查 MethodOverride 里面的 methodName 是否存在,或者是否重载
try {
mbd.prepareMethodOverrides();
} catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException();
}

try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
// 给BeanPostProcessors机会返回一个代理bean, 替代掉目标bean
Object bean = resolveBeforeInstantiation(beanName, mbd);
if (bean != null) {
return bean;
}
} catch (Throwable ex) {
throw new BeanCreationException();
}
// 创建 bean
Object beanInstance = doCreateBean(beanName, mbd, args);
return beanInstance;
}
5. doCreateBean

看看真正创建Bean的方法 doCreateBean

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
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
// 如果是单例,看是否存在FactoryBean缓存,这个缓存主要是在调用 isTypeMatch() 或 getType() 检查类型匹配或者获取类型后缓存起来的
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 创建Bean实例,并返回Bean的包装实例
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);

// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
// post-processors 可以在此修改definition
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
// 这里的目的就是解决循环依赖的问题
if (earlySingletonExposure) {
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
// 将xml文件配置的各种属性值填充到Bean中
populateBean(beanName, mbd, instanceWrapper);
// 初始化bean, 顺序:postProcessBeforeInitialization() -> afterPropertiesSet() ->
// 自定义的 init-method -> postProcessAfterInitialization()
exposedObject = initializeBean(beanName, exposedObject, mbd);
} catch (Throwable ex) {}

......

return exposedObject;
}

Bean实例化的大致流程基本上就是这样了,上面提到的一些重要方法这里还没具体看,我们下次再来分析一波。

在上文 Spring容器加载过程源码解析之默认标签解析 中,我们学习了默认标签的解析,今天我们来看看自定义的标签和属性是如何解析的。

提醒:本文是基于Spring 3.0.0.RELEASE 版本进行讲解的,其他版本可能稍有差异,在贴源码的时候,部分不影响流程的代码也在本文省略了

1. 常用的自定义标签和属性

自定义标签:<context:annotation-config/>, <context:component-scan base-package="com.xx.xx"/>, <mvc:annotation-driven>, <aop:aspectj-autoproxy/>等等都是自定义的标签;

自定义属性见得比较少,不过也有,如带 “p” 前缀的属性,是在spring-beansjar包中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 启用注解 -->
<context:annotation-config/>
<!-- 设置扫描的包 -->
<context:component-scan base-package="com.xxx"/>
<bean id="hello" class="com.xxx.Hello"/>
<!-- p:hello-ref 定义 HelloWorld 属性 hello,有后缀 -ref 表示bean的id -->
<!-- p:world 定义 HelloWorld 属性 world 的值 -->
<!-- p:world 会与下方的 property world 冲突 -->
<bean id="helloWorld" class="com.xxx.HelloWorld"
p:hello-ref="hello" p:world="world-value-p">
<property name="world" value="world-value"/>
</bean>
</beans>
2. parseCustomElement分析

我们先从自定义标签解析方法的起始位置 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 看起

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
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(delegate.getNamespaceURI(root))) {
// 循环遍历 解析默认标签
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
String namespaceUri = delegate.getNamespaceURI(ele);
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(namespaceUri)) {
// 默认标签解析方法
parseDefaultElement(ele, delegate);
} else {
// 自定义标签解析方法
delegate.parseCustomElement(ele);
}
}
}
} else {
// 自定义标签解析方法
delegate.parseCustomElement(root);
}
}

委托给了BeanDefinitionParserDelegate#parseCustomElement进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
String namespaceUri = getNamespaceURI(ele);
// 根据命名空间获取对应的处理器 (命名空间处理解析器,下面会分析)
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 处理器进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
3. NamespaceHandler分析

结合上面类图,查看源码发现,命名空间处理器的主要逻辑是放在其子类NamespaceHandlerSupport中,而NamespaceHandlerSupport的子类主要是实现init方法,在里面注册该命名空间下标签对应的解析器,如ContextNamespaceHandlerinit方法,一个标签对应一个解析器。

1
2
3
4
5
6
7
8
9
10
11
public void init() {
// 注册解析器 BeanDefinitionParser
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}

NamespaceHandler调用解析器进行解析的方法是 parse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 查找对应的解析器进行解析
return findParserForElement(element, parserContext).parse(element, parserContext);
}

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
String localName = parserContext.getDelegate().getLocalName(element);
// 根据标签名获取解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
4. BeanDefinitionParser分析

我们拿常用的context:component-scan标签解析器看看

1
2
3
4
5
6
7
8
9
10
11
12
13
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 获取 base-package 值
String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

// Actually scan for bean definitions and register them.
// 扫描并注册
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

return null;
}

不同标签的具体解析逻辑就在parse方法中了。

5. NamespaceHandler的注册

我们在前面提过的命名空间处理解析器,用来解析命名空间对应的处理器

1
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

它的默认实现是DefaultNamespaceHandlerResolver

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
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有处理器
Map<String, Object> handlerMappings = getHandlerMappings();
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
} else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
} else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException(".....");
}
// 实例化处理器
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 初始化处理器,里面就是注册解析器
namespaceHandler.init();
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
//..........此处省略部分源码
}
}

private Map<String, Object> getHandlerMappings() {
if (this.handlerMappings == null) {
synchronized (this) {
if (this.handlerMappings == null) {
try {
// 加载 META-INF/spring.handlers 属性文件
// this.handlerMappingsLocation = "META-INF/spring.handlers"
Properties mappings = PropertiesLoaderUtils
.loadAllProperties(this.handlerMappingsLocation, this.classLoader);

Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>();
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
//..........此处省略部分源码
}
}
}
return this.handlerMappings;
}

代码中出现了"META-INF/spring.handlers"这么一个文件,打开spring-beans,spring-contextjar包下该文件,有如下内容:

1
2
3
4
5
6
7
8
9
10
11
spring-beans下

http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler

spring-context下

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler

我们看到命名空间 http\://www.springframework.org/schema/context的处理器org.springframework.context.config.ContextNamespaceHandler的对应关系就是在这里确定的。

6. 自定义属性解析

我们来回顾下 BeanDefinitionParserDelegate#parseBeanDefinitionElement方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// bean标签比较复杂,委托给了BeanDefinitionParserDelegate进行解析,返回封装了含 BeanDefinition,beanName,aliases 的BeanDefinitionHolder
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 解析自定义属性或标签
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
// 注册到 BeanDefinitionRegistry,也就是DefaultListableBeanFactory中的 beanDefinitionMap 中
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}catch (BeanDefinitionStoreException ex) {}
// Send registration event. 发送bean注册事件
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

delegate.decorateBeanDefinitionIfRequired(ele, bdHolder)就是自定义属性的入口了

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
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) {
return decorateBeanDefinitionIfRequired(ele, definitionHolder, null);
}

public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, BeanDefinition containingBd) {
BeanDefinitionHolder finalDefinition = definitionHolder;
// Decorate based on custom attributes first.
NamedNodeMap attributes = ele.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node node = attributes.item(i);
// 遍历属性
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
// Decorate based on custom nested elements.
NodeList children = ele.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
// 遍历子标签
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
}
return finalDefinition;
}

private BeanDefinitionHolder decorateIfRequired(
Node node, BeanDefinitionHolder originalDef, BeanDefinition containingBd) {
String namespaceUri = getNamespaceURI(node);
if (!isDefaultNamespace(namespaceUri)) {
// 这里跟前面的逻辑类似,就不重复讲了
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler != null) {
return handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd));
}
//..........此处省略部分源码
}
return originalDef;
}

看看装饰方法NamespaceHandlerSupport#decorate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public BeanDefinitionHolder decorate(
Node node, BeanDefinitionHolder definition, ParserContext parserContext) {

return findDecoratorForNode(node, parserContext).decorate(node, definition, parserContext);
}

private BeanDefinitionDecorator findDecoratorForNode(Node node, ParserContext parserContext) {
BeanDefinitionDecorator decorator = null;
String localName = parserContext.getDelegate().getLocalName(node);
if (node instanceof Element) {
// 获取元素装饰器
decorator = this.decorators.get(localName);
} else if (node instanceof Attr) {
// 获取属性装饰器
decorator = this.attributeDecorators.get(localName);
}
//..........此处省略部分源码
return decorator;
}

再看看另外一个处理器 SimplePropertyNamespaceHandler的实现,这个处理器对应的是带 “p” 前缀的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
if (node instanceof Attr) {
Attr attr = (Attr) node;
String propertyName = parserContext.getDelegate().getLocalName(attr);
String propertyValue = attr.getValue();
MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues();
// 判断是否已经存在,默认标签优先解析
if (pvs.contains(propertyName)) {
parserContext.getReaderContext().error("Property '" + propertyName + "' is already defined using " +
"both <property> and inline syntax. Only one approach may be used per property.", attr);
}
// 以 "-ref" 结尾,表示的是值为beanName,非具体值
if (propertyName.endsWith("-ref")) {
propertyName = propertyName.substring(0, propertyName.length() - REF_SUFFIX.length());
pvs.add(Conventions.attributeNameToPropertyName(propertyName), new RuntimeBeanReference(propertyValue));
} else {
// 将值添加到definition中
pvs.add(Conventions.attributeNameToPropertyName(propertyName), propertyValue);
}
}
return definition;
}

相信大家看到这里应该明白自定义的标签或属性是怎么解析的了,大家可以尝试去定义一下自己的属性标签,这里就不带大家去实践了。

在上文 Spring容器加载过程源码解析之Resource解析流程 中,我们已经了解了整个解析流程,今天我们来具体分析下默认标签的解析。

提醒:本文是基于Spring 3.0.0.RELEASE 版本进行讲解的,其他版本可能稍有差异,在贴源码的时候,部分不影响流程的代码也在本文省略了

1.什么是默认标签和自定义标签
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
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(delegate.getNamespaceURI(root))) {
// 循环遍历 解析默认标签
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
String namespaceUri = delegate.getNamespaceURI(ele);
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(namespaceUri)) {
// 默认标签解析方法
parseDefaultElement(ele, delegate);
} else {
// 自定义标签解析方法
delegate.parseCustomElement(ele);
}
}
}
} else {
// 自定义标签解析方法
delegate.parseCustomElement(root);
}
}

// 判断是否为默认命名空间下的标签
public boolean isDefaultNamespace(String namespaceUri) {
return (!StringUtils.hasLength(namespaceUri) || "http://www.springframework.org/schema/beans".equals(namespaceUri));
}

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
// 解析 import 标签
importBeanDefinitionResource(ele);
} else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
// 解析 alias 标签
processAliasRegistration(ele);
} else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
// 解析 bean 标签
processBeanDefinition(ele, delegate);
}
}

isDefaultNamespace() 方法可以看出,在命名空间 http://www.springframework.org/schema/beans 下面定义的标签就是我们所说的默认标签,比如顶级标签 beans, 以及下面的 bean,import,alias等等其他标签。

而不在默认命名空间下的标签就是自定义标签了,比如我们最常见的带 context 前缀的标签:<context:annotation-config/>,<context:component-scan base-package="com.xx.xx"/>等。

2.bean 标签解析方法 processBeanDefinition

委托给了BeanDefinitionParserDelegate#parseBeanDefinitionElement进行解析,解析完成后再进行注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// bean标签比较复杂,委托给了BeanDefinitionParserDelegate进行解析,返回封装了含 BeanDefinition,beanName,aliases 的BeanDefinitionHolder
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 解析自定义属性或标签
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
// 注册到 BeanDefinitionRegistry,也就是DefaultListableBeanFactory中的 beanDefinitionMap 中
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}catch (BeanDefinitionStoreException ex) {}
// Send registration event. 发送bean注册事件
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

接下看看代理类 BeanDefinitionParserDelegate

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
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
// beans 标签下的 bean, containingBean 为空
return parseBeanDefinitionElement(ele, null);
}

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
// 获取属性 id 值,用作 beanName
String id = ele.getAttribute("id");
// 获取属性 name 值,用作别名, 多个别名可用‘,’或‘;’分割开来
String nameAttr = ele.getAttribute("name");
// 分割多个别名
List<String> aliases = new ArrayList<String>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, BEAN_NAME_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
// 属性id值作为 bean 的名称
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
// 如果未定义id, 则将 第一个别名作为 beanName
beanName = aliases.remove(0);
}
// beans 标签下的bean, 需要检查beanName和aliases的在当前xml配置文件中的唯一性
// bean 标签中嵌套的 bean 不需要检查
if (containingBean == null) {
checkNameUniqueness(beanName, aliases, ele);
}
// 解析标签的具体操作
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
// 下面主要是没有定义 beanName 时,名字自动生成的逻辑
if (!StringUtils.hasText(beanName)) {
try {
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
} else {
beanName = this.readerContext.generateBeanName(beanDefinition);

String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
}
//..........此处省略部分源码
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
// 返回 holder
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}

上面这部分主要是处理名字和别名的逻辑,下面看下解析的具体操作

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
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, BeanDefinition containingBean) {
this.parseState.push(new BeanEntry(beanName));

String className = null;
// 解析 class 属性
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
try {
String parent = null;
// 解析 parent 属性
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
// 创建GenericBeanDefinition实例
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// 解析其他属性
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
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;
}
//..........此处省略部分源码
finally {
this.parseState.pop();
}
return null;
}

上面就是解析的具体过程了,更具体的解析细节比较繁琐,不过逻辑简单,这里就不一一贴出来了,小伙伴们可以自己跟进去看看。

3.import 标签解析方法 importBeanDefinitionResource
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
protected void importBeanDefinitionResource(Element ele) {
String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
if (!StringUtils.hasText(location)) {
return;
}
// Resolve system properties: e.g. "${user.dir}"
// 解析占位符
location = SystemPropertyUtils.resolvePlaceholders(location);
Set<Resource> actualResources = new LinkedHashSet<Resource>(4);
// 判断是绝对还是相对路径
boolean absoluteLocation = false;
try {
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
} catch (URISyntaxException ex) {}

// Absolute or relative?
if (absoluteLocation) {
try {
// 确定是绝对路径后,直接交给 XmlBeanDefinitionReader 进行定位,加载,解析
int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
} catch (BeanDefinitionStoreException 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);
}
}
//..........此处省略部分源码
}
Resource[] actResArray = actualResources.toArray(new Resource[actualResources.size()]);
// 发送import事件
getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

import标签解析比较简单,上面提到的资源 定位,加载方法: loadBeanDefinitions, 在Spring容器加载过程源码解析之Resource定位加载一文中有详细分析,想了解的小伙伴可以进去看看。

4.alias 标签解析方法 processAliasRegistration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void processAliasRegistration(Element ele) {
String name = ele.getAttribute(NAME_ATTRIBUTE);
String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
boolean valid = true;
if (!StringUtils.hasText(name)) {
valid = false;
}
if (!StringUtils.hasText(alias)) {
valid = false;
}
if (valid) {
try {
// 注册别名
getReaderContext().getRegistry().registerAlias(name, alias);
} catch (Exception ex) {}
// 发送alias注册事件
getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
}
}

alias标签跟bean标签下的alias属性都一样是bean的别名,那有什么作用呢?最简单的例子,比如,配置文件存在第三方jar包中,我们无法修改,但是又想通过特定的别名进行访问,这时候alias标签就派上用场了。

1
2
3
4
5
// jar包中配置
<bean id="hello" name="hello-alias-1, hello-alias-2" class=“com.xx.xx"/>
// 自己的配置
<alias name="hello" alias="hello-alias-3"/>
<alias name="hello" alias="hello-alias-4"/>

按照上面这么配置,hello 就有4个别名了。

好了,今天的默认标签解析就分析到这里了,下篇文章我们再分析自定义标签是如何解析的。

在上文 Spring容器加载过程源码解析之Resource定位加载 中,我们已经将资源路径解析为Resource了,今天我们来分析下整个解析流程。

提醒:本文是基于Spring 3.0.0.RELEASE 版本进行讲解的,其他版本可能稍有差异,在贴源码的时候,部分不影响流程的代码也在本文省略了

1. XmlBeanDefinitionReader 分析
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
87
88
89
90
91
92
93
94
95
96
97
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
//..........此处省略部分源码
if (resourceLoader instanceof ResourcePatternResolver) {
try {
// 这里才是真正将 资源路径 解析为Resource的地方
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
// 开始解析
int loadCount = loadBeanDefinitions(resources);
//..........此处省略部分源码
}
}
//..........此处省略部分源码
}

public int loadBeanDefinitions(Resource[] resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int counter = 0;
for (Resource resource : resources) {
// 遍历所有资源 进行解析
counter += loadBeanDefinitions(resource);
}
return counter;
}

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//..........此处省略部分源码
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 从Resource获取输入流进行解析
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
//..........此处省略部分源码
}

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
int validationMode = getValidationModeForResource(resource);
// 通过 documentLoader 将资源转换为 Document 对象
Document doc = this.documentLoader.loadDocument(
inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
return registerBeanDefinitions(doc, resource);
}
//..........此处省略部分源码
}

// EntityResolver 由 BeansDtdResolver 和 PluggableSchemaResolver 组成
// 用来在 classpath 下搜寻 schema 和 DTD 文件
protected EntityResolver getEntityResolver() {
if (this.entityResolver == null) {
// Determine default EntityResolver to use.
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
} else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// Read document based on new BeanDefinitionDocumentReader SPI.
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
// 委托 BeanDefinitionDocumentReader 解析document
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

protected XmlReaderContext createReaderContext(Resource resource) {
if (this.namespaceHandlerResolver == null) {
// 创建默认命名空间处理解析器
this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
}
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, this.namespaceHandlerResolver);
}

protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
// 用来解析 META-INF/spring.handlers 目录下对应的处理器,自定义标签的时候也会使用到,我们后面会详细分析
return new DefaultNamespaceHandlerResolver(getResourceLoader().getClassLoader());
}

从上述代码中,我们看到XmlBeanDefinitionReader完成了如下工作:

  1. 由内部的ResourceLoader去获取所有符合条件的Resource (上文已重点分析过 Spring容器加载过程之源码解析之Resource定位加载);
  2. Resource中获取流,转化为方便解析的Document对象;
  3. 委托BeanDefinitionDocumentReader来解析 Document,所以实际的bean注册工作也是由它来完成。
2. DefaultBeanDefinitionDocumentReader 分析
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
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
// 获取根元素,这里一般就是 beans 标签
Element root = doc.getDocumentElement();
// 创建解析bean的代理类
BeanDefinitionParserDelegate delegate = createHelper(readerContext, root);
// 与下方的 postProcessXml 一样,在这里都没有实现,可用来扩展自定义标签
preProcessXml(root);
// 解析根元素
parseBeanDefinitions(root, delegate);
postProcessXml(root);
}

protected BeanDefinitionParserDelegate createHelper(XmlReaderContext readerContext, Element root) {
BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
// 初始化默认的设置,即 beans 标签的属性
// Initialize the default lazy-init, autowire, dependency check settings, init-method, destroy-method and merge settings
delegate.initDefaults(root);
return delegate;
}

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(delegate.getNamespaceURI(root))) {
// 循环遍历 解析默认标签
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
String namespaceUri = delegate.getNamespaceURI(ele);
// 判断是否为默认命名空间下的标签
if (delegate.isDefaultNamespace(namespaceUri)) {
// 默认标签解析方法
parseDefaultElement(ele, delegate);
} else {
// 自定义标签解析方法
delegate.parseCustomElement(ele);
}
}
}
} else {
// 自定义标签解析方法
delegate.parseCustomElement(root);
}
}

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
// 解析 import 标签
importBeanDefinitionResource(ele);
} else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
// 解析 alias 标签
processAliasRegistration(ele);
} else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
// 解析 bean 标签
processBeanDefinition(ele, delegate);
}
}

来到这里,Resource 解析的整个流程就清晰了许多,默认标签和自定义标签的具体解析我们将在后面的文章再来分析。

在上文 如何手动启动Spring容器 中,我们知道了可以通过传入资源文件来启动容器,如果将applicationContext.xml替换为绝对路径就启动不了,报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//启动容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml")

//更换为绝对路径,启动失败
ApplicationContext context = new ClassPathXmlApplicationContext(
"E:\\IDEA_workspace\\SpringLearningDemo\\spring-java\\src\\main\\resources\\applicationContext.xml");

//报 FileNotFoundException:Caused by: java.io.FileNotFoundException: class path resource [E:/IDEA_workspace/SpringLearningDemo/spring-java/src/main/resources/applicationContext.xml] cannot be opened because it does not exist

//使用 FileSystemXmlApplicationContext 正常启动
ApplicationContext context = new FileSystemXmlApplicationContext(
"E:\\IDEA_workspace\\SpringLearningDemo\\spring-java\\src\\main\\resources\\applicationContext.xml");

// 启动成功
ApplicationContext context = new FileSystemXmlApplicationContext(
"classpath:applicationContext.xml");
// 启动成功
ApplicationContext context = new ClassPathXmlApplicationContext(
"file:E:\\IDEA_workspace\\SpringLearningDemo\\spring-java\\src\\main\\resources\\applicationContext.xml");

通过字面意思,可以看出ClassPathXmlApplicationContext, 传入的是classpath目录下的资源文件, FileSystemXmlApplicationContext, 传入的是文件系统下资源文件(即文件绝对路径), 我们在资源文件前面加上 classpath 或 file,启动试试,发现也都成功了,看来资源的定位没有上面说的那么简单,还跟资源路径前缀有关。

提醒:本文是基于Spring 3.0.0.RELEASE 版本进行讲解的,其他版本可能稍有差异,在贴源码的时候,部分不影响流程的代码也在本文省略了

下面带着疑问来看看Spring资源到底是怎么定位和加载进来的。进入ClassPathXmlApplicationContext源码,按下面路径走, 找到getResources方法:

refresh() -> obtainFreshBeanFactory() -> refreshBeanFactory() -> loadBeanDefinitions(DefaultListableBeanFactory beanFactory)

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
// AbstractXmlApplicationContext.java
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 创建 XmlBeanDefinitionReader
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 传入ResourceLoader
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
initBeanDefinitionReader(beanDefinitionReader);

loadBeanDefinitions(beanDefinitionReader);
}

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
// 获取容器启动时,传入的资源文件路径
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}

// AbstractBeanDefinitionReader.java
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
//..........此处省略部分源码
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
// 这里才是真正将 资源路径 解析为Resource的地方
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
int loadCount = loadBeanDefinitions(resources);
//..........此处省略部分源码
}
}
//..........此处省略部分源码
}

从上面可以看出,AbstractXmlApplicationContextBean的装载任务委派给了XmlBeanDefinitionReader, 而ResourceLoader负责将xml解析为Resource

先看看类结构图

从上面可以看到,Spring为了统一不同类型的资源访问,把所有资源都抽象成Resource接口, 屏蔽不同资源之间的差异,其实现了InputStreamSource, 这样所有Resource都可以通过getInputStream()获取InputStream

其中资源定位路径 -> Resource 这个过程是由 ResourceLoader 这个类来完成的,我们暂且称之为: 资源定位器, 其默认实现是 DefaultResourceLoader

ResourcePatternResolver 继承自 ResourceResolver ,扩展了一个能通过路径模式匹配定位的方法: Resource[] getResources(String locationPattern),其路径模式支持以 classpath / classpath* 或 URI协议名(例如 http、file、jar:file)为其前缀,还支持 Ant风格 的匹配模式。

看看 ResourcePatternResolver 的实现类 PathMatchingResourcePatternResolver

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
// 构造方法创建的 DefaultResourceLoader
private final ResourceLoader resourceLoader;
// Ant风格 路径匹配器
private PathMatcher pathMatcher = new AntPathMatcher();

public Resource[] getResources(String locationPattern) throws IOException {
if (locationPattern.startsWith("classpath*:")) {
// "classpath*:" 后的路径是否含有通配符 “*” 或 “?”,即是否为路径模式
if (getPathMatcher().isPattern(locationPattern.substring("classpath*:".length()))) {
// eg: classpath*:a/b/applicationContext-*.xml
// 根据路径模式 查找所有匹配的资源
return findPathMatchingResources(locationPattern);
} else {
// eg: classpath*:a/b/applicationContext-dao.xml 或 classpath*:a/b/
// 根据确定路径 在所有classpath中(包含所有jar包)查找资源
return findAllClassPathResources(locationPattern.substring("classpath*:".length()));
}
} else {
// 第一个 ":" 后的路径是否含有通配符 “*” 或 “?”,即是否为路径模式
int prefixEnd = locationPattern.indexOf(":") + 1;
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// 根据路径模式 查找所有匹配的资源
return findPathMatchingResources(locationPattern);
} else {
// 加载单个资源,后面再详细讲讲 getResource 这个方法
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}

// 根据路径模式 查找所有匹配的资源
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 获取到一个不含通配符的根目录,如 classpath*:a/b/*/applicationContext-*.xml 返回 classpath*:a/b/
String rootDirPath = determineRootDir(locationPattern);
// 返回 */applicationContext-*.xml
String subPattern = locationPattern.substring(rootDirPath.length());
// 递归调用getResources(), 获取的符合根目录的所有资源
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<Resource>(16);
// 遍历根目录资源,将匹配的资源添加到 result 中
for (Resource rootDirResource : rootDirResources) {
// jar:file:开头的为jar包资源
if (isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));
} else if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));
} else {
// 其他类型资源走这里
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
return result.toArray(new Resource[result.size()]);
}

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
File rootDir = rootDirResource.getFile().getAbsoluteFile();
//..........此处省略部分源码
return doFindMatchingFileSystemResources(rootDir, subPattern);
}

protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
// retrieveMatchingFiles 流程比较长就不贴出来了,有兴趣的朋友可以debug进去看看
Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
for (File file : matchingFiles) {
// 符合条件的路径 此时就解析为Resource了
result.add(new FileSystemResource(file));
}
return result;
}

上面就是PathMatchingResourcePatternResolver路径模式匹配的基本逻辑。加载单个资源的任务是委托给了构造方法里面创建的 DefaultResourceLoader

DefaultResourceLoader 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Resource getResource(String location) {
// 以 classpath: 开头的返回 ClassPathResource
if (location.startsWith("classpath:")) {
return new ClassPathResource(location.substring("classpath:".length()), getClassLoader());
} else {
try {
// 能够解析为URL的返回 UrlResource
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException ex) {
// 不能够解析为URL的 由getResourceByPath返回
return getResourceByPath(location);
}
}
}

protected Resource getResourceByPath(String path) {
// 默认返回 ClassPathContextResource
return new ClassPathContextResource(path, getClassLoader());
}

从上面的类结构图,我们看到 FileSystemXmlApplicationContextClassPathXmlApplicationContext 都是 DefaultResourceLoader 的子类,区别在于前者重写了 getResourceByPath 方法

1
2
3
4
5
6
protected Resource getResourceByPath(String path) {
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemResource(path);
}

那么 不带前缀的 路径或者 自定义前缀 的路径能否定位成功,关键在于getResourceByPath方法了,回到文章开始 传入的路径:

applicationContext.xmlE:\\IDEA_workspace\\SpringLearningDemo\\spring-java\\src\\main\\resources\\applicationContext.xml

走的是 DefaultResourceLoader 默认的 getResourceByPath方法,返回的classpath里面的资源,所以后者在ClassPathXmlApplicationContext里面是启动不了的,反之前者在FileSystemXmlApplicationContext也启动不了

classpath:applicationContext.xmlfile:E:\\IDEA_workspace\\SpringLearningDemo\\spring-java\\src\\main\\resources\\applicationContext.xml

都带有前缀,所以跟getResourceByPath无关,也就是跟 FileSystemXmlApplicationContext 和 ClassPathXmlApplicationContext 无关,传入哪个Context都能启动成功

总结起来, Spring的资源路径填写方式如下:

前缀 示例 说明
classpath: classpath:a/c.xml 从classpath中加载,存在多个资源则返回最先加载的那个资源,易导致资源加载不进来的问题
classpath*: classpath*:a/c.xml 从classpath中加载,返回全部符合条件的资源,需要遍历所有classpath, 因此加载效率低
file: 或 jar:file: 等URI协议名 file:d:\b\f.xml 作为URL进行加载
没有前缀 a/c.xml 或 d:\b\f.xml 根据context的getResourceByPath方法判断

Ant风格 的匹配模式

“ ? ”:匹配一个字符, 如 a?.xml 匹配 ab.xml

“ * ”:匹配零个或多个字符串,如“a/ * /c.xml”将匹配“a/b/c.xml”,但不匹配匹配“a/c.xml”;而“a/c-*.xml”将匹配“a/c-dao.xml”

“ ** ”:匹配路径中的零个或多个目录,如“a/ ** /c.xml”将匹配“a /c.xml”,也匹配“a/b/b/c.xml”;而“a/b/c- ** .xml”将匹配“a/b/c-dao.xml”,即把“ ** ”当做两个“ * ”处理。

这就是Spring容器中资源的定位,然后通过 ResourcegetInputStream() 加载进内存进行解析,至于如何解析,就请听下回分解啦。

工作中,我想大家最熟悉的Spring容器启动方法,就是在web环境下,通过在web.xml中配置如下代码进行启动。

1
2
3
4
5
6
7
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

那么,离开了web环境,想单独的启动一个Spring容器该怎么做呢,其实也很简单,有两种方式,直接看代码:

1. 手动启动

目录结构:

pom.xml

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
<properties>
<spring.version>3.0.0.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 日志依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.13</version>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- spring单元测试依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<bean id="helloWorld-id" name="helloWorld" class="com.bing.lan.spring.HelloWorld"/>

</beans>

日志配置: logback.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<Configuration debug="true">
<contextName>spring</contextName>
<property name="NORMAL_PATTERN"
value=" %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level - %logger{100} - %msg%n"/>

<Appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<Layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>${NORMAL_PATTERN}</Pattern>
</Layout>
</Appender>

<ROOT level="DEBUG">
<Appender-ref ref="STDOUT"/>
</ROOT>

</Configuration>

HelloWorld.java

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorld {

private String name = "OOPcoder";

@Override
public String toString() {
return "HelloWorld{" +
"name='" + name + '\'' +
'}';
}
}

SpringStartup.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.bing.lan.spring;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringStartup {

public static void main(String[] args) {
// 手动启动spring容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

HelloWorld helloWorld = (HelloWorld) context.getBean("helloWorld");
System.out.println("main(): " + helloWorld);
}
}

启动main函数,容器就启动了。

2. 通过 junit 来启动

在上面这些类的基础上再添加一个测试类

SpringTest.java

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
package com.bing.lan.spring;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class SpringTest {

@Autowired
BeanFactory beanFactory;

@Autowired
ApplicationContext applicationContext;

@Test
public void test() {
HelloWorld helloWorld = (HelloWorld) applicationContext.getBean("helloWorld");
System.out.println("main(): " + helloWorld);

helloWorld = (HelloWorld) beanFactory.getBean("helloWorld");
System.out.println("main(): " + helloWorld);
}
}

运行test(),容器启动成功。

学会了怎么启动,有啥好处呢,好处很多,比如

  1. 可以脱离web环境测试我们的 service / mapper 层,极大的提高开发效率;

  2. 还可以debug进Spring源码里学习各种原理,这对我们小白来说,是非常友好的,因为这只是一个单纯的Spring, 没有其他框架的干扰。

需求是这样的,我们的产品要在某个展厅中展示,主办方要求我们提供部分系统界面在电视上作为宣传页,时间紧迫,已经来不及开发了,我就在想,能不能轮播我们系统已经存在的页面(哈哈,我是有多么懒),省心省力,想法虽好,但是作为一个前端白痴 ( 没怎么写过前端代码,轮播图都没实现过 ),对我来说还是有难度的啊~~ 但是没办法,自己提出来的想法,含着泪也得实现。

经过一天的查资料,终于实现了,实现后发现原来这么简单,下面把代码贴出来,鼓励自己继续学习前端。

代码是jsp写的,用在html的话改改就可以了

轮播控件用的是 swiper , 然后用iframe实现网页内嵌

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@page import="java.util.Calendar" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Swiper demo</title>
<!-- Link Swiper's CSS -->
<link rel="stylesheet" href="${ctx}/static/swiper/css/swiper.min.css">
<!-- Demo styles -->
<style>
html, body {
position: relative;
height: 100%;
}
body {
background: #eee;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #000;
margin: 0;
padding: 0;
}
.swiper-container {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;

/* Center slide text vertically */
display: -webkit-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
}
</style>
</head>
<body>
<!-- Swiper -->
<div class="swiper-container">
<div class="swiper-wrapper">
<!-- url1,url2,url3,url4 替换成网页地址,注意跨域问题-->
<div class="swiper-slide">
<iframe height="100%" width="100%" frameborder="0" scrolling="no"
src="url1"></iframe>
</div>
<div class="swiper-slide">
<iframe height="100%" width="100%" frameborder="0" scrolling="no"
src="url2"></iframe>
</div>
<div class="swiper-slide">
<iframe height="100%" width="100%" frameborder="0" scrolling="no"
src="url3"></iframe>
</div>
<div class="swiper-slide">
<iframe height="100%" width="100%" frameborder="0" scrolling="no"
src="url4"></iframe>
</div>
</div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>

<!-- Swiper JS -->
<script src="${ctx}/static/swiper/js/swiper.min.js"></script>
<script type="text/javascript" src="${ctx}/static/jquery/1.9.1/jquery.min.js"></script>

<!-- Initialize Swiper -->
<script
var swiper = new Swiper('.swiper-container', {
loop: true,
autoplay: {
delay: 10000//10秒切换一次
// stopOnLastSlide: false,
// disableOnInteraction: true
},
//开启循环
speed: 2000,
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
});
</script>
</body>
</html>

在使用linux命令行时候,总是难以区分命令和结果的具体界线,不知道当前的路径在哪里,就像下面这样,无形之中降低了我们的效率。但是我们在用git bash的时候不会有这个烦恼,那我们能不能将linux中的界面样式定制一下,变成git bash这样呢?答案是肯定的。

linux命令行界面

git bash 界面

通过查资料发现,要想修改命令行头部显示样式,实际的操作就是覆盖系统本身的 PS1 变量,如下代码所示,在当前用户(这里是root用户)的./bashrc中添加 PS1 的值就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM_72_235_centos ~]# vi .bashrc
[root@VM_72_235_centos ~]# cat .bashrc
# .bashrc
# User specific aliases and functions
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# 添加这行就可以了
PS1='[\[\e[00;35m\]\u@dev \t\[\e[0m\]]\[\e[0;33m\](\w)\$\[\e[0m\] '

[root@VM_72_235_centos ~]# source .bashrc (执行这句设置生效,只对当前用户生效)
[root@dev 22:36:51](~)# ls (发现生效了)

先将这行代码拆分如下,再来分析具体意思

1
2
3
4
5
                    [               //表示【 [ 】 颜色为 默认颜色
\[\e[00;35m\] \u@dev \t //表示【 \u@dev \t 】 颜色为 【 \[\e[00;35m\] 】
\[\e[0m\] ] //表示【 ] 】 颜色为 【 \[\e[0m\] 】
\[\e[0;33m\] (\w)\$ //表示【 (\w)\$ 】 颜色为 【 \[\e[0;33m\] 】
\[\e[0m\] //取消设置(设置回默认颜色)

[\e[00;35m] 该设置是应用到后面字符上,直到有其他设置,所以最后要设置回默认颜色

设置字符序列颜色的格式为:[\e[F;Bm] 其中“F”为字体颜色,编号30 ~ 37;“B”为背景色,编号40 ~ 47

颜色表
前景 | 背景 | 颜色
—|—|—
30 | 40 | 黑色
31 | 41 | 红色
32 | 42 | 绿色
33 | 43 | 黄色
34 | 44 | 蓝色
35 | 45 | 紫红色
36 | 46 | 青蓝色
37 | 47 | 白色

变量值:

  • \d :可显示出『星期 月 日』的日期格式,如:”Mon Feb 2”
  • \H :完整的主机名称。举例来说,鸟哥的练习机为『www.vbird.tsai』
  • \h :仅取主机名称在第一个小数点之前的名字,如鸟哥主机则为『www』后面省略
  • \t :显示时间,为 24 小时格式的『HH:MM:SS』
  • \T :显示时间,为 12 小时格式的『HH:MM:SS』
  • \A :显示时间,为 24 小时格式的『HH:MM』
  • @ :显示时间,为 12 小时格式的『am/pm』样式
  • \u :目前使用者的帐号名称,如『root』;
  • \v :BASH 的版本资讯,如鸟哥的测试主机版本为 3.2.25(1),仅取『3.2』显示
  • \w :完整的工作目录名称,由根目录写起的目录名称。但家目录会以 ~ 取代;
  • \W :利用 basename 函数取得工作目录名称,所以仅会列出最后一个目录名。
  • # :下达的第几个指令。
  • $ :提示字元,如果是 root 时,提示字元为 # ,否则就是 $
  • \n : new line,表示另起一行显示指令符

通过上面的颜色及变量值对照表,就可以轻松的定制出适合自己的样式啦。

我的配置如下,跟git bash相近

1
2
3
4
5
PS1='\[\e[00;35m\]\u@\h \t \[\e[0;33m\]\w (12.22.34.179)\n\$\[\e[0m\] ' 

效果如下(颜色在这里没显示出来):
root@VM_72_235_centos 21:22:43 /usr/local/share (12.22.34.179)
$ less log.log

参考:在xshell中将命令行移至下一行: http://blog.sina.com.cn/s/blog_96a11ddf0102vbb7.html

在使用maven管理pom项目的时候,多多少少会遇到一些奇葩的问题,比如网络良好,idea非离线状态,科学上网模式,maven就是死活无法更新jar包,很令人头大,无从下手找原因,当我们去查看maven本地仓库的时候,发现有些包里会多了一些以 .lastUpdated 结尾的文件,那么问题来了

为什么maven仓库会出现这些文件? 又会导致什么问题?

在idea网络状态很差或者离线状态时,需要从远程仓库下载某些jar包到本地参库时,因网络差,无法下载,导致本地仓库出现 xx.jar.lastUpdated 或者 xx.pom.lastUpdated 文件(文件具体作用暂时不清楚), 由于这些文件的存在,即使网络变好后,项目仍然报错, 无法重新下载需要的jar包

解决方法:

方法非常简单粗暴,找到本地仓库对应jar包目录位置将 .lastUpdated 文件删除, 刷新项目重新下载即可

有的小伙伴可能会问,仓库那么多,总不会让我一个个找到然后删除吧,机智,下面给大家提供了两个一键删除脚本:

1、cleanLastUpdated.bat(windows版本)
1
2
3
4
5
6
7
8
9
rem 这里写你的仓库路径
set REPOSITORY_PATH=D:\Java\maven-repository
rem 正在搜索...
for /f "delims=" %%i in ('dir /b /s "%REPOSITORY_PATH%\*lastUpdated*"') do (
echo %%i
del /s /q "%%i"
)
rem 搜索完毕
pause
2、cleanLastUpdated.sh(linux版本)
1
2
3
4
5
# 这里写你的仓库路径
REPOSITORY_PATH=~/Documents/tools/repository
echo 正在搜索...
find $REPOSITORY_PATH -name "*lastUpdated*" | xargs rm -fr
echo 搜索完

参考:maven仓库中的LastUpdated文件生成原因及删除