Unlock Spring Boot’s Hidden Power‑Ups: Conditional Beans, ConfigProps, Actuator & More
Explore the hidden power‑ups of Spring Boot—including @Conditional, @ConfigurationProperties, Actuator, DevTools, Retry, Cache, testing strategies, custom starters, Admin UI, and CLI—through concise explanations and complete code examples that demonstrate how to boost development efficiency and build production‑ready applications.
1. @Conditional Annotation
Some developers need to load different bean configurations for different environments. While @Profile can handle simple cases, @Conditional provides more flexible control.
Basic usage
@Configuration
public class DataSourceConfig {
@Bean
@Conditional(ProdDataSourceCondition.class)
public DataSource prodDataSource() {
// Production environment data source
return DataSourceBuilder.create()
.url("jdbc:mysql://prod-host:3306/app")
.username("prod-user")
.password("prod-pass")
.build();
}
@Bean
@Conditional(DevDataSourceCondition.class)
public DataSource devDataSource() {
// Development environment data source
return DataSourceBuilder.create()
.url("jdbc:h2:mem:testdb")
.username("sa")
.password("")
.build();
}
}
// Production environment condition
public class ProdDataSourceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String env = context.getEnvironment().getProperty("app.env");
return "prod".equals(env);
}
}
// Development environment condition
public class DevDataSourceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String env = context.getEnvironment().getProperty("app.env");
return "dev".equals(env) || env == null;
}
}Advanced usage: combined conditions
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnDatabaseTypeCondition.class)
public @interface ConditionalOnDatabaseType {
String value();
}
public class OnDatabaseTypeCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnDatabaseType.class.getName());
String expectedType = (String) attributes.get("value");
String actualType = context.getEnvironment().getProperty("app.db.type");
return expectedType.equals(actualType);
}
}
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnDatabaseType("redis")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}
@Bean
@ConditionalOnDatabaseType("caffeine")
public CacheManager caffeineCacheManager() {
return new CaffeineCacheManager();
}
}Deep analysis: The core value of @Conditional is conditional configuration, which underpins Spring Boot’s auto‑configuration mechanism. By implementing the Condition interface, developers can decide bean registration based on any criteria such as environment variables, classpath presence, or bean existence.
2. @ConfigurationProperties
Many developers still inject configuration properties one by one with @Value. @ConfigurationProperties offers a more elegant, type‑safe solution.
Basic binding
@Component
@ConfigurationProperties(prefix = "app.datasource")
@Validated
public class DataSourceProperties {
@NotBlank
private String url;
@NotBlank
private String username;
private String password;
@Min(1)
@Max(100)
private int maxPoolSize = 10;
private Duration connectionTimeout = Duration.ofSeconds(30);
// Nested configuration
private Pool pool = new Pool();
public static class Pool {
private int maxSize = 20;
private int minIdle = 5;
// getters and setters omitted
}
// getters and setters omitted
}
# application.yml
app:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: secret
max-pool-size: 20
connection-timeout: 60s
pool:
max-size: 50
min-idle: 10Type‑safe configuration
@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(properties.getUrl());
dataSource.setUsername(properties.getUsername());
dataSource.setPassword(properties.getPassword());
dataSource.setMaximumPoolSize(properties.getMaxPoolSize());
dataSource.setConnectionTimeout(properties.getConnectionTimeout().toMillis());
return dataSource;
}
}Deep analysis: @ConfigurationProperties not only provides type‑safe binding, but also supports nested properties, collections, validation, and relaxed binding (e.g., kebab‑case to camelCase). It embodies Spring Boot’s “convention over configuration” philosophy.
3. Spring Boot Actuator
Production monitoring is essential for system stability. Actuator offers ready‑made monitoring endpoints.
Core endpoint configuration
@Configuration
public class ActuatorConfig {
// Custom health check
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(1000)) {
return Health.up()
.withDetail("database", "Available")
.withDetail("validationQuery", "SUCCESS")
.build();
}
} catch (SQLException e) {
return Health.down(e)
.withDetail("database", "Unavailable")
.withDetail("error", e.getMessage())
.build();
}
return Health.unknown().build();
}
}
// Custom metric
@Component
public class OrderMetrics {
private final Counter orderCounter;
private final DistributionSummary orderAmountSummary;
public OrderMetrics(MeterRegistry registry) {
this.orderCounter = Counter.builder("order.count")
.description("Total number of orders")
.register(registry);
this.orderAmountSummary = DistributionSummary.builder("order.amount")
.description("Order amount distribution")
.baseUnit("USD")
.register(registry);
}
public void recordOrder(Order order) {
orderCounter.increment();
orderAmountSummary.record(order.getAmount().doubleValue());
}
}
// Management endpoint exposure configuration (application.yml)
// management:
// endpoints:
// web:
// exposure:
// include: health,info,metrics,prometheus
// endpoint:
// health:
// show-details: always
// show-components: always
// metrics:
// enabled: true
}Custom info endpoint
@Component
public class BuildInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
Map<String, String> buildDetails = new HashMap<>();
buildDetails.put("version", "1.0.0");
buildDetails.put("timestamp", Instant.now().toString());
buildDetails.put("commit", getGitCommit());
builder.withDetail("build", buildDetails)
.withDetail("environment", getEnvironmentInfo());
}
private String getGitCommit() {
try {
return new String(Files.readAllBytes(Paths.get("git.properties")));
} catch (IOException e) {
return "unknown";
}
}
}Deep analysis: Actuator is more than a monitoring tool; it provides full observability—health checks, metrics, audit events, HTTP tracing—allowing teams to build a complete application‑monitoring ecosystem.
4. Spring Boot DevTools
Some developers still restart the application manually to see code changes. DevTools offers an extreme development experience.
Hot reload configuration
// application-dev.yml
spring:
devtools:
restart:
enabled: true
exclude: static/**,public/**
additional-paths: src/main/java
livereload:
enabled: true
thymeleaf:
cache: false
freemarker:
cache: false
@Component
public class CustomRestartTrigger implements ApplicationListener<ClassPathChangedEvent> {
private final RestartScope restartScope;
public CustomRestartTrigger(RestartScope restartScope) {
this.restartScope = restartScope;
}
@Override
public void onApplicationEvent(ClassPathChangedEvent event) {
if (event.getChangeSet().isModified()) {
restartScope.clear();
System.out.println("Detected classpath change, preparing restart...");
}
}
}Development‑time bean overrides
@Profile("dev")
@Configuration
public class DevConfig {
@Bean
public SomeService someService() {
// Return mock or dev‑specific implementation
return new MockSomeService();
}
}Deep analysis: DevTools uses class‑loader tricks to achieve rapid restarts, while also providing LiveReload, global settings, and property overrides that dramatically improve developer productivity.
5. Spring Retry
In distributed systems, network glitches and temporary service outages are normal. Spring Retry offers a declarative retry solution.
Basic retry configuration
@Service
public class PaymentService {
@Retryable(
value = {PaymentException.class, NetworkException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2))
public PaymentResult processPayment(PaymentRequest request) {
// Call payment gateway
return paymentGateway.process(request);
}
@Recover
public PaymentResult recover(PaymentException e, PaymentRequest request) {
log.error("Payment processing failed, entering recovery logic", e);
return PaymentResult.failed("Payment temporarily unavailable");
}
}
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
return RetryTemplate.builder()
.maxAttempts(5)
.exponentialBackoff(1000, 2, 10000)
.retryOn(RemoteAccessException.class)
.traversingCauses()
.build();
}
}Advanced retry strategy
@Component
public class CircuitBreakerRetryListener extends RetryListenerSupport {
private final CircuitBreaker circuitBreaker;
public CircuitBreakerRetryListener() {
this.circuitBreaker = CircuitBreaker.ofDefaults("payment-service");
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
// Record failure, may trigger circuit breaker
circuitBreaker.onError(throwable);
if (circuitBreaker.tryAcquirePermission()) {
log.warn("Retry failed, but circuit breaker still allows attempts");
} else {
log.error("Retry failed, circuit breaker opened, stopping retries");
context.setExhaustedOnly(); // Mark as exhausted
}
}
}Deep analysis: Spring Retry’s core lies in its flexible retry policies and back‑off mechanisms. Using @Retryable and @Recover, developers can declaratively handle transient faults and improve system resilience.
6. Spring Cache
Some developers still manage caches manually. Spring Cache provides a unified caching abstraction.
Multiple cache‑manager configuration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
@Primary
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(Collections.singletonMap(
"users",
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1))))
.transactionAware()
.build();
}
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(1000));
return cacheManager;
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// Database query
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#username", cacheManager = "caffeineCacheManager")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "users", key = "#user.username")
})
public void deleteUser(User user) {
userRepository.delete(user);
}
}Deep analysis: Spring Cache’s value is in providing a uniform abstraction layer, allowing seamless switching between implementations (Redis, Caffeine, Ehcache, etc.) while keeping business code clean.
7. Spring Boot Test
Testing is key to code quality. Spring Boot Test offers comprehensive testing support.
Layered testing strategy
// 1. Unit test – no Spring context
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenExists() {
User expected = new User(1L, "john");
when(userRepository.findById(1L)).thenReturn(Optional.of(expected));
User actual = userService.getUserById(1L);
assertThat(actual).isEqualTo(expected);
verify(userRepository).findById(1L);
}
}
// 2. Slice test – only part of the context
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void shouldFindByUsername() {
User user = new User(null, "john", "[email protected]");
entityManager.persistAndFlush(user);
User found = userRepository.findByUsername("john");
assertThat(found.getEmail()).isEqualTo("[email protected]");
}
}
// 3. Integration test – full container
@SpringBootTest
@ActiveProfiles("test")
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private EmailService emailService;
@Test
void shouldCreateUserAndSendEmail() {
UserCreateRequest request = new UserCreateRequest("john", "[email protected]");
doNothing().when(emailService).sendWelcomeEmail(anyString());
User user = userService.createUser(request);
assertThat(user.getUsername()).isEqualTo("john");
verify(emailService).sendWelcomeEmail("[email protected]");
}
@Test
void shouldReturnUserViaRest() {
ResponseEntity<User> response = restTemplate.getForEntity("/users/1", User.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
}Test configuration optimization
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public DataSource testDataSource() {
// Use H2 in‑memory database for tests
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:test-schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}Deep analysis: Spring Boot Test’s core value lies in its layered testing philosophy. By selecting the appropriate test slice, teams can balance test speed and coverage, achieving efficient and reliable verification.
8. Spring Boot Starter
Some developers want to package reusable functionality. Creating a custom starter is the best practice.
Creating a custom starter
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyServiceProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyServiceProperties properties) {
return new MyService(properties);
}
@Bean
@ConditionalOnProperty(name = "my.service.metrics.enabled", havingValue = "true")
public MyServiceMetrics myServiceMetrics() {
return new MyServiceMetrics();
}
}
@ConfigurationProperties(prefix = "my.service")
public class MyServiceProperties {
private String endpoint = "http://localhost:8080";
private Duration timeout = Duration.ofSeconds(30);
private int maxConnections = 100;
// getters and setters omitted
}
// spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.myservice.MyServiceAutoConfigurationConditional bean configuration
@Configuration
public class ConditionalBeans {
@Bean
@ConditionalOnWebApplication
public WebSpecificBean webSpecificBean() {
return new WebSpecificBean();
}
@Bean
@ConditionalOnNotWebApplication
public NonWebBean nonWebBean() {
return new NonWebBean();
}
@Bean
@ConditionalOnBean(DataSource.class)
public DataSourceAwareBean dataSourceAwareBean() {
return new DataSourceAwareBean();
}
}Deep analysis: Custom starters are the core mechanism for extending the Spring Boot ecosystem, enabling plug‑and‑play modules that boost code reuse.
9. Spring Boot Admin
Although Actuator provides endpoints, Spring Boot Admin offers a friendlier management UI.
Server configuration
@Configuration
@EnableAdminServer
public class AdminServerConfig {
@Bean
public Notifier notifier() {
return new RemindingNotifier(
new FilteringNotifier(new LoggingNotifier(),
instanceEvent -> instanceEvent.getType() == StatusChangeEvent.TYPE),
AdminServerNotifier::shouldNotify,
Duration.ofMinutes(10));
}
}Client configuration
@Configuration
public class AdminClientConfig {
@Bean
public SecurityContext securityContext() {
return SecurityContext.builder()
.username("admin")
.password("secret")
.build();
}
}10. Spring Boot CLI
For rapid prototyping, Spring Boot CLI delivers an ultra‑fast development experience.
CLI example
# Create a simple web app
echo '@RestController class App { @RequestMapping("/") String home() { "Hello World" } }' > app.groovy
# Run the app
spring run app.groovy
# Add a dependency
spring install com.example:my-starter:1.0.0
# Package the app
spring jar myapp.jar *.groovyCustom CLI command
@Component
@Order(0)
public class MyCommand implements CommandLineRunner {
private final ApplicationContext context;
public MyCommand(ApplicationContext context) {
this.context = context;
}
@Override
public void run(String... args) throws Exception {
if (args.length > 0 && "init".equals(args[0])) {
System.out.println("Initializing application...");
initializeDatabase();
loadSampleData();
}
}
private void initializeDatabase() {
// Database initialization logic
}
}Deep analysis: Spring Boot CLI dramatically lowers the entry barrier by allowing developers to write Groovy scripts and manage dependencies automatically, letting them focus on business logic instead of configuration.
Conclusion
Spring Boot’s core philosophy can be summarized as:
Convention over configuration – sensible defaults and auto‑configuration free developers from boilerplate.
Modular design – each starter is a self‑contained feature that can be added on demand.
Production‑ready – monitoring, health checks, metrics, and management tools are built‑in.
Developer‑friendly – DevTools, CLI, and layered testing improve productivity and experience.
Understanding and applying these “hidden gems” can multiply development efficiency, improve code quality, and ensure system stability.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
