Table of Contents#
- Prerequisites
- Understanding ServiceLocatorFactoryBean and the Factory Pattern
- Step-by-Step Implementation
- Common Pitfalls and Best Practices
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have:
- Basic knowledge of Java and Spring Core (dependency injection,
@Service,@Configuration). - Familiarity with the Factory Design Pattern.
- A Java IDE (IntelliJ IDEA, Eclipse, etc.).
- Maven or Gradle (we’ll use Maven here).
- JDK 11 or higher.
Understanding ServiceLocatorFactoryBean and the Factory Pattern#
The Factory Pattern Recap#
The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. In Spring, this translates to a "service factory" that dynamically returns the right service implementation based on input (e.g., a string, enum, or condition).
What is ServiceLocatorFactoryBean?#
ServiceLocatorFactoryBean is a Spring-specific factory bean that acts as a dynamic service locator. It generates a proxy for a user-defined "locator interface" and uses Spring’s BeanFactory to resolve and return the appropriate service bean by name at runtime.
Key Benefits:#
- Dynamic Resolution: Unlike
@Autowired(which injects dependencies at startup),ServiceLocatorFactoryBeanresolves services on-demand, making it ideal for scenarios where the required service isn’t known until runtime. - Loose Coupling: Decouples the client from concrete service implementations; clients depend only on the locator interface.
- Annotation Support: Modern Spring applications can configure it via JavaConfig (no XML required!).
When to Use It?#
Use ServiceLocatorFactoryBean when:
- You need to select a service implementation dynamically (e.g., payment processors, notification services).
- You want to avoid hardcoding dependencies with
@Qualifieror@Resource. - You need to manage a family of related services (e.g., different report generators).
Step-by-Step Implementation#
Let’s build a practical example: a payment processing system that supports multiple payment methods (e.g., Credit Card, PayPal, Bitcoin). The factory will return the appropriate PaymentProcessor based on a user’s input (e.g., "creditCard", "paypal").
Step 1: Set Up the Project#
We’ll use Spring Boot for simplicity (it auto-configures the BeanFactory and reduces boilerplate).
Maven pom.xml#
Add these dependencies to your pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>service-locator-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-locator-demo</name>
<description>Demo project for ServiceLocatorFactoryBean with Annotations</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Core -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> Step 2: Define Service Interface and Implementations#
First, create a PaymentProcessor interface for all payment methods:
// com/example/servicelocator/service/PaymentProcessor.java
package com.example.servicelocator.service;
public interface PaymentProcessor {
String processPayment(double amount);
} Next, implement concrete payment processors. Annotate each with @Service and assign a unique bean name (this name will be used by the locator to resolve the service):
Credit Card Processor#
// com/example/servicelocator/service/CreditCardPaymentProcessor.java
package com.example.servicelocator.service;
import org.springframework.stereotype.Service;
@Service("creditCard") // Bean name: "creditCard"
public class CreditCardPaymentProcessor implements PaymentProcessor {
@Override
public String processPayment(double amount) {
return String.format("Processing $%.2f via Credit Card. Transaction ID: %s",
amount, generateTransactionId());
}
private String generateTransactionId() {
return "CC-" + System.currentTimeMillis();
}
} PayPal Processor#
// com/example/servicelocator/service/PayPalPaymentProcessor.java
package com.example.servicelocator.service;
import org.springframework.stereotype.Service;
@Service("paypal") // Bean name: "paypal"
public class PayPalPaymentProcessor implements PaymentProcessor {
@Override
public String processPayment(double amount) {
return String.format("Processing $%.2f via PayPal. Transaction ID: %s",
amount, generateTransactionId());
}
private String generateTransactionId() {
return "PP-" + System.currentTimeMillis();
}
} Bitcoin Processor (Optional)#
Add another processor to extend the example:
// com/example/servicelocator/service/BitcoinPaymentProcessor.java
package com.example.servicelocator.service;
import org.springframework.stereotype.Service;
@Service("bitcoin") // Bean name: "bitcoin"
public class BitcoinPaymentProcessor implements PaymentProcessor {
@Override
public String processPayment(double amount) {
return String.format("Processing $%.2f via Bitcoin. Transaction ID: %s",
amount, generateTransactionId());
}
private String generateTransactionId() {
return "BTC-" + System.currentTimeMillis();
}
} Step 3: Create the Service Locator Interface#
Define a locator interface with a method to fetch the PaymentProcessor by name. Spring will generate a proxy for this interface using ServiceLocatorFactoryBean.
// com/example/servicelocator/locator/PaymentProcessorLocator.java
package com.example.servicelocator.locator;
import com.example.servicelocator.service.PaymentProcessor;
public interface PaymentProcessorLocator {
// Method to get PaymentProcessor by bean name (e.g., "creditCard", "paypal")
PaymentProcessor getPaymentProcessor(String processorName);
} Note: The method name (getPaymentProcessor) and parameter (String processorName) are critical. Spring uses this method to resolve the bean by name.
Step 4: Configure ServiceLocatorFactoryBean with Annotations#
Now, configure ServiceLocatorFactoryBean via JavaConfig. Create a @Configuration class to define the factory bean:
// com/example/servicelocator/config/ServiceLocatorConfig.java
package com.example.servicelocator.config;
import com.example.servicelocator.locator.PaymentProcessorLocator;
import org.springframework.beans.factory.config.ServiceLocatorFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServiceLocatorConfig {
@Bean
public ServiceLocatorFactoryBean paymentProcessorLocator() {
ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
// Set the locator interface (Spring will generate a proxy for this)
factoryBean.setServiceLocatorInterface(PaymentProcessorLocator.class);
return factoryBean;
}
} What’s Happening?
ServiceLocatorFactoryBeanis registered as a Spring bean.setServiceLocatorInterfacetells Spring to generate a proxy forPaymentProcessorLocator, which will use Spring’sBeanFactoryto resolvePaymentProcessorbeans by name.
Step 5: Inject and Use the Service Locator#
Create a PaymentService to use the locator and process payments dynamically:
// com/example/servicelocator/service/PaymentService.java
package com.example.servicelocator.service;
import com.example.servicelocator.locator.PaymentProcessorLocator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final PaymentProcessorLocator paymentProcessorLocator;
// Inject the locator (Spring injects the proxy generated by ServiceLocatorFactoryBean)
@Autowired
public PaymentService(PaymentProcessorLocator paymentProcessorLocator) {
this.paymentProcessorLocator = paymentProcessorLocator;
}
public String processPayment(String processorType, double amount) {
// Use the locator to fetch the processor by name (e.g., "creditCard")
PaymentProcessor processor = paymentProcessorLocator.getPaymentProcessor(processorType);
return processor.processPayment(amount);
}
} Key Point: The PaymentProcessorLocator is injected like any other Spring bean. Spring provides the proxy implementation at runtime.
Step 6: Test the Implementation#
Let’s verify the setup with a test. Use JUnit 5 and Spring Boot Test to inject PaymentService and test dynamic resolution.
// com/example/servicelocator/ServiceLocatorDemoApplicationTests.java
package com.example.servicelocator;
import com.example.servicelocator.service.PaymentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ServiceLocatorDemoApplicationTests {
@Autowired
private PaymentService paymentService;
@Test
void testCreditCardPayment() {
String result = paymentService.processPayment("creditCard", 99.99);
assertTrue(result.contains("Credit Card"));
assertTrue(result.contains("CC-")); // Transaction ID starts with CC-
}
@Test
void testPayPalPayment() {
String result = paymentService.processPayment("paypal", 149.99);
assertTrue(result.contains("PayPal"));
assertTrue(result.contains("PP-"));
}
@Test
void testBitcoinPayment() {
String result = paymentService.processPayment("bitcoin", 299.99);
assertTrue(result.contains("Bitcoin"));
assertTrue(result.contains("BTC-"));
}
@Test
void testUnknownProcessor() {
assertThrows(IllegalStateException.class, () ->
paymentService.processPayment("applePay", 49.99)
);
}
} Explanation:
- Tests verify that the correct
PaymentProcessoris returned for "creditCard", "paypal", and "bitcoin". - The
testUnknownProcessorchecks that an exception is thrown for an invalid processor name (since no bean named "applePay" exists).
Common Pitfalls and Best Practices#
Pitfalls to Avoid#
-
Incorrect Bean Names: The
@Servicebean name must match the string passed to the locator (e.g.,@Service("creditCard")andlocator.getPaymentProcessor("creditCard")). Mismatched names causeNoSuchBeanDefinitionException. -
Locator Interface Method Signature: The locator method must return the service interface (e.g.,
PaymentProcessor) and take a singleStringparameter (the bean name). Invalid signatures break proxy generation. -
Missing Service Implementations: If a processor type is requested but no
@Servicewith that name exists, Spring throwsNoSuchBeanDefinitionException. Always validate input! -
Overusing ServiceLocator: Avoid using it for static dependencies. Stick to
@Autowiredwhen the service is known at startup.
Best Practices#
-
Use Enums for Safety: Replace raw strings with enums (e.g.,
PaymentMethod.CREDIT_CARD) to avoid typos. Convert the enum to a string (e.g.,paymentMethod.name().toLowerCase()) when calling the locator.public enum PaymentMethod { CREDIT_CARD, PAYPAL, BITCOIN } // In PaymentService: public String processPayment(PaymentMethod method, double amount) { return paymentProcessorLocator.getPaymentProcessor(method.name().toLowerCase()) .processPayment(amount); } -
Add Fallback Mechanism: Handle unknown processor types gracefully (e.g., return a default processor or throw a custom exception).
-
Document the Locator Interface: Clearly document the expected bean names (e.g., "Valid processor names: 'creditCard', 'paypal', 'bitcoin'").
Conclusion#
ServiceLocatorFactoryBean is a powerful tool for implementing the Factory Pattern in Spring with annotations. It enables dynamic service resolution, reduces coupling, and aligns with modern JavaConfig practices. By following this guide, you’ve learned how to:
- Define service interfaces and implementations.
- Create a locator interface for dynamic resolution.
- Configure
ServiceLocatorFactoryBeanwithout XML. - Inject and use the locator to fetch services at runtime.
Use this pattern to build flexible, maintainable applications where service implementations need to be swapped or selected dynamically!