前言
关于Spring Framework中资源的访问,上篇已经讲解了org.springframework.core.io.Resource接口,它有非常多的实现类,来针对不同的场景去规范统一的资源获取方式。
【小家Spring】资源访问利器—Spring提供的Resource接口以及它的常用子类源码分析
可能有小伙伴会想,既然有了Resource接口,为啥又搞出来一个ResourceLoader呢?
这其实我认为也是Spring优秀的特点的之一,它非常擅长用接口来规范操作,隔离一些操作。
Spring框架为了更方便的获取资源,尽量弱化程序员对各个Resource接口的实现类的感知(那么多实现类要程序员去记忆,其实也是不小的一个工作量),因此定义了另一个ResourceLoader接口。
ResourceLoader接口
先从接口本身下手,看看都提供了什么功能:
// @since 10.03.2004 小细节:Resource接口是@since 28.12.2003 所以这个接口晚了大概4个月的样子 public interface ResourceLoader { // 常量:classpath: // 其实ResourceLoader接口只提供了classpath前缀的支持。而classpath*的前缀支持是在它的子接口ResourcePatternResolver中 String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // 这个方法可谓是核心方法:返回Resource实例 // 它的目的很简单:就是希望程序员在使用时,不需要太在意使用的是哪个实例。面向接口操作即可 // 至于选取哪个实例去操作,交给框架来完成 Resource getResource(String location); // Expose the ClassLoader used by this ResourceLoader. // 暴露出ResourceLoader使用的类加载器~~~ @Nullable ClassLoader getClassLoader(); }
从上可以看出:程序员在使用Spring容器时,可以不去过于计较底层Resource的实现,也不需要自己创建Resource实现类,而是直接使用applicationContext.getResource(),获取到bean容器本身的Resource,进而取到相关的资源信息。
所有ApplicationContext实例都实现了这个接口方法
从继承体系中可议看出,实现还是非常多的。但是本文先从另外一个角度出发,看看实现了Resource getResource(String location);方法的实现类:
很清晰的可以看到,真正实现了这个方法的只有三个类而已。而且GenericApplicationContext还继承自DefaultResourceLoader,而PathMatchingResourcePatternResolver实现的是子接口ResourcePatternResolver,因此放到最后说明。
DefaultResourceLoader 默认实现
它是一个非常重要的类,实现了ResourceLoader的所有功能。而且很多的类都会继承于它,再它的基础上进行扩展。
// @since 10.03.2004 public class DefaultResourceLoader implements ResourceLoader { // 这里面classLoader是允许为null的 @Nullable private ClassLoader classLoader; // 这个特别重要:ProtocolResolver这个接口事Spring为开发者提供了自定义扩展接口(允许我们自己去介入参与到具体的获取资源的处理上,后面getResouce方法可议看出来) // ProtocolResolver接口Spring没有提供任何实现,开发者可议自己实现,从而参与到资源获取的路子上去 // 备注:这个接口@since 4.3 所以只有Spring4.3后才有这个能力哦~~~ private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4); private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4); // ClassLoader可以不指定(一般情况下也不需要指定) public DefaultResourceLoader() { this.classLoader = ClassUtils.getDefaultClassLoader(); } public DefaultResourceLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } // 我们可以自己实现一个ProtocolResolver ,然后实现我们自己的获取资源的逻辑~~~下面会有示例 public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); } // @since 5.0 和ASM有关 public <T> Map<Resource, T> getResourceCache(Class<T> valueType) { return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>()); } ... // 这个是核心方法~~~ @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); // 首先,Spring会看我们自己有没有实现自己的ProtocolResolver 若有实现,会先以我们自己的为准 // 备注:它会把ResourceLoader传给开发者,这点特别重要~~~ for (ProtocolResolver protocolResolver : this.protocolResolvers) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } // 如果以/打头,就交给getResourceByPath(),注意,它是一个protected方法,子类是可议复写的 if (location.startsWith("/")) { return getResourceByPath(location); } // 如果以classpath:打头,毫无疑问,交给ClassPathResource // 需要注意的是,此处必须`classpath:` 区分大小写的哦 else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } // 最后的处理方式:就是当作一个URL来处理 else { try { // Try to parse the location as a URL... // 如果是文件类型,交给FileUrlResource(@since 5.0.2) URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. // fallback 相当于还是交给子类去弄吧 return getResourceByPath(location); } } } // 它是一个protected方法,很多子类都有复写它。可议看到,默认的处理方式是:去classpth里查找这个资源 // ClassPathContextResource事一个protected内部类 ` extends ClassPathResource implements ContextResource` protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); } }
下面简单给一个使用DefaultResourceLoader去加载资源的Demo:
public static void main(String[] args) { DefaultResourceLoader bf = new DefaultResourceLoader(); // 自己定义一个 如果是`fsx:`前缀的,那就也从classpath里面去查找这个资源~~~ bf.addProtocolResolver((String location, ResourceLoader resourceLoader) -> { // 可以写自己的实现 返回值是Resource类型即可~~~ if (location.startsWith("fsx:")) { return new ClassPathResource(location.substring("fsx:".length())); } else { return null; // 若返回null,Spring还是会继续往下走的~~~~ } }); Resource resource = bf.getResource("spring.properties"); if (resource.exists()) { dumpStream(resource); //name=fangshixiang } }
再看看DefaultResourceLoader
的子类们,实现就相对简单了:
ServletContextResourceLoader
显然它位于web包,和web相关。重写了getResourceByPath()
public class ServletContextResourceLoader extends DefaultResourceLoader { private final ServletContext servletContext; public ServletContextResourceLoader(ServletContext servletContext) { this.servletContext = servletContext; } // 从写此方法,用ServletContextResource去加载资源~~~~ @Override protected Resource getResourceByPath(String path) { return new ServletContextResource(this.servletContext, path); } }
FileSystemResourceLoader
public class FileSystemResourceLoader extends DefaultResourceLoader { // 如果以斜杠开头,那就去掉斜杠。并且使用FileSystemResource去装载 @Override protected Resource getResourceByPath(String path) { if (path.startsWith("/")) { path = path.substring(1); } return new FileSystemContextResource(path); } }
ClassRelativeResourceLoader
这个出现得稍微晚一点,@since 3.0,实现也是非常的简单。区别在于:setClassLoader(clazz.getClassLoader());并且使用了ClassPathResource去装载资源
AbstractApplicationContext
这个分支的实现,就很重要了。它直接和应用上下文有关了。
因为
AbstractApplicationContext
这个类之前非常详细的分析过,因此此处只讲解和ResourceLoader
相关的方法
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { ... // 它没有重写getResource()也没重写getResourceByPath()方法,直接使用的是父类`DefaultResourceLoader`的处理方式(毕竟人家是)Abstract实现,还没有具体指向的~~~ // 但是有两个方法,可议引起注意~~~ // ResourcePatternResolver 是 ResourceLoader的子接口 protected ResourcePatternResolver getResourcePatternResolver() { return new PathMatchingResourcePatternResolver(this); } // ApplicationContext实现了ResourcePatternResolver接口~~~~ @Override public Resource[] getResources(String locationPattern) throws IOException { return this.resourcePatternResolver.getResources(locationPattern); } }
AbstractRefreshableApplicationContext
它没有对任何相关方法重写
AbstractRefreshableConfigApplicationContext
AbstractXmlApplicationContext
上面都没有重写相关方法
FileSystemXmlApplicationContext
它重写一个一个方法:getResourceByPath
public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext { ... // 显然根据类名也知道,它使用FileSystemResource去加载xml文件 @Override protected Resource getResourceByPath(String path) { if (path.startsWith("/")) { path = path.substring(1); } return new FileSystemResource(path); } }
ClassPathXmlApplicationContext
它没有重写任何方法。为何? 原因很简单:因为DefaultResourceLoader默认就是采用去Classpath里查找资源的方案,所以此处无需重写~~~