IOC的资源加载

112 阅读8分钟

概述

资源加载策略需要满足如下要求:

  1. 职能划分清楚,资源的定义和资源的加载应该有一个清晰的界限
  2. 同意的抽象,同意的资源定义和资源加载策略。资源加载后要返回同意的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。

Resource:统一资源

Resource接口

org.springframework.core.io.Resource是Spring框架所有资源的抽象和访问接口,它继承org.springframework.core.io.InputStreamSource接口,Source定义了一些通用的方法,由子类AbstractResource提供统一的默认实现。

public interface Resource extends InputStreamSource {
   // 资源是否存在
   boolean exists();
   
   // 资源是否可读
   default boolean isReadable() {
      return exists();
   }
   
   // 资源所代表的句柄是否被一个Stream打开
   default boolean isOpen() {
      return false;
   }
   
   // 是否为File
   default boolean isFile() {
      return false;
   }
   
   // 返回资源的URL的句柄
   URL getURL() throws IOException;
   
   // 返回资源的URI的句柄
   URI getURI() throws IOException;
   
   // 返回资源的File的句柄
   File getFile() throws IOException;
   
   // 返回ReadableByteChannel
   default ReadableByteChannel readableChannel() throws IOException {
      return Channels.newChannel(getInputStream());
   }
   
   // 资源内容长度
   long contentLength() throws IOException;
   
   // 资源最后修改时间
   long lastModified() throws IOException;
   
   // 根据资源的相对路径创建资源
   Resource createRelative(String relativePath) throws IOException;
   
   // 资源的文件名
   String getFilename();
   
   // 资源的描述
   String getDescription();
}

Resource根据不同的资源类型提供不同的具体实现,例如:

  • FileSystemResource:对 java.io.File 类型的资源的封装,只要是和 File 有关联,基本上与 FileSystemResource 也有关联。支持解析为 File 和 URL,实现扩展 WritableResource 接口。
  • ClassPathResource:class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
  • ServletContextResource:为访问 Web 容器上下文中的资源而设计的类,负责以相对于Web应用根目录的路径加载资源,它支持以流和URL的方式访问。ServletContextResource 持有一个 ServletContext 的引用 ,其底层是通过 ServletContext 的 getResource()方法和 getResourceAsStream()方法来获取资源的。

AbstractResource默认实现

AbstractResource为Resource的默认实现,它实现了Resource接口的大部分的公共实现。

public abstract class AbstractResource implements Resource {

   // 判断文件是否存在,若判断过程产生异常,就关闭对应的流
   @Override
   public boolean exists() {
      // 首先判断文件是否存在
      if (isFile()) {
         try {
            return getFile().exists();
         }
         catch (IOException ex) {
            Log logger = LogFactory.getLog(getClass());
            if (logger.isDebugEnabled()) {
               logger.debug("Could not retrieve File for existence check of " + getDescription(), ex);
            }
         }
      }
      // 尝试关闭对应的流
      try {
         getInputStream().close();
         return true;
      }
      catch (Throwable ex) {
         Log logger = LogFactory.getLog(getClass());
         if (logger.isDebugEnabled()) {
            logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex);
         }
         return false;
      }
   }

   // 当文件存在时,永远返回True
   public boolean isReadable() {
      return exists();
   }
   
   // 直接返回false,表示未打开
   @Override
   public boolean isOpen() {
      return false;
   }
   
   // 直接返回false,表示部位File
   @Override
   public boolean isFile() {
      return false;
   }
   
   // 抛出FileNotFoundException异常
   @Override
   public URL getURL() throws IOException {
      throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
   }
   
   // 基于getURL()返回的URL构建URI
   @Override
   public URI getURI() throws IOException {
      URL url = getURL();
      try {
         return ResourceUtils.toURI(url);
      }
      catch (URISyntaxException ex) {
         throw new NestedIOException("Invalid URI [" + url + "]", ex);
      }
   }
   
   // 抛出 FileNotFoundException 异常,交给子类实现
   @Override
   public File getFile() throws IOException {
      throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
   }
   
   
   @Override
   public ReadableByteChannel readableChannel() throws IOException {
      return Channels.newChannel(getInputStream());
   }

   // 获取资源长度,该资源内容长度实际就是资源的字节长度,通过全部读取一遍来实现
   @Override
   public long contentLength() throws IOException {
      InputStream is = getInputStream();
      try {
         long size = 0;
         byte[] buf = new byte[256];
         int read;
         while ((read = is.read(buf)) != -1) {
            size += read;
         }
         return size;
      }
      finally {
         try {
            is.close();
         }
         catch (IOException ex) {
            Log logger = LogFactory.getLog(getClass());
            if (logger.isDebugEnabled()) {
               logger.debug("Could not close content-length InputStream for " + getDescription(), ex);
            }
         }
      }
   }

   // 返回资源最后的修改时间
   @Override
   public long lastModified() throws IOException {
      File fileToCheck = getFileForLastModifiedCheck();
      long lastModified = fileToCheck.lastModified();
      if (lastModified == 0L && !fileToCheck.exists()) {
         throw new FileNotFoundException(getDescription() +
               " cannot be resolved in the file system for checking its last-modified timestamp");
      }
      return lastModified;
   }
   
   // 返回当前资源文件
   protected File getFileForLastModifiedCheck() throws IOException {
      return getFile();
   }

   // 抛出 FileNotFoundException 异常,交给子类实现
   @Override
   public Resource createRelative(String relativePath) throws IOException {
      throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
   }
   
   // 默认返回null
   @Override
   @Nullable
   public String getFilename() {
      return null;
   }
   
   // 资源描述是否相等
   @Override
   public boolean equals(@Nullable Object other) {
      return (this == other || (other instanceof Resource &&
            ((Resource) other).getDescription().equals(getDescription())));
   }
   
   
   @Override
   public int hashCode() {
      return getDescription().hashCode();
   }
   
   // 返回资源描述
   @Override
   public String toString() {
      return getDescription();
   }

}

