Dumping the Spring Environment in Spring Boot
When developing Spring Boot applications, I often need to check if property values are correctly received from configuration files, environment variables, or other sources. There are various ways to perform this task. This article will present my solution for dumping the Spring Environment using a simple application listener.
- Introduction
- The Environment and ConfigurableEnvironment interfaces
- The MutablePropertySources and PropertySource classes
- The solution code
- Registering the listener
- Sample output
- Final notes
Introduction
If you need to check the Spring Environment, one possible solution is Spring Boot Actuator. However, this approach may not be suitable for several reasons. First, not all Spring Boot applications have or need to have Actuator. Second, the Spring application context may fail to start; in this case, Actuator will not help you since the application is not running.
Another solution is dumping (using logging) the entire Spring Environment at a very early stage of the Spring application. This is the solution described in this article.
The Environment and ConfigurableEnvironment interfaces
In Spring, the “Environment” models two different aspects of every application: profiles and properties. Properties come from various sources, like system properties, environment variables, and configuration files.
The Environment is represented at the highest level by the Environment
interface, which is an extension of PropertyResolver
. Environment
has only one direct sub-interface, ConfigurableEnvironment
. This latter interface can manipulate profiles and properties in various ways.
In Spring Boot applications, the concrete environment object is typically always an implementation of ConfigurableEnvironment
. This interface is essential because it provides the getPropertySources()
method, which returns a MutablePropertySources
object described in the following section.
The MutablePropertySources and PropertySource classes
MutablePropertySources
is the default implementation of the PropertySources
interface and acts as a container for a sequence of PropertySource
objects. This characteristic is easy to spot because MutablePropertySources
also implements Iterable<PropertySource<?>>
, so you can iterate on property sources.
Each PropertySource
object represents a single source of properties, like the environment variables or the application.properties
/.yaml
file. There is, however, an important aspect to know about PropertySource
. It allows you to look up a property value by name but doesn’t offer any method to iterate on property names.
Fortunately, PropertySource
has a sub-class named EnumerablePropertySource
. It provides the getPropertyNames()
method, so you can get an array of all property names available in a PropertySource
object.
In real Spring Boot applications, some property sources are enumerable while others are not. You will typically find that the most helpful property sources are enumerable while a few others are not. Thus, as described in this article, dumping the entire Spring Environment is possible and very valuable.
The solution code
After the theoretical part, it’s time to see my personal solution. The class I will show is an implementation of ApplicationListener
, which is the Spring interface used to receive events from the application context. This is an application of the well-known “Observer” design pattern.
I have chosen to handle the event named ApplicationEnvironmentPreparedEvent
. Note that this is a Spring Boot-specific event; thus, you cannot use it in a “pure” Spring Framework application. This event is fired very early in a Spring Boot application life since it happens just after the Environment is available but before the Spring context is created.
Since the context is not yet available at this stage, we cannot simply use @Component
to make the class a Spring “bean”. This listener must be registered using the SpringApplication
class or, more generally, using the META-INF/spring.factories
file (described later).
The following is my solution code.
package your.pkgname;
import java.util.Arrays;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
public class SpringEnvDumper implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private static final Logger logger = LoggerFactory.getLogger(SpringEnvDumper.class);
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
if (logger.isDebugEnabled()) {
dumpEnvironment(event.getEnvironment());
}
}
private static void dumpEnvironment(ConfigurableEnvironment environment) {
logger.debug("======== SPRING ENVIRONMENT DUMP ========");
for (PropertySource<?> source : environment.getPropertySources()) {
dumpPropertySource(environment, source);
}
logger.debug("=========================================");
}
private static void dumpPropertySource(PropertyResolver resolver, PropertySource<?> source) {
logger.debug("PropertySource: \"{}\"", source.getName());
if (!(source instanceof EnumerablePropertySource)) {
logger.debug(" <not an EnumerablePropertySource>");
return;
}
EnumerablePropertySource<?> enumerableSource = (EnumerablePropertySource<?>) source;
String[] propNames = enumerableSource.getPropertyNames();
if (propNames.length == 0) {
logger.debug(" <no properties>");
return;
}
Arrays.sort(propNames);
for (String propName : propNames) {
Object propValue = enumerableSource.getProperty(propName);
dumpProperty(resolver, propName, propValue);
}
}
private static void dumpProperty(PropertyResolver resolver, String propName, Object propValue) {
logger.debug(" \"{}\" = {}", propName, valueAsString(propValue));
if (propValue instanceof String) {
String propValueResolved = resolver.resolvePlaceholders((String) propValue);
if (!Objects.equals(propValueResolved, propValue)) {
logger.debug(" `---> {}", valueAsString(propValueResolved));
}
}
}
private static String valueAsString(Object value) {
if (value instanceof String) {
return "\"" + value + "\"";
}
return String.valueOf(value);
}
}
Registering the listener
As anticipated, the SpringEnvDumper
class, which implements the listener, must be explicitly registered. The most simple and general way to do this is by using the spring.factories
file. In a Maven or Gradle project, this file is usually located under the path:
<your-project>/src/main/resources/META-INF/spring.factories
So, create the spring.factories
file with the following content:
org.springframework.context.ApplicationListener = your.pkgname.SpringEnvDumper
(note: replace your.pkgname
with your real package name!)
Sample output
Suppose that you also have the following application.yaml
(or .properties
equivalent) configuration file:
server:
port: ${SVRPORT:8080}
logging:
level:
your.pkgname: DEBUG
If everything works correctly, you should see a logging output that is similar to this:
……… your.pkgname.SpringEnvDumper : ======== SPRING ENVIRONMENT DUMP ========
……… your.pkgname.SpringEnvDumper : PropertySource: "configurationProperties"
……… your.pkgname.SpringEnvDumper : <not an EnumerablePropertySource>
……… your.pkgname.SpringEnvDumper : PropertySource: "servletConfigInitParams"
……… your.pkgname.SpringEnvDumper : <not an EnumerablePropertySource>
……… your.pkgname.SpringEnvDumper : PropertySource: "servletContextInitParams"
……… your.pkgname.SpringEnvDumper : <not an EnumerablePropertySource>
……… your.pkgname.SpringEnvDumper : PropertySource: "systemProperties"
……… your.pkgname.SpringEnvDumper : "file.encoding" = "UTF-8"
……… your.pkgname.SpringEnvDumper : "file.separator" = "\"
……… your.pkgname.SpringEnvDumper : "java.awt.headless" = "true"
……… ⁞ ⁞ ⁞
……… ⁞ ⁞ ⁞
……… your.pkgname.SpringEnvDumper : "user.script" = ""
……… your.pkgname.SpringEnvDumper : "user.timezone" = "Europe/Rome"
……… your.pkgname.SpringEnvDumper : "user.variant" = ""
……… your.pkgname.SpringEnvDumper : PropertySource: "systemEnvironment"
……… ⁞ ⁞ ⁞
……… your.pkgname.SpringEnvDumper : "COMPUTERNAME" = "MYPC"
……… ⁞ ⁞ ⁞
……… your.pkgname.SpringEnvDumper : "DIRCMD" = "/OGN"
……… ⁞ ⁞ ⁞
……… your.pkgname.SpringEnvDumper : PropertySource: "random"
……… your.pkgname.SpringEnvDumper : <not an EnumerablePropertySource>
……… your.pkgname.SpringEnvDumper : PropertySource: "Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'"
……… your.pkgname.SpringEnvDumper : "logging.level.your.pkgname" = "DEBUG"
……… your.pkgname.SpringEnvDumper : "server.port" = "${SVRPORT:8080}"
……… your.pkgname.SpringEnvDumper : `---> "8080"
……… your.pkgname.SpringEnvDumper : =========================================
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.2)
………
Note that the Spring Environment dumping happens before the text banner’s output!
Final notes
Before I wrap up, I’d like to provide some notes about the code.
First of all, I used SLF4J for logging. You are free to use any other logging library or “facade” logging library. Secondly, I logged all events at DEBUG level. Again, you are free to change the level to whatever you want.
In particular, you should notice the part of the code that checks if a property source is enumerable:
if (!(source instanceof EnumerablePropertySource)) {
logger.debug(" <not an EnumerablePropertySource>");
return;
}
If the property source object is not of type EnumerablePropertySource
, there is no straightforward way to get all the property names.