Java Agent на службе JVM

в 9:17, , рубрики: java, Программирование, разработка

Java Agent на службе JVM
Наверное многие слышали или сталкивались с таким параметром JVM как -javaagent, увидеть этот параметр вы могли используя Jrebel или Plumbr это могло выглядеть например так JAVA_OPTS=-javaagent:[path/to/]jrebel.jar или так -javaagent:/path-to/plumbr.jar
Хотя javaagent появился еще в версии java 1.5, многие разработчики так никогда и не использовали возможности агентов и имеют смутное представление что это такое.
Что же это за агент? Зачем он может нам понадобиться и как написать свой?

Что такое javaagent

Как я написал выше javaagent это один из параметров JVM, который позволяет указать агент который будет запущен с вашим приложением, а точнее он будет запущен еще перед запуском вашего приложения. Сам агент это отдельное приложение которое предоставляет доступ к механизму манипуляции байт-кодом (java.lang.instrument) в runtime. Это если вкратце. Официальную документацию можно почитать тут, но она довольно скудная. Ничего непонятно? Итак, давайте разбираться. Лучше всего разбираться на примерах.

Напишем элементарный агент

package ru.habrahabr.agent;

public class Agent007 {
    public static void premain(String args) {
        System.out.println("Hello! I`m java agent");        
    }
}

Обратите внимание, агент обязательно должен реализовывать метод premain со следующей сигнатурой
public static void premain(String args);
или
public static void premain(String args, Instrumentation inst);

Класс агента должен быть упакован в jar и содержать MANIFEST.MF, с обязательным атрибутом
PreMain-Class — указывает на класс агента с premain методом. Есть и другие атрибуты агента, но они необязательные и сейчас нам не понадобятся.

Вот так будет выглядеть наш manifest.mf.

Manifest-Version: 1.0
PreMain-Class: ru.habrahabr.agent.Agent007

не забудьте добавить перевод строки в конец файла

Теперь упакуем все это в jar

jar -cvfm Agent007.jar manifest.mf  ru/habrahabr/agent/Agent007.class

И наконец класс испытатель

package ru.habrahabr.agent;

public class AgentTester {
	public static void main(String[] args) {
		System.out.println("Hello! I`m agent tester");
	}
}

Запускаем AgentTester из командной строки

java -javaagent:Agent007.jar ru.habrahabr.agent.AgentTester
Hello! I`m java agent
Hello! I`m agent tester

Из этого примера видно что:

  • метод premain исполняется еще до вызова метода main основного приложения.
  • агент указывается с помощью параметра -javaagent:jarpath[=options]

Давайте попробуем извлечь из агента какую-нибудь пользу

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

