Original: Taste of Little Sister (wechat official ID: XjjDog), welcome to share, please reserve the source.

Although SpringBoot now supports multiple environments, changes to configuration files often require repackaging.

While developing the SpringBoot framework integration, I had a problem getting @propertysource to “scan” and load properties files outside the JAR package.

This way, I can change the configuration file anytime, anywhere, without having to repackage it.

The crudest way to specify these files is to use –classpath. But this introduces other issues that are “easy to deploy,” “container independent,” and can be tricky. This problem can be tricky to solve in test environments, multi-room deployments, and collaboration with configuration centers, because of the hard rules and even communication costs involved.

Getting back to the basics of the technology, I wanted to develop a compatibility suite based on the Spring container that could scan properties files outside of the JAR. For ease of implementation, we agreed that these properties files would always be located in the directory next to the JAR file.

Design the premise

1. Files and directories

File directories look something like this. You can see that the configuration file is parallel to the JAR package.

-- application. Jar (springboot project, jarLaucher) | | sample. The properties | config / | | sample. The propertiesCopy the code

2. Scan strategy (involving coverage priority)

1) We agree that the default configuration file directory is config, which is the first. The rest of the application.jar class is the same; The relative path starts with the JAR path.

2) the first lookup. / config/sample. The properties file exists, if there is a load.

3) Check whether the./sample.properties file exists and load it if it does.

4) Otherwise, use classpath to load the file.

3. Development strategy

1) Use the Spring mechanism (Resource loading) whenever possible, not local files or deployment script interventions.

2) Through research, the extension of customized ResourceLoader can achieve this goal, but the potential risk is high, because springboot and Cloud frameworks support various contexts with their own ResourceLoader implementation. Will there be some unknown problems if we extend our loader again? This strategy was abandoned.

3) Spring provides a ProtocolResolver mechanism that matches a custom file schema to load files; It does not interfere with ResourceLoader and most importantly it is added to all loaders in the Spring environment. We simply extend a ProtocolResolver class and add it to the ResourceLoader as appropriate, and our ProtocolResolver is always executed when loading the Properties file.

code

The following is the specific code implementation. The most important is the writing of the configuration file parser. The notes are very detailed, so I won’t go into that.

1, XPathProtocolResolver. Java

import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ResourceUtils; import java.util.Collection; import java.util.LinkedHashSet; /** * To load the properties file outside the JAR, extend the classpath: xjjdog * -- app.jar * -- config/a.property INSIDE order=3 * -- a.property INSIDE order=4 * -- config/a.property OUTSIDE Order =1 * -- a.property OUTSIDE order=2 * <p> *"::a.property") * search path is:. / config/Amy polumbo roperty,. / Amy polumbo roperty, if can't find it returns null, path relative to the app. Jar * 2, @ PropertySource ("::x/a.property") * search path is:. / config/x/Amy polumbo roperty,. / x/Amy polumbo roperty, path relative to the app. Jar * 3, @ PropertySource ("*:a.property") * search path is:. / config/Amy polumbo roperty,. / Amy polumbo roperty, CLASSPATH: / config/Amy polumbo roperty, CLASSPATH: / Amy polumbo roperty * 4, @ PropertySource ("*:x/a.property") * Search path is: ./config/x/a.property,./x/a.property,CLASSPATH:/config/x/a.property,CLASSPATH:/x/a.property * <p> * If customConfigPath is specified, ** @author xjjdog **/ public class XPathProtocolResolver implements ProtocolResolver {/** * Find OUTSIDE's configuration path. If not found, return NULL */ private static final String X_PATH_OUTSIDE_PREFIX ="... ""; /** * find OUTSIDE and inside, where inside will convert to CLASS_PATH */ private static final String X_PATH_GLOBAL_PREFIX ="*:";  
  
    private String customConfigPath;  
  
    public XPathProtocolResolver(String configPath) {  
        this.customConfigPath = configPath;  
    }  
  
    @Override  
    public Resource resolve(String location, ResourceLoader resourceLoader) {  
        if(! location.startsWith(X_PATH_OUTSIDE_PREFIX) && ! location.startsWith(X_PATH_GLOBAL_PREFIX)) {return null;  
        }  
  
        String real = path(location);  
  
        Collection<String> fileLocations = searchLocationsForFile(real);  
        for (String path : fileLocations) {  
            Resource resource = resourceLoader.getResource(path);  
            if(resource ! = null && resource.exists()) {return resource;  
            }  
        }  
        boolean global = location.startsWith(X_PATH_GLOBAL_PREFIX);  
        if(! global) {return null;  
        }  
  
        Collection<String> classpathLocations = searchLocationsForClasspath(real);  
        for (String path : classpathLocations) {  
            Resource resource = resourceLoader.getResource(path);  
            if(resource ! = null && resource.exists()) {returnresource; }}return resourceLoader.getResource(real);  
    }  
  
    private Collection<String> searchLocationsForFile(String location) {  
        Collection<String> locations = new LinkedHashSet<>();  
        String _location = shaping(location);  
        if(customConfigPath ! = null) { String prefix = ResourceUtils.FILE_URL_PREFIX + customConfigPath;if(! customConfigPath.endsWith("/")) {  
                locations.add(prefix + "/" + _location);  
            } else{ locations.add(prefix + _location); }}else {  
            locations.add(ResourceUtils.FILE_URL_PREFIX + "./config/" + _location);  
        }  
        locations.add(ResourceUtils.FILE_URL_PREFIX + ". /" + _location);  
        return locations;  
    }  
  
    private Collection<String> searchLocationsForClasspath(String location) {  
        Collection<String> locations = new LinkedHashSet<>();  
        String _location = shaping(location);  
        if(customConfigPath ! = null) { String prefix = ResourceUtils.CLASSPATH_URL_PREFIX + customConfigPath;if(! customConfigPath.endsWith("/")) {  
                locations.add(prefix + "/" + _location);  
            } else{ locations.add(prefix + _location); }}else {  
            locations.add(ResourceUtils.CLASSPATH_URL_PREFIX + "/config/" + _location);  
        }  
  
        locations.add(ResourceUtils.CLASSPATH_URL_PREFIX + "/" + _location);  
        return locations;  
    }  
  
    private String shaping(String location) {  
        if (location.startsWith(". /")) {  
            return location.substring(2);  
        }  
        if (location.startsWith("/")) {  
            return location.substring(1);  
        }  
        return location;  
    }  
  
    /** 
     * remove protocol 
     * 
     * @param location 
     * @return 
     */  
    private String path(String location) {  
        returnlocation.substring(2); }}Copy the code

