之前写了java的多级缓存,是一个简单的util工具包,想着能不能跟springboot 做集成,顺便了解下spring boot 组件原理,比如众多的 xx-spring-boot-starter.这篇文章以 spring boot 2.x为基础。

如何读取配置文件

这个是面临的第一个问题,以logging 日志的集成为例。我们都知道logging日志的配置可以配置以"logging.level"打头,而后面跟上的是包名,有没有想过这种配置读取是怎么做到的?

在spring 初始化启动的过程中,会根据生命周期的不同阶段,发出对应的动作。这就是Spring ApplicationListener,设计基于观察者模式,而其中LoggingApplicationListener类便是负责logging日志框架的初始化操作。

LoggingApplicationListener被配置在spring-boot-x.x.x.jar的spring.factories文件中,spring启动的时候会去读取这个文件

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

LoggingApplicationListener 的事件处理

@Override
//spring内置了很多event事件,LoggingApplicationListener根据spring生命周期的不同阶段,做不同的处理
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationStartingEvent) {
			onApplicationStartingEvent((ApplicationStartingEvent) event);
		}
		//这里会触发读取"logging.level"的操作
		else if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent(
					(ApplicationEnvironmentPreparedEvent) event);
		}
		else if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent((ApplicationPreparedEvent) event);
		}
		else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
				.getApplicationContext().getParent() == null) {
			onContextClosedEvent();
		}
		else if (event instanceof ApplicationFailedEvent) {
			onApplicationFailedEvent();
		}
	}

读取操作就在于Binder,是spring boot 2.0版本推出新的属性绑定,用来替换1.0中 RelaxedPropertyResolver等类的功能(所以多级缓存的包也会分2.x和1.x),从下面的代码也可以看出,它提供了部分匹配查找的功能,所以可以实现类似"logging.level"配置的读取.

private static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName
			.of("logging.level");

	protected void setLogLevels(LoggingSystem system, Environment environment) {
		if (!(environment instanceof ConfigurableEnvironment)) {
			return;
		}
		Binder binder = Binder.get(environment);
		Map<String, String[]> groups = getGroups();
		binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups));
		Map<String, String> levels = binder.bind(LOGGING_LEVEL, STRING_STRING_MAP)
				.orElseGet(Collections::emptyMap);
		levels.forEach((name, level) -> {
			String[] groupedNames = groups.get(name);
			if (ObjectUtils.isEmpty(groupedNames)) {
				setLogLevel(system, name, level);
			}
			else {
				setLogLevel(system, groupedNames, level);
			}
		});
	}

在 何时/如何 初始化组件

不得不说Spring良好的设计给了我们很大的拓展空间, 光初始化一个对象就可以在多个层面操作,比如实现InitializingBean,BeanPostProcessor,Listener,我用的是BeanPostProcessor.

实现了BeanPostProcessor的类会接收到所有spring容器管理的对象,你可以在任何初始化回调前或后(比如实现了InitializingBean接口) 执行自己的方法,对这个bean加以改造,这就给自定义bean提供了很大的空间.

具体的执行方法在AbstractAutowireCapableBeanFactory类里的applyBeanPostProcessorsBeforeInitialization方法,众所周知这里负责了bean 装配的整个流程.从代码可以看出它遍历所有实现了BeanPostProcessor的类,然后执行方法.也可以看出这里是用观察者模式实现的.

@Override
	public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
			throws BeansException {

		Object result = existingBean;
		for (BeanPostProcessor processor : getBeanPostProcessors()) {
			Object current = processor.postProcessBeforeInitialization(result, beanName);
			if (current == null) {
				return result;
			}
			result = current;
		}
		return result;
	}
spring.factories

完成BeanPostProcessor的编写,并不是万事大吉,因为我们肯定是用starter组件形式来引入到其它工程,所以直接引入jar包 spring并不会帮我们把组件注入到容器,我们想到的可以用import注解在工程启动类里注入,但是这样代码会增加代码耦合,并没有达到starter组件即拔即用的特性(不需要直接在pom文件删除对应引入即可),所以我们用上了spring.factories文件帮助我们

SpringFactoriesLoader 负责读取工程和所有引用jar包里的META-INF/spring.factories文件,可以根据key返回集合value,方便后面实例化。在spring启动的时候应用如下:

spring需要拿到ApplicationContextInitializer类型对象的集合,所以它利用SpringFactoriesLoader工具去读取。返回6个String字段,后面就是通过反射进行实例化的过程。

springboot logback log没有输出_BeanPostProcessor

大功告成

上两个问题搞定了基本上写一个spring的组件就没那么难了,以前觉得提高需要多看spring的源码,但是发现不从实际需求出发,还是很难形成印象.
上面的代码已经上传github,跨年后才刚刚写好,肯定还有问题,多多见谅

https://github.com/lovejj1994/simplify-cache-spring-boot-starter
https://github.com/lovejj1994/simplify-cache-spring-boot-starter-test

记得一年前写过一篇年终总结,那时候还在北京,说要以后每年都要写一次总结,然后今年就没写了,因为太多给自己定的任务没有去完成.而且跨年前后也在完善多级缓存的组件.感觉写了也没有意思.这是在上海的第一年,工作上感觉学的还有那么多帮助的,说了再多还是继续给自己加油.