practically, fundamentally and in more detail. Part 3

Working with application parameters

Activation via annotation

CosmoZoo is not limited to just observing animals in enclosures. Those who wish can see the zoo's inhabitants in their natural environment, right on their home planets. The nearby observatory will help with this.

Let's describe the parameters of the observatory, indicate its name, the diameter of the main telescope, and whether the AI ​​that exists there supports automatic observation of the fauna of other planets:

@ConfigurationProperties(prefix = "app.observatory")
@ConstructorBinding
@Value
public class ObservatoryProperties {

  String name;
  Integer telescopeDiameter;
  Boolean automaticMode;
}

Let's create an Observatory class, into whose constructor we will pass the received parameters:

public class Observatory {

  public Observatory(ObservatoryProperties properties) {
    System.out.println("ObservatoryProperties: " + properties);
  }

  //some code here
}

Let's add data to application.properties:

app.observatory.name=Kopernik Station
app.observatory.telescope-diameter=8
app.observatory.automatic-mode=true

Let's create a factory with a method that initiates the service bean. Note the added annotation @EnableConfigurationProperties:

@AutoConfiguration("observatoryFactory")
@EnableConfigurationProperties(ObservatoryProperties.class)
@RequiredArgsConstructor
public class ObservatoryFactory {

  @Bean
  public Observatory observatory(ObservatoryProperties properties) {
    return new Observatory(properties);
  }
}

Let's test the service:

@SpringBootTest(classes = ObservatoryFactory.class)
class ObservatoryFactoryTest {

  @Autowired
  private ApplicationContext context;

  @ParameterizedTest
  @MethodSource("beanNames")
  void applicationContextContainsBean(String beanName) {
    Assertions.assertTrue(context.containsBean(beanName));
  }

  private static Stream<String> beanNames() {
    return Stream.of(
        "observatoryFactory",
        "app.observatory-science.zoology.properties.ObservatoryProperties",
        "observatory"
    );
  }
}

Note the name of the class parameters bean: “app.observatory-science.zoology.properties.ObservatoryProperties”. It contains a concatenation of the settings prefix and the full class name separated by the “-” sign. You can also see the line in the console:

ObservatoryProperties: ObservatoryProperties(name=Kopernik Station, telescopeDiameter=8, automaticMode=true)

Implementation as a bean

The data received from the observatory needs to be stored somewhere. Let's add a planetarium to our objects.

public class Planetarium {

  public Planetarium(PlanetariumProperties properties) {
    System.out.println("PlanetariumProperties: " + properties);
  }

  //some code here
}

Settings class:

@ConfigurationProperties(prefix = "app.planetarium")
@Data
public class PlanetariumProperties {

  private String name;
  private Integer numberOfAuditoriums;
  private List<String> programmes;
}

Configuration parameters:

app.planetarium.name=Star Theatre
app.planetarium.number-of-auditoriums=16
app.planetarium.programmes=Travelling through the solar system, Secrets of the Universe, Is there life on Mars?

Now let's add a factory:

@AutoConfiguration("planetariumFactory")
@RequiredArgsConstructor
public class PlanetariumFactory {

  @Bean
  public PlanetariumProperties planetariumProperties() {
    return new PlanetariumProperties();
  }

  @Bean
  public Planetarium planetarium(PlanetariumProperties properties) {
    return new Planetarium(properties);
  }
}

You can test:

@SpringBootTest(classes = PlanetariumFactory.class)
class PlanetariumFactoryTest {

  @Autowired
  private ApplicationContext context;

  @ParameterizedTest
  @MethodSource("beanNames")
  void applicationContextContainsBean(String beanName) {
    Assertions.assertTrue(context.containsBean(beanName));
  }

  private static Stream<String> beanNames() {
    return Stream.of(
        "planetariumFactory",
        "planetariumProperties",
        "planetarium"
    );
  }
}

These are two approaches to getting parameters from a configuration file. The key difference between them is that in the annotation version @EnableConfigurationProperties in the parameter class you can use final fields with initialization via the constructor. Immutability is good. But the characteristics of these fields will be absent in spring-configuration-metadata.json — a file automatically generated by Spring. Let's figure out what this file is and why it is needed.

Description of parameters

By using annotation processor Spring Boot generates a metadata representation of the application configuration. File spring-configuration-metadata.json Contains information about available configuration parameters, their types, default values, descriptions, and other attributes.

For example, if an application's application.properties file specifies an invalid value for some configuration parameter, Spring Boot may display an error message when the application starts. This error message will contain information from the spring-configuration-metadata.json file that will help the developer fix the error.

Additionally, this file can be used to automatically substitute values ​​from pre-specified options or hints when editing configuration files in the development environment.

To auto-generate this file, you need to add the following line to build.gradle and build the project:

dependencies {
  annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:2.7.18"
}

But first, I suggest supplementing ObservatoryProperties with a description of each field:

@ConfigurationProperties(prefix = "app.observatory")
@ConstructorBinding
@Value
public class ObservatoryProperties {

