Table of Contents#
- Understanding Factory Methods in Spring
- Why Use Annotation-Based Factory Methods?
- Prerequisites
- Setting Up the Project
- Step-by-Step Implementation
- Key Annotations Explained
- Verifying Singleton Scope
- Common Pitfalls and Best Practices
- Conclusion
- References
Understanding Factory Methods in Spring#
A factory method is a design pattern where a method (rather than a constructor) is responsible for creating and returning an instance of a class. In Spring, factory methods are used when:
- Bean instantiation requires complex logic (e.g., conditional setup, reading configuration, or legacy class integration).
- The bean type is determined dynamically at runtime.
- The target class lacks a no-arg constructor or has non-trivial initialization steps.
Historically, Spring supported factory methods via XML configuration (e.g., using <bean factory-method="..."/>), but modern Spring applications leverage annotation-based configuration with @Configuration and @Bean annotations. These annotations turn Java methods into Spring-managed factory methods, eliminating the need for XML.
Why Use Annotation-Based Factory Methods?#
- No XML Required: All configuration is done in Java, making it type-safe and easier to refactor.
- Simplified Dependency Management: Spring automatically injects dependencies into factory methods (e.g., other beans or property values).
- Singleton by Default: Beans created via
@Beanmethods are singletons by default, ensuring a single instance is reused throughout the application. - Integration with Spring Ecosystem: Seamlessly works with other annotations like
@Autowired,@Value, and@Profile.
Prerequisites#
To follow along, ensure you have:
- JDK 8 or higher
- Maven or Gradle (we’ll use Maven)
- A basic understanding of Spring IoC and dependency injection
Setting Up the Project#
Step 1: Create a Maven Project#
Add the following dependency to your pom.xml for Spring Core (which includes IoC container support):
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version> <!-- Use the latest stable version -->
</dependency>
</dependencies>Step-by-Step Implementation#
Let’s build a sample application where a UserRepository bean is created via a factory method (due to complex initialization logic), and then autowired into a UserService.
Step 1: Define the Target Bean Class#
First, create the UserRepository class. It has a non-trivial constructor requiring a database URL and a read-only flag, simulating complex initialization.
// UserRepository.java
public class UserRepository {
private final String dbUrl;
private final boolean isReadOnly;
// Complex constructor (not a no-arg constructor)
public UserRepository(String dbUrl, boolean isReadOnly) {
this.dbUrl = dbUrl;
this.isReadOnly = isReadOnly;
System.out.println("Initializing UserRepository with DB URL: " + dbUrl + ", Read-Only: " + isReadOnly);
}
// Getters for verification
public String getDbUrl() { return dbUrl; }
public boolean isReadOnly() { return isReadOnly; }
}Step 2: Create a Configuration Class with Factory Methods#
Next, create a @Configuration class to host the factory method for UserRepository. The @Bean annotation marks a method as a factory method, and Spring will manage the returned object as a bean.
// AppConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
@Configuration // Marks this class as a source of bean definitions
public class AppConfig {
// Factory method to create UserRepository (annotated with @Bean)
@Bean // Default bean name is the method name ("userRepository")
public UserRepository userRepository(
@Value("${db.url:jdbc:mysql://localhost:3306/mydb}") String dbUrl, // Inject property or use default
@Value("${db.readOnly:false}") boolean isReadOnly) { // Inject property or use default
// Complex initialization logic (e.g., validate URL, setup connections)
validateDbUrl(dbUrl);
// Return the UserRepository instance
return new UserRepository(dbUrl, isReadOnly);
}
// Helper method for validation (simulating complex logic)
private void validateDbUrl(String dbUrl) {
if (!dbUrl.startsWith("jdbc:")) {
throw new IllegalArgumentException("Invalid DB URL: Must start with 'jdbc:'");
}
}
}Key Details:#
@Configuration: Tells Spring this class contains bean definitions.@Bean: Declares a factory method. The method name (userRepository) is the default bean ID (override with@Bean(name = "customName")).@Value: Injects property values (fromapplication.properties, environment variables, etc.). Default values (after:) are used if the property is undefined.- Dependency Injection: The
userRepositorymethod accepts parameters (dbUrl,isReadOnly), which Spring injects automatically (supports other beans too—e.g., aDataSourcebean).
Step 3: Autowire the Factory-Created Bean#
Now, create a UserService that depends on UserRepository. Use @Autowired to inject the UserRepository bean created by our factory method.
// UserService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // Marks this as a Spring-managed service bean
public class UserService {
private final UserRepository userRepository;
// Constructor injection (preferred for dependencies)
@Autowired // Optional in Spring 4.3+ for single-arg constructors
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("UserService initialized with UserRepository");
}
// Example method using the repository
public void fetchUser() {
System.out.println("Fetching user from: " + userRepository.getDbUrl());
}
}Step 4: Bootstrap the Spring Context#
Finally, create a main class to start the Spring IoC container and verify the setup.
// Main.java
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// Initialize Spring context with annotation-based configuration
try (ConfigurableApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class)) {
// Retrieve UserService (which depends on UserRepository)
UserService userService = context.getBean(UserService.class);
userService.fetchUser();
// Verify singleton scope (see next section)
verifySingletonScope(context);
}
}
private static void verifySingletonScope(ConfigurableApplicationContext context) {
UserRepository repo1 = context.getBean(UserRepository.class);
UserRepository repo2 = context.getBean(UserRepository.class);
System.out.println("Are UserRepository instances the same? " + (repo1 == repo2)); // true
}
}Add Configuration Properties (Optional)#
Create src/main/resources/application.properties to customize db.url and db.readOnly:
db.url=jdbc:postgresql://prod-db:5432/users
db.readOnly=trueIf this file is missing, the @Value defaults (jdbc:mysql://localhost:3306/mydb and false) will be used.
Key Annotations Explained#
| Annotation | Purpose |
|---|---|
@Configuration | Marks a class as a source of bean definitions. Spring processes @Bean methods here. |
@Bean | Declares a factory method. Spring calls this method to create a bean, managing its lifecycle. |
@Value | Injects property values (from files, environment variables, etc.). Supports default values. |
@Autowired | Injects dependencies into constructors, fields, or methods. For UserService, it injects the UserRepository bean. |
@Service | A stereotype annotation marking UserService as a service-layer bean (auto-detected by Spring). |
Verifying Singleton Scope#
Run the Main class. The output will show:
Initializing UserRepository with DB URL: jdbc:postgresql://prod-db:5432/users, Read-Only: true
UserService initialized with UserRepository
Fetching user from: jdbc:postgresql://prod-db:5432/users
Are UserRepository instances the same? true
UserRepositoryis initialized once (due to singleton scope).repo1 == repo2confirms the same instance is reused.
Common Pitfalls and Best Practices#
Pitfalls:#
-
Missing
@Configuration: IfAppConfiglacks@Configuration,@Beanmethods behave as "lite" factories—calling them directly creates new instances (bypassing singleton checks). Always use@Configurationfor singleton guarantees. -
Overcomplicating Factory Methods: Avoid heavy logic in
@Beanmethods. Delegate complex setup to helper classes or use constructor injection for dependencies. -
Ignoring Default Scopes:
@Beanmethods default tosingletonscope. Use@Scope("prototype")only if multiple instances are needed (rare for most applications).
Best Practices:#
- Use Constructor Injection: Prefer constructor injection (as in
UserService) over field injection for immutability and testability. - Name Beans Explicitly: Use
@Bean(name = "customRepo")to avoid naming conflicts if multiple beans of the same type exist. - Document Factory Logic: Add comments to
@Beanmethods explaining why a factory is needed (e.g., "Complex DB setup for legacy system").
Conclusion#
Annotation-based factory methods in Spring (via @Configuration and @Bean) provide a powerful, XML-free way to create beans with custom initialization logic. By following this approach, you can:
- Leverage type-safe, maintainable configuration.
- Ensure singleton behavior for beans by default.
- Seamlessly integrate with Spring’s dependency injection ecosystem.
Whether you’re handling complex initialization, dynamic bean creation, or legacy code, annotation-based factory methods simplify bean management while keeping your codebase clean and modern.