开篇
一说到Spring Ioc,我们很多小伙伴很本能的想到了在开发时候,我们在一个类上加上诸如@Component之类的注解,然后再在另外一个同样加着注解的类中用@Autowired之类的注解去引用就好了。那么这样子编程有什么好处呢?我们一起看看下面的代码,注意看注释部分:
package com.example.demo.article1.nothaveioc; public class TestController { private AService aService; private BService bService; private CService cService; public void doSomeThing() { // 先初始化需要的各种对象,需要使用大量set get,如果ADao,BDao,CDao也需要很多参数才能初始化的话, // 那么我们的业务逻辑很多都是这样的set get代码了,在引入设计模式之后,类和类的关系变得复杂(因为设计模式中, // 类和类的关系是和业务没有关系的),我们编程的时候必须操心这些类和类,以及对象和对象之间的关系,这让我们的编程十分痛苦 // 补充一点:维护接盘的人更痛苦,因为他还要搞明白你这些乱七八糟的关系 if (this.aService == null) { this.aService = new AService(new ADao(), new BDao(), new CDao()); } if (this.bService == null) { this.bService = new BService(new ADao(), new BDao(), new CDao()); } if (this.cService == null) { this.cService = new CService(new ADao(), new BDao(), new CDao()); } this.aService.doA(); this.bService.doB(); this.cService.doC(); } }
AService:
package com.example.demo.article1.nothaveioc; public class AService { private ADao aDao; private BDao bDao; private CDao cDao; public AService(ADao aDao, BDao bDao, CDao cDao) { this.aDao = aDao; this.bDao = bDao; this.cDao = cDao; } public void doA() { } }
BService,和CService的代码和AService的代码一样。 再看看有了Spring IOC的吧,虽然大家都知道,但是我还是贴出来:
package com.example.demo.article1.haveioc; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class TestController { @Autowired private AService aService; @Autowired private BService bService; @Autowired private CService cService; public void doSomeThing() { // 省去了各种new 和set和get,再也不需要关系那些乱七八糟的类和类的关系, // 极大程度的实现了解耦,给开发维护带来了巨大的方便 // 代码变得简单,从此可以只关注自己的业务逻辑 this.aService.doA(); this.bService.doB(); this.cService.doC(); } }
其他的类也就加上了@Component注解而已,代码很简单,大家应该一下就能看明白,引入Ioc之后,我们不再需要关心对象和对象之间的依赖关系,开发的时候只需要在需要的类上标上注解,然后使用的时候用注解引用即可。我们称这种好处为解耦,即不再需要关注对象和对象之间耦合的关系。同时,我们的代码也变得简洁很多,比如上面的图中,doSomeThing这个方法只需要关注自己的业务逻辑即可,无需再关注复杂的对象创建过程。
很多人依然不知道这个不需要关注对象和对象之间耦合关系意味着什么。面向对象编程的痛点其实就是对象和对象之间的耦合关系,在早些时候的大型单体项目里面,类的扩展往往是通过设计模式来搞,但是引入了设计模式之后,类和类的关系变得复杂,这导致了别人在用的时候,需要先理清楚这个类和它所依赖的类之间的关系,然后才可以去创建对象,而且一旦依赖关系发生了改变,很多地方其他人使用你这玩意的代码也要变,这就是个很悲剧的事情。当然,也不一定是因为设计模式导致类和类关系变的复杂,也可能是别的原因。现在因为人们都在玩微服务了,业务类之间的依赖关系由于服务拆分的原因,一般都不会太复杂。
但是这背后的,Spring为我们做了什么呢?这是一个很复杂的流程,也是本系列文章要为小伙伴们讲清楚的东西,除此之外,还有在这个加载过程中一些可以扩展的点。争取把Ioc这个复杂的东西讲清楚。
本系列文章是基于spring 4.0版本,本文中所使用的源码在这里github的这里https://github.com/BillBillLi/spring-framework-study,这是我fork的spring-framework的项目,只不过在里面都加上了我自己的注释,如果看文章里的代码块看不明白,你可以把它clone到本地,然后对着里面的代码来看,当然你应该切换到4.0.x的分支上。
在开始之前,我们先来看一段熟悉的代码:
public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:application-context.xml"); ATest aTest = applicationContext.getBean(ATest.class); aTest.doSomeThing(); }
application-context文件内容如下:
<?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:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 一个灰常简单的xml --> <bean id="aTest" class="com.example.demo.article2.entity.ATest" name="aTest"> </bean> <bean id="bTest" class="com.example.demo.article2.entity.BTest" name="bTest"> </bean> </beans>
整个过程就是创建了一个Ioc 容器,然后根据类型获取了xml里面定义的对象而已。接下来,我们先以使用xml配置的这种场景(注解的会随后讲),来讲述下xml是怎么被加载,解析,以及如果根据解析出来的xml的信息来创建需要的对象。
画外音:xml这种配置方式现在确实已经不怎么使用,但是我们先顺着这一条路下去讲通,然后其他的场景也会容易理解很多。
1. Resource
我们很容易可以想到,使用xml配置的情况下,Spring需要先加载解析我们的xml,在Spring的世界里,把类似于xml这样的东西定义为Resource,对应的接口为Resource.java,代表了Spring对资源的统一抽象,我们来看看它的代码:
public interface Resource extends InputStreamSource { boolean exists(); boolean isReadable(); boolean isOpen(); URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; String getFilename(); String getDescription(); }
画外音:后边还会贴很多这种最顶层的接口,目的是为了让大家理解,这个接口的实现类的最核心的行为,比如说对于Resource,最核心的行为就是getFile。
我们通过方法名字可以可以看出这些方法是想让它的实现类来做什么的,在这里我就不一一解释。然后我们一起来看看Resource类及其实现类的结构图(来自网络):
我们可以看出来,根据资源类型的不同,Spring为我们提供了不同的实现。其中,AbstractResource是Resource的默认实现,它实现了Resource的大部分接口,这个类也是Resource体系中的重中之重,大部分的子类也是继承的它的方法。如果我们想要定义一种资源,那么一定是继承AbstractResource类,然后根据我们的资源的特点,去覆盖它的方法。 我们来看看AbstractResource的源码:
public abstract class AbstractResource implements Resource { // 文件是否存在 @Override public boolean exists() { // Try file existence: can we find the file in the file system? try { return getFile().exists(); } catch (IOException ex) { // Fall back to stream existence: can we open the stream? try { InputStream is = getInputStream(); is.close(); return true; } catch (Throwable isEx) { return false; } } } // 是否可读默认实现是总是返回true,如果我们自己定义Resource需要用到这个方法的话,覆盖它即可 @Override public boolean isReadable() { return true; } // 是否可以打开,同上,如果自定义Resource要用到,需要覆盖 @Override public boolean isOpen() { return false; } @Override public URL getURL() throws IOException { throw new FileNotFoundException(getDescription() + " cannot be resolved to URL"); } @Override public URI getURI() throws IOException { URL url = getURL(); try { return ResourceUtils.toURI(url); } catch (URISyntaxException ex) { throw new NestedIOException("Invalid URI [" + url + "]", ex); } } @Override public File getFile() throws IOException { throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path"); } // 资源大小 @Override public long contentLength() throws IOException { InputStream is = this.getInputStream(); Assert.state(is != null, "resource input stream must not be null"); try { long size = 0; byte[] buf = new byte[255]; int read; while ((read = is.read(buf)) != -1) { size += read; } return size; } finally { try { is.close(); } catch (IOException ex) { } } } @Override public long lastModified() throws IOException { long lastModified = getFileForLastModifiedCheck().lastModified(); if (lastModified == 0L) { throw new FileNotFoundException(getDescription() + " cannot be resolved in the file system for resolving its last-modified timestamp"); } return lastModified; } protected File getFileForLastModifiedCheck() throws IOException { return getFile(); } @Override public Resource createRelative(String relativePath) throws IOException { throw new FileNotFoundException("Cannot create a relative resource for " + getDescription()); } @Override public String getFilename() { return null; } @Override public String toString() { return getDescription(); } @Override public boolean equals(Object obj) { return (obj == this || (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription()))); } @Override public int hashCode() { return getDescription().hashCode(); } }
2. ResourceLoader
Resource这里说完了,然后我们来说Resource的加载。在Spring里,Resource和它的加载是分开的,我想这么设计的道理大家肯定都懂,一方面是单一职责的原则嘛,各个部分只需要专注于自己的事情就好,另外一方面其实就是面向对象思想,比如说吃饭这个过程按照面向对象的思想来设计的话,那就是饭和吃两个接口,饭就像Resource,而吃就像加载Resource的接口。在Spring中,这个加载Resource的接口是ResourceLoader,还是按照惯例,贴一下它的源码:
public interface ResourceLoader { /** Pseudo URL prefix for loading from the class path: "classpath:" */ String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; Resource getResource(String location); ClassLoader getClassLoader(); }
这个接口的代码非常简单,只有两个方法,其中最重要的当然是getResource这个方法,其实现类往往是通过这个方法定位到具体的资源实例。它的默认实现是DefaultResourceLoader,当然最核心的方法依然是getResource这个方法,我们一起来看看:
这里限于篇幅,这里就不把所有DefaultResourceLoader的源码贴出来了
public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); // 如果location是以"/"开头,则返回ClassPathContextResource类型的Resource, // 其实它也是ClassPathResource的子类 if (location.startsWith("/")) { return getResourceByPath(location); } // 如果location以"classpath:"开头,则返回ClassPathResource类型的resource else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // 将location解析为一个URLResource URL url = new URL(location); return new UrlResource(url); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. return getResourceByPath(location); } } }
我们通过上面的代码可以看出来一个问题,就是如果localtion是诸如D:/abc/cde/fgh这种格式的时候,它会被解析成一个ClassPathContextResource(因为显然location不属于Url,在 URL url = new URL(location)的时候会抛MalFormedURLException),这个显然不是我们想要的,我们更希望,这个可以被解析成一个FileSystemResource。这时候,我们可以使用FileSystemResourceLoader中的getResource方法来获取,具体用法就不在这里提了。
3. ResourcePatternReslover
上面的ResourceLoader中的getResource方法每次只能加载一个Resource,如果要同时加载某个文件夹下面的所有Resource的话,需要使用到ResourcePatternReslover相关的的实现类,我们还是先来看看ResourcePatternReslover的源码:
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
我们可以看到这里只有一个getResources方法,可以根据传入的路径来匹配,返回多个资源。注意看哈,其实这个接口还继承了ResourceLoader这个接口,所以其实它的子类,也是具备ResourceLoader的功能的。同时,ResourcePatternReslover还增加了一种新的协议前缀classpath*,对这一点的支持也是由相应的子类来实现的。它最常用的一个子类是PathMatchingResourceReslover,它能够像ResourceLoader一样加载资源(但是注意,这里它具备ResourceLoader的功能,完全是通过一个委派来实现的)。这个类还支持基于Ant风格的路径匹配模式(**/*.xml之类的,也就是匹配某个路径下的资源,然后加载)。关于PathMatchingResourceReslover的getResources方法,由于篇幅原因,就不具体讲了,大家可以直接去我的github里面的代码看源码,还能锻炼自己阅读复杂代码的能力,这一点是很重要的。
大家不用害怕,这个方法即使不看不会影响大家阅读后边的文章~😊
4. ApplicationContext和资源加载的关系
在看到这块之前,希望大家是可以搞明白上面到底讲了什么的(你最少要明白Resource、ResourceLoader、PathMatchingResourceReslover是干嘛的),如果还不明白,建议先滚动到之前的地方看一看,如果没看明白的话,接下来的东西看了也不会理解的。
我们这篇文章的题目是统一资源的加载,那么是谁的统一资源的加载呢?答案其实就是ApplicationContext,之所以没有在开始就说这玩意,是因为这ApplicationContext的东西太多了,不想让大家过多关注了这个,现在我们可以来看看部分源码了:
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver { ······
What??说好的源码你就让我看个类名?别急😊,我们看这个接口所继承的接口中的最后一个,原来这个ApplicationContext继承了ResourcePatternReslover接口了啊,而我们前面的文章里面提过ResourcePatternReslover它又继承了ResourceLoader接口。这里需要多提一个关于ApplicationContext的点是一般来说,各种各样场景的AplicationContext(比如最开始代码中的ClassPathXmlApplicationContext)都是直接或者间接的继承了AbstractApplicationContext,而这个AbstractApplicationContext它又继承了DefaultResourceLoader,所以说ApplicationContext具体子类的getResource方法,其实就是DefaultResourceLoader方法,然后需要加载多个资源时,AbstractApplicationContext里面还有一个ResourcePatternReslover,而它的实例就是PathMatchingResourcePatternReslover。也就是说,在需要进行资源加载的时候,ApplicationContext的实现类们完全就可以作为一个ResourcePatternReslover或者是ResourceLoader来进行资源的加载,只不过在干活的时候,还是交给具体的ResourceLoader以及ResourcePatternReslover的实例来的。我们可以看一看UML图(来自揭秘Spring):