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

Java

содержание

вводить

Пока мы доделали функции простых IOC и DI, хотя это определенно очень просто по сравнению со Spring, но ведь мы пытаемся понять принцип, и нет необходимости делать то же самое, что и Spring. Теперь мы не можем вздохнуть с облегчением, предыдущие IOC и DI относительно просты, а вводимый здесь АОП немного сложнее.

tips

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

AOP

Что такое АОП

Аспектно-ориентированное программирование: Аспектно-ориентированное программирование, функция заключается в том, чтобы просто улучшить функции в классе или добавить новые функции без изменения исходного кода класса.

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

Некоторые концепции АОП

Advice: Advice определяет, когда аспект должен выполнять, какие функции, советы и pointcut составляют аспект.

Point Cut (pointCut): Точка среза определяет, где должна действовать поверхность среза.

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

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

Weaving: Расширение функциональности без изменения исходного кода.

Простой анализ АОП

Уведомление (совет)

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

Как воспринимается созданная пользователем структура рекомендаций? Как фреймворк изолирует различные советы, зарегистрированные пользователем?

Эта проблема очень проста. Большинство людей понимают, что это похоже на JDBC в Java. Java предоставляет набор общедоступных интерфейсов, и каждый поставщик базы данных реализует интерфейсы, предоставляемые Java, для завершения работы базы данных. Здесь мы также предоставляем набор интерфейсов для АОП, и пользователи могут реализовывать интерфейсы при их использовании.

Каковы сроки консультации? Какие интерфейсы необходимо предоставить?

Здесь мы напрямую берем улучшенное время, определенное в Spring.

  • Перед вызовом совета перед вызовом метода
  • После - уведомление вызывается после завершения метода, независимо от того, был успешно выполнен метод или нет.
  • After-return - уведомление о вызове после успешного выполнения метода
  • After-throwing — уведомлять после того, как метод выдает исключение
  • Вокруг — совет оборачивает рекомендуемый метод, выполняя настраиваемые действия до и после вызова рекомендуемого метода.

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

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

Мы должны сделать эти разные методы необязательными для пользователя и реализовать любой из них. Итак, нужно ли сопоставлять каждый метод с интерфейсом? ненужный. вышеafter(...)иafterSuccess(...)Оба реализуются после выполнения метода. Разница в том, что один требует возвращаемое значение после успеха, а другой нет. Эти два можно отличить как одну реализацию по возвращаемому значению. Усовершенствованная обработка после исключения. Это требует обертывания исполняемого метода и перехвата исключения. Это почти то же самое, что и объемный звук, их можно объединить.

Диаграмма класса:

pointcut

Совет в основном таков, далее идет pointcut. Говоря о pointcut, те, кто использовал АОП в Spring, должны лучше понимать выражения pointcut.В Spring пользователи используют выражения pointcut, чтобы определить, на какие типы методов будут воздействовать наши улучшения. Это выражение pointcut очень важно. Для нашего рукописного АОП нам тоже нужно предусмотреть такую ​​функцию. Конечно, выражение пишет пользователь, и наш фреймворк анализирует выражение пользователя, а затем соответствует конкретному методу.

Как анализировать пользовательские выражения? Как упоминалось выше, строка символов используется для соответствия одной или нескольким различным целям.Нашей первой реакцией должны быть регулярные выражения.Очевидно, что эта функция может быть реализована с использованием регулярных выражений. Но на самом деле таких выражений гораздо больше. НапримерAspectJ,Ant pathЖдать. Вам решать, что использовать.Здесь я реализую обычное сопоставление.

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
  1. Как мы находим способ улучшить его?

Когда мы определяем, какие классы и какие методы нужно улучшить, нам нужно подумать, как мы можем получить эти методы (для улучшения методов мы должны получить конкретные методы).

  1. С помощью выражения мы можем определить конкретный класс и метод.Выражение определяет только относительный путь.Как получить адрес файла класса в соответствии с относительным путем?

Усовершенствование экземпляра компонента завершается во время инициализации.Если он оценивается во время инициализации, прокси-объект будет сгенерирован через прокси, и прокси-объект будет зарегистрирован в контейнере вместо исходного экземпляра, когда он вернется.

  1. С файлом класса, как получить методы в классе?

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

