Method Injection
In most application scenarios, beans in a Spring container are often singletons. This means that only one instance of a bean is created and shared throughout the application. However, sometimes you may need a singleton bean to collaborate with a non-singleton (prototype) bean. For example, you may want a singleton bean to use a fresh instance of a prototype bean every time a method is invoked.
The Problem
When a singleton bean (A) depends on a prototype bean (B), the container creates the singleton bean only once and injects the prototype dependency into it. However, this means the same instance of the prototype bean will be reused, which may not meet your requirements if a new instance of the prototype bean is needed for every method call.
Traditional Solution (Coupling with Spring)
One way to solve this is to make the singleton bean aware of the Spring container. By implementing
the ApplicationContextAware interface, the singleton bean can directly request a new instance of the
prototype bean from the container whenever needed:
package fiona.apple;
import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) { // Get a new instance of the prototype bean Command command = createCommand(); command.setState(commandState); return command.execute(); }
protected Command createCommand() { // Fetch a new instance from the container return this.applicationContext.getBean("command", Command.class); }
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}Why This is Undesirable
While this approach works, it introduces tight coupling between your business logic and the Spring Framework, making your code less portable and harder to test.
Method Injection: A Cleaner Approach
Method Injection is an advanced feature of the Spring IoC container that allows you to cleanly handle such scenarios without coupling your code to Spring.
Lookup Method Injection
Lookup Method Injection enables the container to override specific methods in a bean to dynamically
return another bean from the container. This is particularly useful when the dependent bean
(e.g., myCommand) is a prototype.
Key Requirements
- Class Restrictions: The class should not be
final, and the method to be injected must also not befinal. - Abstract Method: If the method is abstract, the container provides the implementation dynamically.
How Does It Work?
The Spring Framework uses bytecode generation with the CGLIB library to dynamically create a
subclass of the target bean. This subclass overrides the lookup method (e.g., createCommand()) and
provides the desired bean from the Spring container. Since this subclass is generated at runtime, it
allows Spring to seamlessly integrate the lookup behavior without modifying the original class.
Example: XML Configuration
<!-- Define a prototype-scoped bean --><bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype"> <!-- Configure dependencies for myCommand here --></bean>
<!-- Define a singleton bean with a lookup method --><bean id="commandManager" class="fiona.apple.CommandManager"> <lookup-method name="createCommand" bean="myCommand" /></bean>In this configuration:
- The
commandManagerbean calls its owncreateCommand()method whenever it needs a new instance of themyCommandprototype bean. - The
createCommand()method is dynamically overridden by the Spring container.
Example: Abstract Lookup Method with Implementation
-
Define the
Commandinterface and its implementation:public interface Command {void setState(Object state);Object execute();}public class AsyncCommand implements Command {private Object state;@Overridepublic void setState(Object state) {this.state = state;}@Overridepublic Object execute() {return "Executing with state: " + state;}} -
Define the abstract
CommandManager:public abstract class CommandManager {public Object process(Object commandState) {Command command = createCommand();command.setState(commandState);return command.execute();}@Lookup("myCommand")protected abstract Command createCommand();} -
Spring Configuration:
@Configurationpublic class AppConfig {@Bean@Scope("prototype")public Command myCommand() {return new AsyncCommand();}@Beanpublic CommandManager commandManager() {return new CommandManager() {@Overrideprotected Command createCommand() {// Placeholder stub; will be overridden by Spring.return null;}};}} -
Using the Bean:
public class Main {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);CommandManager manager = context.getBean(CommandManager.class);Object result = manager.process("Sample State");System.out.println(result);context.close();}}
Notes
- Annotated lookup methods should typically have concrete stub implementations to ensure compatibility with component scanning.
Alternative: ObjectFactory or Provider Injection
You can use ObjectFactory or javax.inject.Provider to retrieve a new instance of a prototype bean
as needed:
@Componentpublic class CommandManager {
@Autowired private ObjectFactory<Command> commandFactory;
public Object process(Object commandState) { Command command = commandFactory.getObject(); command.setState(commandState); return command.execute(); }}ServiceLocatorFactoryBean
Another option is to use ServiceLocatorFactoryBean from the org.springframework.beans.factory.config
package, which dynamically creates a service locator for accessing beans.
Arbitrary Method Replacement
A more flexible but less common form of method injection is arbitrary method replacement, where you
replace an existing method with a new implementation using the MethodReplacer interface.
Example: Replacing a Method
public class MyValueCalculator { public String computeValue(String input) { // Original implementation }}
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { String input = (String) args[0]; // New implementation logic return ...; }}XML Configuration
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> <replaced-method name="computeValue" replacer="replacementComputeValue"> <arg-type>String</arg-type> </replaced-method></bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue" />Notes on Arbitrary Method Replacement
- Use
<arg-type>to specify method arguments when dealing with overloaded methods. - This approach is rarely needed but can be useful for highly dynamic scenarios.
Real Use Case: Handling Dynamic DataSource Resolution Using Method Injection
In scenarios where a dynamic DataSource is required based on an identifier (e.g., appId), method
injection using @Lookup might not be the best approach because @Lookup-based methods require
a no-arguments signature. For such cases, leveraging ObjectProvider or a factory method is a more
appropriate solution.
Why Lookup Method Injection Has Limitations
The Spring documentation specifies that methods intended for lookup injection must follow this format:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);This restriction exists because Spring dynamically overrides the method using a runtime-generated
subclass to provide the desired bean. The no-arguments requirement ensures that Spring can replace
the method without needing additional runtime logic to handle arguments.
For scenarios requiring runtime parameters (like appId), lookup method injection does not
directly apply. Instead, you can use ObjectProvider or a factory pattern to dynamically resolve
dependencies based on runtime values.
Using ObjectProvider for Dynamic Bean Resolution
ObjectProvider is a more flexible version of ObjectFactory. It allows for more advanced functionality such as checking if a bean is available in the application context and obtaining a bean lazily. It also supports passing arguments.
Key Characteristics:
- Can return optional or multiple beans.
- It provides additional methods like
getIfAvailable()orgetIfUnique()to handle scenarios where the bean might not exist or where multiple beans of the same type exist. - Supports lazy initialization and is more powerful for complex dependency injection scenarios.
1. Define a DataSourceManager to Resolve Beans
import org.springframework.beans.factory.ObjectProvider;import org.springframework.stereotype.Component;import javax.sql.DataSource;
@Componentpublic class DataSourceManager {
private final ObjectProvider<DataSource> dataSourceProvider;
public DataSourceManager(ObjectProvider<DataSource> dataSourceProvider) { this.dataSourceProvider = dataSourceProvider; }
public DataSource getDataSource(String appId) { return dataSourceProvider.getObject(appId); }}2. Configure the DataSource Bean
Define a DataSource bean with a prototype scope that accepts the appId as an argument.
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Scope;
@Configurationpublic class DataSourceConfig {
@Bean @Scope("prototype") public DataSource appDataSource(String appId) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:vertica://host:port/database_" + appId); dataSource.setUsername("username"); dataSource.setPassword("password"); return dataSource; }}3. Use the DataSourceManager
Now you can dynamically resolve the DataSource using the appId at runtime.
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
import javax.sql.DataSource;import java.sql.Connection;
@Servicepublic class DemoService {
private final DataSourceManager dataSourceManager;
@Autowired public DemoService(DataSourceManager dataSourceManager) { this.dataSourceManager = dataSourceManager; }
public void performOperation(String appId) { DataSource dataSource = dataSourceManager.getDataSource(appId);
try (Connection connection = dataSource.getConnection()) { // Perform database operations } catch (Exception e) { e.printStackTrace(); } }}Key Takeaways
- Lookup Method Injection Limitations:
- The injected method must have a
no-argumentssignature. - It is suitable when no runtime parameters are required for the dependency resolution.
- ObjectProvider Advantages:
- Allows dynamic resolution of beans with runtime arguments.
- Provides flexibility and supports prototype-scoped beans effectively.
- Prototype Scope:
- Ensure beans like
DataSourceare defined with aprototypescope to get a new instance for each request when required.
By using ObjectProvider or factory methods, you can elegantly handle scenarios like dynamically
resolving DataSource based on appId while maintaining clean and decoupled code.