JMSpy - шпигун за викликами методів

image
Здрастуй, Хабрахабр! Хочу розповісти про одну бібліотеку, яку я розробив у рамках мого минулого проекту і так вийшло, що вона потрапила в OpenSource.

Для початку скажу пару слів про те, чому з'явилася необхідність у даній бібліотеці. В рамках проекту доводилось працювати зі складною доменної bidirectional tree-like структурою, тобто за графу об'єктів можна ходити зверху вниз (від батьків до дитини) і навпаки. Тому об'єкти вийшли об'ємні. В якості сховища ми використовували MongoDB, а так як об'єкти були об'ємні, то деякі з них перевищували максимальний розмір MongoDB документа. Для того, щоб вирішити цю проблему ми рознесли композитний об'єкт з різних колекцій (хоча в MongoDB краще зберігати цільними документами). Таким чином, дочірні об'єкти зберігалися в окремі колекції, а документ, який був батьком, містив посилання на них. Використовуючи даний підхід ми реалізували механізм ледачою завантаження (lazy loading). Тобто рутовий об'єкт завантажувався не з усіма вкладеними об'єктами, а тільки з top-level, його дочірні елементи вантажилися на вимогу. Репозиторій, який віддавав основний об'єкт, використовувався в кастомних тегах (Java Custom Tag), а теги в свою чергу на FTL сторінках. В ході performance тестування ми помітили, що на сторінках відбувається багато lazy-load викликів. Почали переглядати сторінки і виявили неоптимальні виклики виду:

rootObject.getObjectA().getObjectB().getName()

getObjectA() призводить до завантаження об'єкта з іншої колекції, та ж ситуація і з getObjectB(). Але так як в rootObject є поле objectBName то рядок вище можна переписати наступним чином:

rootObject.getObjectBName()

такий підхід не призводить до завантаження дочірніх об'єктів і працює набагато швидше.

Постало питання: "Як знайти всі сторінки, де є такі неоптимальні виклики, і усунути їх?". Простим пошуком по коду це займало багато часу і ми вирішили реалізувати щось на зразок debug мода. Включаємо debug mode, запускаємо UI тести, а по закінченню отримуємо інформацію про те, які методи нашого парент об'єкта викликалися і де. Так і з'явилася ідея створення JMSpy.

Бібліотека доступна в maven central, таким чином все, що вам потрібно, це вказати залежність у вашому build tool.
Приклад для maven:

<dependency>
<groupId>com.github.dmgcodevil</groupId>
<artifactId>jmspy-core</artifactId>
<version>1.1.2</version>
</dependency>

jmspy-core — це модуль, який містить основні можливості бібліотеки. Так само є jmspy-agent і jmspy-ext-freemarker, але про це пізніше. JMspy дозволяє записувати дзвінки будь-якої вкладеності, наприклад:

object.getCollection().iterator().next().getProperty()

Для початку розглянемо основні компоненти бібліотеки та їх призначення.

MethodInvocationRecorder — це основний клас, з яким взаємодіє кінцевий користувач.
ProxyFactory — фекторі, який використовує cglib для створення проксі. ProxyFactory це сінглетон, що приймає Configuration як параметр, таким чином, можна налаштувати фекторі під свої потреби, про це нижче.
ContextExplorer — інтерфейс, який надає методи для отримання інформації про контексті виконання методу. Наприклад, jmspy-ext-freemarker — це реалізація ContextExplorer для того, щоб отримувати інформацію про сторінку, на якій зголосився метод об'єкта (bean'a або pojo, як вам зручніше)

ProxyFactory

Фекторі дозволяє створювати проксі для об'єктів. Є можливість для конфігурації фекторі, це може бути корисно в разі складних кейсів, хоча для простих об'єктів дефолтної конфігурації має вистачити. Для того, щоб створити екземпляр фекторі потрібно скористатися методом getInstance і передати туди інстанси конфигруции (Configiration), наприклад так:

Configuration.Builder builder = Configuration.builder()
.ignoreType(DataLoader.class) // objects with type DataLoader for which no proxy should be created
.ignoreType(java.util.logging.Logger.class) // ignore objects with type DataLoader
.ignorePackage("com.mongodb"); // ignore objects with types exist in specified package
ProxyFactory proxyFactory = ProxyFactory.getInstance(builder.build());

ContextExplorer

ContextExplorer це інтерфейс, реалізації якого повинні надавати інформацію про контексті виконання. Jmspy надає готову реалізацію для Freemarker (FreemarkerContextExplorer), яка поставляється окремим jar модулем jmspy-ext-freemarker. Ця реалізація надає інформацію про сторінки, адресу запиту і т. д. Ви можете створити свою реалізацію і зареєструвати її в MethodInvocationRecorder. Можна зареєструвати тільки одну реалізацію для MethodInvocationRecorder. Інтерфейс ContextExplorer містить два методу, нижче трохи про кожного з них.

