Skip to main content

Spring Environment Configuration

In modern application development, your code will run in multiple environments - development, testing, staging, and production. Each environment has different requirements: development might use an in-memory database, while production connects to a high-availability cluster. Spring's environment configuration capabilities let you manage these differences elegantly.

Introduction to Spring Environment

Spring's Environment abstraction provides a unified interface for property management and profile configuration. It helps you:

  1. Define different behaviors for different environments
  2. Externalize configuration from your code
  3. Override configuration based on deployment context
  4. Maintain environment-specific properties

Let's explore how to effectively manage environment configurations in Spring applications.

Spring Profiles

Profiles are a core mechanism for conditional component registration and configuration property selection in Spring.

What Are Profiles?

Profiles let you define beans that should only be created in specific environments. For example, you might have:

  • A development profile that uses an embedded database
  • A production profile that connects to a real database server
  • A test profile that uses mock services

Configuring Profiles

Using Annotations

You can mark beans to be included only when specific profiles are active:

java
@Component
@Profile("development")
public class DevDatabaseConfig implements DatabaseConfig {
// Development database configuration
}

@Component
@Profile("production")
public class ProdDatabaseConfig implements DatabaseConfig {
// Production database configuration
}

You can also combine multiple profiles using logical operators:

java
@Profile("production & !eu")  // Active in production but not EU region
@Profile("development | test") // Active in either development or test

Using XML Configuration

For XML-based configurations:

xml
<beans profile="development">
<!-- beans that only activate in development profile -->
</beans>

Activating Profiles

You can activate profiles in several ways:

Using application.properties/application.yml

properties
# application.properties
spring.profiles.active=development

Or in YAML format:

yaml
# application.yml
spring:
profiles:
active: development

Using JVM System Properties

When starting your application:

bash
java -jar myapp.jar -Dspring.profiles.active=production

Using Environment Variables

bash
export SPRING_PROFILES_ACTIVE=production
java -jar myapp.jar

Programmatically

java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setAdditionalProfiles("production");
application.run(args);
}
}

Profile Groups

Spring Boot 2.4+ lets you define profile groups to activate multiple profiles together:

properties
spring.profiles.group.production=prod-db,prod-logging,metrics

Now, activating the production profile automatically activates prod-db, prod-logging, and metrics profiles.

Property Sources

Spring's environment abstraction uses a hierarchy of property sources to resolve configuration values.

Property Resolution Order

Spring checks these sources (from highest to lowest priority):

  1. Command line arguments
  2. JNDI attributes from java:comp/env
  3. JVM system properties (System.getProperties())
  4. OS environment variables
  5. Profile-specific application properties
  6. Application properties
  7. @PropertySource annotations
  8. Default properties

Common Property Sources

application.properties/application.yml

The standard configuration files located in:

  • /config subdirectory of the working directory
  • Working directory
  • config package in the classpath
  • Root of classpath
properties
# application.properties
server.port=8080
app.feature.enabled=true
app.cache.timeout=3600

The YAML equivalent:

yaml
# application.yml
server:
port: 8080
app:
feature:
enabled: true
cache:
timeout: 3600

Profile-Specific Properties

Spring loads properties from profile-specific files when a profile is active:

application-development.properties
application-production.properties
application-test.properties

External Configuration

For production deployment, externalize configuration:

bash
java -jar myapp.jar --spring.config.location=file:/path/to/config/

@PropertySource Annotation

Load additional property files:

java
@Configuration
@PropertySource("classpath:db/database.properties")
public class AppConfig {
// Configuration
}

Accessing Environment Properties

Spring provides several ways to access configuration properties in your code.

Using the Environment Object

java
@Autowired
private Environment environment;

public void someMethod() {
String serverName = environment.getProperty("app.server.name");
int serverPort = environment.getProperty("app.server.port", Integer.class, 8080);
boolean isEnabled = environment.getProperty("app.feature.enabled", Boolean.class, false);

// Check if a property exists
if (environment.containsProperty("app.feature.new")) {
// Use the new feature
}

// Check if a profile is active
if (environment.acceptsProfiles(Profiles.of("production"))) {
// Do production-specific initialization
}
}

Using @Value Annotation

Inject properties directly into fields:

java
@Component
public class MyService {
@Value("${app.server.name}")
private String serverName;

@Value("${app.server.port:8080}")
private int serverPort; // Default is 8080

@Value("${app.feature.enabled:false}")
private boolean featureEnabled;
}

Using @ConfigurationProperties

Group related properties into typed Java objects:

java
@Component
@ConfigurationProperties(prefix = "app.server")
public class ServerProperties {
private String name;
private int port = 8080; // Default value
private boolean secure = false;

// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }

public int getPort() { return port; }
public void setPort(int port) { this.port = port; }

public boolean isSecure() { return secure; }
public void setSecure(boolean secure) { this.secure = secure; }
}

Then in your properties file:

properties
app.server.name=production-server
app.server.port=443
app.server.secure=true

And use it in your code:

