# Spring Resource

## 1. SpringFramework为什么要自己写Resource

一看到资源管理，或许会有小伙伴立马想到 `ClassLoader` 的 `getResource` 和 `getResourceAsStream` 方法，它们本身就是 jdk 内置的加载资源文件的方式。然而 SpringFramework 中并没有直接拿它的这一套，而是自己重新造了一套比原生 jdk 更强大的资源管理。既然是造了，那就肯定有原因，而这个原因，咱们可以翻看 SpringFramework 的官方文档：

<https://docs.spring.io/spring-framework/docs/5.2.x/spring-framework-reference/core.html#resources>

原文咱就不在这里贴了，概述一下就是说，jdk 原生的 URL 那一套资源加载方式，对于加载 classpath 或者 `ServletContext` 中的资源来说没有标准的处理手段，而且即便是实现起来也很麻烦，倒不如我自己写一套。

如果对比原生 jdk 和 SpringFramework 中的资源管理，可能 SpringFramework 的资源管理真的要更强大吧，下面咱来了解下 SpringFramework 中定义的资源模型都有什么结构，分别负责哪些功能。

## 2. SpringFramework中的资源模型

先来一张图概览一下吧：

![img](https://cdn.nlark.com/yuque/0/2022/png/229542/1668764689331-4ea4ae14-9f9d-4864-8f71-2b8186a2bcf2.png)

可以发现，SpringFramework 中资源模型最顶级的其实不是 `Resource` ，而是一个叫 `InputStreamSource` 的接口：

#### 2.1 InputStreamSource

```java
public interface InputStreamSource {
	InputStream getInputStream() throws IOException;
}
```

这个接口只有一个 `getInputStream` 方法，很明显它表达了一件事情：实现了 `InputStreamSource` 接口的实现类，都可以从中取到资源的输入流。

#### 2.2 Resource

然后就是 `InputStreamSource` 的子接口 `Resource` 了，它的文档注释中有写到这么一句话：

Interface for a resource descriptor that abstracts from the actual type of underlying resource, such as a file or class path resource.

它是资源描述符的接口，它可以从基础资源的实际类型中抽象出来，例如文件或类路径资源。

这个翻译看起来很生硬，不过咱只需要关注到一个点：**文件或类路径的资源**，仅凭这一个点，咱就可以说，`Resource` 确实更适合 SpringFramework 做资源加载（配置文件通常都放到类路径下）。

#### 2.3 EncodedResource

在 `Resource` 的旁边，有一个 `EncodedResource` 直接实现了 `InputStreamSource` 接口，从类名上也能看得出来它是编码后的资源。通过源码，发现它内部组合了一个 `Resource` ，说明它本身并不是直接加载资源的。

```java
public class EncodedResource implements InputStreamSource {
	private final Resource resource;
    // ......
```

#### 2.4 WritableResource

继续往下看，自打 SpringFramework 3.1 之后，`Resource` 有了一个新的子接口：`WritableResource` ，它代表着“可写的资源”，那 `Resource` 就可以理解为“可读的资源”（有木有想起来 `BeanFactory` 与 `ConfigurableBeanFactory` ？）。

#### 2.5 ContextResource

跟 `WritableResource` 并列的还有一个 `ContextResource` ，看到类名是不是突然一阵狂喜？它肯定是跟 `ApplicationContext` 有关系吧！打开源码，看一眼文档注释：

Extended interface for a resource that is loaded from an enclosing 'context', e.g. from a javax.servlet.ServletContext but also from plain classpath paths or relative file system paths (specified without an explicit prefix, hence applying relative to the local ResourceLoader's context).

从一个封闭的 “上下文” 中加载的资源的扩展接口，例如来自 `javax.servlet.ServletContext` ，也可以来自普通的类路径路径或相对的文件系统路径（在没有显式前缀的情况下指定，因此相对于本地 `ResourceLoader` 的上下文应用）。

emmm它是跟 `ServletContext` 有关的？那跟 `ApplicationContext` 没关系咯。。。是的，它强调的是从一个**封闭的 “上下文” 中加载**，这其实就是说像 `ServletContext` 这种域（当然文档也说了只是举个例子）。

以上就是 SpringFramework 中设计的资源模型，不过平时咱用的只有 `Resource` 接口而已。

## 3. SpringFramework中的资源模型实现

说完了接口，下面说说实现类。不过在说实现类之前，咱先说另外一件事，就是 Java 原生的资源加载方式。

### 3.1 Java原生资源加载方式

回想一下，Java 原生能加载到哪些地方的资源？应该大致上分 3 种吧：

* 借助 ClassLoader 加载类路径下的资源
* 借助 File 加载文件系统中的资源
* 借助 URL 和不同的协议加载本地 / 网络上的资源

这三种方式基本上就囊括了大部分的加载方式了。为什么要提它呢？那是因为，SpringFramework 中的资源模型实现，就是这三种的体现。

### 3.2 SpringFramework的实现

SpringFramework 分别对上面提到的这三种情况提供了三种不同的实现：

* ClassLoader → `ClassPathResource` \[ classpath:/ ]
* File → `FileSystemResource` \[ file:/ ]
* URL → `UrlResource` \[ xxx:/ ]

注意每一行最后的方括号，它代表的是资源路径的前缀：如果是 **classpath** 开头的资源路径，SpringFramework 解析到后会自动去类路径下找；如果是 **file** 开头的资源路径，则会去文件系统中找；如果是 URL 支持的协议开头，则底层会使用对应的协议，去尝试获取相应的资源文件。

除了这三种实现，还有对应于 `ContextResource` 的实现：`ServletContextResource` ，它意味着资源是去 `ServletContext` 域中寻找。

## 4. SpringFramework加载资源的方式

其实关于 SpringFramework 的资源加载，在 15 章的 1.6 节就已经提到过一个 `ResourcePatternResolver` ，它的父接口 `ResourceLoader` 就是那个真正负责加载资源的角色。另外在 15 章的 2.1.4 节，咱也提过在 `AbstractApplicationContext` 中，通过类继承关系可以得知它继承了 `DefaultResourceLoader` ，也就是说，`**ApplicationContext**` **具有加载资源的能力**。

下面咱简单了解一下 `DefaultResourceLoader` 是如何根据一个路径，加载到相应的资源的。

这段源码的篇幅稍微有点长，咱分开几个部分来看：

### 4.1 DefaultResourceLoader组合了一堆ProtocolResolver

```java
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

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;
        }
    }
    // ......
}
```

这一段，它会先取它内部组合的几个 `ProtocolResolver` 来尝试着加载资源，而这个 `ProtocolResolver` 的设计也是跟 `ResourceLoader` 有关。

#### 4.1.1 ProtocolResolver

它的设计倒是蛮简单了：

```java
/**
 * A resolution strategy for protocol-specific resource handles.
 *
 * <p>Used as an SPI for {@link DefaultResourceLoader}, allowing for
 * custom protocols to be handled without subclassing the loader
 * implementation (or application context implementation).
 *
 * @author Juergen Hoeller
 * @since 4.3
 * @see DefaultResourceLoader#addProtocolResolver
 */
@FunctionalInterface
public interface ProtocolResolver {
	Resource resolve(String location, ResourceLoader resourceLoader);
}
```

它只有一个接口，而且是在 SpringFramework 4.3 版本才出现的，它本身可以搭配 `ResourceLoader` ，在 `ApplicationContext` 中实现**自定义协议的资源加载**，但它还可以脱离 `ApplicationContext` ，直接跟 `ResourceLoader` 搭配即可。

#### 4.1.2 ProtocolResolver使用方式

在工程的 `resources` 目录下新建一个 `Dog.txt` 文件，然后写一个 `DogProtocolResolver` ，实现 `ProtocolResolver` 接口：

```java
public class DogProtocolResolver implements ProtocolResolver {

    public static final String DOG_PATH_PREFIX = "dog:";

    @Override
    public Resource resolve(String location, ResourceLoader resourceLoader) {
        if (!location.startsWith(DOG_PATH_PREFIX)) {
            return null;
        }
        // 把自定义前缀去掉
        String realpath = location.substring(DOG_PATH_PREFIX.length());
        String classpathLocation = "classpath:resource/" + realpath;
        return resourceLoader.getResource(classpathLocation);
    }
}
```

然后，编写启动类，分别实例化 `DefaultResourceLoader` 与 `DogProtocolResolver` ，并将 `DogProtocolResolver` 加入到 `ResourceLoader` 中：

```java
public class ProtocolResolverApplication {

    public static void main(String[] args) throws Exception {
        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
        DogProtocolResolver dogProtocolResolver = new DogProtocolResolver();
        resourceLoader.addProtocolResolver(dogProtocolResolver);
    }
}
```

然后，用 `ResourceLoader` 获取刚编写好的 `Dog.txt` ，并用缓冲流读取：

```java
public static void main(String[] args) throws Exception {
    DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
    DogProtocolResolver dogProtocolResolver = new DogProtocolResolver();
    resourceLoader.addProtocolResolver(dogProtocolResolver);

    Resource resource = resourceLoader.getResource("dog:Dog.txt");
    InputStream inputStream = resource.getInputStream();
    InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
    BufferedReader br = new BufferedReader(reader);
    String readLine;
    while ((readLine = br.readLine()) != null) {
        System.out.println(readLine);
    }
    br.close();
}
```

运行 `main` 方法，控制台打印出 `Dog.txt` 的内容，证明 `DogProtocolResolver` 已经起到了作用：

```
wangwangwang
```

### 4.2 DefaultResourceLoader可自行加载类路径下的资源

```java
public Resource getResource(String location) {
    // ......
    if (location.startsWith("/")) {
        return getResourceByPath(location);
    } else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    // ......
}
```

这部分，且不看上面的 `startsWith` ，只看中间的 else if 部分返回的类型，就知道它能解析类路径下的资源了，而上面的 `getResourceByPath` 方法，点进去发现默认还是加载类路径下：

```java
protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, getClassLoader());
}
```

不过这个不是绝对的，如果小伙伴现在手头的工程还有引入 `spring-web` 模块的 pom 依赖，会发现 `DefaultResourceLoader` 的几个 Web 级子类中有重写这个方法，以 `GenericWebApplicationContext` 为例：

```java
protected Resource getResourceByPath(String path) {
    Assert.state(this.servletContext != null, "No ServletContext available");
    return new ServletContextResource(this.servletContext, path);
}
```

可以发现这里创建的不再是类路径下了，Web 环境下 SpringFramework 更倾向于从 `ServletContext` 中加载。

### 4.3 DefaultResourceLoader可支持特定协议

```java
public Resource getResource(String location) {
    // ......
    else {
        try {
            // 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);
        }
    }
}
```

如果上面它不能处理类路径的文件，就会尝试通过 URL 的方式加载，这里面包含文件系统的资源，和特殊协议的资源。这里面咱就不进一步深入了，小伙伴们了解 `DefaultResourceLoader` 能加载的资源类型和大体的流程即可。

## 5. `@PropertySource`

### 5.1 @PropertySource引入properties文件

常规的使用方式，当然是引入 properties 文件了，下面咱快速回顾一下这种使用方式。

#### 5.1.1 声明properties文件

在 `resources` 目录下新建一个 `propertysource` 文件夹，此处存放本章声明的所有资源文件。

新建一个 `jdbc.properties` 文件，用来代表声明一个 jdbc 的连接属性：

```properties
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver-class-name=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=123456
```

#### 5.1.2 编写配置模型类

为了方便观察资源文件是否注入到了 IOC 容器，咱写一个模型类来接收这些配置项，就叫 `JdbcProperties` 吧：

```java
@Component
public class JdbcProperties {

    @Value("${jdbc.url}")
    private String url;
    
    @Value("${jdbc.driver-class-name}")
    private String driverClassName;
    
    @Value("${jdbc.username}")
    private String username;
    
    @Value("${jdbc.password}")
    private String password;
    
    // 省略getter setter toString
}
```

#### 5.1.3 编写配置类

新建一个 `JdbcPropertiesConfiguration` ，扫描配置模型类所在的包，并声明导入上面的 `jdbc.properties` 文件：

```java
@Configuration
@ComponentScan("com.linkedbear.spring.annotation.g_propertysource.bean")
@PropertySource("classpath:propertysource/jdbc.properties")
public class JdbcPropertiesConfiguration {
    
}
```

#### 5.1.4 测试运行

编写启动类，驱动 `JdbcPropertiesConfiguration` 配置类，并尝试打印容器中的配置模型类的属性：

```java
public class PropertySourcePropertiesApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                JdbcPropertiesConfiguration.class);
        System.out.println(ctx.getBean(JdbcProperties.class).toString());
    }
}
```

运行 `main` 方法，控制台打印了 `jdbc.properties` 中的属性，证明 properties 文件导入成功。

```properties
JdbcProperties{url='jdbc:mysql://localhost:3306/test', driverClassName='com.mysql.jdbc.Driver', username='root', password='123456'}
```

### 5.2 @PropertySource引入xml文件

意料之外吧，`@PropertySource` 还可以引入 xml 文件，其实在它的注解属性上已经有标注了：

Indicate the resource location(s) of the properties file to be loaded. Both traditional and XML-based properties file formats are supported.

指示要加载的属性文件的资源位置。 支持原生 properties 和基于 XML 的属性文件格式。

那下面咱就来体会一下导入 xml 的方式。

#### 5.2.1 声明xml文件

新建一个 `jdbc.xml` 文件，但是这里面的写法可是有严格的格式要求的：

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <entry key="xml.jdbc.url">jdbc:mysql://localhost:3306/test</entry>
    <entry key="xml.jdbc.driver-class-name">com.mysql.jdbc.Driver</entry>
    <entry key="xml.jdbc.username">root</entry>
    <entry key="xml.jdbc.password">123456</entry>
</properties>
```

可能这个时候小伙伴们的表情已经有点 “地铁老人看手机” 的样子了。

写个这玩意咋还这么费劲了？然而没招，这是 sun 当时给出的 Properties 格式的 xml 标准规范写法，必须按照这个格式来，才能解析为 `Properties` 。咱先这么写，稍后会解释这样写的原因。

#### 5.2.2 编写配置模型类

仿造上面的配置模型类，搞一个基本一样的出来：（注意这里的 `@Value` 取值加了 xml 前缀）

```java
@Component
public class JdbcXmlProperty {
    
    @Value("${xml.jdbc.url}")
    private String url;
    
    @Value("${xml.jdbc.driver-class-name}")
    private String driverClassName;
    
    @Value("${xml.jdbc.username}")
    private String username;
    
    @Value("${xml.jdbc.password}")
    private String password;
    
    // 省略getter setter toString
}
```

#### 5.2.3 编写配置类

仿造上面的配置类写法，造一个基本一样的配置类，注意扫描包的位置不要写错了：

```java
@Configuration
@ComponentScan("com.linkedbear.spring.annotation.h_propertyxml.bean")
@PropertySource("classpath:propertysource/jdbc.xml")
public class JdbcXmlConfiguration {
    
}
```

#### 5.2.4 测试运行

编写启动类，驱动 `JdbcXmlConfiguration` ，并打印 IOC 容器中的 `JdbcXmlProperty` ：

```java
public class PropertySourceXmlApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JdbcXmlConfiguration.class);
        System.out.println(ctx.getBean(JdbcXmlProperty.class).toString());
    }
}
```

运行 `main` 方法，控制台也可以打印出各项属性：

```properties
JdbcXmlProperty{url='jdbc:mysql://localhost:3306/test', driverClassName='com.mysql.jdbc.Driver', username='root', password='123456'}
```

#### 5.2.5 xml格式被限制的原因

好了，来解答上面那个问题：为了完成跟 `.properties` 文件一样的写法，反而在 xml 中要写这么一大堆乱七八糟的格式，这都哪来的？？？别着急，在这个问题之前，先请小伙伴思考另一个问题：SpringFramework 是怎么加载那些 `.properties` 文件的呢？

答案很简单嘛，肯定是走的 jdk 原生的 `Properties` 类咯。下面咱来解答这个问题。

**5.2.5.1 解析Properties的入口**

答案的追踪可以从 `@PropertySource` 注解的一个属性入手：

```java
public @interface PropertySource {
    // ......

