对原理不感兴趣的可以直接使用框架https://blog.csdn.net/cjc000/article/details/134021209?spm=1001.2014.3001.5501
我们回忆下上一篇文章的内容,属性注入的关键节点是PropertySourcesPlaceholderConfigurer的BeanFactory后置处理器org.springframework.context.support.PropertySourcesPlaceholderConfigurer#postProcessBeanFactory,只有在执行这个方法前设置到Environment的数据源,才能被应用到springboot启动中所有属性注入的阶段,这样才能优雅的将自己的代码加入扩展点中,否则还得自己手动处理@value注解,手动绑定@ConfigurationProperties等
上一篇文章说过任何地方都能加入数据源,但是我们通常需要在application.yml中读取一些属性来确定怎么获取数据源,比如获取数据源的地址、账号、密码什么的,那就需要我们在加载application.yml后再进行我们数据源的加载,下面我就介绍几个常用的场景
application.yml就是用的这个接口实现加载的,他是在ConfigDataEnvironmentPostProcessor#postProcessEnvironment中加载的,而这个方法的调用是EnvironmentPostProcessorApplicationListener在springboot启动时创建完Environment对象后收到的ApplicationEnvironmentPreparedEvent消息后,执行中spring.factories中定义的所有EnvironmentPostProcessor的接口,可以看到一些Environment中默认的数据源(random等)也用的这个接口来实现
所以我们也可以自定一EnvironmentPostProcessor接口来加入到springboot的启动流程中
EnvironmentPostProcessorApplicationListener在执行EnvironmentPostProcessor接口时是先进行排序根据order的顺序来依次执行,如果我们想在application.yml后执行,那么只需要比ConfigDataEnvironmentPostProcessor后执行即可,ConfigDataEnvironmentPostProcessor的order是Ordered.HIGHEST_PRECEDENCE + 10(这个+10就是springboot框架作为一个可扩展框架留给我们的扩展点,我们可以在+9或者+11来确定在他的前还是后执行我们的代码)
那我们只需要继承Ordered或用@Order注解将order设置为Ordered.HIGHEST_PRECEDENCE + 11即可,不能用PriorityOrdered,因为PriorityOrdered的类会优先于order类执行
- public class EnhanceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
-
- @Override
- public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
- String userId = environment.getProperty("user.id");
- Map<String, Object> enhanceSource = new HashMap<>();
- enhanceSource.put("user." + userId + ".name", "EnhanceEnvironmentPostProcessor设置的名称");
-
- MutablePropertySources propertySources = environment.getPropertySources();
- MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
- propertySources.addFirst(enhancePropertySource);
- }
-
- @Override
- public int getOrder() {
- return Ordered.HIGHEST_PRECEDENCE + 11;
- }
- }
springboot在SpringApplication构造器时会从spring.factories中获取所有EnvironmentPostProcessor的接口保存起来,并且在prepareContext阶段的applyInitializers()方法中以此去执行(也是根据order排好序的),因为这个阶段是在EnvironmentPostProcessor阶段后执行的,所以也可以获取到application.yml的属性
- public class EnhanceApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
-
- @Override
- public void initialize(ConfigurableApplicationContext applicationContext) {
- ConfigurableEnvironment environment = applicationContext.getEnvironment();
- String userId = environment.getProperty("user.id");
- Map<String, Object> enhanceSource = new HashMap<>();
- enhanceSource.put("user." + userId + ".name", "EnhanceApplicationContextInitializer设置的名称");
-
- MutablePropertySources propertySources = environment.getPropertySources();
- MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
- propertySources.addFirst(enhancePropertySource);
- }
- }
BeanFactoryPostProcessor就是更靠后的一个阶段了,这种方式注入的时候不仅能获取到application.yml中的属性,还可以获取到分布式配置中心的属性。因为springboot第一次用到配置也就是Environment对象,是在PropertySourcesPlaceholderConfigurer中解析BeanDefinition的propertyValues(上一篇讲过),那么分布式也要在这之前配置进去,如
而PropertySourcesPlaceholderConfigurer也是一个BeanFactoryPostProcessor,那我们只要在他之前加载即可,它使用了父类的PriorityOrdered设置的order
那我们只要比他优先一点就可以了,也继承PriorityOrdered,设置order为Ordered.LOWEST_PRECEDENCE-1
我们还是针对那四种获取属性的方式来执行自定义解析,BeanDefinition、@Value、@ConfigurationProperties、environment.getProperty。但environment.getProperty是在我们代码中调用的,我们完全可以自由控制对结果再执行自定义的解析方法,所以这个就没必要在讨论了,我们主要对另外三种springboot自动注入的属性(用户无感知)来做解析,比如我要写一个自定义解密的方法decode()
之前已经说过BeanDefinition是在org.springframework.beans.factory.config.PlaceholderConfigurerSupport#doProcessProperties中解析bean属性的
可以看到他是用的valueResolver来进行解析的,而springboot并没有在这留扩展点,所以我们需要,自己写一个StringValueResolver,并用这个解析器重新解析下BeanDefinition,而且我们要在PropertySourcesPlaceholderConfigurer之后执行,这样我们就能对springboot解析后的属性再进行一次解析,比如${user.123.name},springboot先会解析为decode(abc),然后我们这个再会将decode(abc)解析为123
- @Override
- public void postProcessBeanFactory(@Nullable ConfigurableListableBeanFactory beanFactory) throws BeansException {
- PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
- StringValueResolver valueResolver = strVal -> helper.replacePlaceholders(strVal, this::decodeValue);
-
- BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
-
- String[] beanNames = beanFactory.getBeanDefinitionNames();
- for (String curName : beanNames) {
- if (!(curName.equals(this.beanName) && beanFactory.equals(this.beanFactory))) {
- BeanDefinition bd = beanFactory.getBeanDefinition(curName);
- try {
- visitor.visitBeanDefinition(bd);
- } catch (Exception ex) {
- throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
- }
- }
- }
- }
- private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
- Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
- return new PropertyPlaceholderHelper("decode(", ")",
- PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
- }
-
- public String decodeValue(String strVal) {
- if (strVal != null && strVal.equals("abc")) {
- return "123";
- }
- return strVal;
- }
可以看到,我用了PropertyPlaceholderHelper来帮助解析属性,并且设置了前缀"decode("和后缀")",replacePlaceholders方法有两个参数,一个是要解析的值,第二个是PlaceholderResolver对象,当调用replacePlaceholders方法时会先将字符串中所有的decode(xxx)的子串,依次递归先去掉前缀和后缀,然后再调用PlaceholderResolver对象的resolvePlaceholder方法(我们自定义的)进行解析,解析完成后的子串如果还有decode(xxx)的子串会接续递归执行上面的步骤直到没有前缀。springboot默认的${xxx}这种类型的解析也是这里处理的只不过它的PlaceholderResolver对象的resolvePlaceholder方法,是从数据源集合里面调用获取属性对应的值。
对于@value,之前也说过,他也是在执行PropertySourcesPlaceholderConfigurer时,将StringValueResolver添加到beanFactory中,等后面解析的时候从这里面获取,也就是和上面的BeanDefinition解析用的同一个StringValueResolver。幸运的是springboot对于解析@value留了扩展点(因为beanFactory中保存了StringValueResolver的集合,解析@value的时候是从这个集合中遍历,用每一个解析器来对上一个解析的结果再做解析)
所以只要把我们自定义的那个StringValueResolver也加到beanFactory中即可,即对刚才的代码加上一行beanFactory.addEmbeddedValueResolver(valueResolver);
@ConfigurationProperties是由org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization来绑定的,之前也说过本质是是用的ConfigurationPropertiesBinder类的binder成员变量,而这个binder也是写死的生成方式,没有给扩展点。所以如果实现@ConfigurationProperties的自定义解析,那我们只能自定义binder,然后用binder来对@ConfigurationProperties的类来进行解析,或者利用反射来手动为ConfigurationPropertiesBinder的binder变量赋值
Binder中有数据源和解析方法,调用其bind方法时只要传入属性的前缀(prefix = "user.123")和要绑定的对象,即可对该目标对象进行属性绑定
ConfigurationPropertiesBindingPostProcessor在InitializingBean阶段中,从bean工厂中获取ConfigurationPropertiesBinder对象
并且注册这个bean时会判断是否已经有这个bean了,如果有的话就就不创建了,直接用已有的bean
所以针对这个@ConfigurationProperties的解析有以下几种方案
因为ConfigurationPropertiesBinder的作用域是friendly的,所以只能同一个包里能访问,我们只能反射来使用或者自己创建个org.springframework.boot.context.properties包,然后在里面写我们的替换逻辑,ConfigurationPropertiesBinder中的binder是私有的,所以要想设置上,只能用反射了
- public class BinderPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
-
- private Environment environment;
-
- @Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
- String beanName = "org.springframework.boot.context.internalConfigurationPropertiesBinder";
- Object configurationPropertiesBinder = beanFactory.getBean(beanName);
- try {
- //获取configurationPropertiesBinder的 binder 对象
- Field binderField = configurationPropertiesBinder.getClass().getDeclaredField("binder");
- binderField.setAccessible(true);
- Binder binder = (Binder) binderField.get(configurationPropertiesBinder);
- if (binder == null) {
- //如果binder为空,要先获取springboot定义的binder
- Method getBinder = configurationPropertiesBinder.getClass().getDeclaredMethod("getBinder");
- getBinder.setAccessible(true);
- binder = (Binder) getBinder.invoke(configurationPropertiesBinder);
- }
- //获取springboot原生binder的解析方法,解析${xxx}的
- Field placeholdersResolverField = binder.getClass().getDeclaredField("placeholdersResolver");
- placeholdersResolverField.setAccessible(true);
- PlaceholdersResolver springbootResolver = (PlaceholdersResolver) placeholdersResolverField.get(binder);
- //自定义的解析方法,解析decode(xxx)的
- PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
- PlaceholdersResolver myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
- //将这两个解析方法组合到一起,先执行springboot的解析,对解析的结果在进行自定义的解析
- MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
- //将新的解析器设置回binder中
- placeholdersResolverField.set(binder, mutablePlaceholdersResolver);
- } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-
- private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
- Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
- return new PropertyPlaceholderHelper("decode(", ")",
- PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
- }
-
- public String decodeValue(String strVal) {
- if (strVal != null && strVal.equals("abc")) {
- return "123";
- }
- return strVal;
- }
-
- @Override
- public void setEnvironment(@Nullable Environment environment) {
- this.environment = environment;
- }
- }
- public class MutablePlaceholdersResolver implements PlaceholdersResolver {
-
- private final PlaceholdersResolver[] placeholdersResolvers;
-
- public MutablePlaceholdersResolver(PlaceholdersResolver... placeholdersResolvers) {
- if (placeholdersResolvers == null) {
- throw new IllegalArgumentException("placeholdersResolvers is null");
- }
- this.placeholdersResolvers = placeholdersResolvers;
- }
-
- @Override
- public Object resolvePlaceholders(Object value) {
- for (PlaceholdersResolver placeholdersResolver : placeholdersResolvers) {
- value = placeholdersResolver.resolvePlaceholders(value);
- }
- return value;
- }
- }
可以看到只是对原有的binder对象里面的placeholdersResolver解析器进行了修改,而且之前的placeholdersResolver也没去掉,是组合到一起使用。里面的那个MutablePlaceholdersResolver是自定义的一个解析器,他的作用是使用多个解析器对一个属性进行解析,我将binder对象原本的解析器和我自定义的decode解析器一同放到MutablePlaceholdersResolver里面,用这个MutablePlaceholdersResolver替换了原本的解析器,解析的时候先通过原本的解析器(解析${xxx})进行解析,然后再用decode解析器对解析结果进行解析。
自定义一个BeanPostProcessor对ConfigurationPropertiesBindingPostProcessor已经绑定好的@ConfigurationProperties类进行二次解析,已知ConfigurationPropertiesBindingPostProcessor是实现的PriorityOrdered,如果想在他后面执行就比他优先级低可以,所以干脆就不实现order即可
- @Slf4j
- @Component
- public class EnhanceConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, EnvironmentAware, InitializingBean {
-
- private Environment environment;
-
- private PlaceholdersResolver myResolver;
-
- @Override
- public void afterPropertiesSet() throws Exception {
- PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
- myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
- }
-
- @Override
- public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
- ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
- if (annotation != null) {
- try {
- String beanJson = JSON.toJSONString(bean, SerializerFeature.IgnoreNonFieldGetter);
- JSONObject source = JSON.parseObject(beanJson);
-
- Map<String, Object> target = new LinkedHashMap<>();
- EnhanceUtils.buildFlattenedMap(target, source, annotation.prefix());
- List<ConfigurationPropertySource> mapPropertySources = Collections.singletonList(new MapConfigurationPropertySource(target));
-
- Binder binder = new Binder(mapPropertySources, myResolver);
- binder.bind(annotation.prefix(), Bindable.ofInstance(bean));
- } catch (Exception e) {
- log.error("EnhanceConfigurationPropertiesBindingPostProcessor bind fail,beanName:{}", beanName, e);
- }
- }
- return bean;
- }
-
- private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
- Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
- return new PropertyPlaceholderHelper("decode(", ")",
- PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
- }
-
- public String decodeValue(String strVal) {
- if (strVal != null && strVal.equals("abc")) {
- return "123";
- }
- return strVal;
- }
-
- @Override
- public void setEnvironment(Environment environment) {
- this.environment = environment;
- }
-
- }
可以看到这个Binder的sources我用的是这个bean的json转的,这样做的目的是,对这个bean的值再执行自定义的值解析,不重新从数据源里找值了,当然也可以用environment作为sources覆盖之前springboot赋的值,不过如果之前绑定的数据源没在environment里就没法自定义解析了,而且解析的方式还有加上springboot的那个解析方式,不然值是${}这样的属性就没办法解析了,如下修改上面的那个binder
Binder binder = new Binder(ConfigurationPropertySources.get(environment), myResolver);
可以看到${my.decode}并没有得到解析,所以还有修改Binder的第二个参数,也就是解析器要加上springboot的默认解析方式,参考3.1的MutablePlaceholdersResolver
- PlaceholdersResolver springbootResolver = new PropertySourcesPlaceholdersResolver(environment);
- MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
- Binder binder = new Binder(ConfigurationPropertySources.get(environment), mutablePlaceholdersResolver);
如果觉得一个个模块的设置比较麻烦,我在下一篇会写出一个框架,可以直接使用框架来自定义相关的数据源和解析