java
@Service
public class NetworkService {
private final ServerProperties serverProperties;

@Autowired
public NetworkService(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}

public void connect() {
String protocol = serverProperties.isSecure() ? "https" : "http";
String serverUrl = protocol + "://" + serverProperties.getName() + ":" + serverProperties.getPort();
// Connect to the server
}
}

Practical Example: Multi-Environment Application

Let's build a simple Spring Boot application with environment-specific configurations.

Project Structure

src/
└── main/
├── java/
│ └── com/
│ └── example/
│ ├── DemoApplication.java
│ ├── config/
│ │ ├── DevDatabaseConfig.java
│ │ └── ProdDatabaseConfig.java
│ └── service/
│ └── NotificationService.java
└── resources/
├── application.properties
├── application-development.properties
└── application-production.properties

Configuration Classes

java
// DemoApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
java
// DevDatabaseConfig.java
package com.example.config;

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

@Configuration
@Profile("development")
public class DevDatabaseConfig {

@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("dev-data.sql")
.build();
}
}
java
// ProdDatabaseConfig.java
package com.example.config;

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration
@Profile("production")
public class ProdDatabaseConfig {

@Value("${db.url}")
private String dbUrl;

@Value("${db.username}")
private String dbUsername;

@Value("${db.password}")
private String dbPassword;

@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(dbUrl);
dataSource.setUsername(dbUsername);
dataSource.setPassword(dbPassword);
return dataSource;
}
}
java
// NotificationService.java
package com.example.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

@Value("${notification.service.url}")
private String serviceUrl;

@Value("${notification.service.token:}")
private String serviceToken;

@Value("${notification.enabled}")
private boolean notificationsEnabled;

public void sendNotification(String message) {
if (!notificationsEnabled) {
System.out.println("Notifications disabled: " + message);
return;
}

System.out.println("Sending to " + serviceUrl);
if (!serviceToken.isEmpty()) {
System.out.println("Using auth token: " + serviceToken);
}
System.out.println("Message: " + message);
}
}

Property Files

properties
# application.properties (common settings)
spring.application.name=demo-app
server.port=8080
notification.enabled=true
properties
# application-development.properties
notification.service.url=http://localhost:8888/notify
notification.enabled=false
logging.level.com.example=DEBUG
properties
# application-production.properties
notification.service.url=https://notification-service.example.com/api/notify
notification.service.token=${NOTIFICATION_TOKEN}
db.url=jdbc:mysql://prod-db.example.com:3306/myapp
db.username=${DB_USERNAME}
db.password=${DB_PASSWORD}
logging.level.com.example=INFO

Running the Application

For development:

bash
java -jar demo-app.jar --spring.profiles.active=development

Output:

... Starting DemoApplication with development profile
... Initialized embedded H2 database
... Notifications disabled: Application started

For production:

bash
# Set required environment variables
export NOTIFICATION_TOKEN=abc123
export DB_USERNAME=produser
export DB_PASSWORD=s3cr3t
java -jar demo-app.jar --spring.profiles.active=production

Output:

... Starting DemoApplication with production profile
... Connected to MySQL database at prod-db.example.com
... Sending to https://notification-service.example.com/api/notify
... Using auth token: abc123
... Message: Application started

Best Practices for Environment Configuration

  1. Separate code from configuration: Never hardcode environment-specific values in your code.

  2. Use meaningful profiles: Name them for specific purposes rather than environments (e.g., inmemory-db rather than just dev).

  3. Keep secrets secure: Use vault systems, environment variables or encrypted property files for sensitive data.

  4. Provide sensible defaults: Use default values for properties when appropriate:

    java
    @Value("${cache.timeout:60}")  // Default to 60 seconds
  5. Document required properties: Comment your property files and document required environment variables.

  6. Use property validation: Add validation to detect missing or invalid properties:

    java
    @ConfigurationProperties(prefix = "app.mail")
    @Validated
    public class MailProperties {
    @NotNull
    private String host;

    @Min(1)
    @Max(65535)
    private int port = 25;

    // Getters and setters
    }
  7. Use relaxed binding: Spring allows various formats for the same property:

    properties
    # All these map to the same property
    app.page-size=100
    app.pageSize=100
    APP_PAGE_SIZE=100
  8. Test with different profiles: Include tests that verify your application behaves correctly with different profiles.

Summary

Spring's environment configuration provides powerful tools for managing your application across different environments:

  • Profiles let you activate different beans and configurations based on the environment
  • Property sources establish a hierarchy for resolving configuration values
  • Configuration properties provide type-safe access to externalized configuration
  • Environment abstraction unifies access to profiles and properties

Properly configuring your Spring application for different environments improves maintainability, security, and flexibility. By separating configuration from code and using Spring's environment tools, you can easily deploy the same application across development, testing, and production environments.

Additional Resources

Exercises

  1. Create a Spring Boot application that serves different content based on profiles (e.g., dev, staging, prod).

  2. Implement a @ConfigurationProperties class to hold database connection settings, and use different property files for different environments.

  3. Write a test that verifies your application behaves correctly when switching between profiles.

  4. Extend the example application to load configuration from a remote source (e.g., Spring Cloud Config).

  5. Create a profile-based bean that provides sample data in development but real data in production.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)