2, ResourceLoaderPostProcessor. Java

import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; /** * @author xjjdog * Adjust and optimize environment variables. The Boot framework overwrites some environment variables by default, so we need to execute in the processor * we don't need to use a separate YML file to solve this problem. Principle: * 1) All set as system attributes, the original intention is"Visible to system administrators","Visible to external access components"(starter, log component, etc.) * 2"When the user does not pass YML"Default value when configuring options - guarantee policy. **/ public class ResourceLoaderPostProcessor implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered { @Override public void initialize(ConfigurableApplicationContext applicationContext) { Environment environment = applicationContext.getEnvironment(); String configPath = environment.getProperty("CONF_PATH");  
        if (configPath == null) {  
            configPath = environment.getProperty("config.path");  
        }  
        applicationContext.addProtocolResolver(new XPathProtocolResolver(configPath));  
    }  
  
    @Override  
    public int getOrder() {  
        returnHIGHEST_PRECEDENCE + 100; }}Copy the code

With spring.Factories, we’re becoming more and more of a starter. Yes, it is.

3, spring. Factories

org.springframework.context.ApplicationContextInitializer=\  
com.github.xjjdog.commons.spring.io.ResourceLoaderPostProcessor  
Copy the code

PropertyConfiguration. Java (springboot environment, the properties loader)

@Configuration  
@PropertySources(  
    {  
            @PropertySource("*:login.properties"),  
            @PropertySource("*:ldap.properties")  
    }  
)  
public class PropertyConfiguration {  
   
    @Bean  
    @ConfigurationProperties(prefix = "login")  
    public LoginProperties loginProperties() {  
        return new LoginProperties();  
    }  
   
    @Bean  
    @ConfigurationProperties(prefix = "ldap")  
    public LdapProperties ldapProperties() {  
        returnnew LdapProperties(); }}Copy the code

This completes our custom loader. We’ve also added new functionality to the SpringBoot component.

End

SpringBoot can specify different environments by setting “spring.profiles. Active “, but requirements are always variable. The configuration requirements in this article, for example, may be a corporate pain in the butt.

SpringBoot provides a variety of extensions to support these custom operations, which is part of the appeal. There’s nothing that developing a Spring Boot starter can’t solve.

Xjjdog is a public account that doesn’t allow programmers to get sidetracked. Focus on infrastructure and Linux. Ten years architecture, ten billion daily flow, and you discuss the world of high concurrency, give you a different taste. My personal wechat xjjdog0, welcome to add friends, further communication.