Принцип и ручная реализация DI в серии Spring

Java Spring

содержание

предисловие

В предыдущей главе мы представили и просто реализовали некоторые функции контейнера, но осталось еще много проблем. Например, когда мы создаем экземпляр компонента, если у компонента есть параметры, что нам нужно сделать? Конечно, это можно сделать в методе init. Например, когда мы обычно получаем объект в Spring, мы можем получить класс через аннотацию. Экземпляр, как этот экземпляр внедряется?

МОК и ДИ

Внедрение зависимостей (DI) и инверсия управления (IOC) в основном выражают одно и то же, и ни одно из них не может быть отделено от другого.

Предположим, что класс a должен зависеть от класса b, но a не управляет циклом объявления b, в этом классе используется только b, а создание и уничтожение класса b передаются другим компонентам для обработки, что называется инверсией управления ( IOC ), и если класс a зависит от класса b, он должен получить экземпляр класса b. Этот процесс получения экземпляра класса b называется внедрением зависимостей (DI).

Инъекционный анализ

Проанализируйте, где мы могли бы внедрить такое поведение?

Чтобы узнать, где существует внедрение зависимостей, вы должны сначала понять, что именно вводится.В сочетании с приведенным выше анализом становится очевидным, что внедрение на самом деле является процессом создания экземпляра и, в более общем смысле, процессом присваивания. И где у нас обычно есть действия присваивания? Прежде всего, в конструкторе не должно быть возможности запуска инстанцируемого конструктора класса, и большинство операций присвоения начальных значений также выполняются в конструкторе. Затем происходит еще одна модификация значения атрибута в процессе выполнения.

Тогда необходимость внедрения зависимостей очевидна:

  • Построить зависимость параметра
  • зависимость свойства

Какие виды заданий у нас есть?

Очевидно, что в Java мы назначаем типы, включая примитивные типы данных (int, double...) и ссылочные типы.

После анализа поведения и типов, которые необходимо внедрить, возникают другие проблемы.Хотя мы знаем, что внедренные типы являются базовыми типами данных и ссылочными типами, фактические типы, которые необходимо внедрить, непредсказуемы, и мы не знаем определенного параметр заранее.Требуется тип int или boolean или ссылочный тип. К счастью, Java фактически решила эту проблему для нас.Все типы в Java наследуются от Object, и нам нужно использовать Object только для того, чтобы принимать значения.

Когда мы вводим значения, нам обязательно нужно знать конкретный тип, который мы вводим.Ключевое слово instanceof может помочь нам определить конкретный тип, но на самом деле оно ограничено базовыми типами данных, потому что ссылочные типы слишком сложны. для определения типа ссылки, определенного пользователем заранее. Поэтому мы должны найти способ сообщить нам, какой тип вводится.

BeanReference

Мы определяем класс для определения типа компонента. Интерфейс содержит только параметр beanName, который идентифицирует тип компонента. При вводе параметров нам нужно только определить значение beanName, чтобы получить соответствующий компонент непосредственно из контейнера IOC. Данные type и String могут быть назначены напрямую.

Конечно, это не решает всех проблем, т. к. входящие параметры могут нести несколько эталонных значений, таких как эталонные массивы, Списки, Карты, чтение файла свойств (Properties) и т. д. Для решения этих проблем аналогично приведенному выше , а ссылочный тип по-прежнему использует BeanReference.Можно пройти несколько значений.

Анализ зависимости параметров конструкции

Проблема с количеством параметров построения

По сути, приведенный выше анализ почти решил нашу проблему.Еще одна проблема заключается в том, что нет возможности определить количество параметров построения.Как мы можем хранить все параметры, необходимые для создания экземпляра бина, и как значения и параметры соответствует.

Очевидно, что если нам нужна соответствующая связь, мы можем сразу думать о карте в виде ключ-значение, На самом деле, мы также можем использовать список, чтобы хранить его в соответствии с порядком, и порядок остается тем же, когда мы возьми.

Как сопоставить конструкторы

Количество перегруженных методов в классе может быть кратным, как найти именно тот метод, который нам нужен?

Класс Class в JDK предоставляет нам ряд методов:

method вводить
Constructor getConstructor(Class<?>... parameterTypes) Возвращает объект Constructor, отражающий указанные функции открытого класса класса, представленного объектом Constructor.
Method getMethod(String name, Class<?>... parameterTypes) Возвращает объект метода, отражающий указанные открытые методы-члены класса или интерфейса, представленного этим объектом класса.
Method[] getMethods() Возвращает массив, содержащий объект метода, отражающий все общедоступные методы класса или интерфейса, представленного этим объектом класса, включая объявленные классом или интерфейсом и унаследованные от суперклассов и суперинтерфейсов.
Constructor<?>[] getConstructors() Возвращает массив, содержащий объекты Constructor, отражающие все объекты общедоступного конструктора класса, представленного this.

Мы взяли параметры ранее:

  1. Сопоставьте методы с одинаковым количеством параметров в соответствии с количеством параметров
  2. Метод фильтрации точно совпадающих шагов на основе типов параметров.
