Complete guide to Spring Beans: learn how Spring manages bean configuration, lifecycle, scopes, and dependencies. Master the art of defining and configuring beans in Spring applications.
Table of Contents
1. What are Spring Beans?
A Spring Bean is an object that is instantiated, assembled, and managed by the Spring IoC container. Beans are the backbone of any Spring application - they represent the components that make up your application and are managed by Spring's container.
The Spring container is responsible for:
- Creating bean instances
- Resolving dependencies between beans
- Injecting dependencies into beans
- Managing bean lifecycle (creation, initialization, destruction)
- Providing beans to your application when requested
In essence, any Java object that Spring manages is a bean. Beans are defined through configuration metadata (XML, annotations, or Java code) and Spring uses this metadata to create and wire beans together.
2. Bean Definition
A Bean Definition contains configuration metadata that tells Spring how to create, configure, and manage a bean. This metadata includes:
- Class: The fully qualified class name of the bean
- Name/ID: Unique identifier for the bean
- Scope: Bean scope (singleton, prototype, etc.)
- Constructor Arguments: Dependencies to inject via constructor
- Properties: Dependencies to inject via setters
- Initialization Method: Method to call after bean creation
- Destruction Method: Method to call before bean destruction
2.1 Bean Naming
Beans can be identified by name or ID. If not explicitly specified, Spring generates a default name based on the class name (with first letter lowercase).
// Explicit naming
@Component("userService")
public class UserService {
}
// Default naming (becomes "userService")
@Component
public class UserService {
}
// Multiple names using @Qualifier
@Component
@Qualifier("primaryUserService")
public class UserService {
}
3. Bean Configuration
Spring supports three main approaches for defining beans: XML configuration, annotation-based configuration, and Java-based configuration.
The following diagram shows the different configuration approaches:
3.1 XML Configuration
Traditional XML-based configuration provides explicit bean definitions:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Simple bean definition -->
<bean id="userRepository" class="com.example.repository.UserRepositoryImpl"/>
<!-- Bean with constructor injection -->
<bean id="userService" class="com.example.service.UserService">
<constructor-arg ref="userRepository"/>
</bean>
<!-- Bean with property injection -->
<bean id="emailService" class="com.example.service.EmailService">
<property name="smtpHost" value="smtp.example.com"/>
<property name="smtpPort" value="587"/>
</bean>
<!-- Bean with scope -->
<bean id="prototypeBean" class="com.example.PrototypeBean" scope="prototype"/>
</beans>
3.2 Annotation Configuration
Annotation-based configuration uses Java annotations to mark classes as Spring beans:
// Enable component scanning
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}
// Define beans using stereotypes
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
@Repository
public class UserRepositoryImpl implements UserRepository {
// Implementation
}
@Component
public class UtilityService {
// Utility component
}
@Controller
public class UserController {
@Autowired
private UserService userService;
}
3.3 Java-Based Configuration
Java-based configuration uses @Configuration classes with @Bean methods:
@Configuration
public class AppConfig {
@Bean
public UserRepository userRepository() {
return new UserRepositoryImpl();
}
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
@Primary
public PaymentService creditCardPaymentService() {
return new CreditCardPaymentService();
}
@Bean
public PaymentService paypalPaymentService() {
return new PayPalPaymentService();
}
}
4. Bean Lifecycle
Understanding the bean lifecycle is crucial for managing resources and ensuring proper initialization and cleanup. The Spring container manages the complete lifecycle of beans from creation to destruction.
The following diagram illustrates the complete bean lifecycle:
4.1 Lifecycle Overview
The bean lifecycle consists of several stages:
- Instantiation: Container creates bean instance
- Population: Container injects dependencies
- Initialization: Custom initialization logic executes
- Ready: Bean is ready for use
- Destruction: Custom cleanup logic executes before destruction
4.1 Lifecycle Callbacks
Spring provides multiple ways to hook into the bean lifecycle:
4.1.1 Using @PostConstruct and @PreDestroy
@Component
public class DatabaseConnection {
private Connection connection;
@PostConstruct
public void init() {
// Called after dependency injection, before bean is ready
System.out.println("Initializing database connection");
connection = createConnection();
}
@PreDestroy
public void cleanup() {
// Called before bean destruction
System.out.println("Closing database connection");
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
// Handle exception
}
}
}
private Connection createConnection() {
// Create connection logic
return null;
}
}
4.1.2 Implementing InitializingBean and DisposableBean
@Component
public class ResourceManager implements InitializingBean, DisposableBean {
private Resource resource;
@Override
public void afterPropertiesSet() throws Exception {
// Called after all properties are set
System.out.println("Initializing resource");
resource = acquireResource();
}
@Override
public void destroy() throws Exception {
// Called before bean destruction
System.out.println("Releasing resource");
if (resource != null) {
releaseResource(resource);
}
}
private Resource acquireResource() {
// Acquire resource logic
return null;
}
private void releaseResource(Resource resource) {
// Release resource logic
}
}
4.1.3 Using init-method and destroy-method
public class FileProcessor {
private File file;
public void initialize() {
System.out.println("Initializing file processor");
file = new File("data.txt");
}
public void cleanup() {
System.out.println("Cleaning up file processor");
// Cleanup logic
}
}
// XML configuration
<bean id="fileProcessor"
class="com.example.FileProcessor"
init-method="initialize"
destroy-method="cleanup"/>
// Java configuration
@Configuration
public class AppConfig {
@Bean(initMethod = "initialize", destroyMethod = "cleanup")
public FileProcessor fileProcessor() {
return new FileProcessor();
}
}
4.2 Initialization Order
Initialization callbacks are called in this order:
- Constructor
- Dependency injection (setters/fields)
- BeanPostProcessor.postProcessBeforeInitialization()
- @PostConstruct or InitializingBean.afterPropertiesSet()
- Custom init-method
- BeanPostProcessor.postProcessAfterInitialization()
4.3 Destruction Order
Destruction callbacks are called in this order:
- @PreDestroy
- DisposableBean.destroy()
- Custom destroy-method
5. Bean Scopes
Bean scope determines the lifecycle and visibility of a bean instance. Spring supports several scopes:
5.1 Singleton Scope (Default)
One instance per Spring container. This is the default scope.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class SingletonService {
// One instance shared across entire application
}
// Or simply
@Component
public class SingletonService {
// Default is singleton
}
5.2 Prototype Scope
New instance created every time the bean is requested.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeService {
// New instance each time
}
// Usage
@Autowired
private ApplicationContext context;
public void usePrototype() {
PrototypeService service1 = context.getBean(PrototypeService.class);
PrototypeService service2 = context.getBean(PrototypeService.class);
// service1 != service2 (different instances)
}
5.3 Request Scope (Web)
One instance per HTTP request. Only available in web applications.
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class RequestScopedBean {
// New instance for each HTTP request
}
5.4 Session Scope (Web)
One instance per HTTP session. Only available in web applications.
@Component
@Scope(WebApplicationContext.SCOPE_SESSION)
public class SessionScopedBean {
// One instance per user session
}
5.5 Application Scope (Web)
One instance per ServletContext. Only available in web applications.
@Component
@Scope(WebApplicationContext.SCOPE_APPLICATION)
public class ApplicationScopedBean {
// One instance per web application
}
6. Bean Wiring
Bean wiring is the process of connecting beans together by injecting dependencies. Spring supports various wiring strategies.
6.1 Constructor Injection
@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentService paymentService;
public OrderService(OrderRepository repository,
PaymentService paymentService) {
this.repository = repository;
this.paymentService = paymentService;
}
}
6.2 Setter Injection
@Service
public class NotificationService {
private EmailService emailService;
private SmsService smsService;
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
}
6.3 Field Injection
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
@Autowired
@Qualifier("primaryPaymentService")
private PaymentService paymentService;
}
6.4 Autowiring Modes
Spring supports different autowiring modes:
- byType: Matches by type
- byName: Matches by bean name
- constructor: Constructor-based autowiring
- no: No autowiring (default)
@Service
@Autowired(required = false) // Optional dependency
public class OptionalService {
private OptionalDependency dependency;
}
@Service
public class RequiredService {
@Autowired(required = true) // Required dependency (default)
private RequiredDependency dependency;
}
6.5 Qualifier for Multiple Beans
public interface PaymentService {
void processPayment(BigDecimal amount);
}
@Service("creditCard")
public class CreditCardService implements PaymentService {
// Implementation
}
@Service("paypal")
public class PayPalService implements PaymentService {
// Implementation
}
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Qualifier("creditCard") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
7. Real-World Examples
7.1 Example 1: Database Connection Bean
@Configuration
public class DatabaseConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(),
id
);
}
}
7.2 Example 2: Bean with Lifecycle Management
@Component
public class CacheManager implements InitializingBean, DisposableBean {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing cache manager");
scheduler = Executors.newScheduledThreadPool(1);
// Schedule cache cleanup every hour
scheduler.scheduleAtFixedRate(
this::cleanupExpiredEntries,
1, 1, TimeUnit.HOURS
);
}
@Override
public void destroy() throws Exception {
System.out.println("Shutting down cache manager");
if (scheduler != null) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
cache.clear();
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
private void cleanupExpiredEntries() {
// Cleanup logic
}
}
7.3 Example 3: Conditional Bean Creation
@Configuration
public class FeatureConfig {
@Bean
@ConditionalOnProperty(name = "feature.email.enabled", havingValue = "true")
public EmailService emailService() {
return new EmailServiceImpl();
}
@Bean
@ConditionalOnProperty(name = "feature.sms.enabled", havingValue = "true")
public SmsService smsService() {
return new SmsServiceImpl();
}
@Bean
@ConditionalOnClass(name = "com.example.ExternalService")
public ExternalServiceAdapter externalServiceAdapter() {
return new ExternalServiceAdapter();
}
@Bean
@Profile("dev")
public DevelopmentDataSource developmentDataSource() {
return new H2DataSource();
}
@Bean
@Profile("prod")
public ProductionDataSource productionDataSource() {
return new PostgresDataSource();
}
}
7.4 Example 4: Factory Bean Pattern
public class ConnectionFactoryBean implements FactoryBean<Connection> {
private String url;
private String username;
private String password;
private boolean singleton = true;
@Override
public Connection getObject() throws Exception {
return DriverManager.getConnection(url, username, password);
}
@Override
public Class<?> getObjectType() {
return Connection.class;
}
@Override
public boolean isSingleton() {
return singleton;
}
// Setters
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setSingleton(boolean singleton) {
this.singleton = singleton;
}
}
// Configuration
@Configuration
public class DatabaseConfig {
@Bean
public FactoryBean<Connection> connectionFactory() {
ConnectionFactoryBean factory = new ConnectionFactoryBean();
factory.setUrl("jdbc:postgresql://localhost:5432/mydb");
factory.setUsername("user");
factory.setPassword("password");
return factory;
}
}
8. Best Practices
8.1 Use Constructor Injection
Prefer constructor injection for required dependencies as it ensures immutability and makes dependencies explicit.
8.2 Initialize Resources in @PostConstruct
Use @PostConstruct for initialization logic that depends on injected dependencies.
8.3 Clean Up Resources in @PreDestroy
Always clean up resources (connections, threads, files) in @PreDestroy methods.
8.4 Use Appropriate Scopes
Choose the right scope for your beans. Use singleton for stateless services, prototype for stateful objects.
8.5 Avoid Circular Dependencies
Design your beans to avoid circular dependencies. If necessary, use setter injection or refactor the design.
8.6 Use @Primary for Default Implementations
@Service
@Primary
public class DefaultPaymentService implements PaymentService {
// This will be injected when no qualifier is specified
}
@Service
public class AlternativePaymentService implements PaymentService {
// Requires @Qualifier to inject
}
9. Testing
Spring provides excellent support for testing beans in isolation and in integration tests.
9.1 Unit Testing with Mocks
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUser() {
User expectedUser = new User(1L, "John");
when(userRepository.findById(1L)).thenReturn(expectedUser);
User result = userService.getUser(1L);
assertEquals(expectedUser, result);
}
}
9.2 Integration Testing
@SpringJUnitConfig(AppConfig.class)
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
void testCreateUser() {
User user = new User(null, "Jane");
userService.createUser(user);
assertNotNull(user.getId());
}
}
9.3 Testing Bean Lifecycle
@SpringJUnitConfig(AppConfig.class)
class BeanLifecycleTest {
@Autowired
private ApplicationContext context;
@Test
void testBeanInitialization() {
CacheManager cacheManager = context.getBean(CacheManager.class);
assertNotNull(cacheManager);
// Verify initialization occurred
}
@Test
void testBeanDestruction() {
ConfigurableApplicationContext ctx =
(ConfigurableApplicationContext) context;
CacheManager cacheManager = ctx.getBean(CacheManager.class);
ctx.close(); // Triggers @PreDestroy
// Verify cleanup occurred
}
}
10. Advanced Concepts
10.1 BeanPostProcessor
BeanPostProcessor allows you to intercept and modify beans during initialization.
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// Called before @PostConstruct
if (bean instanceof Auditable) {
((Auditable) bean).setCreatedAt(LocalDateTime.now());
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// Called after @PostConstruct
if (bean instanceof Loggable) {
System.out.println("Bean initialized: " + beanName);
}
return bean;
}
}
10.2 BeanFactoryPostProcessor
BeanFactoryPostProcessor allows you to modify bean definitions before beans are instantiated.
@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinition definition = beanFactory.getBeanDefinition("userService");
// Modify bean definition
definition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
}
}
10.3 Custom Scope
You can create custom scopes for special requirements.
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadScope =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadScope.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return threadScope.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// Register cleanup callback
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return String.valueOf(Thread.currentThread().getId());
}
}
// Register custom scope
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new ThreadScope());
return configurer;
}
}
0 Comments