Spring Configuration & Profiles

Java-Based Configuration

18 min Lesson 2 of 13

Java-Based Configuration

Spring has supported multiple ways to define beans across its lifetime. The XML style came first, then annotations like @Component and @Autowired, and finally — with Spring 3.0 and refined heavily in Spring 4+ — Java-based configuration: plain Java classes annotated with @Configuration whose methods annotated with @Bean declare and wire beans in code. Today, in a Spring Boot 3 application, this is the dominant style and the one you need to own completely.

The @Configuration Class

A class annotated with @Configuration tells Spring: "treat this class as a source of bean definitions." Spring creates a CGLIB proxy of that class so it can intercept @Bean method calls and enforce the singleton contract (explained below). At its simplest:

import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; import jakarta.persistence.EntityManagerFactory; @Configuration public class AppConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } }

Spring calls passwordEncoder() once, stores the result in the application context under the bean name passwordEncoder (the method name by default), and returns that same instance to any component that injects PasswordEncoder.

Bean name defaults: The default name is the method name. You can override it: @Bean("encoder") or @Bean(name = {"encoder", "pwdEncoder"}) (aliases). The first name is canonical; the rest are aliases.

Declaring Dependencies Between Beans

The most readable way to express that bean A depends on bean B is to declare bean B as a parameter of the @Bean method for A. Spring resolves it by type from the context, exactly as it would with constructor injection:

@Configuration public class ServiceConfig { @Bean public UserRepository userRepository(DataSource dataSource) { return new JdbcUserRepository(dataSource); } @Bean public UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder) { return new UserServiceImpl(userRepository, passwordEncoder); } }

This style makes the dependency graph explicit and compile-time-visible. You can see at a glance what each bean needs without diving into a class's constructor.

The CGLIB Proxy and the Singleton Guarantee

Consider what happens if one @Bean method calls another directly:

@Configuration public class AppConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } @Bean public UserService userService() { // Calling another @Bean method directly: return new UserServiceImpl(passwordEncoder()); // safe — Spring intercepts this } }

Because Spring replaces the class with a CGLIB subclass, the call to passwordEncoder() inside userService() does not create a second BCryptPasswordEncoder. The proxy intercepts the call, checks whether a bean of that name already exists in the context, and returns the existing one. This preserves the singleton contract.

@Configuration vs @Component: If you annotate a config class with plain @Component instead of @Configuration, Spring does NOT create a CGLIB proxy. Inter-@Bean calls become plain Java method calls, creating a new instance each time. This is a common subtle bug — always use @Configuration for classes whose @Bean methods call each other.

Bean Scope

By default every @Bean is a singleton — one instance per application context. You change the scope with @Scope:

import org.springframework.context.annotation.Scope; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public ReportGenerator reportGenerator() { return new ReportGenerator(); }

Prototype means Spring creates a brand-new instance every time the bean is requested. Other built-in scopes (request, session) are used in web applications and backed by the servlet context.

Lifecycle Callbacks: initMethod and destroyMethod

You can hook into a bean's lifecycle without the bean class knowing about Spring at all:

@Bean(initMethod = "connect", destroyMethod = "disconnect") public MessageBrokerClient brokerClient() { MessageBrokerClient client = new MessageBrokerClient(); client.setHost("localhost"); client.setPort(5672); return client; }

Spring calls connect() after the bean is fully constructed and its properties set, and disconnect() when the context closes. This keeps third-party classes free of Spring dependencies while still hooking them into the lifecycle properly.

Prefer @Bean lifecycle attributes for third-party classes. For your own classes, @PostConstruct and @PreDestroy (from jakarta.annotation) on the bean class itself are cleaner — the intent is right there in the code, and they work regardless of how the bean was declared (component scan or explicit @Bean).

@Primary and @Qualifier

When multiple beans of the same type exist in the context, Spring needs guidance. Use @Primary to mark the default choice, or rely on @Qualifier at the injection site for explicit selection:

@Bean @Primary public DataSource primaryDataSource() { // HikariCP pool for the main DB return buildPool("jdbc:postgresql://primary:5432/app"); } @Bean public DataSource reportingDataSource() { // Separate read-replica pool return buildPool("jdbc:postgresql://replica:5432/app"); }

When you inject DataSource without further qualification, Spring picks primaryDataSource. To inject the replica explicitly: @Qualifier("reportingDataSource") at the injection point.

Organizing Configuration: Multiple @Configuration Classes

A real application has dozens of beans. Splitting configuration by concern keeps files focused and maintainable:

// DatabaseConfig.java — everything DataSource and JPA related @Configuration public class DatabaseConfig { /* @Bean methods ... */ } // SecurityConfig.java — security beans @Configuration public class SecurityConfig { /* @Bean methods ... */ } // MessagingConfig.java — Kafka / RabbitMQ / SQS beans @Configuration public class MessagingConfig { /* @Bean methods ... */ }

Spring Boot discovers all @Configuration classes reachable from the package of the @SpringBootApplication class through component scanning. There is no master list to maintain — just place them in the right package.

Java Config vs XML vs @Component Scan — When to Use Each

  • Java-based @Configuration: best for infrastructure beans (data sources, HTTP clients, caches, message brokers, security beans) and for third-party objects your code does not own. The factory logic is visible, testable Java code.
  • @Component / @Service / @Repository: best for your own application classes — services, repositories, controllers. Less ceremony; the class declares itself a bean.
  • XML: legacy; avoid for new code. You may still encounter it when integrating with older Spring modules or enterprise libraries that ship XML namespaces.

In practice you mix all three. Spring Boot auto-configuration (the @AutoConfiguration classes in every starter JAR) is almost exclusively written as Java-based configuration, which is why understanding @Configuration and @Bean deeply lets you read and override anything the framework sets up for you.

Summary

Java-based configuration gives you type-safe, IDE-navigable bean definitions in plain Java. Annotate a class with @Configuration so Spring proxies it, declare each bean as a @Bean method, and express dependencies as method parameters. Use @Scope to change a bean's lifetime, initMethod/destroyMethod to hook lifecycle events for third-party classes, and @Primary/@Qualifier to resolve ambiguity. Split large configurations across multiple focused classes — component scanning finds them all. In the next lesson we look at how component scanning itself works so you understand exactly which classes Spring picks up and when.