синглтон или прототип

Проще говоря, синглтон означает, что bean-компонент создается только один раз во время работы контейнера, и этот же объект необходимо использовать позже. Прототип (Prototype) не создается при работе контейнера, он создается только при его использовании, и каждый раз, когда он не используется, создается новый.

Если одноэлементный шаблон создается только один раз, описанный выше процесс сопоставления будет выполнен только один раз и не повлияет на работу программы. Но шаблон-прототип повторно сопоставляется каждый раз при его создании, что в определенной степени замедляет работу программы. Так что здесь мы можем рассмотреть кеширование соответствующего метода создания прототипа bean-компонента, чтобы нам не нужно было повторять сопоставление при использовании и создании в том же месте позже.

требуемый интерфейс

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

Внедрение параметров построения должно быть при создании бина.Ранее мы определили несколько различных методов создания бина для класса.Теперь мы должны добавить параметры построения к этим методам.

Код:

BeanReference

public class BeanReference {

    private String beanName;

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
}

Код добавления DefaultBeanDefinition:

public class DefaultBeanDefinition implements BeanDefinition{

    ...
    
    private Constructor constructor;

    private Method method;

    private List<?> constructorArg;

    ... 

    //getter setter
}

DefaultBeanFactory добавить код

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {

    //other method

    /**
     * 解析传入的构造参数值
     * @param constructorArgs
     * @return
     */
    private Object[] parseConstructorArgs(List constructorArgs) throws IllegalAccessException, InstantiationException {

        if(constructorArgs==null || constructorArgs.size()==0){
            return null;
        }

        Object[] args = new Object[constructorArgs.size()];
        for(int i=0;i<constructorArgs.size();i++){
            Object arg = constructorArgs.get(i);
            Object value = null;
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) arg).getBeanName();
                value = this.doGetBean(beanName);
            }else if(arg instanceof List){
                value = parseListArg((List) arg);
            }else if(arg instanceof Map){
                //todo 处理map
            }else if(arg instanceof Properties){
                //todo 处理属性文件
            }else {
                value = arg;
            }
            args[i] = value;
        }
        return args;
    }

    private Constructor<?> matchConstructor(BeanDefinition bd, Object[] args) throws Exception {
        
        if(args == null){
            return bd.getBeanClass().getConstructor(null);
        }
        //如果已经缓存了 则直接返回
        if(bd.getConstructor() != null)
            return bd.getConstructor();

        int len = args.length;
        Class[] param = new Class[len];
        //构造参数列表
        for(int i=0;i<len;i++){
            param[i] = args[i].getClass();
        }
        //先进行精确匹配 如果能匹配到相应的构造方法 则后续不用进行
        Constructor constructor = null;
        try {
            constructor = bd.getBeanClass().getConstructor(param);
        } catch (Exception e) {
            //这里上面的代码如果没匹配到会抛出空指针异常
            //为了代码继续执行 这里我们来捕获 但是不需要做其他任何操作
        }
        if(constructor != null){
            return constructor;
        }

        //未匹配到 继续匹配
        List<Constructor> firstFilterAfter = new LinkedList<>();
        Constructor[] constructors = bd.getBeanClass().getConstructors();
        //按参数个数匹配
        for(Constructor cons:constructors){
            if(cons.getParameterCount() == len){
                firstFilterAfter.add(cons);
            }
        }

        if(firstFilterAfter.size()==1){
            return firstFilterAfter.get(0);
        }
        if(firstFilterAfter.size()==0){
            log.error("不存在对应的构造函数:" + args);
            throw new Exception("不存在对应的构造函数:" + args);
        }
        //按参数类型匹配
        //获取所有参数类型
        boolean isMatch = true;
        for(int i=0;i<firstFilterAfter.size();i++){
            Class[] types = firstFilterAfter.get(i).getParameterTypes();
            for(int j=0;j<types.length;j++){
                if(types[j].isAssignableFrom(args[j].getClass())){
                    isMatch = false;
                    break;
                }
            }
            if(isMatch){
                //对于原型bean 缓存方法
                if(bd.isPrototype()){
                    bd.setConstructor(firstFilterAfter.get(i));
                }
                return firstFilterAfter.get(i);
            }
        }
        //未能匹配到
        throw new Exception("不存在对应的构造函数:" + args);
    }
    private List parseListArg(List arg) throws Exception {
        //遍历list
        List param = new LinkedList();
        for(Object value:arg){
            Object res = new Object();
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) value).getBeanName();
                res = this.doGetBean(beanName);
            }else if(arg instanceof List){
                //递归 因为list中可能还存有list
                res = parseListArg(arg);
            }else if(arg instanceof Map){
                //todo 处理map
            }else if(arg instanceof Properties){
                //todo 处理属性文件
            }else {
                res = arg;
            }
            param.add(res);
        }
        return param;
    }
}

Соответствующий код размещен на github:myspring