Aspect/Advisor

У нас есть реализация улучшений и определены те методы, которые необходимо улучшить. Теперь нам нужно улучшить полученный метод.

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

Итак, что нужно предоставить пользователям для реализации АОП?

Если пользователи хотят внедрить АОП, они должны сначала предоставить совет (уведомление) для улучшения функции, выражение, определяющее, какие методы улучшать, и фактически должны указать, какой синтаксический анализатор использовать для анализа входящего выражения (обычный, AspectJ.. . ). Для пользователей было бы более хлопотно предоставлять эти вещи по отдельности, но роль фреймворка состоит в том, чтобы помочь пользователям упростить процесс разработки и сделать его максимально простым. Итак, здесь мы можем предоставить пользователям новый внешний вид (фасад), чтобы упростить пользователям использование. Здесь действительно используетсяВнешний вид Режимподумал о.

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

AdvisorRegistry

Weaving

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

Выше было сказано, что улучшение классов и методов улучшается с использованием шаблона прокси. Итак, когда мы должны улучшить его как основу?

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

В этом процессе инициализации bean-компонента для повышения? Будет ли проблема?

Согласно предыдущему введению, наша структура инициализирует bean-компоненты в BeanFactory, что также включает создание экземпляров bean-компонентов, внедрение параметров и размещение bean-компонентов в контейнерах. Очевидно, что улучшения bean-компонента должны выполняться после того, как bean-компонент создан и еще не помещен в контейнер. Затем он находится в методе doGetBean BeanFactory. Здесь есть небольшая проблема: метод doGetBean делает достаточно вещей, и добавление в него кода, несомненно, взорвет код, который сложно поддерживать и расширять. Для решения этой проблемы мы можем использоватьШаблон наблюдателяЧтобы решить эту проблему, каждый процесс в методе doGetBean существует как наблюдатель, когда нам нужно добавить функцию, мы можем добавить наблюдателя, а затем внедрить его, чтобы существующий код не изменился.

Определите интерфейс наблюдателя:

BeanPostProcessor

Здесь мы на данный момент определяем только наблюдателя приложения aop, другие, такие как создание экземпляров и внедрение параметров, будут добавлены позже.

BeanPostProcessor запускается, когда бин работает в BeanFactory.Мы также должны добавить список BeanPostProcessor и метод регистрации BeanPostProcessor в BeanFactory.

BeanFactory

При применении режима наблюдателя здесь BeanFactory действует как субъект, BeanPostProcessor действует как наблюдатель, а BeanFactory слушает BeanPostProcessor.Мы можем извлечь функцию как BeanPostProcessor и зарегистрировать ее в BeanFactory, чтобы код в BeanFactory не будет блокироваться.Слишком много, да и развязать функции проще.Если какая-то функция нам не нужна, то мы можем напрямую обращаться к привязке без каких-либо других операций. Здесь мы реализуем только регистрацию функции Aop.

image

Предположим, мы хотим отрисовывать другие функции в качестве наблюдателя, затем напрямую унаследовать интерфейс BeanPostProcessor для реализации наших собственных функций, а затем зарегистрировать их в BeanFactory.

Анализ реализации функции

Теперь, когда интерфейс доступен, теперь нам нужно подумать о том, как реализовать функциональность. Итак, давайте разберемся, что нам нужно сделать сейчас.

  1. При создании bean-компонента необходимо определить, нуждается ли bean-компонент в улучшении.Эта работа выполняется интерфейсом AopPostProcessor, чтобы определить, нужно ли его улучшать и каким образом (прокси JDK или прокси-сервер cglib). Создайте прокси-объект, если вам нужно его улучшить, и используйте прокси-объект при регистрации в контейнере.
  2. Как упоминалось в 1, нам нужно создать прокси-объект, затем нам также необходимо обеспечить реализацию прокси-сервера.В настоящее время прокси-сервер в основном через прокси-сервер JDK и режим прокси-сервера cglib.Основное различие между ними заключается в том, что прокси-режим JDK должен требовать, чтобы класс реализовывал интерфейс, а cglib — нет.
  3. При фактическом вызове метода расширения экземпляра инфраструктуре необходимо вызвать метод расширения метода, как его вызвать и как вызвать несколько методов расширения.

