Spring Boot
Value Annotation
The @Value annotation injects property values, environment variables, and Spring Expression Language (SpEL) expressions directly into Spring beans. It is the most concise way to inject a single configuration value into a field, constructor parameter, or method parameter. Understanding @Value's syntax, default values, type conversion, and when to prefer @ConfigurationProperties over @Value is essential for clean Spring Boot configuration.
What Is @Value?
@Value is a Spring annotation that injects a value into a field, constructor parameter, setter parameter, or method parameter. The value can come from property files, environment variables, system properties, or Spring Expression Language (SpEL) expressions evaluated at runtime.
@Value uses two syntax forms: the property placeholder syntax ${...} reads a value from the Spring Environment (property files, environment variables, system properties), and the SpEL syntax #{...} evaluates a Spring Expression Language expression which can perform computations, call methods, access other beans, and read system properties.
@Value is best suited for injecting individual configuration values. When you need to inject a group of related properties, @ConfigurationProperties is cleaner — it binds an entire prefix of properties to a typed class with IDE auto-completion, validation, and no string-based property keys.
Property Placeholder Syntax — ${...}
The ${property.key} syntax reads a value from the Spring Environment. This includes all sources: application.properties, application.yml, environment variables, system properties, and command-line arguments.
Java
@Service
public class JwtTokenService {
// Inject from application.properties:
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private long expirationSeconds;
@Value("${app.jwt.issuer}")
private String issuer;
// With default value — used when property is not defined:
@Value("${app.jwt.algorithm:HS256}")
private String algorithm; // "HS256" if app.jwt.algorithm is not set
@Value("${server.port:8080}")
private int serverPort; // 8080 if server.port is not set
@Value("${app.debug:false}")
private boolean debugMode; // false if app.debug is not set
// Inject from environment variables:
// STRIPE_API_KEY=sk_live_abc123 (OS environment variable)
@Value("${STRIPE_API_KEY}")
private String stripeApiKey;
// Environment variables with relaxed binding:
// MY_APP_NAME=MyApp → accessible as ${my.app.name} or ${MY_APP_NAME}
@Value("${my.app.name}")
private String appName;
// Default value as another property reference:
@Value("${app.timeout:${spring.mvc.async.request-timeout:30000}}")
private long timeout; // falls back through two levels of defaults
}
// application.properties:
// app.jwt.secret=my-256-bit-secret
// app.jwt.expiration=86400
// app.jwt.issuer=https://myapp.comConstructor and Method Parameter Injection
@Value works on constructor parameters and method parameters — the recommended approach alongside constructor injection for mandatory configuration values.
Java
// Constructor parameter injection with @Value — immutable fields:
@Service
public class StripePaymentService implements PaymentService {
private final String apiKey;
private final String webhookSecret;
private final boolean sandboxMode;
private final int maxRetries;
// @Value on constructor parameters — fields can be final:
public StripePaymentService(
@Value("${stripe.api.key}") String apiKey,
@Value("${stripe.webhook.secret}") String webhookSecret,
@Value("${stripe.sandbox.mode:false}") boolean sandboxMode,
@Value("${stripe.max.retries:3}") int maxRetries) {
this.apiKey = apiKey;
this.webhookSecret = webhookSecret;
this.sandboxMode = sandboxMode;
this.maxRetries = maxRetries;
}
public PaymentResult charge(String customerId, BigDecimal amount) {
Stripe.apiKey = this.apiKey;
// use sandboxMode, maxRetries...
return processCharge(customerId, amount);
}
}
// Setter injection with @Value:
@Service
public class EmailService {
private String smtpHost;
private int smtpPort;
@Autowired
public void setSmtpConfig(
@Value("${spring.mail.host}") String host,
@Value("${spring.mail.port:587}") int port) {
this.smtpHost = host;
this.smtpPort = port;
}
}
// @Bean method parameter injection:
@Configuration
public class ClientConfig {
@Bean
public RestTemplate restTemplate(
@Value("${app.http.connect-timeout:5000}") int connectTimeout,
@Value("${app.http.read-timeout:30000}") int readTimeout) {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofMillis(connectTimeout))
.setReadTimeout(Duration.ofMillis(readTimeout))
.build();
}
}Type Conversion
Spring automatically converts property string values to the declared field type. This covers all primitive types, their wrappers, arrays, lists, and many other types through Spring's ConversionService.
Java
@Component
public class TypeConversionExamples {
// Primitive types — automatic conversion from String:
@Value("${server.port:8080}")
private int port; // "8080" → int 8080
@Value("${app.max.connections:100}")
private long maxConnections; // "100" → long 100L
@Value("${app.tax.rate:0.18}")
private double taxRate; // "0.18" → double 0.18
@Value("${app.feature.enabled:true}")
private boolean featureEnabled; // "true" → boolean true
// Array — comma-separated values:
@Value("${app.allowed.origins:http://localhost:3000}")
private String[] allowedOrigins;
// "https://myapp.com,https://admin.myapp.com" → String[]{"https://myapp.com","https://admin.myapp.com"}
// List — also comma-separated:
@Value("${app.supported.currencies:USD,EUR,GBP}")
private List<String> supportedCurrencies;
// "USD,EUR,GBP" → List.of("USD", "EUR", "GBP")
// Duration (Spring 5.1+):
@Value("${app.cache.ttl:PT10M}")
private Duration cacheTtl; // "PT10M" → Duration.ofMinutes(10)
// Resource — loads a classpath or file resource:
@Value("classpath:templates/email/welcome.html")
private Resource welcomeEmailTemplate;
@Value("file:/etc/myapp/banner.txt")
private Resource bannerFile;
// Read resource content:
public String getEmailTemplate() throws IOException {
return new String(welcomeEmailTemplate.getInputStream().readAllBytes());
}
// Inject the entire Environment (not @Value but related):
@Autowired
private Environment environment;
// environment.getProperty("any.key", Integer.class, defaultValue)
}
// application.properties:
// app.allowed.origins=https://myapp.com,https://admin.myapp.com
// app.supported.currencies=USD,EUR,GBP,JPY
// app.cache.ttl=PT30MSpEL Expressions — #{...}
The Spring Expression Language (SpEL) syntax #{...} evaluates expressions at runtime — enabling computation, method calls, bean references, system property access, and conditional logic directly in @Value.
Java
@Component
public class SpelExamples {
// ── Literals ──────────────────────────────────────────────────────
@Value("#{42}")
private int constantValue; // literal integer 42
@Value("#{'Hello, World!'}")
private String greeting; // literal string
@Value("#{true}")
private boolean flag; // literal boolean
// ── Arithmetic ────────────────────────────────────────────────────
@Value("#{1024 * 1024 * 10}")
private long tenMegabytes; // 10485760
@Value("#{100 / 4 * 3}")
private int computation; // 75
// ── System properties ─────────────────────────────────────────────
@Value("#{systemProperties['java.home']}")
private String javaHome;
@Value("#{systemProperties['user.timezone']}")
private String timezone;
@Value("#{systemEnvironment['PATH']}")
private String systemPath;
// ── Combining ${} and #{} — property value used in SpEL ──────────
// Read a property, then compute with it:
@Value("#{'${app.jwt.expiration}' * 1000}")
private long expirationMillis; // property seconds × 1000 = milliseconds
// Split a property string into a list:
@Value("#{'${app.allowed.origins}'.split(',')}")
private List<String> allowedOrigins;
// Trim and uppercase a property value:
@Value("#{'${app.environment}'.trim().toUpperCase()}")
private String environment; // " prod " → "PROD"
// ── Bean references ───────────────────────────────────────────────
// Access another bean's property:
@Value("#{dataSource.username}")
private String dbUsername;
// Call a method on another bean:
@Value("#{userService.getAdminEmail()}")
private String adminEmail;
// ── Conditional (ternary) ─────────────────────────────────────────
@Value("#{'${app.env}' == 'prod' ? '${prod.db.url}' : '${dev.db.url}'}")
private String dbUrl;
@Value("#{systemProperties['os.name'].toLowerCase().contains('windows') ? 'C:/logs' : '/var/logs'}")
private String logPath;
// ── Collections ───────────────────────────────────────────────────
@Value("#{ {'USD', 'EUR', 'GBP'} }")
private List<String> currencies; // inline list
@Value("#{ {'key1': 'value1', 'key2': 'value2'} }")
private Map<String, String> config; // inline map
// ── Math utility ─────────────────────────────────────────────────
@Value("#{T(java.lang.Math).PI}")
private double pi; // 3.141592653589793
@Value("#{T(java.lang.Math).random()}")
private double random; // random value 0.0-1.0
@Value("#{T(java.lang.Runtime).getRuntime().availableProcessors()}")
private int cpuCount; // number of CPU cores
}Missing Properties and Required Values
When a property referenced by @Value is missing and no default is provided, Spring throws IllegalArgumentException at startup. This is the correct behavior — missing required configuration should fail fast.
Java
// Missing property — throws at startup:
@Value("${app.jwt.secret}")
private String secret;
// If app.jwt.secret is not defined:
// IllegalArgumentException: Could not resolve placeholder 'app.jwt.secret'
// With default — never throws:
@Value("${app.jwt.secret:default-secret-change-in-production}")
private String secret;
// Empty string default — property exists but may be empty:
@Value("${app.optional.feature:}")
private String optionalFeature; // "" (empty string) if not defined
// Null default via SpEL:
@Value("#{'${app.optional.key:}' ?: null}")
private String nullableValue; // null if property is empty or missing
// Validate required properties at startup with @PostConstruct:
@Component
public class ConfigValidator {
@Value("${app.jwt.secret:}")
private String jwtSecret;
@Value("${stripe.api.key:}")
private String stripeKey;
@PostConstruct
public void validateRequiredProperties() {
List<String> missing = new ArrayList<>();
if (jwtSecret.isBlank()) missing.add("app.jwt.secret");
if (stripeKey.isBlank()) missing.add("stripe.api.key");
if (!missing.isEmpty()) {
throw new IllegalStateException(
"Required properties not configured: " + missing
);
}
}
}
// Better approach — use @ConfigurationProperties with @Validated:
@ConfigurationProperties(prefix = "app.jwt")
@Validated
public class JwtProperties {
@NotBlank(message = "app.jwt.secret must be configured")
private String secret;
@Min(300)
private long expiration = 3600;
// getters and setters
}
// Spring validates this at startup — much cleaner than manual @PostConstruct@Value vs @ConfigurationProperties
Both @Value and @ConfigurationProperties inject configuration, but they serve different use cases. Understanding when to use each prevents over-engineering and inconsistent patterns.
Java
// ── USE @Value WHEN: ──────────────────────────────────────────────────
// 1. Injecting a single configuration value:
@Value("${server.port:8080}")
private int serverPort;
// 2. Using SpEL to compute a derived value:
@Value("#{'${app.jwt.expiration}' * 1000}")
private long expirationMillis;
// 3. Injecting a resource file:
@Value("classpath:templates/welcome.html")
private Resource emailTemplate;
// 4. Quick injection in a test:
@SpringBootTest
class MyTest {
@Value("${app.test.base-url}")
private String baseUrl;
}
// ── USE @ConfigurationProperties WHEN: ────────────────────────────────
// 1. Multiple related properties share a prefix — bind them as a group:
// Instead of this (@Value for each property):
@Value("${app.mail.host}") private String mailHost;
@Value("${app.mail.port:587}") private int mailPort;
@Value("${app.mail.username}") private String mailUsername;
@Value("${app.mail.password}") private String mailPassword;
@Value("${app.mail.ssl:true}") private boolean mailSsl;
// Use this (@ConfigurationProperties — one class, IDE auto-complete, validation):
@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
@NotBlank private String host;
@Min(1) @Max(65535) private int port = 587;
@NotBlank private String username;
private String password;
private boolean ssl = true;
// getters and setters
}
// 2. You need validation (@Validated with JSR-303 annotations):
// @Value has no built-in validation — you must validate manually
// @ConfigurationProperties validates at startup automatically
// 3. You want IDE auto-completion in application.properties:
// Add spring-boot-configuration-processor dependency
// IDE shows app.mail.host, app.mail.port etc. with documentation
// RULE OF THUMB:
// 1-2 properties: @Value
// 3+ related properties: @ConfigurationProperties@Value in Tests
@Value works in test classes and test configurations. @TestPropertySource lets you override or add properties specifically for a test without modifying the main configuration files.
Java
// @Value in a test class:
@SpringBootTest
class JwtTokenServiceTest {
@Value("${app.jwt.secret}")
private String jwtSecret; // reads from application.properties
@Value("${app.jwt.expiration}")
private long expiration;
@Autowired
private JwtTokenService jwtTokenService;
@Test
void generateToken_containsCorrectExpiration() {
String token = jwtTokenService.generateToken("user-123");
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
assertThat(claims.getExpiration()).isAfter(new Date());
}
}
// @TestPropertySource — override specific properties for a test:
@SpringBootTest
@TestPropertySource(properties = {
"app.jwt.secret=test-secret-key-for-unit-tests-only",
"app.jwt.expiration=60",
"app.jwt.issuer=https://test.example.com"
})
class JwtTokenServiceShortExpiryTest {
@Value("${app.jwt.expiration}")
private long expiration; // 60 — overridden by @TestPropertySource
@Autowired
private JwtTokenService jwtTokenService;
@Test
void token_expiresQuickly() {
// Test with 60-second expiry
String token = jwtTokenService.generateToken("user-1");
assertThat(token).isNotBlank();
}
}
// @TestPropertySource with a properties file:
@SpringBootTest
@TestPropertySource(locations = "classpath:test-config.properties")
class ExternalConfigTest { }
// ReflectionTestUtils — set @Value fields without Spring context:
@ExtendWith(MockitoExtension.class)
class UnitTestWithValueFields {
@InjectMocks
private JwtTokenService jwtTokenService;
@BeforeEach
void setUp() {
// Inject @Value fields manually in unit tests:
ReflectionTestUtils.setField(jwtTokenService, "secret", "test-secret");
ReflectionTestUtils.setField(jwtTokenService, "expirationSeconds", 3600L);
ReflectionTestUtils.setField(jwtTokenService, "issuer", "https://test.com");
}
@Test
void generateToken_returnsValidToken() {
String token = jwtTokenService.generateToken("user-1");
assertThat(token).isNotBlank();
}
}