9 Hidden Spring Boot 3 Annotations That Supercharge Your Apps
This article introduces nine often‑overlooked Spring Boot 3 annotations—including @ConditionalOnExpression, @ConstructorBinding, @EventListener, @Lazy, @EnableConfigurationProperties, @Profile, @Scheduled, @Scope, and @Retryable—explaining their purpose, showing practical code examples, and demonstrating how they improve configuration, immutability, event handling, circular‑dependency resolution, and fault tolerance.
1. Introduction
In everyday development we often master only the basic Spring Boot annotations and overlook powerful "treasure" annotations that can dramatically improve code quality, type safety, and flexibility. This article selects nine practical yet easily missed annotations covering conditional configuration, event‑driven programming, immutable configuration, and other core scenarios to help you write more elegant and robust code.
2. Practical Cases
2.1 @ConditionalOnExpression
Compared with the simpler @ConditionalOnProperty, @ConditionalOnExpression leverages SpEL to combine multiple factors such as properties and method results, enabling far more complex configuration logic.
@Configuration
@ConditionalOnExpression("${app.cache.enabled:false} and '${spring.profiles.active}' != 'test'")
public class CacheConfiguration {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}The example creates a conditional cache configuration that activates only when two conditions are met: ${app.cache.enabled:false}: checks whether caching is enabled in application.properties (defaults to false). '${spring.profiles.active}' != 'test': ensures the current environment is not the test profile.
You can also base the condition on other bean methods:
@Configuration
@ConditionalOnExpression("@featureChecker.isAdvancedFeatureAvailable()")
public class AdvancedFeatureConfig {
}The configuration becomes active only when the featureChecker bean’s isAdvancedFeatureAvailable() returns true.
2.2 @ConstructorBinding for Immutable Configuration
Combining @ConfigurationProperties with @ConstructorBinding creates immutable configuration classes, enhancing security, thread‑safety, and maintainability.
@ConfigurationProperties(prefix = "pack.app")
@ConstructorBinding
public class AppProperties {
private final Boolean enabled;
private final String apiKey;
private final String title;
private final String version;
public AppProperties(Boolean enabled, String apiKey, String title, String version) {
this.enabled = enabled;
this.apiKey = apiKey;
this.title = title;
this.version = version;
}
// getters only
} @ConstructorBindingensures all properties are set during object construction.
Using final fields prevents accidental modification after initialization. List.copyOf() can create immutable copies of collections.
Absence of setters guarantees immutability.
2.3 @EventListener for Advanced Event Handling
@EventListeneroffers sophisticated event‑driven capabilities, allowing conditional processing based on event attributes.
@Component
public class OrderProcessorListener {
@EventListener(condition = "#event.total > 1000")
@Order(1)
public void processLargeOrder(OrderCreatedEvent event) {
logger.info("处理订单【{}】, 总金额: {}", event.getOrderId(), event.getTotal());
// TODO
}
}The condition attribute filters events so that only orders with a total amount greater than 1000 are processed.
2.4 @Lazy to Resolve Circular Dependencies
@Service
public class CartService {
private final OrderService orderService;
public CartService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
public void checkout(Long cartId) {
// handle checkout
orderService.createOrder(cartId);
}
}
@Service
public class OrderService {
private final CartService cartService;
public OrderService(CartService cartService) {
this.cartService = cartService;
}
public void cancelOrder(Long orderId) {
// restore cart
cartService.restoreCart(orderId);
}
}CartService depends on OrderService and vice versa. @Lazy creates a proxy to break the circular dependency.
The actual OrderService bean is instantiated only on first use.
Use cautiously; circular dependencies often indicate design issues.
2.5 @EnableConfigurationProperties for Automatic Property Binding
@EnableConfigurationProperties({DatabaseProperties.class, CacheProperties.class})
@Configuration
public class AppConfig {
@Bean
DataSource dataSource(DatabaseProperties props) {
return DataSourceBuilder.create()
.url(props.getUrl())
.username(props.getUsername())
.password(props.getPassword())
.build();
}
} @EnableConfigurationPropertiesregisters multiple property classes at once.
Properties are automatically injected into beans that need them.
Provides a centralized location for configuration management.
2.6 @Profile for Configuration Expressions
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new ProductionDataSource();
}
@Bean
@Profile("dev | test")
public DataSource devDataSource() {
return new EmbeddedDataSource();
} @Profile("prod")activates when spring.profiles.active=prod. @Profile("dev | test") activates when the active profile is either dev or test.
These annotations decide which configuration beans are loaded based on the spring.profiles.active property.
2.7 Task Scheduling Configuration
@Configuration
public class SchedulingConfig {
@Scheduled(cron = "${jobs.cleanup.schedule}")
public void databaseCleanup() {
// cleanup logic
}
@Scheduled(fixedDelayString = "${jobs.health.interval}")
public void healthCheck() {
// health check logic
}
}Both methods can be configured via external properties, allowing different schedules per environment without code changes.
2.8 @Scope for Request‑Scoped Beans
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserContext {
private User currentUser;
public void setCurrentUser(User user) {
this.currentUser = user;
}
public User getCurrentUser() {
return currentUser;
}
}A new instance is created for each HTTP request. proxyMode ensures proper isolation when injected into singleton beans.
Ideal for storing user‑specific data during request processing.
The instance is automatically cleared after the request completes.
2.9 @Retryable for Automatic Retries
@Service
public class PaymentGatewayService {
@Retryable(
value = {TimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PaymentResult processPayment(Payment payment) {
// TODO
}
@Recover
public PaymentResult handlePaymentFailure(TimeoutException e, Payment payment) {
return PaymentResult.failed(payment.getId());
}
}Automatically retries on TimeoutException up to three attempts.
Uses exponential backoff (1 s, 2 s, 4 s). @Recover provides fallback logic when retries are exhausted.
Dependency
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