	/**
	 * Specify a custom {@link PropertySourceFactory}, if any.
	 * <p>By default, a default factory for standard resource files will be used.
	 * @since 4.3
	 */
	Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}
```

这里有一个 `factory` 的属性，它自 SpringFramework 4.3 开始出现，它代表的是：**使用什么类型的解析器解析当前导入的资源文件**，说的简单点，它想表达的是,用 `@PropertySource` 注解引入的资源文件需要用什么策略来解析它。默认情况下它只放了一个 `PropertySourceFactory` 在这里，看一眼 `factory` 属性的泛型也能大概猜得出来，`PropertySourceFactory` 应该是一个接口 / 抽象类，它肯定有默认实现的子类。果不其然，借助 IDEA 咱很容易就能找到它在 SpringFramework 中默认的唯一实现：`DefaultPropertySourceFactory` 。

**5.2.5.2 默认的Properties解析工厂**

```java
public class DefaultPropertySourceFactory implements PropertySourceFactory {

	@Override
	public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
		return (name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource));
	}
}
```

默认实现中，它只是 new 了一个 `ResourcePropertySource` 而已，而这个构造方法中有一句让我们很敏感的方法调用：`PropertiesLoaderUtils.loadProperties`

```java
/**
 * Create a PropertySource having the given name based on Properties
 * loaded from the given encoded resource.
 */
