springboot configuration injection enhancement (3) Custom data source/custom parsing method

Let's recall the content of the previous article. The key node for attribute injection is the BeanFactory post-processor org.springframework.context.support.PropertySourcesPlaceholderConfigurer#postProcessBeanFactory of PropertySourcesPlaceholderConfigurer. Only the data source set to Environment before executing this method can be It is applied to all property injection stages in springboot startup, so that you can elegantly add your own code to the extension point. Otherwise, you have to manually handle @value annotations, manually bind @ConfigurationProperties, etc.

A custom data source

The previous article said that data sources can be added anywhere, but we usually need to read some properties in application.yml to determine how to obtain the data source, such as obtaining the address, account, password, etc. of the data source, then we need We load our data source after loading application.yml. Below I will introduce a few common scenarios.

1 Customize EnvironmentPostProcessor interface

application.yml is loaded using this interface. It is loaded in ConfigDataEnvironmentPostProcessor#postProcessEnvironment, and the call of this method is the ApplicationEnvironmentPreparedEvent message received after the EnvironmentPostProcessorApplicationListener creates the Environment object when springboot starts, and spring.factories is executed. All EnvironmentPostProcessor interfaces defined in , you can see that some default data sources (random, etc.) in Environment are also implemented using this interface.

So we can also customize an EnvironmentPostProcessor interface to add it to the springboot startup process.

When EnvironmentPostProcessorApplicationListener executes the EnvironmentPostProcessor interface, it is first sorted and executed in order. If we want to execute it after application.yml, then we only need to execute it after ConfigDataEnvironmentPostProcessor. The order of ConfigDataEnvironmentPostProcessor is Ordered.HIGHEST_PRECEDENCE + 10 (this + 10 is the extension point left to us by the springboot framework as an extensible framework. We can determine whether to execute our code before or after it at +9 or +11)

Then we only need to inherit Ordered or use the @Order annotation to set the order to Ordered.HIGHEST_PRECEDENCE + 11. PriorityOrdered cannot be used because the PriorityOrdered class will be executed prior to the order class.

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

2 Customize the ApplicationContextInitializer interface

Springboot will obtain all EnvironmentPostProcessor interfaces from spring.factories during the SpringApplication constructor, save them, and execute them in the applyInitializers() method of the prepareContext stage (also sorted according to order), because this stage is in EnvironmentPostProcessor It is executed after the stage, so you can also get the properties of 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);
    }
}

3 Customize BeanFactoryPostProcessor

BeanFactoryPostProcessor is a later stage. When injected in this way, not only the properties in application.yml can be obtained, but also the properties of the distributed configuration center can be obtained. Because the first time springboot uses configuration, which is the Environment object, it parses the propertyValues ​​of BeanDefinition in PropertySourcesPlaceholderConfigurer (discussed in the previous article), then the distribution must also be configured before this, such as

  • disconf is a configuration that customizes a PropertyPlaceholderConfigurer and loads it when initializing the bean.
  • nacos is loaded using NacosConfigApplicationContextInitializer using the above-mentioned custom ApplicationContextInitializer interface.

The PropertySourcesPlaceholderConfigurer is also a BeanFactoryPostProcessor, so we only need to load it before it. It uses the order set by the PriorityOrdered of the parent class.

Then we just need to give him a little priority, inherit PriorityOrdered, and set order to Ordered.LOWEST_PRECEDENCE-1

2. Custom attribute analysis

We still perform custom parsing for the four ways to obtain properties, BeanDefinition, @Value, @ConfigurationProperties, and environment.getProperty. But environment.getProperty is called in our code. We are completely free to control the results and then execute custom parsing methods, so there is no need to discuss this. We mainly focus on the other three properties automatically injected by springboot (users have no need to Perception) to do the analysis, for example, I want to write a custom decryption method decode()

1 BeanDefinition

As mentioned before, BeanDefinition parses bean properties in org.springframework.beans.factory.config.PlaceholderConfigurerSupport#doProcessProperties

You can see that he uses valueResolver for parsing, and springboot does not leave any extension points here, so we need to write a StringValueResolver ourselves and use this parser to re-parse the BeanDefinition, and we have to execute it after the PropertySourcesPlaceholderConfigurer, so We can parse the attributes parsed by springboot again, such as ${user.123.name}. Springboot will first parse decode(abc), and then we will parse decode(abc) into 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;
    }

