@Configuration and @Bean
Declare beans in code with @Bean methods when you need construction logic or classes you cannot annotate, and replace Spring Boot's defaults.
@Configuration(proxyBeanMethods = false)
class Config {
@Bean Type name(Dependency dep) { return new Type(dep); }
}@Component works for your own classes. For classes from libraries, which you cannot annotate, and for objects that need setting up before use, write a @Bean method in a @Configuration class. The method's return value becomes the bean, its name becomes the bean name, and its parameters are injected like constructor parameters.
@Configuration(proxyBeanMethods = false)
class TimeConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
}When to use which
Use @Component on your own service and repository classes. Use @Bean for a Clock, a configured HttpClient, a third-party SDK client, or when the same class should exist twice with different settings. Keep configuration classes small and grouped by concern, such as HttpClientConfig or SecurityConfig, rather than one giant AppConfig.
proxyBeanMethods
By default Spring subclasses every @Configuration class so that when one @Bean method calls another, the call returns the existing singleton instead of building a second object. That only matters if your methods call each other. Prefer declaring the dependency as a method parameter, and set proxyBeanMethods = false: no subclass is generated, startup is slightly cheaper, and Spring Boot's own auto-configuration is written this way. If you turn it off and still call another @Bean method directly, you silently get a new, unmanaged instance.
Replacing Spring Boot's defaults
Most auto-configured beans are guarded by @ConditionalOnMissingBean. Declare your own bean of the same type and Boot's version steps aside. This is the intended way to customise behaviour, and it is why reading the auto-configuration report (--debug) is worthwhile when a bean you expected is missing or a default is still in place.
Do not try to override a bean by declaring one with the same name. Spring Boot disables bean definition overriding by default, so the duplicate name stops startup with an error. That is deliberate: an accidental override is far harder to spot than a failed boot.
Things that surprise people
- A
@Beanmethod that returns an interface type hides the concrete type from type matching until the bean is created. Return the most specific type that callers need. @Beanmethods that produce aBeanFactoryPostProcessorshould bestatic, so creating them does not force the configuration class to be built too early.- A
@Beanmethod inside a plain@Componentworks, but is never proxied, so calls between methods always create new objects.
For registering beans in a loop or from runtime conditions, Spring Framework 7 adds the BeanRegistrar interface, imported with @Import on a configuration class.
Example
import java.net.http.HttpClient;
import java.time.Clock;
import java.time.Duration;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
(proxyBeanMethods = false)
public class InfrastructureConfig {
Clock clock() {
return Clock.systemUTC();
}
// A JDK class: we cannot put @Component on it, so we build it here.
HttpClient partnerHttpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.followRedirects(HttpClient.Redirect.NEVER)
.executor(Executors.newVirtualThreadPerTaskExecutor())
.build();
}
// Parameters are resolved from the context, like constructor arguments.
PartnerGateway partnerGateway(HttpClient partnerHttpClient, Clock clock) {
return new PartnerGateway(partnerHttpClient, clock, "https://partner.example.com");
}
}
class PartnerGateway {
private final HttpClient client;
private final Clock clock;
private final String baseUrl;
PartnerGateway(HttpClient client, Clock clock, String baseUrl) {
this.client = client;
this.clock = clock;
this.baseUrl = baseUrl;
}
String describe() {
return baseUrl + " at " + clock.instant() + " via " + client.version();
}
}
When to use it
- Expose a Clock bean so services can be tested with Clock.fixed instead of the system time.
- Build a JDK or vendor SDK client with timeouts and share one instance across the application.
- Replace an auto-configured default by declaring your own bean of the same type.
More examples
A default that applications can replace
This is the pattern Spring Boot's auto-configuration uses throughout. Shared libraries can supply sensible defaults this way, and an application or a test that declares its own Clock, such as Clock.fixed for repeatable tests, wins automatically. Note that @ConditionalOnMissingBean is reliable in auto-configuration classes; in ordinary configuration classes its result depends on the order definitions were read.
import java.time.Clock;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ClockDefaults {
// Created only if nobody else has declared a Clock bean.
@Bean
@ConditionalOnMissingBean
Clock clock() {
return Clock.systemUTC();
}
}Two beans of the same class with different settings
Each method name becomes a bean name, so the context now holds fastClient and patientClient, both of type HttpClient. Injecting HttpClient by type is now ambiguous, and you must pick one with @Qualifier or by parameter name. The next lessons cover both.
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class HttpClients {
@Bean
HttpClient fastClient() {
return HttpClient.newBuilder().connectTimeout(Duration.ofMillis(500)).build();
}
@Bean
HttpClient patientClient() {
return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
}
}Registering beans programmatically with BeanRegistrar
A @Bean method declares exactly one bean, so a variable number of beans needs code. BeanRegistrar receives a registry and the Environment and can register as many definitions as it likes, each with a name, a supplier and options such as lazy or primary. Reach for it rarely; a single bean holding a Map of tenants is usually simpler.
import java.util.List;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
@Configuration(proxyBeanMethods = false)
@Import(TenantRegistrar.class)
public class TenantConfig {
}
class TenantRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
List<String> tenants = List.of(env.getProperty("app.tenants", "acme,globex").split(","));
for (String tenant : tenants) {
String name = tenant.trim();
registry.registerBean(name + "Ledger", Ledger.class, spec -> spec
.description("Ledger for tenant " + name)
.supplier(context -> new Ledger(name)));
}
}
}
class Ledger {
private final String tenant;
Ledger(String tenant) {
this.tenant = tenant;
}
String tenant() {
return tenant;
}
}
Discussion