public ResourcePropertySource(String name, EncodedResource resource) throws IOException {
    super(name, PropertiesLoaderUtils.loadProperties(resource));
    this.resourceName = getNameForResource(resource.getResource());
}
```

进入这个 `loadProperties` 方法中：

```java
public static Properties loadProperties(EncodedResource resource) throws IOException {
    Properties props = new Properties();
    fillProperties(props, resource);
    return props;
}
```

得了，它的底层果然是这么用的，那问题自然也就解开了。`@PropertySource` 解析 xml 也是用 `Properties` 这个类解析的。可是我们在之前的 JavaSE 中可能没学过 `Properties` 解析 xml 啊，这还第一次听说咧。

**5.2.5.3 jdk原生Properties解析xml**

其实在 jdk 内置的 `Properties` 类中有这么一个方法可以解析 xml 文件：

```java
public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException {
    XmlSupport.load(this, Objects.requireNonNull(in));
    in.close();
}
```

只是这个 xml 的要求，属实有点高，它是 sun 公司在很早之前就制定的一个 xml 表达 properties 的标准：（以下是 dtd 约束文件内容）

```java
<!--
   Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
  -->

<!-- DTD for properties -->

<!ELEMENT properties ( comment?, entry* ) >

<!ATTLIST properties version CDATA #FIXED "1.0">