ResourceLoader:统一资源定位

ResourceLoader接口

Spring将资源的定义和资源的加载区分开了,Resource定义了统一的资源,ResourceLoader定义了统一的资源加载。

ResourceLoader是Spring资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,ResourceLoader称作统一资源定位器。

public interface ResourceLoader {
   // CLASSPATH_URL_PREFIX = "classpath:"
   String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

   Resource getResource(String location);

   @Nullable
   ClassLoader getClassLoader();

}

getResource()根据所提供的路径location返回Resource实例,该方法的实现主要在其子类DefaultResourceLoader中实现,支持以下模式的资源加载:

  • URL位置资源,例如"file:D/app/bean.xml"
  • ClassPath位置资源,例如"classpath:bean.xml"
  • 相对路径资源,例如"/conf/bean.xml"

getClassLoader()返回ResourceLoader使用的ClassLoader。

DefaultResourceLoader默认实现

DefaultResourceLoader是ResourceLoader的默认实现,它有两个构造函数。

// 默认构造函数
public DefaultResourceLoader() {
}

// 指定ClassLoader构造函数
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
   this.classLoader = classLoader;
}

// 设置ClassLoader
public void setClassLoader(@Nullable ClassLoader classLoader) {
   this.classLoader = classLoader;
}

// 获取ClassLoader,如果为null,使用默认的ClassLoader
@Override
@Nullable
public ClassLoader getClassLoader() {
   return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}

ResourceLoader最核心的方法是getResource(),它根据提供的location返回相应的Resource,DefaultResource对该方法提供了核心实现。

public Resource getResource(String location) {
   Assert.notNull(location, "Location must not be null");

   // 循环调用protocolResolver加载资源
   for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
      Resource resource = protocolResolver.resolve(location, this);
      if (resource != null) {
         return resource;
      }
   }
   
   // 处理以/开头的location
   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
      // 处理以classpath:开头的location,且使用getClassLoader()获取当前的ClassLoader
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }
   else {
      try {
         // 构造URL,尝试通过它进行资源定位
         URL url = new URL(location);
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
      catch (MalformedURLException ex) {
         // 加载出现异常时处理
         return getResourceByPath(location);
      }
   }
}

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

getResource()使用的ProtocolResolver叫做"协议解析器",允许我们自己定义协议资源加载策略,而不需要继承ResourceLoader的子类。在Resource中,如果需要自定义Resource,我们只需要继承AbstractResource即可。在ResouceLoader中,则不需要直接继承DefaultResourceLoader,而是实现ProtocolResolver接口也可以实现自定义的ResourceLoader,而该接口只有一个方法,可以根据传入的location解析出对应的资源。

public interface ProtocolResolver {
   @Nullable
   Resource resolve(String location, ResourceLoader resourceLoader);

}

ResourcePatternResolver加载多个Resouce

ResourLoader的Resource getResource(String location)每次根据location返回一个Resource,当需要加载多个资源时,必须要调用多次getResource()方法才行。而ResourcePatternResolver可以根据指定的资源路径匹配模式每次返回多个Resource实例,该接口继承自ResourceLoader。

public interface ResourcePatternResolver extends ResourceLoader {
   String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

   Resource[] getResources(String locationPattern) throws IOException;

}

ResourcePatternResolver在ResourceLoader的基础上增加了getResources(String locationPattern),以支持根据路径匹配模式返回多个Resource实例,同时也增加了一种新的协议前缀classpath*:

PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver为ResourcePatternResolver的实现类,它除了支持ResourceLoader和ResourcePatternResolver新增的classpath*:前缀外,还支持Ant风格的路径匹配模式。PathMatchingResourcePatternResolver提供了三个构造方法。

// 默认构造方法,使用的ResourceLoader为DefaultResourceLoader
public PathMatchingResourcePatternResolver() {
   this.resourceLoader = new DefaultResourceLoader();
}

// 指定ResourceLoader的构造方法
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
   Assert.notNull(resourceLoader, "ResourceLoader must not be null");
   this.resourceLoader = resourceLoader;
}