круговая зависимость

На этом этапе зависимость параметров конструктора в основном завершена, и после тестирования в основном нет проблем, но во время тестирования обнаруживается, что если построенные параметры имеют циклические зависимости, весь процесс завершится ошибкой.

Что такое циклическая зависимость? Как разрешить циклические зависимости?

Как показано на рисунке выше, A зависит от B, B зависит от C, а C зависит от A. В процессе инициализации A должен загрузить B, B должен загрузить C, а в C он снова загружает A, и описанный выше процесс повторяется все время, что называется циклическими зависимостями.

При настройке bean-компонентов в Spring framework есть атрибутlazy-init. Как только это свойство установлено в true, проблема циклических зависимостей не существует, почему это так? На самом деле, если настроена отложенная загрузка, бин не будет инициализирован сразу, а будет инициализирован при его использовании, а другие бины уже инициализированы, когда их нужно использовать. зависимый экземпляр не требует создания экземпляра, поэтому циклических зависимостей не будет.

Итак, как мы решим это здесь?

Согласно вдохновению Spring, для решения циклической зависимости главное, чтобы экземпляры bean-компонентов не были созданы, затем мы определяем контейнер для записи созданных bean-компонентов.Каждый раз, когда создается экземпляр bean-компонента, он обнаруживается один раз.Пропустить, если экземпляр bean-компонента уже создан.

Добавлен код:

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {
    //记录正在创建的bean
    private ThreadLocal<Set<String>> initialedBeans = new ThreadLocal<>();

    public Object doGetBean(String beanName) throws InstantiationException, IllegalAccessException {
    
        //other operation
        // 记录正在创建的Bean
        Set<String> beans = this.initialedBeans.get();
        if (beans == null) {
            beans = new HashSet<>();
            this.initialedBeans.set(beans);
        }

        // 检测循环依赖
        if (beans.contains(beanName)) {
            throw new Exception("检测到" + beanName + "存在循环依赖:" + beans);
        }

        // 记录正在创建的Bean
        beans.add(beanName);
        //other operation
        //创建完成 移除该bean的记录
        beans.remove(beanName);
        return instance;
    }
}

По сути, в singleton bean-компоненте экземпляр уже созданного bean-компонента получается непосредственно из контейнера, и нет необходимости создавать его экземпляр снова, поэтому проблемы циклической зависимости не возникает. Однако для бинов-прототипов созданный экземпляр не помещается в контейнер, а каждый раз создается заново и инициализируется, поэтому возникает проблема циклической зависимости.

зависимость свойства

В дополнение к инициализации параметров в конструкторе мы также можем присваивать значения свойствам.Преимущество присвоения свойств заключается в том, что значение свойств может динамически изменяться во время выполнения.

Чем это отличается от зависимости параметра конструктора?

В целом разницы нет.Разница в том,что есть конкретные соответствующие методы зависимости от параметров построения.Способ построения можно определить по количеству и порядку параметров.Поэтому в закачке можно использовать Список, выбранный выше в качестве параметра в соответствии с порядком хранения. Для зависимостей атрибутов мы должны вводить значения в соответствии с именем атрибута, поэтому использование списка не сработает.

решать:

  1. Используйте контейнер карты, ключ — это имя атрибута, значение — это значение атрибута, а карту можно анализировать при использовании
  2. Настройте класс, который оборачивает атрибуты, параметрами являются имя атрибута и значение атрибута, а затем используйте список для размещения класса, который оборачивает атрибуты, что на самом деле похоже на карту выше.

Здесь я использую класс карты.

Тогда другие места в основном такие же, все еще используются для ссылочных типовBeanReference. существуетBeanDefinitionДобавьте методы получения и установки значения свойства в:

    //属性依赖
    Map<String,Object> getPropertyKeyValue();
    void setPropertyKeyValue(Map<String,Object> properties);

Добавьте в реализацию BeanFactory метод разбора свойств:

private void parsePropertyValues(BeanDefinition bd, Object instance) throws Exception {
        Map<String, Object> propertyKeyValue = bd.getPropertyKeyValue();
        if(propertyKeyValue==null || propertyKeyValue.size()==0){
            return ;
        }
        Class<?> aClass = instance.getClass();
        Set<Map.Entry<String, Object>> entries = propertyKeyValue.entrySet();
        for(Map.Entry<String, Object> entry:entries){
            //获取指定的字段信息
            Field field = aClass.getDeclaredField(entry.getKey());
            //将访问权限设置为true
            field.setAccessible(true);
            Object arg = entry.getValue();
            Object value = null;
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) arg).getBeanName();
                value = this.doGetBean(beanName);
            }else if(arg instanceof List){
                List param = parseListArg((List) arg);
                value = param;
            }else if(arg instanceof Map){
                //todo 处理map
            }else if(arg instanceof Properties){
                //todo 处理属性文件
            }else {
                value = arg;
            }
            field.set(instance, value);
        }
    }
Соответствующий код размещен на github:myspring

Полная диаграмма классов