External configuration and usage records of SpringBoot

Keywords: Spring SpringBoot Java JDBC

Statement: if there are any mistakes, please point out!

Make a note of some of the problems encountered when using spring boot configuration, although this problem is caused by lack of knowledge.

Make a note to avoid forgetting again after a period of time, just in case.

If you can help the comrades who meet the same problems in your blog, it's excellent!

The external configuration of SpringBoot mainly refers to some configuration files that are usually used. Because these configurations are not hard coded, they are placed in configuration files, so they are relatively external configurations

The online document of the official external configuration of SpringBoot Externalized Configuration

Primary usage

Spring boot provides great convenience for Configuration. Just write a Yaml file or Properties file, write our Configuration information according to its specified format, and then write a corresponding Java class. Use the annotation @ ConfigurationProperties and @ Configuration, or @ Configuration and @ Value to map the Configuration Value to We configure classes or JavaBean s.

There are the following Java configuration classes

@Configuration
@ConfigurationProperties(prefix="spring.server")
public class AppConfig{
  private String name;
  private String port;
  public void setName(String name){
    this.name = name;
  }
  public void setPort(String port){
    this.port = port;
  }
}

The following configuration file,

Yaml format profile

# application.yml
spring:
    server:
        name: spring-app
        port: 9527

Properties format profile

# application.properties
spring.server.name=spring-app
spring.server.port=9237

When using @ ConfigurationProperties, there must be Setter method, which is used to bind

See org.springframework.boot.context.properties.configurationpropertiesbindingpostprocessor? Postprocessbeforeinitialization, a BeanPostProcessor

In this way, in spring boot, we can inject the AppConfig Bean into other beans to use our configuration.

The above basically meets the needs in development. Most of our configurations are simple, usually numeric and string.

But everything can't be absolute.

Advanced Usage

The following configuration references This is

Array/List

If there are the following requirements, the application only has open access to a few limited IPS, and then we want to write these licensed IP addresses in the configuration file.

At this time, if configuration parsing only supports strings and numeric types, we need to obtain the configuration values ourselves, and then carry out some subsequent processing, such as conversion to arrays or lists.

Fortunately, an excellent framework can always meet most of the requirements. SpringBoot is directly configured to map arrays or lists to

How to use

Java configuration class

@Configuration
@ConfigurationProperties(prefix="allows")
public class AllowedAccessConfig{
  private String[] ipList; // The field type can be list < string >
  public void setPort(String[] port){
    this.ipList = ipList;
  }
}

configuration file

# application.yml
allows:
      ipList: 
	  - 192.168.1.1
	  - 192.168.1.2
	  - 192.168.1.3
	  - 192.168.1.4
# or
allows:
     ipList: 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4
# application.properties
allows.ipList[0]=192.168.1.1
allows.ipList[1]=192.168.1.2
allows.ipList[2]=192.168.1.3
allows.ipList[3]=192.168.1.4
# or
allows.ipList= 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4

Map

If the array or list does not meet the requirements, the key vlaue type is needed. No problem. SpringBoot is also supported.

Suppose that different AES keys are used for different services of a docking party, then the key should be used as the key according to the service type and the corresponding key as the value when configuring.

Java configuration class

@Configuration
@ConfigurationProperties(prefix="aes.keys")
public class AesKeyConfig{
  private Map<String,String> keys;
  public void setKeys(Map<String,String> keys){
    this.keys = keys;
  }
}

configuration file

# application.yml
aes:
    keys:
        order: 28jsaS2asf2fSA2
        pay: @ra@3safdsR5&sDa
# or
aes:
    keys[order]: 28jsaS2asf2fSA2
    keys[pay]: @ra@3safdsR5&sDa
# application.properties
aes.keys.order=28jsaS2asf2fSA2
aes.keys.pay=@ra@3safdsR5&sDa
# or
aes.keys[order]=28jsaS2asf2fSA2
aes.keys[pay]=@ra@3safdsR5&sDa

Enum

Enumerate? That must be supported

