В большинстве сценариев применения большинство бинов в контейнере являются одиночками (singletons). Если бину-одиночке необходимо взаимодействовать с другим бином-одиночкой или бину, не являющемуся одиночкой, нужно взаимодействовать с другим бином, не являющимся одиночкой, то, как правило, работа с зависимостью заключается в определении одного бина как свойства другого. Проблема возникает, если жизненные циклы бинов различны. Предположим, что бин-одиночка A должен использовать бин, не являющийся одиночкой (прототип) B, например, при каждом обращении к методу в A. Контейнер создает бин-одиночку A только единожды и, таким образом, возможность задать свойства появляется только единожды. Контейнер не может предоставлять бину A новый экземпляр бина B каждый раз, когда он необходим.
Решение состоит в том, чтобы отказаться в определенной степени от инверсии контроля. Вы можете сделать так, чтобы бин A был оповещен о контейнере, реализовав интерфейс ApplicationContextAware
и выполнив вызов getBean("B")
для контейнера, запрашивая (как правило, новый) экземпляр бина B каждый раз, когда это требуется бину A. В следующем примере продемонстрирован этот подход:
// класс, который использует класс стиля Command с сохранением состояния для выполнения определенной обработки
package fiona.apple;
// Spring-API imports
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) {
// создаем новый экземпляр соответствующего Command
Command command = createCommand();
// устанавливаем состояние для (как ожидается, совершенно нового) экземпляра Command
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// обратите внимание на зависимость от Spring API!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
// класс, который использует класс стиля Command с сохранением состояния для выполнения определенной обработки
package fiona.apple
// Spring-API imports
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
class CommandManager : ApplicationContextAware {
private lateinit var applicationContext: ApplicationContext
fun process(commandState: Map<*, *>): Any {
// создаем новый экземпляр соответствующего Command
val command = createCommand()
// устанавливаем состояние для (как ожидается, совершенно нового) экземпляра Command
command.state = commandState
return command.execute()
}
// обратите внимание на зависимость от Spring API!
protected fun createCommand() =
applicationContext.getBean("command", Command::class.java)
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
}
Предыдущий вариант нежелателен, поскольку бизнес-код (слоя предметной области) оповещен о фреймворке Spring и связан с ним. Внедрение зависимости через метод (Method Injection) - это несколько расширенная функция IoC-контейнера Spring, которая позволяет чисто работать с данным случаем использования.
Внедрение зависимости через метод поиска
Внедрение зависимости через метод поиска - это способность контейнера переопределять методы управляемых контейнером бинов и возвращать результат поиска для другого именованного бина в контейнере. Поиск обычно предусматривает бин-прототип, как в сценарии, описанном в предыдущем разделе. Фреймворк Spring реализует это внедрение зависимости через метод, используя генерацию байт-кода из библиотеки CGLIB для динамической генерации подкласса, который переопределяет метод.
-
Чтобы это динамическое построение подклассов работало, класс, который контейнер бина Spring разделяет на подклассы, не может быть
final
, и переопределяемый метод тоже не может бытьfinal
. -
Модульное тестирование (юнит-тестирование) класса, в котором присутствует
abstract
метод, требует, чтобы вы самостоятельно создали подкласс класса и обеспечили реализацию функции-заглушкиabstract
метода. -
Также необходимы конкретные методы для сканирования компонентов, для чего требуются конкретные классы.
-
Еще одним ключевым ограничением является то, что методы поиска не работают с фабричными методами и, в частности, с методами
@Bean
в конфигурационных классах, поскольку в этом случае контейнер не отвечает за создание экземпляра и поэтому не может создать генерируемый во время выполнения подкласс на лету.
В случае класса CommandManager
в предыдущем фрагменте кода контейнер Spring динамически переопределяет реализацию метода createCommand()
. Класс CommandManager
не имеет никаких зависимостей Spring, как показывает переработанный пример:
package fiona.apple;
// больше никакого импорта Spring!
public abstract class CommandManager {
public Object process(Object commandState) {
// создаем новый экземпляр соответствующего интерфейса Command
Command command = createCommand();
// устанавливаем состояние для (как ожидается, совершенно нового) экземпляра Command
command.setState(commandState);
return command.execute();
}
// хорошо... но где реализация этого метода?
protected abstract Command createCommand();
}
package fiona.apple
// больше никакого импорта Spring!
abstract class CommandManager {
fun process(commandState: Any): Any {
// создаем новый экземпляр соответствующего интерфейса Command
val command = createCommand()
// устанавливаем состояние для (как ожидается, совершенно нового) экземпляра Command
command.state = commandState
return command.execute()
}
// хорошо... но где реализация этого метода?
protected abstract fun createCommand(): Command
}
В классе клиента, содержащем метод, который нужно внедрить (в данном случае CommandManager
), должен иметь сигнатуру следующего вида:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
Если метод является abstract
, то динамически сгенерированный подкласс реализует этот метод. В противном случае динамически сгенерированный подкласс переопределяет конкретный метод, определенный в исходном классе. Рассмотрим следующий пример:
<!-- бин, сохраняющий состояние, развернутый как прототип (не одиночка) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- внедряем зависимости здесь по мере необходимости -->
</bean>
<!-- commandProcessor использует statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
Бин, идентифицированный как commandManager
, вызывает свой собственный метод createCommand()
всякий раз, когда ему нужен новый экземпляр бина myCommand
. Разворачивать бин myCommand
как прототип следует с осторожностью, если это действительно необходимо. Если это бин-одиночка, то каждый раз будет возвращаться один и тот же экземпляр бина myCommand
.
В качестве альтернативы в модели компонента на основе аннотаций можно объявить метод поиска с помощью аннотации @Lookup
, как показано в следующем примере:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup("myCommand")
protected abstract fun createCommand(): Command
}
Или, что более характерно, вы можете полагаться на то, что целевой бин будет разрешен в соответствии с объявленным типом возврата метода поиска:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup
protected abstract fun createCommand(): Command
}
Обратите внимание, что, как правило, следует объявлять такие аннотированные методы поиска с конкретной реализацией функции-заглушки, чтобы они были совместимы с правилами сканирования компонентов Spring, где абстрактные классы игнорируются по умолчанию. Это ограничение не распространяется на явно зарегистрированные или явно импортированные классы бинов.
Другим способом доступа к целевым бинам, входящим в разные области видимости является точка внедрения ObjectFactory
/ Provider
. См. раздел " Бины, находящиеся в области видимости, как зависимости".
Вам также может пригодиться ServiceLocatorFactoryBean
(в пакете org.springframework.beans.factory.config
).
Произвольная замена метода
Менее полезной формой внедрения зависимости через метод, чем внедрение зависимости через метод поиска, является возможность произвольной замены методов в управляемом бине на другую реализацию метода. Вы можете спокойно пропустить остальную часть этого раздела, пока вам не понадобится эта функциональность.
С помощью конфигурационных метаданных на основе XML вы можете использовать элемент replaced-method
для замены существующей реализации метода на другую для развернутого бина. Рассмотрим следующий класс, в котором присутствует метод computeValue
, который нам необходимо переопределить:
public class MyValueCalculator {
public String computeValue(String input) {
// немного реального кода...
}
// некоторые другие методы...
}
class MyValueCalculator {
fun computeValue(input: String): String {
// немного реального кода...
}
// некоторые другие методы...
}
Класс, реализующий интерфейс org.springframework.beans.factory.support.MethodReplacer
, предоставляет новое определение метода, как показано в следующем примере:
/**
* предназначен для переопределения существующего computeValue(String).
* реализация в MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// получаем входное значение, работаем с ним и возвращаем вычисленный результат
String input = (String) args[0];
...
return ...;
}
}
/**
* предназначен для переопределения существующего computeValue(String).
* реализация в MyValueCalculator
*/
class ReplacementComputeValue : MethodReplacer {
override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
// получаем входное значение, работаем с ним и возвращаем вычисленный результат
val input = args[0] as String;
...
return ...;
}
}
Определение бина для развертывания исходного класса и задания метода переопределения будет выглядеть следующим образом:
<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"/>
Вы можете использовать один или несколько элементов <arg-type/>
внутри элемента <replaced-method/>
для указания сигнатуры метода для переопределяемого метода. Сигнатура для аргументов необходима только в том случае, если метод перегружен и в классе присутствует несколько вариантов. Для удобства строка типа для аргумента может быть подстрокой полного имени типа. Например, все нижеперечисленное соответствует java.lang.String
:
java.lang.String
String
Str
Поскольку количества аргументов зачастую достаточно, чтобы различать каждый возможный вариант, это сокращение может сэкономить уйму времени, позволив вводить только самую короткую строку, соответствующую типу аргумента.