getRootContextInfo — повертає основну інформацію про контексті виклику, таку, як рутовий метод, назва програми, інформацію про запит, url і т.д. Цей метод викликається відразу після того, як буде створено InvocationRecord, тобто відразу після виклику методу

MethodInvocationRecorder#record(java.lang.reflect.Method, Object)} 

або

MethodInvocationRecorder#record(Object)} 

getCurrentContextInfo — надає більш детальну інформацію, таку як ім'я сторінки FTL, JSP и т. д. Цей метод викликається в той час, коли який-небудь метод був викликаний на об'єкті, отриманому з MethodInvocationRecorder#record, наприклад:

User user = new User();
MethodInvocationRecorder methodInvocationRecorder = new MethodInvocationRecorder();
MethodInvocationRecorder.record(user).getName(); // в цей час буде викликаний getCurrentContextInfo()

MethodInvocationRecorder

Як ви вже здогадалися, це основний клас, з яким доведеться працювати. Його основною функцією є запуск процесу шпигунства за викликами методів. MethodInvocationRecorder надає конструктори, які можна передати примірник ProxyFactory і ContextExplorer.
У цьому класі є ще один важливий метод: makeSnapshot(). Цей метод зберігає поточний граф викликів для подальшого аналізу за допомогою jmspy-viewer.

Обмеження
Так як бібліотека використовує CGLIB для створення проксі, вона має ряд обмежень, які випливають із природи CGLIB. Відомо, що CGLIB використовує спадкування і може створювати проксі для типів, які не реалізують ніяких інтерфейсів. Тобто CGLIB успадковує згенерований проксі клас від цільового типу об'єкта, для якого створюється проксі. Java має ряд деяких обмежень надаються до механізму спадкування, а саме:

1. CGLIB не може створити проксі для final класів, так як фінальні класи не можуть успадковуватися;
2. final методи не можуть бути перехоплені, так як наслідуваний клас не може перевизначити фінальний метод.
Для того що б обійти ці обмеження можна скористатися двома підходами:

1. Створити wrapper для класу (працює тільки в тому випадку, якщо ваш клас реалізує деякий інтерфейс, з яким ви працюєте)
Приклад:

Інтерфейс
public interface IFinalClass {
String getId();
}

Клас:
public final class FinalClass implements IFinalClass {

private String id;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}

Створюємо wrapper
public class FinalClassWrapper implements IFinalClass, Wrapper<IFinalClass> {

private IFinalClass target;

public FinalClassWrapper() {
}

public FinalClassWrapper(IFinalClass target) {
this.target = target;
}

@Override
public Wrapper create(IFinalClass target) {
return new FinalClassWrapper(target);
}

@Override
public void setTarget(IFinalClass target) {
this.target = target;
}

@Override
public IFinalClass getTarget() {
return target;
}

@Override
public Class<? extends Wrapper<IFinalClass>> getType() {
return FinalClassWrapper.class;
}

@Override
public String getId(){
return target.getId();
}
}


Тепер потрібно зареєструвати врапперов FinalClassWrapper використовуючи метод registerWrapper.

public static void main(String[] args) {
Configuration conf = Configuration.builder()
.registerWrapper(FinalClass.class, new FinalClassWrapper()) //register our wrapper
.build();
ProxyFactory proxyFactory = ProxyFactory.getInstance(conf);
MethodInvocationRecorder invocationRecorder = new MethodInvocationRecorder(proxyFactory);
IFinalClass finalClass = new FinalClass(); 
IFinalClass proxy = invocationRecorder.record(finalClass); 
System.out.println(isCglibProxy(proxy));
}

2. Використовувати jmspy-agent.

Jmspy-agent — це простий java agent. Для того, що б використовувати агент, його треба вказати в рядку запуску програми використовуючи параметр-javaagent, наприклад:

-javaagent:{path_to_jar}/jmspy-agent-x.y.z.jar=[параметр]

В якості параметра задається список класів або пакетів, які потрібно инструментировать. Jmspy-agent змінить класи якщо потрібно: прибере final модифікатори з типів і методів, таким чином зможе створювати проксі без проблем.

JMSpy Viewer Вьювер для перегляду і аналізу jmspy снэпшотов.
UI не багатий, але його цілком достатньо для того, що б отримати необхідну інформацію, правда, поки що є тільки збірка для windows. Нижче наведено скріншот основного вікна:

image

Документація по вьюверу ще в процесі, але ui простий і інтуїтивно зрозумілий.

Буду радий, якщо це стаття і сама бібліотека виявляться корисними. Хотілося б почути ваші коментарі, що б зрозуміти, чи варто покращувати і розвивати бібліотеку далі.

Проект на github.

Дякую за увагу.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.