Java Method Logging with AOP and Annotations

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Иногда мне хочется записывать (через slf4j и log4j) каждое выполнение метода, видеть, какие аргументы он получает, что он возвращает и сколько времени занимает каждое выполнение. Вот как я делаю это с помощью AspectJ, jcabi-aspects и аннотаций Java 6.

Вот что я вижу в выводе log4j:

Приятно, не так ли? Теперь давайте посмотрим, как это работает.

Аннотации - это техника, введенная в Java 6. Это метапрограммный инструмент, который не изменяет работу кода, но дает отметки определенным элементам (методам, классам или переменным). Другими словами, аннотации - это просто метки, прикрепленные к коду, которые можно увидеть и прочитать. Некоторые аннотации предназначены только для компиляции - они не существуют в файлах .class после компиляции. Другие остаются видимыми после компиляции и могут быть доступны во время выполнения.

Например, @Override относится к первому типу (его тип сохранения SOURCE), в то время как @Test из JUnit относится ко второму типу (тип сохранения RUNTIME). @Loggable - аннотация второго типа, которую я использую в приведенном выше скрипте, из jcabi-aspects. Она остается с байт-кодом в файле .class после компиляции.

Опять же, важно понимать, что хотя метод power() аннотирован и скомпилирован, он до сих пор не отправляет ничего в slf4j. Он просто содержит маркер, который говорит “пожалуйста, зарегистрируйте мое выполнение”.

AOP - это полезная техника, которая позволяет добавлять исполняемые блоки в исходный код без явного изменения его. В нашем примере мы не хотим регистрировать выполнение метода внутри класса. Вместо этого мы хотим, чтобы другой класс перехватывал каждый вызов метода power(), измерял время его выполнения и отправлял эту информацию в slf4j.

Мы хотим, чтобы перехватчик понимал нашу аннотацию @Loggable и регистрировал каждый вызов этого конкретного метода power(). И, конечно же, этот же перехватчик должен использоваться для других методов, где мы будем размещать ту же аннотацию в будущем.

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

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

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

Давайте посмотрим, что означают эти волшебные слова. Но сначала давайте посмотрим, как jcabi-aspects реализует их с использованием AspectJ (это упрощенный пример, полный код вы можете найти в MethodLogger.java).

Это аспект с единственным советом вокруг around(). Аспект аннотирован с помощью @Aspect, а совет аннотирован с помощью @Around. Как обсуждалось ранее, эти аннотации являются всего лишь маркерами в файлах .class. Они ничего не делают, кроме предоставления некоторой мета-информации тем, кто интересуется временем выполнения.

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

  1. его имя * (любое имя);

  2. его аргументы - .. (любые аргументы); и

  3. он аннотирован с помощью @Loggable

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

Чтобы вызвать оригинальный метод power(), совет должен вызвать proceed() объекта точки присоединения.

Мы компилируем это аспектное и делаем его доступным в classpath вместе с нашим основным файлом Foo.class. Пока все хорошо, но нам нужно сделать последний шаг, чтобы применить наш совет в действие—мы должны применить наш совет.

Сплетение аспектов - это процесс применения советов. Сплетатель аспектов изменяет исходный код, внедряя вызовы аспектов. AspectJ делает именно это. Мы передаем ему два двоичных Java-класса Foo.class и MethodLogger.class; в ответ мы получаем три измененных класса - Foo.class, Foo$AjcClosure1.class и неизменный MethodLogger.class.

Чтобы понять, к каким методам следует применять советы, средство сборки AspectJ использует аннотации из файлов .class. Оно также использует reflection, чтобы просмотреть все классы в класспасе. Оно анализирует, какие методы удовлетворяют условиям аннотации @Around. Конечно же, оно находит наш метод power().

Итак, есть два шага. Сначала мы компилируем наши файлы .java с помощью javac и получаем два файла. Затем AspectJ осуществляет сплетение/изменение и создает свой собственный дополнительный класс. Наш класс Foo выглядит примерно так после сплетения:

Аспектный средство AspectJ перемещает нашу исходную функциональность в новый метод power_aroundBody() и перенаправляет все вызовы power() в аспектный класс MethodLogger.

Вместо одного метода power() в классе Foo теперь у нас есть четыре класса, работающих вместе. Теперь это происходит за кулисами при каждом вызове power():

hide footbox skinparam sequence { ArrowFontName “Courier New” ArrowFontSize 17 ParticipantFontName “Courier New” ParticipantFontSize 17 ActorFontSize 17 }

actor “Client” as client participant “Foo” as foo participant “MethodLogger” as aspect participant “JoinPoint” as point participant “Foo$AjcClosure1” as closure

client -> foo : power() activate foo foo -> aspect : around() activate aspect aspect -> point : proceed() activate point point -> closure : run() activate closure closure -> foo : power_aroundBody() activate foo #228b22 foo –> closure deactivate foo closure –> point deactivate closure point –> aspect deactivate point aspect –> foo deactivate aspect foo –> client deactivate foo

Исходная функциональность метода power() обозначена маленькой зеленой линией на диаграмме.

Как видите, процесс аспектного включения соединяет вместе классы и аспекты, передавая вызовы между ними через точки соединения. Без включения, как классы, так и аспекты являются просто скомпилированными двоичными файлами Java с прикрепленными аннотациями.

jcabi-aspects - это JAR-библиотека, которая содержит аннотацию Loggable и аспект MethodLogger (кстати, есть еще много аспектов и аннотаций). Вам не нужно писать свой собственный аспект для логирования методов. Просто добавьте несколько зависимостей в ваш classpath и настройте jcabi-maven-plugin для слияния аспектов (получите их последние версии в Maven Central).

Поскольку этот процесс вязки требует значительных усилий для настройки, я создал удобный плагин Maven с целью ajc, который выполняет всю работу по вязке аспектов. Вы можете использовать AspectJ напрямую, но я рекомендую использовать jcabi-maven-plugin.

Вот и все. Теперь вы можете использовать аннотацию @com.jcabi.aspects.Loggable, и ваши методы будут логироваться через slf4j.

Если что-то не работает, как объяснялось, не стесняйтесь отправить проблему на GitHub.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 10:26

sixnines availability badge   GitHub stars