  /**
  * Название обсерватории.
  */
  String name;

  /**
  * Диаметр основного телескопа.
  */
  Integer telescopeDiameter;

  /**
  * Автоматический режим ИИ.
  */
  Boolean automaticMode;
 
  //more properties
}

In the PlanetariumProperties class, also specify default values:

@ConfigurationProperties(prefix = "app.planetarium")
@Data
public class PlanetariumProperties {

  /**
  * Название планетария.
  */
  private String name;

  /**
  * Количество аудиторий.
  */
  private Integer numberOfAuditoriums;

  /**
  * Список программ.
  */
  private List<String> programmes;
}

And now, having completed ./gradlew clean build, we will get a file with the following content:

{
 "groups": [
  {
   "name": "app.observatory",
   "type": "science.zoology.properties.ObservatoryProperties",
   "sourceType": "science.zoology.properties.ObservatoryProperties"
  },
  {
   "name": "app.planetarium",
   "type": "science.zoology.properties.PlanetariumProperties",
   "sourceType": "science.zoology.properties.PlanetariumProperties"
  }
 ],
 "properties": [
  {
   "name": "app.planetarium.name",
   "type": "java.lang.String",
   "description": "Название планетария.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": "Stars"
  },
  {
   "name": "app.planetarium.number-of-auditoriums",
   "type": "java.lang.Integer",
   "description": "Количество аудиторий.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": 1
  },
  {
   "name": "app.planetarium.programmes",
   "type": "java.util.List<java.lang.String>",
   "description": "Список программ.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": "Teenagers in the Universe"
  }
 ],
 "hints": []
}

The difference between the two approaches to working with parameters is now clearly visible. ObservatoryProperties only has a general description of the type, while PlanetariumProperties contains detailed information about each field, including the “description” where the javadoc content and default values ​​are located.

Additional metadata

If you prefer to work with immutable classes but want to generate parameter metadata automatically, there are two options for doing this.

Default values

A hacky option is to add a constructor with default values. Then the description for the ObservatoryProperties class fields will be generated in the same way as for the PlanetariumProperties fields:

public ObservatoryProperties(@DefaultValue("Observatory") String name,
              @DefaultValue("42") Integer telescopeDiameter,
              @DefaultValue("false") Boolean automaticMode) {
  this.name = name;
  this.telescopeDiameter = telescopeDiameter;
  this.automaticMode = automaticMode;
}

The advantages of this solution:

  • allows you to automatically create a configuration description;

  • data from javadoc will be available to the consumer of this starter.

Flaws:

Data enrichment

Let's remove the created constructor from the ObservatoryProperties class and use more flexible metadata management capabilities. To do this, create the additional-spring-configuration-metadata.json file in the META-INF directory with the following content:

{
 "properties": [
  {
   "name": "app.observatory.automatic-mode",
   "type": "java.lang.Boolean",
   "description": "Автоматический режим ИИ.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": false
  },
  {
   "name": "app.observatory.name",
   "type": "java.lang.String",
   "description": "Название обсерватории.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": "Observatory"
  },
  {
   "name": "app.observatory.telescope-diameter",
   "type": "java.lang.Integer",
   "description": "Диаметр основного телескопа.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": 42
  }
 ],
 "hints": [
  {
   "name": "app.observatory.name",
   "values": [
    {
     "value": "Star Trek"
    },
    {
     "value": "Galactic beacon"
    },
    {
     "value": "Star Rider"
    }
   ]
  },
  {
   "name": "app.observatory.telescope-diameter",
   "values": [
    {
     "value": 4
    },
    {
     "value": 8
    },
    {
     "value": 16
    }
   ]
  },
  {
   "name": "app.observatory.automatic-mode",
   "values": [
    {
     "value": true
    },
    {
     "value": false
    }
   ]
  }
 ]
}

After rebuilding the project, you can see the updated metadata:

{
 "groups": [
  {
   "name": "app.observatory",
   "type": "science.zoology.properties.ObservatoryProperties",
   "sourceType": "science.zoology.properties.ObservatoryProperties"
  },
  {
   "name": "app.planetarium",
   "type": "science.zoology.properties.PlanetariumProperties",
   "sourceType": "science.zoology.properties.PlanetariumProperties"
  }
 ],
 "properties": [
  {
   "name": "app.observatory.automatic-mode",
   "type": "java.lang.Boolean",
   "description": "Автоматический режим ИИ.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": false
  },
  {
   "name": "app.observatory.name",
   "type": "java.lang.String",
   "description": "Название обсерватории.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": "Observatory"
  },
  {
   "name": "app.observatory.telescope-diameter",
   "type": "java.lang.Integer",
   "description": "Диаметр основного телескопа.",
   "sourceType": "science.zoology.properties.ObservatoryProperties",
   "defaultValue": 42
  },
  {
   "name": "app.planetarium.name",
   "type": "java.lang.String",
   "description": "Название планетария.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": "Stars"
  },
  {
   "name": "app.planetarium.number-of-auditoriums",
   "type": "java.lang.Integer",
   "description": "Количество аудиторий.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": 1
  },
  {
   "name": "app.planetarium.programmes",
   "type": "java.util.List<java.lang.String>",
   "description": "Список программ.",
   "sourceType": "science.zoology.properties.PlanetariumProperties",
   "defaultValue": "Teenagers in the Universe"
  }
 ],
 "hints": [
  {
   "name": "app.observatory.automatic-mode",
   "values": [
    {
     "value": true
    },
    {
     "value": false
    }
   ]
  },
  {
   "name": "app.observatory.name",
   "values": [
    {
     "value": "Star Trek"
    },
    {
     "value": "Galactic beacon"
    },
    {
     "value": "Star Rider"
    }
   ]
  },
  {
   "name": "app.observatory.telescope-diameter",
   "values": [
    {
     "value": 4
    },
    {
     "value": 8
    },
    {
     "value": 16
    }
   ]
  }
 ]
}