However, the actual meaning is not great. If the configured value can be converted to enumeration value, the configured value must be the same as the name of enumeration value, and the case must not be bad. Because the spring boot implementation of the configuration to enumeration conversion uses

/* java.lang.Enum#valueOf */
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                            String name) {
  // The name here is the character representation of the enumeration value, which is generally uppercase
  T result = enumType.enumConstantDirectory().get(name);
  if (result != null)
    return result;
  if (name == null)
    throw new NullPointerException("Name is null");
  throw new IllegalArgumentException(
    "No enum constant " + enumType.getCanonicalName() + "." + name);
}

For the understanding of this code, please refer to another article Deep understanding of Java enumeration

If there are other fields in the enumeration, there is no way

JavaBean

what? Or not satisfied? Want to bind the configuration directly to a JavaBean?

Just do it!

JavaBean

@Configuration
@ConfigurationProperties(prefix="upload")
public class UploadConfig{
  private String rootPath;
  private String fileType;
  private int fileSize;
	private boolean rename;
  // Omit Setter method
}

configuration file

# application.yml
upload:
	root-path: /xx/xx/xx
	file-type: zip
	fileSize: 1M
	rename: false
# application.properties
upload.rootPath=/xx/xx/xx
upload.fileType=zip
upload.fileSize=1M
upload.rename=false

The above usages can be used in combination, which is very flexible

However, if it is an array or a List of JavaBean s, or as the value of a Map, it will not be bound.

The reason is that the default binding is based on the Setter method to bind and assign a single field. What we want here is a JavaBean. We need to create a JavaBean object, and then do the attribute binding assignment.

If you follow these two steps, you can also successfully bind to a JavaBean object as an element.

It's just that SpringBoot doesn't do that. Instead, it provides an @ ConstructorBinding annotation. Let's use constructors to bind data.

Complex configuration

JavBean

@Configuration
@ConfigurationProperties(prefix="app")
public class AppConfig{
  
  private Map<String, DataSourceMetadata> multiDataSourceMap;
  
  public void setMultiDataSourceMap(Map<String, DataSourceMetadata> multiDataSourceMap){
    this.multiDataSourceMap = multiDataSourceMap;
  }
}

public class DataSourceMetadata{
  private String url;
  private String driverClass;
  private String username;
  private String passowrd;
  // Omit Setter and Getter
}

configuration file

app:
      multiDataSourceMap:
		ds1:
			url: jdbc://
			driver-class: com.mysql.cj.Driver
			username: xxx
			password: xxx
		ds2:
			url: jdbc://
			driver-class: com.mysql.cj.Driver
			username: 12sds
			password:	adfwqw
# or 
app:
      multiDataSourceMap:
		ds1: {
			url: jdbc://
			driver-class: com.mysql.cj.Driver
			username: xxx
			password: xxx
		}
			
		ds2: {
			url: jdbc://
			driver-class: com.mysql.cj.Driver
			username: 12sds
			password:	adfwqw
		}

Then start, walk, and immediately you will find the familiar and angry NPE

The reason is simple. SpringBoot failed to read the corresponding configuration data from the configuration file and instantiate a Map because

The situation it is facing now is more complicated than before. Now JavaBean is a Map value

The solution is to use the constructor binding method, and you need to use the annotation @ ConstructorBinding in the constructor

public class DataSourceMetadata{
  private String url;
  private String driverClass;
  private String username;
  private String passowrd;
  @ConstructorBinding
  public DataSourceMetadata(String url, String driverClass, String username, String passowrd){
    this.url = url;
    this.driverClass = driverClass;
    this.username = username;
    this.password = password;
  }
  // Omit Setter and Getter
}

As long as it's done, it's normal. There won't be any annoying NPE

I don't know if there are other ways to do it, such as continuing to use Setter method for data binding

Project Svelte

All of the above configurations, if any, are written to the application.yml or application.properties file, which will result in too much content in the configuration file, and various configurations are mixed together, which is not easy to manage and maintain.

If you need to change a configuration, you need to change the entire file. There is a certain risk that other configurations will be changed by mistake.

So it should be a kind of configuration, put together to manage.

