Spring 资源管理 (Resource)

Spring 为什么引入资源管理?

Java 中有各种各样的资源,资源的位置包括本地文件系统、网络、类路径等,资源的形式可以包括文件、二进制流、字节流等,针对不同的资源又有不同的加载形式。本地文件系统中的文件在 Java 中使用 File 表示,使用 FileInputStream 读取。网络上的资源使用 URL 表示,使用 URLConnection 获取 InputStream 进行读取。而类路径下的资源使用 ClassLoader 进行读取。为了使用统一的方式访问资源,Spring 将资源抽象为 Resource,将资源的加载抽象为 ResourceLoader。Spring 配置文件的读取以及扫描包中的 bean 都会通过 Resource 访问资源。

资源抽象 Resource

Resource 是 Spring 对资源抽象的一个接口,具体的资源可以有不同的实现类。Resource 相关方法如下:

public interface Resource extends InputStreamSource {
    
    
	// 资源是否以物理的形式真实存在
	boolean exists();
	// 资源是否可以通过 #getInputStream() 方法进行读取
	default boolean isReadable() {
    
    
		return exists();
	}
	// 资源是否已经被打开
	default boolean isOpen() {
    
    
		return false;
	}
	// 资源是否为文件系统中的资源
	default boolean isFile() {
    
    
		return false;
	}
	// 获取资源 URL 的表示形式
	URL getURL() throws IOException;
	// 获取资源 URI 的表示形式
	URI getURI() throws IOException;
	// 获取资源文件的表示形式
	File getFile() throws IOException;
	// 获取资源 Channel 的表示形式
	default ReadableByteChannel readableChannel() throws IOException {
    
    
		return Channels.newChannel(getInputStream());
	}
	// 获取资源的内容长度
	long contentLength() throws IOException;
	// 获取资源最后修改的时间戳
	long lastModified() throws IOException;
	// 创建一个位置相对于当前资源的资源
	Resource createRelative(String relativePath) throws IOException;
	// 获取资源的文件名称
	@Nullable
	String getFilename();
	// 获取资源的描述信息
	String getDescription();
}

Resource 接口继承了接口 InputStreamSource ,InputStreamSource 源码如下:

public interface InputStreamSource {
    
    
	// 获取输入流
	InputStream getInputStream() throws IOException;
}

因此,每个 Resource 都可以获取到 InputStream。常见的 Resource 如下面的类图所示。
Spring 资源类图每个 Resource 的实现都封装了具体的资源。Resource 由 AbstractResource 进行主要的抽象实现,其子类可能根据封装的资源进行重写,由于源码比较简单,这里不再进行分析,感兴趣的朋友可以自行查看相关源码。 主要的 Resource 包括如下。

  • FileSystemResource:对文件系统中 File 及 Path 的封装,除了可以读取资源,还可以对资源进行写操作。
  • ClassPathResource:类路径下资源的封装。
  • UrlResource:URL 资源的封装。
  • InputStreamResource:输入流资源的封装。
  • ByteArrayResource:字节数组的封装。
  • ServletContextResource:对 Servlet 上下文的封装。

资源加载抽象 ResourceLoader

与 Java 中的类加载相似,Java 使用 ClassLoader 加载类,而 Spring 抽象出 ResourceLoader 加载 Resource。ResourceLoader 也是一个接口,根据不同的资源可以有不同的实现。ResourceLoader 源码如下:

public interface ResourceLoader {
    
    
	//类资源位置的前缀 classpath:
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
	// 根据指定的资源位置获取资源
	Resource getResource(String location);
	// 获取当前类加载器中使用的 ClassLoader
	@Nullable
	ClassLoader getClassLoader();
}

ResourceLoader 中定义了根据资源位置获取资源的方法,相关类图见下图。
Spring ResourceLoader 类图DefaultResourceLoader 是 ResourceLoader 的默认实现,其根据资源路径的协议进行解析为不同的 Resource 实现,但是它只能够根据资源路径获取一个 Resource。其获取资源的方法源码如下。

	@Override
	public Resource getResource(String location) {
    
    
		Assert.notNull(location, "Location must not be null");
		// 先根据保存的协议解析器解析支持协议的资源
		for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
    
    
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
    
    
				return resource;
			}
		}
		// 使用 Class 或 ClassLoader 获取资源
		if (location.startsWith("/")) {
    
    
			return getResourceByPath(location);
		}
		else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
    
    
			// 获取类路径下的资源
			return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
		}
		else {
    
    
			try {
    
    
				// 尝试获取 URL 资源
				// Try to parse the location as a URL...
				URL url = new URL(location);
				return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
			}
			catch (MalformedURLException ex) {
    
    
				// No URL -> resolve as resource path.
				return getResourceByPath(location);
			}
		}
	}