The default values ​​added strictly limit the data that is considered valid for the specified field. Any other value of the appropriate type can be added, but the development environment will prompt you that the data entered is invalid. For example, we have the possible observatory names specified: Star Trek, Galactic beacon, and Star Rider. But in application.properties, Kopernik Station is specified, which leads to this warning:

To add any other value to the list of valid values, you must specify providers: “any” in the description of the name field:

{
 "name": "app.observatory.name",
 "values": [
  {
   "value": "Star Trek"
  },
  {
   "value": "Galactic beacon"
  },
  {
   "value": "Star Rider"
  }
 ],
 "providers": [
  {
   "name": "any"
  }
 ]
}

The main blocks of spring-configuration-metadata are:

  • Groups — parameter blocks, the value is equal to the configuration prefix;

  • Properties — directly the configuration parameters defined in the *.properties or *.yml files. Contain:
    name — parameter name;
    type — Java type;
    name — parameter name;
    type — Java type;
    description — a description available as a tooltip from the IDE when defining a parameter;
    sourceType — parameter container class;
    defaultValue — default value;

Hints — a block of hints indicating the available values ​​when filling in a parameter.

You can read more about the JSON structure of metadata in official documentation.

Hierarchy of parameters

When integrating a starter into an application, you need to take into account some features of working with parameters.

Format priority

The configuration specified in application.propertiestakes precedence over application.yml. Accordingly, if the starter has a description in the application.properties file, and you try to override some parameter in application.yml, then you will not succeed.

Priority placement

The application.properties file of an application has a higher priority than the application.properties file of the starter. If the names are the same, the application configuration completely overwrites the contents of the starter configuration file.

One way to avoid such collisions is to define the starter parameters in a file whose name differs from the default name. For example, if the configuration options for different types of environments are known in advance, you can create files with parameters for each stand:

application-dev.properties
application-test.properties

And to add data to the context, use the @PropertySource annotation. The class itself, of course, needs to be added to the list of autoconfigurations:

@AutoConfiguration
@PropertySource("classpath:application-${spring.profiles.active:test}.properties")
public class PropertiesConfiguration {
}

In this case, the @PropertySource annotation parameter accepts SpEL. It allows dynamically using one or another profile, depending on the active spring profile. Or the test profile will be used by default.

There may be several connected profiles or none at all, and the result of the expression will cause an exception, since the corresponding profile will not be found. In addition, the spring profile is usually used not just like that, but to load data that is in the corresponding project configuration file. Then again there is a risk of name conflict and content substitution. The same applies to commonly used templates, such as application-default, application-starter, and so on, because sooner or later another starter will be written, which will have its own application-default.

That is, the best option is to create a configuration file with a name that matches the project:

cosmozoo-spring-boot-starter-application.properties

And then add the configuration to the application context:

@AutoConfiguration
@PropertySource("classpath:cosmozoo-spring-boot-starter-application.properties")
public class PropertiesConfiguration {
}

The chances that two different starters with the same name will be developed and both will be used in the same microservice are close to zero. Also, the configuration clearly defines the scope of the parameters by its name. As a result, we have the following behavior:

  1. The default starter configuration is added to the context and is ready to use without any additional instructions or profile connection.

  2. Settings overridden in the application's application.properties or application.yml will take precedence over the default value.

  3. In the application configuration, you can override any number of parameters; those that are not overridden will be used from the starter configuration.

  4. An additionally connected application profile will be able to set its own value for any starter parameter.

This issue could be solved in another way – by adding config-map to classpath directly in environment variables. But the option using annotation @PropertySource It seems more visual and convenient to work with. We will talk about this possibility in more detail in the next part.


Previous articles from the Spring Boot Starter series:

  1. Spring Boot Starter: Practical, Fundamental and More Detailed. Part 1 — What is good about Spring Boot Starter and what is autoconfiguration

  2. Spring Boot Starter: Practical, Fundamental and Detailed. Part 2 — Conditions and dependencies when creating beans

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *