Spring BootValue Annotation
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.com

Constructor 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=PT30M

SpEL 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();
    }
}