11. 资源管理 Resources

130 阅读5分钟

11. 资源管理 Resources

Resource 都包括哪些东西 ?

基本上可以理解为项目中 resources 目录下的所有内容

  1. Spring 配置 xml 文件
  2. 外部化配置,properties 文件或 yaml 文件
  3. Class 文件,被 @Component 标记的 Class 文件 ?算吗

11.1 引入动机

为什么 Spring 不使用 Java 标准资源管理,而选择重新发明轮子?

  • Java 标准资源管理虽然强大,但是扩展复杂,资源存储的方式并不统一
  • Spring 要自立门户
  • Spring 三个原则,抄,超,潮;抄 java,也要超越 java,还要使用新潮的技术和思想

11.2 Java 标准资源管理

image

11.3 Spring 资源接口

image

image

public interface Resource extends InputStreamSource {

	boolean exists();

	default boolean isReadable() {
		return exists();
	}

	default boolean isOpen() {
		return false;
	}

	default boolean isFile() {
		return false;
	}

	URL getURL() throws IOException;

	URI getURI() throws IOException;

	File getFile() throws IOException;
}

11.4 Spring 内建 Resource 实现

Spring 内建 Resource 实现,这些类都是 Resource 接口的实现类:

image

11.5 Spring 资源加载器

  1. 使用 FileSystemResource 读取当前 class 文件内容,代码如下所示:
/**
 * 读取当前class文件内容
 *
 * @author mao  2021/5/21 1:49
 */
public class EncodeFileSystemResourceDemo {
    public static void main(String[] args) throws IOException {
        String path = System.getProperty("user.dir")
                + "\\thinking-in-spring\\resource\\src\\main\\java\\org\\geekbang\\resource\\EncodeFileSystemResourceDemo.java";
        System.out.println(path);

        FileSystemResource fileSystemResource = new FileSystemResource(path);
        EncodedResource encodedResource = new EncodedResource(fileSystemResource, "UTF-8");
        Reader reader = encodedResource.getReader();
        System.out.println(IOUtils.toString(reader));
    }
}

输出结果,输出当前 class 文件的路径,输出 class 文件的内容

D:\MyProjects\idea\think-in-spring\thinking-in-spring\resource\src\main\java\org\geekbang\resource\EncodeFileSystemResourceDemo.java

package org.geekbang.resource;

import com.sun.javafx.runtime.SystemProperties;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.EncodedResource;


import java.io.IOException;
import java.io.Reader;

/**
 * 读取当前class文件内容
 * 
 * @author mao  2021/5/21 1:49
 */
public class EncodeFileSystemResourceDemo {
    public static void main(String[] args) throws IOException {
        String path = System.getProperty("user.dir")
                + "\\thinking-in-spring\\resource\\src\\main\\java\\org\\geekbang\\resource\\EncodeFileSystemResourceDemo.java";
        System.out.println(path);

        FileSystemResource fileSystemResource = new FileSystemResource(path);
        EncodedResource encodedResource = new EncodedResource(fileSystemResource, "UTF-8");
        try (Reader reader = encodedResource.getReader()) {
            System.out.println(IOUtils.toString(reader));
        }
    }
}

image

  1. 使用 FileSystemResourceLoader 读取当前 class 文件内容,代码如下所示:
public static void main(String[] args) throws IOException {
    String path = System.getProperty("user.dir")
        + "\\thinking-in-spring\\resource\\src\\main\\java\\org\\geekbang\\resource\\EncodeFileSystemResourceDemo.java";
    System.out.println(path);

    FileSystemResourceLoader resourceLoader = new FileSystemResourceLoader();
    // 本质是创建了一个 ClassPathContextResource
    Resource resource = resourceLoader.getResource(path);

    EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
    try (Reader reader = encodedResource.getReader()) {
        System.out.println(IOUtils.toString(reader));
    }
}

输出结果

D:\MyProjects\idea\think-in-spring\thinking-in-spring\resource\src\main\java\org\geekbang\resource\EncodeFileSystemResourceDemo.java
package org.geekbang.resource;

import org.apache.commons.io.IOUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.EncodedResource;


import java.io.IOException;
import java.io.Reader;

/**
 * 读取当前class文件内容
 * {@link FileSystemResource} 示例
 * @author mao  2021/5/21 1:49
 */
public class EncodeFileSystemResourceDemo {
    public static void main(String[] args) throws IOException {
        String path = System.getProperty("user.dir")
                + "\\thinking-in-spring\\resource\\src\\main\\java\\org\\geekbang\\resource\\EncodeFileSystemResourceDemo.java";
        System.out.println(path);

        FileSystemResource fileSystemResource = new FileSystemResource(path);
        EncodedResource encodedResource = new EncodedResource(fileSystemResource, "UTF-8");
        Reader reader = encodedResource.getReader();
        System.out.println(IOUtils.toString(reader));
    }
}

11.6 Spring 通配路径资源加载器

  • 通配路径解析
    • 接口 - ResourcePatternResolver
    • 实现类 - PathMatchingResourcePatternResolver
  • 路径匹配
    • 接口 -
    • 实现 - AntPathMatcher

源码如下

public interface ResourcePatternResolver extends ResourceLoader {

	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

	Resource[] getResources(String locationPattern) throws IOException;
}
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
	private final ResourceLoader resourceLoader;

	private PathMatcher pathMatcher = new AntPathMatcher();
    // 底层还是ResourceLoader来获取资源
    public Resource getResource(String location) {
		return getResourceLoader().getResource(location);
	}
	public Resource[] getResources(String locationPattern) throws IOException {
		Assert.notNull(locationPattern, "Location pattern must not be null");

        // 资源路径是 classpath*: 开头的
		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)};
			}
		}
	}
}

