Большинство веб-приложений Java используют стандартные интерфейсы Java при взаимодействии с другими системами. HTTP-сервисы, такие как Web-страницы или REST-серверы, реализованы с использованием интерфейса javax.servlet.Servlet
. Взаимодействие с базой данных осуществляется с использованием интерфейсов JDBC java.sql.Statement
и java.sql.Connection
. Эти стандарты используются практически повсеместно, независимо от базовой структуры (Spring или Java EE) и контейнера сервлетов (Tomcat, Wildfly и т. Д.).
В этой статье показано, как реализовать агент Java, который подключается к этим интерфейсам с помощью манипулирования байт-кодом и собирает метрики о частоте и продолжительности вызовов HTTP и базы данных. Демонстрационный код доступен по адресу https://github.com/fstab/promagent , который является агентом, использующим веб-приложения Java для системы мониторинга Prometheus . Однако эта статья не относится к Prometheus, она фокусируется на базовых технологиях, таких как Java-агенты, манипуляции с байт-кодом и загрузчики классов.
1. Агенты Java
Агенты Java — это программы Java, которые можно подключить к JVM для манипулирования байт-кодом Java. Например, агенты Java могут использоваться для изменения всех реализаций интерфейса javax.servlet.Servlet
для получения статистики о количестве и продолжительности HTTP-вызовов.
Агенты Java поставляются в виде файлов JAR. В то время как обычные Java-программы имеют метод main()
в качестве точки входа приложения, Java-агенты имеют метод premain()
который будет вызываться перед методом main()
:
Схема агента Java
1
2
3
4
5
|
public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) throws Exception { // ... } } |
В то время как исполняемые файлы JAR содержат файл MANIFEST.MF
котором Premain-Class
Main-Class
, файлы JAR агентов содержат файл MANIFEST.MF
котором Premain-Class
. Агент может быть подключен во время запуска приложения с помощью параметра командной строки -javaagent:
::
Командная строка агента Java
1
|
java -javaagent:myagent.jar -jar myapp.jar |
Затем premain()
может вызвать inst.addTransformer()
для регистрации ClassFileTransformer
. Преобразователь файла класса реализует метод transform()
который будет вызываться всякий раз, когда загружается класс Java. Он может проверять и изменять байт-код любого Java-класса для добавления дополнительных функций.
2. Манипуляция байт-кодом
Существует несколько доступных библиотек, помогающих разработчикам Java реализовывать манипуляции с байт-кодом. Самый низкий уровень — это ASM . Другие библиотеки, такие как cglib и javassist, предоставляют API более высокого уровня. Самая новая и самая простая в использовании библиотека — Byte Buddy . Он предоставляет легко читаемый свободно распространяемый Java API для создания ClassFileTransformer
и регистрации его в Instrumentation
:
Byte Buddy Agent Пример
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
package io.promagent; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.agent.builder.AgentBuilder.Transformer; import net.bytebuddy.matcher.ElementMatchers; import java.lang.instrument.Instrumentation; import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.named; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) throws Exception { new AgentBuilder.Default() .type(hasSuperType(named( "javax.servlet.Servlet" ))) .transform( new Transformer.ForAdvice() .include(MyAgent. class .getClassLoader()) .advice(ElementMatchers.named( "service" ), "io.promagent.MyAdvice" )) .installOn(inst); } |
В приведенном выше примере показан полный код, необходимый для инструментирования метода service()
всех реализаций javax.servlet.Servlet
. Метод service()
вызывается всякий раз, когда сервлет обрабатывает веб-запрос. Класс MyAdvice
определяет код, который будет MyAdvice
в метод service()
сервлета. Этот код аннотируется с помощью @Advice.OnMethodEnter
и @Advice.OnMethodExit
:
Byte Buddy Advice Пример
01
02
03
04
05
06
07
08
09
10
11
12
|
public class MyAdvice { @Advice .OnMethodEnter public static void before(ServletRequest request, ServletResponse response) { System.out.println( "before serving the request..." ); } @Advice .OnMethodExit public static void after(ServletRequest request, ServletResponse response) { System.out.println( "after serving the request..." ); } } |
Byte Buddy предлагает два способа инструментирования: советы (как показано выше) и перехватчики. Разница @Advice.OnMethodEnter
: в случае с советами байт-код методов @Advice.OnMethodEnter
и @Advice.OnMethodExit
копируется в начало и в блок finally перехваченного метода. Эффект такой же, как если бы вы копировали и вставляли код в реализацию service()
вы хотите перехватить. В результате класс MyAdvice
больше не используется после выполнения инструментария. Перехваченному методу service()
не требуется доступ к классу MyAdvice
, его можно выполнить в контексте загрузчика классов, где класс MyAdvice
недоступен.
Перехватчики, с другой стороны, являются обычными вызовами методов, которые выполняются в начале и в блоке finally перехваченных методов. Это означает, что перехваченный метод должен выполняться в контексте, где доступен класс перехватчика.
В следующих разделах мы увидим, что видимость классов в средах сервера приложений может быть ограничена, поэтому Promagent использует Advices вместо Interceptors.
3. Добавление зависимостей
Чтобы превратить приведенный выше пример во что-то полезное, нам нужно заменить сообщения System.out.println()
кодом, поддерживающим метрики и предоставляющим метрики для системы мониторинга. Например, Promagent использует клиентскую библиотеку Prometheus для поддержки и предоставления метрик Prometheus .
JVM автоматически добавляет файл JAR, указанный в -javaagent:
командной строки -javaagent:
к загрузчику системных классов приложения. Следовательно, теоретически должно быть возможно создать JAR — -javaagent:
Uber, содержащий агент и все его зависимости, и использовать его в -javaagent:
командной строки -javaagent:
.
Однако сделать все зависимости доступными для системного загрузчика классов проблематично в среде сервера приложений по двум причинам:
- Некоторые из зависимостей агента могут конфликтовать с библиотеками, используемыми внутри сервера приложений, или с библиотеками, поставляемыми в файле WAR как часть развернутого приложения.
- Чтобы предотвратить конфликты, серверы приложений ограничивают доступ к классам из системного загрузчика классов. Например, модули Wildfly не могут получить доступ к классам из системного загрузчика классов, если
jboss.modules.system.pkgs
пакет явно не открыт сjboss.modules.system.pkgs
системного свойстваjboss.modules.system.pkgs
. Нетривиально отслеживать все зависимости и настраивать систему модулей соответствующим образом.
Лучший подход состоит в том, чтобы представить только несколько классов Java без внешних зависимостей от загрузчика системных классов и загрузить фактическую реализацию метрик с помощью пользовательского загрузчика классов. Это минимизирует потенциальные конфликты и конфигурацию, необходимую для запуска агента.
4. Загрузка хуков из загрузчика пользовательских классов
Реализовать пользовательский загрузчик классов в Java легко, так как мы можем просто использовать java.net.URLClassLoader
и инициализировать его путем к файлу JAR, в котором находятся наши классы. Чтобы сделать агент простым в использовании, Promagent поставляется в виде файла JAR, содержащего другие файлы JAR. Внутренние JAR-файлы копируются во временный каталог при запуске, а пользовательский загрузчик классов настраивается с использованием временных путей. Таким образом, пользователь получает один агент JAR, а внутри агент различает классы в системном загрузчике классов (эти классы содержатся непосредственно в агенте JAR) и классы в пользовательском загрузчике классов (эти классы загружаются из JAR-файлов в временный каталог).
Фактическая аппаратура реализована в классе под названием hook . Хук загружается из пользовательского загрузчика классов. Таким образом, ловушка может ссылаться на любые необходимые ей зависимости, если загрузчик пользовательских классов может предоставить эти зависимости. В качестве примера, ServletHook
выглядит так:
Пример пользовательского класса Hook
01
02
03
04
05
06
07
08
09
10
|
public class ServletHook { public void before(ServletRequest request, ServletResponse response) { // ... } public void after(ServletRequest request, ServletResponse response) { // ... } } |
Крючок похож на совет Byte Buddy. Разница в том, что рекомендация Byte Buddy — это всего лишь несколько строк кода с минимальными зависимостями, необходимыми для загрузки соответствующего обработчика из загрузчика пользовательского класса и делегирования с помощью отражения в методы обработчика before()
и after()
. Рекомендация Byte Buddy не имеет никаких зависимостей от библиотеки инструментов, поскольку фактическая библиотека инструментов видна только в загрузчике пользовательских классов.
Однако при загрузке ловушки есть ServletRequest
ловушка: параметры ServletRequest
и ServletResponse
будут переданы из инструментированного Servlet
. Это означает, что ServletRequest
и ServletResponse
в ServletResponse
должны быть загружены тем же загрузчиком классов, что и перехваченный сервлет, иначе мы не сможем передать параметры сервлета в метод ловушки before()
и after()
.
Решением является использование Thread.currentThread().getContextClassLoader()
качестве родителя пользовательских загрузчиков классов. Таким образом, все классы, которые можно загрузить из загрузчика класса контекста, будут загружены из загрузчика класса контекста. Это включает в себя ServletRequest
и ServletResponse
. Только классы, которые не доступны в текущем контексте, такие как сам хук и его зависимости, будут загружены из пользовательских файлов JAR. Это означает, что нам нужен один пользовательский загрузчик классов на контекст, потому что каждый пользовательский загрузчик классов делегирует другому загрузчику класса контекста в качестве своего родителя.
5. Внедрение глобального реестра метрик
Используя описанную выше реализацию, можно использовать одно веб-приложение. Однако, если на сервере приложений имеется несколько развертываний, у каждого инструментария будет свой загрузчик классов. Когда библиотека метрик загружается из разных загрузчиков классов, развертывания не могут совместно использовать глобальные статические переменные, определенные в этой библиотеке метрик. Например, невозможно использовать глобальный реестр метрик, который поставляется с клиентской библиотекой Prometheus, в нескольких развертываниях. В отсутствие глобального реестра каждое развертывание должно поддерживать и предоставлять свои показатели независимо.
Один из способов решения этой проблемы — расширить загрузчик пользовательских классов и заставить его делегировать загрузку разделяемой библиотеки метрик в другой общий загрузчик пользовательских классов. Однако JVM также поставляется со встроенным глобальным реестром, который мы можем использовать в качестве хранилища метрик для всей виртуальной машины: сервер MBean платформы JMX. Регистрация метрик как MBeans имеет следующие преимущества:
- Глобальный реестр: сервер MBean платформы JMX предоставляет реестр для всей виртуальной машины, что позволяет нам поддерживать глобальный набор метрик для инструментирования всех развертываний на сервере приложений.
- Единый экспортер в систему мониторинга: Легко реализовать небольшое веб-приложение, которое считывает все метрики с сервера MBean и делает их доступными для системы мониторинга. Например, Promagent включает развертывание WAR для экспорта метрик на сервер Prometheus .
- Инструменты JMX: поскольку все метрики доступны в виде MBean-компонентов, любой клиент JMX может быть использован для изучения состояния метрик.
Сервер MBean платформы JMX является частью Java SE и доступен через статический метод ManagementFactory.getPlatformMBeanServer()
. Объекты Java, зарегистрированные на сервере MBean, называются MBean. MBeans должен определять свой общедоступный API в интерфейсе, который по соглашению именуется как класс Java с добавленным суффиксом MBean
. Например, чтобы зарегистрировать класс Counter
в качестве MBean, класс должен реализовать интерфейс с именем CounterMBean
. Каждый MBean адресуется через уникальное имя ObjectName
. Методы, определенные в интерфейсе MBean, можно вызывать с помощью MBeanServer.invoke()
.
6. Резюме
В этой статье дается обзор того, как использовать Java-приложения на инструментах без изменения их исходного кода. Он основан на Promagent , который оснащает Java-приложения метриками Prometheus . Тем не менее, статья была сфокусирована на базовых технологиях, таких как Java-агенты, библиотека манипулирования байт-кодом Byte Buddy , загрузка классов в среде сервера приложений, такой как Wildfly , и сервер MBean платформы JMX.
Есть несколько слабых сторон, которые лучше всего выбрать из примера кода Promagent, например, как избежать двойного подсчета HTTP-запросов, когда они проходят через цепочку из нескольких сервлетов. Для большего количества примеров, возможно, стоит взглянуть на связанные проекты, такие как inspectIT или stagemonitor .