springboot配置注入增强(三)自定义数据源/自定义解析方法

我们回忆下上一篇文章的内容,属性注入的关键节点是PropertySourcesPlaceholderConfigurer的BeanFactory后置处理器org.springframework.context.support.PropertySourcesPlaceholderConfigurer#postProcessBeanFactory,只有在执行这个方法前设置到Environment的数据源,才能被应用到springboot启动中所有属性注入的阶段,这样才能优雅的将自己的代码加入扩展点中,否则还得自己手动处理@value注解,手动绑定@ConfigurationProperties等

一 自定义数据源

上一篇文章说过任何地方都能加入数据源,但是我们通常需要在application.yml中读取一些属性来确定怎么获取数据源,比如获取数据源的地址、账号、密码什么的,那就需要我们在加载application.yml后再进行我们数据源的加载,下面我就介绍几个常用的场景

1 自定义EnvironmentPostProcessor接口

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

2 自定义ApplicationContextInitializer接口

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

3 自定义BeanFactoryPostProcessor

BeanFactoryPostProcessor就是更靠后的一个阶段了,这种方式注入的时候不仅能获取到application.yml中的属性,还可以获取到分布式配置中心的属性。因为springboot第一次用到配置也就是Environment对象,是在PropertySourcesPlaceholderConfigurer中解析BeanDefinition的propertyValues(上一篇讲过),那么分布式也要在这之前配置进去,如

  • disconf就是自定义了一个PropertyPlaceholderConfigurer并在初始化这个bean的时候加载的配置
  • nacos则是用NacosConfigApplicationContextInitializer使用上述自定义ApplicationContextInitializer接口的方式加载的

而PropertySourcesPlaceholderConfigurer也是一个BeanFactoryPostProcessor,那我们只要在他之前加载即可,它使用了父类的PriorityOrdered设置的order

那我们只要比他优先一点就可以了,也继承PriorityOrdered,设置order为Ordered.LOWEST_PRECEDENCE-1

二 自定义属性解析

我们还是针对那四种获取属性的方式来执行自定义解析,BeanDefinition、@Value、@ConfigurationProperties、environment.getProperty。但environment.getProperty是在我们代码中调用的,我们完全可以自由控制对结果再执行自定义的解析方法,所以这个就没必要在讨论了,我们主要对另外三种springboot自动注入的属性(用户无感知)来做解析,比如我要写一个自定义解密的方法decode()

1 BeanDefinition

之前已经说过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方法,是从数据源集合里面调用获取属性对应的值。

2 @Value

对于@value,之前也说过,他也是在执行PropertySourcesPlaceholderConfigurer时,将StringValueResolver添加到beanFactory中,等后面解析的时候从这里面获取,也就是和上面的BeanDefinition解析用的同一个StringValueResolver。幸运的是springboot对于解析@value留了扩展点(因为beanFactory中保存了StringValueResolver的集合,解析@value的时候是从这个集合中遍历,用每一个解析器来对上一个解析的结果再做解析)

所以只要把我们自定义的那个StringValueResolver也加到beanFactory中即可,即对刚才的代码加上一行beanFactory.addEmbeddedValueResolver(valueResolver);

3 @ConfigurationProperties

@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的解析有以下几种方案

3.1 手动设置ConfigurationPropertiesBinder#binder(有侵入性,但是进行了一次绑定)

因为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解析器对解析结果进行解析。

3.2 自定义BeanPostProcessor(无侵入性,但是进行了两次绑定)

自定义一个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);

三 简化开发

如果觉得一个个模块的设置比较麻烦,我在下一篇会写出一个框架,可以直接使用框架来自定义相关的数据源和解析

猜你喜欢

转载自blog.csdn.net/cjc000/article/details/132946847