Similarly, a type of configuration usually corresponds to a function. If one of the configuration changes, the corresponding test can also ensure that the modification of the same configuration file will not cause other problems.

So it's necessary to split application.yml.

It took a lot of effort to split one to upload.yml, and then import the configuration file as follows

By default, the configuration file is placed in the resources directory (maven/gradle). After compiling and packaging, the configuration file will be located in the root directory of classes, which is called classpath

@Configuration
@PropertySource("classpath:upload.yml")
@ConfigurationProperties(prefix="upload")
public class UploadConfig{
  private String rootPath;
  private String fileType;
  private int fileSize;
	private boolean rename;
  // Omit Setter method
}

The problem is that you can't bind data to JavaBean properties.

Debug looks at the source code and can't get out of it. Try to use profile to solve it. Although it can be solved, it is not the same use scenario after all, and it is not appropriate.

Finally, I asked for help and told @ PropertySource doesn't support yaml files, only properties. So I tried. It was true

SpringBootVersion is2.2.6,A group of friends said he1.5Yes, I do, butSpringBootThe official made it clear that they did not support

2.7.4. YAML Shortcomings

YAML files cannot be loaded by using the @PropertySource annotation. So, in the case that you need to load values that way, you need to use a properties file.

As you can see above, in fact, yaml configuration has more advantages, so if you want to continue to use yaml, it's not impossible

@PropertySource supports custom file formats

// Here, the DefaultPropertySourceFactory is inherited, or the PropertySourceFactory can be directly implemented
public class YamlPropertySourceFactory extends DefaultPropertySourceFactory {

    public YamlPropertySourceFactory () {
        super();
    }

    @Override
    public PropertySource<?> createPropertySource (String name, EncodedResource resource)
            throws IOException {
        // This judgment is necessary, because the direct use of name is null, and the reason is not explored
        String nameToUse = name != null ? name : resource.getResource().getFilename();
        // yml file, use YamlPropertiesFactoryBean to build a Properties object from yaml file Resource
        // Then use PropertiesPropertySource to encapsulate the propertiesource object, which can be added to the Environment
        if (nameToUse.endsWith(".yml")) {
            YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
            factoryBean.setResources(resource.getResource());
            factoryBean.afterPropertiesSet();
            return new PropertiesPropertySource(nameToUse, factoryBean.getObject());
        }
        // If not, use the default implementation
        return super.createPropertySource(name, resource);
    }
}

When using, @ PropertySource(factory=YamlPropertySourceFactory.class) is enough.

Use @ Value

@Value is an annotation of the Spring Framework and does not belong to spring boot. Its typical use scenario is to inject external configuration attributes and official documents @Values introduce

@Value uses the Spring built-in converter simpletypconverter, which supports Integer, String, and comma separated String arrays.

If the support is not enough, you can customize the transformation support, customize a Converter, and then add it to the ConverterService Bean, because the latter BeanPostProcessor relies on the ConverterService to process the transformation

So if there are some complex configurations, it's better to use spring boot.

@The advantage of Value is that it supports spiel and can be used on any Bean's method parameters or fields

So these are two different use scenarios, depending on the situation.

However, in general, I prefer the former one, because if @ Value is used directly in other beans, in case we need to change the configuration name, the result is that @ Value is used everywhere, which is troublesome to change, so from the perspective of management and maintenance, @ Value is too wild.

By the way, the processing location for @ Value is org.springframework.beans.factory.support.abstractautowirecapablebeanfactory ා populatebean. Of course, this is also the place to process @ input @ Autowired @ resource

Postscript

There are two steps from configuration file to configuration value used in program

  1. Read the configuration content into the Environment. No matter what the configuration is, the last is a Property Source
  2. Configuration value binding injection through BeanPostProcessor

If the configuration does not meet the properties or yaml format, you can customize the PropertySourceLoader, which can refer to

org.springframework.boot.env.YamlPropertySourceLoader and org.springframework.boot.env.PropertiesPropertySourceLoader

Posted by lena_k198 on Sun, 17 May 2020 04:04:45 -0700