У переважній кількості сценаріїв застосування більшість бінів у контейнері є одинаками (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
Оскільки кількості аргументів часто достатньо, щоб розрізняти кожен можливий варіант, це скорочення може зберегти багато часу, дозволивши вводити лише найкоротший рядок, відповідний типу аргументу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