DefaultResourceLoader 先根据协议解析器获取资源,因此我们可以定义自己的协议解析器解析自定义的协议的资源。如果路径以 / 开头,它会获取到一个 ClassPathContextResource 资源,否则如果以资源位置以 classpath: 开头,会获取到一个 ClassPathResource 资源,最后会尝试获取 UrlResource 资源。

如果想要根据资源路径的模式字符串获取多个 Resource ,则只能通过 ResourcePatternResolver,ResourcePatternResolver 源码如下。

public interface ResourcePatternResolver extends ResourceLoader {
    
    
	// 类路径下资源文件的前缀
	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
	// 根据资源路径模式字符串获取资源
	Resource[] getResources(String locationPattern) throws IOException;
}

ResourcePatternResolver 只有一个实现 PathMatchingResourcePatternResolver,它会根据 ant 风格的路径去查找资源。实现源码如下。

	// ant 风格的路径匹配
	private PathMatcher pathMatcher = new AntPathMatcher();
	
	@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 {
    
    
			// 处理非类路径下的资源
			// Generally only look for a pattern after a prefix here,
			// and on Tomcat only after the "*/" separator for its "war:" protocol.
			int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
					locationPattern.indexOf(':') + 1);
			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)};
			}
		}
	}

获取资源时会先判断资源路径是否为类路径,然后再判断路径是否为支持的模式,默认支持 ant 风格的路径匹配,对类路径下的资源和非类路径下的资源具有不同的处理。

如何在 Spring 中获取 Resource 和 ResourceLoader

Spring 的内部有关资源的加载大量使用了 Resource 和 ResourceLoader,自然我们也同样可以使用 Resource 获取资源。
由于 Resource 与具体的资源进行绑定,Spring 并未把它作为 bean 注入到容器中,为了获取 Resource ,我们可以通过在 bean 的 成员变量中通过 @Value 注入 Resource 及其数组对象。示例如下。

扫描二维码关注公众号,回复: 11896767 查看本文章
// 类路径下创建文件 META-INF/dev.properties 内容为 profile=dev
// 类路径下创建文件 META-INF/prod.properties 内容为 profile=prod
public class Main {
    
    

    @Value("classpath:/META-INF/prod.properties")
    private Resource resource;

    @Value("classpath*:/META-INF/*.properties")
    private Resource[] resources;

    public static void main(String[] args) throws IOException {
    
    
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        Main bean = context.getBean(Main.class);
        String content = FileReader.create(bean.resource.getFile()).readString();
        System.out.println(content);
        System.out.println("=========");
        for (Resource resource : bean.resources) {
    
    
            System.out.println(FileReader.create(resource.getFile()).readString());
            System.out.println("=========");
        }

    }
}

执行结果如下:

profile=prod
=========
profile=dev
=========
profile=prod
=========

通过 @Value 注入 Resource ,成功读取到了类路径下的资源文件。

ResourceLoader 作为可能会被经常使用的组件,Spring 已经将其注册为 bean,因此可以直接通过 @Autowire 注入,另外由于 ApplicationContext 继承了 ResourceLoader 接口,因此也可以直接通过 @Autowire 注入 ApplicationContext 来使用 ResourceLoader,此外 Spring 还提供了 ResourceLoaderAware 接口,在 bean 的生命周期中,如果 bean 实现了接口 ResourceLoaderAware ,则 Spring 会调用 setResourceLoader 方法,这样就拿到了 ResourceLoader,拿到后我们就可以直接用来加载资源。示例代码如下。

public class Main implements ResourceLoaderAware {
    
    

    @Autowired
    private ResourceLoader resourceLoader;

    @Autowired
    private ApplicationContext applicationContext;

    private ResourceLoader awareResourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
    
    
        this.awareResourceLoader = resourceLoader;
    }

    public static void main(String[] args) throws IOException {
    
    
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        Main bean = context.getBean(Main.class);
        System.out.println(bean.resourceLoader);
        System.out.println(bean.applicationContext);
        System.out.println(bean.awareResourceLoader);
    }

}

执行结果如下。

org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020

三种方式都打印出来了结果,说明这三种方式都可以正常获取 ResourceLoader,并且这三种方式获取到的对象为同一个。

总结

Resource 和 ResourceLoader 作为 Spring 中资源和加载资源的抽象,在底层加载资源的地方都会被用到,通过对这两者的熟悉,在阅读 Spring 源码时,可以把精力放在其他地方,并且我们也可以使用 Resource 获取我们自己的资源。

猜你喜欢

转载自blog.csdn.net/zzuhkp/article/details/107749156
今日推荐