В большинстве сценариев применения большинство бинов в контейнере являются одиночками (singletons). Если бину-одиночке необходимо взаимодействовать с другим бином-одиночкой или бину, не являющемуся одиночкой, нужно взаимодействовать с другим бином, не являющимся одиночкой, то, как правило, работа с зависимостью заключается в определении одного бина как свойства другого. Проблема возникает, если жизненные циклы бинов различны. Предположим, что бин-одиночка A должен использовать бин, не являющийся одиночкой (прототип) B, например, при каждом обращении к методу в A. Контейнер создает бин-одиночку A только единожды и, таким образом, возможность задать свойства появляется только единожды. Контейнер не может предоставлять бину A новый экземпляр бина B каждый раз, когда он необходим.

Решение состоит в том, чтобы отказаться в определенной степени от инверсии контроля. Вы можете сделать так, чтобы бин A был оповещен о контейнере, реализовав интерфейс ApplicationContextAware и выполнив вызов getBean("B") для контейнера, запрашивая (как правило, новый) экземпляр бина B каждый раз, когда это требуется бину A. В следующем примере продемонстрирован этот подход:

Java
// класс, который использует класс стиля 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;
    }
}
Kotlin
// класс, который использует класс стиля 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, как показывает переработанный пример:

Java
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();
}
Kotlin
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, как показано в следующем примере:

Java
public abstract class CommandManager {
    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }
    @Lookup("myCommand")
    protected abstract Command createCommand();
}
Kotlin
abstract class CommandManager {
    fun process(commandState: Any): Any {
        val command = createCommand()
        command.state = commandState
        return command.execute()
    }
    @Lookup("myCommand")
    protected abstract fun createCommand(): Command
}

Или, что более характерно, вы можете полагаться на то, что целевой бин будет разрешен в соответствии с объявленным типом возврата метода поиска:

Java
public abstract class CommandManager {
    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }
    @Lookup
    protected abstract Command createCommand();
}
Kotlin
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, который нам необходимо переопределить:

Java
public class MyValueCalculator {
    public String computeValue(String input) {
        // немного реального кода...
    }
    // некоторые другие методы...
}
Kotlin
class MyValueCalculator {
    fun computeValue(input: String): String {
        // немного реального кода...
    }
    // некоторые другие методы...
}

Класс, реализующий интерфейс org.springframework.beans.factory.support.MethodReplacer, предоставляет новое определение метода, как показано в следующем примере:

Java
/**
 * предназначен для переопределения существующего 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 ...;
    }
}
Kotlin
/**
 * предназначен для переопределения существующего 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

Поскольку количества аргументов зачастую достаточно, чтобы различать каждый возможный вариант, это сокращение может сэкономить уйму времени, позволив вводить только самую короткую строку, соответствующую типу аргумента.