“If you want your code to be easy to write, make it easy to read.” — Robert C. Martin, Clean Code

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

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

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

Проксирование и его вкрапление в код на лету

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

  1. JDK dynamic proxy - Spring AOP по умолчанию использует JDK dynamic proxy, которые позволяют проксировать любой интерфейс (или набор интерфейсов). Если целевой объект реализует хотя бы один интерфейс, то будет использоваться динамический прокси JDK.

  2. CGLIB-прокси - используется по умолчанию, если бизнес-объект не реализует ни одного интерфейса.

Так как прокси по сути просто оборачивает bean - он может добавить логику до и после выполнения методов. Что он, по сути, и делает.

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

Aspect: некий код, который актуален для несколько классов. Управление транзакциями является хорошим примером сквозного аспекта в корпоративных Java-приложениях. В Spring AOP аспекты реализуются с помощью аннотации @Aspect (стиль@AspectJJ) или XML-конфигурации для класса.

Join point: точка во время выполнения программы, такая как выполнение метода или обработка исключения. В Spring AOP точка соединения всегда представляет собой выполнение метода.

Advice: действие, предпринимаемое аспектом в определенной точке соединения. Advice можно разделить на те, которые выполняются только "до" основной логики метода либо "после" либо "вокруг" (и до и после). Многие AOP-фреймворки, включая Spring, моделируют advice как перехватчик который поддерживает цепочку других перехватчиков вокруг точки соединения.

Pointcut: предикат, который соответствует join point. Advice ассоциируется с выражением pointcut и запускается в любой точке соединения, совпадающей с указателем (например, выполнение метода с определенным именем). Концепция точек соединения (join point), сопоставляемых выражениями pointcut, является центральной в AOP, и Spring по умолчанию использует язык выражений AspectJ pointcut.

Introduction: объявление дополнительных методов или полей от имени типа. Spring AOP позволяет вам вводить новые интерфейсы (и соответствующую реализацию) в любой рекомендуемый объект. Например, вы можете использовать introduction, чтобы заставить bean реализовать интерфейс IsModified, чтобы упростить кэширование.

Target object: объект, который советуется одним или несколькими аспектами. Также известен как "advised object". Поскольку Spring AOP реализуется с помощью прокси во время выполнения, этот объект всегда является проксированным объектом.

AOP proxy: объект, созданный AOP-фреймворком для реализации аспектов. В Spring Framework прокси AOP - это динамический прокси JDK или прокси CGLIB.

Weaving: связывание аспектов с другими типами приложений или объектами для создания нужной логики. Это может быть сделано во время компиляции (например, с помощью компилятора AspectJ), во время загрузки или во время выполнения. Spring AOP, как и другие чисто Java AOP-фреймворки, выполняет weaving во время выполнения.

Пример работы прокси

Рассмотрим пример создания аспекта Logger, который определяет время, затраченное на выполнение каждого метода, аннотированного @Loggable

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Loggable {

}
@Aspect
@Component
public class LoggerAspect {

    @Pointcut("@annotation(Loggable)")
    public void loggableMethod() {
    }

    @Around("loggableMethod()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        final StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            System.out.println("Execution time for " + className + "." + methodName + " :: " + stopWatch.getTotalTimeMillis() + " ms");
        }
    }
}
@Component
public class Pojo {

    @Loggable
    public void test(){
        System.out.println("test method called");
        this.testUtil();
    }

    @Loggable
    public void testUtil(){
        System.out.println("testUtil method called");
    }

}
@SpringBootApplication
public class SpringAopDemoApplication implements CommandLineRunner {

	@Autowired
	Pojo pojo;

	public static void main(String[] args) {
		SpringApplication.run(SpringAopDemoApplication.class, args);
	}

	@Override
	public void run(String... args){

		pojo.test();
		System.out.println("Out of Test");
		pojo.testUtil();
	}
}

Результат выполнения кода:

test method called
testUtil method called
Execution time for Test.test :: 18 ms
Out of Test
testUtil method called
Execution time for Test.testUtil :: 0 ms

Когда spring определяет, что bean Test советует одним или несколькими аспектами, он автоматически генерирует для него прокси, чтобы перехватывать все вызовы методов и выполнять дополнительную логику, когда это необходимо. Однако из вывода видно, что дополнительная логика работает для pojo.testUtil(), но не для this.testUtil(). Почему так? Потому что последний перехватывается не прокси, а реальным целевым классом. В результате прокси никогда не срабатывает. Давайте посмотрим детальнее:

Вызов pojo.test()происходит на объекте класса Pojo. Спринг перехватывает данный вызов и создает прокси, который, в свою очередь, вызывает advice. Advice непосредственно вызывает целевой метод. И проблема заключается в том, что целевой метод сам у себя вызывает еще один метод, о котором спринг ничего не знает. Для понимания кратко еще раз:

  1. Прокси создан

  2. Вызвана какая-то логика до основного метода

  3. Происходит вызов основного метода

  4. Данный метод внутри себя "что-то делает" и что именно - прокси не имеет понятия.

  5. В числе этих самых "что-то" метод вызывает другой метод не через бин - а у себя напрямую. Таким образом, вызов самого себя не приводит к выполнению условий создания прокси.

Примечание: Аннотация @Aspect на классе помечает его кандидатом в прокси и, следовательно, исключает его из автопроксирования. Следовательно, в Spring AOP невозможно, чтобы сами аспекты были целью рекомендаций от других аспектов.

Влияние на производительность

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

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

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

Для таких редких сценариев, когда требования не могут быть решены с помощью систем на основе прокси, предпочтительнее использовать byte code weaving. При byte code weaving берутся классы и аспекты, а на выходе получаются woven файлы .class. Поскольку аспекты вплетаются непосредственно в код, это обеспечивает лучшую производительность, но сложнее в реализации по сравнению с Spring AOP.

Вывод

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

Комментарии (6)


  1. csl
    24.12.2021 22:08

    Где-нибудь в продакшене используется?


    1. sheapshop Автор
      24.12.2021 22:11
      +3

      Везде где есть спринг


      1. csl
        24.12.2021 22:15

        Spring не обязательно в продакшене с AOP

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

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

        https://habr.com/ru/post/582588/comments/#comment_23623770

        "Предлагаю рассмотреть интересную аналогию

        Если к примеру взять задачу исключительно по блокировкам, то решениям с декоратором вы можете сопоставить блок synchronize {} в java, причем который вы можете добавлять только на метод целиком. Т.е. на самом деле даже получается не блок synchronize, а именно ключевое слово дополнительное к сигнатуре метода.

        А решениям с наблюдателями, можно сопоставить использование ReentrantLock, которые позволяют в любом месте захватить блокировку и отпустить."

        https://habr.com/ru/post/582588/comments/#comment_23628406


        1. sheapshop Автор
          24.12.2021 22:56
          +3

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


        1. panzerfaust
          26.12.2021 08:07
          +2

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

          Спору нет, код бывает всякий, - но спринговый механизм успешно закрывает 99% кейсов. Труднее найти код, где аспектов нет. Я в курсе, что есть пуристы и сектанты, которым любой спринговый сахар как ножом по сердцу. Но в среднестатистическом приложении менеджмент транзакций - аспект, логирование запросов - аспект, обработка исключений в контроллерах - аспект.


  1. Serg---sw
    26.12.2021 10:30

    Перформанс можно довести до максимума если использовать aspectj компилятор. Тогда аспекты будут навешиваться при компиляции без использования прокси.