11.7 Spring 通配路径资源扩展

11.8 依赖注入 Resource

  • 基于 @Value 注入 - @Value("classpath:/...")
  • 不能通过 @Autowired 注入,Spring 内建单例对象并没有 Resource,不要记混了
public class InjectingResourceDemo {

    @Value("classpath:/hello.properties")
    private Resource helloResource;

    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(InjectingResourceDemo.class);

        applicationContext.refresh();
        InjectingResourceDemo bean = applicationContext.getBean(InjectingResourceDemo.class);
        System.out.println(bean.helloResource);
        
        EncodedResource encodedResource = new EncodedResource(bean.helloResource);
        try (Reader reader = encodedResource.getReader()) {
            System.out.println(IOUtils.toString(reader));
        }
    }
}

输出结果

class path resource [hello.properties]
name=tracccer

11.9 依赖注入 ResourceLoader

  • @Autowired 注入 ResourceLoader
  • 注入 ApplicationContext 作为 ResourceLoader
  • 实现 ResourceLoaderAware 回调方法
  1. 下面的代码演示了上述三种方式,依赖注入 ResourceLoader
public class InjectingResourceLoaderDemo implements ResourceLoaderAware {

    public static final String LOCATION = "classpath:/hello.properties";

    // 1.@Autowired 注入 ResourceLoader
    @Autowired
    private ResourceLoader resourceLoader;

    // 2.注入 ApplicationContext 作为 ResourceLoader
    @Autowired
    private ApplicationContext applicationContext;

    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(InjectingResourceLoaderDemo.class);

        applicationContext.refresh();
        InjectingResourceLoaderDemo bean = applicationContext.getBean(InjectingResourceLoaderDemo.class);
        System.out.println(bean.resourceLoader);

        // 获取@Autowired注入的resourceLoader
        Resource resource = bean.resourceLoader.getResource(LOCATION);
        getContent(resource);

        // 通过@Autowired注入的ApplicationContext,获取resourceLoader
        Resource resource1 = bean.resourceLoader.getResource(LOCATION);
        getContent(resource1);
        
        // 二者其实是同一个对象
        System.out.println(bean.resourceLoader == bean.applicationContext);
    }

    // 3. 实现ResourceLoaderAware回调方法, 加载resource, 打印内容
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        Resource resource1 = resourceLoader.getResource(LOCATION);
        try {
            getContent(resource1);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 打印资源的内容
    private static void getContent(Resource resource) throws IOException {
        EncodedResource encodedResource = new EncodedResource(resource);
        try (Reader reader = encodedResource.getReader()) {
            System.out.println(IOUtils.toString(reader));
        }
    }
}

输出结果

name=tracccer
org.springframework.context.annotation.AnnotationConfigApplicationContext@1a6c5a9e, started on Fri May 21 03:04:55 CST 2021
name=tracccer
name=tracccer
true
  1. Spring 框架在应用上下文启动时refresh()会调用下面这个方法,注册 4 个非Spring容器管理的对象到容器,其中就包括我们上面使用的 ResourceLoader 和 ApplicationContext,源码如下:
AbstractApplicationContext.java

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	// 注册ApplicationContextAwareProcessor,用于在初始化前回调各个Aware
    beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
    
    // 注册非容器管理对象ResolvableDependency, this是当前应用上下文	
    // 可以看到后3个其实是同一个对象this
	beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
    beanFactory.registerResolvableDependency(ResourceLoader.class, this);
    beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
    beanFactory.registerResolvableDependency(ApplicationContext.class, this);
}

DefaultListableBeanFactory.java
    
// 注册ResolvableDependency, 将其保存到resolvableDependencies中
public void registerResolvableDependency(Class<?> dependencyType, @Nullable Object autowiredValue) {
    Assert.notNull(dependencyType, "Dependency type must not be null");
    if (autowiredValue != null) {
        this.resolvableDependencies.put(dependencyType, autowiredValue);
    }
}

至于依赖查找是如果找到 ResourceLoader 对象的,见 7.2.3 章节 依赖注入的来源

  1. ApplicationContextAwareProcessor 实现了 BeanPostProcessor#postProcessBeforeInitialization 方法,该方法会在 bean 初始化前被回调,该方法会对调用所有 Aware 接口的实现类,也就包括我们前面示例代码中实现的 ResourceLoaderAware 接口。
ApplicationContextAwareProcessor.java

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
          bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
          bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)){
        return bean;
    }
    // 调用Aware接口,见下个方法
    invokeAwareInterfaces(bean);
    return bean;
}

private void invokeAwareInterfaces(Object bean) {
    if (bean instanceof EnvironmentAware) {
        ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
    }
	// 回调ResourceLoaderAware的setResourceLoader方法
    // 也就是我们重写的方法
    if (bean instanceof ResourceLoaderAware) {
        ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
    }
}

这也恰好说明了,示例代码中,最先输出的是实现 ResourceLoaderAware 接口的方式依赖注入的 Resource,因为是在 bean 初始化前执行的。

11.10 面试题

  1. Spring 配置资源中有哪些常见类型?

    1. xml 资源
      • BeanDefinition 配置资源
    2. properties 资源
      • 外部化配置 properties
      • Spring Handler 实现类资源文件 - spring.handlers
      • Spring Schema 资源映射文件 - spring.schemas
    3. yaml 资源
      • 外部化配置 yaml
  2. Resource 与 ResourceLoader 的区别

  3. 依赖注入 Resource 和 ResourceLoader 有哪几种方式?