As you can see, I used PropertyPlaceholderHelper to help parse properties, and set the prefix "decode(" and suffix ")". The replacePlaceholders method has two parameters, one is the value to be parsed, and the second is the PlaceholderResolver object. When called The replacePlaceholders method will first remove all decode(xxx) substrings in the string, remove the prefixes and suffixes recursively, and then call the resolvePlaceholder method of the PlaceholderResolver object (our custom one) to parse, and the substring after parsing is completed If there is a substring of decode(xxx), the above steps will be performed recursively until there is no prefix. Springboot's default ${xxx} type of resolution is also handled here, but the resolvePlaceholder method of its PlaceholderResolver object is called from the data source collection to obtain the value corresponding to the attribute.

2 @Value

As for @value, as I said before, he also added the StringValueResolver to the beanFactory when executing the PropertySourcesPlaceholderConfigurer, and obtained it from it when parsing later, which is the same StringValueResolver used in the above BeanDefinition parsing. Fortunately, springboot has extension points for parsing @value (because the beanFactory stores a collection of StringValueResolvers, and when parsing @value, it is traversed from this collection, and each parser is used to parse the result of the previous parsing)

So just add our customized StringValueResolver to the beanFactory, that is, add a line of beanFactory.addEmbeddedValueResolver(valueResolver); to the code just now.

3 @ConfigurationProperties

@ConfigurationProperties is bound by org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization. As mentioned before, it essentially uses the binder member variable of the ConfigurationPropertiesBinder class, and this binder is also a hard-coded generation method, without giving Extension points. So if we implement custom parsing of @ConfigurationProperties, we can only customize the binder, and then use binder to parse the @ConfigurationProperties class, or use reflection to manually assign values ​​to the binder variables of ConfigurationPropertiesBinder.

There are data sources and parsing methods in Binder. When calling its bind method, you only need to pass in the prefix of the attribute (prefix = "user.123") and the object to be bound, and you can perform attribute binding on the target object.

ConfigurationPropertiesBindingPostProcessor obtains the ConfigurationPropertiesBinder object from the bean factory in the InitializingBean phase.

And when registering this bean, it will be judged whether the bean already exists. If so, it will not be created and the existing bean will be used directly.

So there are several options for parsing this @ConfigurationProperties:

3.1 Manually set ConfigurationPropertiesBinder#binder (intrusive, but it is bound once)

Because the scope of ConfigurationPropertiesBinder is friendly, it can only be accessed in the same package. We can only use it through reflection or create an org.springframework.boot.context.properties package ourselves, and then write our replacement logic in it, ConfigurationPropertiesBinder. The binder in is private, so if you want to set it up, you can only use reflection.

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

It can be seen that only the placeholdersResolver resolver in the original binder object has been modified, and the previous placeholdersResolver has not been removed, but is combined and used together. The MutablePlaceholdersResolver inside is a custom parser. Its function is to use multiple parsers to parse an attribute. I put the original parser of the binder object and my custom decode parser into the MutablePlaceholdersResolver, using This MutablePlaceholdersResolver replaces the original parser. When parsing, it is first parsed through the original parser (parsing ${xxx}), and then the decode parser is used to parse the parsing results.

3.2 Custom BeanPostProcessor (not intrusive, but bound twice)

Customize a BeanPostProcessor to perform secondary analysis on the @ConfigurationProperties class that has been bound to ConfigurationPropertiesBindingPostProcessor. It is known that ConfigurationPropertiesBindingPostProcessor is implemented as PriorityOrdered. If you want to execute it after it, it will have a lower priority than it, so you can simply not implement 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;
    }

}

You can see that I used the JSON of this bean to convert the sources of this Binder. The purpose of this is to perform custom value parsing on the value of this bean without re-finding the value from the data source. Of course, this is also possible. Use environment as sources to overwrite the value previously assigned by springboot. However, if the previously bound data source is not in the environment, you cannot customize the parsing, and the parsing method also includes the parsing method of springboot, otherwise the value is ${ }Such an attribute cannot be parsed. Modify the binder above as follows

Binder binder = new Binder(ConfigurationPropertySources.get(environment), myResolver);

It can be seen that ${my.decode} has not been parsed, so the second parameter of Binder needs to be modified, that is, the parser needs to add the default parsing method of springboot. Refer to MutablePlaceholdersResolver in 3.1

PlaceholdersResolver springbootResolver = new PropertySourcesPlaceholdersResolver(environment);
MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
Binder binder = new Binder(ConfigurationPropertySources.get(environment), mutablePlaceholdersResolver);

Three simplifies development

If you find it troublesome to set up each module, I will write a framework in the next article. You can use the framework directly to customize related data sources and parsing.

Guess you like

Origin blog.csdn.net/cjc000/article/details/132946847