Напишем AgentCounter который будет выводить имя загружаемого класс и подсчитывать кол-во загруженных классов. Так мы сможем понаблюдать за работой classloader`a.

package ru.habrahabr.agent;

import java.lang.instrument.Instrumentation;

public class AgentCounter {
	public static void premain(String agentArgument, Instrumentation instrumentation) {
		System.out.println("Agent Counter");
		instrumentation.addTransformer(new ClassTransformer());
	}
}

Обратите внимание, теперь я использую другую сигнатуру метода premain. В объект instrumentation я передаю ClassTransformer который и выполняет всю работу. ClassTransformer будет срабатывать каждый раз при загрузке класса. Если вы хотите использовать свой ClassTransformer, вы должны реализовать интерфейс java.lang.instrument.ClassFileTransformer и добавить свой объект через метод Instrumentation.addTransformer

package ru.habrahabr.agent;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ClassTransformer implements ClassFileTransformer {
	
	private static int count = 0;
	
	@Override
	public byte[] transform(ClassLoader loader, 
							String className,
							Class<?> classBeingRedefined, 
							ProtectionDomain protectionDomain,
							byte[] classfileBuffer) {
		System.out.println("load class: " + className.replaceAll("/", "."));
		System.out.println(String.format("loaded %s classes", ++count));
		return classfileBuffer;
	}
}

classfileBuffer — это и есть байт-код текущего класса представленный в виде массива байт, для его переопределения трансформер должен вернуть новый массив байт, в данном примере мы не меняем содержимое класса поэтому просто возвращаем тот же массив.

Пакуем агент и трансформер в новый jar

jar -cvfm agentCounter.jar manifest.mf  ru/habrahabr/agent/AgentCounter.class ru/habrahabr/agent/ClassTransformer.class

Немного модифицируем класс тестер

package ru.habrahabr.agent;

public class AgentTester {
	public static void main(String[] args) {
		A a = new A();
		B b = new B();			
		C c = null;
	}
}

class A {};
class B {};
class C {};

Запускаем AgentTester c новым агентом

java -javaagent:agentCounter.jar ru.habrahabr.agent.AgentTester
Agent Counter
load class: sun.launcher.LauncherHelper
loaded 1 classes
load class: ru.habrahabr.agent.AgentTester
loaded 2 classes
load class: ru.habrahabr.agent.A
loaded 3 classes
load class: ru.habrahabr.agent.B
loaded 4 classes

для разных версий java результаты могут отличаться

Если запустить какое-нибудь enterprise приложение с таким агентом, можно получить довольно интересные результаты, например один из проектов после старта выдал мне следующее:

sun.reflect.GeneratedMethodAccessor230
loaded 33597 classes
java.rmi.server.Unreferenced
loaded 33598 classes

Измеряем размер java объектов

Рассмотрим еще один пример использования агентов. Напишем класс который будет возвращать размер java объектов и javaagent будет играть ключевую роль. Кто как ни JVM может знать реальный размер созданного объекта, в интерфейсе Instrumentation есть замечательный метод long getObjectSize(Object objectToSize) который возвращает размер объекта. Но как из нашего приложения получить доступ к агенту? А делать ничего особенного и не придется, javaagent автоматически добавляется в classpath и нам остается только добавить в агент поле типа Instrumentation instrumentation и инициализировать его в методе premain.

package ru.habrahabr.agent;

import java.lang.instrument.Instrumentation;

public class AgentMemoryCounter {
	
	private static Instrumentation instrumentation;
	
	public static void premain(String args, Instrumentation instrumentation) {
		AgentMemoryCounter.instrumentation = instrumentation;
	}
	
	public static long getSize(Object obj) {
		if (instrumentation == null) {
			throw new IllegalStateException("Agent not initialised");
		}
		return instrumentation.getObjectSize(obj);
	}
}

Мы получаем доступ к методу AgentMemoryCounter.getSize(obj) из класса приложения.

package ru.habrahabr.agent;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;

public class AgentTester {
	public static void main(String[] args) {
		printObjectSize(new Object());
		printObjectSize(new A());
		printObjectSize(1);
		printObjectSize("string");
		printObjectSize(Calendar.getInstance());
		printObjectSize(new BigDecimal("999999999999999.999"));
		printObjectSize(new ArrayList<String>());
		printObjectSize(new Integer[100]);
	}

	public static void printObjectSize(Object obj) {
		System.out.println(String.format("%s, size=%s", obj.getClass()
				.getSimpleName(), AgentMemoryCounter.getSize(obj)));
	}
}

class A {
	Integer id;
	String name;
}

Результаты работы приложения могу выглядеть следующим образом

java -javaagent:agentMemoryCounter.jar ru.habrahabr.agent.AgentTester
Agent Counter
Object, size=8
A, size=16
Integer, size=16
String, size=24
GregorianCalendar, size=112
BigDecimal, size=32
ArrayList, size=24
Integer[], size=416

Обратите внимание что метод getObjectSize() не учитывает размер вложенных объектов т.е учитывается только память затраченная на ссылку на объект.

Заключение

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

Автор: ilinchik

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js