乐趣区

关于java:Spring-Cloud-中自定义外部化扩展机制原理及实战

Spring Cloud 针对 Environment 的属性源性能做了加强,

在 spring-cloud-contenxt 这个包中,提供了 PropertySourceLocator 接口,用来实现属性文件加载的扩大。咱们能够通过这个接口来扩大本人的内部化配置加载。这个接口的定义如下

public interface PropertySourceLocator {

   /**
    * @param environment The current Environment.
    * @return A PropertySource, or null if there is none.
    * @throws IllegalStateException if there is a fail-fast condition.
    */
   PropertySource<?> locate(Environment environment);
}

locate 这个形象办法,须要返回一个 PropertySource 对象。

这个 PropertySource 就是 Environment 中存储的属性源。也就是说,咱们如果要实现自定义内部化配置加载,只须要实现这个接口并返回 PropertySource 即可。

依照这个思路,咱们依照上面几个步骤来实现内部化配置的自定义加载。

自定义 PropertySource

既然 PropertySourceLocator 须要返回一个 PropertySource,那咱们必须要定义一个本人的 PropertySource,来从内部获取配置起源。

GpDefineMapPropertySource 示意一个以 Map 后果作为属性起源的类。

/**
 * 咕泡教育,ToBeBetterMan
 * Mic 老师微信: mic4096
 * 微信公众号:跟着 Mic 学架构
 * https://ke.gupaoedu.cn
 * 应用 Map 作为属性起源。**/
public class GpDefineMapPropertySource extends MapPropertySource {
    /**
     * Create a new {@code MapPropertySource} with the given name and {@code Map}.
     *
     * @param name   the associated name
     * @param source the Map source (without {@code null} values in order to get
     *               consistent {@link #getProperty} and {@link #containsProperty} behavior)
     */
    public GpDefineMapPropertySource(String name, Map<String, Object> source) {super(name, source);
    }

    @Override
    public Object getProperty(String name) {return super.getProperty(name);
    }

    @Override
    public String[] getPropertyNames() {return super.getPropertyNames();
    }
}

扩大 PropertySourceLocator

扩大 PropertySourceLocator,重写 locate 提供属性源。

而属性源是从 gupao.json 文件加载保留到自定义属性源 GpDefineMapPropertySource 中。

public class GpJsonPropertySourceLocator implements PropertySourceLocator {

    //json 数据起源
    private final static String DEFAULT_LOCATION="classpath:gupao.json";
    // 资源加载器
    private final ResourceLoader resourceLoader=new DefaultResourceLoader(getClass().getClassLoader());

    @Override
    public PropertySource<?> locate(Environment environment) {
        // 设置属性起源
        GpDefineMapPropertySource jsonPropertySource=new GpDefineMapPropertySource
                ("gpJsonConfig",mapPropertySource());
        return jsonPropertySource;
    }

    private Map<String,Object> mapPropertySource(){Resource resource=this.resourceLoader.getResource(DEFAULT_LOCATION);
        if(resource==null){return null;}
        Map<String,Object> result=new HashMap<>();
        JsonParser parser= JsonParserFactory.getJsonParser();
        Map<String,Object> fileMap=parser.parseMap(readFile(resource));
        processNestMap("",result,fileMap);
        return result;
    }
    // 加载文件并解析
    private String readFile(Resource resource){
        FileInputStream fileInputStream=null;
        try {fileInputStream=new FileInputStream(resource.getFile());
            byte[] readByte=new byte[(int)resource.getFile().length()];
            fileInputStream.read(readByte);
            return new String(readByte,"UTF-8");
        } catch (IOException e) {e.printStackTrace();
        }finally {if(fileInputStream!=null){
                try {fileInputStream.close();
                } catch (IOException e) {e.printStackTrace();
                }
            }
        }
        return null;
    }
    // 解析残缺的 url 保留到 result 汇合。为了实现 @Value 注入
    private void processNestMap(String prefix,Map<String,Object> result,Map<String,Object> fileMap){if(prefix.length()>0){prefix+=".";}
        for (Map.Entry<String, Object> entrySet : fileMap.entrySet()) {if (entrySet.getValue() instanceof Map) {processNestMap(prefix + entrySet.getKey(), result, (Map<String, Object>) entrySet.getValue());
            } else {result.put(prefix + entrySet.getKey(), entrySet.getValue());
            }
        }
    }
}

Spring.factories

/META-INF/spring.factories 文件中,增加上面的 spi 扩大,让 Spring Cloud 启动时扫描到这个扩大从而实现 GpJsonPropertySourceLocator 的加载。

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.gupaoedu.env.GpJsonPropertySourceLocator

编写 controller 测试

@RestController
public class ConfigController {@Value("${custom.property.message}")
    private String name;
    
    @GetMapping("/")
    public String get(){String msg=String.format("配置值:%s",name);
        return msg;
    }
}

阶段性总结

通过上述案例能够发现,基于 Spring Boot 提供的 PropertySourceLocator 扩大机制,能够轻松实现自定义配置源的扩大。

于是,引出了两个问题。

  1. PropertySourceLocator 是在哪个被触发的?
  2. 既然可能从 gupao.json 中加载数据源,是否能从近程服务器上加载呢?

PropertySourceLocator 加载原理

先来摸索第一个问题,PropertySourceLocator 的执行流程。

SpringApplication.run

在 spring boot 我的项目启动时,有一个 prepareContext 的办法,它会回调所有实现了 ApplicationContextInitializer 的实例,来做一些初始化工作。