// 指定Classloader的构造方法
public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
   this.resourceLoader = new DefaultResourceLoader(classLoader);
}

getResource()获取单个resource

getResource()方法直接委托相应的ResourceLoader来实现

public Resource getResource(String location) {
   return getResourceLoader().getResource(location);
}

getResources()获取多个resource

PathMatchingResourcePatternResolver支持Ant风格的路径匹配,该实现是通过AntPathMatcher类中的实现isPattern()方法实现。

// PathMatchingResourcePatternResolver中支持Ant路径匹配的pathMatcher
private PathMatcher pathMatcher = new AntPathMatcher();

public PathMatcher getPathMatcher() {
        return this.pathMatcher;
}

// AntPathMatcher类中的isPattern方法
public boolean isPattern(@Nullable String path) {
    // 主要判断location中是否包含*和?    
    if (path == null) {
            return false;
    }
    boolean uriVar = false;
    for (int i = 0; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '*' || c == '?') {
                    return true;
            }
            if (c == '{') {
                    uriVar = true;
                    continue;
            }
            if (c == '}' && uriVar) {
                    return true;
            }
    }
    return false;
}

getResources()实现主要依据不同的路径模式进行不同的处理,逻辑为

image.png

public Resource[] getResources(String locationPattern) throws IOException {
   Assert.notNull(locationPattern, "Location pattern must not be null");
   if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
      if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
         return findPathMatchingResources(locationPattern);
      }
      else {
         return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
      }
   }
   else {
      int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
            locationPattern.indexOf(':') + 1);
      if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
         return findPathMatchingResources(locationPattern);
      }
      else {
         return new Resource[] {getResourceLoader().getResource(locationPattern)};
      }
   }
}
  • classpath:* 开头但是不包含通配符*的使用findAllClassPathResources()加载路径下全部的资源
  • classpath:*开头或者以war:开头,且包含通配符的,使用findPathMatchingResources()加载路径下全部的资源
  • 其他情况使用resourceLoader加载一个resource
findAllClassPathResources方法

当以classpath:* 开头但是不包含通配符,使用findAllClassPathResources()方法加载资源,该方法返回class路径下和jar包中的相匹配的资源。

protected Resource[] findAllClassPathResources(String location) throws IOException {
   String path = location;
   if (path.startsWith("/")) {
      path = path.substring(1);
   }
   // 真正加载资源的地方
   Set<Resource> result = doFindAllClassPathResources(path);
   if (logger.isTraceEnabled()) {
      logger.trace("Resolved classpath location [" + location + "] to resources " + result);
   }
   return result.toArray(new Resource[0]);
}

// 使用classLoader加载资源
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
   Set<Resource> result = new LinkedHashSet<>(16);
   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));
   }
   // 如果path为空,则加载路径下所有jar包
   if (!StringUtils.hasLength(path)) {
      addAllClassLoaderJarRoots(cl, result);
   }
   return result;
}

classLoader的getResource()方法如下,主要通过父类委托来加载资源

public Enumeration<URL> getResources(String name) throws IOException {
    @SuppressWarnings("unchecked")
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        tmp[0] = parent.getResources(name);
    } else {
        tmp[0] = getBootstrapResources(name);
    }
    tmp[1] = findResources(name);

    return new CompoundEnumeration<>(tmp);
}
findPathMatchingResources方法

findPathMatchingResources()方法先确定目录,获取该目录下所有资源。在获得资源后,对资源进行迭代匹配获取我们想获取的资源。

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
   // 确认根路径
   String rootDirPath = determineRootDir(locationPattern);
   String subPattern = locationPattern.substring(rootDirPath.length());
   // 获取根路径下全部的资源
   Resource[] rootDirResources = getResources(rootDirPath);
   Set<Resource> result = new LinkedHashSet<>(16);
   // 遍历所有的资源,不同资源进行不同的处理
   for (Resource rootDirResource : rootDirResources) {
      rootDirResource = resolveRootDirResource(rootDirResource);
      URL rootDirUrl = rootDirResource.getURL();
      if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
         URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
         if (resolvedUrl != null) {
            rootDirUrl = resolvedUrl;
         }
         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.isTraceEnabled()) {
      logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
   }
   return result.toArray(new Resource[0]);
}

总结

  • Spring提供了Resource和ResourceLoader来统一抽象整个资源以及资源获取,使得资源与资源获取有了更加清晰地界限,并且提供了合适地Default类,使得自定义实现更加方便和清晰
  • AbstractResource为Resource地默认实现类,它对Resource接口做了统一的实现,子类继承该类后只需要覆盖相应的方法即可,对于自定义的Resource也是继承该类
  • DefaultResourceLoader为ResourceLoader的默认实现,且每次只能返回单一的资源,在自定义ResourceLoader时,除了可以继承该类外还可以实现ProtocolResolver接口来实现自定义资源加载协议
  • ResourcePatternResolver是一个可以返回多个资源的接口,其子类PathMatchingResourcePatternResolver即实现了getResource()可以获取单一资源,也实现了getResources()可以获取多个资源