Spring Resource

1. SpringFramework为什么要自己写Resource

一看到资源管理,或许会有小伙伴立马想到 ClassLoadergetResourcegetResourceAsStream 方法,它们本身就是 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中的资源模型

先来一张图概览一下吧:

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

2.1 InputStreamSource

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 ,说明它本身并不是直接加载资源的。

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

2.4 WritableResource

继续往下看,自打 SpringFramework 3.1 之后,Resource 有了一个新的子接口:WritableResource ,它代表着“可写的资源”,那 Resource 就可以理解为“可读的资源”(有木有想起来 BeanFactoryConfigurableBeanFactory ?)。

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

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

它的设计倒是蛮简单了:

/**
 * 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 接口:

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);
    }
}

然后,编写启动类,分别实例化 DefaultResourceLoaderDogProtocolResolver ,并将 DogProtocolResolver 加入到 ResourceLoader 中:

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 ,并用缓冲流读取:

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可自行加载类路径下的资源

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 方法,点进去发现默认还是加载类路径下:

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

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

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可支持特定协议

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 的连接属性:

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 吧:

@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 文件:

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

5.1.4 测试运行

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

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 文件导入成功。

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 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 前缀)

@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 编写配置类

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

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

5.2.4 测试运行

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

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 方法,控制台也可以打印出各项属性:

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 注解的一个属性入手:

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解析工厂

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

/**
 * 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 方法中:

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 文件:

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

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

<!--
   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 的编写风格:

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 同时又可以表达层级关系的标记语言,它的基本格式如下:

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

可以发现这种写法既可以表达出 properties 的 key-value 形式,同时可以非常清晰的看到层级之间的关系( cat 在 person 中,person 与 dog 在一个层级)。这种写法同等于下面的 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 的内容:

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 :

@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 编写配置类

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

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

5.3.5 测试运行

直接编写启动类,驱动 JdbcYmlConfiguration ,取出 JdbcYmlProperty 并打印:

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 方法,发现属性是一个也没有注入呀:

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

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

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 文件的解析底层。

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

5.3.6.2 自定义PropertySourceFactory

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

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

之后,把这个 YmlPropertySourceFactory 设置到 @PropertySource 中:

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

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

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

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 ,具体写法可参加下面的实现:

@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 文件解析成功。

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

最后更新于