ApplicationContextInitializer 是 Spring 框架原有的货色,它的次要作用就是在,ConfigurableApplicationContext 类型 (或者子类型) 的 ApplicationContext 做 refresh 之前,容许咱们对 ConfiurableApplicationContext 的实例做进一步的设置和解决。

它能够用在须要对应用程序上下文进行编程初始化的 web 应用程序中,比方依据上下文环境来注册 propertySource,或者配置文件。而 Config 的这个配置核心的需要恰好须要这样一个机制来实现。

public ConfigurableApplicationContext run(String... args) {
   // 省略代码...
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
   // 省略代码
    return context;
}

PropertySourceBootstrapConfiguration.initialize

其中,PropertySourceBootstrapConfiguration 就实现了 ApplicationContextInitializerinitialize 办法代码如下。

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {List<PropertySource<?>> composite = new ArrayList<>();
    // 对 propertySourceLocators 数组进行排序,依据默认的 AnnotationAwareOrderComparator
    AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
    boolean empty = true;
    // 获取运行的环境上下文
    ConfigurableEnvironment environment = applicationContext.getEnvironment();
    for (PropertySourceLocator locator : this.propertySourceLocators) {
        // 回调所有实现 PropertySourceLocator 接口实例的 locate 办法,并收集到 source 这个汇合中。Collection<PropertySource<?>> source = locator.locateCollection(environment);
        if (source == null || source.size() == 0) { // 如果 source 为空,间接进入下一次循环
            continue;
        }
        // 遍历 source,把 PropertySource 包装成 BootstrapPropertySource 退出到 sourceList 中。List<PropertySource<?>> sourceList = new ArrayList<>();
        for (PropertySource<?> p : source) {sourceList.add(new BootstrapPropertySource<>(p));
        }
        logger.info("Located property source:" + sourceList);
        composite.addAll(sourceList);// 将 source 增加到数组
        empty = false; // 示意 propertysource 不为空
    }
     // 只有 propertysource 不为空的状况,才会设置到 environment 中
    if (!empty) {
        // 获取以后 Environment 中的所有 PropertySources.
        MutablePropertySources propertySources = environment.getPropertySources();
        String logConfig = environment.resolvePlaceholders("${logging.config:}");
        LogFile logFile = LogFile.get(environment);
       // 遍历移除 bootstrapProperty 的相干属性
        for (PropertySource<?> p : environment.getPropertySources()) {if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {propertySources.remove(p.getName());
            }
        }
        // 把后面获取到的 PropertySource,插入到 Environment 中的 PropertySources 中。insertPropertySources(propertySources, composite);
        reinitializeLoggingSystem(environment, logConfig, logFile);
        setLogLevels(applicationContext, environment);
        handleIncludedProfiles(environment);
    }
}

上述代码逻辑阐明如下。

  1. 首先 this.propertySourceLocators,示意所有实现了PropertySourceLocators 接口的实现类,其中就包含咱们后面自定义的GpJsonPropertySourceLocator
  2. 依据默认的 AnnotationAwareOrderComparator 排序规定对 propertySourceLocators 数组进行排序。
  3. 获取运行的环境上下文 ConfigurableEnvironment
  4. 遍历 propertySourceLocators 时
  • 调用 locate 办法,传入获取的上下文 environment
  • 将 source 增加到 PropertySource 的链表中
  • 设置 source 是否为空的标识标量 empty
  1. source 不为空的状况,才会设置到 environment 中
  • 返回 Environment 的可变模式,可进行的操作如 addFirst、addLast
  • 移除 propertySources 中的 bootstrapProperties
  • 依据 config server 覆写的规定,设置 propertySources
  • 解决多个 active profiles 的配置信息

留神:this.propertySourceLocators 这个汇合中的 PropertySourceLocator,是通过主动拆卸机制实现注入的,具体的实现在 BootstrapImportSelector 这个类中。

ApplicationContextInitializer 的了解和应用

ApplicationContextInitializer 是 Spring 框架原有的货色,它的次要作用就是在,ConfigurableApplicationContext 类型 (或者子类型) 的 ApplicationContext 做 refresh 之前,容许咱们对 ConfiurableApplicationContext 的实例做进一步的设置和解决。

它能够用在须要对应用程序上下文进行编程初始化的 web 应用程序中,比方依据上下文环境来注册 propertySource,或者配置文件。而 Config 的这个配置核心的需要恰好须要这样一个机制来实现。

创立一个 TestApplicationContextInitializer

public class TestApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {ConfigurableEnvironment ce=applicationContext.getEnvironment();
        for(PropertySource<?> propertySource:ce.getPropertySources()){System.out.println(propertySource);
        }
        System.out.println("--------end");
    }
}

增加 spi 加载

创立一个文件 /resources/META-INF/spring.factories。增加如下内容

org.springframework.context.ApplicationContextInitializer= \
  com.gupaoedu.example.springcloudconfigserver9091.TestApplicationContextInitializer

在控制台就能够看到以后的 PropertySource 的输入后果。

ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
StubPropertySource {name='servletConfigInitParams'}
StubPropertySource {name='servletContextInitParams'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
MapPropertySource {name='configServerClient'}
MapPropertySource {name='springCloudClientHostInfo'}
OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.yml]'}
MapPropertySource {name='kafkaBinderDefaultProperties'}
MapPropertySource {name='defaultProperties'}
MapPropertySource {name='springCloudDefaultProperties'}

版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自 Mic 带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!

退出移动版