Это вторая часть моей серии “DI с нуля”. В предыдущей статье мы обсудили наш базовый пример и проблемы, связанные с “ручным” подходом. Теперь мы хотим автоматизировать подключение нашей сервисной сети.
Найдите исходный код этого раздела на github
Давайте сосредоточимся на методе main()
и попытаемся найти более автоматический способ создания сервисной сети. Конечно, забыть вызвать сеттер здесь – реальная угроза, и компилятор даже не сможет предупредить вас об этом. Но наши сервисы структурно отличаются, и нет единого способа доступа к ним … или есть? Java Reflection спешит на помощь!
Все, что мы, по сути, хотим предоставить нашей установке, – это список наших классов обслуживания. Оттуда настройка должна построить и подключить сервисную сеть. В частности, наш метод main()
действительно заинтересован только в Сервисах
, для этого даже не нужно Услуга
. Давайте перепишем это так:
public static void main(String[] args) throws Exception { Set> serviceClasses = new HashSet<>(); serviceClasses.add(ServiceAImpl.class); serviceClasses.add(ServiceBImpl.class); ServiceA serviceA = createServiceA(serviceClasses); // call business logic System.out.println(serviceA.jobA()); }
Но как мы можем реализовать “волшебный” метод createServiceA
? Оказывается, это не так сложно…
private static ServiceA createServiceA(Set> serviceClasses) throws Exception{ // step 1: create an instance of each service class Set
Давайте разберем это по полочкам. В Шаге 1 мы перебираем наши классы, и для каждого класса мы пытаемся получить конструктор по умолчанию (т.е. конструктор без аргументов). Поскольку ни ServiceAImpl
, ни ServiceBImpl
не указывают никакого конструктора (мы удалили их при введении getters/setters), компилятор Java предоставляет общедоступный конструктор по умолчанию – так что это будет работать нормально. Затем мы делаем этот конструктор доступным . Это просто защитное программирование, чтобы убедиться, что частные конструкторы тоже будут работать. Наконец, мы вызываем newInstance()
в конструкторе, чтобы создать экземпляр класса, и добавить
это к нашему набору экземпляров.
В Шаге 2 мы хотим соединить наши отдельные экземпляры сервиса. Для этого мы рассматриваем каждый объект сервиса один за другим. Мы извлекаем его класс Java через getClass()
и запрашиваем у этого класса все его объявленные поля
( (
объявленный означает, что
частные поля тоже будут возвращены). Так же, как и для конструктора, мы убеждаемся, что поле доступно, а затем проверяем
Введите поля. Это предоставит нам класс обслуживания, который нам нужно ввести в поле. Все, что осталось сделать, это найти подходящего
подходящего партнера , объект, который имеет тип, указанный в поле. Как только мы его найдем, мы вызываем
field.set(…) и назначьте партнера по матчу на поле. Обратите внимание, что первый параметр метода
field.set(…)
В Шаге 3 сеть уже завершена; все, что осталось сделать, это найти экземпляр Service A
. Мы можем просто просмотреть экземпляры или и проверить, нашли ли мы правильный, используя instanceof serviceable
.
Это может показаться немного пугающим, так что, возможно, попробуйте прочитать это еще раз. Кроме того, вы можете освежить свои знания основ отражения Java, если что-то из этого покажется вам странным.
Так что же мы получили?
- Наши услуги подключаются автоматически.
- Мы больше не можем забывать вызывать сеттера (на самом деле они нам больше не нужны).
- Наше приложение завершится ошибкой при запуске если подключение выходит из строя, то не во время бизнес-логики.
Основная проблема, которую нам нужно решить в следующий раз, заключается в том, что мы не хотим повторять всю эту процедуру каждый раз, когда хотим получить доступ к сервису; мы хотим иметь возможность получить доступ к каждой службе в сети, а не только к одной.
Найдите исходный код этого раздела на github
Объект, который отвечает за удержание сервисной сети, называется Контейнер для внедрения зависимостей или (в терминах Spring) Контекст приложения . Я собираюсь использовать терминологию “контекста”, но на самом деле эти термины являются синонимами. Основная задача контекста – предоставить метод getServiceInstance(...)
, который принимает класс сервиса в качестве параметра и возвращает (готовый и подключенный) экземпляр службы. Итак, поехали:
public class DIContext { private final Set
Как вы можете видеть, код не сильно изменился по сравнению с предыдущим шагом, за исключением того, что теперь у нас есть объект для инкапсуляции контекста ( DIContext
). Внутренне он управляет набором экземпляров службы
, который создается точно так же, как и раньше, из коллекции классов служб. Приведенный выше Шаг 3 перешел в свой собственный метод getServiceInstance
, который принимает класс для извлечения в качестве параметра. Поскольку мы не можем использовать instanceof
больше (для этого требуется жестко запрограммированный класс, а не значение динамической переменной), мы должны вернуться к
ServiceClass.isInstance(…)
Мы можем использовать этот класс в нашем новом main()
:
public static void main(String[] args) throws Exception { DIContext context = createContext(); doBusinessLogic(context); } private static DIContext createContext() throws Exception { Set> serviceClasses = new HashSet<>(); serviceClasses.add(ServiceAImpl.class); serviceClasses.add(ServiceBImpl.class); return new DIContext(serviceClasses); } private static void doBusinessLogic(DIContext context){ ServiceA serviceA = context.getServiceInstance(ServiceA.class); ServiceB serviceB = context.getServiceInstance(ServiceB.class); System.out.println(serviceA.jobA()); System.out.println(serviceB.jobB()); }
Как вы можете видеть, теперь мы можем легко извлекать полные экземпляры службы из контекста, вызывая getServiceInstance
так часто, как нам нужно, с разными классами ввода. Также обратите внимание, что сами службы могут получать доступ друг к другу, просто объявив поле соответствующего типа – им даже не нужно знать о Dircontext
объект.
Однако некоторые проблемы все еще существуют. Например, что, если мы хотим иметь поле в наших службах, которое не ссылается на другую службу (скажем, поле int
)? Нам нужен способ указать нашему алгоритму , какие поля мы хотим, чтобы он установил, а какие оставить в покое.
Найдите исходный код этого раздела на github
Итак, как мы можем указать нашему алгоритму, какие поля ему нужно назначить? Мы могли бы ввести какую-нибудь причудливую схему именования и проанализировать field.getName()
, но это очень подверженное ошибкам решение. Вместо этого мы будем использовать Аннотацию :
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Inject { }
@Target
сообщает компилятору, для каких элементов мы можем использовать эту аннотацию – мы хотим, чтобы она была применима к полям. С помощью @Retention
мы даем указание компилятору сохранить эту аннотацию до выполнения, а не отбрасывать ее во время компиляции.
Давайте прокомментируем наши поля:
public class ServiceAImpl implements ServiceA { @Inject private ServiceB serviceB; // rest is the same as before }
public class ServiceBImpl implements ServiceB { @Inject private ServiceA serviceA; // rest is the same as before }
Аннотация сама по себе и сама по себе не делает ничего . Нам нужно активно читать аннотацию. Итак, давайте сделаем это в конструкторе нашего Dircontext
:
// wire them together for(Object serviceInstance : this.serviceInstances){ for(Field field : serviceInstance.getClass().getDeclaredFields()){ // check that the field is annotated if(!field.isAnnotationPresent(Inject.class)){ // this field is none of our business continue; } // rest is the same as before
Снова запустите метод main()
; он должен работать так же, как и раньше. Однако теперь вы можете свободно добавлять дополнительные поля в свои сервисы, и алгоритм подключения не нарушится.
До сих пор мы создали контейнер DI, который является очень простым, но функциональным. Он полагается на то, что мы предоставляем ему набор классов обслуживания. В следующей части мы обсудим, как мы можем на самом деле обнаружить наши классы обслуживания.
Оригинал: “https://dev.to/martinhaeusler/understanding-dependency-injection-by-writing-a-di-container-from-scratch-part-2-2np6”