ДИ С Нуля (Серия Из 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”