Spring Boot starters are a powerful mechanism for packaging and distributing reusable configurations, but understanding how they actually inject those configurations can feel like magic.
Let’s see it in action. Imagine we have a simple service that needs a MessageService.
// src/main/java/com/example/myservice/MyService.java
package com.example.myservice;
public class MyService {
private final String message;
public MyService(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
Now, we want to create a starter that automatically configures this MyService when it’s on the classpath.
First, we need to define our auto-configuration class. This class will contain the logic to create our MyService bean.
// src/main/java/com/example/myautoconfigure/MyAutoConfiguration.java
package com.example.myautoconfigure;
import com.example.myservice.MyService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(MyServiceProperties.class) // Enables binding properties
@ConditionalOnProperty(name = "my.service.enabled", havingValue = "true", matchIfMissing = true) // Only if property is set or missing
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // Only create if no other MyService bean exists
public MyService myService(MyServiceProperties properties) {
return new MyService(properties.getMessage());
}
}
Notice the @ConditionalOnProperty and @ConditionalOnMissingBean annotations. These are crucial for controlling when this auto-configuration is applied. @ConditionalOnProperty ensures our starter only activates if a specific property (my.service.enabled) is set to true (or not set at all, thanks to matchIfMissing = true). @ConditionalOnMissingBean prevents our auto-configuration from overriding a MyService bean that a user might have explicitly defined.
We also need a MyServiceProperties class to bind external configuration.
// src/main/java/com/example/myautoconfigure/MyServiceProperties.java
package com.example.myautoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my.service")
public class MyServiceProperties {
private String message = "Default Message"; // Default value
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
This MyServiceProperties class will allow users to configure the message via application.properties or application.yml using the prefix my.service.
To make Spring Boot discover our auto-configuration, we need to create a META-INF/spring.factories file in our starter project.
# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.myautoconfigure.MyAutoConfiguration
This file tells Spring Boot which auto-configuration classes to load.
Now, in a separate application that depends on our starter (let’s call it my-starter and my-app), we can simply add the starter as a dependency.
<!-- my-app's pom.xml -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
When my-app starts, Spring Boot will scan its classpath, find META-INF/spring.factories, and load MyAutoConfiguration. Because my.service.enabled is likely true by default (or not set, triggering matchIfMissing), and assuming no other MyService bean is defined, Spring Boot will execute the @Bean method in MyAutoConfiguration, creating a MyService bean.
We can then inject and use it:
// src/main/java/com/example/myapp/MyApplication.java
package com.example.myapp;
import com.example.myservice.MyService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(MyApplication.class, args);
MyService myService = context.getBean(MyService.class);
System.out.println("Message from MyService: " + myService.getMessage());
context.close();
}
}
If we add my.service.message=Hello from Application! to src/main/resources/application.properties in my-app, the output will be "Message from MyService: Hello from Application!". If we don’t set the property, it will be "Message from MyService: Default Message".
The real power of starters isn’t just about providing beans; it’s about providing conditional and configurable beans. The @Conditional annotations are what allow starters to be intelligent. They don’t blindly enable configurations; they check the environment, existing beans, and properties. This makes starters flexible and prevents conflicts. The EnableConfigurationProperties annotation is the bridge between your starter’s auto-configuration and the application’s external properties, allowing for fine-grained control without requiring code changes in the consuming application.
The META-INF/spring.factories file is the discovery mechanism, but it’s also a potential point of failure if misconfigured. If you have multiple starters, you’ll see multiple entries separated by commas in that file.
The next challenge is managing multiple related auto-configurations within a single starter, often controlled by a single parent @Configuration class.