У переважній кількості сценаріїв застосування більшість бінів у контейнері є одинаками (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 {
        // призначений для перевизначення існуючого computeValue(String)
        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

Оскільки кількості аргументів часто достатньо, щоб розрізняти кожен можливий варіант, це скорочення може зберегти багато часу, дозволивши вводити лише найкоротший рядок, відповідний типу аргументу.