Spring Beans

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:

graph TB subgraph "Configuration Approaches" A[Spring Container] end subgraph "XML Configuration" B[beans.xml] -->|defines| C[Bean Definitions] C --> A end subgraph "Annotation Configuration" D[Annotations: Component/Service/Repository] -->|scanned| E[Component Scanner] E --> A end subgraph "Java Configuration" F[Configuration Classes with Bean Methods] -->|processed| G[Configuration Processor] G --> A end A --> H[Bean Registry] H --> I[Bean Instances] style A fill:#e1f5ff,stroke:#0273bd,stroke-width:3px style B fill:#fff4e1,stroke:#f57c00,stroke-width:2px style D fill:#fff4e1,stroke:#f57c00,stroke-width:2px style F fill:#fff4e1,stroke:#f57c00,stroke-width:2px style H fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style I fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

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:

graph TD A[Container Starts] --> B[Load Bean Definitions] B --> C[Instantiate Bean] C --> D[Populate Properties] D --> E[BeanNameAware.setBeanName] E --> F[BeanFactoryAware.setBeanFactory] F --> G[ApplicationContextAware.setApplicationContext] G --> H[BeanPostProcessor.postProcessBeforeInitialization] H --> I[@PostConstruct] I --> J[InitializingBean.afterPropertiesSet] J --> K[Custom init-method] K --> L[BeanPostProcessor.postProcessAfterInitialization] L --> M[Bean Ready for Use] M --> N[Application Running] N --> O[Container Shutdown] O --> P[@PreDestroy] P --> Q[DisposableBean.destroy] Q --> R[Custom destroy-method] R --> S[Bean Destroyed] style A fill:#e1f5ff,stroke:#0273bd,stroke-width:2px style C fill:#fff4e1,stroke:#f57c00,stroke-width:2px style D fill:#fff4e1,stroke:#f57c00,stroke-width:2px style M fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px style N fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style O fill:#ffebee,stroke:#c62828,stroke-width:2px style S fill:#ffebee,stroke:#c62828,stroke-width:2px

4.1 Lifecycle Overview

The bean lifecycle consists of several stages:

  1. Instantiation: Container creates bean instance
  2. Population: Container injects dependencies
  3. Initialization: Custom initialization logic executes
  4. Ready: Bean is ready for use
  5. 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:

  1. Constructor
  2. Dependency injection (setters/fields)
  3. BeanPostProcessor.postProcessBeforeInitialization()
  4. @PostConstruct or InitializingBean.afterPropertiesSet()
  5. Custom init-method
  6. BeanPostProcessor.postProcessAfterInitialization()

4.3 Destruction Order

Destruction callbacks are called in this order:

  1. @PreDestroy
  2. DisposableBean.destroy()
  3. 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;
    }
}

11. Conclusion

Spring Beans are the fundamental building blocks of Spring applications. Understanding bean configuration, lifecycle, scopes, and wiring is essential for building robust and maintainable Spring applications.

By mastering bean definitions, lifecycle callbacks, scopes, and dependency injection patterns, you can leverage Spring's powerful IoC container to create loosely coupled, testable, and maintainable applications. The flexibility of Spring's configuration options (XML, annotations, Java-based) allows you to choose the approach that best fits your project's needs.

As you continue your Spring journey, remember that beans are managed by the Spring container, which handles their creation, dependency injection, lifecycle, and destruction automatically, allowing you to focus on your business logic.

Post a Comment

0 Comments