1、環境搭建
工程目錄結構:
首先我們從最基礎的spring開發代碼開始,首先上場的是spring配置檔案:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd ">
<!-- 引入配置檔案 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--屬性注入-->
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 連接配接池最大使用連接配接數 -->
<property name="maxActive" value="${jdbc.maxActive}"/>
<!-- 初始化連接配接大小 -->
<property name="initialSize" value="${jdbc.initialSize}"/>
<!-- 擷取連接配接最大等待時間 -->
<property name="maxWait" value="${jdbc.maxWait}"/>
<!-- 連接配接池最大空閑 -->
<!-- property name="maxIdle" value="${jdbc.maxIdle}"/>-->
<!-- 連接配接池最小空閑 -->
<property name="minIdle" value="${jdbc.minIdle}"/>
<!-- 自動清除無用連接配接 -->
<property name="removeAbandoned" value="${jdbc.removeAbandoned}"/>
<!-- 清除無用連接配接的等待時間 -->
<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}"/>
<!-- 連接配接屬性 -->
<property name="connectionProperties" value="${jdbc.connectionProperties}"/>
</bean>
<bean id="zhangsan" class="com.igood.entity.User">
<constructor-arg type="java.lang.String">
<value>zhangsan</value>
</constructor-arg>
<constructor-arg type="java.lang.String">
<value>123456</value>
</constructor-arg>
<constructor-arg type="java.lang.Integer">
<value>20</value>
</constructor-arg>
</bean>
<!--給zhangsan這個bean起幾個别名,其中有一個别名和原來bean名稱相同-->
<alias name="zhangsan" alias="zhangsan,zhang3,alias1"/>
<!--設定名稱為wangwu的bean不是懶加載-->
<bean id="wangwu" class="com.igood.entity.User" lazy-init="false" scope="prototype">
<constructor-arg type="java.lang.String">
<value>wangwu</value>
</constructor-arg>
<constructor-arg type="java.lang.String">
<value>111111</value>
</constructor-arg>
<constructor-arg type="java.lang.Integer">
<value>25</value>
</constructor-arg>
</bean>
</beans>
接着是main函數啟動代碼
//代碼片段1
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:spring-beans.xml");
User bean = (User)context.getBean("zhang3");
bean.getUsername();
context.close();
}
在跟蹤源碼前,先來看一下spring IOC 體系結構相關類的繼承關系圖:
為了思路清晰,類圖中隻列出比較重要的方法和變量。
好了,開始跟蹤spring源碼。首先進入ClassPathXmlApplicationContext的構造函數
//代碼片段2
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
this(configLocations, true, null);
}
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
一直沿着繼承關系檢視父類的構造函數,到AbstractApplicationContext
//代碼片段3
public AbstractApplicationContext() {
this.resourcePatternResolver = getResourcePatternResolver();
}
protected ResourcePatternResolver getResourcePatternResolver() {
return new PathMatchingResourcePatternResolver(this);
}
從ClassPathXmlApplicationContext的類圖可以看出AbstractApplicationContext和PathMatchingResourcePatternResolver實作了ResourceLoader接口,也就是說AbstractApplicationContext也有ResourcePatternResolver的getResource()的能力,但是AbstractApplicationContext不親自具體實作getResource,而是委派給PathMatchingResourcePatternResolver去做。
2、徑路解析
回到“代碼片段2”中,ClassPathXmlApplicationContext的構造函數中setConfigLocations,這個函數在父類AbstractRefreshableConfigApplicationContext中實作
//代碼片段4
public void setConfigLocations(String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = ; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}
protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}
resolvePath的作用就是将建立ApplicationContext傳入進來的locations(classpath: spring-beans.xml)配置檔案路徑轉換為合法的location,就是将路徑中包含環境變量轉成變量的實際值,比如路徑有 占位符中的變量替換成變量實際的值。getEnviroment()拿到StandardEnvironment對象來進行占位符 {}的替換。
//代碼片段5
public class StandardEnvironment extends AbstractEnvironment {
/** System environment property source name: {@value} */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/** JVM system properties property source name: {@value} */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
}
StandardEnvironment就是讀取作業系統的環境變量和JVM的變量。當傳進來的路徑中包含這些環境變量就替換成系統中變量的實際值。比如我們傳進來的路徑是”classpath: JAVAHOME/spring−beans.xml",經過resolveRequiredPlaceholders函數處理就把 {JAVA_HOME}替換成你電腦組態的JAVA_HOME環境變量的值,如”classpath:C:/Program Files/Java/jdk1.8.0_91/spring-beans.xml”。
3、容器重新整理
好了,資源路徑準備好了,我們回到”代碼片段2”中的refresh()函數,重頭戲都在這個函數裡了。這個函數在父類AbstractApplicationContext中實作。
//代碼片段6
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 重新整理工廠之前需要做一些準備工作的啦,就想你在運動之前要做一些準備運動一樣哦
prepareRefresh();
// 我會告訴我的子類創造一個工廠,來把我需要建立bean的原料BeanDefinition準備好
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 原料準備好之後呢,我要聲明一些特殊的依賴關系, 所謂依賴,就是我在創造一個bean A的時候,發現它裡面有另外一個屬性B
// 那麼B就是A的依賴,我在創造A的時候,必須先把B創造好,特殊關系的依賴就是指我遇到B的類型,我該放棄呢,還是告訴他直接用
// 現成的(也就是不用再去創造B了)
prepareBeanFactory(beanFactory);
try {
// 這裡沒啥,就是留給子類做擴充的啦
postProcessBeanFactory(beanFactory);
// 到了這裡,工廠已經準備好了,如果你之前告訴過我工廠準備好之後應該幹什麼事情,這邊我就可以滿足你的需求哦
// 不信,你去看看BeanFactoryPostProcessors接口是幹嘛用的吧==
invokeBeanFactoryPostProcessors(beanFactory);
// 在建立一個bean的前後,我也留給你很多擴充,原理上和上面的工廠擴充差不多的哦
registerBeanPostProcessors(beanFactory);
// 就是處理一些國際化的操作啦,啊?什麼是國際化,就是i18n啦,還不懂?你沒救了
initMessageSource();
// 我的功能很豐富,除了可以給你建立bean,還可以有事件管理的功能哦,這裡我就建立一個管理器(ApplicationEventMulticaster(),
// 用來注冊事件(ApplicationEvent)
// 我會将這些事件廣播給合适的監聽者(ApplicationListener)那邊哦
initApplicationEventMulticaster();
// 啥也不幹,留給子類擴充啦
onRefresh();
// 前面不是事件管理器搞好了嘛,這邊呢,就是把那些事件監聽器給注冊進來啦,這樣來一個新的事件我就知道該發給誰啦
registerListeners();
// 如果某些bean告我我,他想在我工廠建立之初就想初始化(一般要是單件singleton并且lazy-init為false),那麼我在這個函數會滿足他
finishBeanFactoryInitialization(beanFactory);
// 終于重新整理完了,我要開始釋出事件了!
finishRefresh();
}
// 什麼?重新整理的時候報錯了?oh my god,我需要做一些清理
catch (BeansException ex) {
logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
// 我需要将我建立的bean銷毀掉
destroyBeans();
// 我不再活躍
cancelRefresh(ex);
// 我要告訴你,我出異常了,救我!!
throw ex;
}
finally {
// 一些通用的緩存清掉!!
resetCommonCaches();
}
}
}
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
refresh()方法主要為 IOC 容器 Bean 的生命周期管理提供條件, Spring IOC 容器載入 Bean 定義資源檔案從其子類容器的 refreshBeanFactory()方法啟動, 是以整個 refresh()中“ConfigurableListableBeanFactory beanFactory =obtainFreshBeanFactory();” 這句以後代碼的都是注冊容器的資訊源和生命周期事件, 載入過程就是從這句代碼啟動。
3.1、建立容器
順着obtainFreshBeanFactory方法,進入到AbstractRefreshableApplicationContext的refreshBeanFactory方法,這個方法的主要功能就是建立預設的IOC容器和Xml配置檔案的相關操作。
//代碼片段7
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {//如果已經有容器, 銷毀容器中的 bean, 關閉容器,以保證在 refresh 之後使用的是建立立起來的 IOC 容器
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();//建立 IOC 容器了
beanFactory.setSerializationId(getId());
//對 IOC 容器進行定制化, 如設定啟動參數, 開啟注解的自動裝配等
customizeBeanFactory(beanFactory);
// 加載bean的定義,我們用xml描述了各種bean的具體定義,是以這個函數
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
第7行代碼
DefaultListableBeanFactory beanFactory = createBeanFactory()
為我們新建立一個IOC容器,所謂容器就是一個Map資料結構,進入DefaultListableBeanFactory代碼就發現有
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(256);
這個成員變量,spring就是将以bean的名字做key,bean的定義作為值存到這個Map中,這就是我們需要建立bean所需要的原材料。
3.2、加載bean定義
既然容器建立好了,那麼第12行代碼
loadBeanDefinitions(beanFactory)
就要為beanFactory容器加載bean定義(BeanDefinition)了。
AbstractRefreshableApplicationContext 中隻定義了抽象的 loadBeanDefinitions 方法, 容器真正調用的是其子類 AbstractXmlApplicationContext 對該方法的實作。
//代碼片段8
//AbstractXmlApplicationContext實作父類的抽象方法
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(this.getEnvironment());
//設定資源加載器就是容器本身,1、環境搭建小節中“代碼片段3”說明AbstractApplicationContext也有資源加載器的能力
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
方法裡定義了一個XmlBeanDefinitionReader類型的對象,這個對象的作用就是讀取Xml配置檔案。最後一行代碼
loadBeanDefinitions(beanDefinitionReader);
就是用這個讀取器來加載bean定義的。
//代碼片段9
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);
}
}
我們會看到有兩種方式加載配置檔案,一種是從Resource類型的路徑中加載,還有一種是從String類型的路徑中加載。 由于我們main函數入口是
new ClassPathXmlApplicationContext("classpath:spring-beans.xml");
是以new出來ClassPathXmlApplicationContext對象裡getConfigResources()是null的,而getConfigLocations()就前面“ 2、徑路解析”小節準備的配置檔案路徑,是以進入到第8行代碼,從從String類型的路徑中加載xml檔案。
進入XmlBeanDefinitionReader的loadBeanDefinitions方法:
//代碼片段10
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int counter = ;
for (String location : locations) {
counter += loadBeanDefinitions(location);
}
return counter;
}
loadBeanDefinitions循環從每個路徑中加載xml檔案。真正加載資源的是它重載函數:
//代碼片段11
//真正幹活的函數
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
}
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
int loadCount = loadBeanDefinitions(resources);
if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
}
return loadCount;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
Resource resource = resourceLoader.getResource(location);
int loadCount = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
}
return loadCount;
}
}
第4行代碼
ResourceLoader resourceLoader = getResourceLoader();
擷取的ResourceLoader對象就是“代碼片段8”中
beanDefinitionReader.setResourceLoader(this);
,也就是ClassPathXmlApplicationContext類的對象,1、環境搭建小節中“代碼片段3”說明AbstractApplicationContext實作ResourcePatternResolver接口,是以作為AbstractApplicationContext的子類ClassPathXmlApplicationContext對象擁有getResource()的能力,但是ClassPathXmlApplicationContext不親自具體實作getResource,而是委派給PathMatchingResourcePatternResolver去做。是以”代碼片段11”中
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
最終調用如下圖:
4、資源定位
好了,跟蹤這麼久現在才進入本文的标題真正要說的事。進入PathMatchingResourcePatternResolver類的getResource方法:
//代碼片段12
@Override
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {//處理以classpath*:開頭的路徑
// a class path resource (multiple resources for same name possible)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {//路徑中包含*或?這樣的通配符
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
//路徑中不包含通配符
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
//處理不以classpath*:開頭的路徑
// Only look for a pattern after a prefix here
// (to not get fooled by a pattern symbol in a strange prefix).
int prefixEnd = locationPattern.indexOf(":") + ;
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {//路徑中包含*或?這樣的通配符
// a file pattern
return findPathMatchingResources(locationPattern);
}
else {
//路徑中不包含通配符
// a single resource with the given name
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
由上面代碼我們可以看出在加載配置檔案時,以是否是以classpath*開頭分為2種情況處理場景,每種情況類在又根據路徑中是否包括通配符進行處理。
4.1、處理以classpath*開頭且不包含通配符
進入
findAllClassPathResources
函數看看
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring();
}
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isDebugEnabled()) {
logger.debug("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[result.size()]);
}
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<Resource>();
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
// The above result is likely to be incomplete, i.e. only containing file system references.
// We need to have pointers to each of the jar files on the classpath as well...
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
跟蹤
doFindAllClassPathResources
方法中的
ClassLoader cl = getClassLoader();
進入到
ClassLoader
的getResources()方法:
//ClassLoader.java
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[];
if (parent != null) {
//如果存在父加載器,則向上疊代擷取資源
tmp[] = parent.getResources(name);
} else {
//Bootstrap classLoader主要加載JVM自身工作需要的類,位于$JAVA_HOME/jre/lib/下的jar包
tmp[] = getBootstrapResources(name);
}
tmp[] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
目前類加載器,如果存在父加載器,則向上疊代擷取資源, 是以能加到jar包裡面的資源檔案。
4.2、處理不以classpath*開頭且不包含通配符
就是一行代碼
new Resource[] {getResourceLoader().getResource(locationPattern)};
getResourceLoader()
方法擷取到就是預設的資源加載器
public ResourceLoader getResourceLoader() {
return this.resourceLoader;
}
public PathMatchingResourcePatternResolver(ClassLoader classLoader) {
this.resourceLoader = new DefaultResourceLoader(classLoader);
}
是以,我們進入
DefaultResourceLoader
類的
getResource
方法:
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
//如果以classpath開頭,則建立為一個ClassPathResource
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
//以URL的方式加載資源,建立一個UrlResource.
URL url = new URL(location);
return new UrlResource(url);
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
由此可看出,不以“classpath*”的開頭路徑getResource()僅傳回路徑(包括jar包)中的一個且僅一個資源;對于多個比對的也隻傳回一個。
4.3、路徑包含通配符的
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
//擷取确定的根路徑(沒有通配符的最長路徑),如cn/javass/config-*.xml,則根路徑是cn/javass/;
//如cn/**/config.xml,跟路徑是cn/
String rootDirPath = determineRootDir(locationPattern);
//含通配符的,如cn/javass/config-*.xml,則subPattern是config-*.xml
String subPattern = locationPattern.substring(rootDirPath.length());
//擷取跟路徑下所有資源
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<Resource>();
for (Resource rootDirResource : rootDirResources) {//查找滿足通配符的資源
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirURL = rootDirResource.getURL();
if (equinoxResolveMethod != null) {
if (rootDirURL.getProtocol().startsWith("bundle")) {
rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirURL);
rootDirResource = new UrlResource(rootDirURL);
}
}
if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isDebugEnabled()) {
logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(new Resource[result.size()]);
}
以“classpath*”的開頭路徑,getResources()加載類路徑(包括jar包)中的所有比對的資源。
至此, Spring IOC 容器在初始化時将配置的 Bean 定義資源檔案定位為 Spring 封裝的 Resource。
總結
來一張圖過一遍代碼執行簡要過程
參考
http://blog.csdn.net/ray_seu/article/details/50096889
http://blog.csdn.net/zl3450341/article/details/9306983