Теперь проанализируем и решим вышеуказанные проблемы по отдельности.

реализация прокси

Реализация прокси является обычной реализацией.Мы предоставляем метод создания экземпляра прокси извне и метод обработки исполнения.

AopProxy

JDKDynamicProxy и CglibDynamicProxy совместно реализуют интерфейс AopProxy. Кроме того, для реализации прокси JDKDynamicProxy также необходимо реализовать интерфейс InvocationHandler, а CglibDynamicProxy также необходимо реализовать интерфейс MethodInterceptor.

Некоторые друзья могли заметить, что в классе, создающем прокси, есть переменная BeanFactory Причина, по которой используется этот тип переменной, заключается в том, что экземпляр Advice может быть получен из BeanFactory, когда метод соответствует расширению совета. В Advisor не хранится экземпляр Advice, но сохраняется имя экземпляра (beanName). Но проблема в том, как получить значение этой переменной, для общих бинов мы можем получить его из контейнера, а сама BeanFactory является контейнером, конечно же, получить его из контейнера невозможно. Давайте сначала разберем метод получения значения переменной:

  1. Получается из контейнера с помощью внедрения зависимостей, что здесь неуместно.
  2. Непосредственно создайте новое значение, здесь нужен экземпляр в контейнере, а новое значение определенно исчезло.Если вы будете следовать исходному процессу, чтобы снова создать то же значение, это, несомненно, глупый способ, и он не подходит здесь.
  3. Передача параметров, если процесс вызова метода можно проследить до всего процесса переменной, его можно передать, передав параметры
  4. Метод в Spring похож на 3, и это также метод, который мы обычно используем чаще. Предоставьте серию интерфейсов, единственная функция интерфейса — передать значение переменной, и в интерфейсе есть только один уникальный метод Set.

Aware

Предоставьте родительский интерфейс Aware и ряд подинтерфейсов, таких как BeanFactoryAware, ApplicationContextAware используется для размещения этих значений там, где это необходимо. Если классу необходимо использовать значение переменной контейнера Spring, он может напрямую реализовать интерфейс xxxAware. Подход Spring заключается в том, чтобы определить, какие классы реализуют интерфейс Aware в определенном процессе, а затем вставить в него значение.

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

image

Как вызвать, если есть несколько методов улучшения разных типов

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

执行顺序

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

//before working
//invoke 被加强的方法执行
//after working 

Таким образом, если цикл for все еще выполняется, метод будет выполняться несколько раз, поэтому метод цикла for определенно неприемлем. Нам нужно вложенное выполнение, похожее на рекурсивные вызовы, например:

递归顺序

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

Конкретная реализация вызывающего процесса:

public class AopAdviceChain {

    private Method nextMethod;
    private Method method;
    private Object target;
    private Object[] args;
    private Object proxy;
    private List<Advice> advices;

    //通知的索引 记录执行到第多少个advice
    private int index = 0;

    public AopAdviceChain(Method method, Object target, Object[] args, Object proxy, List<Advice> advices) {
        try {
            //对nextMethod初始化 确保调用正常进行
            nextMethod = AopAdviceChain.class.getMethod("invoke", null);
        } catch (NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }

        this.method = method;
        this.target = target;
        this.args = args;
        this.proxy = proxy;
        this.advices = advices;
    }

    public Object invoke() throws InvocationTargetException, IllegalAccessException {
        if(index < this.advices.size()){
            Advice advice = this.advices.get(index++);
            if(advice instanceof BeforeAdvice){
                //前置增强
                ((BeforeAdvice) advice).before(method, args, target);
            }else if(advice instanceof AroundAdvice){
                //环绕增强
                return ((AroundAdvice) advice).around(nextMethod, null, this);
            } else if(advice instanceof AfterAdvice){
                //后置增强
                //如果是后置增强需要先取到返回值
                Object res = this.invoke();
                ((AfterAdvice) advice).after(method, args, target, res);
                //后置增强后返回  否则会多执行一次
                return res;
            }
            return this.invoke();
        }else {
            return method.invoke(target, args);
        }
    }
}

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

хостинг кода

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

резюме

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