jjzjj

SpringBoot 阅读源码之RandomValuePropertySource 是如何工作的

dk168 2023-04-17 原文

最近在极客时间上面学习丁雪丰老师的《玩转 Spring 全家桶》,看到一个在application.properties 里面生成随机数的例子,觉得很厉害,带着崇拜的心情去阅读了一下Spring的源码,总算搞清楚了它是怎么工作的,阅读优秀的源代码是一个很不错的学习方式,这篇文章就是记录一下这个学习的过程。

还是先通过示例来进入学习

首先我们建一个springboot的工程,入口程序如下, 就是打印出两个从配置文件中定义的value

@SpringBootApplication
@Slf4j
public class PropertySourceDemoApplication implements ApplicationRunner {
    @Value("${property.demo.test}")
    private String hello;

    @Value("${property.demo.random}")
    private int random_value;
    public static void main(String[] args) {
        SpringApplication.run(PropertySourceDemoApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("{} {}", hello, random_value);
    }
}


配置文件 application.properties

property.demo.random=${random.int}
property.demo.test=hello

程序输入出为 "hello -14137621", 这个-14137621就是我们生成的随机数
那么这个功能是怎么在spring-boot实现的呢?

准备阶段

首先我们需要工具类,我们自己可以设计一下,如果我们需要完成这项任务,那么我们需要两个最基本的类,一个是随机数生成类org.springframework.boot.env.RandomValuePropertySource,一个是读取配置文件类 org.springframework.boot.env.PropertiesPropertySourceLoader。我们来看下这两个类的代码。

上图是RandomValuePropertySource, 我们可以看到它的getProperty方法就是看name是不是以random开头,是的话就生成随机数返回,这就是我们为什么值为${random.int}

再来看PropertiesPropertySourceLoader

public class PropertiesPropertySourceLoader implements PropertySourceLoader {

	private static final String XML_FILE_EXTENSION = ".xml";

	@Override
	public String[] getFileExtensions() {
		return new String[] { "properties", "xml" };
	}

	@Override
	public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
		List<Map<String, ?>> properties = loadProperties(resource);
		if (properties.isEmpty()) {
			return Collections.emptyList();
		}
		List<PropertySource<?>> propertySources = new ArrayList<>(properties.size());
		for (int i = 0; i < properties.size(); i++) {
			String documentNumber = (properties.size() != 1) ? " (document #" + i + ")" : "";
			propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
					Collections.unmodifiableMap(properties.get(i)), true));
		}
		return propertySources;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private List<Map<String, ?>> loadProperties(Resource resource) throws IOException {
		String filename = resource.getFilename();
		List<Map<String, ?>> result = new ArrayList<>();
		if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
			result.add((Map) PropertiesLoaderUtils.loadProperties(resource));
		}
		else {
			List<Document> documents = new OriginTrackedPropertiesLoader(resource).load();
			documents.forEach((document) -> result.add(document.asMap()));
		}
		return result;
	}

}

通过阅读这个代码我们可以看出或者猜测,它就是把后缀名是properties或者xml的文件读到内存中,实际上也是这样的。
有了这两个类,剩下的就是如何把它们组织起来,这是spring强大的地方,也是复杂的地方。
这里不同版本可能代码不一样,但是大同小异吧,我使用的是springboot 3.0.0。
这里把代码的组织结构贴出来

最后我们可以看到执行了ApplicationListener.onApplicationEvent.
我们打开spring.factories 中看到EnviormentPostProcessorApplicationListener实现了这个接口

打开它的代码可以看到

最后到了EnvironmentPostProcessor.postProcessEnvironment
这个EnvironmentPostProcessor也是在spring.factories里面配置了


可以看到这里调用了我们前面说的工具类的RandomValuePropertySource.addToEnvironment(environment, this.logger);
我们来看一眼它的代码

可以看到它就是把RandomValuePropertySource添加到Enviorment的PropertySources里面,留在后面用。 后面我们会看到它是怎么用。

代码的流程大概是这样的
SpringApplication -> Run() -> prepareEnvironment() ->listeners.environmentPrepared(bootstrapContext, environment)
EventPublishingRunListener -> SimpleApplicationEventMulticaster -> multicastEvent() -> listener.onApplicationEvent(event)
onApplicationEnvironmentPreparedEvent-> RandomValuePropertySource.addToEnvironment()

代码到这里RandomValuePropertySource工具就准备好了, 读配置文件的类PropertiesPropertySourceLoader代码类似,这里不详述。

生成随机数

我原本以为读出配置文件后,就直接用RandomValuePropertySource生成了随机数,后来通过调试发现不是的,生成随机数是在Bean创建的时候,拿到需要注入的field的时候,再去找配置文件内存中找到,然后再生成的。 这个过程有些复杂,我花了不少时间才将它理出来。

SpringApplication.run

这里跳到到spring framework包里面的org.springframework.context.support.refresh()方法

这里图太多不贴了,只贴主要类和方法
AbstractApplicationContext.finishBeanFactoryInitialization -> DefaultListableBeanFactory.preInstantiateSingletons ->
DefaultListableBeanFactory.getBean ->
AbstractBeanFactory.getBean ->
AbstractAutowireCapableBeanFactory.createBean() ->
AbstractAutowireCapableBeanFactory.doCreateBean() ->
AbstractAutowireCapableBeanFactory.populateBean() ->

这里调用InstantiationAwareBeanPostProcessor.postProcessProperties 会有一个List,只有当对象是AutoWiredAnnotationBeanPostProcessor的时候才会调用我们的给注解加载值。

这里调用InjectMetadata.inject() 就是注入我们要的value.
InjectedElement.inject() ->
InjectionMetadata.getResourceToInject() ->
CommonAnnotationBeanPostProcessor.getResource() ->
CommonAnnotationBeanPostProcessor.autowireResource() ->
AutowireCapableBeanFactory.resolveDependency() ->
DefaultListableBeanFactory.doResolveDependency() ->
AbstractBeanFactory.resolveEmbeddedValue

这个embeddedValueResolvers是这么来的
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));

AbstractPropertyResolver.resolvePlaceholders()
AbstractPropertyResolver.doResolvePlaceholders()
PropertySourcesPropertyResolver.getPropertyAsRawString()

这里的this.PropertySources就是包含了最开始准备阶段的RandomValuePropertySource。有好几个PropertySource都去找值,找到值就返回。
这样我们整个流程就打通了。

总结

Spring的代码非常的庞大,代码水平也非常高,往往不是我们常规的思路,需要非常复杂的组织起代码来,我阅读整个代码的时候就是不停的debug,带着问题去找,这是我第一次去阅读它的代码,花的时间比较多,还有许多问题没有来的及看,比如spring.factories 是怎么去初始化那些接口的呢?等等问题。 万事开头难,走出第一步,我想后面再读会容易很多。

有关SpringBoot 阅读源码之RandomValuePropertySource 是如何工作的的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