之前写了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 groups = getGroups();
binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups));
Map 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;
}
完成BeanPostProcessor的编写,并不是万事大吉,因为我们肯定是用starter组件形式来引入到其它工程,所以直接引入jar包 spring并不会帮我们把组件注入到容器,我们想到的可以用import注解在工程启动类里注入,但是这样代码会增加代码耦合,并没有达到starter组件即拔即用的特性(不需要直接在pom文件删除对应引入即可),所以我们用上了spring.factories文件帮助我们
SpringFactoriesLoader 负责读取工程和所有引用jar包里的META-INF/spring.factories文件,可以根据key返回集合value,方便后面实例化。在spring启动的时候应用如下:
spring需要拿到ApplicationContextInitializer类型对象的集合,所以它利用SpringFactoriesLoader工具去读取。返回6个String字段,后面就是通过反射进行实例化的过程。
上两个问题搞定了基本上写一个spring的组件就没那么难了,以前觉得提高需要多看spring的源码,但是发现不从实际需求出发,还是很难形成印象.
上面的代码已经上传github,跨年后才刚刚写好,肯定还有问题,多多见谅
https://github.com/lovejj1994/simplify-cache-spring-boot-starter
https://github.com/lovejj1994/simplify-cache-spring-boot-starter-test
记得一年前写过一篇年终总结,那时候还在北京,说要以后每年都要写一次总结,然后今年就没写了,因为太多给自己定的任务没有去完成.而且跨年前后也在完善多级缓存的组件.感觉写了也没有意思.这是在上海的第一年,工作上感觉学的还有那么多帮助的,说了再多还是继续给自己加油.