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


    对原理不感兴趣的可以直接使用框架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后再进行我们数据源的加载,下面我就介绍几个常用的场景

    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类执行

    1. public class EnhanceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    2. @Override
    3. public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    4. String userId = environment.getProperty("user.id");
    5. Map<String, Object> enhanceSource = new HashMap<>();
    6. enhanceSource.put("user." + userId + ".name", "EnhanceEnvironmentPostProcessor设置的名称");
    7. MutablePropertySources propertySources = environment.getPropertySources();
    8. MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
    9. propertySources.addFirst(enhancePropertySource);
    10. }
    11. @Override
    12. public int getOrder() {
    13. return Ordered.HIGHEST_PRECEDENCE + 11;
    14. }
    15. }

    2 自定义ApplicationContextInitializer接口

    springboot在SpringApplication构造器时会从spring.factories中获取所有EnvironmentPostProcessor的接口保存起来,并且在prepareContext阶段的applyInitializers()方法中以此去执行(也是根据order排好序的),因为这个阶段是在EnvironmentPostProcessor阶段后执行的,所以也可以获取到application.yml的属性

    1. public class EnhanceApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    2. @Override
    3. public void initialize(ConfigurableApplicationContext applicationContext) {
    4. ConfigurableEnvironment environment = applicationContext.getEnvironment();
    5. String userId = environment.getProperty("user.id");
    6. Map<String, Object> enhanceSource = new HashMap<>();
    7. enhanceSource.put("user." + userId + ".name", "EnhanceApplicationContextInitializer设置的名称");
    8. MutablePropertySources propertySources = environment.getPropertySources();
    9. MapPropertySource enhancePropertySource = new MapPropertySource("enhance", enhanceSource);
    10. propertySources.addFirst(enhancePropertySource);
    11. }
    12. }

    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

    1. @Override
    2. public void postProcessBeanFactory(@Nullable ConfigurableListableBeanFactory beanFactory) throws BeansException {
    3. PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
    4. StringValueResolver valueResolver = strVal -> helper.replacePlaceholders(strVal, this::decodeValue);
    5. BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
    6. String[] beanNames = beanFactory.getBeanDefinitionNames();
    7. for (String curName : beanNames) {
    8. if (!(curName.equals(this.beanName) && beanFactory.equals(this.beanFactory))) {
    9. BeanDefinition bd = beanFactory.getBeanDefinition(curName);
    10. try {
    11. visitor.visitBeanDefinition(bd);
    12. } catch (Exception ex) {
    13. throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
    14. }
    15. }
    16. }
    17. }
    18. private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
    19. Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
    20. return new PropertyPlaceholderHelper("decode(", ")",
    21. PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    22. }
    23. public String decodeValue(String strVal) {
    24. if (strVal != null && strVal.equals("abc")) {
    25. return "123";
    26. }
    27. return strVal;
    28. }

    可以看到,我用了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是私有的,所以要想设置上,只能用反射了

    1. public class BinderPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
    2. private Environment environment;
    3. @Override
    4. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    5. String beanName = "org.springframework.boot.context.internalConfigurationPropertiesBinder";
    6. Object configurationPropertiesBinder = beanFactory.getBean(beanName);
    7. try {
    8. //获取configurationPropertiesBinder的 binder 对象
    9. Field binderField = configurationPropertiesBinder.getClass().getDeclaredField("binder");
    10. binderField.setAccessible(true);
    11. Binder binder = (Binder) binderField.get(configurationPropertiesBinder);
    12. if (binder == null) {
    13. //如果binder为空,要先获取springboot定义的binder
    14. Method getBinder = configurationPropertiesBinder.getClass().getDeclaredMethod("getBinder");
    15. getBinder.setAccessible(true);
    16. binder = (Binder) getBinder.invoke(configurationPropertiesBinder);
    17. }
    18. //获取springboot原生binder的解析方法,解析${xxx}的
    19. Field placeholdersResolverField = binder.getClass().getDeclaredField("placeholdersResolver");
    20. placeholdersResolverField.setAccessible(true);
    21. PlaceholdersResolver springbootResolver = (PlaceholdersResolver) placeholdersResolverField.get(binder);
    22. //自定义的解析方法,解析decode(xxx)的
    23. PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
    24. PlaceholdersResolver myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
    25. //将这两个解析方法组合到一起,先执行springboot的解析,对解析的结果在进行自定义的解析
    26. MutablePlaceholdersResolver mutablePlaceholdersResolver = new MutablePlaceholdersResolver(springbootResolver, myResolver);
    27. //将新的解析器设置回binder中
    28. placeholdersResolverField.set(binder, mutablePlaceholdersResolver);
    29. } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
    30. e.printStackTrace();
    31. }
    32. }
    33. private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
    34. Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
    35. return new PropertyPlaceholderHelper("decode(", ")",
    36. PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    37. }
    38. public String decodeValue(String strVal) {
    39. if (strVal != null && strVal.equals("abc")) {
    40. return "123";
    41. }
    42. return strVal;
    43. }
    44. @Override
    45. public void setEnvironment(@Nullable Environment environment) {
    46. this.environment = environment;
    47. }
    48. }
    1. public class MutablePlaceholdersResolver implements PlaceholdersResolver {
    2. private final PlaceholdersResolver[] placeholdersResolvers;
    3. public MutablePlaceholdersResolver(PlaceholdersResolver... placeholdersResolvers) {
    4. if (placeholdersResolvers == null) {
    5. throw new IllegalArgumentException("placeholdersResolvers is null");
    6. }
    7. this.placeholdersResolvers = placeholdersResolvers;
    8. }
    9. @Override
    10. public Object resolvePlaceholders(Object value) {
    11. for (PlaceholdersResolver placeholdersResolver : placeholdersResolvers) {
    12. value = placeholdersResolver.resolvePlaceholders(value);
    13. }
    14. return value;
    15. }
    16. }

    可以看到只是对原有的binder对象里面的placeholdersResolver解析器进行了修改,而且之前的placeholdersResolver也没去掉,是组合到一起使用。里面的那个MutablePlaceholdersResolver是自定义的一个解析器,他的作用是使用多个解析器对一个属性进行解析,我将binder对象原本的解析器和我自定义的decode解析器一同放到MutablePlaceholdersResolver里面,用这个MutablePlaceholdersResolver替换了原本的解析器,解析的时候先通过原本的解析器(解析${xxx})进行解析,然后再用decode解析器对解析结果进行解析。

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

    自定义一个BeanPostProcessor对ConfigurationPropertiesBindingPostProcessor已经绑定好的@ConfigurationProperties类进行二次解析,已知ConfigurationPropertiesBindingPostProcessor是实现的PriorityOrdered,如果想在他后面执行就比他优先级低可以,所以干脆就不实现order即可

    1. @Slf4j
    2. @Component
    3. public class EnhanceConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, EnvironmentAware, InitializingBean {
    4. private Environment environment;
    5. private PlaceholdersResolver myResolver;
    6. @Override
    7. public void afterPropertiesSet() throws Exception {
    8. PropertyPlaceholderHelper helper = getPropertyPlaceholderHelper(environment);
    9. myResolver = val -> helper.replacePlaceholders(String.valueOf(val), this::decodeValue);
    10. }
    11. @Override
    12. public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    13. ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
    14. if (annotation != null) {
    15. try {
    16. String beanJson = JSON.toJSONString(bean, SerializerFeature.IgnoreNonFieldGetter);
    17. JSONObject source = JSON.parseObject(beanJson);
    18. Map<String, Object> target = new LinkedHashMap<>();
    19. EnhanceUtils.buildFlattenedMap(target, source, annotation.prefix());
    20. List<ConfigurationPropertySource> mapPropertySources = Collections.singletonList(new MapConfigurationPropertySource(target));
    21. Binder binder = new Binder(mapPropertySources, myResolver);
    22. binder.bind(annotation.prefix(), Bindable.ofInstance(bean));
    23. } catch (Exception e) {
    24. log.error("EnhanceConfigurationPropertiesBindingPostProcessor bind fail,beanName:{}", beanName, e);
    25. }
    26. }
    27. return bean;
    28. }
    29. private PropertyPlaceholderHelper getPropertyPlaceholderHelper(Environment environment) {
    30. Boolean ignore = environment.getProperty("enhance.ignore.unresolvable", Boolean.class);
    31. return new PropertyPlaceholderHelper("decode(", ")",
    32. PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR, Optional.ofNullable(ignore).filter(Boolean.TRUE::equals).orElse(false));
    33. }
    34. public String decodeValue(String strVal) {
    35. if (strVal != null && strVal.equals("abc")) {
    36. return "123";
    37. }
    38. return strVal;
    39. }
    40. @Override
    41. public void setEnvironment(Environment environment) {
    42. this.environment = environment;
    43. }
    44. }

    可以看到这个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

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

    三 简化开发

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

  • 相关阅读:
    关于编辑器QScintilla(Scintilla)词法分析器取消非活动代码灰色显示
    Comate SaaS版:开发者的梦想工具终于来了
    SAP MM学习笔记28- 供给元(供货源)决定
    kubernetes网络模型
    无法上网问题解决过程
    项目实战:中央控制器实现(4)-实现RequestBody注解的功能-获取请求体参数
    芯片胶点胶加工的效果和质量的检测方法有哪些?
    Invalid bound statement (not found)出现的原因和解决方法
    【前端基础知识】TS 类型、接口interface、泛型、Type
    Kafka 分区
  • 原文地址:https://blog.csdn.net/cjc000/article/details/132946847