Рубрики
Без рубрики

Понимание внедрения зависимостей путем написания контейнера DI – с нуля! (Часть 3)

Внедрение зависимостей – трудная тема для понимания. Давайте разберем его на простые части, написав базовый, но полностью функциональный контейнер DI с нуля!. Помечено как java, внедрение зависимостей, spring, jee.

ДИ С Нуля (Серия Из 3 Частей)

Это третья часть моей серии “DI с нуля”. В предыдущей статье мы создали базовый контейнер DI. Теперь мы хотим сделать еще один шаг вперед и автоматически находить доступные классы услуг.

Найдите исходный код этого раздела на github

Наше текущее состояние представляет собой (сильно упрощенную, но функциональную) версию библиотек, таких как Google Guice . Однако, если вы знакомы с Spring Boot , он делает еще один шаг вперед. Считаете ли вы, что это раздражает, что мы должны явно указывать классы обслуживания в наборе? Разве не было бы неплохо, если бы существовал способ автоматического определения классов обслуживания? Давайте найдем их!

public class ClassPathScanner {

    // this code is very much simplified; it works, but do not use it in production!
    public static Set> getAllClassesInPackage(String packageName) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String path = packageName.replace('.', '/');
        Enumeration resources = classLoader.getResources(path);
        List dirs = new ArrayList<>();
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }
        Set> classes = new HashSet<>();
        for (File directory : dirs) {
            classes.addAll(findClasses(directory, packageName));
        }
        return classes;
    }

    private static List> findClasses(File directory, String packageName) throws Exception {
        List> classes = new ArrayList<>();
        if (!directory.exists()) {
            return classes;
        }
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }

}

Здесь нам приходится иметь дело с API ClassLoader . Этот конкретный API довольно старый и восходит к самым ранним дням Java, но он все еще работает. Мы начинаем с Имени пакета для сканирования. Каждому потоку в JVM назначен загрузчик классов контекста , из которого загружаются объекты класса . Поскольку загрузчик классов работает с файлами, нам необходимо преобразовать имя пакета в путь к файлу (заменив '.' по '/' ). Затем мы запрашиваем у загрузчика классов все ресурсы по этому пути и преобразуйте их в Файл ы один за другим. На практике здесь будет только один ресурс: наш пакет, представленный в виде каталога.

Оттуда мы рекурсивно перебираем дерево файлов нашего пакета каталогов в поисках файлов, заканчивающихся на .класс . Мы преобразуем каждый файл класса, с которым сталкиваемся, в имя класса (отсекая завершающий .класс ), чтобы в итоге получить имя нашего класса. Затем мы, наконец, называем Класс.имя_класса(...) на нем, чтобы получить класс.

Таким образом, у нас есть способ получить все классы в нашем базовом пакете. Как мы его используем? Давайте добавим статический фабричный метод в наш класс Dircontext , который создает Dircontext для данного базового пакета:

    public static DIContext createContextForPackage(String rootPackageName) throws Exception {
        Set> allClassesInPackage = ClassPathScanner.getAllClassesInPackage(rootPackageName);
        Set> serviceClasses = new HashSet<>();
        for(Class aClass : allClassesInPackage){   
            serviceClasses.add(aClass);
        }
        return new DIContext(serviceClasses);
    }

Наконец, нам нужно использовать этот новый фабричный метод в нашем методе createContext() :

    private static DIContext createContext() throws Exception {
        String rootPackageName = Main.class.getPackage().getName();
        return DIContext.createContextForPackage(rootPackageName);
    }

Мы извлекаем имя базового пакета из класса Main (класс, который я использовал для хранения моего метода main() ).

Но подождите! У нас есть проблема. Наш сканер путей к классам обнаружит все классы, независимо от того, являются ли они услугами или нет. Нам нужно указать алгоритму, какие из них нам нужны, с помощью – как вы уже догадались – аннотации:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {

}

Давайте прокомментируем наши услуги с его помощью:

@Service
public class ServiceAImpl implements ServiceA { ... }

@Service
public class ServiceBImpl implements ServiceB { ... }

… и соответствующим образом фильтруйте наши классы:

    public static DIContext createContextForPackage(String rootPackageName) throws Exception {
        Set> allClassesInPackage = ClassPathScanner.getAllClassesInPackage(rootPackageName);
        Set> serviceClasses = new HashSet<>();
        for(Class aClass : allClassesInPackage){
            if(aClass.isAnnotationPresent(Service.class)){
                serviceClasses.add(aClass);
            }
        }
        return new DIContext(serviceClasses);
    }

И вот оно – минималистичный, плохо оптимизированный, но полностью функциональный контейнер DI. Но подождите, зачем вам библиотечный код на несколько мегабайт, если ядро такое простое? Хорошо…

  • Наш сканер путей к классам очень ненадежен. Это, конечно, не охватывает все случаи, глубины вложенности, внутренние классы и т.д. Библиотеки, такие как Гуава Путь к классам , справляются с этим гораздо лучше.

  • Большим преимуществом DI является то, что мы можем подключиться к жизненному циклу сервиса. Например, мы можем захотеть что-то сделать после создания службы ( @Постконструкция ). Возможно, мы захотим вводить зависимости с помощью сеттеров, а не полей. Возможно, мы захотим использовать инъекцию конструктора, как мы делали в начале. Возможно, мы захотим обернуть наши сервисы прокси-серверами, чтобы код выполнялся до и после каждого метода (например, @Транзакционный ). Все эти “навороты и свистки” предоставляются, например, весной.

  • Наш алгоритм подключения вообще не учитывает базовые классы (и их поля).

  • Наш getServiceInstance(...) метод очень плохо оптимизирован, так как он каждый раз линейно сканирует соответствующий экземпляр.

  • Вы, безусловно, захотите иметь разные контексты для тестирования и производства. Если вас это интересует, взгляните на Профили Spring .

  • У нас есть только один способ определения сервисов; некоторые из них могут потребовать дополнительной настройки. См. раздел Пружины @Конфигурация и @Bean аннотации для получения подробной информации об этом.

  • Много других мелких деталей и кусочков.

Мы создали очень простой контейнер DI, который:

  • инкапсулирует создание сервисной сети
  • создает сервисы и соединяет их вместе
  • способен сканировать путь к классам для классов обслуживания
  • демонстрирует использование отражения и аннотаций

Мы также обсудили обоснование нашего выбора:

  • Во-первых, мы заменили статические ссылки объектами и конструкторами.
  • Затем мы ввели интерфейсы для дальнейшего разделения объектов.
  • Мы обнаружили, что циклические зависимости являются проблемой, поэтому мы ввели сеттеры.
  • Мы заметили, что вызов всех установщиков для построения сервисной сети вручную подвержен ошибкам. Мы прибегли к рефлексии, чтобы автоматизировать этот процесс.
  • Наконец, мы добавили сканирование путей к классам для автоматического определения классов служб.

Если вы зашли так далеко, спасибо, что читаете вместе с нами! Надеюсь, вам понравилось прочитанное.

ДИ С Нуля (Серия Из 3 Частей)

Оригинал: “https://dev.to/martinhaeusler/understanding-dependency-injection-by-writing-a-di-container-from-scratch-part-3-1one”