<!ELEMENT comment (#PCDATA) >

<!ELEMENT entry (#PCDATA) >

<!ATTLIST entry key CDATA #REQUIRED>
```

可以发现确实是有固定格式的，必须按照这个约束来编写 xml 文件。这个东西知道就好了，估计你以后也用不到 \~ \~ \~

**5.2.5.4 properties与xml的对比**

对比一下 properties 与 xml 的编写风格：

```properties
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver-class-name=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=123456
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <entry key="xml.jdbc.url">jdbc:mysql://localhost:3306/test</entry>
    <entry key="xml.jdbc.driver-class-name">com.mysql.jdbc.Driver</entry>
    <entry key="xml.jdbc.username">root</entry>
    <entry key="xml.jdbc.password">123456</entry>
</properties>
```

难易程度高下立判，properties 完胜，所以对于这种配置型的资源文件，通常都是使用 properties 来编写。

当然，properties 也不是完全 OK ，由于它的特征是 key-value 的形式，整个文件排下来是没有任何层次性可言的（换句话说，每个配置项之间的地位都是平等的）。这个时候 xml 的优势就体现出来了，它可以非常容易的体现出层次性，不过咱不能因为这一个点就觉得 xml 还可以，因为有一个更适合解决这个问题的配置格式：**yml** 。

### 5.3 @PropertySource引入yml文件

接触过 SpringBoot 的小伙伴对 yml 肯定很熟悉了，当然也不乏有一些新学习的小伙伴，所以咱还是在这里简单介绍下 yml 。

#### 5.3.1 yml的语法格式

**yml** 又称 **yaml** ，它是可以代替 properties 同时又可以表达层级关系的标记语言，它的基本格式如下：

```yaml
person: 
  name: zhangsan
  age: 18
  cat: 
    name: mimi
    color: white
dog: 
  name: wangwang
```

可以发现这种写法既可以表达出 properties 的 key-value 形式，同时可以非常清晰的看到层级之间的关系（ cat 在 person 中，person 与 dog 在一个层级）。这种写法同等于下面的 properties ：

```properties
person.name=zhangsan
person.age=18
person.cat.name=mimi
person.cat.color=white
dog.name=wangwang
```

两种写法各有优劣，在 SpringBoot 中这两种写法都予以支持。不过这不是咱本章讨论的重点了，下面咱介绍如何把 yml 引入到 IOC 容器中。

#### 5.3.2 声明yml文件

根据上面的 yaml 格式，可以编写出如下 yml 的内容：

```yaml
yml: 
  jdbc:
    url: jdbc:mysql://localhost:3306/test
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 123456
```

#### 5.3.3 编写配置模型类

写法与上面的一模一样，不过 `@Value` 中的 key 前缀改为了 yml ：

```java
@Component
public class JdbcYmlProperty {
    
    @Value("${yml.jdbc.url}")
    private String url;
    
    @Value("${yml.jdbc.driver-class-name}")
    private String driverClassName;
    
    @Value("${yml.jdbc.username}")
    private String username;
    
    @Value("${yml.jdbc.password}")
    private String password;
    
    // 省略getter setter toString
}
```

#### 5.3.4 编写配置类

继续仿照上面的写法，把配置类也造出来：

```java
@Configuration
@ComponentScan("com.linkedbear.spring.annotation.i_propertyyml.bean")
@PropertySource("classpath:propertysource/jdbc.yml")
public class JdbcYmlConfiguration {
    
}
```

#### 5.3.5 测试运行

直接编写启动类，驱动 `JdbcYmlConfiguration` ，取出 `JdbcYmlProperty` 并打印：

```java
public class PropertySourceYmlApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JdbcYmlConfiguration.class);
        System.out.println(ctx.getBean(JdbcYmlProperty.class).toString());
    }
}
```

运行 `main` 方法，发现属性是一个也没有注入呀：

```dart
JdbcYmlProperty{url='${yml.jdbc.url}', driverClassName='${yml.jdbc.driver-class-name}', username='${yml.jdbc.username}', password='${yml.jdbc.password}'}
```

小伙伴们是不是又开始 “地铁老人看手机” 了？如果你真的在 “地铁老人看手机” ，那你可得往回翻一翻了哦，上面咱看到解析资源文件的默认实现策略是 `DefaultPropertySourceFactory` ，它是解析 properties 和标准 xml 文件的，要是能把 yml 文件也解析出来，那才奇了怪呢！

```java
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
```

那这个时候小伙伴们又有话说了：咋地搁这玩我呐，正经方法不教，净搁这乱跳 \~ \~ \~ 好吧，不闹了，下面介绍如何把 yml 文件也解析出来，并且加载进 IOC 容器。

#### 5.3.6 自定义PropertySourceFactory解析yml

解析 yml 文件，讲道理咱能搞，但是太费劲了，而且现有的开源技术中已经有很成熟的组件能解决 yml 文件的解析，所以咱就来引入一个目前来讲非常成熟，且一直被 SpringBoot 使用的 yml 解析器：`snake-yaml` 。

**5.3.6.1 导入snake-yaml的maven坐标**

在 2020 年 2 月，snake-yaml 升级了 1.26 版本，自此之后很长一段时间没有再升级过，且观察最近几个版本的更新速度也非常慢，基本可以断定它的近几个版本都是很稳定的，于是咱就选择这个 1.26 版本作为 yml 文件的解析底层。

```xml
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.26</version>
</dependency>
```

**5.3.6.2 自定义PropertySourceFactory**

为了代替原有的 `DefaultPropertySourceFactory` ，就需要咱来自定义一个 `PropertySourceFactory` 的实现了，那就造一个吧，名就叫 `YmlPropertySourceFactory` ：

```java
public class YmlPropertySourceFactory implements PropertySourceFactory {
    
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        return null;
    }
}
```

之后，把这个 `YmlPropertySourceFactory` 设置到 `@PropertySource` 中：

```java
@PropertySource(value = "classpath:propertysource/jdbc.yml", factory = YmlPropertySourceFactory.class)
```

注意看这个接口的方法，它要返回一个 `PropertySource<?>` ，借助 IDEA 观察它的继承关系，可以发现它里头有一个实现类叫 `PropertiesPropertySource` ：

![img](https://cdn.nlark.com/yuque/0/2022/png/229542/1668767596422-63939c4e-1953-447e-aead-1093d8bf8705.png)

那估计咱用这个返回就可以吧！点开它，发现它只有一个公开的构造方法：

```java
public PropertiesPropertySource(String name, Properties source) {
    super(name, (Map) source);
}
```

果然，它只需要传 name 和 `Properties` 对象就可以了。

于是，现在的目标就变成了：如何把 yml 资源文件的对象，转换为 `Properties` 的对象。

**5.3.6.3 资源文件转换为Properties对象**

在 snake-yaml 中有一个能快速解析 yml 文件的类，叫 `YamlPropertiesFactoryBean` ，它可以快速加载 `Resource` 并转为 `Properties` ，具体写法可参加下面的实现：

```java
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
    YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
    // 传入resource资源文件
    yamlPropertiesFactoryBean.setResources(resource.getResource());
    // 直接解析获得Properties对象
    Properties properties = yamlPropertiesFactoryBean.getObject();
    // 如果@PropertySource没有指定name，则使用资源文件的文件名
    return new PropertiesPropertySource((name != null ? name : resource.getResource().getFilename()), properties);
}
```

注意这个 `EncodedResource` ，它就是上一章 2.3 节中提到的封装式资源哦。

#### 5.3.7 重新测试

重新运行 `main` 方法，控制台打印出配置文件的内容，证明 yml 文件解析成功。

```properties
JdbcYmlProperty{url='jdbc:mysql://localhost:3306/test', driverClassName='com.mysql.jdbc.Driver', username='root', password='123456'}
```
